From ca8c9723d9546d3aead69cf18f4b459f1b5a0dba Mon Sep 17 00:00:00 2001 From: Hitesh Raghuvanshi Date: Fri, 6 Oct 2023 22:06:55 +0530 Subject: [PATCH] Adding update api for instance GCP Added update API for instance level GCP logging config for audit event streaming Changelog: added EE: true --- .../audit_event_types.md | 1 + doc/api/graphql/reference/index.md | 24 +++ ee/app/graphql/ee/types/mutation_type.rb | 1 + .../common_update.rb | 63 +++++++ .../update.rb | 48 +----- .../update.rb | 56 +++++++ ...le_cloud_logging_configuration_updated.yml | 9 + .../update_spec.rb | 157 ++++++++++++++++++ 8 files changed, 316 insertions(+), 43 deletions(-) create mode 100644 ee/app/graphql/mutations/audit_events/google_cloud_logging_configurations/common_update.rb create mode 100644 ee/app/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/update.rb create mode 100644 ee/config/audit_events/types/instance_google_cloud_logging_configuration_updated.yml create mode 100644 ee/spec/requests/api/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/update_spec.rb diff --git a/doc/administration/audit_event_streaming/audit_event_types.md b/doc/administration/audit_event_streaming/audit_event_types.md index 7fea381fd09140..d0ee6a53a958dc 100644 --- a/doc/administration/audit_event_streaming/audit_event_types.md +++ b/doc/administration/audit_event_streaming/audit_event_types.md @@ -155,6 +155,7 @@ audit events to external destinations. | [`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) | +| [`instance_google_cloud_logging_configuration_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131790) | Triggered when instance level Google Cloud Logging configuration is updated. | **{check-circle}** Yes | **{check-circle}** Yes | `audit_events` | GitLab [16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/423039) | | [`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 3d5c53e2bca50c..03304b3b21a355 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -4161,6 +4161,30 @@ Input type: `InstanceGoogleCloudLoggingConfigurationDestroyInput` | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +### `Mutation.instanceGoogleCloudLoggingConfigurationUpdate` + +Input type: `InstanceGoogleCloudLoggingConfigurationUpdateInput` + +#### 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. | +| `id` | [`AuditEventsInstanceGoogleCloudLoggingConfigurationID!`](#auditeventsinstancegooglecloudloggingconfigurationid) | ID of the instance google Cloud configuration to update. | +| `logIdName` | [`String`](#string) | Unique identifier used to distinguish and manage different logs within the same Google Cloud project. | +| `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 updated. | + ### `Mutation.issuableResourceLinkCreate` Input type: `IssuableResourceLinkCreateInput` diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb index 70f22d66b8fbb4..0d411023363960 100644 --- a/ee/app/graphql/ee/types/mutation_type.rb +++ b/ee/app/graphql/ee/types/mutation_type.rb @@ -133,6 +133,7 @@ module MutationType mount_mutation ::Mutations::AuditEvents::Streaming::InstanceEventTypeFilters::Destroy mount_mutation ::Mutations::Security::CiConfiguration::ProjectSetContinuousVulnerabilityScanning mount_mutation ::Mutations::AuditEvents::Instance::GoogleCloudLoggingConfigurations::Destroy + mount_mutation ::Mutations::AuditEvents::Instance::GoogleCloudLoggingConfigurations::Update prepend(Types::DeprecatedMutations) end diff --git a/ee/app/graphql/mutations/audit_events/google_cloud_logging_configurations/common_update.rb b/ee/app/graphql/mutations/audit_events/google_cloud_logging_configurations/common_update.rb new file mode 100644 index 00000000000000..7f04a572d11e15 --- /dev/null +++ b/ee/app/graphql/mutations/audit_events/google_cloud_logging_configurations/common_update.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Mutations + module AuditEvents + module GoogleCloudLoggingConfigurations + module CommonUpdate + extend ActiveSupport::Concern + + AUDIT_EVENT_COLUMNS = [:google_project_id_name, :client_email, :private_key, :log_id_name, :name].freeze + + included do + include ::Audit::Changes + + argument :name, GraphQL::Types::String, + required: false, + description: 'Destination name.' + + argument :google_project_id_name, GraphQL::Types::String, + required: false, + description: 'Unique identifier of the Google Cloud project ' \ + 'to which the logging configuration belongs.' + + argument :client_email, GraphQL::Types::String, + required: false, + 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.' + + argument :private_key, GraphQL::Types::String, + required: false, + 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.' + end + + def update_config( + id:, google_project_id_name: nil, client_email: nil, private_key: nil, log_id_name: nil, name: nil + ) + config = authorized_find!(id) + config_attributes = { + google_project_id_name: google_project_id_name, + client_email: client_email, + private_key: private_key, + log_id_name: log_id_name, + name: name + }.compact + + if config.update(config_attributes) + audit_update(config) + [config, []] + else + [nil, Array(config.errors)] + end + end + end + end + end +end diff --git a/ee/app/graphql/mutations/audit_events/google_cloud_logging_configurations/update.rb b/ee/app/graphql/mutations/audit_events/google_cloud_logging_configurations/update.rb index abd05095388303..77d23a954335f4 100644 --- a/ee/app/graphql/mutations/audit_events/google_cloud_logging_configurations/update.rb +++ b/ee/app/graphql/mutations/audit_events/google_cloud_logging_configurations/update.rb @@ -6,63 +6,25 @@ module GoogleCloudLoggingConfigurations class Update < Base graphql_name 'GoogleCloudLoggingConfigurationUpdate' - include ::Audit::Changes + include Mutations::AuditEvents::GoogleCloudLoggingConfigurations::CommonUpdate UPDATE_EVENT_NAME = 'google_cloud_logging_configuration_updated' - AUDIT_EVENT_COLUMNS = [:google_project_id_name, :client_email, :private_key, :log_id_name, :name].freeze - authorize :admin_external_audit_events argument :id, ::Types::GlobalIDType[::AuditEvents::GoogleCloudLoggingConfiguration], required: true, description: 'ID of the google Cloud configuration to update.' - argument :name, GraphQL::Types::String, - required: false, - description: 'Destination name.' - - argument :google_project_id_name, GraphQL::Types::String, - required: false, - description: 'Unique identifier of the Google Cloud project ' \ - 'to which the logging configuration belongs.' - - argument :client_email, GraphQL::Types::String, - required: false, - 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.' - - argument :private_key, GraphQL::Types::String, - required: false, - 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 :google_cloud_logging_configuration, ::Types::AuditEvents::GoogleCloudLoggingConfigurationType, null: true, description: 'configuration updated.' def resolve(id:, google_project_id_name: nil, client_email: nil, private_key: nil, log_id_name: nil, name: nil) - config = authorized_find!(id) - config_attributes = { - google_project_id_name: google_project_id_name, - client_email: client_email, - private_key: private_key, - log_id_name: log_id_name, - name: name - }.compact + config, errors = update_config(id: id, google_project_id_name: google_project_id_name, + client_email: client_email, private_key: private_key, + log_id_name: log_id_name, name: name) - if config.update(config_attributes) - audit_update(config) - { google_cloud_logging_configuration: config, errors: [] } - else - { google_cloud_logging_configuration: nil, errors: Array(config.errors) } - end + { google_cloud_logging_configuration: config, errors: errors } end private diff --git a/ee/app/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/update.rb b/ee/app/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/update.rb new file mode 100644 index 00000000000000..ab2d0bf8adc90c --- /dev/null +++ b/ee/app/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/update.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Mutations + module AuditEvents + module Instance + module GoogleCloudLoggingConfigurations + class Update < Base + graphql_name 'InstanceGoogleCloudLoggingConfigurationUpdate' + + include Mutations::AuditEvents::GoogleCloudLoggingConfigurations::CommonUpdate + + UPDATE_EVENT_NAME = 'instance_google_cloud_logging_configuration_updated' + + argument :id, ::Types::GlobalIDType[::AuditEvents::Instance::GoogleCloudLoggingConfiguration], + required: true, + description: 'ID of the instance google Cloud configuration to update.' + + field :instance_google_cloud_logging_configuration, + ::Types::AuditEvents::Instance::GoogleCloudLoggingConfigurationType, + null: true, + description: 'configuration updated.' + + def resolve( + id:, + google_project_id_name: nil, + client_email: nil, + private_key: nil, + log_id_name: nil, + name: nil + ) + config, errors = update_config(id: id, google_project_id_name: google_project_id_name, + client_email: client_email, private_key: private_key, + log_id_name: log_id_name, name: name) + + { instance_google_cloud_logging_configuration: config, errors: errors } + end + + private + + def audit_update(config) + AUDIT_EVENT_COLUMNS.each do |column| + audit_changes( + column, + as: column.to_s, + entity: Gitlab::Audit::InstanceScope.new, + model: config, + event_type: UPDATE_EVENT_NAME, + target_details: config.name + ) + end + end + end + end + end + end +end diff --git a/ee/config/audit_events/types/instance_google_cloud_logging_configuration_updated.yml b/ee/config/audit_events/types/instance_google_cloud_logging_configuration_updated.yml new file mode 100644 index 00000000000000..f7fbc59dfc961d --- /dev/null +++ b/ee/config/audit_events/types/instance_google_cloud_logging_configuration_updated.yml @@ -0,0 +1,9 @@ +--- +name: instance_google_cloud_logging_configuration_updated +description: Triggered when instance level Google Cloud Logging configuration is updated. +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/423039 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131790 +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/update_spec.rb b/ee/spec/requests/api/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/update_spec.rb new file mode 100644 index 00000000000000..ceeb7774aa7e07 --- /dev/null +++ b/ee/spec/requests/api/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/update_spec.rb @@ -0,0 +1,157 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Update Instance Google Cloud logging configuration', feature_category: :audit_events do + include GraphqlHelpers + + let_it_be_with_reload(:config) { create(:instance_google_cloud_logging_configuration) } + let_it_be(:admin) { create(:admin) } + let_it_be(:updated_google_project_id_name) { 'updated-project' } + let_it_be(:updated_client_email) { 'updated-email@example.com' } + let_it_be(:updated_private_key) { OpenSSL::PKey::RSA.new(4096).to_pem } + let_it_be(:updated_log_id_name) { 'updated_log_id_name' } + let_it_be(:updated_destination_name) { 'updated_destination_name' } + let_it_be(:config_gid) { global_id_of(config) } + + let(:current_user) { admin } + let(:mutation) { graphql_mutation(:instance_google_cloud_logging_configuration_update, input) } + let(:mutation_response) { graphql_mutation_response(:instance_google_cloud_logging_configuration_update) } + + let(:input) do + { + id: config_gid, + googleProjectIdName: updated_google_project_id_name, + clientEmail: updated_client_email, + privateKey: updated_private_key, + logIdName: updated_log_id_name, + name: updated_destination_name + } + end + + subject(:mutate) { post_graphql_mutation(mutation, current_user: current_user) } + + shared_examples 'a mutation that does not update the configuration' do + it 'does not update the configuration' do + expect { mutate }.not_to change { config.reload.attributes } + 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 'updates the configuration' do + mutate + + config.reload + + expect(config.google_project_id_name).to eq(updated_google_project_id_name) + expect(config.client_email).to eq(updated_client_email) + expect(config.private_key).to eq(updated_private_key) + expect(config.log_id_name).to eq(updated_log_id_name) + expect(config.name).to eq(updated_destination_name) + end + + it 'audits the update' do + Mutations::AuditEvents::Instance::GoogleCloudLoggingConfigurations::Update::AUDIT_EVENT_COLUMNS.each do |column| + message = if column == :private_key + "Changed #{column}" + else + "Changed #{column} from #{config[column]} to #{input[column.to_s.camelize(:lower).to_sym]}" + end + + expected_hash = { + name: Mutations::AuditEvents::Instance::GoogleCloudLoggingConfigurations::Update::UPDATE_EVENT_NAME, + author: current_user, + scope: an_instance_of(Gitlab::Audit::InstanceScope), + target: config, + message: message, + target_details: updated_destination_name + } + + expect(Gitlab::Audit::Auditor).to receive(:audit).once.ordered.with(hash_including(expected_hash)) + end + + subject + end + + context 'when the fields are updated with existing values' do + let(:input) do + { + id: config_gid, + googleProjectIdName: config.google_project_id_name, + name: config.name + } + end + + it 'does not audit the event' do + expect(Gitlab::Audit::Auditor).not_to receive(:audit) + + subject + end + end + + context 'when no fields are provided for update' do + let(:input) do + { + id: config_gid + } + end + + it_behaves_like 'a mutation that does not update the configuration' + end + + context 'when there is error while updating' do + before do + allow_next_instance_of( + Mutations::AuditEvents::Instance::GoogleCloudLoggingConfigurations::Update) do |mutation| + allow(mutation).to receive(:authorized_find!).with(config_gid).and_return(config) + end + + allow(config).to receive(:update).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 update the configuration and returns the error' do + mutate + + expect(mutation_response).to include( + 'instanceGoogleCloudLoggingConfiguration' => nil, + '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 returns top-level errors', + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + it_behaves_like 'a mutation that does not update the 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 returns top-level errors', + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + it_behaves_like 'a mutation that does not update the configuration' + end +end -- GitLab