diff --git a/doc/administration/audit_event_streaming/audit_event_types.md b/doc/administration/audit_event_streaming/audit_event_types.md index 88212045d8e1fa54340bc28df0c0d764502f7f6f..1dbf1e19143275ccf1ac4ee4bcab067285e7c70f 100644 --- a/doc/administration/audit_event_streaming/audit_event_types.md +++ b/doc/administration/audit_event_streaming/audit_event_types.md @@ -47,6 +47,7 @@ Audit event types belong to the following product categories. | [`create_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74632) | Event triggered when an external audit event destination is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [14.6](https://gitlab.com/gitlab-org/gitlab/-/issues/344664) | | [`create_http_namespace_filter`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136047) | Event triggered when a namespace filter for an external audit event destination for a top-level group is created.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.6](https://gitlab.com/gitlab-org/gitlab/-/issues/424176) | | [`create_instance_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123882) | Event triggered when an instance level external audit event destination is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.2](https://gitlab.com/gitlab-org/gitlab/-/issues/404730) | +| [`delete_http_namespace_filter`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136302) | Event triggered when a namespace filter for an external audit event destination for a top-level group is deleted.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.7](https://gitlab.com/gitlab-org/gitlab/-/issues/424177) | | [`destroy_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74632) | Event triggered when an external audit event destination is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [14.6](https://gitlab.com/gitlab-org/gitlab/-/issues/344664) | | [`destroy_instance_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125846) | Event triggered when an instance level external audit event destination is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.2](https://gitlab.com/gitlab-org/gitlab/-/issues/404730) | | [`event_type_filters_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113081) | Event triggered when a new audit events streaming event type filter is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.10](https://gitlab.com/gitlab-org/gitlab/-/issues/344848) | diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 4a1b536fd4002b876afc614f7cd39a5df0fd2348..670591353e39599e59041fc62ec9ac5d0a7bd7d5 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1587,6 +1587,24 @@ Input type: `AuditEventsStreamingHTTPNamespaceFiltersAddInput` | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | `namespaceFilter` | [`AuditEventStreamingHTTPNamespaceFilter`](#auditeventstreaminghttpnamespacefilter) | Namespace filter created. | +### `Mutation.auditEventsStreamingHttpNamespaceFiltersDelete` + +Input type: `AuditEventsStreamingHTTPNamespaceFiltersDeleteInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `namespaceFilterId` | [`AuditEventsStreamingHTTPNamespaceFilterID!`](#auditeventsstreaminghttpnamespacefilterid) | Namespace filter ID. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | + ### `Mutation.auditEventsStreamingInstanceHeadersCreate` Input type: `AuditEventsStreamingInstanceHeadersCreateInput` @@ -14109,6 +14127,17 @@ Autogenerated return type of AuditEventsStreamingHTTPNamespaceFiltersAdd. | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | `namespaceFilter` | [`AuditEventStreamingHTTPNamespaceFilter`](#auditeventstreaminghttpnamespacefilter) | Namespace filter created. | +### `AuditEventsStreamingHTTPNamespaceFiltersDeletePayload` + +Autogenerated return type of AuditEventsStreamingHTTPNamespaceFiltersDelete. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | + ### `AuditEventsStreamingInstanceHeader` Represents a HTTP header key/value that belongs to an instance level audit streaming destination. @@ -30724,6 +30753,12 @@ A `AuditEventsInstanceGoogleCloudLoggingConfigurationID` is a global ID. It is e An example `AuditEventsInstanceGoogleCloudLoggingConfigurationID` is: `"gid://gitlab/AuditEvents::Instance::GoogleCloudLoggingConfiguration/1"`. +### `AuditEventsStreamingHTTPNamespaceFilterID` + +A `AuditEventsStreamingHTTPNamespaceFilterID` is a global ID. It is encoded as a string. + +An example `AuditEventsStreamingHTTPNamespaceFilterID` is: `"gid://gitlab/AuditEvents::Streaming::HTTP::NamespaceFilter/1"`. + ### `AuditEventsStreamingHeaderID` A `AuditEventsStreamingHeaderID` is a global ID. It is encoded as a string. diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb index 42ffa61607094020e894db7cd827a650c001b06a..b7d5567874ab6f354d0dc6a4b90b02542c72899c 100644 --- a/ee/app/graphql/ee/types/mutation_type.rb +++ b/ee/app/graphql/ee/types/mutation_type.rb @@ -143,6 +143,7 @@ module MutationType mount_mutation ::Mutations::Analytics::CycleAnalytics::ValueStreams::Update, alpha: { milestone: '16.6' } mount_mutation ::Mutations::Analytics::CycleAnalytics::ValueStreams::Destroy, alpha: { milestone: '16.6' } mount_mutation ::Mutations::AuditEvents::Streaming::HTTP::NamespaceFilters::Create + mount_mutation ::Mutations::AuditEvents::Streaming::HTTP::NamespaceFilters::Delete prepend(Types::DeprecatedMutations) end diff --git a/ee/app/graphql/mutations/audit_events/streaming/http/namespace_filters/base.rb b/ee/app/graphql/mutations/audit_events/streaming/http/namespace_filters/base.rb new file mode 100644 index 0000000000000000000000000000000000000000..1bf63c9faee97b61a271bb0f7c27a07a5f166b1f --- /dev/null +++ b/ee/app/graphql/mutations/audit_events/streaming/http/namespace_filters/base.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Mutations + module AuditEvents + module Streaming + module HTTP + module NamespaceFilters + class Base < BaseMutation + authorize :admin_external_audit_events + + private + + def audit(filter, action:) + audit_context = { + name: "#{action}_http_namespace_filter", + author: current_user, + scope: filter.external_audit_event_destination.group, + target: filter.external_audit_event_destination, + message: "#{action.capitalize} namespace filter for http audit event streaming destination " \ + "#{filter.external_audit_event_destination.name} and namespace #{filter.namespace.full_path}" + } + + ::Gitlab::Audit::Auditor.audit(audit_context) + end + end + end + end + end + end +end diff --git a/ee/app/graphql/mutations/audit_events/streaming/http/namespace_filters/create.rb b/ee/app/graphql/mutations/audit_events/streaming/http/namespace_filters/create.rb index 054d60d0cb3be11c9685e188fceb35efae17228a..24078f5b32b81d52623542dd1aa892fb3cb7b850 100644 --- a/ee/app/graphql/mutations/audit_events/streaming/http/namespace_filters/create.rb +++ b/ee/app/graphql/mutations/audit_events/streaming/http/namespace_filters/create.rb @@ -5,9 +5,8 @@ module AuditEvents module Streaming module HTTP module NamespaceFilters - class Create < BaseMutation + class Create < Base graphql_name 'AuditEventsStreamingHTTPNamespaceFiltersAdd' - authorize :admin_external_audit_events argument :destination_id, ::Types::GlobalIDType[::AuditEvents::ExternalAuditEventDestination], required: true, @@ -66,19 +65,6 @@ def namespace(group_path, project_path) namespace.project_namespace end - def audit(filter, action:) - audit_context = { - name: "#{action}_http_namespace_filter", - author: current_user, - scope: filter.external_audit_event_destination.group, - target: filter.external_audit_event_destination, - message: "#{action.capitalize} namespace filter for http audit event streaming destination " \ - "#{filter.external_audit_event_destination.name} and namespace #{filter.namespace.full_path}" - } - - ::Gitlab::Audit::Auditor.audit(audit_context) - end - def mutually_exclusive_args [:group_path, :project_path] end diff --git a/ee/app/graphql/mutations/audit_events/streaming/http/namespace_filters/delete.rb b/ee/app/graphql/mutations/audit_events/streaming/http/namespace_filters/delete.rb new file mode 100644 index 0000000000000000000000000000000000000000..80fb79f359638d4704e578fbd4d61d7db2e49b15 --- /dev/null +++ b/ee/app/graphql/mutations/audit_events/streaming/http/namespace_filters/delete.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Mutations + module AuditEvents + module Streaming + module HTTP + module NamespaceFilters + class Delete < Base + graphql_name 'AuditEventsStreamingHTTPNamespaceFiltersDelete' + + argument :namespace_filter_id, ::Types::GlobalIDType[::AuditEvents::Streaming::HTTP::NamespaceFilter], + required: true, + description: 'Namespace filter ID.' + def resolve(namespace_filter_id:) + filter = authorized_find!(id: namespace_filter_id) + + audit(filter, action: :delete) if filter.destroy + + { namespace_filter: nil, errors: [] } + end + + private + + def find_object(id:) + ::GitlabSchema.object_from_id(id, expected_type: ::AuditEvents::Streaming::HTTP::NamespaceFilter) + end + end + end + end + end + end +end diff --git a/ee/config/audit_events/types/delete_http_namespace_filter.yml b/ee/config/audit_events/types/delete_http_namespace_filter.yml new file mode 100644 index 0000000000000000000000000000000000000000..f4843aa3ad285edc0e64519e80c5cf626438c919 --- /dev/null +++ b/ee/config/audit_events/types/delete_http_namespace_filter.yml @@ -0,0 +1,8 @@ +name: delete_http_namespace_filter +description: Event triggered when a namespace filter for an external audit event destination for a top-level group is deleted. +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/424177 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136302 +feature_category: audit_events +milestone: "16.7" +saved_to_database: true +streamed: true diff --git a/ee/spec/requests/api/graphql/audit_events/streaming/http/namespace_filters/delete_spec.rb b/ee/spec/requests/api/graphql/audit_events/streaming/http/namespace_filters/delete_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..44d288a293f7ba5b4075757784c422632892655d --- /dev/null +++ b/ee/spec/requests/api/graphql/audit_events/streaming/http/namespace_filters/delete_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Delete a namespace filter for group level external audit event destinations', feature_category: :audit_events do + include GraphqlHelpers + + let(:current_user) { create(:user) } + let(:group) { create(:group) } + let(:subgroup) { create(:group, parent: group) } + let(:destination) { create(:external_audit_event_destination, group: group) } + let!(:filter) do + create(:audit_events_streaming_http_namespace_filter, external_audit_event_destination: destination, + namespace: subgroup) + end + + let(:mutation) { graphql_mutation(:audit_events_streaming_http_namespace_filters_delete, input) } + let(:mutation_response) { graphql_mutation_response(:audit_events_streaming_http_namespace_filters_delete) } + + let(:input) do + { namespaceFilterId: filter.to_gid } + end + + subject(:mutate) { post_graphql_mutation(mutation, current_user: current_user) } + + shared_examples 'does not delete the namespace filter' do + it do + expect(::Gitlab::Audit::Auditor).not_to receive(:audit) + .with(a_hash_including(name: 'delete_http_namespace_filter')) + + expect { subject }.not_to change { destination.reload.namespace_filter } + end + end + + context 'when feature is licensed' do + before do + stub_licensed_features(external_audit_events: true) + end + + context 'when current user is group owner' do + before do + group.add_owner(current_user) + end + + it 'deletes the filter', :aggregate_failures do + expect(::Gitlab::Audit::Auditor).to receive(:audit).with(a_hash_including( + name: 'delete_http_namespace_filter', + author: current_user, + scope: group, + target: destination, + message: "Delete namespace filter for http audit event streaming destination #{destination.name} " \ + "and namespace #{subgroup.full_path}")).once.and_call_original + + expect { mutate }.to change { AuditEvents::Streaming::HTTP::NamespaceFilter.count }.by(-1) + + expect(destination.reload.namespace_filter).to be nil + expect_graphql_errors_to_be_empty + expect(mutation_response['errors']).to be_empty + expect(mutation_response['namespaceFilter']).to be nil + end + end + + context 'when current user is a group maintainer' do + before do + group.add_maintainer(current_user) + end + + it_behaves_like 'does not delete the namespace filter' + end + end + + context 'when feature is not licensed' do + before do + stub_licensed_features(external_audit_events: false) + end + + it_behaves_like 'a mutation on an unauthorized resource' + + it_behaves_like 'does not delete the namespace filter' + end +end