From df5e82018f62fdd50a0e991787ffc65c56f25060 Mon Sep 17 00:00:00 2001 From: huzaifaiftikhar1 Date: Tue, 10 Oct 2023 16:21:48 +0530 Subject: [PATCH] GraphQL API for updating audit event streaming amazon_s3_configurations Changelog: added EE: true --- .../audit_event_types.md | 1 + doc/api/graphql/reference/index.md | 30 +++ ee/app/graphql/ee/types/mutation_type.rb | 1 + .../amazon_s3_configurations/update.rb | 82 +++++++++ .../types/amazon_s3_configuration_updated.yml | 9 + .../amazon_s3_configurations/update_spec.rb | 174 ++++++++++++++++++ 6 files changed, 297 insertions(+) create mode 100644 ee/app/graphql/mutations/audit_events/amazon_s3_configurations/update.rb create mode 100644 ee/config/audit_events/types/amazon_s3_configuration_updated.yml create mode 100644 ee/spec/requests/api/graphql/mutations/audit_events/amazon_s3_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 eea570f65940c3..3b2ae098469300 100644 --- a/doc/administration/audit_event_streaming/audit_event_types.md +++ b/doc/administration/audit_event_streaming/audit_event_types.md @@ -37,6 +37,7 @@ Audit event types belong to the following product categories. | Name | Description | Saved to database | Streamed | Introduced in | |:-----|:------------|:------------------|:---------|:--------------| | [`amazon_s3_configuration_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132443) | Triggered when Amazon S3 configuration for audit events streaming is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/423229) | +| [`amazon_s3_configuration_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133691) | Triggered when Amazon S3 configuration for audit events streaming is updated.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/423229) | | [`audit_events_streaming_headers_create`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/92068) | Triggered when a streaming header for audit events is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.3](https://gitlab.com/gitlab-org/gitlab/-/issues/366350) | | [`audit_events_streaming_headers_destroy`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/92068) | Triggered when a streaming header for audit events is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.3](https://gitlab.com/gitlab-org/gitlab/-/issues/366350) | | [`audit_events_streaming_instance_headers_create`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125870) | Triggered when a streaming header for instance level external audit event destination is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.3](https://gitlab.com/gitlab-org/gitlab/-/issues/417433) | diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 3d0e9c113d7149..25fc7791cf533a 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1298,6 +1298,30 @@ Input type: `AmazonS3ConfigurationCreateInput` | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +### `Mutation.amazonS3ConfigurationUpdate` + +Input type: `AmazonS3ConfigurationUpdateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `accessKeyXid` | [`String`](#string) | Access key ID of the Amazon S3 account. | +| `awsRegion` | [`String`](#string) | AWS region where the bucket is created. | +| `bucketName` | [`String`](#string) | Name of the bucket where the audit events would be logged. | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `id` | [`AuditEventsAmazonS3ConfigurationID!`](#auditeventsamazons3configurationid) | ID of the Amazon S3 configuration to update. | +| `name` | [`String`](#string) | Destination name. | +| `secretAccessKey` | [`String`](#string) | Secret access key of the Amazon S3 account. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `amazonS3Configuration` | [`AmazonS3ConfigurationType`](#amazons3configurationtype) | Updated Amazon S3 configuration. | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | + ### `Mutation.approveDeployment` Input type: `ApproveDeploymentInput` @@ -29877,6 +29901,12 @@ A `AppSecFuzzingCoverageCorpusID` is a global ID. It is encoded as a string. An example `AppSecFuzzingCoverageCorpusID` is: `"gid://gitlab/AppSec::Fuzzing::Coverage::Corpus/1"`. +### `AuditEventsAmazonS3ConfigurationID` + +A `AuditEventsAmazonS3ConfigurationID` is a global ID. It is encoded as a string. + +An example `AuditEventsAmazonS3ConfigurationID` is: `"gid://gitlab/AuditEvents::AmazonS3Configuration/1"`. + ### `AuditEventsExternalAuditEventDestinationID` A `AuditEventsExternalAuditEventDestinationID` 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 f9c0ea716479c1..f086bc24430ef1 100644 --- a/ee/app/graphql/ee/types/mutation_type.rb +++ b/ee/app/graphql/ee/types/mutation_type.rb @@ -124,6 +124,7 @@ module MutationType mount_mutation ::Mutations::AuditEvents::GoogleCloudLoggingConfigurations::Destroy mount_mutation ::Mutations::AuditEvents::GoogleCloudLoggingConfigurations::Update mount_mutation ::Mutations::AuditEvents::AmazonS3Configurations::Create + mount_mutation ::Mutations::AuditEvents::AmazonS3Configurations::Update mount_mutation ::Mutations::AuditEvents::Instance::GoogleCloudLoggingConfigurations::Create mount_mutation ::Mutations::Forecasting::BuildForecast, alpha: { milestone: '16.0' } mount_mutation ::Mutations::AuditEvents::Streaming::InstanceHeaders::Create diff --git a/ee/app/graphql/mutations/audit_events/amazon_s3_configurations/update.rb b/ee/app/graphql/mutations/audit_events/amazon_s3_configurations/update.rb new file mode 100644 index 00000000000000..8a19466e71a0d3 --- /dev/null +++ b/ee/app/graphql/mutations/audit_events/amazon_s3_configurations/update.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +module Mutations + module AuditEvents + module AmazonS3Configurations + class Update < Base + graphql_name 'AmazonS3ConfigurationUpdate' + + include ::Audit::Changes + + UPDATE_EVENT_NAME = 'amazon_s3_configuration_updated' + AUDIT_EVENT_COLUMNS = [:access_key_xid, :secret_access_key, :bucket_name, :aws_region, :name].freeze + + authorize :admin_external_audit_events + + argument :id, ::Types::GlobalIDType[::AuditEvents::AmazonS3Configuration], + required: true, + description: 'ID of the Amazon S3 configuration to update.' + + argument :name, GraphQL::Types::String, + required: false, + description: 'Destination name.' + + argument :access_key_xid, GraphQL::Types::String, + required: false, + description: 'Access key ID of the Amazon S3 account.' + + argument :secret_access_key, GraphQL::Types::String, + required: false, + description: 'Secret access key of the Amazon S3 account.' + + argument :bucket_name, GraphQL::Types::String, + required: false, + description: 'Name of the bucket where the audit events would be logged.' + + argument :aws_region, GraphQL::Types::String, + required: false, + description: 'AWS region where the bucket is created.' + + field :amazon_s3_configuration, ::Types::AuditEvents::AmazonS3ConfigurationType, + null: true, + description: 'Updated Amazon S3 configuration.' + + def resolve(id:, access_key_xid: nil, secret_access_key: nil, bucket_name: nil, aws_region: nil, name: nil) + config = authorized_find!(id) + config_attributes = { + access_key_xid: access_key_xid, + secret_access_key: secret_access_key, + bucket_name: bucket_name, + aws_region: aws_region, + name: name + }.compact + + if config.update(config_attributes) + audit_update(config) + { amazon_s3_configuration: config, errors: [] } + else + { amazon_s3_configuration: nil, errors: Array(config.errors) } + end + end + + private + + def audit_update(config) + AUDIT_EVENT_COLUMNS.each do |column| + audit_changes( + column, + as: column.to_s, + entity: config.group, + model: config, + event_type: UPDATE_EVENT_NAME + ) + end + end + + def find_object(config_gid) + GitlabSchema.object_from_id(config_gid, expected_type: ::AuditEvents::AmazonS3Configuration).sync + end + end + end + end +end diff --git a/ee/config/audit_events/types/amazon_s3_configuration_updated.yml b/ee/config/audit_events/types/amazon_s3_configuration_updated.yml new file mode 100644 index 00000000000000..512d07ab4e424d --- /dev/null +++ b/ee/config/audit_events/types/amazon_s3_configuration_updated.yml @@ -0,0 +1,9 @@ +--- +name: amazon_s3_configuration_updated +description: Triggered when Amazon S3 configuration for audit events streaming is updated. +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/423229 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133691 +feature_category: audit_events +milestone: '16.5' +saved_to_database: true +streamed: true diff --git a/ee/spec/requests/api/graphql/mutations/audit_events/amazon_s3_configurations/update_spec.rb b/ee/spec/requests/api/graphql/mutations/audit_events/amazon_s3_configurations/update_spec.rb new file mode 100644 index 00000000000000..7e29514846c0d5 --- /dev/null +++ b/ee/spec/requests/api/graphql/mutations/audit_events/amazon_s3_configurations/update_spec.rb @@ -0,0 +1,174 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Update Amazon S3 configuration', feature_category: :audit_events do + include GraphqlHelpers + + let_it_be_with_reload(:config) { create(:amazon_s3_configuration) } + let_it_be(:group) { config.group } + # let_it_be(:owner) { create(:user) } + let_it_be(:current_user) { create(:user) } + let_it_be(:updated_access_key_xid) { 'AKIA1234RANDOM5678' } + let_it_be(:updated_secret_access_key) { 'TEST/SECRET/XYZ/PQR' } + let_it_be(:updated_bucket_name) { 'test-rspec-bucket' } + let_it_be(:updated_aws_region) { 'us-east-2' } + let_it_be(:updated_destination_name) { 'updated_destination_name' } + let_it_be(:config_gid) { global_id_of(config) } + + let(:mutation) { graphql_mutation(:amazon_s3_configuration_update, input) } + let(:mutation_response) { graphql_mutation_response(:amazon_s3_configuration_update) } + + let(:input) do + { + id: config_gid, + accessKeyXid: updated_access_key_xid, + secretAccessKey: updated_secret_access_key, + bucketName: updated_bucket_name, + awsRegion: updated_aws_region, + 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 a group owner' do + before_all do + group.add_owner(current_user) + end + + it 'updates the configuration' do + mutate + + config.reload + + expect(config.access_key_xid).to eq(updated_access_key_xid) + expect(config.secret_access_key).to eq(updated_secret_access_key) + expect(config.bucket_name).to eq(updated_bucket_name) + expect(config.aws_region).to eq(updated_aws_region) + expect(config.name).to eq(updated_destination_name) + end + + it 'audits the update' do + Mutations::AuditEvents::AmazonS3Configurations::Update::AUDIT_EVENT_COLUMNS.each do |column| + message = if column == :secret_access_key + "Changed #{column}" + else + "Changed #{column} from #{config[column]} to #{input[column.to_s.camelize(:lower).to_sym]}" + end + + expected_hash = { + name: Mutations::AuditEvents::AmazonS3Configurations::Update::UPDATE_EVENT_NAME, + author: current_user, + scope: group, + target: config, + message: message + } + + 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, + accessKeyXid: config.access_key_xid, + 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::AmazonS3Configurations::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( + 'amazonS3Configuration' => nil, + 'errors' => ['error message'] + ) + end + end + end + + context 'when current user is a group maintainer' do + before_all do + group.add_maintainer(current_user) + end + + it_behaves_like 'a mutation on an unauthorized resource' + it_behaves_like 'a mutation that does not update the configuration' + end + + context 'when current user is a group developer' do + before_all do + group.add_developer(current_user) + end + + it_behaves_like 'a mutation on an unauthorized resource' + it_behaves_like 'a mutation that does not update the configuration' + end + + context 'when current user is a group guest' do + before_all do + group.add_guest(current_user) + end + + it_behaves_like 'a mutation on an unauthorized resource' + 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 on an unauthorized resource' + it_behaves_like 'a mutation that does not update the configuration' + end +end -- GitLab