Skip to content
Snippets Groups Projects
Verified Commit 0f9e1dd5 authored by Moaz Khalifa's avatar Moaz Khalifa Committed by GitLab
Browse files

Send audit event for packages creation

parent 8e023f17
No related branches found
No related tags found
1 merge request!178848Audit events for package deletion
Showing
with 422 additions and 57 deletions
......@@ -42,3 +42,5 @@ def track_exception(error)
end
end
end
Packages::MarkPackageForDestructionService.prepend_mod
......@@ -33,7 +33,7 @@ def execute(batch_size: BATCH_SIZE)
min_batch_size = [batch_size, BATCH_SIZE].min
package_ids = []
@packages.each_batch(of: min_batch_size) do |batched_packages|
packages.each_batch(of: min_batch_size) do |batched_packages|
loaded_packages = batched_packages.including_project_route.to_a
package_ids = loaded_packages.map(&:id)
......@@ -42,9 +42,7 @@ def execute(batch_size: BATCH_SIZE)
::Packages::Package.id_in(package_ids)
.update_all(status: :pending_destruction)
sync_maven_metadata(loaded_packages)
sync_npm_metadata(loaded_packages)
mark_package_files_for_destruction(loaded_packages)
after_marked_for_destruction(loaded_packages)
end
return UNAUTHORIZED_RESPONSE if no_access
......@@ -57,11 +55,19 @@ def execute(batch_size: BATCH_SIZE)
private
attr_reader :packages, :current_user
def after_marked_for_destruction(packages)
sync_maven_metadata(packages)
sync_npm_metadata(packages)
mark_package_files_for_destruction(packages)
end
def mark_package_files_for_destruction(packages)
::Packages::MarkPackageFilesForDestructionWorker.bulk_perform_async_with_contexts(
packages,
arguments_proc: ->(package) { package.id },
context_proc: ->(package) { { project: package.project, user: @current_user } }
context_proc: ->(package) { { project: package.project, user: current_user } }
)
end
......@@ -69,8 +75,8 @@ def sync_maven_metadata(packages)
maven_packages_with_version = packages.select { |pkg| pkg.maven? && pkg.version? }
::Packages::Maven::Metadata::SyncWorker.bulk_perform_async_with_contexts(
maven_packages_with_version,
arguments_proc: ->(package) { [@current_user.id, package.project_id, package.name] },
context_proc: ->(package) { { project: package.project, user: @current_user } }
arguments_proc: ->(package) { [current_user.id, package.project_id, package.name] },
context_proc: ->(package) { { project: package.project, user: current_user } }
)
end
......@@ -79,13 +85,13 @@ def sync_npm_metadata(packages)
::Packages::Npm::CreateMetadataCacheWorker.bulk_perform_async_with_contexts(
npm_packages,
arguments_proc: ->(package) { [package.project_id, package.name] },
context_proc: ->(package) { { project: package.project, user: @current_user } }
context_proc: ->(package) { { project: package.project, user: current_user } }
)
end
def can_destroy_packages?(packages)
packages.all? do |package|
can?(@current_user, :destroy_package, package)
can?(current_user, :destroy_package, package)
end
end
......@@ -94,3 +100,5 @@ def track_exception(error, package_ids)
end
end
end
Packages::MarkPackagesForDestructionService.prepend_mod
......@@ -454,6 +454,7 @@ Audit event types belong to the following product categories.
| Type name | Event triggered when | Saved to database | Introduced in | Scope |
|:----------|:---------------------|:------------------|:--------------|:------|
| [`package_registry_package_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/178181) | A package was deleted from GitLab package registry. Available only when the feature flag `package_registry_audit_events` is enabled. | {{< icon name="check-circle" >}} Yes | GitLab [17.10](https://gitlab.com/gitlab-org/gitlab/-/issues/329588) | Project, Group |
| [`package_registry_package_published`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/178181) | A package was published to GitLab package registry. Available only when the feature flag `package_registry_audit_events` is enabled. | {{< icon name="check-circle" >}} Yes | GitLab [17.9](https://gitlab.com/gitlab-org/gitlab/-/issues/329588) | Project, Group |
### Permissions
......
......@@ -4,10 +4,15 @@ module Auditable
extend ActiveSupport::Concern
include AfterCommitQueue
def push_audit_event(event)
# Use `after_commit: false` when you're already handling the transaction boundary
def push_audit_event(event, after_commit: true)
return unless ::Gitlab::Audit::EventQueue.active?
run_after_commit do
if after_commit
run_after_commit do
::Gitlab::Audit::EventQueue.push(event)
end
else
::Gitlab::Audit::EventQueue.push(event)
end
end
......
......@@ -8,6 +8,8 @@ module Package
PROCESSING_TO_DEFAULT = ::Packages::Package.statuses.invert.values_at(2, 0).freeze
prepended do
include ::Auditable
after_commit :create_audit_event, on: %i[create update]
private
......
# frozen_string_literal: true
module EE
module Packages
module MarkPackageForDestructionService
extend ::Gitlab::Utils::Override
override :execute
def execute
super.tap do |response|
if response.success?
user = current_user
package.run_after_commit_or_now do
::Packages::CreateAuditEventService
.new(self, current_user: user, event_name: 'package_registry_package_deleted')
.execute
end
end
end
end
end
end
end
# frozen_string_literal: true
module EE
module Packages
module MarkPackagesForDestructionService
extend ::Gitlab::Utils::Override
private
override :after_marked_for_destruction
def after_marked_for_destruction(packages)
super
send_audit_events(packages)
end
def send_audit_events(packages)
::Packages::CreateAuditEventsService.new(packages, current_user:).execute
end
end
end
end
# frozen_string_literal: true
module Packages
class AuditEventsBaseService
FEATURE_FLAG_DISABLED_ERROR = ServiceResponse.error(message: 'Feature flag is not enabled').freeze
def execute
if ::Feature.disabled?(:package_registry_audit_events, ::Feature.current_request)
return FEATURE_FLAG_DISABLED_ERROR
end
yield
ServiceResponse.success
end
private
def auth_token_type
::Current.token_info&.dig(:token_type) || token_type_from_current_user
end
def token_type_from_current_user
return unless current_user
return 'DeployToken' if current_user.is_a?(DeployToken)
return 'CiJobToken' if current_user.from_ci_job_token?
'PersonalAccessToken'
end
end
end
# frozen_string_literal: true
module Packages
class CreateAuditEventService
FEATURE_FLAG_DISABLED_ERROR = ServiceResponse.error(message: 'Feature flag is not enabled').freeze
class CreateAuditEventService < ::Packages::AuditEventsBaseService
delegate :project, :creator, to: :package, private: true
def initialize(package, event_name: 'package_registry_package_published')
def initialize(package, current_user: nil, event_name: 'package_registry_package_published')
@package = package
@current_user = current_user
@event_name = event_name
end
def execute
return FEATURE_FLAG_DISABLED_ERROR if ::Feature.disabled?(:package_registry_audit_events, project)
::Gitlab::Audit::Auditor.audit(audit_context)
ServiceResponse.success
super do
::Gitlab::Audit::Auditor.audit(audit_context)
end
end
private
attr_reader :package, :event_name
attr_reader :package, :current_user, :event_name
def audit_context
{
name: event_name,
author: creator || ::Gitlab::Audit::DeployTokenAuthor.new,
author: author,
scope: project.group || project,
target: package,
target_details: target_details,
message: "#{package.package_type.humanize} package published",
message: audit_message,
additional_details: { auth_token_type: }
}
end
def author
current_user || creator || ::Gitlab::Audit::DeployTokenAuthor.new
end
def target_details
"#{project.full_path}/#{package.name}-#{package.version}"
end
def audit_message
action = case event_name
when 'package_registry_package_published'
'published'
when 'package_registry_package_deleted'
'deleted'
end
"#{package.package_type.humanize} package #{action}"
end
def auth_token_type
::Current.token_info&.dig(:token_type) || token_type_from_package_creator
super || token_type_from_package_creator
end
def token_type_from_package_creator
......
# frozen_string_literal: true
module Packages
class CreateAuditEventsService < ::Packages::AuditEventsBaseService
def initialize(packages, current_user: nil, event_name: 'package_registry_package_deleted')
@packages = packages
@current_user = current_user
@event_name = event_name
end
def execute
super do
::Gitlab::Audit::Auditor.audit(initial_audit_context) do
preload_groups
packages.each { |pkg| send_event(pkg) }
end
end
end
private
attr_reader :packages, :current_user, :event_name
def initial_audit_context
{
name: event_name,
author: current_user || ::Gitlab::Audit::NullAuthor.new,
scope: ::Group.new,
target: ::Gitlab::Audit::NullTarget.new,
additional_details: { auth_token_type: }
}
end
def preload_groups
::Group
.select(:id)
.include_route
.id_in(packages.map { |pkg| pkg.project.namespace_id })
.index_by(&:id)
.then do |groups|
packages.each { |pkg| pkg.project.group = groups[pkg.project.namespace_id] }
end
end
def send_event(package)
package.run_after_commit_or_now do
event = {
scope: project.group || project,
target: self,
target_details: "#{project.full_path}/#{name}-#{version}",
message: "#{package_type.humanize} package deleted"
}
push_audit_event(event, after_commit: false)
end
end
end
end
---
name: package_registry_package_deleted
description: A package was deleted from GitLab package registry. Available only when
the feature flag `package_registry_audit_events` is enabled.
introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/329588
introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/178181
feature_category: package_registry
milestone: '17.10'
saved_to_database: true
streamed: true
scope: [Project, Group]
......@@ -132,10 +132,18 @@
expect(Gitlab::Audit::EventQueue).to have_received(:end!).ordered
end
it 'bulk-inserts audit events to database' do
expect(AuditEvent).to receive(:bulk_insert!).with(include(kind_of(AuditEvent)), returns: :ids)
context 'for bulk insert' do
before do
allow(AuditEvent).to receive(:id_in).and_return([build_stubbed(:audit_event), build_stubbed(:audit_event)])
end
audit!
it 'bulk-inserts audit events to database' do
expect(AuditEvent).to receive(:bulk_insert!).with(include(kind_of(AuditEvent)), returns: :ids)
expect(AuditEvents::UserAuditEvent).to receive(:bulk_insert!)
.with(include(kind_of(AuditEvents::UserAuditEvent)))
audit!
end
end
it 'records audit events in correct order', :aggregate_failures do
......@@ -397,7 +405,11 @@
end
end
it_behaves_like 'when audit event is invalid'
it_behaves_like 'when audit event is invalid' do
before do
allow(::AuditEvents::InstanceAuditEvent).to receive(:bulk_insert!).and_raise(ActiveRecord::RecordInvalid)
end
end
end
context 'when recording single event' do
......
......@@ -46,13 +46,31 @@
describe 'approval_project_rule' do
it_behaves_like 'auditable concern' do
let!(:instance) { create(:approval_project_rule) }
let_it_be(:instance) { create(:approval_project_rule) }
end
end
describe 'external_status_check' do
it_behaves_like 'auditable concern' do
let!(:instance) { create(:external_status_check) }
let_it_be(:instance) { create(:external_status_check) }
end
end
describe 'packages_package' do
let_it_be(:instance) { create(:generic_package) }
it_behaves_like 'auditable concern'
context 'when calling push_audit_event with after_commit: false', :request_store do
before do
allow(::Gitlab::Audit::EventQueue).to receive(:active?).and_return(true)
end
it 'does not call run_after_commit' do
expect(instance).not_to receive(:run_after_commit)
instance.push_audit_event('event', after_commit: false)
end
end
end
end
......@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Packages::Package, type: :model, feature_category: :package_registry do
it { is_expected.to be_a ::Auditable }
describe '#create_audit_event callback' do
before do
allow(::Packages::CreateAuditEventService).to receive(:new).and_call_original
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::MarkPackageForDestructionService, :aggregate_failures, feature_category: :package_registry do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user, maintainer_of: project) }
let_it_be(:package) { create(:npm_package, project: project) }
let(:service) { described_class.new(container: package, current_user: user) }
describe '#execute' do
it 'calls CreateAuditEventService' do
expect_next_instance_of(
::Packages::CreateAuditEventService,
package,
current_user: user,
event_name: 'package_registry_package_deleted'
) do |service|
expect(service).to receive(:execute)
end
service.execute
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::MarkPackagesForDestructionService, :aggregate_failures, feature_category: :package_registry do
let_it_be(:project) { create(:project) }
let_it_be_with_reload(:packages) { create_list(:nuget_package, 2, project: project) }
let(:user) { project.owner }
let(:service) { described_class.new(packages: ::Packages::Package.id_in(packages.map(&:id)), current_user: user) }
describe '#send_audit_events' do
it 'calls CreateAuditEventsService' do
expect_next_instance_of(
::Packages::CreateAuditEventsService,
packages,
current_user: user
) do |service|
expect(service).to receive(:execute)
end
service.execute
end
end
end
......@@ -8,32 +8,48 @@
let_it_be(:package) { build_stubbed(:generic_package, project: project, creator: user) }
let_it_be(:deploy_token) { build_stubbed(:deploy_token) }
let(:service) { described_class.new(package) }
let(:current_user) { nil }
let(:event_name) { 'package_registry_package_published' }
let(:service) { described_class.new(package, current_user: current_user, event_name: event_name) }
describe '#execute' do
subject(:execute) { service.execute }
include_examples 'audit event logging' do
let(:operation) { execute }
let(:event_type) { 'package_registry_package_published' }
let(:fail_condition!) { stub_feature_flags(package_registry_audit_events: false) }
let(:attributes) do
{
author_id: user.id,
entity_id: project.group.id,
entity_type: 'Group',
details: {
author_name: user.name,
event_name: 'package_registry_package_published',
target_id: package.id,
target_type: package.class.name,
target_details: "#{project.full_path}/#{package.name}-#{package.version}",
author_class: user.class.name,
custom_message: "#{package.package_type.humanize} package published",
auth_token_type: 'PersonalAccessToken or CiJobToken'
}
let(:operation) { execute }
let(:event_type) { event_name }
let(:fail_condition!) { stub_feature_flags(package_registry_audit_events: false) }
let(:attributes) do
{
author_id: user.id,
entity_id: project.group.id,
entity_type: 'Group',
details: {
author_name: user.name,
event_name: event_name,
target_id: package.id,
target_type: package.class.name,
target_details: "#{project.full_path}/#{package.name}-#{package.version}",
author_class: user.class.name,
custom_message: audit_message,
auth_token_type: auth_token_type
}
end
}
end
context 'for package_registry_package_published event' do
let(:audit_message) { "#{package.package_type.humanize} package published" }
let(:auth_token_type) { 'PersonalAccessToken or CiJobToken' }
include_examples 'audit event logging'
end
context 'for package_registry_package_deleted event' do
let(:current_user) { user }
let(:event_name) { 'package_registry_package_deleted' }
let(:audit_message) { "#{package.package_type.humanize} package deleted" }
let(:auth_token_type) { 'PersonalAccessToken' }
include_examples 'audit event logging'
end
context 'when project does not belong to a group' do
......@@ -65,6 +81,44 @@
end
end
context 'with current_user' do
let(:current_user) { user }
it 'sets auth_token_type as PersonalAccessToken' do
expect(::Gitlab::Audit::Auditor).to receive(:audit).with(
hash_including(additional_details: { auth_token_type: 'PersonalAccessToken' })
)
execute
end
context 'when current user is a deploy token' do
let(:current_user) { deploy_token }
it 'sets auth_token_type as DeployToken' do
expect(::Gitlab::Audit::Auditor).to receive(:audit).with(
hash_including(additional_details: { auth_token_type: 'DeployToken' })
)
execute
end
end
context 'when current user is a CiJobToken' do
before do
allow(current_user).to receive(:from_ci_job_token?).and_return(true)
end
it 'sets auth_token_type as CiJobToken' do
expect(::Gitlab::Audit::Auditor).to receive(:audit).with(
hash_including(additional_details: { auth_token_type: 'CiJobToken' })
)
execute
end
end
end
context 'when package has no creator' do
before do
allow(package).to receive(:creator).and_return(nil)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::CreateAuditEventsService, feature_category: :package_registry do
let_it_be(:group_project) { build_stubbed(:project, :in_group) }
let_it_be(:namespace_project) { build_stubbed(:project) }
let_it_be(:user) { build_stubbed(:user, maintainer_of: [group_project, namespace_project]) }
let_it_be(:group_packages) { build_list(:nuget_package, 2, project: group_project) }
let_it_be(:namespace_packages) { build_list(:npm_package, 2, project: namespace_project) }
let_it_be(:packages) { group_packages + namespace_packages }
let(:service) { described_class.new(packages, current_user: user) }
describe '#execute', :request_store do
subject(:execute) { service.execute }
let(:operation) { execute }
let(:event_type) { 'package_registry_package_deleted' }
let(:event_count) { packages.size }
let(:fail_condition!) { stub_feature_flags(package_registry_audit_events: false) }
let(:attributes) do
packages.map do |package|
{
author_id: user.id,
entity_id: package.project.group ? package.project.namespace_id : package.project_id,
entity_type: package.project.group ? 'Group' : 'Project',
details: {
author_name: user.name,
event_name: event_type,
target_id: package.id,
target_type: package.class.name,
target_details: "#{package.project.full_path}/#{package.name}-#{package.version}",
author_class: user.class.name,
custom_message: "#{package.package_type.humanize} package deleted",
auth_token_type: 'PersonalAccessToken'
}
}
end
end
include_examples 'audit event logging'
end
end
......@@ -18,13 +18,17 @@
)).and_call_original
end
expect { operation }.to change(AuditEvent, :count).by(1)
expect { operation }.to change(AuditEvent, :count).by(defined?(event_count) ? event_count : 1)
end
it 'logs the audit event info' do
operation
expect(AuditEvent.last).to have_attributes(attributes)
if defined?(event_count)
expect(AuditEvent.last(event_count)).to match_array(attributes.map { |attrs| have_attributes(attrs) })
else
expect(AuditEvent.last).to have_attributes(attributes)
end
end
it 'calls the audit method with the event type' do
......
......@@ -169,17 +169,21 @@ def send_to_stream(events)
# Defined in EE
end
def build_event(message)
AuditEvents::BuildService.new(
def build_event(message_or_attrs)
params = {
author: @author,
scope: @scope,
target: @target,
created_at: @created_at,
message: message,
message: message_or_attrs,
additional_details: @additional_details,
ip_address: @ip_address,
target_details: @target_details
).execute
}
params.merge!(message_or_attrs.slice(*params.keys)) if message_or_attrs.is_a?(Hash)
AuditEvents::BuildService.new(**params).execute
end
def log_to_database(events)
......
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