diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index 4ef0d06703086ef36f343636e018d964d3b2f17e..dbdadf371ca24a0592e5fe942139a08c4892a824 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -4,7 +4,8 @@
"AlertManagementPrometheusIntegration"
],
"AmazonS3ConfigurationInterface": [
- "AmazonS3ConfigurationType"
+ "AmazonS3ConfigurationType",
+ "InstanceAmazonS3ConfigurationType"
],
"BaseHeaderInterface": [
"AuditEventStreamingHeader",
diff --git a/doc/administration/audit_event_streaming/audit_event_types.md b/doc/administration/audit_event_streaming/audit_event_types.md
index e0caad66fcabcea7447c1541c7da915900e907fd..fa89074ad53faf85dd113f783a540c98187ef935 100644
--- a/doc/administration/audit_event_streaming/audit_event_types.md
+++ b/doc/administration/audit_event_streaming/audit_event_types.md
@@ -55,6 +55,7 @@ Audit event types belong to the following product categories.
| [`google_cloud_logging_configuration_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122025) | Triggered when Google Cloud Logging configuration is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/409422) |
| [`google_cloud_logging_configuration_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122025) | Triggered when Google Cloud Logging configuration is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/409422) |
| [`google_cloud_logging_configuration_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122025) | Triggered when Google Cloud Logging configuration is updated| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/409422) |
+| [`instance_amazon_s3_configuration_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137651) | Triggered when instance Amazon S3 configuration for audit events streaming is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.7](https://gitlab.com/gitlab-org/gitlab/-/issues/423235) |
| [`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 | 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 | 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 | GitLab [16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/423039) |
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 714b9b8395f37b74fffc45d8bb88461049c9fd66..c826994474326d21f3cc463d04f2b38096c6b7c9 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1472,6 +1472,29 @@ Input type: `AuditEventsAmazonS3ConfigurationUpdateInput`
| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+### `Mutation.auditEventsInstanceAmazonS3ConfigurationCreate`
+
+Input type: `AuditEventsInstanceAmazonS3ConfigurationCreateInput`
+
+#### 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. |
+| `name` | [`String`](#string) | Destination name. |
+| `secretAccessKey` | [`String!`](#string) | Secret access key of the Amazon S3 account. |
+
+#### 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. |
+| `instanceAmazonS3Configuration` | [`InstanceAmazonS3ConfigurationType`](#instanceamazons3configurationtype) | Created instance Amazon S3 configuration. |
+
### `Mutation.auditEventsStreamingDestinationEventsAdd`
Input type: `AuditEventsStreamingDestinationEventsAddInput`
@@ -20317,6 +20340,20 @@ CI/CD variables a project inherites from its parent group and ancestors.
| `raw` | [`Boolean`](#boolean) | Indicates whether the variable is raw. |
| `variableType` | [`CiVariableType`](#civariabletype) | Type of the variable. |
+### `InstanceAmazonS3ConfigurationType`
+
+Stores instance level Amazon S3 configurations for audit event streaming.
+
+#### Fields
+
+| 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. |
+| `id` | [`ID!`](#id) | ID of the configuration. |
+| `name` | [`String!`](#string) | Name of the external destination to send audit events to. |
+
### `InstanceExternalAuditEventDestination`
Represents an external resource to send instance audit events to.
@@ -32377,6 +32414,7 @@ Implementations:
Implementations:
- [`AmazonS3ConfigurationType`](#amazons3configurationtype)
+- [`InstanceAmazonS3ConfigurationType`](#instanceamazons3configurationtype)
##### Fields
diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb
index 4ad78418af0d847d4f96de174e4adf0b9f9480c7..b5288431e0a2727e3ecd1df4b0c703061227221d 100644
--- a/ee/app/graphql/ee/types/mutation_type.rb
+++ b/ee/app/graphql/ee/types/mutation_type.rb
@@ -130,6 +130,7 @@ module MutationType
mount_mutation ::Mutations::AuditEvents::AmazonS3Configurations::Create
mount_mutation ::Mutations::AuditEvents::AmazonS3Configurations::Delete
mount_mutation ::Mutations::AuditEvents::AmazonS3Configurations::Update
+ mount_mutation ::Mutations::AuditEvents::Instance::AmazonS3Configurations::Create
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/instance/amazon_s3_configurations/base.rb b/ee/app/graphql/mutations/audit_events/instance/amazon_s3_configurations/base.rb
new file mode 100644
index 0000000000000000000000000000000000000000..702aa485fd740b5fe133e6b800e34d2000433a2e
--- /dev/null
+++ b/ee/app/graphql/mutations/audit_events/instance/amazon_s3_configurations/base.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Mutations
+ module AuditEvents
+ module Instance
+ module AmazonS3Configurations
+ 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 audit(config, action:)
+ audit_context = {
+ name: "instance_amazon_s3_configuration_#{action}",
+ author: current_user,
+ scope: Gitlab::Audit::InstanceScope.new,
+ target: config,
+ message: "#{action.capitalize} Instance Amazon S3 configuration with name: #{config.name} " \
+ "bucket: #{config.bucket_name} and AWS region: #{config.aws_region}"
+ }
+
+ ::Gitlab::Audit::Auditor.audit(audit_context)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/ee/app/graphql/mutations/audit_events/instance/amazon_s3_configurations/create.rb b/ee/app/graphql/mutations/audit_events/instance/amazon_s3_configurations/create.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2534ea8d69193a1bf4d8907407fd2630971ebccd
--- /dev/null
+++ b/ee/app/graphql/mutations/audit_events/instance/amazon_s3_configurations/create.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Mutations
+ module AuditEvents
+ module Instance
+ module AmazonS3Configurations
+ class Create < Base
+ graphql_name 'AuditEventsInstanceAmazonS3ConfigurationCreate'
+
+ argument :name, GraphQL::Types::String,
+ required: false,
+ description: 'Destination name.'
+
+ argument :access_key_xid, GraphQL::Types::String,
+ required: true,
+ description: 'Access key ID of the Amazon S3 account.'
+
+ argument :secret_access_key, GraphQL::Types::String,
+ required: true,
+ description: 'Secret access key of the Amazon S3 account.'
+
+ argument :bucket_name, GraphQL::Types::String,
+ required: true,
+ description: 'Name of the bucket where the audit events would be logged.'
+
+ argument :aws_region, GraphQL::Types::String,
+ required: true,
+ description: 'AWS region where the bucket is created.'
+
+ field :instance_amazon_s3_configuration, ::Types::AuditEvents::Instance::AmazonS3ConfigurationType,
+ null: true,
+ description: 'Created instance Amazon S3 configuration.'
+
+ def resolve(access_key_xid:, secret_access_key:, bucket_name:, aws_region:, name: nil)
+ config_attributes = {
+ access_key_xid: access_key_xid,
+ secret_access_key: secret_access_key,
+ bucket_name: bucket_name,
+ aws_region: aws_region,
+ name: name
+ }
+
+ config = ::AuditEvents::Instance::AmazonS3Configuration.new(config_attributes)
+
+ if config.save
+ audit(config, action: :created)
+
+ { instance_amazon_s3_configuration: config, errors: [] }
+ else
+ { instance_amazon_s3_configuration: nil, errors: Array(config.errors) }
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/ee/app/graphql/types/audit_events/instance/amazon_s3_configuration_type.rb b/ee/app/graphql/types/audit_events/instance/amazon_s3_configuration_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..517af4c482df0fa23eada417e2b22d9a28acfc4d
--- /dev/null
+++ b/ee/app/graphql/types/audit_events/instance/amazon_s3_configuration_type.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module AuditEvents
+ module Instance
+ class AmazonS3ConfigurationType < ::Types::BaseObject
+ graphql_name 'InstanceAmazonS3ConfigurationType'
+ description 'Stores instance level Amazon S3 configurations for audit event streaming.'
+ authorize :admin_instance_external_audit_events
+
+ implements AmazonS3ConfigurationInterface
+ end
+ end
+ end
+end
diff --git a/ee/app/policies/audit_events/instance/amazon_s3_configuration_policy.rb b/ee/app/policies/audit_events/instance/amazon_s3_configuration_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c83a5997a29a825bcdb037e4b90987682a0786ed
--- /dev/null
+++ b/ee/app/policies/audit_events/instance/amazon_s3_configuration_policy.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module AuditEvents
+ module Instance
+ class AmazonS3ConfigurationPolicy < BasePolicy
+ delegate { :global }
+ end
+ end
+end
diff --git a/ee/config/audit_events/types/instance_amazon_s3_configuration_created.yml b/ee/config/audit_events/types/instance_amazon_s3_configuration_created.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4fc819c53ef962bc82d79a49aadd45682042bc04
--- /dev/null
+++ b/ee/config/audit_events/types/instance_amazon_s3_configuration_created.yml
@@ -0,0 +1,9 @@
+---
+name: instance_amazon_s3_configuration_created
+description: Triggered when instance Amazon S3 configuration for audit events streaming is created
+introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/423235
+introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137651
+feature_category: audit_events
+milestone: '16.7'
+saved_to_database: true
+streamed: true
diff --git a/ee/spec/requests/api/graphql/mutations/audit_events/instance/amazon_s3_configurations/create_spec.rb b/ee/spec/requests/api/graphql/mutations/audit_events/instance/amazon_s3_configurations/create_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..86746b760d639837c7ba91fac8630902d419470f
--- /dev/null
+++ b/ee/spec/requests/api/graphql/mutations/audit_events/instance/amazon_s3_configurations/create_spec.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Create Instance level Amazon S3 configuration', feature_category: :audit_events do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:admin) }
+ let_it_be(:destination_name) { 'test_aws_s3_destination' }
+ let_it_be(:access_key_id) { 'AKIARANDOMID1234' }
+ let_it_be(:secret_access_key) { 'TEST/SECRET/XYZ' }
+ let_it_be(:bucket_name) { 'test-bucket' }
+ let_it_be(:aws_region) { 'us-east-1' }
+
+ let(:mutation) { graphql_mutation(:audit_events_instance_amazon_s3_configuration_create, input) }
+ let(:mutation_response) { graphql_mutation_response(:audit_events_instance_amazon_s3_configuration_create) }
+
+ let(:input) do
+ {
+ name: destination_name,
+ accessKeyXid: access_key_id,
+ secretAccessKey: secret_access_key,
+ bucketName: bucket_name,
+ awsRegion: aws_region
+ }
+ 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::AmazonS3Configuration.last
+
+ expect(Gitlab::Audit::Auditor).to have_received(:audit) do |args|
+ expect(args[:name]).to eq('instance_amazon_s3_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 Amazon S3 configuration with name: #{destination_name} " \
+ "bucket: #{bucket_name} and AWS region: #{aws_region}")
+ 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::AmazonS3Configuration.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', :aggregate_failures do
+ expect { mutate }.to change { AuditEvents::Instance::AmazonS3Configuration.count }.by(1)
+
+ config = AuditEvents::Instance::AmazonS3Configuration.last
+
+ expect(config.name).to eq(destination_name)
+ expect(config.access_key_xid).to eq(access_key_id)
+ expect(config.secret_access_key).to eq(secret_access_key)
+ expect(config.bucket_name).to eq(bucket_name)
+ expect(config.aws_region).to eq(aws_region)
+
+ expect(mutation_response['errors']).to be_empty
+ expect(mutation_response['instanceAmazonS3Configuration']['accessKeyXid']).to eq(access_key_id)
+ expect(mutation_response['instanceAmazonS3Configuration']['id']).not_to be_empty
+ expect(mutation_response['instanceAmazonS3Configuration']['secretAccessKey']).to eq(nil)
+ expect(mutation_response['instanceAmazonS3Configuration']['bucketName']).to eq(bucket_name)
+ expect(mutation_response['instanceAmazonS3Configuration']['awsRegion']).to eq(aws_region)
+ end
+
+ it_behaves_like 'creates an audit event', 'audit_events'
+
+ context 'when there is error while saving' do
+ before do
+ allow_next_instance_of(AuditEvents::Instance::AmazonS3Configuration) do |s3_configuration|
+ allow(s3_configuration).to receive(:save).and_return(false)
+ errors = ActiveModel::Errors.new(s3_configuration).tap { |e| e.add(:bucket_name, 'invalid name') }
+ allow(s3_configuration).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::AmazonS3Configuration.count }
+
+ expect(mutation_response).to include(
+ 'instanceAmazonS3Configuration' => nil,
+ 'errors' => ["Bucket name invalid name"]
+ )
+ end
+ end
+ end
+
+ context 'when current user is not an admin' do
+ let_it_be(:current_user) { create(: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