spec/unit/lib/services/sso/dashboard_client_manager_spec.rb
require 'spec_helper'
module VCAP::Services::SSO
RSpec.describe DashboardClientManager do
let(:client_manager) { double('client_manager') }
let(:user) { VCAP::CloudController::User.make }
let(:email) { 'email@example.com' }
let(:security_context) { double(:security_context, current_user: user, current_user_email: email) }
let(:services_event_repository) do
VCAP::CloudController::Repositories::ServiceEventRepository.new(
VCAP::CloudController::UserAuditInfo.new(user_guid: user.guid, user_email: email)
)
end
context 'for service brokers' do
let(:service_broker) { VCAP::CloudController::ServiceBroker.make }
let(:manager) { DashboardClientManager.new(service_broker, services_event_repository) }
describe '#synchronize_clients_with_catalog' do
let(:dashboard_client_attrs_1) do
{
'id' => 'abcde123',
'secret' => 'sekret',
'redirect_uri' => 'http://example.com'
}
end
let(:dashboard_client_attrs_2) do
{
'id' => 'fghijk456',
'secret' => 'differentsekret',
'redirect_uri' => 'http://example.com/somethingelse'
}
end
let(:catalog_service) do
VCAP::Services::ServiceBrokers::V2::CatalogService.new(service_broker,
'id' => 'f8ccf75f-4552-4143-97ea-24ccca5ad068',
'dashboard_client' => dashboard_client_attrs_1,
'name' => 'service-1')
end
let(:catalog_service_2) do
VCAP::Services::ServiceBrokers::V2::CatalogService.new(service_broker,
'id' => '0489055c-97b8-4754-8221-c69375ddb33b',
'dashboard_client' => dashboard_client_attrs_2,
'name' => 'service-2')
end
let(:catalog_service_without_dashboard_client) do
VCAP::Services::ServiceBrokers::V2::CatalogService.new(service_broker,
'id' => '4b6088af-cdc4-4ee2-8292-9fa93af32fc8',
'name' => 'service-3')
end
let(:catalog_services) { [catalog_service, catalog_service_2, catalog_service_without_dashboard_client] }
let(:catalog) { double(:catalog, services: catalog_services) }
before do
allow(VCAP::Services::SSO::UAA::UaaClientManager).to receive(:new).and_return(client_manager)
allow(client_manager).to receive(:get_clients).and_return([])
allow(client_manager).to receive(:modify_transaction)
end
describe 'modifying the UAA and CCDB' do
context 'when no dashboard sso clients present in the catalog exist in UAA' do
before do
allow(client_manager).to receive(:get_clients).and_return([])
end
it 'creates clients only for all services that specify dashboard_client' do
expect(client_manager).to receive(:modify_transaction) do |changeset|
expect(changeset.length).to eq 2
expect(changeset.all? { |change| change.is_a? Commands::CreateClientCommand }).to be true
expect(changeset[0].client_attrs).to eq dashboard_client_attrs_1
expect(changeset[1].client_attrs).to eq dashboard_client_attrs_2
end
manager.synchronize_clients_with_catalog(catalog)
end
it 'claims the clients' do
expect do
manager.synchronize_clients_with_catalog(catalog)
end.to change(VCAP::CloudController::ServiceDashboardClient, :count).by 2
expect(VCAP::CloudController::ServiceDashboardClient.find(uaa_id: dashboard_client_attrs_1['id'])).not_to be_nil
expect(VCAP::CloudController::ServiceDashboardClient.find(uaa_id: dashboard_client_attrs_2['id'])).not_to be_nil
end
it 'returns true' do
expect(manager.synchronize_clients_with_catalog(catalog)).to be(true)
end
it 'records a create event for each dashboard client' do
client_id = dashboard_client_attrs_1['id']
manager.synchronize_clients_with_catalog(catalog)
expect(VCAP::CloudController::Event.where(type: 'audit.service_dashboard_client.create').count).to eq 2
event = VCAP::CloudController::Event.first(type: 'audit.service_dashboard_client.create', actee_name: client_id)
expect(event.actor_type).to eq('service_broker')
expect(event.actor).to eq(service_broker.guid)
expect(event.actor_name).to eq(service_broker.name)
expect(event.timestamp).to be
expect(event.actee).to eq(client_id)
expect(event.actee_type).to eq('service_dashboard_client')
expect(event.actee_name).to eq(client_id)
expect(event.space_guid).to eq('')
expect(event.organization_guid).to eq('')
expect(event.metadata).to include({
'secret' => '[REDACTED]',
'redirect_uri' => dashboard_client_attrs_1['redirect_uri']
})
end
it 'does not include the metadata.service_instance_guid field' do
client_id = dashboard_client_attrs_1['id']
manager.synchronize_clients_with_catalog(catalog)
event = VCAP::CloudController::Event.first(type: 'audit.service_dashboard_client.create', actee_name: client_id)
expect(event.metadata['service_instance_guid']).to be_nil
end
end
context 'when some, but not all dashboard sso clients exist in UAA' do
before do
allow(client_manager).to receive(:get_clients).and_return([{ 'client_id' => catalog_service.dashboard_client['id'] }])
end
context 'when the broker has already claimed a requested UAA client' do
before do
VCAP::CloudController::ServiceDashboardClient.new(
uaa_id: catalog_service.dashboard_client['id'],
service_broker: service_broker
).save
end
it 'creates the clients that do not currently exist' do
expect(client_manager).to receive(:modify_transaction) do |changeset|
create_commands = changeset.select { |command| command.is_a? Commands::CreateClientCommand }
expect(create_commands.length).to eq 1
expect(create_commands[0].client_attrs).to eq dashboard_client_attrs_2
end
manager.synchronize_clients_with_catalog(catalog)
end
it 'updates the client that is already in uaa' do
expect(client_manager).to receive(:modify_transaction) do |changeset|
update_commands = changeset.select { |command| command.is_a? Commands::UpdateClientCommand }
expect(update_commands.length).to eq 1
expect(update_commands[0].client_attrs).to eq dashboard_client_attrs_1
end
manager.synchronize_clients_with_catalog(catalog)
end
it 'claims the clients' do
expect do
manager.synchronize_clients_with_catalog(catalog)
end.to change(VCAP::CloudController::ServiceDashboardClient, :count).by 1
expect(VCAP::CloudController::ServiceDashboardClient.find(uaa_id: dashboard_client_attrs_1['id'])).not_to be_nil
expect(VCAP::CloudController::ServiceDashboardClient.find(uaa_id: dashboard_client_attrs_2['id'])).not_to be_nil
end
it 'records a create event for each new dashboard client' do
client_id = dashboard_client_attrs_2['id']
manager.synchronize_clients_with_catalog(catalog)
expect(VCAP::CloudController::Event.where(type: 'audit.service_dashboard_client.create').count).to eq 1
event = VCAP::CloudController::Event.first(type: 'audit.service_dashboard_client.create', actee_name: client_id)
expect(event.actor_type).to eq('service_broker')
expect(event.actor).to eq(service_broker.guid)
expect(event.actor_name).to eq(service_broker.name)
expect(event.timestamp).to be
expect(event.actee).to eq(client_id)
expect(event.actee_type).to eq('service_dashboard_client')
expect(event.actee_name).to eq(client_id)
expect(event.space_guid).to eq('')
expect(event.organization_guid).to eq('')
expect(event.metadata).to include({
'secret' => '[REDACTED]',
'redirect_uri' => dashboard_client_attrs_2['redirect_uri']
})
end
it 'returns true' do
expect(manager.synchronize_clients_with_catalog(catalog)).to be(true)
end
end
context 'when there is no claim but the requested client exists in UAA' do
it 'does not create any uaa clients' do
manager.synchronize_clients_with_catalog(catalog)
expect(client_manager).not_to have_received(:modify_transaction)
end
it 'does not claim any clients for CC' do
expect(VCAP::CloudController::ServiceDashboardClient.count).to eq(0)
expect { manager.synchronize_clients_with_catalog(catalog) }.not_to(change(VCAP::CloudController::ServiceDashboardClient, :count))
end
it 'returns false' do
expect(manager.synchronize_clients_with_catalog(catalog)).to be(false)
end
it 'has errors for the service' do
manager.synchronize_clients_with_catalog(catalog)
expect(manager.errors.for(catalog_service)).not_to be_empty
end
end
end
context 'when the UAA has clients claimed by CC that are no longer used by a service' do
let(:unused_id) { 'no-longer-used' }
let(:catalog) do
double(:catalog, services: [catalog_service, catalog_service_without_dashboard_client])
end
before do
allow(client_manager).to receive(:get_clients) do |ids|
ids.map do |id|
{ 'client_id' => id }
end
end
VCAP::CloudController::ServiceDashboardClient.new(
uaa_id: unused_id,
service_broker: service_broker
).save
VCAP::CloudController::ServiceDashboardClient.new(
uaa_id: dashboard_client_attrs_1['id'],
service_broker: service_broker
).save
end
it 'deletes the client from the uaa' do
expect(client_manager).to receive(:modify_transaction) do |changeset|
delete_commands = changeset.select { |command| command.is_a? Commands::DeleteClientCommand }
expect(delete_commands.length).to eq 1
expect(delete_commands[0].client_id).to eq(unused_id)
end
manager.synchronize_clients_with_catalog(catalog)
end
it 'removes the claims for the deleted clients' do
manager.synchronize_clients_with_catalog(catalog)
expect(VCAP::CloudController::ServiceDashboardClient.find(uaa_id: unused_id)).to be_nil
end
it 'returns true' do
expect(manager.synchronize_clients_with_catalog(catalog)).to be true
end
it 'records a delete event for the deleted client' do
manager.synchronize_clients_with_catalog(catalog)
expect(VCAP::CloudController::Event.where(type: 'audit.service_dashboard_client.delete').count).to eq 1
event = VCAP::CloudController::Event.first(type: 'audit.service_dashboard_client.delete', actee_name: unused_id)
expect(event.actor_type).to eq('service_broker')
expect(event.actor).to eq(service_broker.guid)
expect(event.actor_name).to eq(service_broker.name)
expect(event.timestamp).to be
expect(event.actee).to eq(unused_id)
expect(event.actee_type).to eq('service_dashboard_client')
expect(event.actee_name).to eq(unused_id)
expect(event.space_guid).to eq('')
expect(event.organization_guid).to eq('')
expect(event.metadata).to be_empty
end
end
context 'when a different broker has already claimed the requested UAA client' do
let(:other_broker) { double(:other_broker, id: SecureRandom.uuid) }
let(:existing_client) do
double(:client,
uaa_id: dashboard_client_attrs_1['id'],
service_broker: other_broker)
end
before do
allow(VCAP::CloudController::ServiceDashboardClient).to receive(:find_client_by_uaa_id).with(
dashboard_client_attrs_1['id']
).and_return(existing_client)
allow(VCAP::CloudController::ServiceDashboardClient).to receive(:find_client_by_uaa_id).with(
dashboard_client_attrs_2['id']
).and_return(nil)
end
it 'populates errors on the manager' do
manager.synchronize_clients_with_catalog(catalog)
expect(manager.errors).not_to be_empty
end
it 'returns false' do
expect(manager.synchronize_clients_with_catalog(catalog)).to be(false)
end
end
end
describe 'exception handling' do
context 'when getting UAA clients raises an error' do
before do
error = VCAP::CloudController::UaaError.new('my test error')
expect(client_manager).to receive(:get_clients).and_raise(error)
end
it 'raises a ServiceBrokerDashboardClientFailure error' do
expect { manager.synchronize_clients_with_catalog(catalog) }.to(raise_error(CloudController::Errors::ApiError)) do |err|
expect(err.name).to eq('ServiceBrokerDashboardClientFailure')
expect(err.message).to match('my test error')
end
end
end
context 'when modifying UAA clients fails' do
let(:unused_id) { 'no-longer-used' }
before do
allow(client_manager).to receive(:get_clients).and_return([{ 'client_id' => unused_id }])
allow(client_manager).to receive(:modify_transaction).and_raise(VCAP::CloudController::UaaError.new('error message'))
VCAP::CloudController::ServiceDashboardClient.new(
uaa_id: unused_id,
service_broker: service_broker
).save
end
it 'does not add new claims' do
begin
manager.synchronize_clients_with_catalog(catalog)
rescue StandardError
nil
end
dashboard_client = VCAP::CloudController::ServiceDashboardClient.find(uaa_id: dashboard_client_attrs_1['id'])
expect(dashboard_client).to be_nil
end
it 'does not delete existing claims' do
begin
manager.synchronize_clients_with_catalog(catalog)
rescue StandardError
nil
end
dashboard_client = VCAP::CloudController::ServiceDashboardClient.find(uaa_id: unused_id)
expect(dashboard_client).not_to be_nil
end
it 'does not modify any of the claims' do
VCAP::CloudController::ServiceDashboardClient.new(
uaa_id: dashboard_client_attrs_2['id'],
service_broker: nil
).save
begin
manager.synchronize_clients_with_catalog(catalog)
rescue StandardError
nil
end
dashboard_client = VCAP::CloudController::ServiceDashboardClient.find(uaa_id: dashboard_client_attrs_2['id'])
expect(dashboard_client.service_broker).to be_nil
end
it 'raises a ServiceBrokerDashboardClientFailure error' do
expect { manager.synchronize_clients_with_catalog(catalog) }.to(raise_error(CloudController::Errors::ApiError)) do |err|
expect(err.name).to eq('ServiceBrokerDashboardClientFailure')
expect(err.message).to match('error message')
end
end
it 'does not record a create event' do
begin
manager.synchronize_clients_with_catalog(catalog)
rescue StandardError
nil
end
expect(VCAP::CloudController::Event.where(type: 'audit.service_dashboard_client.create').count).to eq 0
end
end
context 'when claiming the client for the broker fails' do
before do
allow(VCAP::CloudController::ServiceDashboardClient).to receive(:claim_client).and_raise
end
it 'does not modify the UAA client' do
begin
manager.synchronize_clients_with_catalog(catalog)
rescue StandardError
nil
end
expect(client_manager).not_to have_received(:modify_transaction)
end
it 'does not record a create event' do
begin
manager.synchronize_clients_with_catalog(catalog)
rescue StandardError
nil
end
expect(VCAP::CloudController::Event.where(type: 'audit.service_dashboard_client.create').count).to eq 0
end
end
end
context 'when the cloud controller is not configured to modify sso_client' do
before do
TestConfig.override(uaa_client_name: nil, uaa_client_secret: nil)
allow(client_manager).to receive(:modify_transaction)
end
it 'does not create/update/delete any clients' do
manager.synchronize_clients_with_catalog(catalog)
expect(client_manager).not_to have_received(:modify_transaction)
end
it 'returns true' do
expect(manager.synchronize_clients_with_catalog(catalog)).to be true
end
context 'and the catalog requested dashboard clients' do
it 'adds a warning' do
manager.synchronize_clients_with_catalog(catalog)
expect(manager.has_warnings?).to be true
expect(manager.warnings).to include(VCAP::Services::SSO::DashboardClientManager::REQUESTED_FEATURE_DISABLED_WARNING)
end
end
context 'and the catalog did not request dashboard clients' do
let(:catalog) { double(:catalog, services: [catalog_service_without_dashboard_client]) }
it 'does not add a warning' do
manager.synchronize_clients_with_catalog(catalog)
expect(manager.has_warnings?).to be false
end
end
end
end
describe '#remove_clients_for_broker' do
let(:client_to_delete_1) { 'client-to-delete-1' }
let(:client_to_delete_2) { 'client-to-delete-2' }
before do
allow(VCAP::Services::SSO::UAA::UaaClientManager).to receive(:new).and_return(client_manager)
allow(client_manager).to receive(:modify_transaction)
VCAP::CloudController::ServiceDashboardClient.new(
uaa_id: client_to_delete_1,
service_broker: service_broker
).save
VCAP::CloudController::ServiceDashboardClient.new(
uaa_id: client_to_delete_2,
service_broker: service_broker
).save
allow(client_manager).to receive(:get_clients).and_return(
[
{ 'client_id' => client_to_delete_1 },
{ 'client_id' => client_to_delete_2 }
]
)
end
it 'deletes all clients for the service broker in UAA' do
expect(client_manager).to receive(:modify_transaction) do |changeset|
delete_commands = changeset.select { |command| command.is_a? Commands::DeleteClientCommand }
expect(changeset.length).to eq(2)
expect(delete_commands.length).to eq(2)
expect(delete_commands[0].client_attrs['id']).to eq(client_to_delete_1)
expect(delete_commands[1].client_attrs['id']).to eq(client_to_delete_2)
end
manager.remove_clients_for_broker
end
it 'deletes the claims for the service broker in CC' do
expect(VCAP::CloudController::ServiceDashboardClient.find_claimed_client(service_broker).count).to eq(2)
manager.remove_clients_for_broker
expect(VCAP::CloudController::ServiceDashboardClient.find_claimed_client(service_broker).count).to eq(0)
end
it 'records a delete event for each dashboard client' do
manager.remove_clients_for_broker
expect(VCAP::CloudController::Event.where(type: 'audit.service_dashboard_client.delete').count).to eq 2
event = VCAP::CloudController::Event.first(type: 'audit.service_dashboard_client.delete', actee_name: client_to_delete_1)
expect(event.actor_type).to eq('service_broker')
expect(event.actor).to eq(service_broker.guid)
expect(event.actor_name).to eq(service_broker.name)
expect(event.timestamp).to be
expect(event.actee).to eq(client_to_delete_1)
expect(event.actee_type).to eq('service_dashboard_client')
expect(event.actee_name).to eq(client_to_delete_1)
expect(event.space_guid).to eq('')
expect(event.organization_guid).to eq('')
expect(event.metadata).to be_empty
end
context 'when deleting UAA clients fails' do
before do
error = VCAP::CloudController::UaaError.new('error message')
allow(client_manager).to receive(:modify_transaction).and_raise(error)
end
it 'raises a ServiceBrokerDashboardClientFailure error' do
expect { manager.remove_clients_for_broker }.to(raise_error(CloudController::Errors::ApiError)) do |err|
expect(err.name).to eq('ServiceBrokerDashboardClientFailure')
expect(err.message).to match('error message')
end
end
it 'does not delete any clients claimed in CC' do
expect(VCAP::CloudController::ServiceDashboardClient.find_claimed_client(service_broker).count).to eq(2)
begin
manager.remove_clients_for_broker
rescue StandardError
nil
end
expect(VCAP::CloudController::ServiceDashboardClient.find_claimed_client(service_broker).count).to eq(2)
end
it 'does not record any events' do
begin
manager.remove_clients_for_broker
rescue StandardError
nil
end
expect(VCAP::CloudController::Event.where(type: 'audit.service_dashboard_client.delete').count).to eq 0
end
end
context 'when getting UAA clients raises an error' do
before do
error = VCAP::CloudController::UaaError.new('my test error')
expect(client_manager).to receive(:get_clients).and_raise(error)
end
it 'raises a ServiceBrokerDashboardClientFailure error' do
expect { manager.remove_clients_for_broker }.to(raise_error(CloudController::Errors::ApiError)) do |err|
expect(err.name).to eq('ServiceBrokerDashboardClientFailure')
expect(err.message).to match('my test error')
end
end
end
context 'when removing CC claims raises an exception' do
before do
allow(VCAP::CloudController::ServiceDashboardClient).to receive(:release_client).and_raise('test error')
end
it 'reraises the error' do
expect { manager.remove_clients_for_broker }.to raise_error('test error')
end
it 'does not delete the UAA clients' do
begin
manager.remove_clients_for_broker
rescue StandardError
nil
end
expect(client_manager).not_to have_received(:modify_transaction)
end
it 'does not record any events' do
begin
manager.remove_clients_for_broker
rescue StandardError
nil
end
expect(VCAP::CloudController::Event.where(type: 'audit.service_dashboard_client.delete').count).to eq 0
end
end
context 'when the cloud controller is not configured to modify sso_client' do
before do
TestConfig.override(uaa_client_name: nil, uaa_client_secret: nil)
end
it 'does not delete any clients in UAA' do
manager.remove_clients_for_broker
expect(client_manager).not_to have_received(:modify_transaction)
end
it 'does not delete any clients claimed in CC' do
expect(VCAP::CloudController::ServiceDashboardClient.find_claimed_client(service_broker).count).to eq(2)
begin
manager.remove_clients_for_broker
rescue StandardError
nil
end
expect(VCAP::CloudController::ServiceDashboardClient.find_claimed_client(service_broker).count).to eq(2)
end
end
end
end
context 'for service instances' do
let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make }
let(:service_broker) { service_instance.service_plan.service.service_broker }
let(:dashboard_client) { VCAP::CloudController::ServiceInstanceDashboardClient }
let(:manager) { DashboardClientManager.new(service_instance, services_event_repository, dashboard_client) }
let(:client_id) { 'client-id-1' }
let(:client_info) do
{
'id' => client_id,
'secret' => 'secret-1',
'redirect_uri' => 'https://dashboard.service.com'
}
end
end
end
end