diff --git a/app/actions/stack_create.rb b/app/actions/stack_create.rb index e73ed8622ea..55e77bf78f7 100644 --- a/app/actions/stack_create.rb +++ b/app/actions/stack_create.rb @@ -1,8 +1,14 @@ +require 'repositories/stack_event_repository' + module VCAP::CloudController class StackCreate class Error < ::StandardError end + def initialize(user_audit_info) + @user_audit_info = user_audit_info + end + def create(message) stack = VCAP::CloudController::Stack.create( name: message.name, @@ -11,6 +17,8 @@ def create(message) MetadataUpdate.update(stack, message) + Repositories::StackEventRepository.new.record_stack_create(stack, @user_audit_info, message.audit_hash) + stack rescue Sequel::ValidationFailed => e validation_error!(e) diff --git a/app/actions/stack_delete.rb b/app/actions/stack_delete.rb index 0666675bfe6..d392e532ae9 100644 --- a/app/actions/stack_delete.rb +++ b/app/actions/stack_delete.rb @@ -1,7 +1,14 @@ +require 'repositories/stack_event_repository' + module VCAP::CloudController class StackDelete + def initialize(user_audit_info) + @user_audit_info = user_audit_info + end + def delete(stack) stack.db.transaction do + Repositories::StackEventRepository.new.record_stack_delete(stack, @user_audit_info) stack.destroy end end diff --git a/app/actions/stack_update.rb b/app/actions/stack_update.rb index ac66eccf5c0..1980cff17b0 100644 --- a/app/actions/stack_update.rb +++ b/app/actions/stack_update.rb @@ -1,17 +1,22 @@ +require 'repositories/stack_event_repository' + module VCAP::CloudController class StackUpdate class InvalidStack < StandardError end - def initialize + def initialize(user_audit_info) + @user_audit_info = user_audit_info @logger = Steno.logger('cc.action.stack_update') end def update(stack, message) stack.db.transaction do MetadataUpdate.update(stack, message) + Repositories::StackEventRepository.new.record_stack_update(stack, @user_audit_info, message.audit_hash) end @logger.info("Finished updating metadata on stack #{stack.guid}") + stack rescue Sequel::ValidationFailed => e raise InvalidStack.new(e.message) diff --git a/app/controllers/v3/stacks_controller.rb b/app/controllers/v3/stacks_controller.rb index 87109b7097b..29da3d29db1 100644 --- a/app/controllers/v3/stacks_controller.rb +++ b/app/controllers/v3/stacks_controller.rb @@ -36,7 +36,7 @@ def create message = StackCreateMessage.new(hashed_params[:body]) unprocessable!(message.errors.full_messages) unless message.valid? - stack = StackCreate.new.create(message) + stack = StackCreate.new(user_audit_info).create(message) render status: :created, json: Presenters::V3::StackPresenter.new(stack) rescue StackCreate::Error => e @@ -52,7 +52,7 @@ def update message = StackUpdateMessage.new(hashed_params[:body]) unprocessable!(message.errors.full_messages) unless message.valid? - stack = StackUpdate.new.update(stack, message) + stack = StackUpdate.new(user_audit_info).update(stack, message) render status: :ok, json: Presenters::V3::StackPresenter.new(stack) end @@ -84,7 +84,7 @@ def destroy unauthorized! unless permission_queryer.can_write_globally? begin - StackDelete.new.delete(stack) + StackDelete.new(user_audit_info).delete(stack) rescue Stack::AppsStillPresentError unprocessable! "Cannot delete stack '#{stack.name}' because apps are currently using the stack." end diff --git a/app/repositories/event_types.rb b/app/repositories/event_types.rb index a0228f2e65c..a71cbe878ee 100644 --- a/app/repositories/event_types.rb +++ b/app/repositories/event_types.rb @@ -123,6 +123,10 @@ class EventTypesError < StandardError SPACE_UPDATE = 'audit.space.update'.freeze, SPACE_DELETE_REQUEST = 'audit.space.delete-request'.freeze, + STACK_CREATE = 'audit.stack.create'.freeze, + STACK_UPDATE = 'audit.stack.update'.freeze, + STACK_DELETE = 'audit.stack.delete'.freeze, + USER_SPACE_AUDITOR_ADD = 'audit.user.space_auditor_add'.freeze, USER_SPACE_AUDITOR_REMOVE = 'audit.user.space_auditor_remove'.freeze, USER_SPACE_SUPPORTER_ADD = 'audit.user.space_supporter_add'.freeze, diff --git a/app/repositories/stack_event_repository.rb b/app/repositories/stack_event_repository.rb new file mode 100644 index 00000000000..ffbbd6e9b25 --- /dev/null +++ b/app/repositories/stack_event_repository.rb @@ -0,0 +1,62 @@ +require 'repositories/event_types' + +module VCAP::CloudController + module Repositories + class StackEventRepository + def record_stack_create(stack, user_audit_info, request_attrs) + Event.create( + type: EventTypes::STACK_CREATE, + actee: stack.guid, + actee_type: 'stack', + actee_name: stack.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 + + def record_stack_update(stack, user_audit_info, request_attrs) + Event.create( + type: EventTypes::STACK_UPDATE, + actee: stack.guid, + actee_type: 'stack', + actee_name: stack.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 + + def record_stack_delete(stack, user_audit_info) + Event.create( + type: EventTypes::STACK_DELETE, + actee: stack.guid, + actee_type: 'stack', + actee_name: stack.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: {} + ) + end + end + end +end 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 a8919243425..c6671ec423a 100644 --- a/docs/v3/source/includes/resources/audit_events/_header.md.erb +++ b/docs/v3/source/includes/resources/audit_events/_header.md.erb @@ -129,6 +129,11 @@ For more information, see the [Cloud Foundry docs](https://docs.cloudfoundry.org - `audit.space.delete-request` - `audit.space.update` +##### Stack lifecycle +- `audit.stack.create` +- `audit.stack.delete` +- `audit.stack.update` + ##### User lifecycle - `audit.user.organization_auditor_add` - `audit.user.organization_auditor_remove` diff --git a/spec/unit/actions/stack_create_spec.rb b/spec/unit/actions/stack_create_spec.rb index 19a45e0748b..e554e431da8 100644 --- a/spec/unit/actions/stack_create_spec.rb +++ b/spec/unit/actions/stack_create_spec.rb @@ -5,6 +5,12 @@ module VCAP::CloudController RSpec.describe StackCreate do describe 'create' do + let(:user) { User.make } + let(:user_email) { 'user@example.com' } + let(:user_audit_info) { UserAuditInfo.new(user_guid: user.guid, user_email: user_email) } + + subject(:stack_create) { StackCreate.new(user_audit_info) } + it 'creates a stack' do message = VCAP::CloudController::StackCreateMessage.new( name: 'the-name', @@ -20,7 +26,7 @@ module VCAP::CloudController } } ) - stack = StackCreate.new.create(message) + stack = stack_create.create(message) expect(stack.name).to eq('the-name') expect(stack.description).to eq('the-description') @@ -35,6 +41,31 @@ module VCAP::CloudController ) end + it 'creates an audit event' do + message = VCAP::CloudController::StackCreateMessage.new( + name: 'my-stack', + description: 'my-description' + ) + created_stack = stack_create.create(message) + + expect(VCAP::CloudController::Event.count).to eq(1) + stack_create_event = VCAP::CloudController::Event.find(type: 'audit.stack.create') + expect(stack_create_event).to exist + expect(stack_create_event.values).to include( + type: 'audit.stack.create', + actor: user_audit_info.user_guid, + actor_type: 'user', + actor_name: user_audit_info.user_email, + actee: created_stack.guid, + actee_type: 'stack', + actee_name: 'my-stack', + space_guid: '', + organization_guid: '' + ) + expect(stack_create_event.metadata).to eq({ 'request' => message.audit_hash }) + expect(stack_create_event.timestamp).to be + end + context 'when a model validation fails' do it 'raises an error' do errors = Sequel::Model::Errors.new @@ -44,7 +75,7 @@ module VCAP::CloudController message = VCAP::CloudController::StackCreateMessage.new(name: 'foobar') expect do - StackCreate.new.create(message) + stack_create.create(message) end.to raise_error(StackCreate::Error, 'blork is busted') end end @@ -59,7 +90,7 @@ module VCAP::CloudController it 'raises a human-friendly error' do message = VCAP::CloudController::StackCreateMessage.new(name:) expect do - StackCreate.new.create(message) + stack_create.create(message) end.to raise_error(StackCreate::Error, 'Name must be unique') end end @@ -71,7 +102,7 @@ module VCAP::CloudController message = VCAP::CloudController::StackCreateMessage.new(name:) # First request, should succeed expect do - StackCreate.new.create(message) + stack_create.create(message) end.not_to raise_error # Mock the validation for the second request to simulate the race condition and trigger a unique constraint violation @@ -79,7 +110,7 @@ module VCAP::CloudController # Second request, should fail with correct error expect do - StackCreate.new.create(message) + stack_create.create(message) end.to raise_error(StackCreate::Error, 'Name must be unique') end end diff --git a/spec/unit/actions/stack_delete_spec.rb b/spec/unit/actions/stack_delete_spec.rb index 4630bd57f39..dda5885bc90 100644 --- a/spec/unit/actions/stack_delete_spec.rb +++ b/spec/unit/actions/stack_delete_spec.rb @@ -3,7 +3,11 @@ module VCAP::CloudController RSpec.describe StackDelete do - subject(:stack_delete) { StackDelete.new } + let(:user) { User.make } + let(:user_email) { 'user@example.com' } + let(:user_audit_info) { UserAuditInfo.new(user_guid: user.guid, user_email: user_email) } + + subject(:stack_delete) { StackDelete.new(user_audit_info) } describe '#delete' do context 'when the stack exists' do @@ -16,6 +20,30 @@ module VCAP::CloudController expect { stack.refresh }.to raise_error(Sequel::Error, 'Record not found') end + it 'creates an audit event' do + stack_guid = stack.guid + stack_name = stack.name + + stack_delete.delete(stack) + + expect(VCAP::CloudController::Event.count).to eq(1) + stack_delete_event = VCAP::CloudController::Event.find(type: 'audit.stack.delete') + expect(stack_delete_event).to exist + expect(stack_delete_event.values).to include( + type: 'audit.stack.delete', + actor: user_audit_info.user_guid, + actor_type: 'user', + actor_name: user_audit_info.user_email, + actee: stack_guid, + actee_type: 'stack', + actee_name: stack_name, + space_guid: '', + organization_guid: '' + ) + expect(stack_delete_event.metadata).to eq({}) + expect(stack_delete_event.timestamp).to be + end + it 'deletes associated labels' do label = StackLabelModel.make(resource_guid: stack.guid, key_name: 'test1', value: 'bommel') expect do diff --git a/spec/unit/actions/stack_update_spec.rb b/spec/unit/actions/stack_update_spec.rb index c28f0608ef8..383d4eb1179 100644 --- a/spec/unit/actions/stack_update_spec.rb +++ b/spec/unit/actions/stack_update_spec.rb @@ -3,7 +3,11 @@ module VCAP::CloudController RSpec.describe StackUpdate do - subject(:stack_update) { StackUpdate.new } + let(:user) { User.make } + let(:user_email) { 'user@example.com' } + let(:user_audit_info) { UserAuditInfo.new(user_guid: user.guid, user_email: user_email) } + + subject(:stack_update) { StackUpdate.new(user_audit_info) } describe '#update' do let(:body) do @@ -29,6 +33,27 @@ module VCAP::CloudController expect(stack).to have_labels({ key_name: 'freaky', value: 'wednesday' }) expect(stack).to have_annotations({ key_name: 'tokyo', value: 'grapes' }) end + + it 'creates an audit event' do + stack_update.update(stack, message) + + expect(VCAP::CloudController::Event.count).to eq(1) + stack_update_event = VCAP::CloudController::Event.find(type: 'audit.stack.update') + expect(stack_update_event).to exist + expect(stack_update_event.values).to include( + type: 'audit.stack.update', + actor: user_audit_info.user_guid, + actor_type: 'user', + actor_name: user_audit_info.user_email, + actee: stack.guid, + actee_type: 'stack', + actee_name: stack.name, + space_guid: '', + organization_guid: '' + ) + expect(stack_update_event.metadata).to eq({ 'request' => message.audit_hash }) + expect(stack_update_event.timestamp).to be + end end end end diff --git a/spec/unit/repositories/event_types_spec.rb b/spec/unit/repositories/event_types_spec.rb index fe660224052..6c6b74bb5c8 100644 --- a/spec/unit/repositories/event_types_spec.rb +++ b/spec/unit/repositories/event_types_spec.rb @@ -121,6 +121,9 @@ module Repositories 'audit.space.create', 'audit.space.update', 'audit.space.delete-request', + 'audit.stack.create', + 'audit.stack.update', + 'audit.stack.delete', 'audit.user.space_auditor_add', 'audit.user.space_auditor_remove', 'audit.user.space_supporter_add', diff --git a/spec/unit/repositories/stack_event_repository_spec.rb b/spec/unit/repositories/stack_event_repository_spec.rb new file mode 100644 index 00000000000..c4d5032743b --- /dev/null +++ b/spec/unit/repositories/stack_event_repository_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' +require 'repositories/stack_event_repository' + +module VCAP::CloudController + module Repositories + RSpec.describe StackEventRepository do + let(:request_attrs) { { 'name' => 'new-stack' } } + let(:user) { User.make } + let(:stack) { Stack.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(:stack_event_repository) { StackEventRepository.new } + + describe '#record_stack_create' do + it 'records event correctly' do + event = stack_event_repository.record_stack_create(stack, 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.stack.create') + expect(event.actee).to eq(stack.guid) + expect(event.actee_type).to eq('stack') + expect(event.actee_name).to eq(stack.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_stack_update' do + it 'records event correctly' do + event = stack_event_repository.record_stack_update(stack, 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.stack.update') + expect(event.actee).to eq(stack.guid) + expect(event.actee_type).to eq('stack') + expect(event.actee_name).to eq(stack.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_stack_delete' do + it 'records event correctly' do + event = stack_event_repository.record_stack_delete(stack, user_audit_info) + event.reload + expect(event.space_guid).to eq('') + expect(event.organization_guid).to eq('') + expect(event.type).to eq('audit.stack.delete') + expect(event.actee).to eq(stack.guid) + expect(event.actee_type).to eq('stack') + expect(event.actee_name).to eq(stack.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 + end + end +end