diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb index 4ee661d89f4491cb5933be63779dbe62e9dd8110..5fc21ba3f285e9ae39b708973821261aee89a29a 100644 --- a/app/models/ci/build_metadata.rb +++ b/app/models/ci/build_metadata.rb @@ -19,6 +19,7 @@ class BuildMetadata < Ci::ApplicationRecord before_create :set_build_project validates :build, presence: true + validates :id_tokens, json_schema: { filename: 'build_metadata_id_tokens' } validates :secrets, json_schema: { filename: 'build_metadata_secrets' } serialize :config_options, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb index aa9669ee208fe857ba10509ab35104000e561113..8c3a05c23f09214e559c2a956861106031079443 100644 --- a/app/models/concerns/ci/metadatable.rb +++ b/app/models/concerns/ci/metadatable.rb @@ -20,6 +20,8 @@ module Metadatable delegate :interruptible, to: :metadata, prefix: false, allow_nil: true delegate :environment_auto_stop_in, to: :metadata, prefix: false, allow_nil: true delegate :set_cancel_gracefully, to: :metadata, prefix: false, allow_nil: false + delegate :id_tokens, to: :metadata, allow_nil: true + before_create :ensure_metadata end @@ -77,6 +79,14 @@ def interruptible=(value) ensure_metadata.interruptible = value end + def id_tokens? + !!metadata&.id_tokens? + end + + def id_tokens=(value) + ensure_metadata.id_tokens = value + end + private def read_metadata_attribute(legacy_key, metadata_key, default_value = nil) diff --git a/app/validators/json_schemas/build_metadata_id_tokens.json b/app/validators/json_schemas/build_metadata_id_tokens.json new file mode 100644 index 0000000000000000000000000000000000000000..7f39c7274f3942900af30c6b8ecd4c1777b23192 --- /dev/null +++ b/app/validators/json_schemas/build_metadata_id_tokens.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "CI builds metadata ID tokens", + "type": "object", + "patternProperties": { + ".*": { + "type": "object", + "patternProperties": { + "^id_token$": { + "type": "object", + "required": ["aud"], + "properties": { + "aud": { "type": "string" }, + "field": { "type": "string" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + } +} diff --git a/db/migrate/20220808190124_add_id_token_to_ci_builds_metadata.rb b/db/migrate/20220808190124_add_id_token_to_ci_builds_metadata.rb new file mode 100644 index 0000000000000000000000000000000000000000..00d27d7c516f619f83ffbb74d925bb2b27af6e00 --- /dev/null +++ b/db/migrate/20220808190124_add_id_token_to_ci_builds_metadata.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddIdTokenToCiBuildsMetadata < Gitlab::Database::Migration[2.0] + enable_lock_retries! + + def change + add_column :ci_builds_metadata, :id_tokens, :jsonb, null: false, default: {} + end +end diff --git a/db/schema_migrations/20220808190124 b/db/schema_migrations/20220808190124 new file mode 100644 index 0000000000000000000000000000000000000000..99b7173cbb6161a00a50afdb5e19d458bf3d3c13 --- /dev/null +++ b/db/schema_migrations/20220808190124 @@ -0,0 +1 @@ +ab8dfd7549b2b61a5cf9d5b46935ec534ea77ec2025fdb58d03f654d81c8f6ee \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index b0afd780e305b0b7e6cce89a19bae725cd883c66..2294f556780dba83b2d660a0f428c24fbce430f7 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -12521,7 +12521,8 @@ CREATE TABLE ci_builds_metadata ( secrets jsonb DEFAULT '{}'::jsonb NOT NULL, build_id bigint NOT NULL, id bigint NOT NULL, - runtime_runner_features jsonb DEFAULT '{}'::jsonb NOT NULL + runtime_runner_features jsonb DEFAULT '{}'::jsonb NOT NULL, + id_tokens jsonb DEFAULT '{}'::jsonb NOT NULL ); CREATE SEQUENCE ci_builds_metadata_id_seq diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index 7e71f53b015da488c3cc693bbcca62622734d138..40c2d62c4659d3049fb13984d07a8bab9e3828a8 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -25,6 +25,8 @@ expect(bridge).to have_many(:sourced_pipelines) end + it_behaves_like 'has ID tokens', :ci_bridge + it 'has one downstream pipeline' do expect(bridge).to have_one(:sourced_pipeline) expect(bridge).to have_one(:downstream_pipeline) diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb index 5e30f9160cd59759adc7751e6389f36e078a00a1..e904463a5ca55cbc324c2b91bbb5e08e71590dc3 100644 --- a/spec/models/ci/build_metadata_spec.rb +++ b/spec/models/ci/build_metadata_spec.rb @@ -105,6 +105,13 @@ } } } + metadata.id_tokens = { + TEST_JWT_TOKEN: { + id_token: { + aud: 'https://gitlab.test' + } + } + } expect(metadata).to be_valid end @@ -113,10 +120,14 @@ context 'when data is invalid' do it 'returns errors' do metadata.secrets = { DATABASE_PASSWORD: { vault: {} } } + metadata.id_tokens = { TEST_JWT_TOKEN: { id_token: { aud: nil } } } aggregate_failures do expect(metadata).to be_invalid - expect(metadata.errors.full_messages).to eq(["Secrets must be a valid json schema"]) + expect(metadata.errors.full_messages).to contain_exactly( + 'Secrets must be a valid json schema', + 'Id tokens must be a valid json schema' + ) end end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 6cdc0ef9d9bfd7d7ec514d6ca21d8b04afa1c4b6..cefa5e9cb28c2cb4a21af215099f9b258ff4413b 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -81,6 +81,8 @@ end end + it_behaves_like 'has ID tokens', :ci_build + describe '.manual_actions' do let!(:manual_but_created) { create(:ci_build, :manual, status: :created, pipeline: pipeline) } let!(:manual_but_succeeded) { create(:ci_build, :manual, status: :success, pipeline: pipeline) } diff --git a/spec/support/shared_examples/models/ci/metadata_id_tokens_shared_examples.rb b/spec/support/shared_examples/models/ci/metadata_id_tokens_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..0c71ebe7a4d999744b115592023d2d22f4fe80ca --- /dev/null +++ b/spec/support/shared_examples/models/ci/metadata_id_tokens_shared_examples.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'has ID tokens' do |ci_type| + subject(:ci) { FactoryBot.build(ci_type) } + + describe 'delegations' do + it { is_expected.to delegate_method(:id_tokens).to(:metadata).allow_nil } + end + + describe '#id_tokens?' do + subject { ci.id_tokens? } + + context 'without metadata' do + let(:ci) { FactoryBot.build(ci_type) } + + it { is_expected.to be_falsy } + end + + context 'with metadata' do + let(:ci) { FactoryBot.build(ci_type, metadata: FactoryBot.build(:ci_build_metadata, id_tokens: id_tokens)) } + + context 'when ID tokens exist' do + let(:id_tokens) { { TEST_JOB_JWT: { id_token: { aud: 'developers ' } } } } + + it { is_expected.to be_truthy } + end + + context 'when ID tokens do not exist' do + let(:id_tokens) { {} } + + it { is_expected.to be_falsy } + end + end + end + + describe '#id_tokens=' do + it 'assigns the ID tokens to the CI job' do + id_tokens = [{ 'JOB_ID_TOKEN' => { 'id_token' => { 'aud' => 'https://gitlab.test ' } } }] + ci.id_tokens = id_tokens + + expect(ci.id_tokens).to match_array(id_tokens) + end + end +end