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.

  1. 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
  2. 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
  3. Expect the token's origin_id to equal the promoted org uuid, and the script to print PASS:

    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) organization

    On master (before the fix) the same script prints FAIL: 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.rb

The 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.

Edited by Rahul Chanila

Merge request reports

Loading
Loading