diff --git a/Gemfile b/Gemfile index bdf30c699335394100a457b16b1c8d891c91b303..97e2c6f602120c61820119f10e298c6a333eeae1 100644 --- a/Gemfile +++ b/Gemfile @@ -743,3 +743,5 @@ gem 'openbao_client', path: 'gems/openbao_client' # rubocop:todo Gemfile/Missing gem 'paper_trail', '~> 15.0' # rubocop:todo Gemfile/MissingFeatureCategory gem "i18n_data", "~> 0.13.1", feature_category: :system_access + +gem "gitlab-cloud-connector", "~> 0.2.1", require: 'cloud_connector', feature_category: :cloud_connector diff --git a/Gemfile.checksum b/Gemfile.checksum index 9b6d34d1c5fb48156eda5661d7bbcf59443810d4..2ad02e36715c9bbc30529db75d7a629dcea9639b 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -220,6 +220,7 @@ {"name":"gitaly","version":"17.5.0.pre.rc42","platform":"ruby","checksum":"15469230245c5d83f09c6e057ae1088ce87133ff156086bf02a2b8b2ec24e817"}, {"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"}, {"name":"gitlab-chronic","version":"0.10.5","platform":"ruby","checksum":"f80f18dc699b708870a80685243331290bc10cfeedb6b99c92219722f729c875"}, +{"name":"gitlab-cloud-connector","version":"0.2.1","platform":"ruby","checksum":"552d760ee2a9d25f681c9b2cf677e9f1b3c65f7516b6348e21b3bdf970640db4"}, {"name":"gitlab-dangerfiles","version":"4.8.0","platform":"ruby","checksum":"b327d079552ec974a63bf34d749a0308425af6ebf51d01064f1a6ff216a523db"}, {"name":"gitlab-experiment","version":"0.9.1","platform":"ruby","checksum":"f230ee742154805a755d5f2539dc44d93cdff08c5bbbb7656018d61f93d01f48"}, {"name":"gitlab-fog-azure-rm","version":"2.2.0","platform":"ruby","checksum":"31aa7c2170f57874053144e7f716ec9e15f32e71ffbd2c56753dce46e2e78ba9"}, diff --git a/Gemfile.lock b/Gemfile.lock index 954dcdc8c00db2a39f1a65ac9332455d93d0a57d..b8ca460ec714ede060de1455ac84fa4f6bd0f945 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -729,6 +729,9 @@ GEM terminal-table (>= 1.5.1) gitlab-chronic (0.10.5) numerizer (~> 0.2) + gitlab-cloud-connector (0.2.1) + activesupport (~> 7.0) + jwt (~> 2.9.3) gitlab-dangerfiles (4.8.0) danger (>= 9.3.0) danger-gitlab (>= 8.0.0) @@ -2067,6 +2070,7 @@ DEPENDENCIES gitaly (~> 17.5.0.pre.rc1) gitlab-backup-cli! gitlab-chronic (~> 0.10.5) + gitlab-cloud-connector (~> 0.2.1) gitlab-dangerfiles (~> 4.8.0) gitlab-duo-workflow-service-client (~> 0.1)! gitlab-experiment (~> 0.9.1) diff --git a/Gemfile.next.checksum b/Gemfile.next.checksum index da5add729d22e002f30ea4556ac11a81a3d2b36f..fd241825a760cd123352f8a18bea8982a51d2226 100644 --- a/Gemfile.next.checksum +++ b/Gemfile.next.checksum @@ -221,6 +221,7 @@ {"name":"gitaly","version":"17.5.0.pre.rc42","platform":"ruby","checksum":"15469230245c5d83f09c6e057ae1088ce87133ff156086bf02a2b8b2ec24e817"}, {"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"}, {"name":"gitlab-chronic","version":"0.10.5","platform":"ruby","checksum":"f80f18dc699b708870a80685243331290bc10cfeedb6b99c92219722f729c875"}, +{"name":"gitlab-cloud-connector","version":"0.2.1","platform":"ruby","checksum":"552d760ee2a9d25f681c9b2cf677e9f1b3c65f7516b6348e21b3bdf970640db4"}, {"name":"gitlab-dangerfiles","version":"4.8.0","platform":"ruby","checksum":"b327d079552ec974a63bf34d749a0308425af6ebf51d01064f1a6ff216a523db"}, {"name":"gitlab-experiment","version":"0.9.1","platform":"ruby","checksum":"f230ee742154805a755d5f2539dc44d93cdff08c5bbbb7656018d61f93d01f48"}, {"name":"gitlab-fog-azure-rm","version":"2.2.0","platform":"ruby","checksum":"31aa7c2170f57874053144e7f716ec9e15f32e71ffbd2c56753dce46e2e78ba9"}, diff --git a/Gemfile.next.lock b/Gemfile.next.lock index bd7ec9b03a11f450f9b4ef3a0f8f43da8b77e063..ea2dcfc0d34bf1a96792ee6df7ccf7ba490cc6f7 100644 --- a/Gemfile.next.lock +++ b/Gemfile.next.lock @@ -739,6 +739,9 @@ GEM terminal-table (>= 1.5.1) gitlab-chronic (0.10.5) numerizer (~> 0.2) + gitlab-cloud-connector (0.2.1) + activesupport (~> 7.0) + jwt (~> 2.9.3) gitlab-dangerfiles (4.8.0) danger (>= 9.3.0) danger-gitlab (>= 8.0.0) @@ -2094,6 +2097,7 @@ DEPENDENCIES gitaly (~> 17.5.0.pre.rc1) gitlab-backup-cli! gitlab-chronic (~> 0.10.5) + gitlab-cloud-connector (~> 0.2.1) gitlab-dangerfiles (~> 4.8.0) gitlab-duo-workflow-service-client (~> 0.1)! gitlab-experiment (~> 0.9.1) diff --git a/ee/lib/cloud_connector/self_signed/available_service_data.rb b/ee/lib/cloud_connector/self_signed/available_service_data.rb index af8c5eb1199ddfc4d4f77eab177befb174b4f71b..2edb86474181a83b25492e98636b1264e52f455a 100644 --- a/ee/lib/cloud_connector/self_signed/available_service_data.rb +++ b/ee/lib/cloud_connector/self_signed/available_service_data.rb @@ -18,7 +18,7 @@ def initialize(name, cut_off_date, bundled_with, backend) override :access_token def access_token(resource = nil, extra_claims: {}) if Feature.enabled?(:cloud_connector_jwt_replace, gitlab_org_group) - ::Gitlab::CloudConnector::JSONWebToken.new( + ::Gitlab::CloudConnector::JsonWebToken.new( issuer: Doorkeeper::OpenidConnect.configuration.issuer, audience: backend, subject: Gitlab::CurrentSettings.uuid, diff --git a/ee/lib/gitlab/cloud_connector/json_web_token.rb b/ee/lib/gitlab/cloud_connector/json_web_token.rb deleted file mode 100644 index 13401ef8cc55392120c763eebf92cc954ef8c7b2..0000000000000000000000000000000000000000 --- a/ee/lib/gitlab/cloud_connector/json_web_token.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module CloudConnector - class JSONWebToken - SIGNING_ALGORITHM = 'RS256' - NOT_BEFORE_TIME = 5.seconds.to_i - - attr_reader :issued_at, :expires_at - - def initialize(issuer:, audience:, subject:, realm:, scopes:, ttl:, extra_claims: {}) - @id = SecureRandom.uuid - @audience = audience - @subject = subject - @issuer = issuer - @issued_at = Time.current.to_i - @not_before = @issued_at - NOT_BEFORE_TIME - @expires_at = (@issued_at + ttl).to_i - @realm = realm - @scopes = scopes - @extra_claims = extra_claims - end - - # jwk: - # The key (pair) as an instance of JWT::JWK. - # - # Returns a signed and Base64-encoded JSON Web Token string, to be - # written to the HTTP Authorization header field. - def encode(jwk) - header_fields = { typ: 'JWT', kid: jwk.kid } - - JWT.encode(payload, jwk.signing_key, SIGNING_ALGORITHM, header_fields) - end - - def payload - { - jti: @id, - aud: @audience, - sub: @subject, - iss: @issuer, - iat: @issued_at, - nbf: @not_before, - exp: @expires_at - }.merge(cloud_connector_claims) - end - - private - - def cloud_connector_claims - { - gitlab_realm: @realm, - scopes: @scopes - }.merge(@extra_claims) - end - end - end -end diff --git a/ee/spec/lib/cloud_connector/self_signed/available_service_data_spec.rb b/ee/spec/lib/cloud_connector/self_signed/available_service_data_spec.rb index 70c3ec8c0d0439a7e10add9f9fa727c24549cadb..168248dd02db13ba506d961513c301b2cb63abbe 100644 --- a/ee/spec/lib/cloud_connector/self_signed/available_service_data_spec.rb +++ b/ee/spec/lib/cloud_connector/self_signed/available_service_data_spec.rb @@ -34,7 +34,7 @@ shared_examples 'issue a token with scopes' do let(:expected_token) do - instance_double('Gitlab::CloudConnector::JSONWebToken') + instance_double('Gitlab::CloudConnector::JsonWebToken') end before do @@ -44,7 +44,7 @@ end it 'returns the encoded token' do - expect(Gitlab::CloudConnector::JSONWebToken).to receive(:new).with( + expect(Gitlab::CloudConnector::JsonWebToken).to receive(:new).with( issuer: issuer, audience: backend, subject: instance_id, diff --git a/ee/spec/lib/gitlab/cloud_connector/json_web_token_spec.rb b/ee/spec/lib/gitlab/cloud_connector/json_web_token_spec.rb deleted file mode 100644 index a3ede2a75f5b66d75bf8b87db6320b06ff74c088..0000000000000000000000000000000000000000 --- a/ee/spec/lib/gitlab/cloud_connector/json_web_token_spec.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::CloudConnector::JSONWebToken, feature_category: :cloud_connector do - let(:extra_claims) { {} } - - let(:expected_issuer) { 'gitlab.com' } - let(:expected_audience) { 'gitlab-ai-gateway' } - let(:expected_subject) { 'ABC-123' } - let(:expected_realm) { 'saas' } - let(:expected_scopes) { [:code_suggestions] } - let(:expected_ttl) { 10.minutes } - - subject(:token) do - described_class.new( - issuer: expected_issuer, - audience: expected_audience, - subject: expected_subject, - realm: expected_realm, - scopes: expected_scopes, - ttl: expected_ttl, - extra_claims: extra_claims - ) - end - - describe '#payload' do - subject(:payload) { token.payload } - - it 'has expected values', :freeze_time, :aggregate_failures do - now = Time.current.to_i - - # standard claims - expect(payload[:iss]).to eq(expected_issuer) - expect(payload[:aud]).to eq(expected_audience) - expect(payload[:sub]).to eq(expected_subject) - expect(payload[:iat]).to eq(now) - expect(payload[:nbf]).to eq(now - 5.seconds) - expect(payload[:exp]).to eq(now + 10.minutes) - - # cloud connector specific claims - expect(payload[:gitlab_realm]).to eq(expected_realm) - expect(payload[:scopes]).to eq(expected_scopes) - end - - context 'when passing extra claims' do - let(:extra_claims) { { custom: 123 } } - - it 'includes them in payload' do - expect(payload[:custom]).to eq(123) - end - end - end - - describe '#encode' do - let(:rsa_key) { ::JWT::JWK.new(OpenSSL::PKey::RSA.new(2048)) } - - subject(:encoded_token) { token.encode(rsa_key) } - - it 'encodes token instance to string' do - expect(encoded_token).to be_instance_of(String) - end - - it 'decodes successfully with public key', :aggregate_failures, :freeze_time do - now = Time.current.to_i - payload, header = JWT.decode(encoded_token, rsa_key.public_key, true, { algorithm: 'RS256' }) - - expect(header).to match( - "alg" => "RS256", - "typ" => "JWT", - "kid" => be_instance_of(String) - ) - expect(payload).to match( - "jti" => be_instance_of(String), - "aud" => expected_audience, - "sub" => expected_subject, - "iss" => expected_issuer, - "iat" => now.to_i, - "nbf" => (now - 5.seconds).to_i, - "exp" => (now + 10.minutes).to_i, - "gitlab_realm" => expected_realm, - "scopes" => ["code_suggestions"] - ) - end - end -end diff --git a/ee/spec/requests/api/internal/ai/x_ray/scan_spec.rb b/ee/spec/requests/api/internal/ai/x_ray/scan_spec.rb index 52dd5601dcc81d1f9a345c3cce9e8f04a7cf649f..9165c6eed0f823e2c375ec2a291c2b81518b5ad3 100644 --- a/ee/spec/requests/api/internal/ai/x_ray/scan_spec.rb +++ b/ee/spec/requests/api/internal/ai/x_ray/scan_spec.rb @@ -194,7 +194,7 @@ end before do - allow_next_instance_of(::Gitlab::CloudConnector::JSONWebToken) do |token| + allow_next_instance_of(::Gitlab::CloudConnector::JsonWebToken) do |token| allow(token).to receive(:encode).and_return(ai_gateway_token) end end @@ -422,7 +422,7 @@ def request end before do - allow_next_instance_of(::Gitlab::CloudConnector::JSONWebToken) do |token| + allow_next_instance_of(::Gitlab::CloudConnector::JsonWebToken) do |token| allow(token).to receive(:encode).and_return(ai_gateway_token) end end