Skip to content
Snippets Groups Projects
Verified Commit a08acfea authored by Hitesh Raghuvanshi's avatar Hitesh Raghuvanshi :two: Committed by GitLab
Browse files

Added update api for group audit event destinations

Added GraphQL Update api for updating top-level group audit event
 streaming consolidated destinations

Changelog: added
EE: true
parent 6e8654c1
No related branches found
No related tags found
1 merge request!148388Update apis for group audit events
......@@ -79,6 +79,7 @@ Audit event types belong to the following product categories.
| [`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) | Instance |
| [`update_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74632) | Event triggered when an external audit event destination is updated| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [14.6](https://gitlab.com/gitlab-org/gitlab/-/issues/344664) | Group |
| [`update_instance_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125846) | Event triggered when an instance level external audit event destination is updated| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.2](https://gitlab.com/gitlab-org/gitlab/-/issues/404730) | Instance |
| [`updated_group_audit_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/148388) | Event triggered when an external audit event destination for a top-level group is updated.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.11](https://gitlab.com/gitlab-org/gitlab/-/issues/436610) | Group |
### Build artifacts
......
......@@ -4783,6 +4783,33 @@ Input type: `GroupAuditEventStreamingDestinationsDeleteInput`
| <a id="mutationgroupauditeventstreamingdestinationsdeleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationgroupauditeventstreamingdestinationsdeleteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
 
### `Mutation.groupAuditEventStreamingDestinationsUpdate`
DETAILS:
**Introduced** in GitLab 16.11.
**Status**: Experiment.
Input type: `GroupAuditEventStreamingDestinationsUpdateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationgroupauditeventstreamingdestinationsupdatecategory"></a>`category` | [`String`](#string) | Destination category. |
| <a id="mutationgroupauditeventstreamingdestinationsupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationgroupauditeventstreamingdestinationsupdateconfig"></a>`config` | [`JSON`](#json) | Destination config. |
| <a id="mutationgroupauditeventstreamingdestinationsupdateid"></a>`id` | [`AuditEventsGroupExternalStreamingDestinationID!`](#auditeventsgroupexternalstreamingdestinationid) | ID of external audit event destination to update. |
| <a id="mutationgroupauditeventstreamingdestinationsupdatename"></a>`name` | [`String`](#string) | Destination name. |
| <a id="mutationgroupauditeventstreamingdestinationsupdatesecrettoken"></a>`secretToken` | [`String`](#string) | Secret token. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationgroupauditeventstreamingdestinationsupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationgroupauditeventstreamingdestinationsupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationgroupauditeventstreamingdestinationsupdateexternalauditeventdestination"></a>`externalAuditEventDestination` | [`GroupAuditEventStreamingDestination`](#groupauditeventstreamingdestination) | Updated destination. |
### `Mutation.groupMemberBulkUpdate`
 
Input type: `GroupMemberBulkUpdateInput`
......@@ -172,6 +172,8 @@ module MutationType
alpha: { milestone: '16.11' }
mount_mutation ::Mutations::AuditEvents::Group::AuditEventStreamingDestinations::Delete,
alpha: { milestone: '16.11' }
mount_mutation ::Mutations::AuditEvents::Group::AuditEventStreamingDestinations::Update,
alpha: { milestone: '16.11' }
mount_mutation ::Mutations::AuditEvents::Instance::AuditEventStreamingDestinations::Create,
alpha: { milestone: '16.11' }
......
# frozen_string_literal: true
module Mutations
module AuditEvents
module Group
module AuditEventStreamingDestinations
class Update < Base
graphql_name 'GroupAuditEventStreamingDestinationsUpdate'
include ::Audit::Changes
UPDATE_EVENT_NAME = 'updated_group_audit_event_streaming_destination'
AUDIT_EVENT_COLUMNS = [:config, :name, :category, :secret_token].freeze
argument :id, ::Types::GlobalIDType[::AuditEvents::Group::ExternalStreamingDestination],
required: true,
description: 'ID of external audit event destination to update.'
argument :config, GraphQL::Types::JSON, # rubocop:disable Graphql/JSONType -- Different type of destinations will have different configs
required: false,
description: 'Destination config.'
argument :name, GraphQL::Types::String,
required: false,
description: 'Destination name.'
argument :category, GraphQL::Types::String,
required: false,
description: 'Destination category.'
argument :secret_token, GraphQL::Types::String,
required: false,
description: 'Secret token.'
field :external_audit_event_destination, ::Types::AuditEvents::Group::StreamingDestinationType,
null: true,
description: 'Updated destination.'
def resolve(id:, config: nil, name: nil, category: nil, secret_token: nil)
destination = authorized_find!(id: id)
destination_attributes = {
config: config,
name: name,
category: category,
secret_token: secret_token
}.compact
if destination.update(destination_attributes)
audit_update(destination)
{
external_audit_event_destination: destination,
errors: []
}
else
{ external_audit_event_destination: nil, errors: Array(destination.errors) }
end
end
private
def audit_update(destination)
AUDIT_EVENT_COLUMNS.each do |column|
audit_changes(
column,
as: column.to_s,
entity: destination.group,
model: destination,
event_type: UPDATE_EVENT_NAME
)
end
end
end
end
end
end
end
name: updated_group_audit_event_streaming_destination
description: Event triggered when an external audit event destination for a top-level group is updated.
introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/436610
introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/148388
feature_category: audit_events
milestone: "16.11"
saved_to_database: true
streamed: true
scope: [Group]
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Update group level external audit event streaming destination', feature_category: :audit_events do
include GraphqlHelpers
let_it_be_with_reload(:destination) { create(:audit_events_group_external_streaming_destination) }
let_it_be(:group) { destination.group }
let_it_be(:current_user) { create(:user) }
let_it_be(:updated_config) do
{
"accessKeyXid" => 'AKIA1234RANDOM5678',
"bucketName" => 'test-rspec-bucket',
"awsRegion" => 'us-east-2'
}
end
let_it_be(:updated_secret_token) { 'TEST/SECRET/XYZ/PQR' }
let_it_be(:updated_category) { 'aws' }
let_it_be(:updated_destination_name) { 'updated_destination_name' }
let_it_be(:destination_gid) { global_id_of(destination) }
let(:mutation) { graphql_mutation(:group_audit_event_streaming_destinations_update, input) }
let(:mutation_response) { graphql_mutation_response(:group_audit_event_streaming_destinations_update) }
let(:input) do
{
id: destination_gid,
config: updated_config,
name: updated_destination_name,
category: updated_category,
secret_token: updated_secret_token
}
end
subject(:mutate) { post_graphql_mutation(mutation, current_user: current_user) }
shared_examples 'a mutation that does not update the destination' do
it 'does not update the destination' do
expect { mutate }.not_to change { destination.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 destination' do
mutate
destination.reload
expect(destination.config).to eq(updated_config)
expect(destination.name).to eq(updated_destination_name)
expect(destination.category).to eq(updated_category)
expect(destination.secret_token).to eq(updated_secret_token)
end
it 'audits the update' do
Mutations::AuditEvents::Group::AuditEventStreamingDestinations::Update::AUDIT_EVENT_COLUMNS.each do |column|
message = if column == :secret_token
"Changed #{column}"
else
"Changed #{column} from #{destination[column]} to #{input[column.to_s.camelize(:lower).to_sym]}"
end
expected_hash = {
name: Mutations::AuditEvents::Group::AuditEventStreamingDestinations::Update::UPDATE_EVENT_NAME,
author: current_user,
scope: group,
target: destination,
message: message
}
expect(Gitlab::Audit::Auditor).to receive(:audit).once.ordered.with(hash_including(expected_hash))
end
mutate
end
context 'when the fields are updated with existing values' do
let(:input) do
{
id: destination_gid,
config: destination.config,
name: destination.name
}
end
it 'does not audit the event' do
expect(Gitlab::Audit::Auditor).not_to receive(:audit)
mutate
end
end
context 'when no fields are provided for update' do
let(:input) do
{
id: destination_gid
}
end
it_behaves_like 'a mutation that does not update the destination'
end
context 'when there is error while updating' do
before do
allow_next_instance_of(Mutations::AuditEvents::Group::AuditEventStreamingDestinations::Update) do |mutation|
allow(mutation).to receive(:authorized_find!).with(id: destination_gid).and_return(destination)
end
allow(destination).to receive(:update).and_return(false)
errors = ActiveModel::Errors.new(destination).tap { |e| e.add(:base, 'error message') }
allow(destination).to receive(:errors).and_return(errors)
end
it 'does not update the destination and returns the error' do
mutate
expect(mutation_response).to include(
'externalAuditEventDestination' => 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 destination'
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 destination'
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 destination'
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 destination'
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment