diff --git a/doc/administration/audit_event_streaming/audit_event_types.md b/doc/administration/audit_event_streaming/audit_event_types.md index e9f2a4a611df90672b4e96e293c6789617a99909..5c27dabe378649354fab452f2d51de72fd3411da 100644 --- a/doc/administration/audit_event_streaming/audit_event_types.md +++ b/doc/administration/audit_event_streaming/audit_event_types.md @@ -152,6 +152,7 @@ audit events to external destinations. | [`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_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130663) | Triggered when Instance level Google Cloud Logging configuration is created | **{check-circle}** Yes | **{check-circle}** Yes | `audit_events` | GitLab [16.4](https://gitlab.com/gitlab-org/gitlab/-/issues/423038) | | [`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) | diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 12688851fb1fe279a48f2d3553634fcb0763548f..ef60be92ee274d65b7b462fdad397aa6c35baf1f 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -4096,6 +4096,29 @@ Input type: `InstanceExternalAuditEventDestinationUpdateInput` | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | `instanceExternalAuditEventDestination` | [`InstanceExternalAuditEventDestination`](#instanceexternalauditeventdestination) | Updated destination. | +### `Mutation.instanceGoogleCloudLoggingConfigurationCreate` + +Input type: `InstanceGoogleCloudLoggingConfigurationCreateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientEmail` | [`String!`](#string) | Email address associated with the service account that will be used to authenticate and interact with the Google Cloud Logging service. This is part of the IAM credentials. | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `googleProjectIdName` | [`String!`](#string) | Unique identifier of the Google Cloud project to which the logging configuration belongs. | +| `logIdName` | [`String`](#string) | Unique identifier used to distinguish and manage different logs within the same Google Cloud project.(defaults to `audit_events`). | +| `name` | [`String`](#string) | Destination name. | +| `privateKey` | [`String!`](#string) | Private Key associated with the service account. This key is used to authenticate the service account and authorize it to interact with the Google Cloud Logging service. | + +#### 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. | +| `instanceGoogleCloudLoggingConfiguration` | [`InstanceGoogleCloudLoggingConfigurationType`](#instancegooglecloudloggingconfigurationtype) | configuration created. | + ### `Mutation.instanceGoogleCloudLoggingConfigurationDestroy` Input type: `InstanceGoogleCloudLoggingConfigurationDestroyInput` diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb index 9467532e3d452fce92a89654826e98efb506846b..5332505397e41b9141e706a65d4d7e303850899c 100644 --- a/ee/app/graphql/ee/types/mutation_type.rb +++ b/ee/app/graphql/ee/types/mutation_type.rb @@ -122,6 +122,7 @@ module MutationType mount_mutation ::Mutations::AuditEvents::GoogleCloudLoggingConfigurations::Create mount_mutation ::Mutations::AuditEvents::GoogleCloudLoggingConfigurations::Destroy mount_mutation ::Mutations::AuditEvents::GoogleCloudLoggingConfigurations::Update + mount_mutation ::Mutations::AuditEvents::Instance::GoogleCloudLoggingConfigurations::Create mount_mutation ::Mutations::Forecasting::BuildForecast, alpha: { milestone: '16.0' } mount_mutation ::Mutations::AuditEvents::Streaming::InstanceHeaders::Create mount_mutation ::Mutations::AuditEvents::Streaming::InstanceHeaders::Update diff --git a/ee/app/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/create.rb b/ee/app/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/create.rb new file mode 100644 index 0000000000000000000000000000000000000000..5e782db6188e0e97319f923ade0faf15111787eb --- /dev/null +++ b/ee/app/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/create.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Mutations + module AuditEvents + module Instance + module GoogleCloudLoggingConfigurations + class Create < Base + graphql_name 'InstanceGoogleCloudLoggingConfigurationCreate' + + argument :name, GraphQL::Types::String, + required: false, + description: 'Destination name.' + + argument :google_project_id_name, GraphQL::Types::String, + required: true, + description: 'Unique identifier of the Google Cloud project ' \ + 'to which the logging configuration belongs.' + + argument :client_email, GraphQL::Types::String, + required: true, + description: 'Email address associated with the service account ' \ + 'that will be used to authenticate and interact with the ' \ + 'Google Cloud Logging service. This is part of the IAM credentials.' + + argument :log_id_name, GraphQL::Types::String, + required: false, + description: 'Unique identifier used to distinguish and manage ' \ + 'different logs within the same Google Cloud project.' \ + '(defaults to `audit_events`).', + default_value: 'audit_events' + + argument :private_key, GraphQL::Types::String, + required: true, + description: 'Private Key associated with the service account. This key ' \ + 'is used to authenticate the service account and authorize it ' \ + 'to interact with the Google Cloud Logging service.' + + field :instance_google_cloud_logging_configuration, + ::Types::AuditEvents::Instance::GoogleCloudLoggingConfigurationType, + null: true, + description: 'configuration created.' + + def resolve(google_project_id_name:, client_email:, private_key:, log_id_name: nil, name: nil) + config_attributes = { + google_project_id_name: google_project_id_name, + client_email: client_email, + private_key: private_key, + name: name + } + + config_attributes[:log_id_name] = log_id_name if log_id_name.present? + + config = ::AuditEvents::Instance::GoogleCloudLoggingConfiguration.new(config_attributes) + + if config.save + audit(config, action: :created) + + { instance_google_cloud_logging_configuration: config, errors: [] } + else + { instance_google_cloud_logging_configuration: nil, errors: Array(config.errors) } + end + end + end + end + end + end +end diff --git a/ee/config/audit_events/types/instance_google_cloud_logging_configuration_created.yml b/ee/config/audit_events/types/instance_google_cloud_logging_configuration_created.yml new file mode 100644 index 0000000000000000000000000000000000000000..2af527cc80cd37c0cbfc01291f2658529063426b --- /dev/null +++ b/ee/config/audit_events/types/instance_google_cloud_logging_configuration_created.yml @@ -0,0 +1,9 @@ +--- +name: instance_google_cloud_logging_configuration_created +description: Triggered when Instance level Google Cloud Logging configuration is created +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/423038 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130663 +milestone: '16.4' +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/create_spec.rb b/ee/spec/requests/api/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/create_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a3bb728e370e5dbd411c1be4f7b7fd00eb28aaa0 --- /dev/null +++ b/ee/spec/requests/api/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/create_spec.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Create Instance level Google Cloud logging configuration', feature_category: :audit_events do + include GraphqlHelpers + + let_it_be(:admin) { create(:admin) } + let_it_be(:user) { create(:user) } + let_it_be(:destination_name) { 'my_google_destination' } + let_it_be(:google_project_id_name) { 'test-project' } + let_it_be(:client_email) { 'test-email@example.com' } + let_it_be(:private_key) { OpenSSL::PKey::RSA.new(4096).to_pem } + let_it_be(:log_id_name) { 'audit_events' } + + let(:current_user) { admin } + let(:mutation) { graphql_mutation(:instance_google_cloud_logging_configuration_create, input) } + let(:mutation_response) { graphql_mutation_response(:instance_google_cloud_logging_configuration_create) } + + let(:input) do + { + name: destination_name, + googleProjectIdName: google_project_id_name, + clientEmail: client_email, + privateKey: private_key + } + end + + subject(:mutate) { post_graphql_mutation(mutation, current_user: current_user) } + + shared_examples 'creates an audit event' do + before do + allow(Gitlab::Audit::Auditor).to receive(:audit) + end + + it 'audits the creation' do + subject + + config = AuditEvents::Instance::GoogleCloudLoggingConfiguration.last + expect(Gitlab::Audit::Auditor).to have_received(:audit) do |args| + expect(args[:name]).to eq('instance_google_cloud_logging_configuration_created') + 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("Created Instance Google Cloud logging configuration with name: #{destination_name} " \ + "project id: #{google_project_id_name} and log id: #{log_id_name}") + end + end + end + + shared_examples 'a mutation that does not create a configuration' do + it 'does not create 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 + + shared_examples 'an unauthorized mutation that does not create a configuration' do + it_behaves_like 'a mutation that returns top-level errors', + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + + it_behaves_like 'a mutation that does not create a configuration' + end + + context 'when feature is licensed' do + before do + stub_licensed_features(external_audit_events: true) + end + + context 'when current user is an admin' do + it 'creates the configuration' do + expect { mutate } + .to change { AuditEvents::Instance::GoogleCloudLoggingConfiguration.count }.by(1) + + config = AuditEvents::Instance::GoogleCloudLoggingConfiguration.last + expect(config.name).to eq(destination_name) + expect(config.google_project_id_name).to eq(google_project_id_name) + expect(config.client_email).to eq(client_email) + expect(config.log_id_name).to eq(log_id_name) + expect(config.private_key).to eq(private_key) + + expect(mutation_response['errors']).to be_empty + expect(mutation_response['instanceGoogleCloudLoggingConfiguration']['googleProjectIdName']) + .to eq(google_project_id_name) + expect(mutation_response['instanceGoogleCloudLoggingConfiguration']['id']).not_to be_empty + expect(mutation_response['instanceGoogleCloudLoggingConfiguration']['name']).to eq(destination_name) + expect(mutation_response['instanceGoogleCloudLoggingConfiguration']['clientEmail']).to eq(client_email) + expect(mutation_response['instanceGoogleCloudLoggingConfiguration']['logIdName']).to eq(log_id_name) + end + + it_behaves_like 'creates an audit event', 'audit_events' + + context 'when overriding log id name' do + let_it_be(:log_id_name) { 'test-log-id' } + + let(:input) do + { + name: destination_name, + googleProjectIdName: google_project_id_name, + clientEmail: client_email, + privateKey: private_key, + logIdName: log_id_name + } + end + + it 'creates the configuration' do + expect { mutate } + .to change { AuditEvents::Instance::GoogleCloudLoggingConfiguration.count }.by(1) + + config = AuditEvents::Instance::GoogleCloudLoggingConfiguration.last + expect(config.name).to eq(destination_name) + expect(config.google_project_id_name).to eq(google_project_id_name) + expect(config.client_email).to eq(client_email) + expect(config.log_id_name).to eq(log_id_name) + expect(config.private_key).to eq(private_key) + end + + it_behaves_like 'creates an audit event' + end + + context 'when there is error while saving' do + before do + allow_next_instance_of(AuditEvents::Instance::GoogleCloudLoggingConfiguration) do |instance| + allow(instance).to receive(:save).and_return(false) + + errors = ActiveModel::Errors.new(instance).tap { |e| e.add(:log_id_name, 'error message') } + allow(instance).to receive(:errors).and_return(errors) + end + end + + it 'does not create the configuration and returns the error' do + expect { mutate } + .not_to change { AuditEvents::Instance::GoogleCloudLoggingConfiguration.count } + + expect(mutation_response).to include( + 'instanceGoogleCloudLoggingConfiguration' => nil, + 'errors' => ["Log id name error message"] + ) + end + end + end + + context 'when current user is not an admin' do + let_it_be(:current_user) { user } + + it_behaves_like 'an unauthorized mutation that does not create a configuration' + end + end + + context 'when feature is unlicensed' do + before do + stub_licensed_features(external_audit_events: false) + end + + it_behaves_like 'an unauthorized mutation that does not create a configuration' + end +end