Skip to content

Commit

Permalink
Add "create-service-key" to cloud controller, based on story #87057732
Browse files Browse the repository at this point in the history
Add the rest API controller and DB model for "create service key", this patch includes the following part:

1. 1 new rest controller for "create-service-key"
2. DB migration script for adding a new table to record service keys
3. 1 new DB model for service key
4. Changes the v2 service broker client to support create service key

This patch is submitted to implement story #87057732 in Service Key API
More test cases will be added in other commits

Signed-off-by: Tom Xing <xingzhou@cn.ibm.com>
  • Loading branch information
Kai Zhang authored and xingzhou committed Mar 25, 2015
1 parent 8aa19ea commit 80cc598
Show file tree
Hide file tree
Showing 12 changed files with 795 additions and 6 deletions.
13 changes: 13 additions & 0 deletions app/access/service_key_access.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module VCAP::CloudController
class ServiceKeyAccess < BaseAccess
def create?(service_key, params=nil)
return true if admin_user?
return false if service_key.in_suspended_org?
service_key.service_instance.space.developers.include?(context.user)
end

def delete?(service_key)
create?(service_key)
end
end
end
62 changes: 62 additions & 0 deletions app/controllers/services/lifecycle/service_key_manager.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
module VCAP::CloudController
class ServiceKeyManager
class ServiceInstanceNotFound < StandardError; end
class ServiceInstanceNotBindable < StandardError; end

def initialize(services_event_repository, access_validator, logger)
@services_event_repository = services_event_repository
@access_validator = access_validator
@logger = logger
end

def create_service_key(request_attrs)
service_instance = ServiceInstance.first(guid: request_attrs['service_instance_guid'])
raise ServiceInstanceNotFound unless service_instance
raise ServiceInstanceNotBindable unless service_instance.bindable?

service_key = ServiceKey.new(request_attrs)
@access_validator.validate_access(:create, service_key)
raise Sequel::ValidationFailed.new(service_key) unless service_key.valid?

lock_service_instance_by_blocking(service_instance) do
attributes_to_update = service_key.client.bind(service_key)
begin
service_key.set_all(attributes_to_update)
service_key.save
rescue
safe_unbind_instance(service_key)
raise
end
end

service_key
end

private

def safe_unbind_instance(service_key)
service_key.client.unbind(service_key)
rescue => e
@logger.error "Unable to unbind #{service_key}: #{e}"
end

def lock_service_instance_by_blocking(service_instance, &block)
return block.call unless service_instance.managed_instance?

original_attributes = service_instance.last_operation.try(:to_hash)
begin
service_instance.lock_by_failing_other_operations('update') do
block.call
end
ensure
if original_attributes
service_instance.last_operation.set_all(original_attributes)
service_instance.last_operation.save
else
service_instance.service_instance_operation.destroy
service_instance.save
end
end
end
end
end
42 changes: 42 additions & 0 deletions app/controllers/services/service_keys_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
require 'services/api'

module VCAP::CloudController
class ServiceKeysController < RestController::ModelController
define_attributes do
to_one :service_instance
attribute :name, String
end

post path, :create
def create
@request_attrs = self.class::CreateMessage.decode(body).extract(stringify_keys: true)
logger.debug 'cc.create', model: self.class.model_class_name, attributes: request_attrs
raise InvalidRequest unless request_attrs
service_key_manager = ServiceKeyManager.new(@services_event_repository, self, logger)
service_key = service_key_manager.create_service_key(@request_attrs)
[HTTP::CREATED,
{ 'Location' => "#{self.class.path}/#{service_key.guid}" },
object_renderer.render_json(self.class, service_key, @opts)
]
rescue ServiceKeyManager::ServiceInstanceNotFound
raise VCAP::Errors::ApiError.new_from_details('ServiceInstanceNotFound', @request_attrs['service_instance_guid'])
rescue ServiceKeyManager::ServiceInstanceNotBindable
raise VCAP::Errors::ApiError.new_from_details('UnbindableService')
end

private

def self.translate_validation_exception(e, attributes)
unique_errors = e.errors.on([:name, :service_instance_id])
if unique_errors && unique_errors.include?(:unique)
Errors::ApiError.new_from_details('ServiceKeyTaken', "#{attributes['name']} #{attributes['service_instance_guid']}")
elsif e.errors.on(:service_instance) && e.errors.on(:service_instance).include?(:presence)
Errors::ApiError.new_from_details('ServiceInstanceNotFound', attributes['service_instance_guid'])
else
Errors::ApiError.new_from_details('ServiceKeyInvalid', e.errors.full_messages)
end
end

define_messages
end
end
1 change: 1 addition & 0 deletions app/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
require 'models/services/service_create_event'
require 'models/services/service_delete_event'
require 'models/services/service_usage_event'
require 'models/services/service_key'

require 'models/job'

Expand Down
93 changes: 93 additions & 0 deletions app/models/services/service_key.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
module VCAP::CloudController
class ServiceKey < Sequel::Model
class InvalidAppAndServiceRelation < StandardError; end

many_to_one :service_instance

export_attributes :name, :service_instance_guid, :credentials, :syslog_drain_url

import_attributes :name, :service_instance_guid, :credentials, :syslog_drain_url

delegate :client, :service, :service_plan, to: :service_instance

plugin :after_initialize

encrypt :credentials, salt: :salt

def to_hash(opts={})
if !VCAP::CloudController::SecurityContext.admin? && !service_instance.space.developers.include?(VCAP::CloudController::SecurityContext.current_user)
opts.merge!({ redact: ['credentials'] })
end
super(opts)
end

def in_suspended_org?
space.in_suspended_org?
end

def space
service_instance.space
end

def validate
validates_presence :name
validates_presence :service_instance
validates_unique [:name, :service_instance_id]
validate_logging_service_binding if service_instance.respond_to?(:service_plan)
end

def validate_logging_service_binding
return if syslog_drain_url.blank?
service_advertised_as_logging_service = service_instance.service_plan.service.requires.include?('syslog_drain')
raise VCAP::Errors::ApiError.new_from_details('InvalidLoggingServiceBinding') unless service_advertised_as_logging_service
end

def credentials_with_serialization=(val)
self.credentials_without_serialization = MultiJson.dump(val)
end
alias_method_chain :credentials=, 'serialization'

def credentials_with_serialization
string = credentials_without_serialization
return if string.blank?
MultiJson.load string
end
alias_method_chain :credentials, 'serialization'

def create!
client.bind(self)
begin
save
rescue => e
safe_unbind
raise e
end
end

def self.user_visibility_filter(user)
{ service_instance: ServiceInstance.user_visible(user) }
end

def after_initialize
super
self.guid ||= SecureRandom.uuid
end

def before_destroy
client.unbind(self)
super
end

def logger
@logger ||= Steno.logger('cc.models.service_key')
end

private

def safe_unbind
client.unbind(self)
rescue => unbind_e
logger.error "Unable to unbind #{self}: #{unbind_e}"
end
end
end
14 changes: 14 additions & 0 deletions db/migrations/20150316184259_create_service_key_table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Sequel.migration do
change do
create_table(:service_keys) do
VCAP::Migration.common(self, :sk)
String :name, null: false
String :salt
String :syslog_drain_url
String :credentials, null: false, size: 2048
Integer :service_instance_id, null: false
foreign_key [:service_instance_id], :service_instances, name: :fk_svc_key_svc_instance_id
index [:name, :service_instance_id], unique: true, name: :svc_key_name_instance_id_index
end
end
end
14 changes: 9 additions & 5 deletions lib/services/service_brokers/v2/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,15 @@ def fetch_service_instance_state(instance)

def bind(binding)
path = service_binding_resource_path(binding)
response = @http_client.put(path, {
service_id: binding.service.broker_provided_id,
plan_id: binding.service_plan.broker_provided_id,
app_guid: binding.app_guid
})
attr = {
service_id: binding.service.broker_provided_id,
plan_id: binding.service_plan.broker_provided_id
}
if binding.respond_to? 'app_guid'
attr[:app_guid] = binding.app_guid
end

response = @http_client.put(path, attr)
parsed_response = @response_parser.parse(:put, path, response)

attributes = {
Expand Down
7 changes: 7 additions & 0 deletions spec/support/fakes/blueprints.rb
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,13 @@ module VCAP::CloudController
syslog_drain_url { nil }
end

ServiceKey.blueprint do
credentials { Sham.service_credentials }
service_instance { ManagedServiceInstance.make }
name { Sham.name }
syslog_drain_url { nil }
end

ServiceBroker.blueprint do
name { Sham.name }
broker_url { Sham.url }
Expand Down
120 changes: 120 additions & 0 deletions spec/unit/access/service_key_access_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
require 'spec_helper'

module VCAP::CloudController
describe ServiceKeyAccess, type: :access do
subject(:access) { ServiceKeyAccess.new(Security::AccessContext.new) }
let(:token) { { 'scope' => ['cloud_controller.read', 'cloud_controller.write'] } }

let(:user) { VCAP::CloudController::User.make }
let(:service) { VCAP::CloudController::Service.make }
let(:org) { VCAP::CloudController::Organization.make }
let(:space) { VCAP::CloudController::Space.make(organization: org) }
let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space: space) }

let(:object) { VCAP::CloudController::ServiceKey.make(name: 'fake-key', service_instance: service_instance) }

before do
SecurityContext.set(user, token)
end

after do
SecurityContext.clear
end

it_should_behave_like :admin_full_access

context 'for a logged in user (defensive)' do
it_behaves_like :no_access
end

context 'a user that isnt logged in (defensive)' do
let(:user) { nil }
it_behaves_like :no_access
end

context 'organization manager (defensive)' do
before { org.add_manager(user) }
it_behaves_like :no_access
end

context 'organization billing manager (defensive)' do
before { org.add_billing_manager(user) }
it_behaves_like :no_access
end

context 'organization auditor (defensive)' do
before { org.add_auditor(user) }
it_behaves_like :no_access
end

context 'organization user (defensive)' do
before { org.add_user(user) }
it_behaves_like :no_access
end

context 'space auditor' do
before do
org.add_user(user)
space.add_auditor(user)
end

it_behaves_like :read_only
end

context 'space manager (defensive)' do
before do
org.add_user(user)
space.add_manager(user)
end

it_behaves_like :no_access
end

context 'space developer' do
before do
org.add_user(user)
space.add_developer(user)
end

it { is_expected.to allow_op_on_object :create, object }
it { is_expected.to allow_op_on_object :read, object }
it { is_expected.not_to allow_op_on_object :read_for_update, object }
it { is_expected.to allow_op_on_object :delete, object }

context 'when the organization is suspended' do
before { allow(object).to receive(:in_suspended_org?).and_return(true) }
it_behaves_like :read_only
end
end

context 'any user using client without cloud_controller.write' do
let(:token) { { 'scope' => ['cloud_controller.read'] } }
before do
org.add_user(user)
org.add_manager(user)
org.add_billing_manager(user)
org.add_auditor(user)
space.add_manager(user)
space.add_developer(user)
space.add_auditor(user)
end

it_behaves_like :read_only
end

context 'any user using client without cloud_controller.read' do
let(:token) { { 'scope' => [] } }
before do
org.add_user(user)
org.add_manager(user)
org.add_billing_manager(user)
org.add_auditor(user)
space.add_manager(user)
space.add_developer(user)
space.add_auditor(user)
end

it_behaves_like :no_access
end
end
end
Loading

0 comments on commit 80cc598

Please sign in to comment.