diff --git a/.rubocop_todo/rspec/feature_category.yml b/.rubocop_todo/rspec/feature_category.yml index 1ed9764798df6dca56235ee0a610e72821566e41..3913bd0cf9f1d68fd6e0d3a4045a84349ffa5718 100644 --- a/.rubocop_todo/rspec/feature_category.yml +++ b/.rubocop_todo/rspec/feature_category.yml @@ -529,6 +529,8 @@ RSpec/FeatureCategory: - 'ee/spec/lib/gitlab/ci/config/entry/dast_configuration_spec.rb' - 'ee/spec/lib/gitlab/ci/config/entry/vault/engine_spec.rb' - 'ee/spec/lib/gitlab/ci/config/entry/vault/secret_spec.rb' + - 'ee/spec/lib/gitlab/ci/config/entry/aws_ssm_parameter_store/secret_spec.rb' + - 'ee/spec/lib/gitlab/ci/config/entry/aws_secrets_manager/secret_spec.rb' - 'ee/spec/lib/gitlab/ci/parsers/license_compliance/license_scanning_spec.rb' - 'ee/spec/lib/gitlab/ci/parsers/metrics/generic_spec.rb' - 'ee/spec/lib/gitlab/ci/parsers/security/cluster_image_scanning_spec.rb' diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json index 3fee6762423fe6c09972cf94abe42055fd926c95..1e8afb5b864fa4d88e872f239611462528f5f2db 100644 --- a/app/assets/javascripts/editor/schema/ci.json +++ b/app/assets/javascripts/editor/schema/ci.json @@ -1028,6 +1028,52 @@ ], "additionalProperties": false }, + "aws_secrets_manager": { + "oneOf": [ + { + "type": "string", + "description": "The ARN or name of the secret to retrieve. To retrieve a secret from another account, you must use an ARN." + }, + { + "type": "object", + "markdownDescription": "Defines the secret to be fetched from AWS Secrets Manager. The secret_id refers to the ARN or name of the secret in AWS Secrets Manager. Version_id and version_stage are optional parameters that can be used to specify a specific version of the secret, else AWSCURRENT version will be returned.", + "properties": { + "secret_id": { + "type": "string", + "description": "The ARN or name of the secret to retrieve. To retrieve a secret from another account, you must use an ARN." + }, + "version_id": { + "type": "string", + "description": "The unique identifier of the version of the secret to retrieve. If you include both this parameter and VersionStage, the two parameters must refer to the same secret version. If you don't specify either a VersionStage or VersionId, Secrets Manager returns the AWSCURRENT version." + }, + "version_stage": { + "type": "string", + "description": "The staging label of the version of the secret to retrieve. If you include both this parameter and VersionStage, the two parameters must refer to the same secret version. If you don't specify either a VersionStage or VersionId, Secrets Manager returns the AWSCURRENT version." + }, + "region": { + "type": "string", + "description": "The AWS region where the secret is stored. Use this to override the region for a specific secret. Defaults to AWS_REGION variable." + }, + "role_arn": { + "type": "string", + "description": "The ARN of the IAM role to assume before retrieving the secret. Use this to override the ARN. Defaults to AWS_ROLE_ARN variable." + }, + "role_session_name": { + "type": "string", + "description": "The name of the session to use when assuming the role. Use this to override the session name. Defaults to AWS_ROLE_SESSION_NAME variable." + }, + "field": { + "type": "string", + "description": "The name of the field to retrieve from the secret. If not specified, the entire secret is retrieved." + } + }, + "required": [ + "secret_id" + ], + "additionalProperties": false + } + ] + }, "akeyless": { "type": "object", "properties": { @@ -1057,6 +1103,35 @@ "token": { "type": "string", "description": "Specifies the JWT variable that should be used to authenticate with the secret provider." + }, + "aws_ssm_parameter_store": { + "oneOf": [ + { + "type": "string", + "markdownDescription": "The secret to be fetched from AWS SSM Parameter Store (e.g. 'production/db/password'). [Learn More](https://docs.gitlab.com/ci/yaml/#secretsawsssmparameterstore)" + }, + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "region": { + "type": "string" + }, + "role_arn": { + "type": "string" + }, + "role_session_name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + ] } }, "anyOf": [ @@ -1075,10 +1150,20 @@ "gcp_secret_manager" ] }, + { + "required": [ + "aws_secrets_manager" + ] + }, { "required": [ "akeyless" ] + }, + { + "required": [ + "aws_ssm_parameter_store" + ] } ], "dependencies": { diff --git a/app/validators/json_schemas/build_metadata_secrets.json b/app/validators/json_schemas/build_metadata_secrets.json index f9122ff811c4f0c0e035c7b724dbe44cbf41f1dc..74ca6db612a7a4a2896b704f70eda24995c9e576 100644 --- a/app/validators/json_schemas/build_metadata_secrets.json +++ b/app/validators/json_schemas/build_metadata_secrets.json @@ -87,6 +87,54 @@ }, "additionalProperties": false }, + "^aws_secrets_manager$": { + "type": "object", + "required": [ + "secret_id" + ], + "properties": { + "secret_id": { + "type": "string" + }, + "version_id": { + "type": [ + "string", + "null" + ] + }, + "version_stage": { + "type": [ + "string", + "null" + ] + }, + "region": { + "type": [ + "string", + "null" + ] + }, + "role_arn": { + "type": [ + "string", + "null" + ] + }, + "role_session_name": { + "type": [ + "string", + "null" + ] + }, + "field": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, "^akeyless$": { "type": "object", "properties": { @@ -215,6 +263,11 @@ "required": [ "gitlab_secrets_manager" ] + }, + { + "required": [ + "aws_secrets_manager" + ] } ], "additionalProperties": false diff --git a/ee/app/models/ci/secrets/integration.rb b/ee/app/models/ci/secrets/integration.rb index 981bfa211c3c2d42334adedfac6868409830afed..4784735b34cdd8a4b037628e1a901ce8c3bc5302 100644 --- a/ee/app/models/ci/secrets/integration.rb +++ b/ee/app/models/ci/secrets/integration.rb @@ -8,7 +8,9 @@ class Integration "akeyless" => :akeyless, "gcp_secret_manager" => :gcp_secret_manager, "vault" => :hashicorp_vault, - "gitlab_secrets_manager" => :gitlab_secrets_manager + "gitlab_secrets_manager" => :gitlab_secrets_manager, + "aws_secrets_manager" => :aws, + "aws_ssm_parameter_store" => :aws }.freeze def initialize(variables:, project:) @@ -60,6 +62,10 @@ def akeyless? variables['AKEYLESS_ACCESS_ID']&.value.present? end + def aws? + variables['AWS_REGION']&.value.present? + end + def gitlab_secrets_manager? # TODO: figure out context for whether GitLab Secrets Manager is # globally enabled on this instance. diff --git a/ee/app/presenters/ee/ci/build_runner_presenter.rb b/ee/app/presenters/ee/ci/build_runner_presenter.rb index 26fbfa34593481b0ea6b5d9f50ea1412a244ccf2..a1edbacf44af1ab48895d5cc0420a9331e25d236 100644 --- a/ee/app/presenters/ee/ci/build_runner_presenter.rb +++ b/ee/app/presenters/ee/ci/build_runner_presenter.rb @@ -15,6 +15,10 @@ def secrets_configuration secret['akeyless']['server'] = akeyless_server(secret) end + if ::Feature.enabled?(:aws_secret_manager, project) && (secret['aws_secrets_manager']) + secret['aws_secrets_manager']['server'] = aws_secrets_manager_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') @@ -56,6 +60,15 @@ def vault_server(secret) } end + def aws_secrets_manager_server(secret) + @aws_secrets_manager_server ||= { + 'region' => variables['AWS_REGION']&.value, + 'jwt' => aws_token(secret), + 'role_arn' => variables['AWS_ROLE_ARN']&.value, + 'role_session_name' => variables['AWS_ROLE_SESSION_NAME']&.value + } + end + def gitlab_secrets_manager_server(psm) @gitlab_secrets_manager_server ||= { 'url' => SecretsManagement::ProjectSecretsManager.server_url, @@ -82,6 +95,10 @@ def id_token_var(secret) secret['token'] || "$#{id_tokens.each_key.first}" end + def aws_token(secret) + secret['token'] || '$AWS_ID_TOKEN' + end + def gcp_secret_manager_server(secret) @gcp_secret_manager_server ||= { 'project_number' => variables['GCP_PROJECT_NUMBER']&.value, diff --git a/ee/config/feature_flags/development/aws_secret_manager.yml b/ee/config/feature_flags/development/aws_secret_manager.yml new file mode 100644 index 0000000000000000000000000000000000000000..05e1c0e7688d12cb46e92127c44816894835f359 --- /dev/null +++ b/ee/config/feature_flags/development/aws_secret_manager.yml @@ -0,0 +1,9 @@ +--- +name: aws_secret_manager +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/191761 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/542277 +feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/17822 +milestone: '18.1' +type: development +group: group::pipeline security +default_enabled: false diff --git a/ee/config/metrics/counts_28d/count_total_create_secrets_aws_secrets_manager_monthly.yml b/ee/config/metrics/counts_28d/count_total_create_secrets_aws_secrets_manager_monthly.yml new file mode 100644 index 0000000000000000000000000000000000000000..f4ef3bd8e66061ae30e2dc7beffc54e77bdf99da --- /dev/null +++ b/ee/config/metrics/counts_28d/count_total_create_secrets_aws_secrets_manager_monthly.yml @@ -0,0 +1,19 @@ +--- +data_category: optional +key_path: redis_hll_counters.ci_secrets_management.i_ci_secrets_management_aws_secrets_manager_build_created_monthly +description: Monthly active users creating pipelines that that have the AWS SecretsManager 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_aws_secrets_manager_build_created +tiers: + - premium + - ultimate +performance_indicator_type: [] +milestone: "16.9" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/191905 diff --git a/ee/lib/gitlab/ci/config/entry/aws_secrets_manager/secret.rb b/ee/lib/gitlab/ci/config/entry/aws_secrets_manager/secret.rb new file mode 100644 index 0000000000000000000000000000000000000000..4f86fff8f8f8accbcbb12e61506c753701932973 --- /dev/null +++ b/ee/lib/gitlab/ci/config/entry/aws_secrets_manager/secret.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + module AwsSecretsManager + ## + # Entry that represents AWS SSM ParameterStore. + # + class Secret < ::Gitlab::Config::Entry::Simplifiable + strategy :StringStrategy, if: ->(config) { config.is_a?(String) } + strategy :HashStrategy, if: ->(config) { config.is_a?(Hash) } + + class UnknownStrategy < ::Gitlab::Config::Entry::Node + def errors + ["#{location} should be a hash or a string"] + end + end + + class StringStrategy < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + + validations do + validates :config, presence: true + validates :config, type: String, + format: { with: /\A[^#]*(#[^#]*)?\z/, message: "must contain at most one '#'" } + end + + def value + # expect to return 1 element for secret without field and 2 elements for secret with field + parts = config.split('#') + + # input "/my/secret" + if parts.size == 1 + { + secret_id: parts[0] + } + # input "/my/secret#field" + else + { + secret_id: parts[0], + field: parts[1] + } + end + end + end + + class HashStrategy < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable + + ALLOWED_KEYS = %i[secret_id region version_id version_stage role_arn role_session_name field].freeze + + attributes ALLOWED_KEYS + + validations do + validates :config, type: Hash, allowed_keys: ALLOWED_KEYS + # Required fields + validates :secret_id, type: String, presence: true + + # Optional fields + validates :region, type: String, allow_nil: true + validates :version_id, type: String, allow_nil: true + validates :version_stage, type: String, allow_nil: true + validates :role_arn, type: String, allow_nil: true + validates :role_session_name, type: String, allow_nil: true + validates :field, type: String, allow_nil: true + end + + def value + { + secret_id: secret_id, + version_id: version_id, + version_stage: version_stage, + region: region, + role_arn: role_arn, + field: field, + role_session_name: role_session_name + } + end + end + end + end + end + end + end +end diff --git a/ee/lib/gitlab/ci/config/entry/aws_ssm_parameter_store/secret.rb b/ee/lib/gitlab/ci/config/entry/aws_ssm_parameter_store/secret.rb new file mode 100644 index 0000000000000000000000000000000000000000..ce732b97611bd6a820558d8a963f1d79c0a2e2fb --- /dev/null +++ b/ee/lib/gitlab/ci/config/entry/aws_ssm_parameter_store/secret.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + module AwsSsmParameterStore + ## + # Entry that represents AWS SSM ParameterStore. + # + class Secret < ::Gitlab::Config::Entry::Simplifiable + strategy :StringStrategy, if: ->(config) { config.is_a?(String) } + strategy :HashStrategy, if: ->(config) { config.is_a?(Hash) } + + class UnknownStrategy < ::Gitlab::Config::Entry::Node + def errors + ["#{location} should be a hash or a string"] + end + end + + class StringStrategy < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + + validations do + validates :config, presence: true + validates :config, type: String + end + + def value + { + name: config + } + end + end + + class HashStrategy < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable + + ALLOWED_KEYS = %i[name region role_arn role_session_name].freeze + + attributes ALLOWED_KEYS + + validations do + validates :config, type: Hash, allowed_keys: ALLOWED_KEYS + validates :name, type: String, presence: true + validates :region, type: String, allow_nil: true + validates :role_arn, type: String, allow_nil: true + validates :role_session_name, type: String, allow_nil: true + end + + def value + { + name: name, + region: region, + role_arn: role_arn, + role_session_name: role_session_name + } + end + 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 bf91e5b33b9eadf0ee344442c9e686f09926ed6c..f8ec2612a0d1a4e7fdf00e5e7514238871dc9181 100644 --- a/ee/lib/gitlab/ci/config/entry/secret.rb +++ b/ee/lib/gitlab/ci/config/entry/secret.rb @@ -11,18 +11,23 @@ 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 gitlab_secrets_manager token].freeze - SUPPORTED_PROVIDERS = %i[vault azure_key_vault gcp_secret_manager akeyless gitlab_secrets_manager].freeze + ALLOWED_KEYS = %i[vault file azure_key_vault gcp_secret_manager akeyless gitlab_secrets_manager token + aws_secrets_manager aws_ssm_parameter_store].freeze + SUPPORTED_PROVIDERS = %i[vault azure_key_vault gcp_secret_manager akeyless gitlab_secrets_manager + aws_secrets_manager aws_ssm_parameter_store].freeze attributes ALLOWED_KEYS entry :vault, Entry::Vault::Secret, description: 'Vault secrets engine configuration' + entry :aws_secrets_manager, Entry::AwsSecretsManager::Secret, description: 'AWS engine configuration' entry :file, ::Gitlab::Config::Entry::Boolean, description: 'Should the created variable be of file type' 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' + entry :aws_ssm_parameter_store, Entry::AwsSsmParameterStore::Secret, + description: 'AWS SSM Parameter Store configuration' validations do validates :config, allowed_keys: ALLOWED_KEYS, only_one_of_keys: SUPPORTED_PROVIDERS @@ -37,9 +42,11 @@ def value { vault: vault_value, gitlab_secrets_manager: gitlab_secrets_manager_value, + aws_secrets_manager: aws_secrets_manager_value, gcp_secret_manager: gcp_secret_manager_value, azure_key_vault: azure_key_vault_value, akeyless: akeyless_value, + aws_ssm_parameter_store: aws_ssm_parameter_store_value, file: file_value, token: token }.compact diff --git a/ee/spec/lib/gitlab/ci/config/entry/aws_secrets_manager/secret_spec.rb b/ee/spec/lib/gitlab/ci/config/entry/aws_secrets_manager/secret_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b1495e5c6e08e8fc0e347cca4a0e42006400013d --- /dev/null +++ b/ee/spec/lib/gitlab/ci/config/entry/aws_secrets_manager/secret_spec.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Entry::AwsSecretsManager::Secret do + let(:entry) { described_class.new(config) } + + describe 'validation' do + before do + entry.compose! + end + + context 'when entry config value is correct' do + let(:hash_config) do + { + secret_id: 'production/db/password', + version_id: 'version_id', + version_stage: 'AWSCURRENT', + field: 'some_field', + region: 'us-east-1', + role_arn: 'arn:aws:iam::123456789012:role/role-name', + role_session_name: 'session-name' + } + end + + let(:hash_config_minimal) do + { + secret_id: 'production/db/password' + } + end + + context 'when config is a hash' do + let(:config) { hash_config } + + describe '#value' do + it 'returns AWS SecretsManager configuration' do + expect(entry.value).to eq(hash_config) + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when config is a string' do + let(:config) { 'production/db/password' } + + describe '#value' do + it 'returns AWS SecretsManager secret configuration' do + expect(entry.value).to eq(hash_config_minimal) + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when config is a string with field' do + let(:config) { 'production/db/password#field' } + + describe '#value' do + it 'returns AWS SecretsManager secret configuration' do + expect(entry.value).to eq({ + secret_id: 'production/db/password', + field: 'field' + }) + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + end + end + + context 'when entry value is not correct' do + describe '#errors' do + context 'when there is an unknown key present' do + let(:config) { { foo: :bar } } + + it 'reports error' do + expect(entry.errors) + .to include 'hash strategy config contains unknown keys: foo' + end + end + + context 'when config is a string and contains more than one #' do + let(:config) { 'production/db/password#field#foo' } + + it 'reports error' do + puts entry.errors + expect(entry.errors) + .to include "string strategy config must contain at most one '#'" + end + end + + context 'when name is not present' do + let(:config) { {} } + + it 'reports error' do + puts entry.errors + expect(entry.errors) + .to include 'hash strategy secret can\'t be blank' + end + end + + context 'when secret_id is is blank' do + let(:config) { { secret_id: '' } } + + it 'reports error' do + expect(entry.errors) + .to include "hash strategy secret can't be blank" + end + end + end + end +end diff --git a/ee/spec/lib/gitlab/ci/config/entry/aws_ssm_parameter_store/secret_spec.rb b/ee/spec/lib/gitlab/ci/config/entry/aws_ssm_parameter_store/secret_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..051fcd3a8647a8544a3496b5e47096a52500f56e --- /dev/null +++ b/ee/spec/lib/gitlab/ci/config/entry/aws_ssm_parameter_store/secret_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Entry::AwsSsmParameterStore::Secret do + let(:entry) { described_class.new(config) } + + describe 'validation' do + before do + entry.compose! + end + + context 'when entry config value is correct' do + let(:hash_config) do + { + name: 'production/db/password', + region: 'us-east-1', + role_arn: 'arn:aws:iam::123456789012:role/role-name', + role_session_name: 'session-name' + } + end + + let(:hash_config_minimal) do + { + name: 'production/db/password' + } + end + + context 'when config is a hash' do + let(:config) { hash_config } + + describe '#value' do + it 'returns AWS ParameterStore configuration' do + expect(entry.value).to eq(hash_config) + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when config is a string' do + let(:config) { 'production/db/password' } + + describe '#value' do + it 'returns AWS ParameterStore secret configuration' do + expect(entry.value).to eq(hash_config_minimal) + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + end + end + + context 'when entry value is not correct' do + describe '#errors' do + context 'when there is an unknown key present' do + let(:config) { { foo: :bar } } + + it 'reports error' do + expect(entry.errors) + .to include 'hash strategy config contains unknown keys: foo' + end + end + + context 'when name is not present' do + let(:config) { {} } + + it 'reports error' do + puts entry.errors + expect(entry.errors) + .to include 'hash strategy name can\'t be blank' + end + end + + context 'when name is is empty' do + let(:config) { { name: "" } } + + it 'reports error' do + expect(entry.errors) + .to include 'hash strategy name can\'t be blank' + end + end + + context 'when optional values are blank' do + let(:config) do + { + name: 'production/db/password', + region: '', + role_arn: '', + role_session_name: '' + } + end + + it 'reports error' do + [ + "hash strategy region can't be blank", + "hash strategy role arn can't be blank", + "hash strategy role session name can't be blank" + ].each do |error| + expect(entry.errors).to include error + end + end + 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 e9035502c65700d2ddadc217deb9f57e03ba0208..0fad2cd1ba20cb4477943c889daa3ee9fd5af049 100644 --- a/ee/spec/lib/gitlab/ci/config/entry/secret_spec.rb +++ b/ee/spec/lib/gitlab/ci/config/entry/secret_spec.rb @@ -90,6 +90,95 @@ end end + context 'for AWS Secrets Manager' do + context 'when `token` is defined' do + let(:config) do + { + aws_secrets_manager: { + secret_id: 'name' + }, + token: '$TEST_ID_TOKEN' + } + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'with optional fields' do + let(:config) do + { + aws_secrets_manager: { + secret_id: 'db-password', + region: 'us-east-1', + version_id: 'abcdef1234567890', + version_stage: 'AWSCURRENT', + role_arn: 'arn:aws:iam::123456789012:role/role-name', + field: 'password', + role_session_name: 'session-name' + } + } + end + + it 'is valid' do + expect(entry).to be_valid + end + end + + context 'with invalid configuration' do + context 'when secret_id is missing' do + let(:config) do + { + aws_secrets_manager: {} + } + end + + it 'is not valid' do + expect(entry).not_to be_valid + expect(entry.errors).to include(/aws_secrets_manager secret can't be blank/) + end + end + end + + context 'when `token` is not defined' do + let(:config) do + { + aws_secrets_manager: { + secret_id: 'name', + region: 'eu-central-1' + } + } + end + + describe '#value' do + it 'returns secret configuration' do + expect(entry.value).to eq( + { + aws_secrets_manager: { + secret_id: 'name', + region: 'eu-central-1', + version_id: nil, + version_stage: nil, + role_arn: nil, + field: nil, + role_session_name: nil + } + } + ) + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + end + context 'for Azure Key Vault' do context 'when `token` is defined' do let(:config) do @@ -329,6 +418,66 @@ end end end + + context 'for AWS SSM ParameterStore' do + context 'when `token` is defined' do + let(:config) do + { + aws_ssm_parameter_store: { + name: 'name' + }, + token: '$TEST_ID_TOKEN' + } + end + + describe '#value' do + it 'returns secret configuration' do + expect(entry.value).to eq( + { + aws_ssm_parameter_store: { + name: 'name' + }, + token: '$TEST_ID_TOKEN' + } + ) + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when `token` is not defined' do + let(:config) do + { + aws_ssm_parameter_store: { + name: 'name' + } + } + end + + describe '#value' do + it 'returns secret configuration' do + expect(entry.value).to eq( + { + aws_ssm_parameter_store: { + name: 'name' + } + } + ) + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + end end end @@ -349,7 +498,8 @@ 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, gitlab_secrets_manager' + 'vault, azure_key_vault, gcp_secret_manager, akeyless, gitlab_secrets_manager, ' \ + 'aws_secrets_manager, aws_ssm_parameter_store' end end @@ -360,7 +510,7 @@ it 'reports error' do expect(entry.errors) .to include "secret config must use exactly one of these keys: " \ - "#{Gitlab::Ci::Config::Entry::Secret::SUPPORTED_PROVIDERS.join(', ')}" + "#{Gitlab::Ci::Config::Entry::Secret::SUPPORTED_PROVIDERS.join(', ')}" 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 45fc0e86860e2b9f0a19562014e2d97e9af32cb6..7f59327408d9843ca851a87424d0d2e79ee0fb2c 100644 --- a/ee/spec/presenters/ci/build_runner_presenter_spec.rb +++ b/ee/spec/presenters/ci/build_runner_presenter_spec.rb @@ -223,7 +223,6 @@ it 'adds the first ID token to the Vault server payload' do jwt = presenter.secrets_configuration.dig('DATABASE_PASSWORD', 'azure_key_vault', 'server', 'jwt') - expect(jwt).to eq('$VAULT_ID_TOKEN_1') end @@ -257,6 +256,99 @@ end end + context 'with AWS Secrets Manager' do + let(:secrets) do + { + DATABASE_PASSWORD: { + aws_secrets_manager: { + secret_id: 'key', + version_id: 'version' + } + } + } + end + + let(:aws_secrets_manager_server) { presenter.secrets_configuration.dig('DATABASE_PASSWORD', 'aws_secrets_manager', 'server') } + + context 'Secrets Manager Region' do + context 'AWS_REGION CI variable is present' do + it 'returns the Region' do + create(:ci_variable, project: ci_build.project, key: 'AWS_REGION', value: 'test') + + expect(aws_secrets_manager_server.fetch('region')).to eq('test') + end + end + + context 'AWS_REGION CI variable is not present' do + it 'returns the nil' do + expect(aws_secrets_manager_server.fetch('region')).to eq(nil) + end + end + end + + context 'Vault tenant id' do + context 'AWS_ROLE_ARN CI variable is present' do + it 'returns the URL' do + create(:ci_variable, project: ci_build.project, key: 'AWS_ROLE_ARN', value: 'test') + + expect(aws_secrets_manager_server.fetch('role_arn')).to eq('test') + end + end + + context 'AWS_ROLE_ARN CI variable is not present' do + it 'returns the nil' do + expect(aws_secrets_manager_server.fetch('role_arn')).to eq(nil) + end + end + end + + context 'when there are ID tokens available' do + before do + rsa_key = OpenSSL::PKey::RSA.generate(3072).to_s + stub_application_setting(ci_jwt_signing_key: rsa_key) + ci_build.id_tokens = { + 'VAULT_ID_TOKEN_2' => { id_token: { aud: 'https://gitlab.link' } }, + 'AWS_ID_TOKEN' => { id_token: { aud: 'https://gitlab.test' } } + } + ci_build.runner = build_stubbed(:ci_runner) + end + + it 'adds the AWS_ID_TOKEN to the Vault server payload' do + jwt = presenter.secrets_configuration.dig('DATABASE_PASSWORD', 'aws_secrets_manager', 'server', 'jwt') + + expect(jwt).to eq('$AWS_ID_TOKEN') + end + + context 'when the token variable is specified for the vault secret' do + let(:secrets) do + { + DATABASE_PASSWORD: { + token: '$VAULT_ID_TOKEN_2', + aws_secrets_manager: { + secret_id: 'key', + version_id: 'version' + } + } + } + end + + it 'uses the specified token variable' do + jwt = presenter.secrets_configuration.dig('DATABASE_PASSWORD', 'aws_secrets_manager', 'server', 'jwt') + + expect(jwt).to eq('$VAULT_ID_TOKEN_2') + end + end + end + + context 'when there are no ID tokens available' do + it 'returns nil so instance profile could be used' do + jwt = presenter.secrets_configuration.dig('DATABASE_PASSWORD', 'aws_secrets_manager', 'server', 'jwt') + + expect(jwt).to be_nil + end + end + end + context 'with GCP Secret Manager' do let(:secrets) do { 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 e12b15f6d392c2f2b52d65c14281f7a3ea6f7a2d..5e8fbf1e788d4dcc9aea92b0be00c6c68f41e23b 100644 --- a/lib/gitlab/usage_data_counters/hll_redis_legacy_events.yml +++ b/lib/gitlab/usage_data_counters/hll_redis_legacy_events.yml @@ -43,6 +43,8 @@ - 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_ci_secrets_management_aws_secrets_manager_build_created +- i_ci_secrets_management_aws_ssm_parameter_store_build_created - i_code_review_click_diff_view_setting - i_code_review_click_file_browser_setting - i_code_review_click_single_file_mode_setting diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/secrets.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/secrets.yml index 23d667eeeffaef3f94f677b3ff79b9ac1d329537..829492ccfb7a73c4b1c64122e2a6fa706c99267f 100644 --- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/secrets.yml +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/secrets.yml @@ -89,3 +89,27 @@ job_with_gcp_secret_manager_secret_without_token: gcp_secret_manager: name: my-secret + +job_with_aws_secrets_manager_secret_without_secret_id: + script: + - echo $TEST_DB_PASSWORD + secrets: + TEST_DB_PASSWORD: + aws_secrets_manager: + region: "us-east-1" + +job_with_aws_secrets_manager_secret_without_region: + script: + - echo $TEST_DB_PASSWORD + secrets: + TEST_DB_PASSWORD: + aws_secrets_manager: + secret_id: "database_password" + +job_with_secrets_with_invalid_aws_ssm_parameter_store_property: + script: + - echo $TEST_DB_PASSWORD + secrets: + TEST_DB_PASSWORD: + aws_ssm_parameter_store: + invalid: TEST \ No newline at end of file diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/secrets.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/secrets.yml index e615fa52dc5e36a0c1fc372767eccc35e1a87662..6090f4cb6090cd7760584b247359d67735c9e003 100644 --- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/secrets.yml +++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/secrets.yml @@ -72,3 +72,60 @@ valid_job_with_gcp_secret_manager_name_and_string_version: name: 'test' version: 'latest' token: $TEST_TOKEN + +valid_job_with_aws_secretsmanager_secret_id: + aws_secrets_manager: + secret_id: 'test' + token: $TEST_TOKEN + +valid_job_with_aws_secretsmanager_secret_id_short: + aws_secrets_manager: 'test' + token: $TEST_TOKEN + +valid_job_with_aws_secretsmanager_secret_id_and_version_id: + script: + - echo $TEST_DB_PASSWORD + secrets: + TEST_DB_PASSWORD: + aws_ssm_parameter_store: + name: test-db-password + region: us-east-1 + role_arn: arn:aws:iam::123456789012:role/role-name + role_session_name: role-session-name + token: $TEST_TOKEN + aws_secrets_manager: + secret_id: 'test' + version_id: "2" + region: 'us-east-1' + token: $TEST_TOKEN + +valid_job_with_aws_secretsmanager_secret_id_and_version_stage: + script: + - echo $TEST_DB_PASSWORD + secrets: + TEST_DB_PASSWORD: + aws_secrets_manager: + secret_id: 'test' + region: "us-east-1" + version_stage: 'AWSCURRENT' + token: $TEST_TOKEN + +valid_job_with_aws_secretsmanager_with_every_keyword: + script: + - echo $TEST_DB_PASSWORD + secrets: + TEST_DB_PASSWORD: + aws_secrets_manager: + secret_id: 'test' + region: "us-east-1" + version_stage: 'AWSCURRENT' + version_id: "2" + token: $TEST_TOKEN + +valid_job_with_secrets_with_aws_ssm_parameter_store_keyword_string: + script: + - echo $TEST_DB_PASSWORD + secrets: + TEST_DB_PASSWORD: + aws_ssm_parameter_store: test-db-password + token: $TEST_TOKEN \ No newline at end of file