From 45317652d580f0c05f21e402d5dca4f1cd4dbd1d Mon Sep 17 00:00:00 2001 From: Sam Gunaratne Date: Tue, 20 Jan 2026 16:27:21 -0700 Subject: [PATCH] Add buildpack upload audit event --- app/actions/buildpack_upload.rb | 6 +- app/controllers/v3/buildpacks_controller.rb | 2 +- app/jobs/v3/buildpack_bits.rb | 9 +- .../buildpack_event_repository.rb | 19 ++++ app/repositories/event_types.rb | 1 + .../resources/audit_events/_header.md.erb | 1 + spec/unit/actions/buildpack_upload_spec.rb | 11 ++- spec/unit/jobs/v3/buildpack_bits_spec.rb | 62 +++++++++++++ .../buildpack_event_repository_spec.rb | 89 +++++++++++++++++++ spec/unit/repositories/event_types_spec.rb | 1 + 10 files changed, 195 insertions(+), 6 deletions(-) create mode 100644 spec/unit/repositories/buildpack_event_repository_spec.rb diff --git a/app/actions/buildpack_upload.rb b/app/actions/buildpack_upload.rb index 7b700a40735..92fa6c81321 100644 --- a/app/actions/buildpack_upload.rb +++ b/app/actions/buildpack_upload.rb @@ -1,9 +1,13 @@ module VCAP::CloudController class BuildpackUpload + def initialize(user_audit_info) + @user_audit_info = user_audit_info + end + def upload_async(message:, buildpack:, config:) logger.info("uploading buildpacks bits for buildpack #{buildpack.guid}") - upload_job = Jobs::V3::BuildpackBits.new(buildpack.guid, message.bits_path, message.bits_name) + upload_job = Jobs::V3::BuildpackBits.new(buildpack.guid, message.bits_path, message.bits_name, @user_audit_info, message.audit_hash) Jobs::Enqueuer.new(queue: Jobs::Queues.local(config)).enqueue_pollable(upload_job) end diff --git a/app/controllers/v3/buildpacks_controller.rb b/app/controllers/v3/buildpacks_controller.rb index 87ef45097d9..b6a3592c374 100644 --- a/app/controllers/v3/buildpacks_controller.rb +++ b/app/controllers/v3/buildpacks_controller.rb @@ -82,7 +82,7 @@ def upload unprocessable!('Buildpack is locked') if buildpack.locked - pollable_job = BuildpackUpload.new.upload_async( + pollable_job = BuildpackUpload.new(user_audit_info).upload_async( message: message, buildpack: buildpack, config: configuration diff --git a/app/jobs/v3/buildpack_bits.rb b/app/jobs/v3/buildpack_bits.rb index 3ca068dbaac..a60a22f8fa1 100644 --- a/app/jobs/v3/buildpack_bits.rb +++ b/app/jobs/v3/buildpack_bits.rb @@ -1,4 +1,5 @@ require 'cloud_controller/upload_buildpack' +require 'repositories/buildpack_event_repository' module VCAP::CloudController module Jobs @@ -7,10 +8,12 @@ class BuildpackBits attr_reader :buildpack_guid alias_method :resource_guid, :buildpack_guid - def initialize(buildpack_guid, buildpack_bits_path, buildpack_bits_name) + def initialize(buildpack_guid, buildpack_bits_path, buildpack_bits_name, user_audit_info=nil, request_attrs=nil) @buildpack_guid = buildpack_guid @file_path = buildpack_bits_path @file_name = buildpack_bits_name + @user_audit_info = user_audit_info + @request_attrs = request_attrs end def perform @@ -19,7 +22,9 @@ def perform buildpack_blobstore = CloudController::DependencyLocator.instance.buildpack_blobstore buildpack = Buildpack.find(guid: buildpack_guid) - VCAP::CloudController::UploadBuildpack.new(buildpack_blobstore).upload_buildpack(buildpack, file_path, file_name) + upload_successful = VCAP::CloudController::UploadBuildpack.new(buildpack_blobstore).upload_buildpack(buildpack, file_path, file_name) + + Repositories::BuildpackEventRepository.new.record_buildpack_upload(buildpack, @user_audit_info, @request_attrs || {}) if upload_successful && @user_audit_info ensure FileUtils.rm_f(file_path) end diff --git a/app/repositories/buildpack_event_repository.rb b/app/repositories/buildpack_event_repository.rb index ade759b718a..3fb54a17e23 100644 --- a/app/repositories/buildpack_event_repository.rb +++ b/app/repositories/buildpack_event_repository.rb @@ -57,6 +57,25 @@ def record_buildpack_delete(buildpack, user_audit_info) metadata: {} ) end + + def record_buildpack_upload(buildpack, user_audit_info, request_attrs) + Event.create( + type: EventTypes::BUILDPACK_UPLOAD, + actee: buildpack.guid, + actee_type: 'buildpack', + actee_name: buildpack.name, + actor: user_audit_info.user_guid, + actor_type: 'user', + actor_name: user_audit_info.user_email, + actor_username: user_audit_info.user_name, + timestamp: Sequel::CURRENT_TIMESTAMP, + space_guid: '', + organization_guid: '', + metadata: { + request: request_attrs + } + ) + end end end end diff --git a/app/repositories/event_types.rb b/app/repositories/event_types.rb index e4442f4b7d5..c0776245db4 100644 --- a/app/repositories/event_types.rb +++ b/app/repositories/event_types.rb @@ -57,6 +57,7 @@ class EventTypesError < StandardError BUILDPACK_CREATE = 'audit.buildpack.create'.freeze, BUILDPACK_UPDATE = 'audit.buildpack.update'.freeze, BUILDPACK_DELETE = 'audit.buildpack.delete'.freeze, + BUILDPACK_UPLOAD = 'audit.buildpack.upload'.freeze, SERVICE_CREATE = 'audit.service.create'.freeze, SERVICE_UPDATE = 'audit.service.update'.freeze, diff --git a/docs/v3/source/includes/resources/audit_events/_header.md.erb b/docs/v3/source/includes/resources/audit_events/_header.md.erb index b12e7275468..fed9c4192a3 100644 --- a/docs/v3/source/includes/resources/audit_events/_header.md.erb +++ b/docs/v3/source/includes/resources/audit_events/_header.md.erb @@ -54,6 +54,7 @@ For more information, see the [Cloud Foundry docs](https://docs.cloudfoundry.org - `audit.buildpack.create` - `audit.buildpack.delete` - `audit.buildpack.update` +- `audit.buildpack.upload` ##### Organization lifecycle - `audit.organization.create` diff --git a/spec/unit/actions/buildpack_upload_spec.rb b/spec/unit/actions/buildpack_upload_spec.rb index 642e56e1428..ffb4b857d40 100644 --- a/spec/unit/actions/buildpack_upload_spec.rb +++ b/spec/unit/actions/buildpack_upload_spec.rb @@ -3,7 +3,12 @@ module VCAP::CloudController RSpec.describe BuildpackUpload do - subject(:buildpack_upload) { BuildpackUpload.new } + let(:user) { User.make } + let(:user_email) { 'user@example.com' } + let(:user_name) { 'user-name' } + let(:user_audit_info) { UserAuditInfo.new(user_guid: user.guid, user_email: user_email, user_name: user_name) } + + subject(:buildpack_upload) { BuildpackUpload.new(user_audit_info) } describe '#upload_async' do let!(:buildpack) { VCAP::CloudController::Buildpack.create_from_hash({ name: 'upload_binary_buildpack', stack: nil, position: 0 }) } @@ -37,7 +42,9 @@ module VCAP::CloudController expect(Jobs::V3::BuildpackBits).to receive(:new).with( buildpack.guid, '/tmp/path', - 'buildpack.zip' + 'buildpack.zip', + user_audit_info, + message.audit_hash ).and_call_original buildpack_upload.upload_async(message:, buildpack:, config:) end diff --git a/spec/unit/jobs/v3/buildpack_bits_spec.rb b/spec/unit/jobs/v3/buildpack_bits_spec.rb index eadf1d454a8..fde04266bdf 100644 --- a/spec/unit/jobs/v3/buildpack_bits_spec.rb +++ b/spec/unit/jobs/v3/buildpack_bits_spec.rb @@ -8,6 +8,11 @@ module Jobs::V3 let(:filename) { 'buildpack.zip' } let!(:buildpack) { Buildpack.make } let(:buildpack_guid) { buildpack.guid } + let(:user) { User.make } + let(:user_email) { 'user@example.com' } + let(:user_name) { 'user-name' } + let(:user_audit_info) { UserAuditInfo.new(user_guid: user.guid, user_email: user_email, user_name: user_name) } + let(:request_attrs) { { 'bits_name' => filename } } subject(:job) do BuildpackBits.new(buildpack_guid, uploaded_path, filename) @@ -27,6 +32,63 @@ module Jobs::V3 it 'knows its job name' do expect(job.job_name_in_configuration).to equal(:buildpack_bits) end + + context 'when user_audit_info is provided' do + subject(:job) do + BuildpackBits.new(buildpack_guid, uploaded_path, filename, user_audit_info, request_attrs) + end + + it 'creates an audit event when upload is successful' do + uploader = instance_double(UploadBuildpack) + allow(UploadBuildpack).to receive(:new).and_return(uploader) + allow(uploader).to receive(:upload_buildpack).and_return(true) + allow(FileUtils).to receive(:rm_f) + + expect do + job.perform + end.to change(Event, :count).by(1) + + event = Event.last + expect(event.values).to include( + type: 'audit.buildpack.upload', + actee: buildpack.guid, + actee_type: 'buildpack', + actee_name: buildpack.name, + actor: user_audit_info.user_guid, + actor_type: 'user', + actor_name: user_audit_info.user_email, + actor_username: user_audit_info.user_name, + space_guid: '', + organization_guid: '' + ) + expect(event.metadata).to eq({ 'request' => request_attrs }) + expect(event.timestamp).to be + end + + it 'does not create an audit event when upload fails' do + uploader = instance_double(UploadBuildpack) + allow(UploadBuildpack).to receive(:new).and_return(uploader) + allow(uploader).to receive(:upload_buildpack).and_return(false) + allow(FileUtils).to receive(:rm_f) + + expect do + job.perform + end.not_to change(Event, :count) + end + end + + context 'when user_audit_info is not provided' do + it 'does not create an audit event' do + uploader = instance_double(UploadBuildpack) + allow(UploadBuildpack).to receive(:new).and_return(uploader) + allow(uploader).to receive(:upload_buildpack).and_return(true) + allow(FileUtils).to receive(:rm_f) + + expect do + job.perform + end.not_to change(Event, :count) + end + end end end end diff --git a/spec/unit/repositories/buildpack_event_repository_spec.rb b/spec/unit/repositories/buildpack_event_repository_spec.rb new file mode 100644 index 00000000000..46f8c5b3313 --- /dev/null +++ b/spec/unit/repositories/buildpack_event_repository_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' +require 'repositories/buildpack_event_repository' + +module VCAP::CloudController + module Repositories + RSpec.describe BuildpackEventRepository do + let(:request_attrs) { { 'name' => 'new-buildpack' } } + let(:user) { User.make } + let(:buildpack) { Buildpack.make } + let(:user_email) { 'email address' } + let(:user_name) { 'user name' } + let(:user_audit_info) { UserAuditInfo.new(user_email: user_email, user_guid: user.guid, user_name: user_name) } + + subject(:buildpack_event_repository) { BuildpackEventRepository.new } + + describe '#record_buildpack_create' do + it 'records event correctly' do + event = buildpack_event_repository.record_buildpack_create(buildpack, user_audit_info, request_attrs) + event.reload + expect(event.space_guid).to eq('') + expect(event.organization_guid).to eq('') + expect(event.type).to eq('audit.buildpack.create') + expect(event.actee).to eq(buildpack.guid) + expect(event.actee_type).to eq('buildpack') + expect(event.actee_name).to eq(buildpack.name) + expect(event.actor).to eq(user.guid) + expect(event.actor_type).to eq('user') + expect(event.actor_name).to eq(user_email) + expect(event.actor_username).to eq(user_name) + expect(event.metadata).to eq({ 'request' => request_attrs }) + end + end + + describe '#record_buildpack_update' do + it 'records event correctly' do + event = buildpack_event_repository.record_buildpack_update(buildpack, user_audit_info, request_attrs) + event.reload + expect(event.space_guid).to eq('') + expect(event.organization_guid).to eq('') + expect(event.type).to eq('audit.buildpack.update') + expect(event.actee).to eq(buildpack.guid) + expect(event.actee_type).to eq('buildpack') + expect(event.actee_name).to eq(buildpack.name) + expect(event.actor).to eq(user.guid) + expect(event.actor_type).to eq('user') + expect(event.actor_name).to eq(user_email) + expect(event.actor_username).to eq(user_name) + expect(event.metadata).to eq({ 'request' => request_attrs }) + end + end + + describe '#record_buildpack_delete' do + it 'records event correctly' do + event = buildpack_event_repository.record_buildpack_delete(buildpack, user_audit_info) + event.reload + expect(event.space_guid).to eq('') + expect(event.organization_guid).to eq('') + expect(event.type).to eq('audit.buildpack.delete') + expect(event.actee).to eq(buildpack.guid) + expect(event.actee_type).to eq('buildpack') + expect(event.actee_name).to eq(buildpack.name) + expect(event.actor).to eq(user.guid) + expect(event.actor_type).to eq('user') + expect(event.actor_name).to eq(user_email) + expect(event.actor_username).to eq(user_name) + expect(event.metadata).to eq({}) + end + end + + describe '#record_buildpack_upload' do + it 'records event correctly' do + event = buildpack_event_repository.record_buildpack_upload(buildpack, user_audit_info, request_attrs) + event.reload + expect(event.space_guid).to eq('') + expect(event.organization_guid).to eq('') + expect(event.type).to eq('audit.buildpack.upload') + expect(event.actee).to eq(buildpack.guid) + expect(event.actee_type).to eq('buildpack') + expect(event.actee_name).to eq(buildpack.name) + expect(event.actor).to eq(user.guid) + expect(event.actor_type).to eq('user') + expect(event.actor_name).to eq(user_email) + expect(event.actor_username).to eq(user_name) + expect(event.metadata).to eq({ 'request' => request_attrs }) + end + end + end + end +end diff --git a/spec/unit/repositories/event_types_spec.rb b/spec/unit/repositories/event_types_spec.rb index 593303eecbf..5b923574888 100644 --- a/spec/unit/repositories/event_types_spec.rb +++ b/spec/unit/repositories/event_types_spec.rb @@ -67,6 +67,7 @@ module Repositories 'audit.buildpack.create', 'audit.buildpack.update', 'audit.buildpack.delete', + 'audit.buildpack.upload', 'audit.service.create', 'audit.service.update', 'audit.service.delete',