Skip to content

Commit c0b1b01

Browse files
committed
Rebrand: access rules → route policies, selector → source
Complete terminology shift for identity-aware routing RFC: - Access Rules → Route Policies (API, models, tables) - Selector → Source (field names, query params) - Domain fields: enforce_access_rules → enforce_route_policies - Domain fields: access_rules_scope → route_policies_scope - Route options: access_scope → route_policy_scope - Route options: access_rules → route_policy_sources Aligns with existing CF 'network policies' terminology and C2C network policy convention (source → destination). Database migrations replaced (no production DBs affected). 67 files changed, 602 insertions(+), 600 deletions(-)
1 parent 273a1db commit c0b1b01

67 files changed

Lines changed: 601 additions & 599 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,34 @@
11
module VCAP::CloudController
2-
class AccessRuleAccess < BaseAccess
3-
# Space Developer of the route's space can manage access rules.
2+
class RoutePolicyAccess < BaseAccess
3+
# Space Developer of the route's space can manage route policies.
44
# No bilateral requirement — destination-controlled auth only.
55

6-
def create?(access_rule, _params=nil)
6+
def create?(route_policy, _params=nil)
77
return true if admin_user?
88

9-
route = access_rule.route
9+
route = route_policy.route
1010
return false unless route
1111

1212
space = route.space
1313
context.user_email && context.user.is_a?(User) &&
1414
space.developers.include?(context.user)
1515
end
1616

17-
def read?(access_rule)
17+
def read?(route_policy)
1818
return true if admin_user? || admin_read_only_user? || global_auditor?
1919

20-
route = access_rule.route
20+
route = route_policy.route
2121
return false unless route
2222

23-
object_is_visible_to_user?(access_rule, context.user)
23+
object_is_visible_to_user?(route_policy, context.user)
2424
end
2525

26-
def update?(access_rule, _params=nil)
27-
create?(access_rule)
26+
def update?(route_policy, _params=nil)
27+
create?(route_policy)
2828
end
2929

30-
def delete?(access_rule)
31-
create?(access_rule)
30+
def delete?(route_policy)
31+
create?(route_policy)
3232
end
3333

3434
def index?(_object_class, _params=nil)

app/actions/domain_create.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ def create(message:, shared_organizations: [])
2121
end
2222

2323
domain.router_group_guid = message.router_group_guid
24-
domain.enforce_access_rules = message.enforce_access_rules || false
25-
domain.access_rules_scope = message.access_rules_scope
24+
domain.enforce_route_policies = message.enforce_route_policies || false
25+
domain.route_policies_scope = message.route_policies_scope
2626

2727
Domain.db.transaction do
2828
domain.save

app/controllers/v3/access_rules_controller.rb

Lines changed: 0 additions & 166 deletions
This file was deleted.
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
require 'messages/route_policy_create_message'
2+
require 'messages/route_policy_update_message'
3+
require 'messages/route_policies_list_message'
4+
require 'presenters/v3/route_policy_presenter'
5+
require 'decorators/include_route_policy_source_decorator'
6+
require 'decorators/include_route_policy_route_decorator'
7+
8+
class RoutePoliciesController < ApplicationController
9+
def index
10+
message = RoutePoliciesListMessage.from_params(query_params)
11+
invalid_param!(message.errors.full_messages) unless message.valid?
12+
13+
dataset = build_dataset(message)
14+
15+
decorators = []
16+
decorators << IncludeRoutePolicySourceDecorator if IncludeRoutePolicySourceDecorator.match?(message.include)
17+
decorators << IncludeRoutePolicyRouteDecorator if IncludeRoutePolicyRouteDecorator.match?(message.include)
18+
19+
render status: :ok, json: Presenters::V3::PaginatedListPresenter.new(
20+
presenter: Presenters::V3::RoutePolicyPresenter,
21+
paginated_result: SequelPaginator.new.get_page(dataset, message.try(:pagination_options)),
22+
path: '/v3/route_policies',
23+
message: message,
24+
decorators: decorators
25+
)
26+
end
27+
28+
def show
29+
route_policy = VCAP::CloudController::RoutePolicy.find(guid: hashed_params[:guid])
30+
resource_not_found!(:route_policy) unless route_policy
31+
32+
route = route_policy.route
33+
resource_not_found!(:route_policy) unless route && permission_queryer.can_read_from_space?(route.space.id, route.space.organization_id)
34+
35+
render status: :ok, json: Presenters::V3::RoutePolicyPresenter.new(route_policy)
36+
end
37+
38+
def create
39+
message = RoutePolicyCreateMessage.new(hashed_params[:body])
40+
unprocessable!(message.errors.full_messages) unless message.valid?
41+
42+
route = find_and_authorize_route(message.route_guid)
43+
validate_route_domain(route)
44+
45+
route_policy = VCAP::CloudController::RoutePolicy.db.transaction do
46+
# Lock existing route policies for this route to prevent concurrent inserts
47+
# from violating cf:any exclusivity or uniqueness constraints
48+
VCAP::CloudController::RoutePolicy.where(route_id: route.id).for_update.all
49+
50+
validate_source_exclusivity(route, message.source)
51+
52+
policy = VCAP::CloudController::RoutePolicy.new(
53+
guid: SecureRandom.uuid,
54+
source: message.source,
55+
route_id: route.id,
56+
created_at: Time.now.utc,
57+
updated_at: Time.now.utc
58+
)
59+
policy.save
60+
policy
61+
end
62+
63+
render status: :created, json: Presenters::V3::RoutePolicyPresenter.new(route_policy)
64+
rescue Sequel::UniqueConstraintViolation
65+
unprocessable!("A route policy with source '#{message.source}' already exists for this route.")
66+
end
67+
68+
def update
69+
route_policy = VCAP::CloudController::RoutePolicy.find(guid: hashed_params[:guid])
70+
resource_not_found!(:route_policy) unless route_policy
71+
72+
route = route_policy.route
73+
resource_not_found!(:route_policy) unless route && permission_queryer.can_read_from_space?(route.space.id, route.space.organization_id)
74+
unauthorized! unless permission_queryer.can_write_to_active_space?(route.space.id)
75+
suspended! unless permission_queryer.is_space_active?(route.space.id)
76+
77+
message = RoutePolicyUpdateMessage.new(hashed_params[:body])
78+
unprocessable!(message.errors.full_messages) unless message.valid?
79+
80+
VCAP::CloudController::MetadataUpdate.update(route_policy, message)
81+
82+
render status: :ok, json: Presenters::V3::RoutePolicyPresenter.new(route_policy.reload)
83+
end
84+
85+
def destroy
86+
route_policy = VCAP::CloudController::RoutePolicy.find(guid: hashed_params[:guid])
87+
resource_not_found!(:route_policy) unless route_policy
88+
89+
route = route_policy.route
90+
resource_not_found!(:route_policy) unless route && permission_queryer.can_read_from_space?(route.space.id, route.space.organization_id)
91+
unauthorized! unless permission_queryer.can_write_to_active_space?(route.space.id)
92+
suspended! unless permission_queryer.is_space_active?(route.space.id)
93+
94+
route_policy.destroy
95+
head :no_content
96+
end
97+
98+
private
99+
100+
def find_and_authorize_route(route_guid)
101+
route = VCAP::CloudController::Route.find(guid: route_guid)
102+
resource_not_found!(:route) unless route && permission_queryer.can_read_from_space?(route.space.id, route.space.organization_id)
103+
unauthorized! unless permission_queryer.can_write_to_active_space?(route.space.id)
104+
suspended! unless permission_queryer.is_space_active?(route.space.id)
105+
route
106+
end
107+
108+
def validate_route_domain(route)
109+
if route.domain.internal?
110+
unprocessable!('Cannot create route policies for routes on internal domains. Internal routes use container-to-container networking and bypass GoRouter.')
111+
end
112+
return if route.domain.enforce_route_policies
113+
114+
unprocessable!("Cannot create route policies for route '#{route.guid}': the route's domain does not have enforce_route_policies enabled.")
115+
end
116+
117+
def validate_source_exclusivity(route, source)
118+
existing_sources = route.route_policies.map(&:source)
119+
120+
# Enforce cf:any exclusivity: if route already has a cf:any policy, reject new policies;
121+
# if new policy is cf:any, reject if route already has any policies.
122+
unprocessable!("Cannot add 'cf:any' source when other route policies already exist for this route.") if source == 'cf:any' && existing_sources.any?
123+
unprocessable!("Cannot add source '#{source}': route already has a 'cf:any' policy.") if existing_sources.include?('cf:any') && source != 'cf:any'
124+
125+
# Uniqueness: source must be unique per route
126+
unprocessable!("A route policy with source '#{source}' already exists for this route.") if existing_sources.include?(source)
127+
end
128+
129+
def build_dataset(message)
130+
dataset = VCAP::CloudController::RoutePolicy.dataset
131+
132+
if permission_queryer.can_read_globally?
133+
readable_route_ids = VCAP::CloudController::Route.select(:id)
134+
else
135+
readable_space_ids = permission_queryer.readable_space_scoped_spaces_query.select(:id)
136+
readable_route_ids = VCAP::CloudController::Route.where(space_id: readable_space_ids).select(:id)
137+
end
138+
139+
dataset = dataset.where(route_id: readable_route_ids)
140+
141+
# Join routes at most once when either route_guids or space_guids is requested
142+
if message.requested?(:route_guids) || message.requested?(:space_guids)
143+
dataset = dataset.
144+
join(:routes, id: :route_id).
145+
select_all(:route_policies)
146+
147+
dataset = dataset.where(Sequel[:routes][:guid] => message.route_guids) if message.requested?(:route_guids)
148+
149+
dataset = dataset.where(Sequel[:routes][:space_id] => VCAP::CloudController::Space.where(guid: message.space_guids).select(:id)) if message.requested?(:space_guids)
150+
end
151+
152+
dataset = dataset.where(guid: message.guids) if message.requested?(:guids)
153+
dataset = dataset.where(source: message.sources) if message.requested?(:sources)
154+
155+
if message.requested?(:source_guids)
156+
# Text-match against source string for resource GUIDs
157+
# Handles cf:app:<guid>, cf:space:<guid>, cf:org:<guid>
158+
# Escape LIKE metacharacters (\, %, _) in user-provided values
159+
conditions = message.source_guids.map do |guid|
160+
escaped_guid = guid.gsub('\\', '\\\\').gsub('%', '\\%').gsub('_', '\\_')
161+
Sequel.like(:source, "%#{escaped_guid}%")
162+
end
163+
dataset = dataset.where(Sequel.|(*conditions))
164+
end
165+
166+
dataset
167+
end
168+
end

0 commit comments

Comments
 (0)