Commit a7713e44 authored by Tan Le's avatar Tan Le Committed by Bob Van Landuyt

Fix missing attributes on token audit events

The bug is caused by the column value being overwritten will nil value
in the `details` hash. The `after_validation` callback always writes the
column value using the `details` value, regardless whether it has been
specified or not.

This change ensures nil check was performed before setting values in
both column and `detail` hash. The column value takes precedence over
the `details` value.

Changelog: fixed
EE: true
parent e4161296
...@@ -88,7 +88,12 @@ class AuditEvent < ApplicationRecord ...@@ -88,7 +88,12 @@ class AuditEvent < ApplicationRecord
end end
def parallel_persist def parallel_persist
PARALLEL_PERSISTENCE_COLUMNS.each { |col| self[col] = details[col] } PARALLEL_PERSISTENCE_COLUMNS.each do |name|
original = self[name] || self.details[name]
next unless original
self[name] = self.details[name] = original
end
end end
end end
......
...@@ -21,6 +21,8 @@ module EE ...@@ -21,6 +21,8 @@ module EE
::AuditEventService.new( ::AuditEventService.new(
current_user, current_user,
resource, resource,
target_id: token&.id,
target_type: token&.class&.name,
target_details: token&.user&.name, target_details: token&.user&.name,
action: :custom, action: :custom,
custom_message: message, custom_message: message,
......
...@@ -13,15 +13,18 @@ module EE ...@@ -13,15 +13,18 @@ module EE
def audit_event_service(token, response) def audit_event_service(token, response)
message = if response.success? message = if response.success?
"Revoked #{resource.class.name.downcase} access token with token_id: #{access_token.id}" "Revoked #{resource.class.name.downcase} access token with token_id: #{token.id}"
else else
"Attempted to revoke #{resource.class.name.downcase} access token with token_id: #{access_token.id}, but failed with message: #{response.message}" "Attempted to revoke #{resource.class.name.downcase} access token with token_id: #{token.id}, " \
"but failed with message: #{response.message}"
end end
::AuditEventService.new( ::AuditEventService.new(
current_user, current_user,
resource, resource,
target_details: access_token.user.name, target_id: token.id,
target_type: token.class.name,
target_details: token.user.name,
action: :custom, action: :custom,
custom_message: message, custom_message: message,
ip_address: current_user.current_sign_in_ip ip_address: current_user.current_sign_in_ip
......
...@@ -14,26 +14,6 @@ RSpec.describe AuditEvent, type: :model do ...@@ -14,26 +14,6 @@ RSpec.describe AuditEvent, type: :model do
end end
describe 'callbacks' do describe 'callbacks' do
context 'parallel_persist' do
let_it_be(:details) do
{ author_name: 'Kungfu Panda', entity_path: 'gitlab-org/gitlab', target_details: 'Project X', target_type: 'User' }
end
let_it_be(:event) { create(:project_audit_event, details: details, entity_path: nil, target_details: nil) }
it 'sets author_name' do
expect(event[:author_name]).to eq('Kungfu Panda')
end
it 'sets entity_path' do
expect(event[:entity_path]).to eq('gitlab-org/gitlab')
end
it 'sets target_details' do
expect(event[:target_details]).to eq('Project X')
end
end
context 'truncate_fields' do context 'truncate_fields' do
shared_examples 'a truncated field' do shared_examples 'a truncated field' do
context 'when values are provided' do context 'when values are provided' do
......
...@@ -7,17 +7,21 @@ RSpec.describe AuditEventPresenter do ...@@ -7,17 +7,21 @@ RSpec.describe AuditEventPresenter do
let(:details) do let(:details) do
{ {
author_name: 'author', change: 'name',
ip_address: '127.0.0.1',
target_details: 'target name',
entity_path: 'path',
from: 'a', from: 'a',
to: 'b' to: 'b',
entity_path: 'path'
} }
end end
let(:audit_event) do let(:audit_event) do
create(:audit_event, ip_address: '10.2.1.1', details: details) create(
:audit_event,
author_name: 'author',
target_details: 'target name',
ip_address: '10.2.1.1',
details: details
)
end end
subject(:presenter) do subject(:presenter) do
...@@ -36,13 +40,7 @@ RSpec.describe AuditEventPresenter do ...@@ -36,13 +40,7 @@ RSpec.describe AuditEventPresenter do
end end
context 'event authored by a user that no longer exists' do context 'event authored by a user that no longer exists' do
let(:user) { create(:user) } let(:audit_event) { build(:audit_event, user: build(:user), details: details) }
let(:audit_event) { create(:audit_event, user: user, details: details) }
before do
user.destroy!
audit_event.reload
end
context 'when `author_name` is not included in the details' do context 'when `author_name` is not included in the details' do
let(:details) do let(:details) do
...@@ -62,52 +60,13 @@ RSpec.describe AuditEventPresenter do ...@@ -62,52 +60,13 @@ RSpec.describe AuditEventPresenter do
end end
context 'when `author_name` is included in the details and not in the author_name column' do context 'when `author_name` is included in the details and not in the author_name column' do
before do let(:audit_event) { build(:audit_event, author_name: nil, details: details) }
audit_event.update!(author_name: nil)
end
it 'shows the author name as provided in the details' do it 'shows the author name as provided in the details' do
expect(presenter.author_name).to eq(details[:author_name]) expect(presenter.author_name).to eq(details[:author_name])
end end
end end
end end
context 'event authored by an unauthenticated user' do
before do
audit_event.author_id = -1
end
context 'when `author_name` is not included in details and not in the author_name column' do
before do
audit_event.update!(author_name: nil)
end
let(:details) do
{
author_name: nil,
ip_address: '127.0.0.1',
target_details: 'target name',
entity_path: 'path',
from: 'a',
to: 'b'
}
end
it 'shows `An unauthenticated user` as the author name' do
expect(presenter.author_name).to eq('An unauthenticated user')
end
end
context 'when `author_name` is included in details and not in the author_name column' do
before do
audit_event.update!(author_name: nil)
end
it 'shows the author name as provided in the details' do
expect(presenter.author_name).to eq('author')
end
end
end
end end
describe '#target' do describe '#target' do
...@@ -153,6 +112,6 @@ RSpec.describe AuditEventPresenter do ...@@ -153,6 +112,6 @@ RSpec.describe AuditEventPresenter do
end end
it 'exposes the action' do it 'exposes the action' do
expect(presenter.action).to eq('Changed author from a to b') expect(presenter.action).to eq('Changed name from a to b')
end end
end end
...@@ -27,9 +27,12 @@ RSpec.describe AuditEvents::ImpersonationAuditEventService do ...@@ -27,9 +27,12 @@ RSpec.describe AuditEvents::ImpersonationAuditEventService do
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(custom_message: message, expect(security_event.details).to eq(
author_name: impersonator.name,
custom_message: message,
ip_address: ip_address, ip_address: ip_address,
action: :custom) action: :custom
)
expect(security_event.author_id).to eq(impersonator.id) expect(security_event.author_id).to eq(impersonator.id)
expect(security_event.entity_id).to eq(impersonator.id) expect(security_event.entity_id).to eq(impersonator.id)
expect(security_event.entity_type).to eq('User') expect(security_event.entity_type).to eq('User')
......
...@@ -84,9 +84,17 @@ RSpec.describe ResourceAccessTokens::CreateService do ...@@ -84,9 +84,17 @@ RSpec.describe ResourceAccessTokens::CreateService do
response = subject response = subject
audit_event = AuditEvent.where(author_id: user.id).last audit_event = AuditEvent.where(author_id: user.id).last
access_token = response.payload[:access_token]
custom_message = <<~MESSAGE.squish
Created project access token with token_id: #{access_token.id} with scopes: #{access_token.scopes}
MESSAGE
expect(audit_event.details[:custom_message]).to eq("Created project access token with token_id: #{response.payload[:access_token].id} with scopes: #{response.payload[:access_token].scopes}") expect(audit_event.details).to include(
expect(audit_event.details[:target_details]).to match(response.payload[:access_token].user.name) custom_message: custom_message,
target_id: access_token.id,
target_type: access_token.class.name,
target_details: access_token.user.name
)
end end
end end
...@@ -101,12 +109,24 @@ RSpec.describe ResourceAccessTokens::CreateService do ...@@ -101,12 +109,24 @@ RSpec.describe ResourceAccessTokens::CreateService do
it 'logs the permission error message' do it 'logs the permission error message' do
subject subject
expect(AuditEvent.where(author_id: user.id).last.details[:custom_message]).to eq('Attempted to create project access token but failed with message: User does not have permission to create project access token') audit_event = AuditEvent.where(author_id: user.id).last
custom_message = <<~MESSAGE.squish
Attempted to create project access token but failed with message:
User does not have permission to create project access token
MESSAGE
expect(audit_event.details).to include(
custom_message: custom_message,
target_id: nil,
target_type: nil,
target_details: nil
)
end end
end end
context "when access provisioning fails" do context "when access provisioning fails" do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:unpersisted_member) { build(:project_member, source: resource, user: user) } let(:unpersisted_member) { build(:project_member, source: resource, user: user) }
before do before do
...@@ -123,7 +143,18 @@ RSpec.describe ResourceAccessTokens::CreateService do ...@@ -123,7 +143,18 @@ RSpec.describe ResourceAccessTokens::CreateService do
it 'logs the provisioning error message' do it 'logs the provisioning error message' do
subject subject
expect(AuditEvent.where(author_id: user.id).last.details[:custom_message]).to eq('Attempted to create project access token but failed with message: Could not provision maintainer access to project access token') audit_event = AuditEvent.where(author_id: user.id).last
custom_message = <<~MESSAGE.squish
Attempted to create project access token but failed with message:
Could not provision maintainer access to project access token
MESSAGE
expect(audit_event.details).to include(
custom_message: custom_message,
target_id: nil,
target_type: nil,
target_details: nil
)
end end
end end
end end
......
...@@ -7,6 +7,7 @@ RSpec.describe ResourceAccessTokens::RevokeService do ...@@ -7,6 +7,7 @@ RSpec.describe ResourceAccessTokens::RevokeService do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:resource_bot) { create(:user, :project_bot) } let_it_be(:resource_bot) { create(:user, :project_bot) }
let(:access_token) { create(:personal_access_token, user: resource_bot) } let(:access_token) { create(:personal_access_token, user: resource_bot) }
shared_examples 'audit event details' do shared_examples 'audit event details' do
...@@ -40,8 +41,12 @@ RSpec.describe ResourceAccessTokens::RevokeService do ...@@ -40,8 +41,12 @@ RSpec.describe ResourceAccessTokens::RevokeService do
audit_event = AuditEvent.where(author_id: user.id).last audit_event = AuditEvent.where(author_id: user.id).last
expect(audit_event.details[:custom_message]).to match(/Revoked project access token with token_id: \d+/) expect(audit_event.details).to include(
expect(audit_event.details[:target_details]).to eq(access_token.user.name) custom_message: match(/Revoked project access token with token_id: \d+/),
target_id: access_token.id,
target_type: access_token.class.name,
target_details: access_token.user.name
)
end end
end end
...@@ -56,7 +61,18 @@ RSpec.describe ResourceAccessTokens::RevokeService do ...@@ -56,7 +61,18 @@ RSpec.describe ResourceAccessTokens::RevokeService do
it 'logs the find error message' do it 'logs the find error message' do
subject subject
expect(AuditEvent.where(author_id: user.id).last.details[:custom_message]).to match(/Attempted to revoke project access token with token_id: \d+, but failed with message: Failed to find bot user/) audit_event = AuditEvent.where(author_id: user.id).last
custom_message = <<~MESSAGE.squish
Attempted to revoke project access token with token_id: \\d+, but failed with message:
Failed to find bot user
MESSAGE
expect(audit_event.details).to include(
custom_message: match(custom_message),
target_id: access_token.id,
target_type: access_token.class.name,
target_details: access_token.user.name
)
end end
end end
...@@ -71,7 +87,18 @@ RSpec.describe ResourceAccessTokens::RevokeService do ...@@ -71,7 +87,18 @@ RSpec.describe ResourceAccessTokens::RevokeService do
it 'logs the permission error message' do it 'logs the permission error message' do
subject subject
expect(AuditEvent.where(author_id: user.id).last.details[:custom_message]).to match(/Attempted to revoke project access token with token_id: \d+, but failed with message: #{user.name} cannot delete #{access_token.user.name}/) audit_event = AuditEvent.where(author_id: user.id).last
custom_message = <<~MESSAGE.squish
Attempted to revoke project access token with token_id: \\d+, but failed with message:
#{user.name} cannot delete #{access_token.user.name}
MESSAGE
expect(audit_event.details).to include(
custom_message: match(custom_message),
target_id: access_token.id,
target_type: access_token.class.name,
target_details: access_token.user.name
)
end end
end end
end end
......
...@@ -60,9 +60,12 @@ RSpec.shared_examples 'logs the custom audit event' do ...@@ -60,9 +60,12 @@ RSpec.shared_examples 'logs the custom audit event' do
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(custom_message: custom_message, expect(security_event.details).to eq(
author_name: user.name,
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)
...@@ -101,11 +104,14 @@ RSpec.shared_examples 'logs the release audit event' do ...@@ -101,11 +104,14 @@ RSpec.shared_examples 'logs the release audit event' do
security_event = AuditEvent.last security_event = AuditEvent.last
expect(security_event.details).to eq(custom_message: custom_message, expect(security_event.details).to eq(
author_name: user.name,
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)
......
...@@ -51,6 +51,7 @@ FactoryBot.define do ...@@ -51,6 +51,7 @@ FactoryBot.define do
trait :unauthenticated do trait :unauthenticated do
author_id { -1 } author_id { -1 }
author_name { 'An unauthenticated user' }
details do details do
{ {
custom_message: 'Custom action', custom_message: 'Custom action',
......
...@@ -3,9 +3,6 @@ ...@@ -3,9 +3,6 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe AuditEvent do RSpec.describe AuditEvent do
let_it_be(:audit_event) { create(:project_audit_event) }
subject { audit_event }
describe 'validations' do describe 'validations' do
include_examples 'validates IP address' do include_examples 'validates IP address' do
let(:attribute) { :ip_address } let(:attribute) { :ip_address }
...@@ -13,6 +10,71 @@ RSpec.describe AuditEvent do ...@@ -13,6 +10,71 @@ RSpec.describe AuditEvent do
end end
end end
describe 'callbacks' do
describe '#parallel_persist' do
shared_examples 'a parallel persisted field' do
using RSpec::Parameterized::TableSyntax
where(:column, :details, :expected_value) do
:value | nil | :value
nil | :value | :value
:value | :another_value | :value
nil | nil | nil
end
with_them do
let(:values) { { value: value, another_value: "#{value}88" } }
let(:audit_event) do
build(:audit_event, name => values[column], details: { name => values[details] })
end
it 'sets both values to be the same', :aggregate_failures do
audit_event.validate
expect(audit_event[name]).to eq(values[expected_value])
expect(audit_event.details[name]).to eq(values[expected_value])
end
end
end
context 'wih author_name' do
let(:name) { :author_name }
let(:value) { 'Mary Poppins' }
it_behaves_like 'a parallel persisted field'
end
context 'with entity_path' do
let(:name) { :entity_path }
let(:value) { 'gitlab-org' }
it_behaves_like 'a parallel persisted field'
end
context 'with target_details' do
let(:name) { :target_details }
let(:value) { 'gitlab-org/gitlab' }
it_behaves_like 'a parallel persisted field'
end
context 'with target_type' do
let(:name) { :target_type }
let(:value) { 'Project' }
it_behaves_like 'a parallel persisted field'
end
context 'with target_id' do
let(:name) { :target_id }
let(:value) { 8 }
it_behaves_like 'a parallel persisted field'
end
end
end
describe '#as_json' do describe '#as_json' do
context 'ip_address' do context 'ip_address' do
subject { build(:group_audit_event, ip_address: '192.168.1.1').as_json } subject { build(:group_audit_event, ip_address: '192.168.1.1').as_json }
......
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