Skip to content

Commit 5640df7

Browse files
committed
Add allowed_sources support for mTLS app-to-app routing
- Add app_to_app_mtls_routing feature flag (default: false) - Add allowed_sources to RouteOptionsMessage with validation - Validate allowed_sources structure (apps/spaces/orgs arrays, any boolean) - Validate that app/space/org GUIDs exist in database - Enforce mutual exclusivity of 'any' with apps/spaces/orgs lists
1 parent 2e40140 commit 5640df7

2 files changed

Lines changed: 95 additions & 2 deletions

File tree

app/messages/route_options_message.rb

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
module VCAP::CloudController
44
class RouteOptionsMessage < BaseMessage
55
# Register all possible keys upfront so attr_accessors are created
6-
register_allowed_keys %i[loadbalancing hash_header hash_balance]
6+
register_allowed_keys %i[loadbalancing hash_header hash_balance allowed_sources]
77

88
def self.valid_route_options
99
options = %i[loadbalancing]
1010
options += %i[hash_header hash_balance] if VCAP::CloudController::FeatureFlag.enabled?(:hash_based_routing)
11+
options += %i[allowed_sources] if VCAP::CloudController::FeatureFlag.enabled?(:app_to_app_mtls_routing)
1112
options.freeze
1213
end
1314

@@ -21,6 +22,7 @@ def self.valid_loadbalancing_algorithms
2122
validate :loadbalancing_algorithm_is_valid
2223
validate :route_options_are_valid
2324
validate :hash_options_are_valid
25+
validate :allowed_sources_options_are_valid
2426

2527
def loadbalancing_algorithm_is_valid
2628
return if loadbalancing.blank?
@@ -82,5 +84,95 @@ def validate_hash_options_with_loadbalancing
8284
errors.add(:base, 'Hash header can only be set when loadbalancing is hash') if hash_header.present? && loadbalancing.present? && loadbalancing != 'hash'
8385
errors.add(:base, 'Hash balance can only be set when loadbalancing is hash') if hash_balance.present? && loadbalancing.present? && loadbalancing != 'hash'
8486
end
87+
88+
def allowed_sources_options_are_valid
89+
# Only validate allowed_sources when the feature flag is enabled
90+
# If disabled, route_options_are_valid will already report it as unknown field
91+
return unless VCAP::CloudController::FeatureFlag.enabled?(:app_to_app_mtls_routing)
92+
return if allowed_sources.blank?
93+
94+
validate_allowed_sources_structure
95+
validate_allowed_sources_any_exclusivity
96+
validate_allowed_sources_guids_exist
97+
end
98+
99+
private
100+
101+
def validate_allowed_sources_structure
102+
unless allowed_sources.is_a?(Hash)
103+
errors.add(:allowed_sources, 'must be an object')
104+
return
105+
end
106+
107+
valid_keys = %w[apps spaces orgs any]
108+
invalid_keys = allowed_sources.keys - valid_keys
109+
errors.add(:allowed_sources, "contains invalid keys: #{invalid_keys.join(', ')}") if invalid_keys.any?
110+
111+
# Validate types
112+
%w[apps spaces orgs].each do |key|
113+
next unless allowed_sources[key].present?
114+
115+
unless allowed_sources[key].is_a?(Array) && allowed_sources[key].all? { |v| v.is_a?(String) }
116+
errors.add(:allowed_sources, "#{key} must be an array of strings")
117+
end
118+
end
119+
120+
return unless allowed_sources['any'].present? && ![true, false].include?(allowed_sources['any'])
121+
122+
errors.add(:allowed_sources, 'any must be a boolean')
123+
end
124+
125+
def validate_allowed_sources_any_exclusivity
126+
return unless allowed_sources.is_a?(Hash)
127+
128+
has_any = allowed_sources['any'] == true
129+
has_lists = %w[apps spaces orgs].any? { |key| allowed_sources[key].present? && allowed_sources[key].any? }
130+
131+
return unless has_any && has_lists
132+
133+
errors.add(:allowed_sources, 'any is mutually exclusive with apps, spaces, and orgs')
134+
end
135+
136+
def validate_allowed_sources_guids_exist
137+
return unless allowed_sources.is_a?(Hash)
138+
return if errors[:allowed_sources].any? # Skip if already invalid
139+
140+
validate_app_guids_exist
141+
validate_space_guids_exist
142+
validate_org_guids_exist
143+
end
144+
145+
def validate_app_guids_exist
146+
app_guids = allowed_sources['apps']
147+
return if app_guids.blank?
148+
149+
existing_guids = AppModel.where(guid: app_guids).select_map(:guid)
150+
missing_guids = app_guids - existing_guids
151+
return if missing_guids.empty?
152+
153+
errors.add(:allowed_sources, "apps contains non-existent app GUIDs: #{missing_guids.join(', ')}")
154+
end
155+
156+
def validate_space_guids_exist
157+
space_guids = allowed_sources['spaces']
158+
return if space_guids.blank?
159+
160+
existing_guids = Space.where(guid: space_guids).select_map(:guid)
161+
missing_guids = space_guids - existing_guids
162+
return if missing_guids.empty?
163+
164+
errors.add(:allowed_sources, "spaces contains non-existent space GUIDs: #{missing_guids.join(', ')}")
165+
end
166+
167+
def validate_org_guids_exist
168+
org_guids = allowed_sources['orgs']
169+
return if org_guids.blank?
170+
171+
existing_guids = Organization.where(guid: org_guids).select_map(:guid)
172+
missing_guids = org_guids - existing_guids
173+
return if missing_guids.empty?
174+
175+
errors.add(:allowed_sources, "orgs contains non-existent organization GUIDs: #{missing_guids.join(', ')}")
176+
end
85177
end
86178
end

app/models/runtime/feature_flag.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ class UndefinedFeatureFlagError < StandardError
2424
hide_marketplace_from_unauthenticated_users: false,
2525
resource_matching: true,
2626
route_sharing: false,
27-
hash_based_routing: false
27+
hash_based_routing: false,
28+
app_to_app_mtls_routing: false
2829
}.freeze
2930

3031
ADMIN_SKIPPABLE = %i[

0 commit comments

Comments
 (0)