diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md index 78db3be8cffe18c644b17296d3ce560ab2f230ff..416b1cbfd214c296301fd468784ce4f24139dcf2 100644 --- a/doc/user/compliance/audit_event_types.md +++ b/doc/user/compliance/audit_event_types.md @@ -113,6 +113,12 @@ Audit event types belong to the following product categories. |:----------|:---------------------|:------------------|:--------------|:------| | [`job_artifact_downloaded`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129608) | A user downloads a job artifact from a project | {{< icon name="dotted-circle" >}} No | GitLab [16.8](https://gitlab.com/gitlab-org/gitlab/-/issues/250663) | Project | +### Ci variables + +| Type name | Event triggered when | Saved to database | Introduced in | Scope | +|:----------|:---------------------|:------------------|:--------------|:------| +| [`variable_viewed_api`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/197385) | A CI/CD variable is accessed with the API | {{< icon name="check-circle" >}} Yes | GitLab [18.3](https://gitlab.com/gitlab-org/gitlab/-/issues/555960) | Project, Group | + ### Code review | Type name | Event triggered when | Saved to database | Introduced in | Scope | diff --git a/ee/config/audit_events/types/variable_viewed_api.yml b/ee/config/audit_events/types/variable_viewed_api.yml new file mode 100644 index 0000000000000000000000000000000000000000..cdac7e4f955cb8b6eb46ed263de01cdcea24708d --- /dev/null +++ b/ee/config/audit_events/types/variable_viewed_api.yml @@ -0,0 +1,10 @@ +--- +name: variable_viewed_api +description: A CI/CD variable is accessed with the API +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/555960 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/197385 +feature_category: ci_variables +milestone: "18.3" +saved_to_database: true +streamed: true +scope: [Project, Group] diff --git a/ee/lib/ee/api/helpers/variables_helpers.rb b/ee/lib/ee/api/helpers/variables_helpers.rb index 52280b7d4265d28c19268150df03c1bc3e021a69..e761a94938d1a6625d187b60a9908de73a938e2f 100644 --- a/ee/lib/ee/api/helpers/variables_helpers.rb +++ b/ee/lib/ee/api/helpers/variables_helpers.rb @@ -21,6 +21,21 @@ def filter_variable_parameters(owner, params) params end + + override :audit_variable_access + def audit_variable_access(variable, scope) + message = "CI/CD variable '#{variable.key}' accessed with the API" + message += " (hidden variable, no value shown)" if variable.hidden? + + audit_context = { + name: 'variable_viewed_api', + author: current_user, + scope: scope, + target: variable, + message: message + } + ::Gitlab::Audit::Auditor.audit(audit_context) + end end end end diff --git a/ee/spec/requests/api/ci/variables_spec.rb b/ee/spec/requests/api/ci/variables_spec.rb index d80fa460afab416d05e124eea958db7d1b64747a..13619095a683460b3e19bbd2bae9a7783157d434 100644 --- a/ee/spec/requests/api/ci/variables_spec.rb +++ b/ee/spec/requests/api/ci/variables_spec.rb @@ -12,6 +12,14 @@ stub_licensed_features(audit_events: true) end + describe 'GET /projects/:id/variables/:key' do + include_examples 'audit event for variable access', :ci_variable do + let(:make_request) { get api("/projects/#{project.id}/variables/#{audited_variable.key}", user) } + let(:expected_entity_id) { project.id } + let(:variable_attributes) { { project: project, hidden: is_hidden_variable, masked: is_masked_variable } } + end + end + describe 'POST /projects/:id/variables' do subject(:post_create) do post api("/projects/#{project.id}/variables", user), params: { key: 'new_variable', value: 'secret_value', protected: true } diff --git a/ee/spec/requests/api/group_variables_spec.rb b/ee/spec/requests/api/group_variables_spec.rb index 02a7c32a96e3f175634e77c8c3f9e1cf4743ed5a..8548cd63e44a39cf34ae1ef2e2ae8cb2c51f08e9 100644 --- a/ee/spec/requests/api/group_variables_spec.rb +++ b/ee/spec/requests/api/group_variables_spec.rb @@ -50,6 +50,12 @@ end end end + + include_examples 'audit event for variable access', :ci_group_variable do + let(:make_request) { get api("/groups/#{group.id}/variables/#{audited_variable.key}", user) } + let(:expected_entity_id) { group.id } + let(:variable_attributes) { { group: group, hidden: is_hidden_variable, masked: is_masked_variable } } + end end describe 'POST /groups/:id/variables' do diff --git a/ee/spec/support/shared_examples/lib/api/variables_shared_examples.rb b/ee/spec/support/shared_examples/lib/api/variables_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..17ab5e3c17f2a47207b9f1ce3802deb07633b1da --- /dev/null +++ b/ee/spec/support/shared_examples/lib/api/variables_shared_examples.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'audit event for variable access' do |variable_type| + let(:audited_variable) { create(variable_type, **variable_attributes) } + + before do + stub_licensed_features(audit_events: true) + end + + context 'when variable is not hidden' do + let(:is_hidden_variable) { false } + let(:is_masked_variable) { false } + + it 'creates an audit event' do + expect do + make_request + end.to change { AuditEvent.count }.by(1) + + audit_event = AuditEvent.order(:id).last + expect(audit_event.details[:custom_message]).to eq( + "CI/CD variable '#{audited_variable.key}' accessed with the API" + ) + expect(audit_event.details[:event_name]).to eq('variable_viewed_api') + expect(audit_event.details[:target_details]).to eq(audited_variable.key) + expect(audit_event.author_id).to eq(user.id) + expect(audit_event.entity_id).to eq(expected_entity_id) + end + end + + context 'when variable is hidden' do + let(:is_hidden_variable) { true } + let(:is_masked_variable) { true } + + it 'creates an audit event with mention to hidden variable' do + expect do + make_request + end.to change { AuditEvent.count }.by(1) + + audit_event = AuditEvent.order(:id).last + expect(audit_event.details[:custom_message]).to eq( + "CI/CD variable '#{audited_variable.key}' accessed with the API (hidden variable, no value shown)" + ) + expect(audit_event.details[:event_name]).to eq('variable_viewed_api') + expect(audit_event.details[:target_details]).to eq(audited_variable.key) + expect(audit_event.author_id).to eq(user.id) + expect(audit_event.entity_id).to eq(expected_entity_id) + end + end +end diff --git a/lib/api/ci/variables.rb b/lib/api/ci/variables.rb index 7b4fede99cf05f6a6ed3bcb137354d087fdc1e97..6e3f35f300a438111c2053aa7db91a6836569f49 100644 --- a/lib/api/ci/variables.rb +++ b/lib/api/ci/variables.rb @@ -44,6 +44,8 @@ class Variables < ::API::Base variable = find_variable(user_project, params) not_found!('Variable') unless variable + audit_variable_access(variable, user_project) + present variable, with: Entities::Ci::Variable end diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb index 3297f2ca868f36e14a17b1c9828230b52cbeeab3..648ce1747787ecee14d58acf05e0b71849b3f449 100644 --- a/lib/api/group_variables.rb +++ b/lib/api/group_variables.rb @@ -41,6 +41,8 @@ class GroupVariables < ::API::Base break not_found!('GroupVariable') unless variable + audit_variable_access(variable, user_group) + present variable, with: Entities::Ci::Variable end diff --git a/lib/api/helpers/variables_helpers.rb b/lib/api/helpers/variables_helpers.rb index edbdcb257e72e76f3eb1579a444f4e89efc040f9..abe8f4ea3e75c4b9fec57f98540cb8a1b2ec25bb 100644 --- a/lib/api/helpers/variables_helpers.rb +++ b/lib/api/helpers/variables_helpers.rb @@ -20,6 +20,10 @@ def find_variable(owner, params) conflict!("There are multiple variables with provided parameters. Please use 'filter[environment_scope]'") end + + def audit_variable_access(variable, scope) + # overridden in EE + end end end end