From 7e2c2ef85d41501377a11216d81b089b2e61c5b2 Mon Sep 17 00:00:00 2001 From: James Healy Date: Thu, 30 Jan 2025 16:57:02 +1100 Subject: [PATCH] Add integration test for assuming an API Key Role using a Buildkite OIDC token Until recently, Buildkite OIDC tokens did not contain a `jti` claim. At some point in early 2024 it was possible to assume an API Key Role using Buildkite OIDC tokens, but when testing in January 2025 we found the assume role request was failing with an error: > Missing/invalid jti Buildkite has addressed that by adding a `jti` claim to tokens - it's a good claim to include. However, to reduce the risk of regressions in the future, this proposes adding an integration test with a Buildkite-shaped OIDC token. The trait added to the OIDC::Provider factory is based on a real token that I generated then anonymized. I only test the happy path with this token - there's a buncha existing tests for various unhappy paths (expired token, etc) using the Github Actions shaped OIDC token and there's little value in replicating them. Most of the added test is copy-pasted from the happy-path Github Actions test further up the file. Fixes #5412 --- test/factories/oidc/api_key_role.rb | 18 ++++++ test/factories/oidc/provider.rb | 51 +++++++++++++++++ .../api/v1/oidc/api_key_roles_test.rb | 56 +++++++++++++++++++ 3 files changed, 125 insertions(+) diff --git a/test/factories/oidc/api_key_role.rb b/test/factories/oidc/api_key_role.rb index 97d9615ddfa..95e68c449cd 100644 --- a/test/factories/oidc/api_key_role.rb +++ b/test/factories/oidc/api_key_role.rb @@ -19,5 +19,23 @@ ] } end + + trait :buildkite do + provider factory: :oidc_provider_buildkite + sequence(:name) { |n| "Buildkite Pusher #{n}" } + access_policy do + { + statements: [ + { effect: "allow", + principal: { oidc: provider.issuer }, + conditions: [ + { operator: "string_equals", claim: "organization_slug", value: "example-org" } + ] } + ] + } + end + end + + factory :oidc_api_key_role_buildkite, traits: [:buildkite] end end diff --git a/test/factories/oidc/provider.rb b/test/factories/oidc/provider.rb index f745ff2cc63..03ee6ffa844 100644 --- a/test/factories/oidc/provider.rb +++ b/test/factories/oidc/provider.rb @@ -63,5 +63,56 @@ transient do pkey { OpenSSL::PKey::RSA.generate(2048) } end + + trait :buildkite do + sequence(:issuer) { |n| "https://#{n}.agent.buildkite.com" } + configuration do + { + issuer: issuer, + jwks_uri: "#{issuer}/.well-known/jwks", + id_token_signing_alg_values_supported: [ + "RS256" + ], + response_types_supported: [ + "id_token" + ], + scopes_supported: [ + "openid" + ], + subject_types_supported: %w[ + public + pairwise + ], + claims_supported: %w[ + sub + aud + exp + iat + iss + nbf + jti + organization_id + organization_slug + pipeline_id + pipeline_slug + build_number + build_branch + build_tag + build_commit + build_source + step_key + job_id + agent_id + cluster_id + cluster_name + queue_id + queue_key + runner_environment + ] + } + end + end + + factory :oidc_provider_buildkite, traits: [:buildkite] end end diff --git a/test/integration/api/v1/oidc/api_key_roles_test.rb b/test/integration/api/v1/oidc/api_key_roles_test.rb index b2f9a08e2b5..035097cc3e9 100644 --- a/test/integration/api/v1/oidc/api_key_roles_test.rb +++ b/test/integration/api/v1/oidc/api_key_roles_test.rb @@ -383,5 +383,61 @@ def jwt(claims = @claims, key: @pkey) ) end end + + context "with a Buildkite OIDC token" do + setup do + @role = create(:oidc_api_key_role_buildkite, provider: build(:oidc_provider_buildkite, issuer: "https://agent.buildkite.com", pkey: @pkey)) + @user = @role.user + + @claims = { + "aud" => "rubygems.org", + "exp" => 1_680_020_837, + "iat" => 1_680_020_537, + "iss" => "https://agent.buildkite.com", + "jti" => "0194b014-8517-7cef-b232-76a827315f08", + "nbf" => 1_680_019_937, + "sub" => "organization:example-org:pipeline:example-pipeline:ref:refs/heads/main:commit:b5ffe3aeea51cec6c41aef16e45ee6bce47d8810:step:", + "organization_slug" => "example-org", + "pipeline_slug" => "example-pipeline", + "build_number" => 5, + "build_branch" => "main", + "build_tag" => nil, + "build_commit" => "b5ffe3aeea51cec6c41aef16e45ee6bce47d8810", + "step_key" => nil, + "job_id" => "01945ecf-80f0-41e8-9b83-a2970a9305a1", + "agent_id" => "01945ecf-8bcf-40a6-9d70-a765db9a0928", + "build_source" => "ui", + "runner_environment" => "buildkite-hosted" + } + + travel_to Time.zone.at(1_680_020_830) # after the JWT iat, before the exp + end + + context "with matching conditions" do + should "return API key" do + post assume_role_api_v1_oidc_api_key_role_path(@role.token), + params: { + jwt: jwt.to_s + }, + headers: {} + + assert_response :created + + resp = response.parsed_body + + assert_match(/^rubygems_/, resp["rubygems_api_key"]) + assert_equal_hash( + { "rubygems_api_key" => resp["rubygems_api_key"], + "name" => "#{@role.name}-0194b014-8517-7cef-b232-76a827315f08", + "scopes" => ["push_rubygem"], + "expires_at" => 30.minutes.from_now }, + resp + ) + hashed_key = @user.api_keys.sole.hashed_key + + assert_equal hashed_key, Digest::SHA256.hexdigest(resp["rubygems_api_key"]) + end + end + end end end