diff --git a/config/audit_events/types/api_request_access_with_scope.yml b/config/audit_events/types/api_request_access_with_scope.yml new file mode 100644 index 0000000000000000000000000000000000000000..015a60fd8987e5d7cea964e3ac76d8efb5c2da34 --- /dev/null +++ b/config/audit_events/types/api_request_access_with_scope.yml @@ -0,0 +1,10 @@ +--- +name: api_request_access_with_scope +description: A susbset of API requests authenticated by a token with an audited scope +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/499461 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172548 +feature_category: duo_workflow +milestone: '17.7' +saved_to_database: true +scope: [User] +streamed: true diff --git a/config/feature_flags/gitlab_com_derisk/api_audit_requests_with_scope.yml b/config/feature_flags/gitlab_com_derisk/api_audit_requests_with_scope.yml new file mode 100644 index 0000000000000000000000000000000000000000..fd822e78f8f9d9c6caac4613e3490096856a776e --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/api_audit_requests_with_scope.yml @@ -0,0 +1,9 @@ +--- +name: api_audit_requests_with_scope +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/499461 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172548 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/505974 +milestone: '17.7' +group: group::duo workflow +type: gitlab_com_derisk +default_enabled: false diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md index 41be7a311633606676c54c5c1ce0154678e0c70f..c52c54150f272f1a627be68949d8379157386b50 100644 --- a/doc/user/compliance/audit_event_types.md +++ b/doc/user/compliance/audit_event_types.md @@ -257,6 +257,12 @@ Audit event types belong to the following product categories. | [`cluster_agent_token_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112036) | Triggered when a user creates a cluster agent token | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.10](https://gitlab.com/gitlab-org/gitlab/-/issues/382133) | Project | | [`cluster_agent_token_revoked`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112036) | Triggered when a user revokes a cluster agent token | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.10](https://gitlab.com/gitlab-org/gitlab/-/issues/382133) | Project | +### Duo workflow + +| Name | Description | Saved to database | Streamed | Introduced in | Scope | +|:------------|:------------|:------------------|:---------|:--------------|:--------------| +| [`api_request_access_with_scope`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172548) | A susbset of API requests authenticated by a token with an audited scope | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.7](https://gitlab.com/gitlab-org/gitlab/-/issues/499461) | User | + ### Dynamic application security testing | Name | Description | Saved to database | Streamed | Introduced in | Scope | diff --git a/doc/user/duo_workflow/index.md b/doc/user/duo_workflow/index.md index 74cc48764154ff9a6297047dd32a9f162a93b621..7d46e02a6cea9f267418b7ef57bd95340827272d 100644 --- a/doc/user/duo_workflow/index.md +++ b/doc/user/duo_workflow/index.md @@ -77,7 +77,7 @@ If you have [Docker Desktop](https://handbook.gitlab.com/handbook/tools-and-tips or a container manager other than Colima installed already: 1. Pull the base Docker image: - + ```shell docker pull registry.gitlab.com/gitlab-org/duo-workflow/default-docker-image/workflow-generic-image:v0.0.4 ``` @@ -160,6 +160,10 @@ If you encounter issues: 1. Search for the setting **GitLab: Debug** and enable it. 1. Examine the [Duo Workflow Service production LangSmith trace](https://smith.langchain.com/o/477de7ad-583e-47b6-a1c4-c4a0300e7aca/projects/p/5409132b-2cf3-4df8-9f14-70204f90ed9b?timeModel=%7B%22duration%22%3A%227d%22%7D&tab=0). +## Audit log + +Audit event is created for each API request done by Duo Workflow. View these events on the [instance audit events](../../administration/audit_event_reports.md#instance-audit-events) page. + ## Give feedback Duo Workflow is an experiment and your feedback is crucial. To report issues or suggest improvements, diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index cb8face9911e7e839da4d7cdf6b2d3455f6ba8ea..72c9e3f77cf71bdcbd0ffbea9e174114322beca1 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -19,6 +19,10 @@ module Helpers API_RESPONSE_STATUS_CODE = 'gitlab.api.response_status_code' INTEGER_ID_REGEX = /^-?\d+$/ + # ai_workflows scope is used by Duo Workflow which is an AI automation tool, requests authenticated by token with + # this scope are audited to keep track of all actions done by Duo Workflow. + TOKEN_SCOPES_TO_AUDIT = [:ai_workflows].freeze + def logger API.logger end @@ -89,6 +93,7 @@ def current_user if @current_user load_balancer_stick_request(::ApplicationRecord, :user, @current_user.id) + audit_request_with_token_scope(@current_user) end @current_user @@ -816,6 +821,29 @@ def validate_search_rate_limit! end end + def audit_request_with_token_scope(user) + return unless Feature.enabled?(:api_audit_requests_with_scope, user) + + token_info = request.env[::Gitlab::Auth::AuthFinders::API_TOKEN_ENV] + return unless token_info + return unless TOKEN_SCOPES_TO_AUDIT.intersect?(Array.wrap(token_info[:token_scopes])) + + context = { + name: 'api_request_access_with_scope', + author: user, + scope: user, + target: ::Gitlab::Audit::NullTarget.new, + message: "API request with token scopes #{token_info[:token_scopes]} - #{request.request_method} #{request.path}", + additional_details: { + request: request.path, + method: request.request_method, + token_scopes: token_info[:token_scopes] + } + } + + ::Gitlab::Audit::Auditor.audit(context) + end + private # rubocop:disable Gitlab/ModuleWithInstanceVariables diff --git a/lib/gitlab/auth/auth_finders.rb b/lib/gitlab/auth/auth_finders.rb index 7288245fc0a99267297f69f00599966f0c16d9a7..0ad4845a768c7ce944936b757a2a12e50613f720 100644 --- a/lib/gitlab/auth/auth_finders.rb +++ b/lib/gitlab/auth/auth_finders.rb @@ -221,7 +221,11 @@ def authentication_token_present? private def save_current_token_in_env - request.env[API_TOKEN_ENV] = { token_id: access_token.id, token_type: access_token.class.to_s } + request.env[API_TOKEN_ENV] = { + token_id: access_token.id, + token_type: access_token.class.to_s, + token_scopes: access_token.scopes.map(&:to_sym) + } end def save_auth_failure_in_application_context(access_token, cause) diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb index f33c2e93bd3ce7494f01e43c24ebabe02c60d43a..757cbf88989ceea641ee013d4574abd26993f69b 100644 --- a/spec/requests/api/api_spec.rb +++ b/spec/requests/api/api_spec.rb @@ -500,4 +500,79 @@ expect(response).to have_gitlab_http_status(:bad_request) end end + + describe 'audit logging of requests with a specific token scope' do + let_it_be(:user) { create(:user) } + let_it_be(:token) { create(:oauth_access_token, user: user, scopes: [:ai_workflows]) } + let_it_be(:project) { create(:project) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:path) { "/projects/#{issue.project.id}/issues/#{issue.iid}" } + + before_all do + project.add_developer(user) + end + + shared_examples 'audited request' do + it 'adds audit log' do + expect(::Gitlab::Audit::Auditor).to receive(:audit).with(hash_including({ + name: 'api_request_access_with_scope', + message: "API request with token scopes [:ai_workflows] - GET /api/v4#{path}" + })).and_call_original + + subject + + expect(response).to have_gitlab_http_status(status) + end + end + + shared_examples 'not audited request' do + it "doesn't add audit log" do + expect(::Gitlab::Audit::Auditor).not_to receive(:audit) + + subject + + expect(response).to have_gitlab_http_status(status) + end + end + + context 'when endpoint allows token with ai_workflow scope' do + subject { get api(path, oauth_access_token: token) } + + context 'when token with ai_workflows scope is used' do + let(:status) { :ok } + + it_behaves_like 'audited request' + + context 'when request fails' do + let_it_be(:path) { "/projects/#{issue.project.id}/issues/#{non_existing_record_id}" } + let(:status) { :not_found } + + it_behaves_like 'audited request' + end + + context 'when api_audit_requests_with_scope flag is disabled' do + before do + stub_feature_flags(api_audit_requests_with_scope: false) + end + + it_behaves_like 'not audited request' + end + end + + context 'when token with ai_workflows scope is not used' do + let_it_be(:token) { create(:oauth_access_token, user: user, scopes: [:api]) } + let(:status) { :ok } + + it_behaves_like 'not audited request' + end + end + + context "when endpoint doesn't allow token with ai_workflow scope" do + subject { delete api(path, oauth_access_token: token) } + + let(:status) { :forbidden } + + it_behaves_like 'not audited request' + end + end end