|
3 | 3 | module VCAP::CloudController |
4 | 4 | class RouteOptionsMessage < BaseMessage |
5 | 5 | # Register all possible keys upfront so attr_accessors are created |
6 | | - register_allowed_keys %i[loadbalancing hash_header hash_balance mtls_allowed_sources] |
| 6 | + # RFC-0027 compliant: only string/number/boolean values (no nested objects/arrays) |
| 7 | + # mtls_allowed_apps, mtls_allowed_spaces, mtls_allowed_orgs are comma-separated GUIDs |
| 8 | + # mtls_allow_any is a boolean |
| 9 | + register_allowed_keys %i[loadbalancing hash_header hash_balance mtls_allowed_apps mtls_allowed_spaces mtls_allowed_orgs mtls_allow_any] |
7 | 10 |
|
8 | 11 | def self.valid_route_options |
9 | 12 | options = %i[loadbalancing] |
10 | 13 | options += %i[hash_header hash_balance] if VCAP::CloudController::FeatureFlag.enabled?(:hash_based_routing) |
11 | | - options += %i[mtls_allowed_sources] if VCAP::CloudController::FeatureFlag.enabled?(:app_to_app_mtls_routing) |
| 14 | + options += %i[mtls_allowed_apps mtls_allowed_spaces mtls_allowed_orgs mtls_allow_any] if VCAP::CloudController::FeatureFlag.enabled?(:app_to_app_mtls_routing) |
12 | 15 | options.freeze |
13 | 16 | end |
14 | 17 |
|
@@ -86,98 +89,93 @@ def validate_hash_options_with_loadbalancing |
86 | 89 | end |
87 | 90 |
|
88 | 91 | def mtls_allowed_sources_options_are_valid |
89 | | - # Only validate mtls_allowed_sources when the feature flag is enabled |
90 | | - # If disabled, route_options_are_valid will already report it as unknown field |
| 92 | + # Only validate mtls options when the feature flag is enabled |
| 93 | + # If disabled, route_options_are_valid will already report them as unknown fields |
91 | 94 | return unless VCAP::CloudController::FeatureFlag.enabled?(:app_to_app_mtls_routing) |
92 | | - return if mtls_allowed_sources.blank? |
93 | 95 |
|
94 | | - validate_mtls_allowed_sources_structure |
95 | | - validate_mtls_allowed_sources_any_exclusivity |
96 | | - validate_mtls_allowed_sources_guids_exist |
| 96 | + validate_mtls_string_types |
| 97 | + validate_mtls_allow_any_type |
| 98 | + validate_mtls_allow_any_exclusivity |
| 99 | + validate_mtls_guids_exist |
97 | 100 | end |
98 | 101 |
|
99 | 102 | private |
100 | 103 |
|
101 | | - # Normalize mtls_allowed_sources to use string keys (Rails may parse JSON with symbol keys) |
102 | | - def normalized_mtls_allowed_sources |
103 | | - @normalized_mtls_allowed_sources ||= mtls_allowed_sources.is_a?(Hash) ? mtls_allowed_sources.transform_keys(&:to_s) : mtls_allowed_sources |
104 | | - end |
105 | | - |
106 | | - def validate_mtls_allowed_sources_structure |
107 | | - unless mtls_allowed_sources.is_a?(Hash) |
108 | | - errors.add(:mtls_allowed_sources, 'must be an object') |
109 | | - return |
110 | | - end |
| 104 | + # Parse comma-separated GUIDs into an array |
| 105 | + def parse_guid_list(value) |
| 106 | + return [] if value.blank? |
111 | 107 |
|
112 | | - valid_keys = %w[apps spaces orgs any] |
113 | | - invalid_keys = normalized_mtls_allowed_sources.keys - valid_keys |
114 | | - errors.add(:mtls_allowed_sources, "contains invalid keys: #{invalid_keys.join(', ')}") if invalid_keys.any? |
| 108 | + value.to_s.split(',').map(&:strip).reject(&:empty?) |
| 109 | + end |
115 | 110 |
|
116 | | - # Validate types |
117 | | - %w[apps spaces orgs].each do |key| |
118 | | - next unless normalized_mtls_allowed_sources[key].present? |
| 111 | + def validate_mtls_string_types |
| 112 | + # These should be strings (comma-separated GUIDs) per RFC-0027 |
| 113 | + %i[mtls_allowed_apps mtls_allowed_spaces mtls_allowed_orgs].each do |key| |
| 114 | + value = public_send(key) |
| 115 | + next if value.blank? |
119 | 116 |
|
120 | | - unless normalized_mtls_allowed_sources[key].is_a?(Array) && normalized_mtls_allowed_sources[key].all? { |v| v.is_a?(String) } |
121 | | - errors.add(:mtls_allowed_sources, "#{key} must be an array of strings") |
| 117 | + unless value.is_a?(String) |
| 118 | + errors.add(key, 'must be a string of comma-separated GUIDs') |
122 | 119 | end |
123 | 120 | end |
| 121 | + end |
124 | 122 |
|
125 | | - return unless normalized_mtls_allowed_sources['any'].present? && ![true, false].include?(normalized_mtls_allowed_sources['any']) |
| 123 | + def validate_mtls_allow_any_type |
| 124 | + return if mtls_allow_any.nil? |
126 | 125 |
|
127 | | - errors.add(:mtls_allowed_sources, 'any must be a boolean') |
| 126 | + unless [true, false, 'true', 'false'].include?(mtls_allow_any) |
| 127 | + errors.add(:mtls_allow_any, 'must be a boolean (true or false)') |
| 128 | + end |
128 | 129 | end |
129 | 130 |
|
130 | | - def validate_mtls_allowed_sources_any_exclusivity |
131 | | - return unless mtls_allowed_sources.is_a?(Hash) |
132 | | - |
133 | | - has_any = normalized_mtls_allowed_sources['any'] == true |
134 | | - has_lists = %w[apps spaces orgs].any? { |key| normalized_mtls_allowed_sources[key].present? && normalized_mtls_allowed_sources[key].any? } |
| 131 | + def validate_mtls_allow_any_exclusivity |
| 132 | + allow_any = mtls_allow_any == true || mtls_allow_any == 'true' |
| 133 | + has_specific = [mtls_allowed_apps, mtls_allowed_spaces, mtls_allowed_orgs].any?(&:present?) |
135 | 134 |
|
136 | | - return unless has_any && has_lists |
| 135 | + return unless allow_any && has_specific |
137 | 136 |
|
138 | | - errors.add(:mtls_allowed_sources, 'any is mutually exclusive with apps, spaces, and orgs') |
| 137 | + errors.add(:mtls_allow_any, 'is mutually exclusive with mtls_allowed_apps, mtls_allowed_spaces, and mtls_allowed_orgs') |
139 | 138 | end |
140 | 139 |
|
141 | | - def validate_mtls_allowed_sources_guids_exist |
142 | | - return unless mtls_allowed_sources.is_a?(Hash) |
143 | | - return if errors[:mtls_allowed_sources].any? # Skip if already invalid |
| 140 | + def validate_mtls_guids_exist |
| 141 | + return if errors.any? # Skip if already invalid |
144 | 142 |
|
145 | 143 | validate_app_guids_exist |
146 | 144 | validate_space_guids_exist |
147 | 145 | validate_org_guids_exist |
148 | 146 | end |
149 | 147 |
|
150 | 148 | def validate_app_guids_exist |
151 | | - app_guids = normalized_mtls_allowed_sources['apps'] |
152 | | - return if app_guids.blank? |
| 149 | + app_guids = parse_guid_list(mtls_allowed_apps) |
| 150 | + return if app_guids.empty? |
153 | 151 |
|
154 | 152 | existing_guids = AppModel.where(guid: app_guids).select_map(:guid) |
155 | 153 | missing_guids = app_guids - existing_guids |
156 | 154 | return if missing_guids.empty? |
157 | 155 |
|
158 | | - errors.add(:mtls_allowed_sources, "apps contains non-existent app GUIDs: #{missing_guids.join(', ')}") |
| 156 | + errors.add(:mtls_allowed_apps, "contains non-existent app GUIDs: #{missing_guids.join(', ')}") |
159 | 157 | end |
160 | 158 |
|
161 | 159 | def validate_space_guids_exist |
162 | | - space_guids = normalized_mtls_allowed_sources['spaces'] |
163 | | - return if space_guids.blank? |
| 160 | + space_guids = parse_guid_list(mtls_allowed_spaces) |
| 161 | + return if space_guids.empty? |
164 | 162 |
|
165 | 163 | existing_guids = Space.where(guid: space_guids).select_map(:guid) |
166 | 164 | missing_guids = space_guids - existing_guids |
167 | 165 | return if missing_guids.empty? |
168 | 166 |
|
169 | | - errors.add(:mtls_allowed_sources, "spaces contains non-existent space GUIDs: #{missing_guids.join(', ')}") |
| 167 | + errors.add(:mtls_allowed_spaces, "contains non-existent space GUIDs: #{missing_guids.join(', ')}") |
170 | 168 | end |
171 | 169 |
|
172 | 170 | def validate_org_guids_exist |
173 | | - org_guids = normalized_mtls_allowed_sources['orgs'] |
174 | | - return if org_guids.blank? |
| 171 | + org_guids = parse_guid_list(mtls_allowed_orgs) |
| 172 | + return if org_guids.empty? |
175 | 173 |
|
176 | 174 | existing_guids = Organization.where(guid: org_guids).select_map(:guid) |
177 | 175 | missing_guids = org_guids - existing_guids |
178 | 176 | return if missing_guids.empty? |
179 | 177 |
|
180 | | - errors.add(:mtls_allowed_sources, "orgs contains non-existent organization GUIDs: #{missing_guids.join(', ')}") |
| 178 | + errors.add(:mtls_allowed_orgs, "contains non-existent organization GUIDs: #{missing_guids.join(', ')}") |
181 | 179 | end |
182 | 180 | end |
183 | 181 | end |
0 commit comments