Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/cloud_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ module VCAP::CloudController; end
require 'cloud_controller/user_audit_info'

require 'cloud_controller/errands/rotate_database_key'
require 'cloud_controller/errands/check_database_key'

require 'services'

Expand Down
42 changes: 42 additions & 0 deletions lib/cloud_controller/errands/check_database_key.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# rubocop:disable Rails/Exit
module VCAP::CloudController
class CheckDatabaseKey
class << self
def perform
@logger = Steno.logger('cc.check_database_key')

if Encryptor.current_encryption_key_label.blank?
logger.error('No current encryption key label is set')
exit 1
end

rows_needing_rotation = count_rows_needing_rotation
if rows_needing_rotation > 0
logger.info("#{rows_needing_rotation} row(s) need re-encryption with the current key")
exit 2
end

logger.info('All rows are encrypted with the current key')
exit 0
rescue StandardError => e
logger.error("Unexpected error: #{e.class}: #{e.message}")
exit 1
end

private

attr_accessor :logger

def count_rows_needing_rotation
current_key_label = Encryptor.current_encryption_key_label
Encryptor.encrypted_classes.sum do |klass|
klass.constantize.
exclude(encryption_key_label: current_key_label).
or(encryption_key_label: nil).
count
end
end
end
end
end
# rubocop:enable Rails/Exit
8 changes: 8 additions & 0 deletions lib/tasks/rotate_database_key.rake
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,12 @@ namespace :rotate_cc_database_key do
BoshErrandEnvironment.new(RakeConfig.config).setup_environment
VCAP::CloudController::RotateDatabaseKey.perform
end

desc 'Check if database rows need re-encryption with the current key'
task check: :environment do
require 'cloud_controller/errands/check_database_key'
RakeConfig.context = :rotate_database_key
BoshErrandEnvironment.new(RakeConfig.config).setup_environment
VCAP::CloudController::CheckDatabaseKey.perform
end
end
89 changes: 89 additions & 0 deletions spec/unit/lib/cloud_controller/errands/check_database_key_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
require 'spec_helper'

module VCAP::CloudController
RSpec.describe CheckDatabaseKey do
describe '#perform' do
let(:logger) { instance_double(Steno::Logger, info: nil, error: nil) }

before do
allow(Steno).to receive(:logger).and_return(logger)
allow(Encryptor).to receive_messages(
encrypted_classes: ['VCAP::CloudController::AppModel', 'VCAP::CloudController::TaskModel'],
current_encryption_key_label: 'current'
)
end

context 'when no current encryption key label is set' do
before do
allow(Encryptor).to receive(:current_encryption_key_label).and_return(nil)
end

it 'exits with 1' do
expect { CheckDatabaseKey.perform }.to raise_error(SystemExit) { |e| expect(e.status).to eq(1) }
end

it 'logs an error' do
expect { CheckDatabaseKey.perform }.to raise_error(SystemExit)
expect(logger).to have_received(:error).with('No current encryption key label is set')
end
end

context 'when all rows are encrypted with the current key' do
before do
allow(AppModel).to receive(:exclude).and_return(double(or: double(count: 0)))
allow(TaskModel).to receive(:exclude).and_return(double(or: double(count: 0)))
end

it 'exits with 0' do
expect { CheckDatabaseKey.perform }.to raise_error(SystemExit) { |e| expect(e.status).to eq(0) }
end

it 'logs that all rows are current' do
expect { CheckDatabaseKey.perform }.to raise_error(SystemExit)
expect(logger).to have_received(:info).with('All rows are encrypted with the current key')
end
end

context 'when rows need re-encryption' do
before do
allow(Encryptor).to receive(:current_encryption_key_label).and_return('old')
allow(Encryptor).to receive(:database_encryption_keys).and_return({ old: 'old-key', current: 'current-key' })

app = create(:app_model)
app.environment_variables = { 'key' => 'value' }
app.save(validate: false)

task = create(:task_model)
task.environment_variables = { 'key' => 'value' }
task.save(validate: false)

allow(Encryptor).to receive(:current_encryption_key_label).and_return('current')
end

it 'exits with 2' do
expect { CheckDatabaseKey.perform }.to raise_error(SystemExit) { |e| expect(e.status).to eq(2) }
end

it 'logs the total number of rows needing re-encryption' do
expect { CheckDatabaseKey.perform }.to raise_error(SystemExit)
expect(logger).to have_received(:info).with(a_string_matching(/\d+ row\(s\) need re-encryption with the current key/))
end
end

context 'when an unexpected error occurs' do
before do
allow(AppModel).to receive(:exclude).and_raise(StandardError.new('db connection failed'))
end

it 'exits with 1' do
expect { CheckDatabaseKey.perform }.to raise_error(SystemExit) { |e| expect(e.status).to eq(1) }
end

it 'logs the error' do
expect { CheckDatabaseKey.perform }.to raise_error(SystemExit)
expect(logger).to have_received(:error).with('Unexpected error: StandardError: db connection failed')
end
end
end
end
end
Loading