From 2563d10a7158f93ae96c0e9c7b2fe322e2fbd181 Mon Sep 17 00:00:00 2001 From: Sven Krieger <37476281+svkrieger@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:04:36 +0200 Subject: [PATCH 1/2] Add destination permission tests --- spec/request/route_destinations_spec.rb | 124 ++++++++++++++++++++---- 1 file changed, 103 insertions(+), 21 deletions(-) diff --git a/spec/request/route_destinations_spec.rb b/spec/request/route_destinations_spec.rb index 9c73dc2bb5e..71ce117f4bb 100644 --- a/spec/request/route_destinations_spec.rb +++ b/spec/request/route_destinations_spec.rb @@ -426,6 +426,14 @@ expect(last_response.status).to eq(422) expect(last_response).to have_error_message("Routes destinations must be in either the route's space or the route's shared spaces") end + + it 'returns 422 even when the user has space_developer in both the route space and the app space' do + # Having write access to both spaces is not enough — the route must be explicitly shared + set_current_user_as_role(user: user, role: 'space_developer', org: space.organization, space: space) + post "/v3/routes/#{route.guid}/destinations", params.to_json, user_header + expect(last_response.status).to eq(422) + expect(last_response).to have_error_message("Routes destinations must be in either the route's space or the route's shared spaces") + end end context 'when the app does not exist' do @@ -583,6 +591,34 @@ end end end + + context "when the app is in the route's shared space" do + let(:shared_app_space) { create(:space) } + let(:shared_app) { create(:app_model, space: shared_app_space) } + let(:api_call) do + ->(user_headers) do + params = { destinations: [{ app: { guid: shared_app.guid, process: { type: 'web' } } }] } + post "/v3/routes/#{route.guid}/destinations", params.to_json, user_headers + end + end + let(:expected_codes_and_responses) do + h = Hash.new({ code: 403, errors: CF_NOT_AUTHORIZED }.freeze) + h['admin'] = { code: 200 } + h['space_developer'] = { code: 200 } + h['space_supporter'] = { code: 200 } + h['org_billing_manager'] = { code: 404 } + h['no_role'] = { code: 404 } + h + end + + before do + route.add_shared_space shared_app_space + shared_app_space.organization.add_user(user) + shared_app_space.add_developer(user) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end end describe 'PATCH /v3/routes/:guid/destinations' do @@ -1175,32 +1211,31 @@ end context "when the app is in the route's shared space" do - let(:app_model) { create(:app_model) } - let(:params) do - { - destinations: [ - { - app: { - guid: app_model.guid, - process: { - type: 'web' - } - } - } - ] - } + let(:shared_app_space) { create(:space) } + let(:shared_app) { create(:app_model, space: shared_app_space) } + let(:api_call) do + ->(user_headers) do + params = { destinations: [{ app: { guid: shared_app.guid, process: { type: 'web' } } }] } + patch "/v3/routes/#{route.guid}/destinations", params.to_json, user_headers + end + end + let(:expected_codes_and_responses) do + h = Hash.new({ code: 403, errors: CF_NOT_AUTHORIZED }.freeze) + h['admin'] = { code: 200 } + h['space_developer'] = { code: 200 } + h['space_supporter'] = { code: 200 } + h['org_billing_manager'] = { code: 404 } + h['no_role'] = { code: 404 } + h end before do - route.add_shared_space app_model.space - set_current_user_as_role(user: user, role: 'space_developer', org: space.organization, space: space) - set_current_user_as_role(user: user, role: 'space_developer', org: app_model.space.organization, space: app_model.space) + route.add_shared_space shared_app_space + shared_app_space.organization.add_user(user) + shared_app_space.add_developer(user) end - it 'succeeds' do - patch "/v3/routes/#{route.guid}/destinations", params.to_json, user_header - expect(last_response.status).to eq(200) - end + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS end end @@ -1298,6 +1333,29 @@ end end + context "when the destination app is in the route's shared space" do + let(:shared_app_space) { create(:space) } + let(:shared_app) { create(:app_model, space: shared_app_space) } + let!(:shared_destination) do + create(:route_mapping_model, app: shared_app, route: route, process_type: 'web', + app_port: VCAP::CloudController::ProcessModel::DEFAULT_HTTP_PORT, weight: nil) + end + let(:api_call) { ->(user_headers) { patch "/v3/routes/#{route.guid}/destinations/#{shared_destination.guid}", { protocol: 'http1' }.to_json, user_headers } } + let(:expected_codes_and_responses) do + h = Hash.new({ code: 403, errors: CF_NOT_AUTHORIZED }.freeze) + h['admin'] = { code: 200 } + h['space_developer'] = { code: 200 } + h['space_supporter'] = { code: 200 } + h['org_billing_manager'] = { code: 404 } + h['no_role'] = { code: 404 } + h + end + + before { route.add_shared_space shared_app_space } + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end + context 'when the route has a protocol of tcp' do let(:routing_api_client) { double('routing_api_client', router_group:) } let(:router_group) { double('router_group', type: 'tcp', guid: 'router-group-guid') } @@ -1458,6 +1516,30 @@ end end + context "when the destination app is in the route's shared space" do + let(:shared_app_space) { create(:space) } + let(:shared_app) { create(:app_model, space: shared_app_space) } + let!(:shared_destination) do + create(:route_mapping_model, app: shared_app, route: route, process_type: 'web', + app_port: VCAP::CloudController::ProcessModel::DEFAULT_HTTP_PORT, weight: nil) + end + let(:api_call) { ->(user_headers) { delete "/v3/routes/#{route.guid}/destinations/#{shared_destination.guid}", nil, user_headers } } + let(:db_check) { -> {} } + let(:expected_codes_and_responses) do + h = Hash.new({ code: 403, errors: CF_NOT_AUTHORIZED }.freeze) + h['admin'] = { code: 204 } + h['space_developer'] = { code: 204 } + h['space_supporter'] = { code: 204 } + h['org_billing_manager'] = { code: 404 } + h['no_role'] = { code: 404 } + h + end + + before { route.add_shared_space shared_app_space } + + it_behaves_like 'permissions for delete endpoint', ALL_PERMISSIONS + end + context 'when there is an existing weighted destination' do let!(:existing_destination) { create(:route_mapping_model, app: app_model, process_type: 'something', route: route, weight: 10) } From 972b99cfdfa82c78ef788d934d3eb0cc5faebeff Mon Sep 17 00:00:00 2001 From: Sven Krieger <37476281+svkrieger@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:38:02 +0200 Subject: [PATCH 2/2] Add destination permission tests for app's space --- spec/request/route_destinations_spec.rb | 127 +++++++++++++++++++++++- 1 file changed, 125 insertions(+), 2 deletions(-) diff --git a/spec/request/route_destinations_spec.rb b/spec/request/route_destinations_spec.rb index 71ce117f4bb..02a5ccf014d 100644 --- a/spec/request/route_destinations_spec.rb +++ b/spec/request/route_destinations_spec.rb @@ -596,7 +596,7 @@ let(:shared_app_space) { create(:space) } let(:shared_app) { create(:app_model, space: shared_app_space) } let(:api_call) do - ->(user_headers) do + lambda do |user_headers| params = { destinations: [{ app: { guid: shared_app.guid, process: { type: 'web' } } }] } post "/v3/routes/#{route.guid}/destinations", params.to_json, user_headers end @@ -619,6 +619,41 @@ it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS end + + context "when the app is in the route's shared space - roles in the shared space" do + let(:route_space) { create(:space) } + let(:route) { create(:route, space: route_space) } + let(:shared_app_space) { create(:space) } + let(:org) { shared_app_space.organization } + let(:space) { shared_app_space } + let(:shared_app) { create(:app_model, space: shared_app_space) } + let(:api_call) do + lambda do |user_headers| + params = { destinations: [{ app: { guid: shared_app.guid, process: { type: 'web' } } }] } + post "/v3/routes/#{route.guid}/destinations", params.to_json, user_headers + end + end + let(:expected_codes_and_responses) do + h = Hash.new({ code: 422 }.freeze) + h['admin'] = { code: 200 } + h['space_developer'] = { code: 200 } + h['space_manager'] = { code: 200 } + h['space_auditor'] = { code: 200 } + h['space_supporter'] = { code: 200 } + h['org_manager'] = { code: 200 } + h['admin_read_only'] = { code: 200 } + h['global_auditor'] = { code: 200 } + h + end + + before do + route.add_shared_space shared_app_space + route_space.organization.add_user(user) + route_space.add_developer(user) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end end describe 'PATCH /v3/routes/:guid/destinations' do @@ -1214,7 +1249,7 @@ let(:shared_app_space) { create(:space) } let(:shared_app) { create(:app_model, space: shared_app_space) } let(:api_call) do - ->(user_headers) do + lambda do |user_headers| params = { destinations: [{ app: { guid: shared_app.guid, process: { type: 'web' } } }] } patch "/v3/routes/#{route.guid}/destinations", params.to_json, user_headers end @@ -1237,6 +1272,41 @@ it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS end + + context "when the app is in the route's shared space - roles in the shared space" do + let(:route_space) { create(:space) } + let(:route) { create(:route, space: route_space) } + let(:shared_app_space) { create(:space) } + let(:org) { shared_app_space.organization } + let(:space) { shared_app_space } + let(:shared_app) { create(:app_model, space: shared_app_space) } + let(:api_call) do + lambda do |user_headers| + params = { destinations: [{ app: { guid: shared_app.guid, process: { type: 'web' } } }] } + patch "/v3/routes/#{route.guid}/destinations", params.to_json, user_headers + end + end + let(:expected_codes_and_responses) do + h = Hash.new({ code: 422 }.freeze) + h['admin'] = { code: 200 } + h['space_developer'] = { code: 200 } + h['space_manager'] = { code: 200 } + h['space_auditor'] = { code: 200 } + h['space_supporter'] = { code: 200 } + h['org_manager'] = { code: 200 } + h['admin_read_only'] = { code: 200 } + h['global_auditor'] = { code: 200 } + h + end + + before do + route.add_shared_space shared_app_space + route_space.organization.add_user(user) + route_space.add_developer(user) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end end describe 'UPDATE /v3/routes/:guid/destinations/:destination_guid' do @@ -1356,6 +1426,32 @@ it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS end + context "when the destination app is in the route's shared space - roles in the shared space" do + let(:route_space) { create(:space) } + let(:route) { create(:route, space: route_space) } + let(:shared_app_space) { create(:space) } + let(:org) { shared_app_space.organization } + let(:space) { shared_app_space } + let(:shared_app) { create(:app_model, space: shared_app_space) } + let!(:shared_destination) do + create(:route_mapping_model, app: shared_app, route: route, process_type: 'web', + app_port: VCAP::CloudController::ProcessModel::DEFAULT_HTTP_PORT, weight: nil) + end + let(:api_call) { ->(user_headers) { patch "/v3/routes/#{route.guid}/destinations/#{shared_destination.guid}", { protocol: 'http1' }.to_json, user_headers } } + let(:expected_codes_and_responses) do + h = Hash.new({ code: 200 }.freeze) + h + end + + before do + route.add_shared_space shared_app_space + route_space.organization.add_user(user) + route_space.add_developer(user) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end + context 'when the route has a protocol of tcp' do let(:routing_api_client) { double('routing_api_client', router_group:) } let(:router_group) { double('router_group', type: 'tcp', guid: 'router-group-guid') } @@ -1540,6 +1636,33 @@ it_behaves_like 'permissions for delete endpoint', ALL_PERMISSIONS end + context "when the destination app is in the route's shared space - roles in the shared space" do + let(:route_space) { create(:space) } + let(:route) { create(:route, space: route_space) } + let(:shared_app_space) { create(:space) } + let(:org) { shared_app_space.organization } + let(:space) { shared_app_space } + let(:shared_app) { create(:app_model, space: shared_app_space) } + let!(:shared_destination) do + create(:route_mapping_model, app: shared_app, route: route, process_type: 'web', + app_port: VCAP::CloudController::ProcessModel::DEFAULT_HTTP_PORT, weight: nil) + end + let(:api_call) { ->(user_headers) { delete "/v3/routes/#{route.guid}/destinations/#{shared_destination.guid}", nil, user_headers } } + let(:db_check) { -> {} } + let(:expected_codes_and_responses) do + h = Hash.new({ code: 204 }.freeze) + h + end + + before do + route.add_shared_space shared_app_space + route_space.organization.add_user(user) + route_space.add_developer(user) + end + + it_behaves_like 'permissions for delete endpoint', ALL_PERMISSIONS + end + context 'when there is an existing weighted destination' do let!(:existing_destination) { create(:route_mapping_model, app: app_model, process_type: 'something', route: route, weight: 10) }