diff --git a/doc/administration/audit_event_streaming/audit_event_types.md b/doc/administration/audit_event_streaming/audit_event_types.md index 01523e173546bae82f18c5736f292c1c91fbc9e4..79f92cb74aedbda1e027cc49993dabc9324c3e43 100644 --- a/doc/administration/audit_event_streaming/audit_event_types.md +++ b/doc/administration/audit_event_streaming/audit_event_types.md @@ -149,6 +149,7 @@ Every audit event is associated with an event type. The association with the eve | [`incident_closed_by_project_bot`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121485) | Triggered when an incident is closed using a project access token | **{check-circle}** Yes | **{check-circle}** Yes | `incident_management` | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/323299) | | [`incident_created_by_project_bot`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121485) | Triggered when an incident is created using a project access token | **{check-circle}** Yes | **{check-circle}** Yes | `incident_management` | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/323299) | | [`incident_reopened_by_project_bot`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121485) | Triggered when an incident is reopened using a project access token | **{check-circle}** Yes | **{check-circle}** Yes | `incident_management` | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/323299) | +| [`instance_google_cloud_logging_configuration_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131752) | Triggered when instance level Google Cloud Logging configuration is deleted. | **{check-circle}** Yes | **{check-circle}** Yes | `audit_events` | GitLab [16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/423040) | | [`ip_restrictions_changed`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86037) | Event triggered on any changes in the IP AllowList | **{check-circle}** Yes | **{check-circle}** Yes | `system_access` | GitLab [15.0](https://gitlab.com/gitlab-org/gitlab/-/issues/358986) | | [`issue_closed_by_project_bot`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121485) | Triggered when an issue is closed using a project access token | **{check-circle}** Yes | **{check-circle}** Yes | `team_planning` | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/323299) | | [`issue_created_by_project_bot`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121485) | Triggered when an issue is created using a project access token | **{check-circle}** Yes | **{check-circle}** Yes | `team_planning` | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/323299) | diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 78f2f584a8a0a6cdce7410cca50ade91a6743c6d..a413ef0ac3109692fb47e2f79f048a2c55636551 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -4085,6 +4085,24 @@ Input type: `InstanceExternalAuditEventDestinationUpdateInput` | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | `instanceExternalAuditEventDestination` | [`InstanceExternalAuditEventDestination`](#instanceexternalauditeventdestination) | Updated destination. | +### `Mutation.instanceGoogleCloudLoggingConfigurationDestroy` + +Input type: `InstanceGoogleCloudLoggingConfigurationDestroyInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `id` | [`AuditEventsInstanceGoogleCloudLoggingConfigurationID!`](#auditeventsinstancegooglecloudloggingconfigurationid) | ID of the Google Cloud logging configuration to destroy. | + +#### 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.issuableResourceLinkCreate` Input type: `IssuableResourceLinkCreateInput` @@ -29218,6 +29236,12 @@ A `AuditEventsInstanceExternalAuditEventDestinationID` is a global ID. It is enc An example `AuditEventsInstanceExternalAuditEventDestinationID` is: `"gid://gitlab/AuditEvents::InstanceExternalAuditEventDestination/1"`. +### `AuditEventsInstanceGoogleCloudLoggingConfigurationID` + +A `AuditEventsInstanceGoogleCloudLoggingConfigurationID` is a global ID. It is encoded as a string. + +An example `AuditEventsInstanceGoogleCloudLoggingConfigurationID` is: `"gid://gitlab/AuditEvents::Instance::GoogleCloudLoggingConfiguration/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 6fa6ee1ae077b63befa7cd4249024e948f262ede..9467532e3d452fce92a89654826e98efb506846b 100644 --- a/ee/app/graphql/ee/types/mutation_type.rb +++ b/ee/app/graphql/ee/types/mutation_type.rb @@ -129,6 +129,7 @@ module MutationType mount_mutation ::Mutations::AuditEvents::Streaming::InstanceEventTypeFilters::Create mount_mutation ::Mutations::AuditEvents::Streaming::InstanceEventTypeFilters::Destroy mount_mutation ::Mutations::Security::CiConfiguration::ProjectSetContinuousVulnerabilityScanning + mount_mutation ::Mutations::AuditEvents::Instance::GoogleCloudLoggingConfigurations::Destroy prepend(Types::DeprecatedMutations) end diff --git a/ee/app/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/base.rb b/ee/app/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/base.rb new file mode 100644 index 0000000000000000000000000000000000000000..1b10b196f5202d355c0d225de172e40730ec7ab7 --- /dev/null +++ b/ee/app/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/base.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Mutations + module AuditEvents + module Instance + module GoogleCloudLoggingConfigurations + class Base < BaseMutation + authorize :admin_instance_external_audit_events + + def ready?(**args) + raise_resource_not_available_error! unless current_user&.can?(:admin_instance_external_audit_events) + + super + end + + private + + def find_object(config_gid) + destination = GitlabSchema.object_from_id( + config_gid, + expected_type: ::AuditEvents::Instance::GoogleCloudLoggingConfiguration).sync + + raise_resource_not_available_error! if destination.blank? + + destination + end + + def audit(config, action:) + audit_context = { + name: "instance_google_cloud_logging_configuration_#{action}", + author: current_user, + scope: Gitlab::Audit::InstanceScope.new, + target: config, + message: "#{action.capitalize} Instance Google Cloud logging configuration with name: #{config.name} " \ + "project id: #{config.google_project_id_name} and log id: #{config.log_id_name}" + } + + ::Gitlab::Audit::Auditor.audit(audit_context) + end + end + end + end + end +end diff --git a/ee/app/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/destroy.rb b/ee/app/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/destroy.rb new file mode 100644 index 0000000000000000000000000000000000000000..ebf7b98794146c8b5da601edbbdb48bb6b9db7d9 --- /dev/null +++ b/ee/app/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/destroy.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Mutations + module AuditEvents + module Instance + module GoogleCloudLoggingConfigurations + class Destroy < Base + graphql_name 'InstanceGoogleCloudLoggingConfigurationDestroy' + + argument :id, ::Types::GlobalIDType[::AuditEvents::Instance::GoogleCloudLoggingConfiguration], + required: true, + description: 'ID of the Google Cloud logging configuration to destroy.' + + def resolve(id:) + config = authorized_find!(id) + + audit(config, action: :deleted) if config.destroy + + { errors: Array(config.errors) } + end + end + end + end + end +end diff --git a/ee/config/audit_events/types/instance_google_cloud_logging_configuration_deleted.yml b/ee/config/audit_events/types/instance_google_cloud_logging_configuration_deleted.yml new file mode 100644 index 0000000000000000000000000000000000000000..5ef3f8908ffedeab1df3442ed39e07886f792bd9 --- /dev/null +++ b/ee/config/audit_events/types/instance_google_cloud_logging_configuration_deleted.yml @@ -0,0 +1,9 @@ +--- +name: instance_google_cloud_logging_configuration_deleted +description: Triggered when instance level Google Cloud Logging configuration is deleted. +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/423040 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131752 +milestone: '16.5' +feature_category: audit_events +saved_to_database: true +streamed: true diff --git a/ee/spec/requests/api/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/destroy_spec.rb b/ee/spec/requests/api/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/destroy_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d3853463dfedd6600afdfbbbaf346005e9d4ad7c --- /dev/null +++ b/ee/spec/requests/api/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/destroy_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Destroy Instance Google Cloud logging configuration', feature_category: :audit_events do + include GraphqlHelpers + + let_it_be(:config) { create(:instance_google_cloud_logging_configuration) } + let_it_be(:admin) { create(:admin) } + + let(:current_user) { admin } + let(:mutation) { graphql_mutation(:instance_google_cloud_logging_configuration_destroy, id: global_id_of(config)) } + let(:mutation_response) { graphql_mutation_response(:instance_google_cloud_logging_configuration_destroy) } + + subject(:mutate) { post_graphql_mutation(mutation, current_user: current_user) } + + shared_examples 'a mutation that does not destroy a configuration' do + it_behaves_like 'a mutation that returns top-level errors', + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + + it 'does not destroy the configuration' do + expect { mutate } + .not_to change { AuditEvents::Instance::GoogleCloudLoggingConfiguration.count } + end + + it 'does not create audit event' do + expect { mutate }.not_to change { AuditEvent.count } + end + end + + context 'when feature is licensed' do + before do + stub_licensed_features(external_audit_events: true) + end + + context 'when current user is instance admin' do + before do + allow(Gitlab::Audit::Auditor).to receive(:audit) + end + + it 'destroys the configuration' do + expect { mutate } + .to change { AuditEvents::Instance::GoogleCloudLoggingConfiguration.count }.by(-1) + end + + it 'audits the deletion' do + subject + + expect(Gitlab::Audit::Auditor).to have_received(:audit) do |args| + expect(args[:name]).to eq('instance_google_cloud_logging_configuration_deleted') + expect(args[:author]).to eq(current_user) + expect(args[:scope]).to be_an_instance_of(Gitlab::Audit::InstanceScope) + expect(args[:target]).to eq(config) + expect(args[:message]) + .to eq("Deleted Instance Google Cloud logging configuration with name: #{config.name} " \ + "project id: #{config.google_project_id_name} and log id: #{config.log_id_name}") + end + end + + context 'when there is an error during destroy' do + before do + allow_next_instance_of( + Mutations::AuditEvents::Instance::GoogleCloudLoggingConfigurations::Destroy) do |mutation| + allow(mutation).to receive(:authorized_find!).and_return(config) + end + + allow(config).to receive(:destroy).and_return(false) + + errors = ActiveModel::Errors.new(config).tap { |e| e.add(:base, 'error message') } + allow(config).to receive(:errors).and_return(errors) + end + + it 'does not destroy the configuration and returns the error' do + expect { mutate } + .not_to change { AuditEvents::Instance::GoogleCloudLoggingConfiguration.count } + + expect(mutation_response).to include('errors' => ['error message']) + end + end + end + + context 'when current user is not instance admin' do + let_it_be(:current_user) { create(:user) } + + it_behaves_like 'a mutation that does not destroy a configuration' + end + end + + context 'when feature is unlicensed' do + before do + stub_licensed_features(external_audit_events: false) + end + + it_behaves_like 'a mutation that does not destroy a configuration' + end +end