diff --git a/app/access/service_instance_access.rb b/app/access/service_instance_access.rb index cd8ab240003..dce57e8faa5 100644 --- a/app/access/service_instance_access.rb +++ b/app/access/service_instance_access.rb @@ -4,13 +4,13 @@ def create?(service_instance, params=nil) return true if admin_user? FeatureFlag.raise_unless_enabled!(:service_instance_creation) return false if service_instance.in_suspended_org? - service_instance.space.has_developer?(context.user) && allowed?(service_instance) + service_instance.space&.has_developer?(context.user) && allowed?(service_instance) end def read_for_update?(service_instance, params=nil) return true if admin_user? return false if service_instance.in_suspended_org? - service_instance.space.has_developer?(context.user) + service_instance.space&.has_developer?(context.user) end def update?(service_instance, params=nil) @@ -20,12 +20,12 @@ def update?(service_instance, params=nil) def delete?(service_instance) return true if admin_user? return false if service_instance.in_suspended_org? - service_instance.space.has_developer?(context.user) + service_instance.space&.has_developer?(context.user) end def manage_permissions?(service_instance) return true if admin_user? - service_instance.space.has_developer?(context.user) + service_instance.space&.has_developer?(context.user) end def manage_permissions_with_token?(service_instance) @@ -33,8 +33,7 @@ def manage_permissions_with_token?(service_instance) end def read_permissions?(service_instance) - return true if admin_user? || admin_read_only_user? - service_instance.space.has_member?(context.user) || service_instance.space.organization.managers.include?(context.user) + admin_user? || admin_read_only_user? || object_is_visible_to_user?(service_instance, context.user) end def read_permissions_with_token?(service_instance) @@ -43,7 +42,7 @@ def read_permissions_with_token?(service_instance) def read_env?(service_instance) return true if admin_user? || admin_read_only_user? - service_instance.space.has_developer?(context.user) + service_instance.space&.has_developer?(context.user) end def read_env_with_token?(service_instance) @@ -64,7 +63,7 @@ def allowed?(service_instance) end def purge?(service_instance) - admin_user? || (service_instance.space.has_developer?(context.user) && service_instance.service_broker.private?) + admin_user? || (service_instance.space&.has_developer?(context.user) && service_instance.service_broker.private?) end def purge_with_token?(instance) diff --git a/app/controllers/runtime/spaces_controller.rb b/app/controllers/runtime/spaces_controller.rb index 7132a96abb6..d7e8780036c 100644 --- a/app/controllers/runtime/spaces_controller.rb +++ b/app/controllers/runtime/spaces_controller.rb @@ -138,20 +138,18 @@ def enumerate_services(guid) def enumerate_service_instances(guid) space = find_guid_and_validate_access(:read, guid) - if params['return_user_provided_service_instances'] == 'true' - model_class = ServiceInstance - relation_name = :service_instances - else - model_class = ManagedServiceInstance - relation_name = :managed_service_instances - end + model_class = params['return_user_provided_service_instances'] == 'true' ? ServiceInstance : ManagedServiceInstance service_instances = Query.filtered_dataset_from_query_params( model_class, - space.user_visible_relationship_dataset(relation_name, @access_context.user, @access_context.admin_override), + model_class.user_visible(@access_context.user, @access_context.admin_override), ServiceInstancesController.query_parameters, @opts) - service_instances.filter(space: space) + + service_instances = service_instances.filter(Sequel.or([ + [:space, space], + [:shared_spaces, space] + ])) collection_renderer.render_json( ServiceInstancesController, diff --git a/app/controllers/services/service_instances_controller.rb b/app/controllers/services/service_instances_controller.rb index 44cc9ebf2ea..40786a6e6fe 100644 --- a/app/controllers/services/service_instances_controller.rb +++ b/app/controllers/services/service_instances_controller.rb @@ -5,6 +5,9 @@ require 'controllers/services/lifecycle/service_instance_deprovisioner' require 'controllers/services/lifecycle/service_instance_purger' require 'fetchers/service_instance_fetcher' +require 'fetchers/service_binding_list_fetcher' +require 'presenters/v2/service_instance_shared_to_presenter' +require 'presenters/v2/service_instance_shared_from_presenter' module VCAP::CloudController class ServiceInstancesController < RestController::ModelController @@ -205,6 +208,46 @@ def permissions(guid) end end + get '/v2/service_instances/:guid/shared_from', :shared_from_information + def shared_from_information(guid) + service_instance = find_guid_and_validate_access(:read, guid, ManagedServiceInstance) + + if service_instance.shared? + [HTTP::OK, {}, JSON.generate(CloudController::Presenters::V2::ServiceInstanceSharedFromPresenter.new.to_hash(service_instance.space))] + else + [HTTP::NO_CONTENT, {}, ''] + end + rescue CloudController::Errors::ApiError => e + if e.name == 'NotAuthorized' + HTTP::NOT_FOUND + else + raise e + end + end + + get '/v2/service_instances/:guid/shared_to', :enumerate_shared_to_information + def enumerate_shared_to_information(guid) + service_instance = find_guid_and_validate_access(:read, guid, ManagedServiceInstance) + validate_access(:read, service_instance.space) + + associated_controller = VCAP::CloudController::SpacesController + associated_path = "#{self.class.url_for_guid(guid)}/shared_to" + + create_paginated_collection_renderer(service_instance).render_json( + associated_controller, + service_instance.shared_spaces_dataset, + associated_path, + @opts, + {}, + ) + rescue CloudController::Errors::ApiError => e + if e.name == 'NotAuthorized' + HTTP::NOT_FOUND + else + raise e + end + end + def self.url_for_guid(guid) object = ServiceInstance.where(guid: guid).first @@ -303,6 +346,28 @@ def unbind_route(route_guid, instance_guid) private + class ServiceInstanceSharedToSerializer + def initialize(service_instance) + @service_instance = service_instance + end + + def serialize(controller, space, opts, orphans=nil) + bound_app_count = ServiceBindingListFetcher.fetch_service_instance_bindings_in_space(@service_instance.guid, space.guid).count + CloudController::Presenters::V2::ServiceInstanceSharedToPresenter.new.to_hash(space, bound_app_count) + end + end + + def create_paginated_collection_renderer(service_instance) + VCAP::CloudController::RestController::PaginatedCollectionRenderer.new( + VCAP::CloudController::RestController::SecureEagerLoader.new, + ServiceInstanceSharedToSerializer.new(service_instance), + { + max_results_per_page: config.get(:renderer, :max_results_per_page), + default_results_per_page: config.get(:renderer, :default_results_per_page), + max_inline_relations_depth: config.get(:renderer, :max_inline_relations_depth), + }) + end + def route_services_enabled? @config.get(:route_services_enabled) end diff --git a/app/controllers/v3/service_instances_controller.rb b/app/controllers/v3/service_instances_controller.rb index 3f1141b7ae4..1aef6c27de5 100644 --- a/app/controllers/v3/service_instances_controller.rb +++ b/app/controllers/v3/service_instances_controller.rb @@ -1,11 +1,30 @@ require 'messages/to_many_relationship_message' +require 'messages/service_instances/service_instances_list_message' require 'presenters/v3/relationship_presenter' require 'presenters/v3/to_many_relationship_presenter' +require 'presenters/v3/paginated_list_presenter' require 'actions/service_instance_share' require 'actions/service_instance_unshare' +require 'fetchers/service_instance_list_fetcher' class ServiceInstancesV3Controller < ApplicationController + def index + message = ServiceInstancesListMessage.from_params(query_params) + invalid_param!(message.errors.full_messages) unless message.valid? + + dataset = if can_read_globally? + ServiceInstanceListFetcher.new.fetch_all(message: message) + else + ServiceInstanceListFetcher.new.fetch(message: message, space_guids: readable_space_guids) + end + + render status: :ok, json: Presenters::V3::PaginatedListPresenter.new( + dataset: dataset, + path: '/v3/service_instances', + message: message) + end + def share_service_instance FeatureFlag.raise_unless_enabled!(:service_instance_sharing) diff --git a/app/fetchers/service_binding_list_fetcher.rb b/app/fetchers/service_binding_list_fetcher.rb index 2939ccd5eac..fd49728ba31 100644 --- a/app/fetchers/service_binding_list_fetcher.rb +++ b/app/fetchers/service_binding_list_fetcher.rb @@ -16,6 +16,13 @@ def fetch_all filter(dataset) end + def self.fetch_service_instance_bindings_in_space(service_instance_guid, space_guid) + ServiceBinding.select_all(ServiceBinding.table_name). + join(:apps, guid: :app_guid). + where(apps__space_guid: space_guid). + where(service_bindings__service_instance_guid: service_instance_guid) + end + private def filter(dataset) diff --git a/app/fetchers/service_instance_list_fetcher.rb b/app/fetchers/service_instance_list_fetcher.rb new file mode 100644 index 00000000000..01212a3f3b1 --- /dev/null +++ b/app/fetchers/service_instance_list_fetcher.rb @@ -0,0 +1,30 @@ +module VCAP::CloudController + class ServiceInstanceListFetcher + def fetch(message:, space_guids:) + source_space_instance_dataset = ServiceInstance.select_all(ServiceInstance.table_name). + join(Space.table_name, id: :space_id, guid: space_guids) + + shared_instance_dataset = ServiceInstance.select_all(ServiceInstance.table_name). + join(:service_instance_shares, service_instance_guid: :guid, target_space_guid: space_guids) + + dataset = source_space_instance_dataset.union(shared_instance_dataset, alias: :service_instances) + + filter(dataset, message) + end + + def fetch_all(message:) + dataset = ServiceInstance.dataset + filter(dataset, message) + end + + private + + def filter(dataset, message) + if message.requested?(:names) + dataset = dataset.where(service_instances__name: message.names) + end + + dataset + end + end +end diff --git a/app/messages/service_instances/service_instances_list_message.rb b/app/messages/service_instances/service_instances_list_message.rb new file mode 100644 index 00000000000..f368e921fe4 --- /dev/null +++ b/app/messages/service_instances/service_instances_list_message.rb @@ -0,0 +1,32 @@ +require 'messages/list_message' + +module VCAP::CloudController + class ServiceInstancesListMessage < ListMessage + ALLOWED_KEYS = [:page, :per_page, :order_by, :names].freeze + + attr_accessor(*ALLOWED_KEYS) + + validates_with NoAdditionalParamsValidator + validates :names, array: true, allow_nil: true + + def initialize(params={}) + super(params.symbolize_keys) + end + + def self.from_params(params) + opts = params.dup + to_array! opts, 'names' + new(opts.symbolize_keys) + end + + def valid_order_by_values + super << :name + end + + private + + def allowed_keys + ALLOWED_KEYS + end + end +end diff --git a/app/models/services/service_instance.rb b/app/models/services/service_instance.rb index b2aea99e2af..02159e1d975 100644 --- a/app/models/services/service_instance.rb +++ b/app/models/services/service_instance.rb @@ -68,6 +68,10 @@ def self.user_visibility_filter(user) [:space, user.spaces_dataset], [:space, user.audited_spaces_dataset], [:space, user.managed_spaces_dataset], + [:shared_spaces, user.spaces_dataset], + [:shared_spaces, user.managed_spaces_dataset], + [:shared_spaces, user.audited_spaces_dataset], + [:shared_spaces, managed_organizations_spaces_dataset(user.managed_organizations_dataset)], ]) end @@ -134,7 +138,7 @@ def credentials_with_serialization alias_method_chain :credentials, 'serialization' def in_suspended_org? - space.in_suspended_org? + space&.in_suspended_org? end def after_create @@ -171,6 +175,10 @@ def volume_service? false end + def shared? + shared_spaces.any? + end + def self.managed_organizations_spaces_dataset(managed_organizations_dataset) VCAP::CloudController::Space.dataset.filter({ organization_id: managed_organizations_dataset.select(:organization_id) }) end diff --git a/app/presenters/v2/service_instance_presenter.rb b/app/presenters/v2/service_instance_presenter.rb index a020fdae8f2..8f688d6a781 100644 --- a/app/presenters/v2/service_instance_presenter.rb +++ b/app/presenters/v2/service_instance_presenter.rb @@ -28,6 +28,8 @@ def entity_hash(controller, obj, opts, depth, parents, orphans=nil) obj_hash['service_plan_guid'] = service_plan.guid obj_hash['service_guid'] = service_plan.service.guid rel_hash['service_url'] = "/v2/services/#{service_plan.service.guid}" + rel_hash['shared_from_url'] = "/v2/service_instances/#{obj.guid}/shared_from" + rel_hash['shared_to_url'] = "/v2/service_instances/#{obj.guid}/shared_to" end obj_hash.merge!(rel_hash) diff --git a/app/presenters/v2/service_instance_shared_from_presenter.rb b/app/presenters/v2/service_instance_shared_from_presenter.rb new file mode 100644 index 00000000000..405ac03c3bd --- /dev/null +++ b/app/presenters/v2/service_instance_shared_from_presenter.rb @@ -0,0 +1,14 @@ +module CloudController + module Presenters + module V2 + class ServiceInstanceSharedFromPresenter + def to_hash(space) + { + 'space_name' => space.name, + 'organization_name' => space.organization.name + } + end + end + end + end +end diff --git a/app/presenters/v2/service_instance_shared_to_presenter.rb b/app/presenters/v2/service_instance_shared_to_presenter.rb new file mode 100644 index 00000000000..0f3229579bf --- /dev/null +++ b/app/presenters/v2/service_instance_shared_to_presenter.rb @@ -0,0 +1,13 @@ +require 'presenters/v2/service_instance_shared_from_presenter' + +module CloudController + module Presenters + module V2 + class ServiceInstanceSharedToPresenter < ServiceInstanceSharedFromPresenter + def to_hash(space, bound_app_count) + super(space).merge({ 'bound_app_count' => bound_app_count }) + end + end + end + end +end diff --git a/app/presenters/v3/paginated_list_presenter.rb b/app/presenters/v3/paginated_list_presenter.rb index 48ad1f07a3f..238e18a7729 100644 --- a/app/presenters/v3/paginated_list_presenter.rb +++ b/app/presenters/v3/paginated_list_presenter.rb @@ -6,6 +6,7 @@ require 'presenters/v3/process_presenter' require 'presenters/v3/route_mapping_presenter' require 'presenters/v3/service_binding_presenter' +require 'presenters/v3/service_instance_presenter' require 'presenters/v3/task_presenter' require 'presenters/v3/organization_presenter' require 'presenters/v3/space_presenter' @@ -24,7 +25,8 @@ class PaginatedListPresenter 'PackageModel' => VCAP::CloudController::Presenters::V3::PackagePresenter, 'RouteMappingModel' => VCAP::CloudController::Presenters::V3::RouteMappingPresenter, 'ServiceBinding' => VCAP::CloudController::Presenters::V3::ServiceBindingPresenter, - 'TaskModel' => VCAP::CloudController::Presenters::V3::TaskPresenter, + 'ManagedServiceInstance' => VCAP::CloudController::Presenters::V3::ServiceInstancePresenter, + 'TaskModel' => VCAP::CloudController::Presenters::V3::TaskPresenter, }.freeze def initialize(dataset:, path:, message: nil, show_secrets: false) diff --git a/app/presenters/v3/service_instance_presenter.rb b/app/presenters/v3/service_instance_presenter.rb new file mode 100644 index 00000000000..537d1969e8a --- /dev/null +++ b/app/presenters/v3/service_instance_presenter.rb @@ -0,0 +1,24 @@ +require 'presenters/v3/base_presenter' + +module VCAP::CloudController + module Presenters + module V3 + class ServiceInstancePresenter < BasePresenter + def to_hash + { + guid: service_instance.guid, + created_at: service_instance.created_at, + updated_at: service_instance.updated_at, + name: service_instance.name + } + end + + private + + def service_instance + @resource + end + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 729606e0a8a..34661d0e55b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -119,6 +119,7 @@ get '/apps/:app_guid/tasks', to: 'tasks#index' # service_instances + get '/service_instances', to: 'service_instances_v3#index' post '/service_instances/:service_instance_guid/relationships/shared_spaces', to: 'service_instances_v3#share_service_instance' delete '/service_instances/:service_instance_guid/relationships/shared_spaces/:space_guid', to: 'service_instances_v3#unshare_service_instance' end diff --git a/docs/v2/index.html b/docs/v2/index.html index b2e77eb7adc..6896378863e 100644 --- a/docs/v2/index.html +++ b/docs/v2/index.html @@ -875,6 +875,12 @@

  • Retrieving permissions on a Service Instance
  • +
  • + Retrieve Information Regarding Where a Service Instance is Shared From (Experimental) +
  • +
  • + Retrieve Information Regarding Where a Service Instance is Shared To (Experimental) +
  • Unbinding a service instance from a route
  • diff --git a/docs/v2/service_instances/binding_a_service_instance_to_a_route.html b/docs/v2/service_instances/binding_a_service_instance_to_a_route.html index 9ef1f2357c0..1912be9e411 100644 --- a/docs/v2/service_instances/binding_a_service_instance_to_a_route.html +++ b/docs/v2/service_instances/binding_a_service_instance_to_a_route.html @@ -164,7 +164,9 @@

    Body

    "service_plan_url": "/v2/service_plans/4cfefd27-3eb7-4f55-806e-1db649038ad2", "service_bindings_url": "/v2/service_instances/6da8d173-b409-4094-949f-3c1cc8a68503/service_bindings", "service_keys_url": "/v2/service_instances/6da8d173-b409-4094-949f-3c1cc8a68503/service_keys", - "routes_url": "/v2/service_instances/6da8d173-b409-4094-949f-3c1cc8a68503/routes" + "routes_url": "/v2/service_instances/6da8d173-b409-4094-949f-3c1cc8a68503/routes", + "shared_from_url": "/v2/service_instances/6da8d173-b409-4094-949f-3c1cc8a68503/shared_from", + "shared_to_url": "/v2/service_instances/6da8d173-b409-4094-949f-3c1cc8a68503/shared_to" } } diff --git a/docs/v2/service_instances/creating_a_service_instance.html b/docs/v2/service_instances/creating_a_service_instance.html index ba70c81d34a..2e6c57b78a7 100644 --- a/docs/v2/service_instances/creating_a_service_instance.html +++ b/docs/v2/service_instances/creating_a_service_instance.html @@ -557,6 +557,38 @@

    Body

    + + + shared_from_url + + + Source information for a shared service instance. Users of a shared service instance can find out its space and org information. + + + + + + + + + + + shared_to_url + + + Information about where the service instance instance has been shared to. + + + + + + + + tags @@ -608,7 +640,9 @@

    Body

    "service_plan_url": "/v2/service_plans/fe173a83-df28-4891-8d91-46334e04600d", "service_bindings_url": "/v2/service_instances/cc3b67fa-cda6-4df7-ba47-eb5f2a123992/service_bindings", "service_keys_url": "/v2/service_instances/cc3b67fa-cda6-4df7-ba47-eb5f2a123992/service_keys", - "routes_url": "/v2/service_instances/cc3b67fa-cda6-4df7-ba47-eb5f2a123992/routes" + "routes_url": "/v2/service_instances/cc3b67fa-cda6-4df7-ba47-eb5f2a123992/routes", + "shared_from_url": "/v2/service_instances/0d632575-bb06-4ea5-bb19-a451a9644d92/shared_from", + "shared_to_url": "/v2/service_instances/0d632575-bb06-4ea5-bb19-a451a9644d92/shared_to" } } diff --git a/docs/v2/service_instances/delete_a_service_instance.html b/docs/v2/service_instances/delete_a_service_instance.html index d97550d3529..077ad3f0a72 100644 --- a/docs/v2/service_instances/delete_a_service_instance.html +++ b/docs/v2/service_instances/delete_a_service_instance.html @@ -461,6 +461,38 @@

    Body

    + + + shared_from_url + + + Source information for a shared service instance. Users of a shared service instance can find out its space and org information. + + + + + + + + + + + shared_to_url + + + Information about where the service instance instance has been shared to. + + + + + + + + tags @@ -512,7 +544,9 @@

    Body

    "service_plan_url": "/v2/service_plans/8ea19d29-2e20-469e-8b91-917a6410e2f2", "service_bindings_url": "/v2/service_instances/1aaeb02d-16c3-4405-bc41-80e83d196dff/service_bindings", "service_keys_url": "/v2/service_instances/1aaeb02d-16c3-4405-bc41-80e83d196dff/service_keys", - "routes_url": "/v2/service_instances/1aaeb02d-16c3-4405-bc41-80e83d196dff/routes" + "routes_url": "/v2/service_instances/1aaeb02d-16c3-4405-bc41-80e83d196dff/routes", + "shared_from_url": "/v2/service_instances/6da8d173-b409-4094-949f-3c1cc8a68503/shared_from", + "shared_to_url": "/v2/service_instances/6da8d173-b409-4094-949f-3c1cc8a68503/shared_to" } } diff --git a/docs/v2/service_instances/list_all_service_instances.html b/docs/v2/service_instances/list_all_service_instances.html index 9b24866534e..16be8a03de0 100644 --- a/docs/v2/service_instances/list_all_service_instances.html +++ b/docs/v2/service_instances/list_all_service_instances.html @@ -512,6 +512,38 @@

    Body

    + + + shared_from_url + + + Source information for a shared service instance. Users of a shared service instance can find out its space and org information. + + + + + + + + + + + shared_to_url + + + Information about where the service instance instance has been shared to. + + + + + + + + tags @@ -571,7 +603,9 @@

    Body

    "service_plan_url": "/v2/service_plans/05a372c6-6dc2-4f7f-8f65-a90ebe5fa6e2", "service_bindings_url": "/v2/service_instances/215b97be-ec77-4224-9c38-c4f2d86b56c1/service_bindings", "service_keys_url": "/v2/service_instances/215b97be-ec77-4224-9c38-c4f2d86b56c1/service_keys", - "routes_url": "/v2/service_instances/215b97be-ec77-4224-9c38-c4f2d86b56c1/routes" + "routes_url": "/v2/service_instances/215b97be-ec77-4224-9c38-c4f2d86b56c1/routes", + "shared_from_url": "/v2/service_instances/0d632575-bb06-4ea5-bb19-a451a9644d92/shared_from", + "shared_to_url": "/v2/service_instances/0d632575-bb06-4ea5-bb19-a451a9644d92/shared_to" } } ] diff --git a/docs/v2/service_instances/retrieve_a_particular_service_instance.html b/docs/v2/service_instances/retrieve_a_particular_service_instance.html index 3b81aee4e6a..22431727eed 100644 --- a/docs/v2/service_instances/retrieve_a_particular_service_instance.html +++ b/docs/v2/service_instances/retrieve_a_particular_service_instance.html @@ -368,6 +368,38 @@

    Body

    + + + shared_from_url + + + Source information for a shared service instance. Users of a shared service instance can find out its space and org information. + + + + + + + + + + + shared_to_url + + + Information about where the service instance instance has been shared to. + + + + + + + + tags @@ -421,7 +453,9 @@

    Body

    "service_plan_url": "/v2/service_plans/779d2df0-9cdd-48e8-9781-ea05301cedb1", "service_bindings_url": "/v2/service_instances/0d632575-bb06-4ea5-bb19-a451a9644d92/service_bindings", "service_keys_url": "/v2/service_instances/0d632575-bb06-4ea5-bb19-a451a9644d92/service_keys", - "routes_url": "/v2/service_instances/0d632575-bb06-4ea5-bb19-a451a9644d92/routes" + "routes_url": "/v2/service_instances/0d632575-bb06-4ea5-bb19-a451a9644d92/routes", + "shared_from_url": "/v2/service_instances/0d632575-bb06-4ea5-bb19-a451a9644d92/shared_from", + "shared_to_url": "/v2/service_instances/0d632575-bb06-4ea5-bb19-a451a9644d92/shared_to" } } diff --git a/docs/v2/service_instances/retrieve_info_where_service_instance_is_shared_from.html b/docs/v2/service_instances/retrieve_info_where_service_instance_is_shared_from.html new file mode 100644 index 00000000000..eadbbdcf0e2 --- /dev/null +++ b/docs/v2/service_instances/retrieve_info_where_service_instance_is_shared_from.html @@ -0,0 +1,152 @@ + + + + Service Instances API + + + + + + +
    +

    Service Instances API

    + +
    +

    Retrieve Information Regarding Where a Service Instance is Shared From (Experimental)

    +

    GET /v2/service_instances/:guid/shared_from

    + +

    Request

    +

    Route

    +
    GET /v2/service_instances/0d632575-bb06-4ea5-bb19-a451a9644d92/shared_from
    + +

    Headers

    +
    Authorization: bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidWFhLWlkLTE1MCIsImVtYWlsIjoiZW1haWwtMTA1QHNvbWVkb21haW4uY29tIiwic2NvcGUiOlsiY2xvdWRfY29udHJvbGxlci5hZG1pbiJdLCJhdWQiOlsiY2xvdWRfY29udHJvbGxlciJdLCJleHAiOjE0NjYwMDg4ODl9.eyvSz10WWSQt0pFotoQI2Lm5XdH22-50O_R8rxy7n4k
    +Host: example.org
    +Cookie: 
    + +

    cURL

    +
    curl "https://api.[your-domain.com]/v2/service_instances/0d632575-bb06-4ea5-bb19-a451a9644d92/shared_from" -X GET \
    +	-H "Authorization: bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidWFhLWlkLTE1MCIsImVtYWlsIjoiZW1haWwtMTA1QHNvbWVkb21haW4uY29tIiwic2NvcGUiOlsiY2xvdWRfY29udHJvbGxlci5hZG1pbiJdLCJhdWQiOlsiY2xvdWRfY29udHJvbGxlciJdLCJleHAiOjE0NjYwMDg4ODl9.eyvSz10WWSQt0pFotoQI2Lm5XdH22-50O_R8rxy7n4k" \
    +	-H "Host: example.org" \
    +	-H "Cookie: "
    + +

    Response

    + +

    Status

    +
    200 OK
    + +

    Body

    + + + + + + + + + + + + + + + + + + + + + + + +
    NameDescriptionValid ValuesExample Values
    + space_name + + The space name that this service instance belongs to. + +
      +
    +
    +
      +
    +
    + organization_name + + The organization name that this service instance belongs to. + +
      +
    +
    +
      +
    +
    + +
    {
    +    "space_name": "space-name",
    +    "organization_name": "org-name"
    +}
    +
    + +

    Headers

    +
    Content-Type: application/json;charset=utf-8
    +X-VCAP-Request-ID: febb14ab-a7bc-492d-b017-3db59d83967d
    +Content-Length: 253
    +X-Content-Type-Options: nosniff
    +
    +
    + + diff --git a/docs/v2/service_instances/retrieve_info_where_service_instance_is_shared_to.html b/docs/v2/service_instances/retrieve_info_where_service_instance_is_shared_to.html new file mode 100644 index 00000000000..4dfd7a1d103 --- /dev/null +++ b/docs/v2/service_instances/retrieve_info_where_service_instance_is_shared_to.html @@ -0,0 +1,244 @@ + + + + Service Instances API + + + + + + +
    +

    Service Instances API

    + +
    +

    Retrieve Information Regarding Where a Service Instance is Shared To (Experimental)

    +

    GET /v2/service_instances/:guid/shared_to

    + +

    Request

    +

    Route

    +
    GET /v2/service_instances/0d632575-bb06-4ea5-bb19-a451a9644d92/shared_to
    + +

    Parameters

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameDescriptionValid ValuesExample Values
    + page + + Page of results to fetch + +
      +
    +
    +
      +
    +
    + results-per-page + + Number of results per page + +
      +
    +
    +
      +
    +
    + order-direction + + Order of the results: asc (default) or desc + +
      +
    +
    +
      +
    +
    + +

    Headers

    +
    Authorization: bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidWFhLWlkLTE1MCIsImVtYWlsIjoiZW1haWwtMTA1QHNvbWVkb21haW4uY29tIiwic2NvcGUiOlsiY2xvdWRfY29udHJvbGxlci5hZG1pbiJdLCJhdWQiOlsiY2xvdWRfY29udHJvbGxlciJdLCJleHAiOjE0NjYwMDg4ODl9.eyvSz10WWSQt0pFotoQI2Lm5XdH22-50O_R8rxy7n4k
    +Host: example.org
    +Cookie: 
    + +

    cURL

    +
    curl "https://api.[your-domain.com]/v2/service_instances/0d632575-bb06-4ea5-bb19-a451a9644d92/shared_to" -X GET \
    +	-H "Authorization: bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidWFhLWlkLTE1MCIsImVtYWlsIjoiZW1haWwtMTA1QHNvbWVkb21haW4uY29tIiwic2NvcGUiOlsiY2xvdWRfY29udHJvbGxlci5hZG1pbiJdLCJhdWQiOlsiY2xvdWRfY29udHJvbGxlciJdLCJleHAiOjE0NjYwMDg4ODl9.eyvSz10WWSQt0pFotoQI2Lm5XdH22-50O_R8rxy7n4k" \
    +	-H "Host: example.org" \
    +	-H "Cookie: "
    + +

    Response

    + +

    Status

    +
    200 OK
    + +

    Body

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameDescriptionValid ValuesExample Values
    + space_name + + The name of the space being shared to. + +
      +
    +
    +
      +
    +
    + organization_name + + The name of the organization of the space the service instance has been shared to. + +
      +
    +
    +
      +
    +
    + bound_app_count + + The number of applications bound to the service instance in the space. + +
      +
    +
    +
      +
    +
    + +
    {
    +   "total_results": 2,
    +   "total_pages": 1,
    +   "prev_url": null,
    +   "next_url": null,
    +   "resources": [
    +      {
    +          "space_name": "target-space-1",
    +          "organization_name": "org-name",
    +          "bound_app_count": 3
    +      },
    +      {
    +          "space_name": "target-space-2",
    +          "organization_name": "org-name",
    +          "bound_app_count": 0
    +      }
    +   ]
    +}
    + +

    Headers

    +
    Content-Type: application/json;charset=utf-8
    +X-VCAP-Request-ID: febb14ab-a7bc-492d-b017-3db59d83967d
    +Content-Length: 378
    +X-Content-Type-Options: nosniff
    + +
    +
    + + diff --git a/docs/v2/service_instances/update_a_service_instance.html b/docs/v2/service_instances/update_a_service_instance.html index 8c5bee3b126..c1703cd1835 100644 --- a/docs/v2/service_instances/update_a_service_instance.html +++ b/docs/v2/service_instances/update_a_service_instance.html @@ -499,6 +499,38 @@

    Body

    + + + shared_from_url + + + Source information for a shared service instance. Users of a shared service instance can find out its space and org information. + + + + + + + + + + + shared_to_url + + + Information about where the service instance instance has been shared to. + + + + + + + + tags @@ -549,7 +581,9 @@

    Body

    "service_plan_url": "/v2/service_plans/4ec73bf4-9f3a-44c7-bbac-61ee9cb5a511", "service_bindings_url": "/v2/service_instances/a34f1423-4b84-4727-ab49-3f1522c4cb16/service_bindings", "service_keys_url": "/v2/service_instances/a34f1423-4b84-4727-ab49-3f1522c4cb16/service_keys", - "routes_url": "/v2/service_instances/a34f1423-4b84-4727-ab49-3f1522c4cb16/routes" + "routes_url": "/v2/service_instances/a34f1423-4b84-4727-ab49-3f1522c4cb16/routes", + "shared_from_url": "/v2/service_instances/0d632575-bb06-4ea5-bb19-a451a9644d92/shared_from", + "shared_to_url": "/v2/service_instances/0d632575-bb06-4ea5-bb19-a451a9644d92/shared_to" } } diff --git a/docs/v2/service_plans/list_all_service_instances_for_the_service_plan.html b/docs/v2/service_plans/list_all_service_instances_for_the_service_plan.html index 5a7aab99fdb..770bcaf779f 100644 --- a/docs/v2/service_plans/list_all_service_instances_for_the_service_plan.html +++ b/docs/v2/service_plans/list_all_service_instances_for_the_service_plan.html @@ -303,7 +303,9 @@

    Body

    "service_plan_url": "/v2/service_plans/85615ea0-9d23-4de8-aabd-89bffcce39d5", "service_bindings_url": "/v2/service_instances/0fac6687-69fd-4567-afb0-dd39503523ff/service_bindings", "service_keys_url": "/v2/service_instances/0fac6687-69fd-4567-afb0-dd39503523ff/service_keys", - "routes_url": "/v2/service_instances/0fac6687-69fd-4567-afb0-dd39503523ff/routes" + "routes_url": "/v2/service_instances/0fac6687-69fd-4567-afb0-dd39503523ff/routes", + "shared_from_url": "/v2/service_instances/9547e9ed-e460-4abe-bda3-7070b9835917/shared_from", + "shared_to_url": "/v2/service_instances/9547e9ed-e460-4abe-bda3-7070b9835917/shared_to" } } ] diff --git a/docs/v2/spaces/list_all_service_instances_for_the_space.html b/docs/v2/spaces/list_all_service_instances_for_the_space.html index 19145598b74..e3cd4cae7f9 100644 --- a/docs/v2/spaces/list_all_service_instances_for_the_space.html +++ b/docs/v2/spaces/list_all_service_instances_for_the_space.html @@ -302,7 +302,9 @@

    Body

    "service_plan_url": "/v2/service_plans/fcf57f7f-3c51-49b2-b252-dc24e0f7dcab", "service_bindings_url": "/v2/service_instances/9547e9ed-e460-4abe-bda3-7070b9835917/service_bindings", "service_keys_url": "/v2/service_instances/9547e9ed-e460-4abe-bda3-7070b9835917/service_keys", - "routes_url": "/v2/service_instances/9547e9ed-e460-4abe-bda3-7070b9835917/routes" + "routes_url": "/v2/service_instances/9547e9ed-e460-4abe-bda3-7070b9835917/routes", + "shared_from_url": "/v2/service_instances/9547e9ed-e460-4abe-bda3-7070b9835917/shared_from", + "shared_to_url": "/v2/service_instances/9547e9ed-e460-4abe-bda3-7070b9835917/shared_to" } } ] diff --git a/docs/v3/source/includes/api_resources/_service_instances.erb b/docs/v3/source/includes/api_resources/_service_instances.erb index 3868b3df80f..a9a15f49183 100644 --- a/docs/v3/source/includes/api_resources/_service_instances.erb +++ b/docs/v3/source/includes/api_resources/_service_instances.erb @@ -18,3 +18,28 @@ } } <% end %> + +<% content_for :paginated_list_of_service_instances do %> +{ + "pagination": { + "total_results": 1, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/service_instances?page=1&per_page=50" + }, + "last": { + "href": "https://api.example.org/v3/service_instances?page=1&per_page=50" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "d4c91047-7b29-4fda-b7f9-04033e5c9c9f", + "created_at": "2017-02-02T00:14:30Z", + "updated_at": "2017-02-02T00:14:30Z", + "name": "my_service_instance" + } + ] +} +<% end %> diff --git a/docs/v3/source/includes/experimental_resources/service_instances/_list.md.erb b/docs/v3/source/includes/experimental_resources/service_instances/_list.md.erb new file mode 100644 index 00000000000..de938427740 --- /dev/null +++ b/docs/v3/source/includes/experimental_resources/service_instances/_list.md.erb @@ -0,0 +1,36 @@ +### List service instances + +``` +Example Request +``` + +```shell +curl "https://api.example.org/v3/service_instances" \ + -X GET \ + -H "Authorization: bearer [token]" +``` + +``` +Example Response +``` + +```http +HTTP/1.1 200 OK +Content-Type: application/json + +<%= yield_content :paginated_list_of_service_instances, '/v3/service_instances' %> +``` +This endpoint retrieves the service instances the user has access to. At the moment, this endpoint only returns managed service instances. This may change in the future. + +This includes access granted by service instance sharing. + +#### Definition +`GET /v3/service_instances` + +#### Query Parameters + +Name | Type | Description +---- | ---- | ------------ +**name** | _list of strings_ | Comma-delimited list of service instance names to filter by. +**page** | _integer_ | Page to display. Valid values are integers >= 1. +**per_page** | _integer_ | Number of results per page.
    Valid values are 1 through 5000. diff --git a/docs/v3/source/index.md b/docs/v3/source/index.md index 75e2112c34b..b035577261e 100644 --- a/docs/v3/source/index.md +++ b/docs/v3/source/index.md @@ -137,6 +137,7 @@ includes: - experimental_resources/service_bindings/delete - experimental_resources/service_bindings/list - experimental_resources/service_instances/header + - experimental_resources/service_instances/list - experimental_resources/service_instances/share_to_space - experimental_resources/service_instances/unshare_from_space search: true diff --git a/spec/request/service_instances_spec.rb b/spec/request/service_instances_spec.rb index c9e0a68bb80..b0376067818 100644 --- a/spec/request/service_instances_spec.rb +++ b/spec/request/service_instances_spec.rb @@ -2,11 +2,127 @@ RSpec.describe 'Service Instances' do let(:user_email) { 'user@email.example.com' } - let(:user_name) { 'sharer_username' } + let(:user_name) { 'username' } let(:user) { VCAP::CloudController::User.make } + let(:user_header) { headers_for(user) } let(:admin_header) { admin_headers_for(user, email: user_email, user_name: user_name) } + let(:space) { VCAP::CloudController::Space.make } let(:target_space) { VCAP::CloudController::Space.make } - let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make } + let!(:service_instance1) { VCAP::CloudController::ManagedServiceInstance.make(space: space, name: 'rabbitmq') } + let!(:service_instance2) { VCAP::CloudController::ManagedServiceInstance.make(space: space, name: 'redis') } + let!(:service_instance3) { VCAP::CloudController::ManagedServiceInstance.make(space: space, name: 'mysql') } + + describe 'GET /v3/service_instances' do + it 'returns a paginated list of service instances the user has access to' do + set_current_user_as_role(role: 'space_developer', org: space.organization, space: space, user: user) + get '/v3/service_instances?per_page=2&order_by=name', nil, user_header + expect(last_response.status).to eq(200) + + parsed_response = MultiJson.load(last_response.body) + expect(parsed_response).to be_a_response_like( + { + 'pagination' => { + 'total_results' => 3, + 'total_pages' => 2, + 'first' => { + 'href' => "#{link_prefix}/v3/service_instances?order_by=name&page=1&per_page=2" + }, + 'last' => { + 'href' => "#{link_prefix}/v3/service_instances?order_by=name&page=2&per_page=2" + }, + 'next' => { + 'href' => "#{link_prefix}/v3/service_instances?order_by=name&page=2&per_page=2" + }, + 'previous' => nil + }, + 'resources' => [ + { + 'guid' => service_instance3.guid, + 'name' => service_instance3.name, + 'created_at' => iso8601, + 'updated_at' => iso8601, + }, + { + 'guid' => service_instance1.guid, + 'name' => service_instance1.name, + 'created_at' => iso8601, + 'updated_at' => iso8601, + } + ] + } + ) + end + + it 'returns a paginated list of service instances filtered by name' do + set_current_user_as_role(role: 'space_developer', org: space.organization, space: space, user: user) + get '/v3/service_instances?per_page=2&names=redis', nil, user_header + expect(last_response.status).to eq(200) + + parsed_response = MultiJson.load(last_response.body) + expect(parsed_response).to be_a_response_like( + { + 'pagination' => { + 'total_results' => 1, + 'total_pages' => 1, + 'first' => { + 'href' => "#{link_prefix}/v3/service_instances?names=redis&page=1&per_page=2" + }, + 'last' => { + 'href' => "#{link_prefix}/v3/service_instances?names=redis&page=1&per_page=2" + }, + 'next' => nil, + 'previous' => nil + }, + 'resources' => [ + { + 'guid' => service_instance2.guid, + 'name' => service_instance2.name, + 'created_at' => iso8601, + 'updated_at' => iso8601, + } + ] + } + ) + end + + context 'when a user has access to a shared service instance' do + before do + service_instance1.add_shared_space(target_space) + end + + it 'returns a paginated list of service instances the user has access to' do + set_current_user_as_role(role: 'space_developer', org: target_space.organization, space: target_space, user: user) + get '/v3/service_instances?per_page=2&order_by=name', nil, user_header + expect(last_response.status).to eq(200) + + parsed_response = MultiJson.load(last_response.body) + expect(parsed_response).to be_a_response_like( + { + 'pagination' => { + 'total_results' => 1, + 'total_pages' => 1, + 'first' => { + 'href' => "#{link_prefix}/v3/service_instances?order_by=name&page=1&per_page=2" + }, + 'last' => { + 'href' => "#{link_prefix}/v3/service_instances?order_by=name&page=1&per_page=2" + }, + 'next' => nil, + 'previous' => nil + }, + 'resources' => [ + { + 'guid' => service_instance1.guid, + 'name' => service_instance1.name, + 'created_at' => iso8601, + 'updated_at' => iso8601, + } + ] + } + ) + end + end + end describe 'POST /v3/service_instances/:guid/relationships/shared_spaces' do before do @@ -20,7 +136,7 @@ ] } - post "/v3/service_instances/#{service_instance.guid}/relationships/shared_spaces", share_request.to_json, admin_header + post "/v3/service_instances/#{service_instance1.guid}/relationships/shared_spaces", share_request.to_json, admin_header parsed_response = MultiJson.load(last_response.body) expect(last_response.status).to eq(200) @@ -30,8 +146,8 @@ { 'guid' => target_space.guid } ], 'links' => { - 'self' => { 'href' => "#{link_prefix}/v3/service_instances/#{service_instance.guid}/relationships/shared_spaces" }, - 'related' => { 'href' => "#{link_prefix}/v3/service_instances/#{service_instance.guid}/shared_spaces" }, + 'self' => { 'href' => "#{link_prefix}/v3/service_instances/#{service_instance1.guid}/relationships/shared_spaces" }, + 'related' => { 'href' => "#{link_prefix}/v3/service_instances/#{service_instance1.guid}/shared_spaces" }, } } @@ -44,11 +160,11 @@ actor_type: 'user', actor_name: user_email, actor_username: user_name, - actee: service_instance.guid, + actee: service_instance1.guid, actee_type: 'service_instance', - actee_name: service_instance.name, - space_guid: service_instance.space.guid, - organization_guid: service_instance.space.organization.guid + actee_name: service_instance1.name, + space_guid: space.guid, + organization_guid: space.organization.guid }) expect(event.metadata['target_space_guids']).to eq([target_space.guid]) end @@ -68,12 +184,12 @@ ] } - post "/v3/service_instances/#{service_instance.guid}/relationships/shared_spaces", share_request.to_json, admin_header + post "/v3/service_instances/#{service_instance1.guid}/relationships/shared_spaces", share_request.to_json, admin_header expect(last_response.status).to eq(200) end it 'unshares the service instance from the target space' do - delete "/v3/service_instances/#{service_instance.guid}/relationships/shared_spaces/#{target_space.guid}", nil, admin_header + delete "/v3/service_instances/#{service_instance1.guid}/relationships/shared_spaces/#{target_space.guid}", nil, admin_header expect(last_response.status).to eq(204) event = VCAP::CloudController::Event.last @@ -83,23 +199,23 @@ actor_type: 'user', actor_name: user_email, actor_username: user_name, - actee: service_instance.guid, + actee: service_instance1.guid, actee_type: 'service_instance', - actee_name: service_instance.name, - space_guid: service_instance.space.guid, - organization_guid: service_instance.space.organization.guid + actee_name: service_instance1.name, + space_guid: space.guid, + organization_guid: space.organization.guid }) expect(event.metadata['target_space_guid']).to eq(target_space.guid) end it 'deletes associated bindings in target space when service instance is unshared' do process = VCAP::CloudController::ProcessModelFactory.make(diego: false, space: target_space) - service_binding = VCAP::CloudController::ServiceBinding.make(service_instance: service_instance, app: process.app, credentials: { secret: 'key' }) + service_binding = VCAP::CloudController::ServiceBinding.make(service_instance: service_instance1, app: process.app, credentials: { secret: 'key' }) get "/v2/service_bindings/#{service_binding.guid}", nil, admin_header expect(last_response.status).to eq(200) - delete "/v3/service_instances/#{service_instance.guid}/relationships/shared_spaces/#{target_space.guid}", nil, admin_header + delete "/v3/service_instances/#{service_instance1.guid}/relationships/shared_spaces/#{target_space.guid}", nil, admin_header expect(last_response.status).to eq(204) get "/v2/service_bindings/#{service_binding.guid}", nil, admin_header diff --git a/spec/request/v2/service_bindings_spec.rb b/spec/request/v2/service_bindings_spec.rb index 11aea28425a..ce50ba51bb5 100644 --- a/spec/request/v2/service_bindings_spec.rb +++ b/spec/request/v2/service_bindings_spec.rb @@ -197,7 +197,9 @@ 'service_plan_url' => "/v2/service_plans/#{service_instance.service_plan.guid}", 'service_bindings_url' => "/v2/service_instances/#{service_instance.guid}/service_bindings", 'service_keys_url' => "/v2/service_instances/#{service_instance.guid}/service_keys", - 'routes_url' => "/v2/service_instances/#{service_instance.guid}/routes" + 'routes_url' => "/v2/service_instances/#{service_instance.guid}/routes", + 'shared_from_url' => "/v2/service_instances/#{service_instance.guid}/shared_from", + 'shared_to_url' => "/v2/service_instances/#{service_instance.guid}/shared_to", } } } diff --git a/spec/request/v2/service_instances_spec.rb b/spec/request/v2/service_instances_spec.rb index 8708ea9e454..2446c5b6414 100644 --- a/spec/request/v2/service_instances_spec.rb +++ b/spec/request/v2/service_instances_spec.rb @@ -54,7 +54,9 @@ 'service_plan_url' => "/v2/service_plans/#{service_plan.guid}", 'service_bindings_url' => "/v2/service_instances/#{service_instance.guid}/service_bindings", 'service_keys_url' => "/v2/service_instances/#{service_instance.guid}/service_keys", - 'routes_url' => "/v2/service_instances/#{service_instance.guid}/routes" + 'routes_url' => "/v2/service_instances/#{service_instance.guid}/routes", + 'shared_from_url' => "/v2/service_instances/#{service_instance.guid}/shared_from", + 'shared_to_url' => "/v2/service_instances/#{service_instance.guid}/shared_to", } } ) @@ -96,7 +98,9 @@ 'service_url' => "/v2/services/#{service_instance.service.guid}", 'service_bindings_url' => "/v2/service_instances/#{service_instance.guid}/service_bindings", 'service_keys_url' => "/v2/service_instances/#{service_instance.guid}/service_keys", - 'routes_url' => "/v2/service_instances/#{service_instance.guid}/routes" + 'routes_url' => "/v2/service_instances/#{service_instance.guid}/routes", + 'shared_from_url' => "/v2/service_instances/#{service_instance.guid}/shared_from", + 'shared_to_url' => "/v2/service_instances/#{service_instance.guid}/shared_to", } } ) @@ -137,7 +141,9 @@ 'service_url' => "/v2/services/#{service_instance.service.guid}", 'service_bindings_url' => "/v2/service_instances/#{service_instance.guid}/service_bindings", 'service_keys_url' => "/v2/service_instances/#{service_instance.guid}/service_keys", - 'routes_url' => "/v2/service_instances/#{service_instance.guid}/routes" + 'routes_url' => "/v2/service_instances/#{service_instance.guid}/routes", + 'shared_from_url' => "/v2/service_instances/#{service_instance.guid}/shared_from", + 'shared_to_url' => "/v2/service_instances/#{service_instance.guid}/shared_to", } } ) @@ -145,4 +151,97 @@ end end end + + describe 'GET /v2/service_instances/:service_instance_guid/shared_from' do + let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space: space) } + + before do + service_instance.add_shared_space(VCAP::CloudController::Space.make) + end + + it 'returns data about the source space and org' do + get "v2/service_instances/#{service_instance.guid}/shared_from", nil, admin_headers + + expect(last_response.status).to eq(200), last_response.body + + parsed_response = MultiJson.load(last_response.body) + expect(parsed_response).to be_a_response_like({ + 'space_name' => space.name, + 'organization_name' => space.organization.name + }) + end + + context 'when the user is a member of the space where a service instance has been shared to' do + let(:other_space) { VCAP::CloudController::Space.make } + let(:other_user) { make_developer_for_space(other_space) } + let(:req_body) do + { + data: [ + { guid: other_space.guid } + ] + }.to_json + end + + before do + VCAP::CloudController::FeatureFlag.make(name: 'service_instance_sharing', enabled: true, error_message: nil) + + other_space.organization.add_user(user) + other_space.add_developer(user) + + post "v3/service_instances/#{service_instance.guid}/relationships/shared_spaces", req_body, headers_for(user) + expect(last_response.status).to eq(200) + end + + it 'returns data about the source space and org' do + get "v2/service_instances/#{service_instance.guid}/shared_from", nil, headers_for(other_user) + + expect(last_response.status).to eq(200) + + parsed_response = MultiJson.load(last_response.body) + expect(parsed_response).to be_a_response_like({ + 'space_name' => space.name, + 'organization_name' => space.organization.name + }) + end + end + end + + describe 'GET /v2/service_instances/:service_instance_guid/shared_to' do + let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space: space) } + let(:space1) { VCAP::CloudController::Space.make } + let(:space2) { VCAP::CloudController::Space.make } + + before do + service_instance.add_shared_space(space1) + service_instance.add_shared_space(space2) + end + + it 'returns data about the source space, org, and bound_app_count' do + get "v2/service_instances/#{service_instance.guid}/shared_to", nil, admin_headers + + expect(last_response.status).to eq(200) + + parsed_response = MultiJson.load(last_response.body) + expect(parsed_response).to be_a_response_like( + { + 'total_results' => 2, + 'total_pages' => 1, + 'prev_url' => nil, + 'next_url' => nil, + 'resources' => [ + { + 'space_name' => space1.name, + 'organization_name' => space1.organization.name, + 'bound_app_count' => 0 + }, + { + 'space_name' => space2.name, + 'organization_name' => space2.organization.name, + 'bound_app_count' => 0 + } + ] + } + ) + end + end end diff --git a/spec/request/v2/spaces_spec.rb b/spec/request/v2/spaces_spec.rb index bdd08d3d8ed..f163cf306fa 100644 --- a/spec/request/v2/spaces_spec.rb +++ b/spec/request/v2/spaces_spec.rb @@ -132,7 +132,7 @@ space.add_developer(user) end - it 'lists the isolation segment for SpaceDvelopers' do + it 'lists the isolation segment for SpaceDevelopers' do get "/v2/spaces/#{space.guid}", {}, headers_for(user) expect(last_response.status).to eq(200) @@ -170,6 +170,63 @@ end end + describe 'GET /v2/spaces/:guid/service_instances' do + let(:originating_space) { VCAP::CloudController::Space.make } + let(:shared_service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space: originating_space) } + let(:space) { VCAP::CloudController::Space.make } + + before do + originating_space.organization.add_user(user) + originating_space.add_developer(user) + space.organization.add_user(user) + space.add_developer(user) + + shared_service_instance.add_shared_space(space) + end + + it 'shows the shared service instances associated with the space' do + get "/v2/spaces/#{space.guid}/service_instances", {}, headers_for(user) + + expect(last_response.status).to eq(200) + parsed_response = MultiJson.load(last_response.body) + + expect(parsed_response).to be_a_response_like({ + 'total_results' => 1, + 'total_pages' => 1, + 'prev_url' => nil, + 'next_url' => nil, + 'resources' => [{ + 'metadata' => { + 'guid' => shared_service_instance.guid, + 'url' => "/v2/service_instances/#{shared_service_instance.guid}", + 'created_at' => iso8601, + 'updated_at' => iso8601, + }, + 'entity' => { + 'name' => shared_service_instance.name, + 'credentials' => shared_service_instance.credentials, + 'service_plan_guid' => shared_service_instance.service_plan_guid, + 'space_guid' => originating_space.guid, + 'gateway_data' => nil, + 'dashboard_url' => nil, + 'type' => 'managed_service_instance', + 'last_operation' => nil, + 'tags' => [], + 'service_guid' => shared_service_instance.service_plan.service_guid, + 'space_url' => "/v2/spaces/#{originating_space.guid}", + 'service_plan_url' => "/v2/service_plans/#{shared_service_instance.service_plan_guid}", + 'service_bindings_url' => "/v2/service_instances/#{shared_service_instance.guid}/service_bindings", + 'service_keys_url' => "/v2/service_instances/#{shared_service_instance.guid}/service_keys", + 'routes_url' => "/v2/service_instances/#{shared_service_instance.guid}/routes", + 'service_url' => "/v2/services/#{shared_service_instance.service_plan.service_guid}", + 'shared_from_url' => "/v2/service_instances/#{shared_service_instance.guid}/shared_from", + 'shared_to_url' => "/v2/service_instances/#{shared_service_instance.guid}/shared_to", + } + }] + }) + end + end + describe 'DELETE /v2/spaces/:guid/unmapped_routes' do let(:space) { VCAP::CloudController::Space.make(organization: org) } let(:process) { VCAP::CloudController::ProcessModelFactory.make(state: 'STARTED') } diff --git a/spec/unit/access/service_instance_access_spec.rb b/spec/unit/access/service_instance_access_spec.rb index 1361e7236d4..7ed67887254 100644 --- a/spec/unit/access/service_instance_access_spec.rb +++ b/spec/unit/access/service_instance_access_spec.rb @@ -149,6 +149,63 @@ module VCAP::CloudController end end + context 'space developer in a space that the service instance has been shared into' do + before do + org.add_user(user) + target_space = VCAP::CloudController::Space.make(organization: org) + target_space.add_developer(user) + service_instance.add_shared_space(target_space) + end + + context 'when the space of the service instance is visible' do + it_behaves_like :read_only_access do + let(:object) { service_instance } + end + + it 'does NOT allow the user to have manage permissions of the service instance' do + expect(subject).to_not allow_op_on_object(:manage_permissions, service_instance) + end + + it 'allows the user to have read permissions of the service instance' do + expect(subject).to allow_op_on_object(:read_permissions, service_instance) + end + + it 'does NOT allow the user to read default credentials of the service instance' do + expect(subject).not_to allow_op_on_object(:read_env, service_instance) + end + + it 'returns false for purge' do + expect(subject).not_to allow_op_on_object(:purge, service_instance) + end + end + + context 'when the space of the service instance is not visible' do + before do + service_instance.space = nil + end + + it_behaves_like :read_only_access do + let(:object) { service_instance } + end + + it 'does NOT allow the user to have manage permissions of the service instance' do + expect(subject).to_not allow_op_on_object(:manage_permissions, service_instance) + end + + it 'allows the user to have read permissions of the service instance' do + expect(subject).to allow_op_on_object(:read_permissions, service_instance) + end + + it 'does NOT allow the user to read default credentials of the service instance' do + expect(subject).not_to allow_op_on_object(:read_env, service_instance) + end + + it 'returns false for purge' do + expect(subject).not_to allow_op_on_object(:purge, service_instance) + end + end + end + context 'organization manager (defensive)' do before { org.add_manager(user) } diff --git a/spec/unit/controllers/runtime/spaces_controller_spec.rb b/spec/unit/controllers/runtime/spaces_controller_spec.rb index 7477158ad49..de2de367020 100644 --- a/spec/unit/controllers/runtime/spaces_controller_spec.rb +++ b/spec/unit/controllers/runtime/spaces_controller_spec.rb @@ -223,6 +223,14 @@ def decoded_guids before { set_current_user(developer) } + it 'returns the shared from url' do + space_instance = ManagedServiceInstance.make(space: space) + + get "/v2/spaces/#{space.guid}/service_instances" + service_instance_response = decoded_response.fetch('resources').first + expect(service_instance_response.fetch('entity').fetch('shared_from_url')).to eq("/v2/service_instances/#{space_instance.guid}/shared_from") + end + context 'when filtering results' do it 'returns only matching results' do user_provided_service_instance_1 = UserProvidedServiceInstance.make(space: space, name: 'provided service 1') @@ -240,6 +248,39 @@ def decoded_guids end end + describe 'shared service instances' do + context 'when a service instance has been shared from another space' do + let(:shared_service_instance) { ManagedServiceInstance.make(space: Space.make) } + + before do + shared_service_instance.add_shared_space(space) + end + + it 'returns the shared service instance' do + get "v2/spaces/#{space.guid}/service_instances" + + guids = decoded_response.fetch('resources').map { |service| service.fetch('metadata').fetch('guid') } + expect(guids).to include(shared_service_instance.guid) + end + end + + context 'when a service instance has been shared between two spaces that are not the queried space' do + let(:other_space) { make_space_for_user(developer) } + let(:irrelevant_shared_service_instance) { ManagedServiceInstance.make(space: Space.make) } + + before do + irrelevant_shared_service_instance.add_shared_space(other_space) + end + + it 'does not return the irrelevant shared service instance' do + get "v2/spaces/#{space.guid}/service_instances" + + guids = decoded_response.fetch('resources').map { |service| service.fetch('metadata').fetch('guid') } + expect(guids).not_to include(irrelevant_shared_service_instance.guid) + end + end + end + context 'when there are provided service instances' do let!(:user_provided_service_instance) { UserProvidedServiceInstance.make(space: space) } let!(:managed_service_instance) { ManagedServiceInstance.make(space: space) } diff --git a/spec/unit/controllers/services/service_instances_controller_spec.rb b/spec/unit/controllers/services/service_instances_controller_spec.rb index 6f6607fa9e2..93f3a78af3e 100644 --- a/spec/unit/controllers/services/service_instances_controller_spec.rb +++ b/spec/unit/controllers/services/service_instances_controller_spec.rb @@ -3496,6 +3496,253 @@ def verify_forbidden(user) end end + describe 'GET /v2/service_instances/:service_instance_guid/shared_from' do + let(:org) { Organization.make } + let(:space) { Space.make(organization: org) } + let(:instance) { ManagedServiceInstance.make(space: space) } + + context 'when the service instance is not shared' do + it 'returns no content' do + set_current_user_as_admin + get "/v2/service_instances/#{instance.guid}/shared_from" + expect(last_response.status).to eql(204) + expect(JSON.parse(last_response.body)).to be nil + end + end + + context 'when the service instance is shared' do + let(:other_org) { Organization.make } + let(:other_space) { Space.make(organization: other_org) } + + before do + instance.add_shared_space(other_space) + end + + it 'returns the correct body' do + set_current_user_as_admin + get "/v2/service_instances/#{instance.guid}/shared_from" + expect(last_response.status).to eql(200), last_response.body + parsed_response = JSON.parse(last_response.body) + expect(parsed_response['space_name']).to eq(space.name) + expect(parsed_response['organization_name']).to eq(space.organization.name) + expect(parsed_response.keys).to match_array(['space_name', 'organization_name']) + end + + describe 'permissions' do + let(:user) { User.make } + + context 'when the user is a member of the org/space this instance exists in' do + { + 'admin' => 200, + 'space_developer' => 200, + 'admin_read_only' => 200, + 'global_auditor' => 200, + 'space_manager' => 200, + 'space_auditor' => 200, + 'org_manager' => 200, + 'org_auditor' => 404, + 'org_billing_manager' => 404, + }.each do |role, expected_status| + context "as an #{role}" do + before do + set_current_user_as_role( + role: role, + org: org, + space: space, + user: user, + scopes: ['cloud_controller.read'] + ) + end + + it "has a #{expected_status} http status code" do + get "/v2/service_instances/#{instance.guid}/shared_from" + expect(last_response.status).to eq(expected_status), "Expected #{expected_status}, got: #{last_response.status}, role: #{role}" + end + end + end + end + + context 'when the user is a member of the org/space where the service instance was shared to' do + { + 'space_developer' => 200, + 'space_manager' => 200, + 'space_auditor' => 200, + 'org_manager' => 200, + 'org_auditor' => 404, + 'org_billing_manager' => 404, + }.each do |role, expected_status| + context "as an #{role}" do + before do + set_current_user_as_role( + role: role, + org: other_org, + space: other_space, + user: user, + scopes: ['cloud_controller.read'] + ) + end + + it "has a #{expected_status} http status code" do + get "/v2/service_instances/#{instance.guid}/shared_from" + expect(last_response.status).to eq(expected_status), "Expected #{expected_status}, got: #{last_response.status}, role: #{role}" + end + end + end + end + + context 'when the user is NOT a member of the space this instance exists in' do + let(:instance) { ManagedServiceInstance.make } + + it 'returns a JSON payload indicating the user does not have permission to manage this instance' do + set_current_user(user) + get "/v2/service_instances/#{instance.guid}/shared_from" + expect(last_response.status).to eql(404) + end + end + end + end + end + + describe 'GET /v2/service_instances/:service_instance_guid/shared_to' do + let(:org) { Organization.make } + let(:space) { Space.make(organization: org) } + let(:instance) { ManagedServiceInstance.make(space: space) } + + it 'returns the correct body' do + set_current_user_as_admin + get "/v2/service_instances/#{instance.guid}/shared_to" + expect(last_response.status).to eql(200) + expect(JSON.parse(last_response.body)['resources']).to eq([]) + end + + context 'when the service instance is shared into multiple spaces' do + let(:space1) { Space.make } + let(:space2) { Space.make } + + before do + FeatureFlag.make(name: 'service_instance_sharing', enabled: true, error_message: nil) + instance.add_shared_space(space1) + instance.add_shared_space(space2) + end + + it 'returns the correct body' do + set_current_user_as_admin + get "/v2/service_instances/#{instance.guid}/shared_to" + decoded_response = JSON.parse(last_response.body) + expect(last_response.status).to eql(200), last_response.body + expect(decoded_response.fetch('total_results')).to eq(2) + resources = decoded_response.fetch('resources') + + space1_resource = resources.find { |resource| resource['space_name'] == space1.name } + space2_resource = resources.find { |resource| resource['space_name'] == space2.name } + + expect(space1_resource.keys).to match_array(['space_name', 'organization_name', 'bound_app_count']) + expect(space2_resource.keys).to match_array(['space_name', 'organization_name', 'bound_app_count']) + + expect(space1_resource.fetch('space_name')).to eq(space1.name) + expect(space2_resource.fetch('space_name')).to eq(space2.name) + + expect(space1_resource.fetch('organization_name')).to eq(space1.organization.name) + expect(space2_resource.fetch('organization_name')).to eq(space2.organization.name) + + expect(space1_resource.fetch('bound_app_count')).to eq(0) + expect(space2_resource.fetch('bound_app_count')).to eq(0) + end + + context 'when there are apps bound to the shared service instance' do + before do + ServiceBinding.make(service_instance: instance, app: AppModel.make(space: space1)) + ServiceBinding.make(service_instance: instance, app: AppModel.make(space: space1)) + ServiceBinding.make(service_instance: ServiceInstance.make(space: space1), app: AppModel.make(space: space1)) + + ServiceBinding.make(service_instance: instance, app: AppModel.make(space: space2)) + end + + it 'returns the correct bound_app_count' do + set_current_user_as_admin + get "/v2/service_instances/#{instance.guid}/shared_to" + decoded_response = JSON.parse(last_response.body) + expect(last_response.status).to eql(200), last_response.body + resources = decoded_response.fetch('resources') + + space1_resource = resources.find { |resource| resource['space_name'] == space1.name } + space2_resource = resources.find { |resource| resource['space_name'] == space2.name } + + expect(space1_resource.fetch('bound_app_count')).to eq(2) + expect(space2_resource.fetch('bound_app_count')).to eq(1) + end + end + end + + describe 'permissions' do + let(:user) { User.make } + + context 'when the user is a member of the org/space this instance exists in' do + { + 'admin' => 200, + 'space_developer' => 200, + 'admin_read_only' => 200, + 'global_auditor' => 200, + 'space_manager' => 200, + 'space_auditor' => 200, + 'org_manager' => 200, + 'org_auditor' => 404, + 'org_billing_manager' => 404, + }.each do |role, expected_status| + context "as an #{role}" do + before do + set_current_user_as_role( + role: role, + org: org, + space: space, + user: user, + ) + end + + 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}" + end + end + end + end + + context 'when the user is a member of the org/space where the service instance was shared to' do + let(:other_org) { Organization.make } + let(:other_space) { Space.make(organization: other_org) } + + before do + instance.add_shared_space(other_space) + end + + { + 'space_developer' => 404, + 'space_manager' => 404, + 'space_auditor' => 404, + 'org_manager' => 404, + 'org_auditor' => 404, + 'org_billing_manager' => 404, + }.each do |role, expected_status| + context "as an #{role}" do + before do + set_current_user_as_role( + role: role, + org: other_org, + space: other_space, + user: user, + ) + end + + 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}" + end + end + end + end + end + end + describe 'GET /v2/service_instances/:service_instance_guid/service_keys' do let(:space) { Space.make } let(:manager) { make_manager_for_space(space) } diff --git a/spec/unit/controllers/v3/service_instance_controller_spec.rb b/spec/unit/controllers/v3/service_instance_controller_spec.rb index 16dad33ef32..b2025bc5e7f 100644 --- a/spec/unit/controllers/v3/service_instance_controller_spec.rb +++ b/spec/unit/controllers/v3/service_instance_controller_spec.rb @@ -2,6 +2,113 @@ RSpec.describe ServiceInstancesV3Controller, type: :controller do let(:user) { set_current_user(VCAP::CloudController::User.make) } + let(:space) { VCAP::CloudController::Space.make } + let!(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space: space) } + + describe '#index' do + context 'when there are multiple service instances' do + let!(:service_instance2) { VCAP::CloudController::ManagedServiceInstance.make } + let!(:service_instance3) { VCAP::CloudController::ManagedServiceInstance.make } + + context 'as an admin' do + before do + set_current_user_as_admin + end + + it 'returns all service instances' do + get :index + expect(response.status).to eq(200), response.body + expect(parsed_body['resources'].length).to eq 3 + + response_names = parsed_body['resources'].map { |resource| resource['name'] } + expect(response_names).to include(service_instance.name, service_instance2.name, service_instance3.name) + end + end + + context 'as a user who only has limited access' do + before do + set_current_user_as_role(role: 'space_developer', org: space.organization, space: space, user: user) + end + + it 'returns a subset of service instances' do + get :index + expect(response.status).to eq(200), response.body + expect(parsed_body['resources'].length).to eq 1 + + response_names = parsed_body['resources'].map { |resource| resource['name'] } + expect(response_names).to include(service_instance.name) + end + end + end + + describe 'permissions by role' do + role_to_expected_http_response = { + 'admin' => true, + 'admin_read_only' => true, + 'global_auditor' => true, + 'org_manager' => true, + 'org_auditor' => false, + 'org_billing_manager' => false, + 'space_manager' => true, + 'space_auditor' => true, + 'space_developer' => true, + }.freeze + + role_to_expected_http_response.each do |role, can_see_service_instance| + context "as an #{role}" do + it "#{can_see_service_instance ? 'can' : 'cannot'} see the service instance" do + set_current_user_as_role(role: role, org: space.organization, space: space, user: user) + + expected_service_instance_names = can_see_service_instance ? [service_instance.name] : [] + + get :index + expect(response.status).to eq(200), response.body + expect(parsed_body['resources'].map { |h| h['name'] }).to match_array(expected_service_instance_names) + end + end + end + end + + describe 'permissions by role for shared services' do + let(:target_space) { VCAP::CloudController::Space.make } + before do + service_instance.add_shared_space(target_space) + end + role_to_expected_http_response = { + 'org_manager' => true, + 'org_auditor' => false, + 'org_billing_manager' => false, + 'space_manager' => true, + 'space_auditor' => true, + 'space_developer' => true, + }.freeze + + role_to_expected_http_response.each do |role, can_see_service_instance| + context "as an #{role}" do + it "#{can_see_service_instance ? 'can' : 'cannot'} see the service instance" do + set_current_user_as_role(role: role, org: target_space.organization, space: target_space, user: user) + + expected_service_instance_names = can_see_service_instance ? [service_instance.name] : [] + + get :index + expect(response.status).to eq(200), response.body + expect(parsed_body['resources'].map { |h| h['name'] }).to match_array(expected_service_instance_names) + end + end + end + end + + context 'when a non-supported value is specified' do + it 'a bad query parameter error is returned' do + set_current_user_as_admin + get :index, { order_by: 'banana' } + + expect(response.status).to eq(400) + expect(response.body).to include 'BadQueryParameter' + expect(response.body).to include("Order by can only be: 'created_at', 'updated_at', 'name'") + end + end + end describe '#share_service_instance' do let(:service_instance) { VCAP::CloudController::ServiceInstance.make } diff --git a/spec/unit/messages/service_instances_list_message_spec.rb b/spec/unit/messages/service_instances_list_message_spec.rb new file mode 100644 index 00000000000..1d935b1d602 --- /dev/null +++ b/spec/unit/messages/service_instances_list_message_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' +require 'messages/service_instances/service_instances_list_message' + +module VCAP::CloudController + RSpec.describe ServiceInstancesListMessage do + describe '.from_params' do + let(:params) do + { + 'page' => 1, + 'per_page' => 5, + 'order_by' => 'name', + 'names' => 'rabbitmq, redis,mysql' + } + end + + it 'returns the correct ServiceInstancesListMessage' do + message = ServiceInstancesListMessage.from_params(params) + + expect(message).to be_a(ServiceInstancesListMessage) + expect(message.page).to eq(1) + expect(message.per_page).to eq(5) + expect(message.order_by).to eq('name') + expect(message.names).to match_array(['mysql', 'rabbitmq', 'redis']) + end + + it 'converts requested keys to symbols' do + message = ServiceInstancesListMessage.from_params(params) + + expect(message.requested?(:page)).to be_truthy + expect(message.requested?(:per_page)).to be_truthy + expect(message.requested?(:order_by)).to be_truthy + expect(message.requested?(:names)).to be_truthy + end + end + + describe 'fields' do + it 'accepts a set of fields' do + message = ServiceInstancesListMessage.new({ + page: 1, + per_page: 5, + order_by: 'created_at', + names: ['rabbitmq', 'redis'] + }) + expect(message).to be_valid + end + + it 'accepts an empty set' do + message = ServiceInstancesListMessage.new + expect(message).to be_valid + end + + it 'does not accept a field not in this set' do + message = ServiceInstancesListMessage.new({ foobar: 'pants' }) + + expect(message).not_to be_valid + expect(message.errors[:base]).to include("Unknown query parameter(s): 'foobar'") + end + end + end +end diff --git a/spec/unit/models/services/service_instance_spec.rb b/spec/unit/models/services/service_instance_spec.rb index 671bcc5b92f..52bb0ff5662 100644 --- a/spec/unit/models/services/service_instance_spec.rb +++ b/spec/unit/models/services/service_instance_spec.rb @@ -298,6 +298,14 @@ module VCAP::CloudController expect(service_instance).not_to be_in_suspended_org end end + + context 'when the service instance space is not visible' do + let(:space) { nil } + + it 'is false' do + expect(service_instance).not_to be_in_suspended_org + end + end end describe '#to_hash' do @@ -326,5 +334,121 @@ module VCAP::CloudController expect(service_instance.to_hash(opts)['credentials']).to eq({ redacted_message: '[PRIVATE DATA HIDDEN]' }) end end + + describe '#user_visibility_filter' do + let(:developer) { make_developer_for_space(service_instance.space) } + let(:auditor) { make_auditor_for_space(service_instance.space) } + let(:user) { make_user_for_space(service_instance.space) } + let(:org_manager) { make_manager_for_org(service_instance.space.organization) } + let(:space_manager) { make_manager_for_space(service_instance.space) } + + context 'when a user is an org manager where the instance was created' do + it 'the service instance is visible' do + filter = ServiceInstance.user_visibility_filter(org_manager) + expect(ServiceInstance.filter(filter).all.length).to eq(1) + end + end + + context 'when a user is a space developer in the space the instance was created' do + it 'the service instance is visible' do + filter = ServiceInstance.user_visibility_filter(developer) + expect(ServiceInstance.filter(filter).all.length).to eq(1) + end + end + + context 'when a user is a space auditor in the space the instance was created' do + it 'the service instance is visible' do + filter = ServiceInstance.user_visibility_filter(auditor) + expect(ServiceInstance.filter(filter).all.length).to eq(1) + end + end + + context 'when a user is a space manager in the space the instance was created' do + it 'the service instance is visible' do + filter = ServiceInstance.user_visibility_filter(space_manager) + expect(ServiceInstance.filter(filter).all.length).to eq(1) + end + end + + context 'when a user does not have access to the originating space' do + it 'the service instance is not visible' do + filter = ServiceInstance.user_visibility_filter(user) + expect(ServiceInstance.filter(filter).all.length).to eq(0) + end + end + + context 'when the service instance is shared' do + let(:target_space) { VCAP::CloudController::Space.make } + let(:target_space_dev) { make_developer_for_space(target_space) } + let(:target_org_user) { make_user_for_org(target_space.organization) } + let(:target_space_auditor) { make_auditor_for_space(target_space) } + let(:target_space_manager) { make_manager_for_space(target_space) } + let(:target_space_org_manager) { make_manager_for_org(target_space.organization) } + + before do + service_instance.add_shared_space(target_space) + end + + context 'when a user is a space developer in the target space' do + it 'the service instance is visible' do + filter = ServiceInstance.user_visibility_filter(target_space_dev) + expect(ServiceInstance.filter(filter).all.length).to eq(1) + end + end + + context 'when a user is a space developer in the source space' do + it 'the service instance is visible' do + filter = ServiceInstance.user_visibility_filter(developer) + expect(ServiceInstance.filter(filter).all.length).to eq(1) + end + end + + context 'when a user is a space auditor in the target space' do + it 'the service instance is visible' do + filter = ServiceInstance.user_visibility_filter(target_space_auditor) + expect(ServiceInstance.filter(filter).all.length).to eq(1) + end + end + + context 'when a user is a space manager in the target space' do + it 'the service instance is visible' do + filter = ServiceInstance.user_visibility_filter(target_space_manager) + expect(ServiceInstance.filter(filter).all.length).to eq(1) + end + end + + context 'when a user is a org manager in the target space' do + it 'the service instance is visible' do + filter = ServiceInstance.user_visibility_filter(target_space_org_manager) + expect(ServiceInstance.filter(filter).all.length).to eq(1) + end + end + + context 'when a user does not have access to the target space' do + it 'the service instance is not visible' do + filter = ServiceInstance.user_visibility_filter(target_org_user) + expect(ServiceInstance.filter(filter).all.length).to eq(0) + end + end + end + end + + describe '#shared?' do + context 'when the service instance has shared spaces' do + before do + service_instance.add_shared_space(Space.make) + end + + it 'returns true' do + expect(service_instance.shared?).to be true + end + end + + context 'when the service instance does not have shared spaces' do + it 'returns false' do + expect(service_instance.shared?).to be false + end + end + end end end diff --git a/spec/unit/presenters/v2/service_instance_presenter_spec.rb b/spec/unit/presenters/v2/service_instance_presenter_spec.rb index 804e909e196..811a2d87a33 100644 --- a/spec/unit/presenters/v2/service_instance_presenter_spec.rb +++ b/spec/unit/presenters/v2/service_instance_presenter_spec.rb @@ -11,35 +11,49 @@ module CloudController::Presenters::V2 let(:relations_hash) { { 'relationship_url' => 'http://relationship.example.com' } } subject { ServiceInstancePresenter.new } - describe '#entity_hash' do - before do - set_current_user_as_admin - end + before do + set_current_user_as_admin + allow(RelationsPresenter).to receive(:new).and_return(relations_presenter) + end - let(:service_instance) do - VCAP::CloudController::ServiceInstance.make( - name: 'things', - ) - end - let(:service_plan) { VCAP::CloudController::ServicePlan.make } + describe 'ManagedServiceInstance' do + describe '#entity_hash' do + let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make } + let(:service_plan) { VCAP::CloudController::ServicePlan.make } - before do - service_instance.service_plan_id = service_plan.id - service_instance.save + before do + service_instance.service_plan_id = service_plan.id + service_instance.save + end - allow(RelationsPresenter).to receive(:new).and_return(relations_presenter) + it 'returns the service instance entity' do + expect(subject.entity_hash(controller, service_instance, opts, depth, parents, orphans)).to eq( + { + 'name' => service_instance.name, + 'service_plan_guid' => service_plan.guid, + 'service_guid' => service_plan.service.guid, + 'relationship_url' => 'http://relationship.example.com', + 'service_url' => "/v2/services/#{service_plan.service.guid}", + 'shared_from_url' => "/v2/service_instances/#{service_instance.guid}/shared_from", + 'shared_to_url' => "/v2/service_instances/#{service_instance.guid}/shared_to", + } + ) + end end + end + + describe 'UserProvidedServiceInstance' do + describe '#entity_hash' do + let(:service_instance) { VCAP::CloudController::UserProvidedServiceInstance.make } - it 'returns the service instance entity' do - expect(subject.entity_hash(controller, service_instance, opts, depth, parents, orphans)).to eq( - { - 'name' => service_instance.name, - 'service_plan_guid' => service_plan.guid, - 'service_guid' => service_plan.service.guid, - 'relationship_url' => 'http://relationship.example.com', - 'service_url' => "/v2/services/#{service_plan.service.guid}" - } - ) + it 'returns the service instance entity' do + expect(subject.entity_hash(controller, service_instance, opts, depth, parents, orphans)).to eq( + { + 'name' => service_instance.name, + 'relationship_url' => 'http://relationship.example.com', + } + ) + end end end end diff --git a/spec/unit/presenters/v2/service_instance_shared_from_presenter_spec.rb b/spec/unit/presenters/v2/service_instance_shared_from_presenter_spec.rb new file mode 100644 index 00000000000..8c18d57602e --- /dev/null +++ b/spec/unit/presenters/v2/service_instance_shared_from_presenter_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +module CloudController::Presenters::V2 + RSpec.describe ServiceInstanceSharedFromPresenter do + describe '#to_hash' do + it 'returns the space and org name' do + space = VCAP::CloudController::Space.make + presenter = ServiceInstanceSharedFromPresenter.new + expect(presenter.to_hash(space)).to eq( + { + 'space_name' => space.name, + 'organization_name' => space.organization.name, + } + ) + end + end + end +end diff --git a/spec/unit/presenters/v2/service_instance_shared_to_presenter_spec.rb b/spec/unit/presenters/v2/service_instance_shared_to_presenter_spec.rb new file mode 100644 index 00000000000..8119cfd3c69 --- /dev/null +++ b/spec/unit/presenters/v2/service_instance_shared_to_presenter_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +module CloudController::Presenters::V2 + RSpec.describe ServiceInstanceSharedToPresenter do + describe '#to_hash' do + it 'returns the space name, org name, and bound app count' do + space = VCAP::CloudController::Space.make + presenter = ServiceInstanceSharedToPresenter.new + expect(presenter.to_hash(space, 42)).to eq( + { + 'space_name' => space.name, + 'organization_name' => space.organization.name, + 'bound_app_count' => 42 + } + ) + end + end + end +end diff --git a/spec/unit/presenters/v3/service_instance_presenter_spec.rb b/spec/unit/presenters/v3/service_instance_presenter_spec.rb new file mode 100644 index 00000000000..58c595f90a4 --- /dev/null +++ b/spec/unit/presenters/v3/service_instance_presenter_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' +require 'presenters/v3/service_instance_presenter' + +module VCAP::CloudController::Presenters::V3 + RSpec.describe ServiceInstancePresenter do + let(:presenter) { ServiceInstancePresenter.new(service_instance) } + let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(name: 'denise-db') } + + describe '#to_hash' do + let(:result) { presenter.to_hash } + + it 'presents the model as a hash' do + expect(result[:guid]).to eq(service_instance.guid) + expect(result[:created_at]).to eq(service_instance.created_at) + expect(result[:updated_at]).to eq(service_instance.updated_at) + expect(result[:name]).to eq('denise-db') + end + end + end +end diff --git a/spec/unit/queries/service_binding_list_fetcher_spec.rb b/spec/unit/queries/service_binding_list_fetcher_spec.rb index 1e6e825dfaf..3e45c6eb02a 100644 --- a/spec/unit/queries/service_binding_list_fetcher_spec.rb +++ b/spec/unit/queries/service_binding_list_fetcher_spec.rb @@ -90,5 +90,53 @@ module VCAP::CloudController end end end + + describe '#fetch_service_instance_bindings_in_space' do + let(:space) { Space.make } + let(:service_instance) { ServiceInstance.make(space: space) } + + it 'returns a Sequel::Dataset' do + results = ServiceBindingListFetcher.fetch_service_instance_bindings_in_space(service_instance.guid, space.guid) + expect(results).to be_a(Sequel::Dataset) + end + + context 'when there are no bindings' do + it 'returns an empty dataset' do + results = ServiceBindingListFetcher.fetch_service_instance_bindings_in_space(service_instance.guid, space.guid) + expect(results.count).to eql(0) + end + end + + context 'when a binding exists in a space' do + let!(:service_binding) { ServiceBinding.make(app: AppModel.make(space: space), service_instance: service_instance) } + let!(:other_service_binding) { ServiceBinding.make } + + it 'returns the binding for the correct space' do + results = ServiceBindingListFetcher.fetch_service_instance_bindings_in_space(service_instance.guid, space.guid) + expect(results.count).to eql(1) + end + end + + context 'when multiple bindings exist in a space' do + let!(:service_binding1) { ServiceBinding.make(app: AppModel.make(space: space), service_instance: service_instance) } + let!(:service_binding2) { ServiceBinding.make(app: AppModel.make(space: space), service_instance: service_instance) } + let!(:other_service_binding) { ServiceBinding.make } + + it 'returns the bindings for the correct space' do + results = ServiceBindingListFetcher.fetch_service_instance_bindings_in_space(service_instance.guid, space.guid) + expect(results.count).to eql(2) + end + end + + context 'when multiple service instances exist' do + let!(:service_binding) { ServiceBinding.make(app: AppModel.make(space: space), service_instance: service_instance) } + let!(:other_service_binding) { ServiceBinding.make(service_instance: ServiceInstance.make(space: space)) } + + it 'returns the binding for the correct service instance' do + results = ServiceBindingListFetcher.fetch_service_instance_bindings_in_space(service_instance.guid, space.guid) + expect(results.count).to eql(1) + end + end + end end end diff --git a/spec/unit/queries/service_instance_list_fetcher_spec.rb b/spec/unit/queries/service_instance_list_fetcher_spec.rb new file mode 100644 index 00000000000..fa07743ce8d --- /dev/null +++ b/spec/unit/queries/service_instance_list_fetcher_spec.rb @@ -0,0 +1,103 @@ +require 'spec_helper' +require 'fetchers/service_instance_list_fetcher' +require 'messages/service_instances/service_instances_list_message' + +module VCAP::CloudController + RSpec.describe ServiceInstanceListFetcher do + let(:filters) { {} } + let(:message) { ServiceInstancesListMessage.new(filters) } + let(:fetcher) { ServiceInstanceListFetcher.new } + + describe '#fetch_all' do + let!(:service_instance_1) { ManagedServiceInstance.make(name: 'rabbitmq') } + let!(:service_instance_2) { ManagedServiceInstance.make(name: 'redis') } + + it 'returns a Sequel::Dataset' do + results = fetcher.fetch_all(message: message) + expect(results).to be_a(Sequel::Dataset) + end + + it 'includes all the V3 Service Instances' do + results = fetcher.fetch_all(message: message).all + expect(results.length).to eq 2 + expect(results).to include(service_instance_1, service_instance_2) + end + + context 'filter' do + context 'by service instance name' do + let(:filters) { { names: ['rabbitmq'] } } + + it 'only returns matching service instances' do + results = fetcher.fetch_all(message: message).all + expect(results).to match_array([service_instance_1]) + expect(results).not_to include(service_instance_2) + end + end + end + end + + describe '#fetch' do + let!(:service_instance_1) { ManagedServiceInstance.make(name: 'rabbitmq', space: space_1) } + let!(:service_instance_2) { ManagedServiceInstance.make(name: 'redis', space: space_1) } + let!(:service_instance_3) { ManagedServiceInstance.make(name: 'mysql', space: space_2) } + + let(:space_1) { Space.make } + let(:space_2) { Space.make } + + it 'returns all of the service instances in the specified space' do + results = fetcher.fetch(message: message, space_guids: [space_1.guid]).all + + expect(results).to match_array([service_instance_1, service_instance_2]) + end + + context 'filter' do + context 'by service instance name' do + let(:filters) { { names: ['rabbitmq'] } } + + it 'only returns matching service instances' do + results = fetcher.fetch(message: message, space_guids: [space_1.guid]).all + expect(results).to match_array([service_instance_1]) + end + end + + context 'by non-existent service instance name' do + let(:filters) { { names: ['made-up-name'] } } + + it 'returns no matching service instances' do + results = fetcher.fetch(message: message, space_guids: [space_1.guid]).all + expect(results).to be_empty + end + end + end + + context 'when service instances are shared' do + let(:shared_to_space) { Space.make } + + before do + service_instance_2.add_shared_space(shared_to_space) + service_instance_1.add_shared_space(shared_to_space) + end + + it 'returns all of the service instances shared into the specified space' do + results = fetcher.fetch(message: message, space_guids: [shared_to_space.guid]).all + expect(results).to match_array([service_instance_1, service_instance_2]) + end + end + + context 'when a space contains both shared and non-shared service instances' do + let(:shared_to_space) { Space.make } + let!(:service_instance_4) { ManagedServiceInstance.make(space: shared_to_space) } + + before do + service_instance_2.add_shared_space(shared_to_space) + service_instance_1.add_shared_space(shared_to_space) + end + + it 'returns all of the service instances shared into the specified space' do + results = fetcher.fetch(message: message, space_guids: [shared_to_space.guid]).all + expect(results).to match_array([service_instance_1, service_instance_2, service_instance_4]) + end + end + end + end +end