diff --git a/app/validators/json_schemas/build_metadata_secrets.json b/app/validators/json_schemas/build_metadata_secrets.json index 952f9cf5cda03abe2c3e68d009739613b810ce26..f9122ff811c4f0c0e035c7b724dbe44cbf41f1dc 100644 --- a/app/validators/json_schemas/build_metadata_secrets.json +++ b/app/validators/json_schemas/build_metadata_secrets.json @@ -39,6 +39,18 @@ }, "additionalProperties": false }, + "^gitlab_secrets_manager$": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, "^gcp_secret_manager$": { "type": "object", "required": [ @@ -198,6 +210,11 @@ "required": [ "akeyless" ] + }, + { + "required": [ + "gitlab_secrets_manager" + ] } ], "additionalProperties": false diff --git a/ee/app/models/ci/secrets/integration.rb b/ee/app/models/ci/secrets/integration.rb index 52f92befc0a206f1eacdf8fd19436699a83bb328..c8ded1cb51872d390c1152d49b160458341375aa 100644 --- a/ee/app/models/ci/secrets/integration.rb +++ b/ee/app/models/ci/secrets/integration.rb @@ -3,19 +3,37 @@ module Ci module Secrets class Integration - PROVIDERS = [ - :azure_key_vault, - :akeyless, - :gcp_secret_manager, - :hashicorp_vault - ].freeze - - def initialize(variables) + PROVIDER_TYPE_MAP = { + "azure_key_vault" => :azure_key_vault, + "akeyless" => :akeyless, + "gcp_secret_manager" => :gcp_secret_manager, + "vault" => :hashicorp_vault, + "gitlab_secrets_manager" => :gitlab_secrets_manager + }.freeze + + def initialize(variables:, project:) @variables = variables + @project = project end - def secrets_provider? - PROVIDERS.any? { |provider| send(:"#{provider}?") } # rubocop:disable GitlabSecurity/PublicSend -- metaprogramming + def secrets_provider?(secrets) + candidates = PROVIDER_TYPE_MAP.values.select { |provider| send(:"#{provider}?") } # rubocop:disable GitlabSecurity/PublicSend -- metaprogramming + + # No providers are enabled. + return false if candidates.empty? + + # No secrets were provided; vacuously this means all provided secrets + # have a provider. This is deferred so global enablement logic can be + # checked independently of secrets value. + return true if secrets.nil? || secrets.empty? + + # If none of the secrets lacks an enabled provider, we're good. + secrets.none? do |(_, secret_info)| + secret_info.any? do |(provider_key, _)| + PROVIDER_TYPE_MAP.has_key?(provider_key) && + candidates.exclude?(PROVIDER_TYPE_MAP[provider_key]) + end + end end def variable_value(key, default = nil) @@ -51,6 +69,12 @@ def hashicorp_vault? def akeyless? variable_value('AKEYLESS_ACCESS_ID').present? end + + def gitlab_secrets_manager? + # TODO: figure out context for whether GitLab Secrets Manager is + # globally enabled on this instance. + SecretsManagement::ProjectSecretsManager.find_by_project_id(@project.id)&.active? + end end end end diff --git a/ee/app/models/ee/ci/build.rb b/ee/app/models/ee/ci/build.rb index 71314b483a5221d51efe3d38b40c038e8f873159..99d4faf8c3bac9b2cffd4cb29cfa69461443d47f 100644 --- a/ee/app/models/ee/ci/build.rb +++ b/ee/app/models/ee/ci/build.rb @@ -186,7 +186,7 @@ def runner_required_feature_names end def secrets_integration - ::Ci::Secrets::Integration.new(variables) + ::Ci::Secrets::Integration.new(variables: variables, project: project) end def playable? diff --git a/ee/app/models/secrets_management/project_secrets_manager.rb b/ee/app/models/secrets_management/project_secrets_manager.rb index 924912bc899940797da88e91ec99dd870c6893b6..18f89ca20faf12ef3040387d207d8267b830c6b1 100644 --- a/ee/app/models/secrets_management/project_secrets_manager.rb +++ b/ee/app/models/secrets_management/project_secrets_manager.rb @@ -4,7 +4,8 @@ module SecretsManagement class ProjectSecretsManager < ApplicationRecord STATUSES = { provisioning: 0, - active: 1 + active: 1, + disabled: 2 }.freeze self.table_name = 'project_secrets_managers' @@ -16,25 +17,76 @@ class ProjectSecretsManager < ApplicationRecord state_machine :status, initial: :provisioning do state :provisioning, value: STATUSES[:provisioning] state :active, value: STATUSES[:active] + state :disabled, value: STATUSES[:disabled] event :activate do transition all - [:active] => :active end + + event :disable do + transition active: :disabled + end + end + + def self.server_url + # Allow setting an external secrets manager URL if necessary. This is + # useful for GitLab.Com's deployment. + return Gitlab.config.openbao.url if Gitlab.config.has_key?("openbao") && Gitlab.config.openbao.has_key?("url") + + default_openbao_server_url + end + + def self.default_openbao_server_url + "#{Gitlab.config.gitlab.protocol}://#{Gitlab.config.gitlab.host}:8200" end + private_class_method :default_openbao_server_url def ci_secrets_mount_path [ namespace_path, "project_#{project.id}", - 'ci' + 'secrets', + 'kv' ].compact.join('/') end + def ci_data_path(secret_key) + [ + 'explicit', + secret_key + ].compact.join('/') + end + + def ci_full_path(secret_key) + [ + ci_secrets_mount_path, + 'data', + ci_data_path(secret_key) + ].compact.join('/') + end + + def ci_auth_mount + [ + namespace_path, + 'pipeline_jwt' + ].compact.join('/') + end + + def ci_auth_role + "project_#{project.id}" + end + + def ci_auth_type + 'jwt' + end + + def ci_jwt(build) + Gitlab::Ci::JwtV2.for_build(build, aud: self.class.server_url) + end + private def namespace_path - return unless project.namespace.type == "User" - [ project.namespace.type.downcase, project.namespace.id.to_s diff --git a/ee/app/presenters/ee/ci/build_runner_presenter.rb b/ee/app/presenters/ee/ci/build_runner_presenter.rb index d32b269736317e3a3174e01bfa1f422e4509d3ce..83c5bd7f6df9249a0c0ae42fa1f2e0e9e0abeb6f 100644 --- a/ee/app/presenters/ee/ci/build_runner_presenter.rb +++ b/ee/app/presenters/ee/ci/build_runner_presenter.rb @@ -15,6 +15,26 @@ def secrets_configuration secret['akeyless']['server'] = akeyless_server(secret) end + # For compatibility with the existing Vault integration in Runner, + # template gitlab_secrets_manager data into the vault field. + if secret.has_key?('gitlab_secrets_manager') + # GitLab Secrets Manager and Vault integrations have different + # structure; remove the old secret but save its data for later. + gtsm_secret = secret.delete('gitlab_secrets_manager') + + psm = SecretsManagement::ProjectSecretsManager.find_by_project_id(project.id) + + # Compute full path to secret in OpenBao for Vault runner + # compatibility. + secret['vault'] = {} + secret['vault']['path'] = psm.ci_data_path(gtsm_secret['name']) + secret['vault']['engine'] = { name: "kv-v2", path: psm.ci_secrets_mount_path } + secret['vault']['field'] = "value" + + # Tell Runner about our server information. + secret['vault']['server'] = gitlab_secrets_manager_server(psm) + end + secret end end @@ -36,6 +56,20 @@ def vault_server(secret) } end + def gitlab_secrets_manager_server(psm) + @gitlab_secrets_manager_server ||= { + 'url' => SecretsManagement::ProjectSecretsManager.server_url, + 'auth' => { + 'name' => psm.ci_auth_type, + 'path' => psm.ci_auth_mount, + 'data' => { + 'jwt' => psm.ci_jwt(self), + 'role' => psm.ci_auth_role + }.compact + } + } + end + def vault_jwt(secret) if id_tokens? id_token_var(secret) diff --git a/ee/app/services/ci/pipeline_creation/drop_secrets_provider_not_found_builds_service.rb b/ee/app/services/ci/pipeline_creation/drop_secrets_provider_not_found_builds_service.rb index fbeb352b8e62145b43ffc1c8661aed0a15ec8345..d618b156dbb7249383b86663c0dc65751c74a061 100644 --- a/ee/app/services/ci/pipeline_creation/drop_secrets_provider_not_found_builds_service.rb +++ b/ee/app/services/ci/pipeline_creation/drop_secrets_provider_not_found_builds_service.rb @@ -12,7 +12,7 @@ def execute pipeline.builds.each do |build| next unless build.created? - next unless build.secrets? && !build.secrets_provider? + next unless build.secrets? && !build.secrets_provider?(build.secrets) build.drop!(:secrets_provider_not_found, skip_pipeline_processing: true) end diff --git a/ee/config/events/create_secrets_gitlab_secrets_manager.yml b/ee/config/events/create_secrets_gitlab_secrets_manager.yml new file mode 100644 index 0000000000000000000000000000000000000000..285cdcf29fa851f08730a0fc45306e0c8c534d3f --- /dev/null +++ b/ee/config/events/create_secrets_gitlab_secrets_manager.yml @@ -0,0 +1,15 @@ +--- +description: Pipeline created with secrets (GitLab Secrets Manager) defined +internal_events: true +action: create_secrets_gitlab_secrets_manager +identifiers: +- namespace +- user +product_group: pipeline_security +milestone: '17.6' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/162963 +distributions: +- ee +tiers: +- premium +- ultimate diff --git a/ee/config/metrics/counts_28d/20210216184251_i_ci_secrets_management_vault_build_created_monthly.yml b/ee/config/metrics/counts_28d/20210216184251_i_ci_secrets_management_vault_build_created_monthly.yml index 8fc3695b4c8b9997a13d22ebbd97d97665e41a01..377a47748223b3ca94694c53fdc02a3c3fabc3ef 100644 --- a/ee/config/metrics/counts_28d/20210216184251_i_ci_secrets_management_vault_build_created_monthly.yml +++ b/ee/config/metrics/counts_28d/20210216184251_i_ci_secrets_management_vault_build_created_monthly.yml @@ -2,7 +2,7 @@ data_category: optional key_path: redis_hll_counters.ci_secrets_management.i_ci_secrets_management_vault_build_created_monthly description: Monthly active users creating pipelines that that have the Vault JWT - with it.' + with it. product_group: environments value_type: number status: active diff --git a/ee/config/metrics/counts_28d/count_total_create_secrets_azure_key_vault_monthly.yml b/ee/config/metrics/counts_28d/count_total_create_secrets_azure_key_vault_monthly.yml index ac953b0b7a697dc4317d3cef4116c66d94f1c632..8dc23f49b857af292f14f28f224366e6b9d349f8 100644 --- a/ee/config/metrics/counts_28d/count_total_create_secrets_azure_key_vault_monthly.yml +++ b/ee/config/metrics/counts_28d/count_total_create_secrets_azure_key_vault_monthly.yml @@ -1,7 +1,7 @@ --- data_category: optional key_path: redis_hll_counters.ci_secrets_management.i_ci_secrets_management_azure_key_vault_build_created_monthly -description: Monthly active users creating pipelines that that have the Azure Key Vault secrets.' +description: Monthly active users creating pipelines that that have the Azure Key Vault secrets. product_group: pipeline_security value_type: number status: active diff --git a/ee/config/metrics/counts_28d/count_total_create_secrets_gitlab_secrets_manager_monthly.yml b/ee/config/metrics/counts_28d/count_total_create_secrets_gitlab_secrets_manager_monthly.yml new file mode 100644 index 0000000000000000000000000000000000000000..23cb91808daf5672934440b33d734b30a640338f --- /dev/null +++ b/ee/config/metrics/counts_28d/count_total_create_secrets_gitlab_secrets_manager_monthly.yml @@ -0,0 +1,21 @@ +--- +data_category: optional +key_path: redis_hll_counters.ci_secrets_management.i_ci_secrets_management_gitlab_secrets_manager_build_created_monthly +description: Monthly active users creating pipelines that that have the GitLab Secret Manager secrets. +product_group: pipeline_security +value_type: number +status: active +time_frame: 28d +data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - i_ci_secrets_management_gitlab_secrets_manager_build_created +distribution: + - ee +tier: + - premium + - ultimate +performance_indicator_type: [] +milestone: "17.6" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/162963 diff --git a/ee/config/metrics/counts_28d/count_total_create_secrets_google_cloud_monthly.yml b/ee/config/metrics/counts_28d/count_total_create_secrets_google_cloud_monthly.yml index 9dc29b29e8e4095bbd1cc8b43c60c861b5e00866..c35535b427b6584644836373174ac1fe001bc272 100644 --- a/ee/config/metrics/counts_28d/count_total_create_secrets_google_cloud_monthly.yml +++ b/ee/config/metrics/counts_28d/count_total_create_secrets_google_cloud_monthly.yml @@ -1,7 +1,7 @@ --- data_category: optional key_path: redis_hll_counters.ci_secrets_management.i_ci_secrets_management_gcp_secret_manager_build_created_monthly -description: Monthly active users creating pipelines that that have the GCP Secret Manager secrets.' +description: Monthly active users creating pipelines that that have the GCP Secret Manager secrets. product_group: pipeline_security value_type: number status: active diff --git a/ee/config/metrics/counts_7d/20210216184249_i_ci_secrets_management_vault_build_created_weekly.yml b/ee/config/metrics/counts_7d/20210216184249_i_ci_secrets_management_vault_build_created_weekly.yml index 4d043269bfa0c0e6fe908762a5d98f062becfbc3..27db5079eecd634c93122a1eb900d677d7510498 100644 --- a/ee/config/metrics/counts_7d/20210216184249_i_ci_secrets_management_vault_build_created_weekly.yml +++ b/ee/config/metrics/counts_7d/20210216184249_i_ci_secrets_management_vault_build_created_weekly.yml @@ -2,7 +2,7 @@ data_category: optional key_path: redis_hll_counters.ci_secrets_management.i_ci_secrets_management_vault_build_created_weekly description: Weekly active users creating pipelines that that have the Vault JWT - with it.' + with it. product_group: environments value_type: number status: active diff --git a/ee/config/metrics/counts_7d/count_total_create_secrets_azure_key_vault_weekly.yml b/ee/config/metrics/counts_7d/count_total_create_secrets_azure_key_vault_weekly.yml index 842de44651bf38d7fc99584ad6080c5346673762..586575a9de7a7f181d1471bcdd01269add708d92 100644 --- a/ee/config/metrics/counts_7d/count_total_create_secrets_azure_key_vault_weekly.yml +++ b/ee/config/metrics/counts_7d/count_total_create_secrets_azure_key_vault_weekly.yml @@ -1,7 +1,7 @@ --- data_category: optional key_path: redis_hll_counters.ci_secrets_management.i_ci_secrets_management_azure_key_vault_build_created_weekly -description: Monthly active users creating pipelines that that have the Azure Key Vault secrets.' +description: Monthly active users creating pipelines that that have the Azure Key Vault secrets. product_group: pipeline_security value_type: number status: active diff --git a/ee/config/metrics/counts_7d/count_total_create_secrets_gitlab_secrets_manager_weekly.yml b/ee/config/metrics/counts_7d/count_total_create_secrets_gitlab_secrets_manager_weekly.yml new file mode 100644 index 0000000000000000000000000000000000000000..7c6fb72c09bf3aa352e6ce3c1020429daf6bc452 --- /dev/null +++ b/ee/config/metrics/counts_7d/count_total_create_secrets_gitlab_secrets_manager_weekly.yml @@ -0,0 +1,21 @@ +--- +data_category: optional +key_path: redis_hll_counters.ci_secrets_management.i_ci_secrets_management_gitlab_secrets_manager_build_created_weekly +description: Monthly active users creating pipelines that that have the GitLab Secrets Manager secrets. +product_group: pipeline_security +value_type: number +status: active +time_frame: 7d +data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - i_ci_secrets_management_gitlab_secrets_manager_build_created +distribution: + - ee +tier: + - premium + - ultimate +performance_indicator_type: [] +milestone: "17.6" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/162963 diff --git a/ee/config/metrics/counts_7d/count_total_create_secrets_google_cloud_weekly.yml b/ee/config/metrics/counts_7d/count_total_create_secrets_google_cloud_weekly.yml index 445c6185c8377b27eaf4eb8c327a2cc2ac8d6477..ff0848caefbc97d33a99585712ea6eecba4935c2 100644 --- a/ee/config/metrics/counts_7d/count_total_create_secrets_google_cloud_weekly.yml +++ b/ee/config/metrics/counts_7d/count_total_create_secrets_google_cloud_weekly.yml @@ -1,7 +1,7 @@ --- data_category: optional key_path: redis_hll_counters.ci_secrets_management.i_ci_secrets_management_gcp_secret_manager_build_created_weekly -description: Monthly active users creating pipelines that that have the GCP Secret Manager secrets.' +description: Monthly active users creating pipelines that that have the GCP Secret Manager secrets. product_group: pipeline_security value_type: number status: active diff --git a/ee/lib/gitlab/ci/config/entry/gitlab_secrets_manager/secret.rb b/ee/lib/gitlab/ci/config/entry/gitlab_secrets_manager/secret.rb new file mode 100644 index 0000000000000000000000000000000000000000..32d224135b2ead93c7024ceb1cb6d44389e72d8f --- /dev/null +++ b/ee/lib/gitlab/ci/config/entry/gitlab_secrets_manager/secret.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + module GitlabSecretsManager + class Secret < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable + + ALLOWED_KEYS = %i[name].freeze + + attributes ALLOWED_KEYS + + validations do + validates :config, type: Hash + validates :name, presence: true, type: String + end + + def value + { + name: name + } + end + 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 ae5625c8b25e1a1639f8f608d651426d7fe81d20..bf91e5b33b9eadf0ee344442c9e686f09926ed6c 100644 --- a/ee/lib/gitlab/ci/config/entry/secret.rb +++ b/ee/lib/gitlab/ci/config/entry/secret.rb @@ -11,8 +11,8 @@ class Secret < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Configurable include ::Gitlab::Config::Entry::Attributable - ALLOWED_KEYS = %i[vault file azure_key_vault gcp_secret_manager akeyless token].freeze - SUPPORTED_PROVIDERS = %i[vault azure_key_vault gcp_secret_manager akeyless].freeze + ALLOWED_KEYS = %i[vault file azure_key_vault gcp_secret_manager akeyless gitlab_secrets_manager token].freeze + SUPPORTED_PROVIDERS = %i[vault azure_key_vault gcp_secret_manager akeyless gitlab_secrets_manager].freeze attributes ALLOWED_KEYS @@ -21,6 +21,8 @@ class Secret < ::Gitlab::Config::Entry::Node entry :azure_key_vault, Entry::AzureKeyVault::Secret, description: 'Azure Key Vault configuration' entry :gcp_secret_manager, Entry::GcpSecretManager::Secret, description: 'GCP Secrets Manager configuration' entry :akeyless, Entry::Akeyless::Secret, description: 'Akeyless Key Vault configuration' + entry :gitlab_secrets_manager, Entry::GitlabSecretsManager::Secret, + description: 'Gitlab Secrets Manager configuration' validations do validates :config, allowed_keys: ALLOWED_KEYS, only_one_of_keys: SUPPORTED_PROVIDERS @@ -34,6 +36,7 @@ class Secret < ::Gitlab::Config::Entry::Node def value { vault: vault_value, + gitlab_secrets_manager: gitlab_secrets_manager_value, gcp_secret_manager: gcp_secret_manager_value, azure_key_vault: azure_key_vault_value, akeyless: akeyless_value, diff --git a/ee/spec/lib/gitlab/ci/config/entry/gitlab_secrets_manager/secret_spec.rb b/ee/spec/lib/gitlab/ci/config/entry/gitlab_secrets_manager/secret_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..47dfb215791c8b1d3e3163e624c665136b87dfe6 --- /dev/null +++ b/ee/spec/lib/gitlab/ci/config/entry/gitlab_secrets_manager/secret_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Entry::GitlabSecretsManager::Secret, feature_category: :secrets_management do + let(:entry) { described_class.new(config) } + + before do + entry.compose! + end + + describe 'validations' do + context 'when all config value is correct' do + let(:config) do + { + name: 'name' + } + end + + it { expect(entry).to be_valid } + end + + context 'when name is nil' do + let(:config) do + { + name: nil + } + end + + it { expect(entry).not_to be_valid } + + it 'reports error' do + expect(entry.errors) + .to include 'secret name can\'t be blank' + end + end + + context 'when there is an unknown key present' do + let(:config) { { foo: :bar } } + + it { expect(entry).not_to be_valid } + + it 'reports error' do + expect(entry.errors) + .to include "secret name can't be blank" + end + end + + context 'when config is not a hash' do + let(:config) { "" } + + it { expect(entry).not_to be_valid } + + it 'reports error' do + expect(entry.errors) + .to include 'secret config should be a hash' + end + end + end + + describe '#value' do + context 'when config is valid' do + let(:config) do + { + name: 'name' + } + end + + let(:result) do + { + name: "name" + } + end + + it 'returns config' do + expect(entry.value).to eq(result) + end + end + end +end diff --git a/ee/spec/lib/gitlab/ci/config/entry/secret_spec.rb b/ee/spec/lib/gitlab/ci/config/entry/secret_spec.rb index ae6e7ecb433d02d3d2e6aefacb2d7ba4bc04b238..e9035502c65700d2ddadc217deb9f57e03ba0208 100644 --- a/ee/spec/lib/gitlab/ci/config/entry/secret_spec.rb +++ b/ee/spec/lib/gitlab/ci/config/entry/secret_spec.rb @@ -299,6 +299,36 @@ end end end + + context 'for Gitlab Secrets Manager' do + context 'when config is valid' do + let(:config) do + { + gitlab_secrets_manager: { + name: 'name' + } + } + end + + describe '#value' do + it 'returns secret configuration' do + expected_result = { + gitlab_secrets_manager: { + name: 'name' + } + } + + expect(entry.value).to eq(expected_result) + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + end end end @@ -319,7 +349,7 @@ it 'reports error' do expect(entry.errors) .to include 'secret config must use exactly one of these keys: ' \ - 'vault, azure_key_vault, gcp_secret_manager, akeyless' + 'vault, azure_key_vault, gcp_secret_manager, akeyless, gitlab_secrets_manager' end end diff --git a/ee/spec/lib/gitlab/ci/yaml_processor_spec.rb b/ee/spec/lib/gitlab/ci/yaml_processor_spec.rb index ed8c0326ce2b784aadc632b22cf738ab5ef50973..b90b5e2b9041e8c63c2ca2ff63b6b0918fd8bce1 100644 --- a/ee/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/ee/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -304,6 +304,32 @@ end end + context 'on gitlab_secrets_manager' do + let(:secrets) do + { + DATABASE_PASSWORD: { + gitlab_secrets_manager: { + name: 'password' + } + } + } + end + + let(:config) { { deploy_to_production: { stage: 'deploy', script: ['echo'], secrets: secrets } } } + + it "returns secrets info" do + secrets = result.builds.first.fetch(:secrets) + + expect(secrets).to eq({ + DATABASE_PASSWORD: { + gitlab_secrets_manager: { + name: 'password' + } + } + }) + end + end + context 'on azure key vault' do let(:secrets) do { diff --git a/ee/spec/models/ci/build_spec.rb b/ee/spec/models/ci/build_spec.rb index 7ce242477c38798a9565b3ff3e1e7fcb9ba6fbc6..66232f18cd66c138c77deb889b34c09d9e352233 100644 --- a/ee/spec/models/ci/build_spec.rb +++ b/ee/spec/models/ci/build_spec.rb @@ -868,7 +868,7 @@ stub_licensed_features(ci_secrets_management: false) end - (Gitlab::Ci::Config::Entry::Secret::SUPPORTED_PROVIDERS - [:akeyless]).each do |provider| + (Gitlab::Ci::Config::Entry::Secret::SUPPORTED_PROVIDERS - [:akeyless, :gitlab_secrets_manager]).each do |provider| context "when using #{provider}" do let(:valid_secret) { valid_secret_configs.fetch(provider) } let(:ci_build) { build(:ci_build, secrets: valid_secret, ci_stage: stage) } @@ -884,7 +884,7 @@ end context 'when there are secrets defined' do - (Gitlab::Ci::Config::Entry::Secret::SUPPORTED_PROVIDERS - [:akeyless]).each do |provider| + (Gitlab::Ci::Config::Entry::Secret::SUPPORTED_PROVIDERS - [:akeyless, :gitlab_secrets_manager]).each do |provider| context "when using #{provider}" do let(:valid_secret) { valid_secret_configs.fetch(provider) } @@ -910,9 +910,10 @@ let(:valid_secret) { valid_secret_configs.values.inject(:merge) } let(:ci_build) { build(:ci_build, secrets: valid_secret, user: user, ci_stage: stage) } + let(:supported_providers_with_tracking) { Gitlab::Ci::Config::Entry::Secret::SUPPORTED_PROVIDERS - [:akeyless, :gitlab_secrets_manager] } it 'tracks RedisHLL event with user_id on all providers' do - (Gitlab::Ci::Config::Entry::Secret::SUPPORTED_PROVIDERS - [:akeyless]).each do |provider| + supported_providers_with_tracking.each do |provider| expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event) .with("i_ci_secrets_management_#{provider}_build_created", values: user.id) end @@ -923,7 +924,7 @@ it 'tracks Snowplow event with RedisHLL context on all providers' do ci_build.save! - (Gitlab::Ci::Config::Entry::Secret::SUPPORTED_PROVIDERS - [:akeyless]).each do |provider| + supported_providers_with_tracking.each do |provider| params = { category: described_class.to_s, action: "create_secrets_#{provider}", diff --git a/ee/spec/models/ci/secrets/integration_spec.rb b/ee/spec/models/ci/secrets/integration_spec.rb index 46a265d2599d8d3400124dc55e5c1203dc8be5e6..521105a171f61c2f0f4fa08dc3369a0e28ff27b0 100644 --- a/ee/spec/models/ci/secrets/integration_spec.rb +++ b/ee/spec/models/ci/secrets/integration_spec.rb @@ -7,7 +7,7 @@ let_it_be_with_refind(:pipeline) { create(:ci_pipeline, project: project) } let(:job) { create(:ci_build, pipeline: pipeline) } - subject(:secrets_provider?) { job.secrets_provider? } + subject(:secrets_provider?) { job.secrets_provider?(nil) } describe '#secrets_provider?' do context 'when no secret CI variables are set' do diff --git a/ee/spec/models/secrets_management/project_secrets_manager_spec.rb b/ee/spec/models/secrets_management/project_secrets_manager_spec.rb index 58ec0d5188c38f23c816191094f4d9cdccf6fa01..b1b3b57a97263225ac05f1717e9b22c988152fc2 100644 --- a/ee/spec/models/secrets_management/project_secrets_manager_spec.rb +++ b/ee/spec/models/secrets_management/project_secrets_manager_spec.rb @@ -35,15 +35,59 @@ let_it_be(:project) { create(:project) } it 'includes the namespace type and ID in the path' do - expect(path).to eq("user_#{project.namespace.id}/project_#{project.id}/ci") + expect(path).to eq("user_#{project.namespace.id}/project_#{project.id}/secrets/kv") end end context 'when the project belongs to a group namespace' do let_it_be(:project) { create(:project, :in_group) } - it 'does not include the namespace type and ID in the path' do - expect(path).to eq("project_#{project.id}/ci") + it 'includes the namespace type and ID in the path' do + expect(path).to eq("group_#{project.namespace.id}/project_#{project.id}/secrets/kv") + end + end + end + + describe '#ci_data_path' do + let(:secrets_manager) { build(:project_secrets_manager, project: project) } + + subject(:path) { secrets_manager.ci_data_path("DB_PASS") } + + context 'when the project belongs to a user namespace' do + let_it_be(:project) { create(:project) } + + it 'does not include any namespace information' do + expect(path).to eq("explicit/DB_PASS") + end + end + + context 'when the project belongs to a group namespace' do + let_it_be(:project) { create(:project, :in_group) } + + it 'does not include any namespace information' do + expect(path).to eq("explicit/DB_PASS") + end + end + end + + describe '#ci_full_path' do + let(:secrets_manager) { build(:project_secrets_manager, project: project) } + + subject(:path) { secrets_manager.ci_full_path("DB_PASS") } + + context 'when the project belongs to a user namespace' do + let_it_be(:project) { create(:project) } + + it 'does not include any namespace information' do + expect(path).to eq("user_#{project.namespace.id}/project_#{project.id}/secrets/kv/data/explicit/DB_PASS") + end + end + + context 'when the project belongs to a group namespace' do + let_it_be(:project) { create(:project, :in_group) } + + it 'does not include any namespace information' do + expect(path).to eq("group_#{project.namespace.id}/project_#{project.id}/secrets/kv/data/explicit/DB_PASS") end end end diff --git a/ee/spec/presenters/ci/build_runner_presenter_spec.rb b/ee/spec/presenters/ci/build_runner_presenter_spec.rb index 46fbc21fef1256e51180e280bffc457cc52e5f54..45fc0e86860e2b9f0a19562014e2d97e9af32cb6 100644 --- a/ee/spec/presenters/ci/build_runner_presenter_spec.rb +++ b/ee/spec/presenters/ci/build_runner_presenter_spec.rb @@ -6,6 +6,7 @@ describe '#secrets_configuration' do let!(:ci_build) { create(:ci_build, secrets: secrets) } + let(:jwt_token) { "TESTING" } context 'build has no secrets' do let(:secrets) { {} } @@ -30,22 +31,18 @@ } end + before do + create(:ci_variable, project: ci_build.project, key: 'VAULT_SERVER_URL', value: 'https://vault.example.com') + end + context 'Vault server URL' do let(:vault_server) { presenter.secrets_configuration.dig('DATABASE_PASSWORD', 'vault', 'server') } context 'VAULT_SERVER_URL CI variable is present' do it 'returns the URL' do - create(:ci_variable, project: ci_build.project, key: 'VAULT_SERVER_URL', value: 'https://vault.example.com') - expect(vault_server.fetch('url')).to eq('https://vault.example.com') end end - - context 'VAULT_SERVER_URL CI variable is not present' do - it 'returns nil' do - expect(vault_server.fetch('url')).to be_nil - end - end end context 'Vault auth role' do @@ -332,6 +329,50 @@ end end + context 'with Gitlab Secrets Manager' do + let(:secrets) do + { + DATABASE_PASSWORD: { + gitlab_secrets_manager: { + name: "password" + } + } + } + end + + before do + create(:project_secrets_manager, project: ci_build.project) + end + + context 'JWT token' do + before do + allow_any_instance_of(SecretsManagement::ProjectSecretsManager).to receive(:ci_jwt).and_return(jwt_token) # rubocop:disable RSpec/AnyInstanceOf -- It's not the next instance + end + + let(:gitlab_secrets_manager_server) { presenter.secrets_configuration.dig('DATABASE_PASSWORD', 'vault', 'server') } + + it 'uses the specified token variable' do + expect(gitlab_secrets_manager_server.fetch('auth')['data']['jwt']).to eq(jwt_token) + end + end + + context 'JWT auth method and path' do + before do + rsa_key = OpenSSL::PKey::RSA.generate(3072).to_s + stub_application_setting(ci_jwt_signing_key: rsa_key) + end + + let(:gitlab_secrets_manager_server) { presenter.secrets_configuration.dig('DATABASE_PASSWORD', 'vault', 'server') } + + it 'uses the specified token variable' do + expect(gitlab_secrets_manager_server.fetch('auth')['name']).to eq("jwt") + expect(gitlab_secrets_manager_server.fetch('auth')['path']).to eq("#{ci_build.project.namespace.type.downcase}_#{ci_build.project.namespace.id}/pipeline_jwt") + expect(gitlab_secrets_manager_server.fetch('auth')['data']['role']).to eq("project_#{ci_build.project.id}") + expect(gitlab_secrets_manager_server.fetch('auth')['data']['jwt']).not_to be_empty + end + end + end + context 'with akeyless secret manager' do let(:secrets) do { diff --git a/ee/spec/services/ci/create_pipeline_service/secrets_spec.rb b/ee/spec/services/ci/create_pipeline_service/secrets_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ec0f5f97ea7217f14ebb5423c1960baa2bf9279b --- /dev/null +++ b/ee/spec/services/ci/create_pipeline_service/secrets_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::CreatePipelineService, feature_category: :secrets_management do # rubocop:disable RSpec/SpecFilePathFormat -- create_pipeline_service is split into components + let(:downstream_project) { create(:project, path: 'project', namespace: create(:namespace, path: 'some')) } + + let_it_be(:project) { create(:project, :repository) } + let(:user) { project.first_owner } + let(:service) { described_class.new(project, user, { ref: 'refs/heads/master' }) } + + let(:config) do + <<~YAML + test_openbao_direct: + secrets: + TEST_SECRET: + gitlab_secrets_manager: + name: foo + script: + - echo "testing Openbao in CI" + - cat $TEST_SECRET + - echo "done." + YAML + end + + before do + downstream_project.add_developer(user) + stub_ci_pipeline_yaml_file(config) + end + + it 'persists pipeline' do + pipeline = create_pipeline! + expect(pipeline).to be_persisted + + job = pipeline.builds.find_by_name('test_openbao_direct') + expect(job).not_to be_failed + end + + def create_pipeline! + service.execute(:push).payload + end +end diff --git a/lib/gitlab/ci/jwt_v2.rb b/lib/gitlab/ci/jwt_v2.rb index 7892c4ad757f7c333fd031ce4c9b3bbdd4b4bc9d..5638a3c42a309d18743318f9c4dbfcb56e81b44c 100644 --- a/lib/gitlab/ci/jwt_v2.rb +++ b/lib/gitlab/ci/jwt_v2.rb @@ -8,7 +8,9 @@ class JwtV2 < Jwt GITLAB_HOSTED_RUNNER = 'gitlab-hosted' SELF_HOSTED_RUNNER = 'self-hosted' - def self.for_build(build, aud:, sub_components: [:project_path, :ref_type, :ref], target_audience: nil) + def self.for_build( + build, aud:, sub_components: [:project_path, :ref_type, + :ref], target_audience: nil) new(build, ttl: build.metadata_timeout, aud: aud, sub_components: sub_components, target_audience: target_audience).encoded end diff --git a/lib/gitlab/usage_data_counters/hll_redis_legacy_events.yml b/lib/gitlab/usage_data_counters/hll_redis_legacy_events.yml index f84348b05ed3105178c87bc514f40acca9abb58a..b95da54b559f338fc52ad7eab28091a26ebd4e91 100644 --- a/lib/gitlab/usage_data_counters/hll_redis_legacy_events.yml +++ b/lib/gitlab/usage_data_counters/hll_redis_legacy_events.yml @@ -42,6 +42,7 @@ - i_ci_secrets_management_gcp_secret_manager_build_created - i_ci_secrets_management_id_tokens_build_created - i_ci_secrets_management_vault_build_created +- i_ci_secrets_management_gitlab_secrets_manager_build_created - i_code_review_click_diff_view_setting - i_code_review_click_file_browser_setting - i_code_review_click_single_file_mode_setting