Commit 5017e232 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch '344664-audit-event-streaming' into 'master'

Auditing for changes to event streaming destinations

See merge request gitlab-org/gitlab!74632
parents 7ba295db fa533c72
...@@ -98,6 +98,7 @@ From there, you can see the following actions: ...@@ -98,6 +98,7 @@ From there, you can see the following actions:
- Roles allowed to create project changed. - Roles allowed to create project changed.
- Group CI/CD variable added, removed, or protected status changed. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30857) in GitLab 13.3. - Group CI/CD variable added, removed, or protected status changed. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30857) in GitLab 13.3.
- Compliance framework created, updated, or deleted. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340649) in GitLab 14.5. - Compliance framework created, updated, or deleted. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340649) in GitLab 14.5.
- Event streaming destination created, updated, or deleted. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344664) in GitLab 14.6.
Group events can also be accessed via the [Group Audit Events API](../api/audit_events.md#group-audit-events) Group events can also be accessed via the [Group Audit Events API](../api/audit_events.md#group-audit-events)
......
# frozen_string_literal: true
module Mutations
module AuditEvents
module ExternalAuditEventDestinations
class Base < BaseMutation
private
def audit(destination, action:, extra_context: {})
audit_context = {
name: "#{action}_event_streaming_destination",
author: current_user,
scope: destination.group,
target: destination.group,
message: "#{action.capitalize} event streaming destination #{destination.destination_url}"
}
::Gitlab::Audit::Auditor.audit(audit_context.merge(extra_context))
end
end
end
end
end
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Mutations module Mutations
module AuditEvents module AuditEvents
module ExternalAuditEventDestinations module ExternalAuditEventDestinations
class Create < BaseMutation class Create < Base
graphql_name 'ExternalAuditEventDestinationCreate' graphql_name 'ExternalAuditEventDestinationCreate'
authorize :admin_external_audit_events authorize :admin_external_audit_events
...@@ -22,12 +22,11 @@ module Mutations ...@@ -22,12 +22,11 @@ module Mutations
def resolve(destination_url:, group_path:) def resolve(destination_url:, group_path:)
group = authorized_find!(group_path) group = authorized_find!(group_path)
destination = ::AuditEvents::ExternalAuditEventDestination.create(group: group, destination_url: destination_url) destination = ::AuditEvents::ExternalAuditEventDestination.new(group: group, destination_url: destination_url)
{ audit(destination, action: :create) if destination.save
external_audit_event_destination: destination&.persisted? ? destination : nil,
errors: Array(destination.errors) { external_audit_event_destination: (destination if destination.persisted?), errors: Array(destination.errors) }
}
end end
private private
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Mutations module Mutations
module AuditEvents module AuditEvents
module ExternalAuditEventDestinations module ExternalAuditEventDestinations
class Destroy < BaseMutation class Destroy < Base
graphql_name 'ExternalAuditEventDestinationDestroy' graphql_name 'ExternalAuditEventDestinationDestroy'
authorize :admin_external_audit_events authorize :admin_external_audit_events
...@@ -15,7 +15,9 @@ module Mutations ...@@ -15,7 +15,9 @@ module Mutations
def resolve(id:) def resolve(id:)
destination = authorized_find!(id) destination = authorized_find!(id)
destination.destroy if destination if destination.destroy
audit(destination, action: :destroy)
end
{ {
external_audit_event_destination: nil, external_audit_event_destination: nil,
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Mutations module Mutations
module AuditEvents module AuditEvents
module ExternalAuditEventDestinations module ExternalAuditEventDestinations
class Update < BaseMutation class Update < Base
graphql_name 'ExternalAuditEventDestinationUpdate' graphql_name 'ExternalAuditEventDestinationUpdate'
authorize :admin_external_audit_events authorize :admin_external_audit_events
...@@ -23,16 +23,24 @@ module Mutations ...@@ -23,16 +23,24 @@ module Mutations
def resolve(id:, destination_url:) def resolve(id:, destination_url:)
destination = authorized_find!(id) destination = authorized_find!(id)
destination.update(destination_url: destination_url) if destination audit_update(destination) if destination.update(destination_url: destination_url)
{ {
external_audit_event_destination: destination, external_audit_event_destination: (destination if destination.persisted?),
errors: Array(destination.errors) errors: Array(destination.errors)
} }
end end
private private
def audit_update(destination)
return unless destination.previous_changes.any?
message = "Updated event streaming destination from #{destination.previous_changes['destination_url'].join(' to ')}"
audit(destination, action: :update, extra_context: { message: message })
end
def find_object(destination_gid) def find_object(destination_gid)
GitlabSchema.object_from_id(destination_gid, expected_type: ::AuditEvents::ExternalAuditEventDestination).sync GitlabSchema.object_from_id(destination_gid, expected_type: ::AuditEvents::ExternalAuditEventDestination).sync
end end
......
...@@ -9,6 +9,8 @@ RSpec.describe 'Create an external audit event destination' do ...@@ -9,6 +9,8 @@ RSpec.describe 'Create an external audit event destination' do
let_it_be(:owner) { create(:user) } let_it_be(:owner) { create(:user) }
let(:current_user) { owner } let(:current_user) { owner }
let(:mutation) { graphql_mutation(:external_audit_event_destination_create, input) }
let(:mutation_response) { graphql_mutation_response(:external_audit_event_destination_create) }
let(:input) do let(:input) do
{ {
...@@ -17,18 +19,28 @@ RSpec.describe 'Create an external audit event destination' do ...@@ -17,18 +19,28 @@ RSpec.describe 'Create an external audit event destination' do
} }
end end
let(:mutation) { graphql_mutation(:external_audit_event_destination_create, input) } let(:invalid_input) do
{
let(:mutation_response) { graphql_mutation_response(:external_audit_event_destination_create) } 'groupPath': group.full_path,
'destinationUrl': 'ftp://gitlab.com/example/testendpoint'
}
end
shared_examples 'a mutation that does not create a destination' do shared_examples 'a mutation that does not create a destination' do
it 'does not destroy the destination' do it 'does not destroy the destination' do
expect { post_graphql_mutation(mutation, current_user: owner) } expect { post_graphql_mutation(mutation, current_user: owner) }
.not_to change { AuditEvents::ExternalAuditEventDestination.count } .not_to change { AuditEvents::ExternalAuditEventDestination.count }
end end
it 'does not audit the creation' do
expect { post_graphql_mutation(mutation, current_user: owner) }
.not_to change { AuditEvent.count }
end
end end
context 'when feature is licensed' do context 'when feature is licensed' do
subject { post_graphql_mutation(mutation, current_user: owner) }
before do before do
stub_licensed_features(external_audit_events: true) stub_licensed_features(external_audit_events: true)
end end
...@@ -39,19 +51,28 @@ RSpec.describe 'Create an external audit event destination' do ...@@ -39,19 +51,28 @@ RSpec.describe 'Create an external audit event destination' do
end end
it 'creates the destination' do it 'creates the destination' do
expect { post_graphql_mutation(mutation, current_user: owner) } expect { subject }
.to change { AuditEvents::ExternalAuditEventDestination.count }.by(1) .to change { AuditEvents::ExternalAuditEventDestination.count }.by(1)
end end
it 'audits the creation' do
expect { subject }
.to change { AuditEvent.count }.by(1)
expect(AuditEvent.last.details[:custom_message]).to eq("Create event streaming destination https://gitlab.com/example/testendpoint")
end end
context 'when current user is a group owner' do context 'when destination is invalid' do
before do let(:mutation) { graphql_mutation(:external_audit_event_destination_create, invalid_input) }
group.add_owner(owner)
it 'returns correct errors' do
post_graphql_mutation(mutation, current_user: owner)
expect(mutation_response['externalAuditEventDestination']).to be_nil
expect(mutation_response['errors']).to contain_exactly('Destination url is blocked: Only allowed schemes are http, https')
end end
it 'creates the destination' do it_behaves_like 'a mutation that does not create a destination'
expect { post_graphql_mutation(mutation, current_user: owner) }
.to change { AuditEvents::ExternalAuditEventDestination.count }.by(1)
end end
end end
......
...@@ -26,6 +26,11 @@ RSpec.describe 'Destroy an external audit event destination' do ...@@ -26,6 +26,11 @@ RSpec.describe 'Destroy an external audit event destination' do
expect { post_graphql_mutation(mutation, current_user: owner) } expect { post_graphql_mutation(mutation, current_user: owner) }
.not_to change { AuditEvents::ExternalAuditEventDestination.count } .not_to change { AuditEvents::ExternalAuditEventDestination.count }
end end
it 'does not audit the destruction' do
expect { post_graphql_mutation(mutation, current_user: owner) }
.not_to change { AuditEvent.count }
end
end end
context 'when feature is licensed' do context 'when feature is licensed' do
...@@ -62,6 +67,13 @@ RSpec.describe 'Destroy an external audit event destination' do ...@@ -62,6 +67,13 @@ RSpec.describe 'Destroy an external audit event destination' do
expect { post_graphql_mutation(mutation, current_user: owner) } expect { post_graphql_mutation(mutation, current_user: owner) }
.to change { AuditEvents::ExternalAuditEventDestination.count }.by(-1) .to change { AuditEvents::ExternalAuditEventDestination.count }.by(-1)
end end
it 'audits the destruction' do
expect { post_graphql_mutation(mutation, current_user: owner) }
.to change { AuditEvent.count }.by(1)
expect(AuditEvent.last.details[:custom_message]).to match /Destroy event streaming destination/
end
end end
context 'when current user is a group maintainer' do context 'when current user is a group maintainer' do
......
...@@ -27,6 +27,11 @@ RSpec.describe 'Update an external audit event destination' do ...@@ -27,6 +27,11 @@ RSpec.describe 'Update an external audit event destination' do
expect { post_graphql_mutation(mutation, current_user: owner) } expect { post_graphql_mutation(mutation, current_user: owner) }
.not_to change { destination.reload.destination_url } .not_to change { destination.reload.destination_url }
end end
it 'does not audit the update' do
expect { post_graphql_mutation(mutation, current_user: owner) }
.not_to change { AuditEvent.count }
end
end end
context 'when feature is licensed' do context 'when feature is licensed' do
...@@ -63,6 +68,13 @@ RSpec.describe 'Update an external audit event destination' do ...@@ -63,6 +68,13 @@ RSpec.describe 'Update an external audit event destination' do
expect { post_graphql_mutation(mutation, current_user: owner) } expect { post_graphql_mutation(mutation, current_user: owner) }
.to change { destination.reload.destination_url }.to("https://example.com/test") .to change { destination.reload.destination_url }.to("https://example.com/test")
end end
it 'audits the update' do
expect { post_graphql_mutation(mutation, current_user: owner) }
.to change { AuditEvent.count }.by(1)
expect(AuditEvent.last.details[:custom_message]).to match(/Updated event streaming destination from .* to .*/)
end
end end
context 'when current user is a group maintainer' do context 'when current user is a group maintainer' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment