Resolve addressed organization for AR per-user token exchange
What does this MR do and why?
ArtifactRegistry::TokenExchange#mint_token minted the per-user JWT for the caller's home organization (current_user.organization) instead of the organization the request addresses.
Accounts stay homed in the Default organization while customer groups are promoted to their own organizations. A user who is a member of promoted organization B but homed in organization A therefore presented a token naming A. IAM keys the principal on (origin, origin_id = org.uuid, local_id), so the token resolved to the wrong principal and saw none of the caller's roles — enforced routes (repository create, single-repository read) answered a masked 404. List still works only because its enforcement has not landed yet.
This MR mints the per-user JWT for the organization the request addresses. CachesClient passes the organization it is included on into ArtifactRegistry::Client, which passes it to TokenExchange#token_for. Minting requires User#member_of_organization? to return true. When it returns false, token_for returns no credential, and the client fails closed. This seam does not infer the organization.
The REST endpoint (ee/lib/api/authn/token_exchange.rb) still infers the organization through ::Authn::TokenExchange::OrganizationResolver, because a package client cannot name an organization. This MR does not change Authn::TokenExchange::TokenIssuer. The issuer-contract change is the deferred group::authentication long-term work in gitlab-org/ops/artifact-registry#977. It sends an explicit organization identifier through glab, and it stays out of scope here.
References
- Closes #626558 (closed)
- Driving discussion / scope decision: gitlab-org/ops/artifact-registry#977
- Same defect one layer down: gitlab-org/ops/artifact-registry#473
- Contrast (REST endpoint, infers because a package client cannot name an organization):
ee/lib/api/authn/token_exchange.rb
Screenshots or screen recordings
Not applicable — this is a backend credential-minting change with no UI surface.
| Before | After |
|---|---|
| n/a | n/a |
How to set up and validate locally
The change is on an in-process path with no UI, so validation mints a token and inspects the JWT it produces. The snippet builds a user homed in org A but a member of a promoted AR org B, mints the per-user AR token through the seam, and asserts the JWT names B.
-
Save the validation script:
cat > /tmp/validate_626558.rb <<'RUBY' # Builds a user homed in org A but a member of a promoted AR org B, mints the # per-user AR token through the seam, and asserts the JWT names B (not A). require 'jwt' ActiveRecord::Base.transaction do key = FactoryBot.create(:cloud_connector_keys) home_org = FactoryBot.create(:organization, name: "home-#{SecureRandom.hex(4)}") promoted_org = FactoryBot.create(:organization, name: "promoted-#{SecureRandom.hex(4)}") user = FactoryBot.create(:user, organization: home_org) # Membership in the promoted org is what the seam checks before minting. FactoryBot.create(:organization_user, organization: promoted_org, user: user) token = ArtifactRegistry::TokenExchange.new.token_for(user, promoted_org) raise 'no token minted' if token.blank? payload = JWT.decode(token, key.public_key, true, algorithm: 'RS256').first origin = payload.dig('gitlab', 'origin_id') puts "home org uuid: #{home_org.uuid}" puts "promoted org uuid: #{promoted_org.uuid}" puts "token origin_id: #{origin}" if origin == promoted_org.uuid puts 'PASS: token names the promoted (addressed) organization' elsif origin == home_org.uuid abort 'FAIL: token still names the home organization (bug present)' else abort "FAIL: token names an unexpected organization: #{origin}" end raise ActiveRecord::Rollback end RUBY -
Run it against the test environment (the script uses
FactoryBot, and the test DB carries the current schema; it rolls back its own records):bundle exec rails runner /tmp/validate_626558.rb -e test -
Expect the token's
origin_idto equal the promoted org uuid, and the script to printPASS:home org uuid: 01a0634f-9528-7ad4-b904-e20dc350ffd1 promoted org uuid: 01a0634f-95c3-732f-9364-1b7581fd6939 token origin_id: 01a0634f-95c3-732f-9364-1b7581fd6939 PASS: token names the promoted (addressed) organizationOn
master(before the fix) the same script printsFAIL: token still names the home organization.
Automated coverage (also runnable locally):
bundle exec rspec ee/spec/lib/artifact_registry/token_exchange_spec.rb ee/spec/models/concerns/artifact_registry/caches_client_spec.rb
bundle exec rubocop ee/lib/artifact_registry/token_exchange.rb ee/spec/lib/artifact_registry/token_exchange_spec.rbThe spec covers: member of the addressed organization → token names it; not a member → no credential; member of several AR orgs → token still names the addressed organization; memo keyed by user and organization; no organization passed → no credential.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.