Skip to content
Snippets Groups Projects
Commit 22bdc6c5 authored by Rajendra Kadam's avatar Rajendra Kadam :two:
Browse files

Merge branch '404730-instance-scope' into 'master'

Adding new audit event scope for instance level audit events

See merge request !123882



Merged-by: Rajendra Kadam's avatarRajendra Kadam <rkadam@gitlab.com>
Approved-by: Rajendra Kadam's avatarRajendra Kadam <rkadam@gitlab.com>
Approved-by: default avatarHarsimar Sandhu <hsandhu@gitlab.com>
Reviewed-by: Rajendra Kadam's avatarRajendra Kadam <rkadam@gitlab.com>
Reviewed-by: default avatarHarsimar Sandhu <hsandhu@gitlab.com>
Reviewed-by: default avatarHitesh Raghuvanshi <hraghuvanshi@gitlab.com>
Co-authored-by: default avatarHitesh Raghuvanshi <hraghuvanshi@gitlab.com>
parents b327c8a2 cb0d8ec0
No related branches found
No related tags found
1 merge request!123882Adding new audit event scope for instance level audit events
Pipeline #923086035 passed
Showing
with 236 additions and 29 deletions
......@@ -25,6 +25,18 @@ def find_object(destination_gid)
destination
end
def audit(destination, action:)
audit_context = {
name: "#{action}_instance_event_streaming_destination",
author: current_user,
scope: Gitlab::Audit::InstanceScope.new,
target: destination,
message: "#{action.capitalize} instance event streaming destination #{destination.destination_url}"
}
::Gitlab::Audit::Auditor.audit(audit_context)
end
end
end
end
......
......@@ -19,7 +19,7 @@ class Create < Base
def resolve(destination_url:)
destination = ::AuditEvents::InstanceExternalAuditEventDestination.new(destination_url: destination_url)
destination.save
audit(destination, action: :create) if destination.save
{
instance_external_audit_event_destination: (destination if destination.persisted?),
......
......@@ -21,7 +21,13 @@ module AuditEvent
attr_accessor :root_group_entity_id
def entity
strong_memoize(:entity) { lazy_entity }
strong_memoize(:entity) do
if entity_type == ::Gitlab::Audit::InstanceScope.name
::Gitlab::Audit::InstanceScope.new
else
lazy_entity
end
end
end
def root_group_entity
......@@ -57,16 +63,6 @@ def ip_address
super&.to_s || details[:ip_address]
end
def lazy_entity
BatchLoader.for(entity_id)
.batch(
key: entity_type, default_value: ::Gitlab::Audit::NullEntity.new
) do |ids, loader, args|
model = Object.const_get(args[:key], false)
model.where(id: ids).find_each { |record| loader.call(record.id, record) }
end
end
def stream_to_external_destinations(use_json: false, event_name: 'audit_operation')
return unless can_stream_to_external_destination?(event_name)
......@@ -80,6 +76,16 @@ def entity_is_group_or_project?
private
def lazy_entity
BatchLoader.for(entity_id)
.batch(
key: entity_type, default_value: ::Gitlab::Audit::NullEntity.new
) do |ids, loader, args|
model = Object.const_get(args[:key], false)
model.where(id: ids).find_each { |record| loader.call(record.id, record) }
end
end
def can_stream_to_external_destination?(event_name)
return false if entity.nil?
......
......@@ -36,8 +36,9 @@ def object
def object_url
return if entity.is_a?(Gitlab::Audit::NullEntity)
url_for(entity)
return Gitlab::Routing.url_helpers.admin_root_url if entity.is_a?(Gitlab::Audit::InstanceScope)
url_for(entity)
rescue NoMethodError
''
end
......@@ -57,6 +58,6 @@ def author
end
def entity
@entity ||= audit_event.lazy_entity
@entity ||= audit_event.entity
end
end
name: create_instance_event_streaming_destination
description: Event triggered when an instance level external audit event destination is created
introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/404730
introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123882
feature_category: audit_events
milestone: "16.2"
saved_to_database: true
streamed: true
......@@ -37,7 +37,7 @@ def audit_enabled?
return true if ::License.feature_available?(:admin_audit_log)
return true if ::License.feature_available?(:extended_audit_events)
scope.respond_to?(:feature_available?) && scope.licensed_feature_available?(:audit_events)
scope.respond_to?(:licensed_feature_available?) && scope.licensed_feature_available?(:audit_events)
end
end
end
......
......@@ -8,7 +8,7 @@ def self.preload!(audit_events)
audit_events.tap do |audit_events|
audit_events.each do |audit_event|
audit_event.lazy_author
audit_event.lazy_entity
audit_event.entity
end
end
end
......@@ -21,7 +21,7 @@ def find_each(&block)
@audit_events.each_batch(column: :created_at) do |relation|
relation.each do |audit_event|
audit_event.lazy_author
audit_event.lazy_entity
audit_event.entity
end
relation.each do |audit_event|
......
# frozen_string_literal: true
module Gitlab
module Audit
class InstanceScope
SCOPE_NAME = "gitlab_instance"
SCOPE_ID = 1
attr_reader :id, :name, :full_path
def initialize
@id = SCOPE_ID
@name = SCOPE_NAME
@full_path = SCOPE_NAME
end
def licensed_feature_available?(feature)
::License.feature_available?(feature)
end
end
end
end
......@@ -103,6 +103,29 @@
end
end
describe 'instance events' do
let(:destination) { create(:instance_external_audit_event_destination) }
before do
audit_context = {
name: "create_instance_event_streaming_destination",
author: admin,
scope: Gitlab::Audit::InstanceScope.new,
target: destination,
message: "Create instance event streaming destination #{destination.destination_url}"
}
::Gitlab::Audit::Auditor.audit(audit_context)
visit admin_audit_logs_path
end
it 'has instance audit event' do
expect(page).to have_content('gitlab_instance')
expect(page).to have_content('Create instance event streaming destination')
end
end
describe 'filter by date' do
let_it_be(:audit_event_1) { create(:user_audit_event, created_at: 5.days.ago) }
let_it_be(:audit_event_2) { create(:user_audit_event, created_at: 3.days.ago) }
......
......@@ -304,19 +304,23 @@
}
end
it 'logs audit event to database', :aggregate_failures do
expect { audit! }.to change(AuditEvent, :count).by(1)
shared_examples 'logs event to database' do
it 'logs audit event to database', :aggregate_failures do
expect { audit! }.to change(AuditEvent, :count).by(1)
audit_event = AuditEvent.last
audit_event = AuditEvent.last
expect(audit_event.author_id).to eq(author.id)
expect(audit_event.entity_id).to eq(scope.id)
expect(audit_event.entity_type).to eq(scope.class.name)
expect(audit_event.details[:target_id]).to eq(target.id)
expect(audit_event.details[:target_type]).to eq(target.class.name)
expect(audit_event.details[:custom_message]).to eq('Project has been deleted')
expect(audit_event.author_id).to eq(author.id)
expect(audit_event.entity_id).to eq(scope.id)
expect(audit_event.entity_type).to eq(scope.class.name)
expect(audit_event.details[:target_id]).to eq(target.id)
expect(audit_event.details[:target_type]).to eq(target.class.name)
expect(audit_event.details[:custom_message]).to eq('Project has been deleted')
end
end
it_behaves_like 'logs event to database'
it 'does not bulk insert and uses save to insert' do
expect(AuditEvent).not_to receive(:bulk_insert!)
expect_next_instance_of(AuditEvent) do |instance|
......@@ -359,6 +363,22 @@
it_behaves_like 'only streamed'
end
context 'when the scope of event is instance' do
let(:scope) { Gitlab::Audit::InstanceScope.new }
let(:context) do
{
name: name,
author: author,
scope: scope,
target: target,
message: 'Project has been deleted'
}
end
it_behaves_like 'logs event to database'
end
end
context 'when audit events are invalid' do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Audit::Events::Preloader do
RSpec.describe Gitlab::Audit::Events::Preloader, feature_category: :audit_events do
let_it_be(:audit_events) do
[
create(:audit_event, created_at: 2.days.ago),
......@@ -31,7 +31,7 @@
#
expect do
subject.map do |event|
[event.author_name, event.lazy_entity.name]
[event.author_name, event.entity.name]
end
end.not_to exceed_query_limit(3)
end
......@@ -59,7 +59,7 @@
# SELECT "users".* FROM "users" WHERE "users"."id" IN (2, 4) ORDER BY "users"."id" ASC LIMIT 1000
expect do
preloader.find_each do |event|
[event.author_name, event.lazy_entity.name]
[event.author_name, event.entity.name]
end
end.not_to exceed_query_limit(5)
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Audit::InstanceScope, feature_category: :audit_events do
describe '#initialize' do
it 'sets correct attributes' do
expect(described_class.new)
.to have_attributes(id: 1, name: Gitlab::Audit::InstanceScope::SCOPE_NAME,
full_path: Gitlab::Audit::InstanceScope::SCOPE_NAME)
end
describe '#licensed_feature_available?' do
subject { described_class.new.licensed_feature_available?(:external_audit_events) }
context 'when license is available' do
before do
stub_licensed_features(external_audit_events: true)
end
it { is_expected.to be_truthy }
end
context 'when license is not available' do
it { is_expected.to be_falsey }
end
end
end
end
......@@ -292,6 +292,16 @@
expect(event.entity).to be_a(Gitlab::Audit::NullEntity)
end
end
context 'when entity is the instance' do
let_it_be(:instance_scope) { Gitlab::Audit::InstanceScope.new }
subject(:event) { described_class.new(entity_id: instance_scope.id, entity_type: instance_scope.class.name) }
it 'returns a InstanceScope object' do
expect(event.entity).to be_a(Gitlab::Audit::InstanceScope)
end
end
end
describe '#root_group_entity' do
......
......@@ -121,6 +121,18 @@
expect(presenter.object_url).to be_blank
end
context 'when object is of type instance scope' do
let_it_be(:audit_event) do
create(
:audit_event, :instance_event
)
end
it 'returns the instance admin root url' do
expect(presenter.object_url).to eq(Gitlab::Routing.url_helpers.admin_root_url)
end
end
context 'when a project in a user namespace has been deleted' do
let(:project) { build(:project, namespace: create(:user).namespace).destroy! }
let(:audit_event) do
......
......@@ -24,6 +24,21 @@
}
end
shared_examples 'creates an audit event' do
it 'audits the creation' do
expect(AuditEvents::AuditEventStreamingWorker).to receive(:perform_async).with(
"create_instance_event_streaming_destination",
nil,
anything
)
expect { subject }
.to change { AuditEvent.count }.by(1)
expect(AuditEvent.last.details[:custom_message]).to eq("Create instance event streaming destination https://gitlab.com/example/testendpoint")
end
end
shared_examples 'a mutation that does not create a destination' do
subject { post_graphql_mutation(mutation, current_user: current_user) }
......@@ -61,6 +76,8 @@
expect(mutation_response['instanceExternalAuditEventDestination']['verificationToken']).not_to be_empty
end
it_behaves_like 'creates an audit event'
context 'when destination is invalid' do
let(:mutation) { graphql_mutation(:instance_external_audit_event_destination_create, invalid_input) }
......
......@@ -342,6 +342,30 @@
include_context 'audit event stream'
end
end
context 'when the entity is InstanceScope' do
let_it_be(:event) { create(:audit_event, :instance_event) }
subject { worker.perform('audit_operation', nil, event.to_json) }
context 'when the gitlab instance has an external destination' do
let_it_be(:destination) { create(:instance_external_audit_event_destination) }
it 'receives HTTP call at destination' do
expect(Gitlab::HTTP).to receive(:post).with(destination.destination_url, anything).once
subject
end
end
context 'when the gitlab instance does not have any external destination' do
let_it_be(:event) { create(:audit_event, :instance_event) }
subject { worker.perform('audit_operation', nil, event.to_json) }
it_behaves_like 'no HTTP calls are made'
end
end
end
context 'when connecting to redis fails' do
......
......@@ -88,6 +88,29 @@
end
end
trait :instance_event do
transient { instance_scope { Gitlab::Audit::InstanceScope.new } }
entity_type { Gitlab::Audit::InstanceScope.name }
entity_id { instance_scope.id }
entity_path { instance_scope.full_path }
target_details { instance_scope.name }
ip_address { IPAddr.new '127.0.0.1' }
details do
{
change: 'project_creation_level',
from: nil,
to: 'Developers + Maintainers',
author_name: user.name,
target_id: instance_scope.id,
target_type: Gitlab::Audit::InstanceScope.name,
target_details: instance_scope.name,
ip_address: '127.0.0.1',
entity_path: instance_scope.full_path
}
end
end
factory :project_audit_event, traits: [:project_event]
factory :group_audit_event, traits: [:group_event]
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