Skip to content

Commit e641586

Browse files
committed
Implement include=route for /v3/access_rules endpoint
Add support for including route resources when listing access rules via the ?include=route query parameter. Changes: - Create IncludeAccessRuleRouteDecorator to handle route inclusion - Wire up decorator in AccessRulesController - Add comprehensive request specs for include=route - Test single/multiple routes, uniqueness, and combining with selector_resource - Follow existing CAPI decorator patterns for resource inclusion The decorator fetches and presents Route resources referenced by the access rules, adding them to the 'included' section of the response.
1 parent 8903be9 commit e641586

3 files changed

Lines changed: 132 additions & 23 deletions

File tree

app/controllers/v3/access_rules_controller.rb

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require 'messages/access_rules_list_message'
44
require 'presenters/v3/access_rule_presenter'
55
require 'decorators/include_access_rule_selector_resource_decorator'
6+
require 'decorators/include_access_rule_route_decorator'
67

78
class AccessRulesController < ApplicationController
89
def index
@@ -13,6 +14,7 @@ def index
1314

1415
decorators = []
1516
decorators << IncludeAccessRuleSelectorResourceDecorator if IncludeAccessRuleSelectorResourceDecorator.match?(message.include)
17+
decorators << IncludeAccessRuleRouteDecorator if IncludeAccessRuleRouteDecorator.match?(message.include)
1618

1719
render status: :ok, json: Presenters::V3::PaginatedListPresenter.new(
1820
presenter: Presenters::V3::AccessRulePresenter,
@@ -42,27 +44,17 @@ def create
4244
unauthorized! unless permission_queryer.can_write_to_active_space?(route.space.id)
4345
suspended! unless permission_queryer.is_space_active?(route.space.id)
4446

45-
unless route.domain.enforce_access_rules
46-
unprocessable!("Cannot create access rules for route '#{route.guid}': the route's domain does not have enforce_access_rules enabled.")
47-
end
47+
unprocessable!("Cannot create access rules for route '#{route.guid}': the route's domain does not have enforce_access_rules enabled.") unless route.domain.enforce_access_rules
4848

4949
# Enforce cf:any exclusivity: if route already has a cf:any rule, reject new rules;
5050
# if new rule is cf:any, reject if route already has any rules.
5151
existing_selectors = route.access_rules.map(&:selector)
52-
if message.selector == 'cf:any' && existing_selectors.any?
53-
unprocessable!("Cannot add 'cf:any' selector when other access rules already exist for this route.")
54-
end
55-
if existing_selectors.include?('cf:any') && message.selector != 'cf:any'
56-
unprocessable!("Cannot add selector '#{message.selector}': route already has a 'cf:any' rule.")
57-
end
52+
unprocessable!("Cannot add 'cf:any' selector when other access rules already exist for this route.") if message.selector == 'cf:any' && existing_selectors.any?
53+
unprocessable!("Cannot add selector '#{message.selector}': route already has a 'cf:any' rule.") if existing_selectors.include?('cf:any') && message.selector != 'cf:any'
5854

5955
# Uniqueness: name and selector must be unique per route
60-
if route.access_rules.any? { |r| r.name == message.name }
61-
unprocessable!("An access rule with name '#{message.name}' already exists for this route.")
62-
end
63-
if existing_selectors.include?(message.selector)
64-
unprocessable!("An access rule with selector '#{message.selector}' already exists for this route.")
65-
end
56+
unprocessable!("An access rule with name '#{message.name}' already exists for this route.") if route.access_rules.any? { |r| r.name == message.name }
57+
unprocessable!("An access rule with selector '#{message.selector}' already exists for this route.") if existing_selectors.include?(message.selector)
6658

6759
access_rule = VCAP::CloudController::RouteAccessRule.new(
6860
guid: SecureRandom.uuid,
@@ -123,16 +115,16 @@ def build_dataset(message)
123115

124116
if message.requested?(:route_guids)
125117
dataset = dataset.
126-
join(:routes, id: :route_id).
127-
where(routes__guid: message.route_guids).
128-
select_all(:route_access_rules)
118+
join(:routes, id: :route_id).
119+
where(routes__guid: message.route_guids).
120+
select_all(:route_access_rules)
129121
end
130122

131123
if message.requested?(:space_guids)
132124
dataset = dataset.
133-
join(:routes, id: :route_id).
134-
where(routes__space_id: VCAP::CloudController::Space.where(guid: message.space_guids).select(:id)).
135-
select_all(:route_access_rules)
125+
join(:routes, id: :route_id).
126+
where(routes__space_id: VCAP::CloudController::Space.where(guid: message.space_guids).select(:id)).
127+
select_all(:route_access_rules)
136128
end
137129

138130
dataset = dataset.where(name: message.names) if message.requested?(:names)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
module VCAP::CloudController
2+
class IncludeAccessRuleRouteDecorator
3+
# Handles `?include=route` for GET /v3/access_rules
4+
# Includes the route resources associated with the access rules
5+
6+
def self.match?(include_params)
7+
include_params&.include?('route')
8+
end
9+
10+
def self.decorate(hash, access_rules)
11+
hash[:included] ||= {}
12+
13+
# Collect all unique route IDs from access rules
14+
route_ids = access_rules.map(&:route_id).uniq
15+
16+
# Fetch routes with their associations
17+
routes = VCAP::CloudController::Route.where(id: route_ids).
18+
order(:created_at, :guid).
19+
eager(VCAP::CloudController::Presenters::V3::RoutePresenter.associated_resources).all
20+
21+
# Present routes
22+
hash[:included][:routes] = routes.map { |route| VCAP::CloudController::Presenters::V3::RoutePresenter.new(route).to_hash }
23+
24+
hash
25+
end
26+
end
27+
end

spec/request/access_rules_spec.rb

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def expected_rule_json(rule)
133133
}.to_json, admin_header
134134

135135
expect(last_response.status).to eq(422)
136-
expect(last_response.body).to include("cf:any")
136+
expect(last_response.body).to include('cf:any')
137137
end
138138
end
139139

@@ -155,7 +155,7 @@ def expected_rule_json(rule)
155155
}.to_json, admin_header
156156

157157
expect(last_response.status).to eq(422)
158-
expect(last_response.body).to include("cf:any")
158+
expect(last_response.body).to include('cf:any')
159159
end
160160
end
161161

@@ -481,6 +481,96 @@ def expected_rule_json(rule)
481481
# Should succeed without error even with cf:any selector
482482
end
483483
end
484+
485+
context 'with include=route' do
486+
let(:route2) { VCAP::CloudController::Route.make(space: space, domain: mtls_domain) }
487+
488+
let!(:rule_on_route1) do
489+
VCAP::CloudController::RouteAccessRule.create(
490+
guid: SecureRandom.uuid,
491+
name: 'rule-on-route1',
492+
selector: 'cf:any',
493+
route_id: mtls_route.id
494+
)
495+
end
496+
497+
let!(:rule_on_route2) do
498+
VCAP::CloudController::RouteAccessRule.create(
499+
guid: SecureRandom.uuid,
500+
name: 'rule-on-route2',
501+
selector: "cf:app:#{valid_uuid}",
502+
route_id: route2.id
503+
)
504+
end
505+
506+
it 'includes route resources' do
507+
get '/v3/access_rules?include=route', nil, admin_header
508+
509+
expect(last_response.status).to eq(200)
510+
parsed = Oj.load(last_response.body)
511+
512+
# Check included structure
513+
expect(parsed['included']).to be_a(Hash)
514+
expect(parsed['included']['routes']).to be_an(Array)
515+
expect(parsed['included']['routes'].length).to be >= 2
516+
517+
# Check routes are included with full details
518+
route1_included = parsed['included']['routes'].find { |r| r['guid'] == mtls_route.guid }
519+
expect(route1_included).to be_present
520+
expect(route1_included['guid']).to eq(mtls_route.guid)
521+
expect(route1_included['url']).to be_present
522+
523+
route2_included = parsed['included']['routes'].find { |r| r['guid'] == route2.guid }
524+
expect(route2_included).to be_present
525+
expect(route2_included['guid']).to eq(route2.guid)
526+
end
527+
528+
it 'includes only unique routes when multiple rules reference the same route' do
529+
# Create another rule on the same route
530+
VCAP::CloudController::RouteAccessRule.create(
531+
guid: SecureRandom.uuid,
532+
name: 'another-rule-on-route1',
533+
selector: "cf:app:#{valid_uuid}",
534+
route_id: mtls_route.id
535+
)
536+
537+
get '/v3/access_rules?include=route', nil, admin_header
538+
539+
expect(last_response.status).to eq(200)
540+
parsed = Oj.load(last_response.body)
541+
542+
# Route should appear only once
543+
route_count = parsed['included']['routes'].count { |r| r['guid'] == mtls_route.guid }
544+
expect(route_count).to eq(1)
545+
end
546+
547+
it 'combines include=route with include=selector_resource' do
548+
app = VCAP::CloudController::AppModel.make(space: space, name: 'test-app')
549+
VCAP::CloudController::RouteAccessRule.create(
550+
guid: SecureRandom.uuid,
551+
name: 'combined-rule',
552+
selector: "cf:app:#{app.guid}",
553+
route_id: mtls_route.id
554+
)
555+
556+
get '/v3/access_rules?include=route,selector_resource', nil, admin_header
557+
558+
expect(last_response.status).to eq(200)
559+
parsed = Oj.load(last_response.body)
560+
561+
# Both routes and selector resources should be included
562+
expect(parsed['included']['routes']).to be_an(Array)
563+
expect(parsed['included']['apps']).to be_an(Array)
564+
565+
# Verify route is present
566+
route_included = parsed['included']['routes'].find { |r| r['guid'] == mtls_route.guid }
567+
expect(route_included).to be_present
568+
569+
# Verify app is present
570+
app_included = parsed['included']['apps'].find { |a| a['guid'] == app.guid }
571+
expect(app_included).to be_present
572+
end
573+
end
484574
end
485575

486576
describe 'DELETE /v3/access_rules/:guid' do

0 commit comments

Comments
 (0)