-
Notifications
You must be signed in to change notification settings - Fork 371
Prevent service instance sharing to space where service access disabled #1006
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,18 +3,9 @@ | |||||||||||||||||||||||||
| module VCAP::CloudController | ||||||||||||||||||||||||||
| class ServiceInstanceShare | ||||||||||||||||||||||||||
| def create(service_instance, target_spaces, user_audit_info) | ||||||||||||||||||||||||||
| supported_service_type?(service_instance) | ||||||||||||||||||||||||||
| service_instance_shareable?(service_instance) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if target_spaces.include?(service_instance.space) | ||||||||||||||||||||||||||
| raise CloudController::Errors::ApiError.new_from_details('InvalidServiceInstanceSharingTargetSpace') | ||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| target_spaces.each do |space| | ||||||||||||||||||||||||||
| if space.service_instances.map(&:name).include?(service_instance.name) | ||||||||||||||||||||||||||
| raise CloudController::Errors::ApiError.new_from_details('SharedServiceInstanceNameTaken', service_instance.name, space.name) | ||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||
| supported_service_type!(service_instance) | ||||||||||||||||||||||||||
| service_instance_shareable!(service_instance) | ||||||||||||||||||||||||||
| valid_target_spaces!(service_instance, target_spaces) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ServiceInstance.db.transaction do | ||||||||||||||||||||||||||
| target_spaces.each do |space| | ||||||||||||||||||||||||||
|
|
@@ -30,7 +21,37 @@ def create(service_instance, target_spaces, user_audit_info) | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| private | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def supported_service_type?(service_instance) | ||||||||||||||||||||||||||
| def valid_target_spaces!(service_instance, target_spaces) | ||||||||||||||||||||||||||
| no_sharing_to_self!(service_instance, target_spaces) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| target_spaces.each do |space| | ||||||||||||||||||||||||||
| plan_visibility!(service_instance, space) | ||||||||||||||||||||||||||
| name_uniqueness!(service_instance, space) | ||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def plan_visibility!(service_instance, space) | ||||||||||||||||||||||||||
| visible_plans = ServicePlan.organization_visible(space.organization) | ||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We investigated and learnt that these two methods are functionally the same here.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changes for this included in #1033 and can be viewed here: cloudfoundry-incubator@ff8276e#diff-e3c96f58c4686b95721452ad906b78a9R42 |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if !visible_plans.include?(service_instance.service_plan) | ||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Feels like we are violating encapsulation here: We are asking ServicePlan for all visible service plans and then checking a service plan if it is included in that list. Could we instead encapsulate this logic in ServicePlan (or another object responsible for service plan visibility) and instead do something like service_instance.service_plan.visible_in_space?(space)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed, we've fixed this!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changes for this included in #1033 and can be viewed here: cloudfoundry-incubator@ff8276e#diff-18a3dcea603c9a492aef637d3251c713R135 |
||||||||||||||||||||||||||
| error_msg = "Access to service #{service_instance.service.label} and plan #{service_instance.service_plan.name} is not enabled in #{space.organization.name}/#{space.name}" | ||||||||||||||||||||||||||
| raise CloudController::Errors::ApiError.new_from_details('UnprocessableEntity', error_msg) | ||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What distinguishes between errors that get custom error codes and ones that are just A pattern that we have been using for our actions is to raise a custom error class with a message and then wrap them all as unprocessable at the controller layer: cloud_controller_ng/app/actions/organization_create.rb Lines 24 to 33 in dc65583
cloud_controller_ng/app/controllers/v3/organizations_controller.rb Lines 47 to 48 in dc65583
Also, it looks like many of these errors are 400s. The api docs are a bit unclear, but in v3 we generally try to return 400s for syntactic errors and 422s for semantic errors (unfortunately, we are not always consistent with this.)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I now see this PR exists: #1014
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @Gerg. As this is a slightly bigger refactor, we will PR this separately. We created a story to track this: https://www.pivotaltracker.com/story/show/153639192 |
||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def name_uniqueness!(service_instance, space) | ||||||||||||||||||||||||||
| if space.service_instances.map(&:name).include?(service_instance.name) | ||||||||||||||||||||||||||
| raise CloudController::Errors::ApiError.new_from_details('SharedServiceInstanceNameTaken', service_instance.name, space.name) | ||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def no_sharing_to_self!(service_instance, spaces) | ||||||||||||||||||||||||||
| if spaces.include?(service_instance.space) | ||||||||||||||||||||||||||
| raise CloudController::Errors::ApiError.new_from_details('InvalidServiceInstanceSharingTargetSpace') | ||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def supported_service_type!(service_instance) | ||||||||||||||||||||||||||
| if service_instance.route_service? | ||||||||||||||||||||||||||
| raise CloudController::Errors::ApiError.new_from_details('RouteServiceInstanceSharingNotSupported') | ||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||
|
|
@@ -40,7 +61,7 @@ def supported_service_type?(service_instance) | |||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def service_instance_shareable?(service_instance) | ||||||||||||||||||||||||||
| def service_instance_shareable!(service_instance) | ||||||||||||||||||||||||||
| unless service_instance.shareable? | ||||||||||||||||||||||||||
| raise CloudController::Errors::ApiError.new_from_details('ServiceShareIsDisabled', service_instance.service.label) | ||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -147,6 +147,61 @@ module VCAP::CloudController | |
| end | ||
| end | ||
| end | ||
|
|
||
| context 'when the service plan is private' do | ||
| let(:service_plan) { ServicePlan.make(public: false) } | ||
| let(:service_instance) { ManagedServiceInstance.make(service_plan: service_plan) } | ||
|
|
||
| it 'raises an api error' do | ||
| error_msg = "Access to service #{service_instance.service.label} and plan #{service_instance.service_plan.name} is not " \ | ||
| "enabled in #{target_space1.organization.name}/#{target_space1.name}" | ||
| expect { | ||
| service_instance_share.create(service_instance, [target_space1], user_audit_info) | ||
| }.to raise_error(CloudController::Errors::ApiError, error_msg) | ||
| end | ||
|
|
||
| context 'and access has been enabled for the target org' do | ||
| before do | ||
| ServicePlanVisibility.make(organization: target_space1.organization, service_plan: service_instance.service_plan) | ||
| end | ||
|
|
||
| it 'creates the share' do | ||
| shared_instance = service_instance_share.create(service_instance, [target_space1], user_audit_info) | ||
| expect(shared_instance.shared_spaces.length).to eq 1 | ||
| end | ||
| end | ||
|
|
||
| context 'and when the source org has service plan access disabled but the target org has service plan access enabled' do | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How is this different than 'and access has been enabled for the target org'?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right, these two tests were doing the same thing. We have removed this test and added the missing "enabled in source + disabled in target" test.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changes for this included in #1033 and can be viewed here: cloudfoundry-incubator@ff8276e#diff-a1a7ea72386627dabb82df75ec2e88dfR175 |
||
| let(:source_org) { Organization.make } | ||
| let(:space) { Space.make(organization: source_org) } | ||
| let(:service_instance) { ManagedServiceInstance.make(service_plan: service_plan, space: space) } | ||
|
|
||
| before do | ||
| ServicePlanVisibility.make(organization: target_space1.organization, service_plan: service_instance.service_plan) | ||
| end | ||
|
|
||
| it 'creates the share' do | ||
| shared_instance = service_instance_share.create(service_instance, [target_space1], user_audit_info) | ||
| expect(shared_instance.shared_spaces.length).to eq 1 | ||
| end | ||
| end | ||
|
|
||
| context 'and when source org has had service plan access enabled and the target org has service plan access enabled' do | ||
| let(:source_org) { Organization.make } | ||
| let(:space) { Space.make(organization: source_org) } | ||
| let(:service_instance) { ManagedServiceInstance.make(service_plan: service_plan, space: space) } | ||
|
|
||
| before do | ||
| ServicePlanVisibility.make(organization: target_space1.organization, service_plan: service_instance.service_plan) | ||
| ServicePlanVisibility.make(organization: source_org, service_plan: service_instance.service_plan) | ||
| end | ||
|
|
||
| it 'creates the share' do | ||
| shared_instance = service_instance_share.create(service_instance, [target_space1], user_audit_info) | ||
| expect(shared_instance.shared_spaces.length).to eq 1 | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These method names are a bit confusing. They read like boolean methods, but then have the danger bang.
Maybe something like
or
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, we will rename it to
validate_supported_service_type!since we throw different errors based on what type of service it is, and it feels a bit cleaner to leave this check inside the method.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes for this included in #1033 and can be viewed here: cloudfoundry-incubator@ff8276e#diff-e3c96f58c4686b95721452ad906b78a9R6