Accept StatelessAccessToken JWTs on the policy store API
What does this MR do and why?
It adds allow_access_with_scope :read_security_policies to API::Govern::Policies, so a token scoped to read_security_policies can authenticate GET /organizations/:id/security/policy_store without the broader read_api scope. The gate itself is unchanged: this only narrows which scope a credential needs.
That scope is reached through Authn::Tokens::StatelessAccessToken, GitLab's existing short-lived JWT already used by Duo Workflow, not a new token class. Gitlab::Auth::StatelessAccessTokenGate is generalized to resolve its feature flag from the token's scope instead of a hardcoded Duo flag, so a read_security_policies-scoped token checks security_policy_store_stateless_access_token_auth while Duo Workflow's tokens keep checking duo_workflow_use_token_issuer unchanged.
Design decisions
This supersedes !251202 (closed), which built the same policy-store authentication on Authn::TokenExchange::TokenIssuer and POST /api/v4/token_exchange, the pattern built for Artifact Registry. In review there, alipniagov and daniele-gitlab pointed out that StatelessAccessToken is the better fit here: Rails is the only service that verifies this token (GLAZ forwards it to the policy store unchecked), and StatelessAccessToken already reads scopes from gitlab.scopes, denylists revoked JTIs, and uses a glsat- prefix the auth finder already dispatches on, so none of the token_exchange endpoint, its job-token/organization rules, or a bespoke verifier are needed.
The admin bot cannot use this yet. GlobalPolicy denies access_api/access_git to internal users, which the admin bot is, so a request authenticated this way currently 403s before reaching the policy check. That exception, together with checking the token's gitlab.origin_id claim against the requested organization, needs its own MR and an AppSec review.
StatelessAccessTokenGate.enabled? also now rejects any StatelessAccessToken that carries neither read_security_policies nor a Duo Workflow scope, closing a gap where such a token previously passed whenever duo_workflow_use_token_issuer was enabled. None of the three Duo Workflow issuer call sites (CreateOauthAccessTokenService, CreateCompositeOauthAccessTokenService, Ai::FlowTriggers::RunService) pass custom scopes today, so none are affected.
How to set up and validate locally
Requires an Ultimate licence.
- Enable the feature flags on the rails console
Feature.enable(:security_policies_v2)
Feature.enable(:security_policy_store_stateless_access_token_auth)- As an administrator, go to Admin > Settings > Security and compliance and turn on the policy store experiment
- Check whether a CloudConnector signing key already exists, and create one if not
bundle exec rake cloud_connector:keys:listbundle exec rake cloud_connector:keys:create- Make yourself an organization owner, on the rails console
organization = Organizations::Organization.find(1)user = User.find_by_username('root')Organizations::OrganizationUser.find_or_create_by!(organization: organization, user: user) do |organization_user|
organization_user.access_level = Gitlab::Access::OWNER
end- Seed a policy, on the rails console
Gitlab::PolicyStore.create(
organization_id: organization.id,
name: 'Block deployments on critical findings',
trigger_type: 'deployment_requested',
rules: [{ type: 'custom', value: "package governance\n\nimport rego.v1\n\ndefault allow := true\n" }]
)- Mint a
StatelessAccessTokenscoped to the policy store, on the rails console
token = Authn::Tokens::StatelessAccessToken.issue(
user: user, organization: organization, ttl: 1.hour.to_i, scopes: ['read_security_policies']
).plaintext_token- Call the policy store list endpoint with the token
curl --header "Authorization: Bearer <token from step 6>" \
--url "<your GDK URL>/api/v4/organizations/1/security/policy_store"- Verify the response carries the seeded policy
- Disable
security_policy_store_stateless_access_token_authand repeat step 7: verify it now fails with401, because there is no personal access token in the request to fall back to - Try the same token against an unrelated endpoint, for example
curl --header "Authorization: Bearer <token>" --url "<your GDK URL>/api/v4/user": verify it fails with403 insufficient_scope, because the token only carriesread_security_policies
References
- Part of https://gitlab.com/gitlab-org/gitlab/-/work_items/606971
- Supersedes !251202 (closed) (open)
- Relates to https://gitlab.com/gitlab-org/gitlab/-/work_items/633577 (open, the caller-identity spike)
- Relates to https://gitlab.com/gitlab-org/gitlab/-/work_items/626823 (open, the deferred CD service account question)