From 57b6f7fd657706c8f758eed131a5ae2d8c1e6d38 Mon Sep 17 00:00:00 2001 From: George Blue Date: Tue, 18 Aug 2020 11:00:21 +0100 Subject: [PATCH] v3(services): introduce service route binding endpoint [#174063444](https://www.pivotaltracker.com/story/show/174063444) --- .../v3/service_route_bindings_controller.rb | 63 +++++++++ .../service_route_binding_create_message.rb | 33 +++++ config/routes.rb | 5 + spec/request/service_route_bindings_spec.rb | 124 ++++++++++++++++++ ...rvice_route_binding_create_message_spec.rb | 64 +++++++++ 5 files changed, 289 insertions(+) create mode 100644 app/controllers/v3/service_route_bindings_controller.rb create mode 100644 app/messages/service_route_binding_create_message.rb create mode 100644 spec/request/service_route_bindings_spec.rb create mode 100644 spec/unit/messages/service_route_binding_create_message_spec.rb diff --git a/app/controllers/v3/service_route_bindings_controller.rb b/app/controllers/v3/service_route_bindings_controller.rb new file mode 100644 index 00000000000..d42802549c3 --- /dev/null +++ b/app/controllers/v3/service_route_bindings_controller.rb @@ -0,0 +1,63 @@ +require 'messages/service_route_binding_create_message' + +class ServiceRouteBindingsController < ApplicationController + def create + message = parse_request + + service_instance = fetch_service_instance(message.service_instance_guid) + route = fetch_route(message.route_guid) + check_space(service_instance, route) + + head :not_implemented + end + + private + + def parse_request + message = ServiceRouteBindingCreateMessage.new(hashed_params[:body]) + unprocessable!(message.errors.full_messages) unless message.valid? + message + end + + def fetch_service_instance(guid) + service_instance = VCAP::CloudController::ServiceInstance.first(guid: guid) + unless service_instance && can_read_space?(service_instance.space) + unprocessable_service_instance!(guid) + end + service_instance + end + + def fetch_route(guid) + route = VCAP::CloudController::Route.first(guid: guid) + unless route && can_read_space?(route.space) + unprocessable_route!(guid) + end + route + end + + def check_space(service_instance, route) + space = service_instance.space + space_mismatch! unless space == route.space + unauthorized! unless can_write_space?(space) + end + + def can_read_space?(space) + permission_queryer.can_read_from_space?(space.guid, space.organization_guid) + end + + def can_write_space?(space) + permission_queryer.can_write_to_space?(space.guid) + end + + def unprocessable_service_instance!(guid) + unprocessable!("The service instance could not be found: #{guid}") + end + + def unprocessable_route!(guid) + unprocessable!("The route could not be found: #{guid}") + end + + def space_mismatch! + unprocessable!('The service instance and the route are in different spaces.') + end +end diff --git a/app/messages/service_route_binding_create_message.rb b/app/messages/service_route_binding_create_message.rb new file mode 100644 index 00000000000..793854dfbdf --- /dev/null +++ b/app/messages/service_route_binding_create_message.rb @@ -0,0 +1,33 @@ +require 'messages/base_message' +require 'utils/hash_utils' + +module VCAP::CloudController + class ServiceRouteBindingCreateMessage < BaseMessage + register_allowed_keys [:relationships] + + validates_with NoAdditionalKeysValidator, RelationshipValidator + + delegate :route_guid, :service_instance_guid, to: :relationships_message + + def relationships_message + @relationships_message ||= Relationships.new(relationships.deep_symbolize_keys) + end + + class Relationships < BaseMessage + register_allowed_keys [:service_instance, :route] + + def route_guid + HashUtils.dig(route, :data, :guid) + end + + def service_instance_guid + HashUtils.dig(service_instance, :data, :guid) + end + + validates_with NoAdditionalKeysValidator + + validates :service_instance, presence: true, allow_nil: false, to_one_relationship: true + validates :route, presence: true, allow_nil: false, to_one_relationship: true + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 6db1cd5ed9d..19dd48ec174 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -189,6 +189,11 @@ param: :guid, only: [:show, :index] + # service_route_bindings + resources :service_route_bindings, + param: :guid, + only: [:create] + # service_brokers get '/service_brokers', to: 'service_brokers#index' get '/service_brokers/:guid', to: 'service_brokers#show' diff --git a/spec/request/service_route_bindings_spec.rb b/spec/request/service_route_bindings_spec.rb new file mode 100644 index 00000000000..332043723c7 --- /dev/null +++ b/spec/request/service_route_bindings_spec.rb @@ -0,0 +1,124 @@ +require 'spec_helper' +require 'request_spec_shared_examples' + +RSpec.describe 'v3 service route bindings' do + describe 'POST /v3/service_route_bindings' do + let(:api_call) { ->(user_headers) { post '/v3/service_route_bindings', request.to_json, user_headers } } + + let(:service_instance) { VCAP::CloudController::UserProvidedServiceInstance.make(space: space) } + let(:route) { VCAP::CloudController::Route.make(space: space) } + let(:request) do + { + relationships: { + service_instance: { + data: { + guid: service_instance.guid + } + }, + route: { + data: { + guid: route.guid + } + } + } + } + end + + describe 'permissions' do + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS do + let(:expected_codes_and_responses) do + Hash.new(code: 403).tap do |h| + h['admin'] = { code: 501 } + h['space_developer'] = { code: 501 } + + h['no_role'] = { code: 422 } + h['org_auditor'] = { code: 422 } + h['org_billing_manager'] = { code: 422 } + end + end + end + end + + describe 'errors' do + context 'invalid body' do + let(:request) do + { foo: 'bar' } + end + + it 'fails with a 422 unprocessable' do + api_call.call(space_dev_headers) + + expect(last_response).to have_status_code(422) + expect(parsed_response['errors']).to include( + include({ + 'detail' => "Unknown field(s): 'foo', Relationships 'relationships' is not an object", + 'title' => 'CF-UnprocessableEntity', + 'code' => 10008, + }) + ) + end + end + + context 'cannot read service instance' do + let(:service_instance) { VCAP::CloudController::UserProvidedServiceInstance.make } + + it 'fails with a 422 unprocessable' do + api_call.call(space_dev_headers) + + expect(last_response).to have_status_code(422) + expect(parsed_response['errors']).to include( + include({ + 'detail' => "The service instance could not be found: #{service_instance.guid}", + 'title' => 'CF-UnprocessableEntity', + 'code' => 10008, + }) + ) + end + end + + context 'cannot read route' do + let(:route) { VCAP::CloudController::Route.make } + + it 'fails with a 422 unprocessable' do + api_call.call(space_dev_headers) + + expect(last_response).to have_status_code(422) + expect(parsed_response['errors']).to include( + include({ + 'detail' => "The route could not be found: #{route.guid}", + 'title' => 'CF-UnprocessableEntity', + 'code' => 10008, + }) + ) + end + end + + context 'route and service instance in different spaces' do + let(:route) { VCAP::CloudController::Route.make } + + it 'fails with a 422 unprocessable' do + api_call.call(admin_headers) + + expect(last_response).to have_status_code(422) + expect(parsed_response['errors']).to include( + include({ + 'detail' => 'The service instance and the route are in different spaces.', + 'title' => 'CF-UnprocessableEntity', + 'code' => 10008, + }) + ) + end + end + end + end + + let(:user) { VCAP::CloudController::User.make } + let(:org) { VCAP::CloudController::Organization.make } + let(:space) { VCAP::CloudController::Space.make(organization: org) } + + let(:space_dev_headers) do + org.add_user(user) + space.add_developer(user) + headers_for(user) + end +end diff --git a/spec/unit/messages/service_route_binding_create_message_spec.rb b/spec/unit/messages/service_route_binding_create_message_spec.rb new file mode 100644 index 00000000000..f43d7386f09 --- /dev/null +++ b/spec/unit/messages/service_route_binding_create_message_spec.rb @@ -0,0 +1,64 @@ +require 'lightweight_spec_helper' +require 'messages/service_route_binding_create_message' + +module VCAP::CloudController + RSpec.describe ServiceRouteBindingCreateMessage do + let(:body) do + { + relationships: { + service_instance: { + data: { guid: 'service-instance-guid' } + }, + route: { + data: { guid: 'route-guid' } + } + } + } + end + + let(:message) { described_class.new(body) } + + it 'accepts the allowed keys' do + expect(message).to be_valid + expect(message.requested?(:relationships)).to be_truthy + end + + it 'builds the right message' do + expect(message.service_instance_guid).to eq('service-instance-guid') + expect(message.route_guid).to eq('route-guid') + end + + describe 'validations' do + it 'is invalid when there are unknown keys' do + body[:parameters] = 'foo' + expect(message).to_not be_valid + expect(message.errors.full_messages).to include("Unknown field(s): 'parameters'") + end + + describe 'service instance relationship' do + it 'fails when not present' do + body[:relationships][:service_instance] = nil + message.valid? + expect(message).to_not be_valid + expect(message.errors[:relationships]).to include( + "Service instance can't be blank", + /Service instance must be structured like this.*/ + ) + expect(message.errors[:relationships].count).to eq(2) + end + end + + describe 'route relationship' do + it 'fails when not present' do + body[:relationships][:route] = nil + expect(message).to_not be_valid + expect(message.errors[:relationships]).to include( + "Route can't be blank", + /Route must be structured like this.*/, + ) + expect(message.errors[:relationships].count).to eq(2) + end + end + end + end +end