Commit b95b38bb authored by Max Woolf's avatar Max Woolf

Merge branch 'mw/predate-audit-events' into 'master'

Add ability to pre-date audit events

See merge request gitlab-org/gitlab!84061
parents fc777983 ff275b33
...@@ -14,14 +14,16 @@ class AuditEventService ...@@ -14,14 +14,16 @@ class AuditEventService
# @param [Hash] details extra data of audit event # @param [Hash] details extra data of audit event
# @param [Symbol] save_type the type to save the event # @param [Symbol] save_type the type to save the event
# Can be selected from the following, :database, :stream, :database_and_stream . # Can be selected from the following, :database, :stream, :database_and_stream .
# @params [DateTime] created_at the time the action occured
# #
# @return [AuditEventService] # @return [AuditEventService]
def initialize(author, entity, details = {}, save_type = :database_and_stream) def initialize(author, entity, details = {}, save_type = :database_and_stream, created_at = DateTime.current)
@author = build_author(author) @author = build_author(author)
@entity = entity @entity = entity
@details = details @details = details
@ip_address = resolve_ip_address(@author) @ip_address = resolve_ip_address(@author)
@save_type = save_type @save_type = save_type
@created_at = created_at
end end
# Builds the @details attribute for authentication # Builds the @details attribute for authentication
...@@ -79,7 +81,8 @@ class AuditEventService ...@@ -79,7 +81,8 @@ class AuditEventService
author_id: @author.id, author_id: @author.id,
author_name: @author.name, author_name: @author.name,
entity_id: @entity.id, entity_id: @entity.id,
entity_type: @entity.class.name entity_type: @entity.class.name,
created_at: @created_at
} }
end end
......
...@@ -19,12 +19,13 @@ actions performed across the application. ...@@ -19,12 +19,13 @@ actions performed across the application.
To instrument an audit event, the following attributes should be provided: To instrument an audit event, the following attributes should be provided:
| Attribute | Type | Required? | Description | | Attribute | Type | Required? | Description |
|:-------------|:---------------------|:----------|:----------------------------------------------------| |:-------------|:---------------------|:----------|:-----------------------------------------------------------------|
| `name` | String | false | Action name to be audited. Used for error tracking | | `name` | String | false | Action name to be audited. Used for error tracking |
| `author` | User | true | User who authors the change | | `author` | User | true | User who authors the change |
| `scope` | User, Project, Group | true | Scope which the audit event belongs to | | `scope` | User, Project, Group | true | Scope which the audit event belongs to |
| `target` | Object | true | Target object being audited | | `target` | Object | true | Target object being audited |
| `message` | String | true | Message describing the action | | `message` | String | true | Message describing the action |
| `created_at` | DateTime | false | The time when the action occured. Defaults to `DateTime.current` |
## How to instrument new Audit Events ## How to instrument new Audit Events
...@@ -97,7 +98,8 @@ if merge_approval_rule.save ...@@ -97,7 +98,8 @@ if merge_approval_rule.save
author: current_user, author: current_user,
scope: project_alpha, scope: project_alpha,
target: merge_approval_rule, target: merge_approval_rule,
message: 'Created a new approval rule' message: 'Created a new approval rule',
created_at: DateTime.current # Useful for pre-dating an audit event when created asynchronously.
} }
::Gitlab::Audit::Auditor.audit(audit_context) ::Gitlab::Audit::Auditor.audit(audit_context)
......
...@@ -8,7 +8,7 @@ module AuditEvents ...@@ -8,7 +8,7 @@ module AuditEvents
# @raise [MissingAttributeError] when required attributes are blank # @raise [MissingAttributeError] when required attributes are blank
# #
# @return [BuildService] # @return [BuildService]
def initialize(author:, scope:, target:, message:) def initialize(author:, scope:, target:, message:, created_at: DateTime.current)
raise MissingAttributeError if missing_attribute?(author, scope, target, message) raise MissingAttributeError if missing_attribute?(author, scope, target, message)
@author = build_author(author) @author = build_author(author)
...@@ -16,6 +16,7 @@ module AuditEvents ...@@ -16,6 +16,7 @@ module AuditEvents
@target = build_target(target) @target = build_target(target)
@ip_address = build_ip_address @ip_address = build_ip_address
@message = build_message(message) @message = build_message(message)
@created_at = created_at
end end
# Create an instance of AuditEvent # Create an instance of AuditEvent
...@@ -52,7 +53,7 @@ module AuditEvents ...@@ -52,7 +53,7 @@ module AuditEvents
author_name: @author.name, author_name: @author.name,
entity_id: @scope.id, entity_id: @scope.id,
entity_type: @scope.class.name, entity_type: @scope.class.name,
created_at: DateTime.current created_at: @created_at
} }
end end
......
...@@ -11,6 +11,7 @@ module Gitlab ...@@ -11,6 +11,7 @@ module Gitlab
# @option context [User, Project, Group] :scope the scope which audit event belongs to # @option context [User, Project, Group] :scope the scope which audit event belongs to
# @option context [Object] :target the target object being audited # @option context [Object] :target the target object being audited
# @option context [String] :message the message describing the action # @option context [String] :message the message describing the action
# @option context [Time] :created_at the time that the event occurred (defaults to the current time)
# #
# @example Using block (useful when events are emitted deep in the call stack) # @example Using block (useful when events are emitted deep in the call stack)
# i.e. multiple audit events # i.e. multiple audit events
...@@ -56,6 +57,7 @@ module Gitlab ...@@ -56,6 +57,7 @@ module Gitlab
@author = @context.fetch(:author) @author = @context.fetch(:author)
@scope = @context.fetch(:scope) @scope = @context.fetch(:scope)
@target = @context.fetch(:target) @target = @context.fetch(:target)
@created_at = @context.fetch(:created_at, DateTime.current)
@message = @context.fetch(:message, '') @message = @context.fetch(:message, '')
end end
...@@ -88,6 +90,7 @@ module Gitlab ...@@ -88,6 +90,7 @@ module Gitlab
author: @author, author: @author,
scope: @scope, scope: @scope,
target: @target, target: @target,
created_at: @created_at,
message: message message: message
).execute ).execute
end end
......
...@@ -52,6 +52,7 @@ RSpec.describe Gitlab::Audit::Auditor do ...@@ -52,6 +52,7 @@ RSpec.describe Gitlab::Audit::Auditor do
end end
it 'logs audit events to database', :aggregate_failures do it 'logs audit events to database', :aggregate_failures do
freeze_time do
audit! audit!
audit_event = AuditEvent.last audit_event = AuditEvent.last
...@@ -59,9 +60,11 @@ RSpec.describe Gitlab::Audit::Auditor do ...@@ -59,9 +60,11 @@ RSpec.describe Gitlab::Audit::Auditor do
expect(audit_event.author_id).to eq(author.id) expect(audit_event.author_id).to eq(author.id)
expect(audit_event.entity_id).to eq(scope.id) expect(audit_event.entity_id).to eq(scope.id)
expect(audit_event.entity_type).to eq(scope.class.name) expect(audit_event.entity_type).to eq(scope.class.name)
expect(audit_event.created_at).to eq(Time.zone.now)
expect(audit_event.details[:target_id]).to eq(target.id) 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[:target_type]).to eq(target.class.name)
end end
end
it 'logs audit events to file' do it 'logs audit events to file' do
expect(::Gitlab::AuditJsonLogger).to receive(:build).and_return(logger) expect(::Gitlab::AuditJsonLogger).to receive(:build).and_return(logger)
...@@ -78,6 +81,44 @@ RSpec.describe Gitlab::Audit::Auditor do ...@@ -78,6 +81,44 @@ RSpec.describe Gitlab::Audit::Auditor do
) )
) )
end end
context 'when overriding the create datetime' do
let(:context) { { name: name, author: author, scope: scope, target: target, created_at: 3.weeks.ago } }
it 'logs audit events to database', :aggregate_failures do
freeze_time do
audit!
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.created_at).to eq(3.weeks.ago)
expect(audit_event.details[:target_id]).to eq(target.id)
expect(audit_event.details[:target_type]).to eq(target.class.name)
end
end
it 'logs audit events to file' do
freeze_time do
expect(::Gitlab::AuditJsonLogger).to receive(:build).and_return(logger)
audit!
expect(logger).to have_received(:info).exactly(2).times.with(
hash_including(
'author_id' => author.id,
'author_name' => author.name,
'entity_id' => scope.id,
'entity_type' => scope.class.name,
'details' => kind_of(Hash),
'created_at' => 3.weeks.ago.iso8601(3)
)
)
end
end
end
end end
context 'when recording single event' do context 'when recording single event' do
......
...@@ -22,7 +22,8 @@ RSpec.describe AuditEvents::ImpersonationAuditEventService do ...@@ -22,7 +22,8 @@ RSpec.describe AuditEvents::ImpersonationAuditEventService do
entity_type: "User", entity_type: "User",
action: :custom, action: :custom,
ip_address: ip_address, ip_address: ip_address,
custom_message: message) custom_message: message,
created_at: anything)
expect { service.security_event }.to change(AuditEvent, :count).by(1) expect { service.security_event }.to change(AuditEvent, :count).by(1)
security_event = AuditEvent.last security_event = AuditEvent.last
......
...@@ -65,7 +65,8 @@ RSpec.describe AuditEvents::ProtectedBranchAuditEventService, :request_store do ...@@ -65,7 +65,8 @@ RSpec.describe AuditEvents::ProtectedBranchAuditEventService, :request_store do
target_id: protected_branch.id, target_id: protected_branch.id,
target_type: 'ProtectedBranch', target_type: 'ProtectedBranch',
custom_message: action == :add ? /Added/ : /Unprotected/, custom_message: action == :add ? /Added/ : /Unprotected/,
ip_address: ip_address ip_address: ip_address,
created_at: anything
) )
end end
end end
......
...@@ -29,7 +29,8 @@ RSpec.describe AuditEvents::RunnerCustomAuditEventService do ...@@ -29,7 +29,8 @@ RSpec.describe AuditEvents::RunnerCustomAuditEventService do
custom_message: custom_message, custom_message: custom_message,
target_details: target_details, target_details: target_details,
target_id: target_id, target_id: target_id,
target_type: target_type) target_type: target_type,
created_at: anything)
expect { service.security_event }.to change(AuditEvent, :count).by(1) expect { service.security_event }.to change(AuditEvent, :count).by(1)
......
...@@ -48,6 +48,7 @@ RSpec.shared_examples 'logs the custom audit event' do ...@@ -48,6 +48,7 @@ RSpec.shared_examples 'logs the custom audit event' do
end end
it 'creates an event and logs to a file with the provided details' do it 'creates an event and logs to a file with the provided details' do
freeze_time do
expect(service).to receive(:file_logger).and_return(logger) expect(service).to receive(:file_logger).and_return(logger)
expect(logger).to receive(:info).with(author_id: user.id, expect(logger).to receive(:info).with(author_id: user.id,
author_name: user.name, author_name: user.name,
...@@ -55,21 +56,21 @@ RSpec.shared_examples 'logs the custom audit event' do ...@@ -55,21 +56,21 @@ RSpec.shared_examples 'logs the custom audit event' do
entity_type: entity_type, entity_type: entity_type,
action: :custom, action: :custom,
ip_address: ip_address, ip_address: ip_address,
custom_message: custom_message) custom_message: custom_message,
created_at: DateTime.current)
expect { service.security_event }.to change(AuditEvent, :count).by(1) expect { service.security_event }.to change(AuditEvent, :count).by(1)
security_event = AuditEvent.last security_event = AuditEvent.last
expect(security_event.details).to eq( expect(security_event.details).to eq(author_name: user.name,
author_name: user.name,
custom_message: custom_message, custom_message: custom_message,
ip_address: ip_address, ip_address: ip_address,
action: :custom action: :custom)
)
expect(security_event.author_id).to eq(user.id) expect(security_event.author_id).to eq(user.id)
expect(security_event.entity_id).to eq(entity.id) expect(security_event.entity_id).to eq(entity.id)
expect(security_event.entity_type).to eq(entity_type) expect(security_event.entity_type).to eq(entity_type)
end end
end
end end
RSpec.shared_examples 'logs the release audit event' do RSpec.shared_examples 'logs the release audit event' do
...@@ -88,7 +89,8 @@ RSpec.shared_examples 'logs the release audit event' do ...@@ -88,7 +89,8 @@ RSpec.shared_examples 'logs the release audit event' do
stub_licensed_features(audit_events: true) stub_licensed_features(audit_events: true)
end end
it 'logs the event to file' do it 'logs the event to file', :aggregate_failures do
freeze_time do
expect(service).to receive(:file_logger).and_return(logger) expect(service).to receive(:file_logger).and_return(logger)
expect(logger).to receive(:info).with(author_id: user.id, expect(logger).to receive(:info).with(author_id: user.id,
author_name: user.name, author_name: user.name,
...@@ -98,23 +100,23 @@ RSpec.shared_examples 'logs the release audit event' do ...@@ -98,23 +100,23 @@ RSpec.shared_examples 'logs the release audit event' do
custom_message: custom_message, custom_message: custom_message,
target_details: target_details, target_details: target_details,
target_id: target_id, target_id: target_id,
target_type: target_type) target_type: target_type,
created_at: DateTime.current)
expect { service.security_event }.to change(AuditEvent, :count).by(1) expect { service.security_event }.to change(AuditEvent, :count).by(1)
security_event = AuditEvent.last security_event = AuditEvent.last
expect(security_event.details).to eq( expect(security_event.details).to eq(author_name: user.name,
author_name: user.name,
custom_message: custom_message, custom_message: custom_message,
ip_address: ip_address, ip_address: ip_address,
target_details: target_details, target_details: target_details,
target_id: target_id, target_id: target_id,
target_type: target_type target_type: target_type)
)
expect(security_event.author_id).to eq(user.id) expect(security_event.author_id).to eq(user.id)
expect(security_event.entity_id).to eq(entity.id) expect(security_event.entity_id).to eq(entity.id)
expect(security_event.entity_type).to eq(entity_type) expect(security_event.entity_type).to eq(entity_type)
end end
end
end end
...@@ -17,7 +17,8 @@ RSpec.describe AuditEventService do ...@@ -17,7 +17,8 @@ RSpec.describe AuditEventService do
author_name: user.name, author_name: user.name,
entity_id: project.id, entity_id: project.id,
entity_type: "Project", entity_type: "Project",
action: :destroy) action: :destroy,
created_at: anything)
expect { service.security_event }.to change(AuditEvent, :count).by(1) expect { service.security_event }.to change(AuditEvent, :count).by(1)
end end
...@@ -39,7 +40,8 @@ RSpec.describe AuditEventService do ...@@ -39,7 +40,8 @@ RSpec.describe AuditEventService do
from: 'true', from: 'true',
to: 'false', to: 'false',
action: :create, action: :create,
target_id: 1) target_id: 1,
created_at: anything)
expect { service.security_event }.to change(AuditEvent, :count).by(1) expect { service.security_event }.to change(AuditEvent, :count).by(1)
...@@ -50,6 +52,25 @@ RSpec.describe AuditEventService do ...@@ -50,6 +52,25 @@ RSpec.describe AuditEventService do
expect(details[:target_id]).to eq(1) expect(details[:target_id]).to eq(1)
end end
context 'when defining created_at manually' do
let(:service) { described_class.new(user, project, { action: :destroy }, :database, 3.weeks.ago) }
it 'is overridden successfully' do
freeze_time do
expect(service).to receive(:file_logger).and_return(logger)
expect(logger).to receive(:info).with(author_id: user.id,
author_name: user.name,
entity_id: project.id,
entity_type: "Project",
action: :destroy,
created_at: 3.weeks.ago)
expect { service.security_event }.to change(AuditEvent, :count).by(1)
expect(AuditEvent.last.created_at).to eq(3.weeks.ago)
end
end
end
context 'authentication event' do context 'authentication event' do
let(:audit_service) { described_class.new(user, user, with: 'standard') } let(:audit_service) { described_class.new(user, user, with: 'standard') }
...@@ -110,7 +131,8 @@ RSpec.describe AuditEventService do ...@@ -110,7 +131,8 @@ RSpec.describe AuditEventService do
author_name: user.name, author_name: user.name,
entity_type: 'Project', entity_type: 'Project',
entity_id: project.id, entity_id: project.id,
action: :destroy) action: :destroy,
created_at: anything)
service.log_security_event_to_file service.log_security_event_to_file
end end
......
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