diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json index 21c50acbf5d934ed0275deb0a9194c6c63ae565e..2e0d3a86388b18f3dda2b681747be474fc8fe164 100644 --- a/app/assets/javascripts/editor/schema/ci.json +++ b/app/assets/javascripts/editor/schema/ci.json @@ -607,53 +607,65 @@ "secrets": { "type": "object", "markdownDescription": "Defines secrets to be injected as environment variables. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#secrets).", - "additionalProperties": { - "type": "object", - "description": "Environment variable name", - "properties": { - "vault": { - "oneOf": [ - { - "type": "string", - "description": "The secret to be fetched from Vault (e.g. 'production/db/password@ops' translates to secret 'ops/data/production/db', field `password`)" - }, - { - "type": "object", - "properties": { - "engine": { - "type": "object", - "properties": { - "name": { - "type": "string" + "patternProperties": { + ".*": { + "type": "object", + "properties": { + "vault": { + "oneOf": [ + { + "type": "string", + "markdownDescription": "The secret to be fetched from Vault (e.g. 'production/db/password@ops' translates to secret 'ops/data/production/db', field `password`). [Learn More](https://docs.gitlab.com/ee/ci/yaml/#secretsvault)" + }, + { + "type": "object", + "properties": { + "engine": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + } }, - "path": { - "type": "string" - } + "required": [ + "name", + "path" + ] }, - "required": [ - "name", - "path" - ] - }, - "path": { - "type": "string" + "path": { + "type": "string" + }, + "field": { + "type": "string" + } }, - "field": { - "type": "string" - } - }, - "required": [ - "engine", - "path", - "field" - ] - } - ] - } - }, - "required": [ - "vault" - ] + "required": [ + "engine", + "path", + "field" + ], + "additionalProperties": false + } + ] + }, + "file": { + "type": "boolean", + "default": true, + "markdownDescription": "Configures the secret to be stored as either a file or variable type CI/CD variable. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#secretsfile)" + }, + "token": { + "type": "string", + "description": "Specifies the JWT variable that should be used to authenticate with Hashicorp Vault." + } + }, + "required": [ + "vault" + ], + "additionalProperties": false + } } }, "before_script": { @@ -1922,4 +1934,4 @@ "additionalProperties": false } } -} +} \ No newline at end of file diff --git a/ee/app/presenters/ee/ci/build_runner_presenter.rb b/ee/app/presenters/ee/ci/build_runner_presenter.rb index 254913e97be02142eccae74fd43e4a9294de227e..bbbd647d07e316e404cd09261c22cc4ad13eb319 100644 --- a/ee/app/presenters/ee/ci/build_runner_presenter.rb +++ b/ee/app/presenters/ee/ci/build_runner_presenter.rb @@ -40,9 +40,7 @@ def vault_jwt(secret) def id_token_var(secret) return unless id_tokens? - token = secret['token'] || id_tokens.each_key.first - - "${#{token}}" + secret['token'] || "$#{id_tokens.each_key.first}" 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..2914c7133cd66dfcb7263d659648c25dc065ac32 100644 --- a/ee/lib/gitlab/ci/config/entry/secret.rb +++ b/ee/lib/gitlab/ci/config/entry/secret.rb @@ -12,7 +12,7 @@ class Secret < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Attributable REQUIRED_KEYS = %i[vault].freeze - ALLOWED_KEYS = (REQUIRED_KEYS + %i[file]).freeze + ALLOWED_KEYS = (REQUIRED_KEYS + %i[file token]).freeze attributes ALLOWED_KEYS @@ -21,6 +21,15 @@ class Secret < ::Gitlab::Config::Entry::Node validations do validates :config, allowed_keys: ALLOWED_KEYS, required_keys: REQUIRED_KEYS + validates :token, type: String, allow_nil: true + end + + def value + { + vault: vault_value, + file: file_value, + token: token + }.compact 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 a7c63059b7fa70339439effd69acf7b5f989f4cc..023cfbde9b618db8b41f1857d2ba2de60a47dea1 100644 --- a/ee/spec/lib/gitlab/ci/config/entry/secret_spec.rb +++ b/ee/spec/lib/gitlab/ci/config/entry/secret_spec.rb @@ -53,6 +53,40 @@ it_behaves_like 'configures secrets' end + + context 'when `token` is defined' do + let(:config) do + { + vault: { + engine: { name: 'kv-v2', path: 'kv-v2' }, + path: 'production/db', + field: 'password' + }, + token: '$TEST_ID_TOKEN' + } + end + + describe '#value' do + it 'returns secret configuration' do + expect(entry.value).to eq( + { + vault: { + engine: { name: 'kv-v2', path: 'kv-v2' }, + path: 'production/db', + field: 'password' + }, + token: '$TEST_ID_TOKEN' + } + ) + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + 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 a20ed9ab8e0af005fe88e99d80663138b0d204ca..9a1ed90d98100744cfc72dbef73fd377b1a948c1 100644 --- a/ee/spec/presenters/ci/build_runner_presenter_spec.rb +++ b/ee/spec/presenters/ci/build_runner_presenter_spec.rb @@ -127,7 +127,7 @@ it 'adds the first ID token to the Vault server payload' do jwt = presenter.secrets_configuration.dig('DATABASE_PASSWORD', 'vault', 'server', 'auth', 'data', 'jwt') - expect(jwt).to eq('${VAULT_ID_TOKEN_1}') + expect(jwt).to eq('$VAULT_ID_TOKEN_1') end context 'when the token variable is specified for the vault secret' do @@ -135,7 +135,7 @@ { DATABASE_PASSWORD: { file: true, - token: 'VAULT_ID_TOKEN_2', + token: '$VAULT_ID_TOKEN_2', vault: { engine: { name: 'kv-v2', path: 'kv-v2' }, path: 'production/db', @@ -148,7 +148,7 @@ it 'uses the specified token variable' do jwt = presenter.secrets_configuration.dig('DATABASE_PASSWORD', 'vault', 'server', 'auth', 'data', 'jwt') - expect(jwt).to eq('${VAULT_ID_TOKEN_2}') + expect(jwt).to eq('$VAULT_ID_TOKEN_2') end end end diff --git a/ee/spec/services/ci/create_pipeline_service_spec.rb b/ee/spec/services/ci/create_pipeline_service_spec.rb index 78c4d39245422e3b7e509475c2046eca2c9dc149..b0302e456b00e7807c0f87ed7f9b7ca21e112e82 100644 --- a/ee/spec/services/ci/create_pipeline_service_spec.rb +++ b/ee/spec/services/ci/create_pipeline_service_spec.rb @@ -155,6 +155,7 @@ secrets: DATABASE_PASSWORD: vault: production/db/password + token: $ID_TOKEN YAML end @@ -172,7 +173,8 @@ 'engine' => { 'name' => 'kv-v2', 'path' => 'kv-v2' }, 'path' => 'production/db', 'field' => 'password' - } + }, + 'token' => '$ID_TOKEN' } }) end diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js index a06f81e4d1c9f233e4d6e10aa99dd7b9cfefafd3..b2680eb72f1dad0f8c18201092dfbaa31e363f95 100644 --- a/spec/frontend/editor/schema/ci/ci_schema_spec.js +++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js @@ -32,6 +32,7 @@ import VariablesYaml from './yaml_tests/positive_tests/variables.yml'; import JobWhenYaml from './yaml_tests/positive_tests/job_when.yml'; import IdTokensYaml from './yaml_tests/positive_tests/id_tokens.yml'; import HooksYaml from './yaml_tests/positive_tests/hooks.yml'; +import SecretsYaml from './yaml_tests/positive_tests/secrets.yml'; // YAML NEGATIVE TEST import ArtifactsNegativeYaml from './yaml_tests/negative_tests/artifacts.yml'; @@ -49,6 +50,7 @@ import VariablesInvalidSyntaxDescYaml from './yaml_tests/negative_tests/variable import VariablesWrongSyntaxUsageExpand from './yaml_tests/negative_tests/variables/wrong_syntax_usage_expand.yml'; import IdTokensNegativeYaml from './yaml_tests/negative_tests/id_tokens.yml'; import HooksNegative from './yaml_tests/negative_tests/hooks.yml'; +import SecretsNegativeYaml from './yaml_tests/negative_tests/secrets.yml'; const ajv = new Ajv({ strictTypes: false, @@ -86,6 +88,7 @@ describe('positive tests', () => { VariablesYaml, ProjectPathYaml, IdTokensYaml, + SecretsYaml, }), )('schema validates %s', (_, input) => { // We construct a new "JSON" from each main key that is inside a @@ -120,6 +123,7 @@ describe('negative tests', () => { ProjectPathIncludeLeadSlashYaml, ProjectPathIncludeNoSlashYaml, ProjectPathIncludeTailSlashYaml, + SecretsNegativeYaml, TriggerNegative, HooksNegative, }), 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 new file mode 100644 index 0000000000000000000000000000000000000000..14ba930b394c11cd57b40a8b7fd6ededd1eb0718 --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/secrets.yml @@ -0,0 +1,39 @@ +job_with_secrets_without_vault: + script: + - echo $TEST_DB_PASSWORD + secrets: + TEST_DB_PASSWORD: + token: $TEST_TOKEN + +job_with_secrets_with_extra_properties: + script: + - echo $TEST_DB_PASSWORD + secrets: + TEST_DB_PASSWORD: + vault: test/db/password + extra_prop: TEST + +job_with_secrets_with_invalid_vault_property: + script: + - echo $TEST_DB_PASSWORD + secrets: + TEST_DB_PASSWORD: + vault: + invalid: TEST + +job_with_secrets_with_missing_required_vault_property: + script: + - echo $TEST_DB_PASSWORD + secrets: + TEST_DB_PASSWORD: + vault: + path: gitlab + +job_with_secrets_with_missing_required_engine_property: + script: + - echo $TEST_DB_PASSWORD + secrets: + TEST_DB_PASSWORD: + vault: + engine: + path: kv 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 new file mode 100644 index 0000000000000000000000000000000000000000..083cb4348ed7db6618d544ed554b7627dfd8149b --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/secrets.yml @@ -0,0 +1,28 @@ +valid_job_with_secrets: + script: + - echo $TEST_DB_PASSWORD + secrets: + TEST_DB_PASSWORD: + vault: test/db/password + +valid_job_with_secrets_and_token: + script: + - echo $TEST_DB_PASSWORD + secrets: + TEST_DB_PASSWORD: + vault: test/db/password + token: $TEST_TOKEN + +valid_job_with_secrets_with_every_vault_keyword: + script: + - echo $TEST_DB_PASSWORD + secrets: + TEST_DB_PASSWORD: + vault: + engine: + name: test-engine + path: test + path: test/db + field: password + file: true + token: $TEST_TOKEN