diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 1228dfd87d6781fdf440a57d9db4ad89aed79516..cb85f09802d3c61be394b314e12ffa72baad6884 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -1266,6 +1266,12 @@ def job_jwt_variables variables.append(key: 'CI_JOB_JWT', value: jwt, public: false, masked: true) variables.append(key: 'CI_JOB_JWT_V1', value: jwt, public: false, masked: true) variables.append(key: 'CI_JOB_JWT_V2', value: jwt_v2, public: false, masked: true) + + id_tokens.map do |token_variable_key, token_config| + token_jwt = Gitlab::Ci::JwtV2.for_build(self, aud: token_config.dig('id_token', 'aud')) + + variables.append(key: token_variable_key, value: token_jwt, public: false, masked: true) + end rescue OpenSSL::PKey::RSAError, Gitlab::Ci::Jwt::NoSigningKeyError => e Gitlab::ErrorTracking.track_exception(e) end diff --git a/app/validators/json_schemas/build_metadata_secrets.json b/app/validators/json_schemas/build_metadata_secrets.json index 3c8035d0dcf41a16cea2fd8c18e23c44fd06f259..c6e19685a2f939b90f496084703149c0d7259915 100644 --- a/app/validators/json_schemas/build_metadata_secrets.json +++ b/app/validators/json_schemas/build_metadata_secrets.json @@ -24,7 +24,15 @@ }, "additionalProperties": false }, - "^file$": { "type": "boolean" } + "^file$": { "type": "boolean" }, + "^id_token$": { + "type": "object", + "required": ["aud"], + "properties": { + "aud": { "type": "string" } + }, + "additionalProperties": false + } }, "additionalProperties": false } diff --git a/db/migrate/20220805181402_add_id_token_to_ci_builds_metadata.rb b/db/migrate/20220805181402_add_id_token_to_ci_builds_metadata.rb new file mode 100644 index 0000000000000000000000000000000000000000..3fd7d4a6897d7fb4fd4be21261631ad6a098dd0e --- /dev/null +++ b/db/migrate/20220805181402_add_id_token_to_ci_builds_metadata.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddIdTokenToCiBuildsMetadata < Gitlab::Database::Migration[2.0] + def change + add_column :ci_builds_metadata, :id_tokens, :jsonb, null: false, default: {} + end +end diff --git a/db/schema_migrations/20220805181402 b/db/schema_migrations/20220805181402 new file mode 100644 index 0000000000000000000000000000000000000000..4631c156268e9dc477b3a24be86b9f8e4df7e12c --- /dev/null +++ b/db/schema_migrations/20220805181402 @@ -0,0 +1 @@ +20cb099ce9e6e96048bd401707b7e362114177f16a82252625c59d643a3d7e45 \ No newline at end of file diff --git a/ee/app/models/concerns/ee/ci/metadatable.rb b/ee/app/models/concerns/ee/ci/metadatable.rb index 015f2c72d10bc355cd92e2a6f20c65c6fe67a1f5..b516cd8c77f9f1904790995169e69f653206b3c7 100644 --- a/ee/app/models/concerns/ee/ci/metadatable.rb +++ b/ee/app/models/concerns/ee/ci/metadatable.rb @@ -7,6 +7,7 @@ module Metadatable prepended do delegate :secrets, to: :metadata, allow_nil: true + delegate :id_tokens, to: :metadata, allow_nil: true end def secrets? @@ -16,6 +17,14 @@ def secrets? def secrets=(value) ensure_metadata.secrets = value end + + def id_tokens? + !!metadata&.id_tokens? + end + + def id_tokens=(value) + ensure_metadata.id_tokens = value + end end end end diff --git a/ee/app/models/ee/ci/build.rb b/ee/app/models/ee/ci/build.rb index 3fe621862e698ac88df0bca42e86ae12923c3570..48d6c53a85b6d06a3fd77e43a44426738cdc9122 100644 --- a/ee/app/models/ee/ci/build.rb +++ b/ee/app/models/ee/ci/build.rb @@ -67,7 +67,7 @@ def clone_accessors end def extra_accessors - (super + %i[secrets]).freeze + (super + %i[secrets id_tokens]).freeze end end diff --git a/ee/app/presenters/ee/ci/build_runner_presenter.rb b/ee/app/presenters/ee/ci/build_runner_presenter.rb index ab03c6cc98d3a384cc5571782e7dbc7052f8fa83..a2bf0e6edad43666ba310eecb254d28f34e19987 100644 --- a/ee/app/presenters/ee/ci/build_runner_presenter.rb +++ b/ee/app/presenters/ee/ci/build_runner_presenter.rb @@ -22,12 +22,22 @@ def vault_server 'name' => 'jwt', 'path' => variable_value('VAULT_AUTH_PATH', 'jwt'), 'data' => { - 'jwt' => '${CI_JOB_JWT}', + 'jwt' => "${#{jwt_var_key}}", 'role' => variable_value('VAULT_AUTH_ROLE') }.compact } } end + + def jwt_var_key + id_token_var_key.presence || 'CI_JOB_JWT_V2' + end + + def id_token_var_key + id_tokens.keys.find do |key| + variable_value(key).present? + end + end end end end diff --git a/ee/lib/ee/gitlab/ci/config/entry/job.rb b/ee/lib/ee/gitlab/ci/config/entry/job.rb index 965e9f6ad84334d4613f145f814b7c78d032e075..7f4045716a7058c377d3ddf70c635237cf7c71b2 100644 --- a/ee/lib/ee/gitlab/ci/config/entry/job.rb +++ b/ee/lib/ee/gitlab/ci/config/entry/job.rb @@ -16,17 +16,17 @@ module Job description: 'DAST configuration for this job', inherit: false - entry :secrets, ::Gitlab::Config::Entry::ComposableHash, + entry :secrets, ::Gitlab::Ci::Config::Entry::Secrets, description: 'Configured secrets for this job', inherit: false, - metadata: { composable_class: ::Gitlab::Ci::Config::Entry::Secret } + default: {} end EE_ALLOWED_KEYS = %i[dast_configuration secrets].freeze override :value def value - super.merge({ dast_configuration: dast_configuration_value, secrets: secrets_value }.compact) + super.merge({ dast_configuration: dast_configuration_value }.merge(secrets_value).compact) end class_methods do diff --git a/ee/lib/ee/gitlab/ci/yaml_processor/result.rb b/ee/lib/ee/gitlab/ci/yaml_processor/result.rb index 3e4fc72e90a863002095f0726447e54bc5973801..a8ea7634f4ef89791501126265e37b9341c55379 100644 --- a/ee/lib/ee/gitlab/ci/yaml_processor/result.rb +++ b/ee/lib/ee/gitlab/ci/yaml_processor/result.rb @@ -14,7 +14,8 @@ def build_attributes(name) super.deep_merge( { options: { dast_configuration: job[:dast_configuration] }.compact, - secrets: job[:secrets] + secrets: job[:secrets], + id_tokens: job[:id_tokens] }.compact ) end diff --git a/ee/lib/gitlab/ci/config/entry/id_token.rb b/ee/lib/gitlab/ci/config/entry/id_token.rb new file mode 100644 index 0000000000000000000000000000000000000000..86ea453fd0ef1304ba7f808b36a4cfebebe5048f --- /dev/null +++ b/ee/lib/gitlab/ci/config/entry/id_token.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents a JWT definition. + # + class IdToken < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Attributable + include ::Gitlab::Config::Entry::Validatable + + REQUIRED_KEYS = %i[aud].freeze + + attributes REQUIRED_KEYS + + validations do + validates :config, required_keys: REQUIRED_KEYS + validates :aud, presence: true, type: String + end + + def value + { aud: aud } + end + end + end + end + end +end diff --git a/ee/lib/gitlab/ci/config/entry/secret.rb b/ee/lib/gitlab/ci/config/entry/secret.rb index d0f595f2a1d632bcfa055943864e1bd5646e76c6..f73662dc829e986233b6d01e1d2f27780b717834 100644 --- a/ee/lib/gitlab/ci/config/entry/secret.rb +++ b/ee/lib/gitlab/ci/config/entry/secret.rb @@ -11,16 +11,16 @@ class Secret < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Configurable include ::Gitlab::Config::Entry::Attributable - REQUIRED_KEYS = %i[vault].freeze - ALLOWED_KEYS = (REQUIRED_KEYS + %i[file]).freeze + ALLOWED_KEYS = %i[vault file id_token].freeze attributes ALLOWED_KEYS entry :vault, Entry::Vault::Secret, description: 'Vault secrets engine configuration' entry :file, ::Gitlab::Config::Entry::Boolean, description: 'Should the created variable be of file type' + entry :id_token, ::Gitlab::Ci::Config::Entry::IdToken, description: 'Configuration for the JWT' validations do - validates :config, allowed_keys: ALLOWED_KEYS, required_keys: REQUIRED_KEYS + validates :config, allowed_keys: ALLOWED_KEYS end end end diff --git a/ee/lib/gitlab/ci/config/entry/secrets.rb b/ee/lib/gitlab/ci/config/entry/secrets.rb new file mode 100644 index 0000000000000000000000000000000000000000..fb19aac92b493d270bd9f131ca311fce3775a544 --- /dev/null +++ b/ee/lib/gitlab/ci/config/entry/secrets.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + class Secrets < ::Gitlab::Config::Entry::ComposableHash + def composable_class(_name, _config) + Secret + end + + def value + secrets = super + + id_tokens = secrets.select do |secret_key, secret_value| + secret_value.is_a?(Hash) && secret_value.has_key?(:id_token) + end + + id_tokens.each do |token_key, token_value| + secrets.delete(token_key) + end + + { id_tokens: id_tokens, secrets: secrets } + end + end + end + end + end +end diff --git a/ee/spec/lib/gitlab/ci/config/entry/job_spec.rb b/ee/spec/lib/gitlab/ci/config/entry/job_spec.rb index d0e1d4939ba52dad2f9abace2bb2f16444c3f174..0655c9632c7a23898a57b95d7f4420abdf67489e 100644 --- a/ee/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/ee/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -93,6 +93,7 @@ let(:config) { { script: 'echo', secrets: secrets } } let(:secrets) do { + TEST_ID_TOKEN: { id_token: { aud: 'https://gitlab.test' } }, DATABASE_PASSWORD: { vault: 'production/db/password' }, SSL_PRIVATE_KEY: { vault: 'production/ssl/private-key@ops' }, S3_SECRET_KEY: { @@ -134,6 +135,9 @@ } } }) + expect(entry.value[:id_tokens]).to eq({ + TEST_ID_TOKEN: { id_token: { aud: 'https://gitlab.test' } } + }) end end end diff --git a/lib/gitlab/ci/jwt.rb b/lib/gitlab/ci/jwt.rb index d3e7210b82074758997413f29d75580a5d8b9587..dbc86ef058c1c3c36b6f355c34cc8bb7c8999e59 100644 --- a/lib/gitlab/ci/jwt.rb +++ b/lib/gitlab/ci/jwt.rb @@ -12,7 +12,7 @@ def self.for_build(build) self.new(build, ttl: build.metadata_timeout).encoded end - def initialize(build, ttl: nil) + def initialize(build, ttl: nil, aud: nil) @build = build @ttl = ttl end diff --git a/lib/gitlab/ci/jwt_v2.rb b/lib/gitlab/ci/jwt_v2.rb index 4e01688a955b77696b61a786e32a3bdc81c39474..bd5e0ad3aa0616997d0e35918a4b1cc11c804e27 100644 --- a/lib/gitlab/ci/jwt_v2.rb +++ b/lib/gitlab/ci/jwt_v2.rb @@ -3,13 +3,25 @@ module Gitlab module Ci class JwtV2 < Jwt + def self.for_build(build, aud: Settings.gitlab.base_url) + new(build, ttl: build.metadata_timeout, aud: aud).encoded + end + + def initialize(build, ttl: nil, aud:) + super + + @aud = aud + end + private + attr_reader :aud + def reserved_claims super.merge( iss: Settings.gitlab.base_url, sub: "project_path:#{project.full_path}:ref_type:#{ref_type}:ref:#{source_ref}", - aud: Settings.gitlab.base_url + aud: aud ) end end