Skip to content
Merged
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
14 changes: 14 additions & 0 deletions app/controllers/services/service_instances_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def update(guid)
raise CloudController::Errors::ApiError.new_from_details('UserProvidedServiceInstanceHandlerNeeded')
end

validate_shared_space_updateable(service_instance)
validate_access(:read_for_update, service_instance)
validate_access(:update, projected_service_instance(service_instance))

Expand Down Expand Up @@ -159,6 +160,7 @@ def delete(guid)
return [HTTP::NO_CONTENT, nil]
end

validate_shared_space_deleteable(service_instance)
validate_access(:delete, service_instance)

unless recursive_delete?
Expand Down Expand Up @@ -449,6 +451,18 @@ def validate_name_update(service_instance)
end
end

def validate_shared_space_updateable(service_instance)
if @access_context.can?(:read, service_instance) && @access_context.cannot?(:read, service_instance.space)
raise CloudController::Errors::ApiError.new_from_details('SharedServiceInstanceNotUpdateableInTargetSpace')
end
end

def validate_shared_space_deleteable(service_instance)
if @access_context.can?(:read, service_instance) && @access_context.cannot?(:read, service_instance.space)
raise CloudController::Errors::ApiError.new_from_details('SharedServiceInstanceNotDeleteableInTargetSpace')
end
end

def invalid_service_instance!(service_instance)
raise Sequel::ValidationFailed.new(service_instance)
end
Expand Down
41 changes: 26 additions & 15 deletions app/controllers/v3/service_instances_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ def share_service_instance
unprocessable!(message.errors.full_messages) unless message.valid?

spaces = Space.where(guid: message.guids)
check_spaces_exist_and_are_readable!(message.guids, spaces)
check_spaces_are_writeable!(spaces)
check_spaces_exist_and_are_writeable!(service_instance, message.guids, spaces)

share = ServiceInstanceShare.new
share.create(service_instance, spaces, user_audit_info)
Expand Down Expand Up @@ -76,26 +75,38 @@ def relationships_shared_spaces

private

def check_spaces_are_writeable!(spaces)
unwriteable_spaces = spaces.reject do |space|
can_write?(space.guid)
end
def check_spaces_exist_and_are_writeable!(service_instance, request_guids, found_spaces)
unreadable_spaces = found_spaces.reject { |s| can_read_space?(s) }
unwriteable_spaces = found_spaces.reject { |s| can_write_space?(s) || unreadable_spaces.include?(s) }

not_found_space_guids = request_guids - found_spaces.map(&:guid)
unreadable_space_guids = not_found_space_guids + unreadable_spaces.map(&:guid)
unwriteable_space_guids = unwriteable_spaces.map(&:guid)

if unreadable_space_guids.any? || unwriteable_space_guids.any?
unreadable_error = unreadable_error_message(service_instance.name, unreadable_space_guids)
unwriteable_error = unwriteable_error_message(service_instance.name, unwriteable_space_guids)

unauthorized! unless unwriteable_spaces.empty?
error_msg = [unreadable_error, unwriteable_error].map(&:presence).compact.join("\n")

unprocessable!(error_msg)
end
end

def check_spaces_exist_and_are_readable!(request_guids, found_spaces)
missing_guids = request_guids - found_spaces.map(&:guid)
def unreadable_error_message(service_instance_name, unreadable_space_guids)
if unreadable_space_guids.any?
unreadable_guid_list = unreadable_space_guids.map { |g| "'#{g}'" }.join(', ')

unreadable_spaces = found_spaces.reject do |space|
can_read_space?(space)
"Unable to share service instance #{service_instance_name} with spaces [#{unreadable_guid_list}]. Ensure the spaces exist and that you have access to them."
end
end

missing_guids += unreadable_spaces.map(&:guid)
def unwriteable_error_message(service_instance_name, unwriteable_space_guids)
if unwriteable_space_guids.any?
unwriteable_guid_list = unwriteable_space_guids.map { |s| "'#{s}'" }.join(', ')

unless missing_guids.empty?
guid_list = missing_guids.map { |g| "'#{g}'" }.join(', ')
unprocessable!("Unable to share to spaces [#{guid_list}] for the service instance. Ensure the spaces exist and you have access to them.")
"Unable to share service instance #{service_instance_name} with spaces [#{unwriteable_guid_list}]. "\
'Write permission is required in order to share a service instance with a space.'
end
end

Expand Down
8 changes: 8 additions & 0 deletions spec/unit/access/service_instance_access_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ module VCAP::CloudController
it 'returns false for purge' do
expect(subject).not_to allow_op_on_object(:purge, service_instance)
end

it 'does not allow the user to update the service' do
expect(subject).not_to allow_op_on_object(:update, service_instance)
end
end

context 'when the space of the service instance is not visible' do
Expand All @@ -203,6 +207,10 @@ module VCAP::CloudController
it 'returns false for purge' do
expect(subject).not_to allow_op_on_object(:purge, service_instance)
end

it 'does not allow the user to update the service' do
expect(subject).not_to allow_op_on_object(:update, service_instance)
end
end
end

Expand Down
119 changes: 100 additions & 19 deletions spec/unit/controllers/services/service_instances_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1771,12 +1771,44 @@ def stub_delete_and_return(status, body)
set_current_user(target_space_developer)
end

it 'should give the user an error' do
put "/v2/service_instances/#{service_instance.guid}", body

expect(last_response).to have_status_code 403
expect(last_response.body).to include 'SharedServiceInstanceNotUpdateableInTargetSpace'
expect(last_response.body).to include 'You cannot update service instances that have been shared with you'
end
end

context 'and an auditor in the target space tries to update the instance' do
let(:target_space_auditor) { make_auditor_for_space(target_space) }

before do
set_current_user(target_space_auditor)
end

it 'should give the user an error' do
put "/v2/service_instances/#{service_instance.guid}", body

expect(last_response).to have_status_code 403
expect(last_response.body).to include 'SharedServiceInstanceNotUpdateableInTargetSpace'
expect(last_response.body).to include 'You cannot update service instances that have been shared with you'
end
end

context 'and a developer in the target space and an auditor in the source space tries to update the instance' do
let(:target_developer_source_auditor) { make_developer_for_space(target_space) }

before do
set_current_user(target_developer_source_auditor)
set_current_user_as_role(user: target_developer_source_auditor, role: 'space_auditor', org: space.organization, space: space)
end

it 'should give the user an error' do
put "/v2/service_instances/#{service_instance.guid}", body

expect(last_response).to have_status_code 403
expect(last_response.body).to include 'CF-NotAuthorized'
expect(last_response.body).to include 'You are not authorized to perform the requested action'
end
end

Expand Down Expand Up @@ -1901,17 +1933,35 @@ def stub_delete_and_return(status, body)
end
end

context 'when the user has read but not write permissions' do
let(:auditor) { User.make }
context 'when the user has no read permissions to the space' do
let(:org_auditor) { User.make }

before do
service_instance.space.organization.add_auditor(auditor)
set_current_user(auditor)
service_instance.space.organization.add_auditor(org_auditor)
set_current_user(org_auditor)
end

it 'does not call out to the service broker' do
it 'does not call out to the service broker and returns an authorization error' do
put "/v2/service_instances/#{service_instance.guid}", body
expect(last_response).to have_status_code 403
expect(decoded_response['error_code']).to eq 'CF-NotAuthorized'
expect(a_request(:patch, service_broker_url)).to have_been_made.times(0)
end
end

context 'when the user has read but not write permissions to the space' do
let(:space_auditor) { User.make }

before do
service_instance.space.organization.add_user(space_auditor)
service_instance.space.add_auditor(space_auditor)
set_current_user(space_auditor)
end

it 'does not call out to the service broker and returns an authorization error' do
put "/v2/service_instances/#{service_instance.guid}", body
expect(last_response).to have_status_code 403
expect(decoded_response['error_code']).to eq 'CF-NotAuthorized'
expect(a_request(:patch, service_broker_url)).to have_been_made.times(0)
end
end
Expand Down Expand Up @@ -2581,10 +2631,11 @@ def stub_delete_and_return(status, body)

context 'when the service instance has been shared' do
let(:originating_space) { Space.make }
let(:shared_to_space) { Space.make }
let!(:service_instance) { ManagedServiceInstance.make(space: originating_space) }

before do
service_instance.add_shared_space(space)
service_instance.add_shared_space(shared_to_space)
end

context 'as a SpaceDeveloper in source and target space' do
Expand All @@ -2609,7 +2660,7 @@ def stub_delete_and_return(status, body)
context 'and there are bindings to the shared instance' do
before do
ServiceBinding.make(
app: AppModel.make(space: space),
app: AppModel.make(space: shared_to_space),
service_instance: service_instance
)
end
Expand Down Expand Up @@ -2638,21 +2689,51 @@ def stub_delete_and_return(status, body)
end
end

context 'as a SpaceAuditor in the source space' do
let(:source_space_auditor) { make_auditor_for_space(originating_space) }

before do
service_instance.add_shared_space(originating_space)
set_current_user(source_space_auditor)
end

it 'should give the user an error' do
delete "/v2/service_instances/#{service_instance.guid}"

expect(last_response).to have_status_code 403
expect(last_response.body).to include 'CF-NotAuthorized'
end
end

context 'as a SpaceDeveloper in target space' do
let(:target_space) { Space.make }
let(:target_space_dev) { make_developer_for_space(target_space) }
let(:target_space_dev) { make_developer_for_space(shared_to_space) }

before do
service_instance.add_shared_space(target_space)
set_current_user(target_space_dev)
end

it 'should give the user an error' do
delete "/v2/service_instances/#{service_instance.guid}"

expect(last_response).to have_status_code 403
expect(last_response.body).to include 'CF-NotAuthorized'
expect(last_response.body).to include 'You are not authorized to perform the requested action'
expect(last_response.body).to include 'SharedServiceInstanceNotDeleteableInTargetSpace'
expect(last_response.body).to include 'You cannot delete service instances that have been shared with you'
end
end

context 'as a SpaceAuditor in the target space' do
let(:target_space_auditor) { make_auditor_for_space(shared_to_space) }

before do
set_current_user(target_space_auditor)
end

it 'should give the user an error' do
delete "/v2/service_instances/#{service_instance.guid}"

expect(last_response).to have_status_code 403
expect(last_response.body).to include 'SharedServiceInstanceNotDeleteableInTargetSpace'
expect(last_response.body).to include 'You cannot delete service instances that have been shared with you'
end
end
end
Expand Down Expand Up @@ -4022,11 +4103,11 @@ def verify_forbidden(user)

describe 'permissions' do
let(:user) { User.make }
let(:other_org) { Organization.make }
let(:other_space) { Space.make(organization: other_org) }
let(:target_org) { Organization.make }
let(:target_space) { Space.make(organization: target_org) }

before do
instance.add_shared_space(other_space)
instance.add_shared_space(target_space)
end

context 'when the user is a member of the org/space this instance exists in' do
Expand All @@ -4053,7 +4134,7 @@ def verify_forbidden(user)

it "has a #{expected_status} http status code" do
get "/v2/service_instances/#{instance.guid}/shared_to"
expect(last_response.status).to eq(expected_status), "Expected #{expected_status}, got: #{last_response.status}, role: #{role}, response: #{last_response.body}"
expect(last_response.status).to eq(expected_status), "Expected #{expected_status}, got: #{last_response.status}, role: #{role}"
end
end
end
Expand All @@ -4072,8 +4153,8 @@ def verify_forbidden(user)
before do
set_current_user_as_role(
role: role,
org: other_org,
space: other_space,
org: target_org,
space: target_space,
user: user,
)
end
Expand Down
Loading