From 6a68b59c21d9ae056220c88470dfef1b7b2eda73 Mon Sep 17 00:00:00 2001 From: rkoster Date: Thu, 5 Mar 2026 08:25:37 +0000 Subject: [PATCH 01/29] 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 --- app/messages/route_options_message.rb | 94 ++++++++++++++++++++++++++- app/models/runtime/feature_flag.rb | 3 +- 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/app/messages/route_options_message.rb b/app/messages/route_options_message.rb index f79a2bb6a0c..b45d0462c95 100644 --- a/app/messages/route_options_message.rb +++ b/app/messages/route_options_message.rb @@ -3,11 +3,12 @@ module VCAP::CloudController class RouteOptionsMessage < BaseMessage # Register all possible keys upfront so attr_accessors are created - register_allowed_keys %i[loadbalancing hash_header hash_balance] + register_allowed_keys %i[loadbalancing hash_header hash_balance allowed_sources] def self.valid_route_options options = %i[loadbalancing] options += %i[hash_header hash_balance] if VCAP::CloudController::FeatureFlag.enabled?(:hash_based_routing) + options += %i[allowed_sources] if VCAP::CloudController::FeatureFlag.enabled?(:app_to_app_mtls_routing) options.freeze end @@ -21,6 +22,7 @@ def self.valid_loadbalancing_algorithms validate :loadbalancing_algorithm_is_valid validate :route_options_are_valid validate :hash_options_are_valid + validate :allowed_sources_options_are_valid def loadbalancing_algorithm_is_valid return if loadbalancing.blank? @@ -82,5 +84,95 @@ def validate_hash_options_with_loadbalancing errors.add(:base, 'Hash header can only be set when loadbalancing is hash') if hash_header.present? && loadbalancing.present? && loadbalancing != 'hash' errors.add(:base, 'Hash balance can only be set when loadbalancing is hash') if hash_balance.present? && loadbalancing.present? && loadbalancing != 'hash' end + + def allowed_sources_options_are_valid + # Only validate allowed_sources when the feature flag is enabled + # If disabled, route_options_are_valid will already report it as unknown field + return unless VCAP::CloudController::FeatureFlag.enabled?(:app_to_app_mtls_routing) + return if allowed_sources.blank? + + validate_allowed_sources_structure + validate_allowed_sources_any_exclusivity + validate_allowed_sources_guids_exist + end + + private + + def validate_allowed_sources_structure + unless allowed_sources.is_a?(Hash) + errors.add(:allowed_sources, 'must be an object') + return + end + + valid_keys = %w[apps spaces orgs any] + invalid_keys = allowed_sources.keys - valid_keys + errors.add(:allowed_sources, "contains invalid keys: #{invalid_keys.join(', ')}") if invalid_keys.any? + + # Validate types + %w[apps spaces orgs].each do |key| + next unless allowed_sources[key].present? + + unless allowed_sources[key].is_a?(Array) && allowed_sources[key].all? { |v| v.is_a?(String) } + errors.add(:allowed_sources, "#{key} must be an array of strings") + end + end + + return unless allowed_sources['any'].present? && ![true, false].include?(allowed_sources['any']) + + errors.add(:allowed_sources, 'any must be a boolean') + end + + def validate_allowed_sources_any_exclusivity + return unless allowed_sources.is_a?(Hash) + + has_any = allowed_sources['any'] == true + has_lists = %w[apps spaces orgs].any? { |key| allowed_sources[key].present? && allowed_sources[key].any? } + + return unless has_any && has_lists + + errors.add(:allowed_sources, 'any is mutually exclusive with apps, spaces, and orgs') + end + + def validate_allowed_sources_guids_exist + return unless allowed_sources.is_a?(Hash) + return if errors[:allowed_sources].any? # Skip if already invalid + + validate_app_guids_exist + validate_space_guids_exist + validate_org_guids_exist + end + + def validate_app_guids_exist + app_guids = allowed_sources['apps'] + return if app_guids.blank? + + existing_guids = AppModel.where(guid: app_guids).select_map(:guid) + missing_guids = app_guids - existing_guids + return if missing_guids.empty? + + errors.add(:allowed_sources, "apps contains non-existent app GUIDs: #{missing_guids.join(', ')}") + end + + def validate_space_guids_exist + space_guids = allowed_sources['spaces'] + return if space_guids.blank? + + existing_guids = Space.where(guid: space_guids).select_map(:guid) + missing_guids = space_guids - existing_guids + return if missing_guids.empty? + + errors.add(:allowed_sources, "spaces contains non-existent space GUIDs: #{missing_guids.join(', ')}") + end + + def validate_org_guids_exist + org_guids = allowed_sources['orgs'] + return if org_guids.blank? + + existing_guids = Organization.where(guid: org_guids).select_map(:guid) + missing_guids = org_guids - existing_guids + return if missing_guids.empty? + + errors.add(:allowed_sources, "orgs contains non-existent organization GUIDs: #{missing_guids.join(', ')}") + end end end diff --git a/app/models/runtime/feature_flag.rb b/app/models/runtime/feature_flag.rb index e64b7d60e7b..4dece1d2df0 100644 --- a/app/models/runtime/feature_flag.rb +++ b/app/models/runtime/feature_flag.rb @@ -24,7 +24,8 @@ class UndefinedFeatureFlagError < StandardError hide_marketplace_from_unauthenticated_users: false, resource_matching: true, route_sharing: false, - hash_based_routing: false + hash_based_routing: false, + app_to_app_mtls_routing: false }.freeze ADMIN_SKIPPABLE = %i[ From 75f5ccfc2c684d1d8bccdda0526ffcaf203ac22b Mon Sep 17 00:00:00 2001 From: rkoster Date: Thu, 5 Mar 2026 08:26:35 +0000 Subject: [PATCH 02/29] Add unit tests for allowed_sources validation Tests cover: - Feature flag disabled: allowed_sources rejected as unknown field - Structure validation: object type, valid keys, array types, boolean any - any exclusivity: cannot combine any:true with apps/spaces/orgs lists - GUID existence validation: apps, spaces, orgs must exist in database - Combined options: allowed_sources works with loadbalancing --- .../messages/route_options_message_spec.rb | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) diff --git a/spec/unit/messages/route_options_message_spec.rb b/spec/unit/messages/route_options_message_spec.rb index 57646d21950..aa60e654deb 100644 --- a/spec/unit/messages/route_options_message_spec.rb +++ b/spec/unit/messages/route_options_message_spec.rb @@ -37,6 +37,204 @@ module VCAP::CloudController end end + describe 'allowed_sources validations' do + context 'when app_to_app_mtls_routing feature flag is disabled' do + it 'does not allow allowed_sources option' do + message = RouteOptionsMessage.new({ allowed_sources: { apps: ['app-guid-1'] } }) + expect(message).not_to be_valid + expect(message.errors_on(:base)).to include("Unknown field(s): 'allowed_sources'") + end + end + + context 'when app_to_app_mtls_routing feature flag is enabled' do + before do + VCAP::CloudController::FeatureFlag.make(name: 'app_to_app_mtls_routing', enabled: true) + end + + describe 'structure validation' do + it 'allows valid allowed_sources with apps' do + app = AppModel.make + message = RouteOptionsMessage.new({ allowed_sources: { 'apps' => [app.guid] } }) + expect(message).to be_valid + end + + it 'allows valid allowed_sources with spaces' do + space = Space.make + message = RouteOptionsMessage.new({ allowed_sources: { 'spaces' => [space.guid] } }) + expect(message).to be_valid + end + + it 'allows valid allowed_sources with orgs' do + org = Organization.make + message = RouteOptionsMessage.new({ allowed_sources: { 'orgs' => [org.guid] } }) + expect(message).to be_valid + end + + it 'allows valid allowed_sources with any: true' do + message = RouteOptionsMessage.new({ allowed_sources: { 'any' => true } }) + expect(message).to be_valid + end + + it 'allows valid allowed_sources with any: false' do + message = RouteOptionsMessage.new({ allowed_sources: { 'any' => false } }) + expect(message).to be_valid + end + + it 'allows empty allowed_sources object' do + message = RouteOptionsMessage.new({ allowed_sources: {} }) + expect(message).to be_valid + end + + it 'does not allow non-object allowed_sources' do + message = RouteOptionsMessage.new({ allowed_sources: 'invalid' }) + expect(message).not_to be_valid + expect(message.errors_on(:allowed_sources)).to include('must be an object') + end + + it 'does not allow array allowed_sources' do + message = RouteOptionsMessage.new({ allowed_sources: ['app-guid-1'] }) + expect(message).not_to be_valid + expect(message.errors_on(:allowed_sources)).to include('must be an object') + end + + it 'does not allow invalid keys in allowed_sources' do + message = RouteOptionsMessage.new({ allowed_sources: { 'invalid_key' => 'value' } }) + expect(message).not_to be_valid + expect(message.errors_on(:allowed_sources)).to include('contains invalid keys: invalid_key') + end + + it 'does not allow non-array apps' do + message = RouteOptionsMessage.new({ allowed_sources: { 'apps' => 'not-an-array' } }) + expect(message).not_to be_valid + expect(message.errors_on(:allowed_sources)).to include('apps must be an array of strings') + end + + it 'does not allow non-string elements in apps array' do + message = RouteOptionsMessage.new({ allowed_sources: { 'apps' => [123, 456] } }) + expect(message).not_to be_valid + expect(message.errors_on(:allowed_sources)).to include('apps must be an array of strings') + end + + it 'does not allow non-array spaces' do + message = RouteOptionsMessage.new({ allowed_sources: { 'spaces' => 'not-an-array' } }) + expect(message).not_to be_valid + expect(message.errors_on(:allowed_sources)).to include('spaces must be an array of strings') + end + + it 'does not allow non-array orgs' do + message = RouteOptionsMessage.new({ allowed_sources: { 'orgs' => 'not-an-array' } }) + expect(message).not_to be_valid + expect(message.errors_on(:allowed_sources)).to include('orgs must be an array of strings') + end + + it 'does not allow non-boolean any' do + message = RouteOptionsMessage.new({ allowed_sources: { 'any' => 'true' } }) + expect(message).not_to be_valid + expect(message.errors_on(:allowed_sources)).to include('any must be a boolean') + end + end + + describe 'any exclusivity validation' do + it 'does not allow any: true with apps list' do + app = AppModel.make + message = RouteOptionsMessage.new({ allowed_sources: { 'any' => true, 'apps' => [app.guid] } }) + expect(message).not_to be_valid + expect(message.errors_on(:allowed_sources)).to include('any is mutually exclusive with apps, spaces, and orgs') + end + + it 'does not allow any: true with spaces list' do + space = Space.make + message = RouteOptionsMessage.new({ allowed_sources: { 'any' => true, 'spaces' => [space.guid] } }) + expect(message).not_to be_valid + expect(message.errors_on(:allowed_sources)).to include('any is mutually exclusive with apps, spaces, and orgs') + end + + it 'does not allow any: true with orgs list' do + org = Organization.make + message = RouteOptionsMessage.new({ allowed_sources: { 'any' => true, 'orgs' => [org.guid] } }) + expect(message).not_to be_valid + expect(message.errors_on(:allowed_sources)).to include('any is mutually exclusive with apps, spaces, and orgs') + end + + it 'allows any: false with apps list' do + app = AppModel.make + message = RouteOptionsMessage.new({ allowed_sources: { 'any' => false, 'apps' => [app.guid] } }) + expect(message).to be_valid + end + + it 'allows any: true with empty apps list' do + message = RouteOptionsMessage.new({ allowed_sources: { 'any' => true, 'apps' => [] } }) + expect(message).to be_valid + end + end + + describe 'GUID existence validation' do + it 'validates that app GUIDs exist' do + message = RouteOptionsMessage.new({ allowed_sources: { 'apps' => ['non-existent-app-guid'] } }) + expect(message).not_to be_valid + expect(message.errors_on(:allowed_sources)).to include('apps contains non-existent app GUIDs: non-existent-app-guid') + end + + it 'validates that space GUIDs exist' do + message = RouteOptionsMessage.new({ allowed_sources: { 'spaces' => ['non-existent-space-guid'] } }) + expect(message).not_to be_valid + expect(message.errors_on(:allowed_sources)).to include('spaces contains non-existent space GUIDs: non-existent-space-guid') + end + + it 'validates that org GUIDs exist' do + message = RouteOptionsMessage.new({ allowed_sources: { 'orgs' => ['non-existent-org-guid'] } }) + expect(message).not_to be_valid + expect(message.errors_on(:allowed_sources)).to include('orgs contains non-existent organization GUIDs: non-existent-org-guid') + end + + it 'reports multiple non-existent app GUIDs' do + message = RouteOptionsMessage.new({ allowed_sources: { 'apps' => ['guid-1', 'guid-2'] } }) + expect(message).not_to be_valid + expect(message.errors_on(:allowed_sources)).to include('apps contains non-existent app GUIDs: guid-1, guid-2') + end + + it 'allows mix of existing apps, spaces, and orgs' do + app = AppModel.make + space = Space.make + org = Organization.make + message = RouteOptionsMessage.new({ + allowed_sources: { + 'apps' => [app.guid], + 'spaces' => [space.guid], + 'orgs' => [org.guid] + } + }) + expect(message).to be_valid + end + + it 'validates all types of GUIDs when multiple are provided' do + app = AppModel.make + message = RouteOptionsMessage.new({ + allowed_sources: { + 'apps' => [app.guid], + 'spaces' => ['non-existent-space'], + 'orgs' => ['non-existent-org'] + } + }) + expect(message).not_to be_valid + expect(message.errors_on(:allowed_sources)).to include('spaces contains non-existent space GUIDs: non-existent-space') + expect(message.errors_on(:allowed_sources)).to include('orgs contains non-existent organization GUIDs: non-existent-org') + end + end + + describe 'combined with other options' do + it 'allows allowed_sources with loadbalancing' do + app = AppModel.make + message = RouteOptionsMessage.new({ + loadbalancing: 'round-robin', + allowed_sources: { 'apps' => [app.guid] } + }) + expect(message).to be_valid + end + end + end + end + describe 'hash-based routing validations' do context 'when hash_based_routing feature flag is disabled' do it 'does not allow hash_header option' do From 915b21a48ee8ad9c6b67a32c0a99d66ffb3669e3 Mon Sep 17 00:00:00 2001 From: rkoster Date: Thu, 5 Mar 2026 08:39:30 +0000 Subject: [PATCH 03/29] Fix allowed_sources validation to handle symbol keys Rails parses JSON with symbol keys, but validation was comparing against string keys. Add normalized_allowed_sources helper to transform keys to strings for consistent comparison. --- app/messages/route_options_message.rb | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/app/messages/route_options_message.rb b/app/messages/route_options_message.rb index b45d0462c95..6983d7e7012 100644 --- a/app/messages/route_options_message.rb +++ b/app/messages/route_options_message.rb @@ -98,6 +98,11 @@ def allowed_sources_options_are_valid private + # Normalize allowed_sources to use string keys (Rails may parse JSON with symbol keys) + def normalized_allowed_sources + @normalized_allowed_sources ||= allowed_sources.is_a?(Hash) ? allowed_sources.transform_keys(&:to_s) : allowed_sources + end + def validate_allowed_sources_structure unless allowed_sources.is_a?(Hash) errors.add(:allowed_sources, 'must be an object') @@ -105,19 +110,19 @@ def validate_allowed_sources_structure end valid_keys = %w[apps spaces orgs any] - invalid_keys = allowed_sources.keys - valid_keys + invalid_keys = normalized_allowed_sources.keys - valid_keys errors.add(:allowed_sources, "contains invalid keys: #{invalid_keys.join(', ')}") if invalid_keys.any? # Validate types %w[apps spaces orgs].each do |key| - next unless allowed_sources[key].present? + next unless normalized_allowed_sources[key].present? - unless allowed_sources[key].is_a?(Array) && allowed_sources[key].all? { |v| v.is_a?(String) } + unless normalized_allowed_sources[key].is_a?(Array) && normalized_allowed_sources[key].all? { |v| v.is_a?(String) } errors.add(:allowed_sources, "#{key} must be an array of strings") end end - return unless allowed_sources['any'].present? && ![true, false].include?(allowed_sources['any']) + return unless normalized_allowed_sources['any'].present? && ![true, false].include?(normalized_allowed_sources['any']) errors.add(:allowed_sources, 'any must be a boolean') end @@ -125,8 +130,8 @@ def validate_allowed_sources_structure def validate_allowed_sources_any_exclusivity return unless allowed_sources.is_a?(Hash) - has_any = allowed_sources['any'] == true - has_lists = %w[apps spaces orgs].any? { |key| allowed_sources[key].present? && allowed_sources[key].any? } + has_any = normalized_allowed_sources['any'] == true + has_lists = %w[apps spaces orgs].any? { |key| normalized_allowed_sources[key].present? && normalized_allowed_sources[key].any? } return unless has_any && has_lists @@ -143,7 +148,7 @@ def validate_allowed_sources_guids_exist end def validate_app_guids_exist - app_guids = allowed_sources['apps'] + app_guids = normalized_allowed_sources['apps'] return if app_guids.blank? existing_guids = AppModel.where(guid: app_guids).select_map(:guid) @@ -154,7 +159,7 @@ def validate_app_guids_exist end def validate_space_guids_exist - space_guids = allowed_sources['spaces'] + space_guids = normalized_allowed_sources['spaces'] return if space_guids.blank? existing_guids = Space.where(guid: space_guids).select_map(:guid) @@ -165,7 +170,7 @@ def validate_space_guids_exist end def validate_org_guids_exist - org_guids = allowed_sources['orgs'] + org_guids = normalized_allowed_sources['orgs'] return if org_guids.blank? existing_guids = Organization.where(guid: org_guids).select_map(:guid) From 29a09b1df2d219582f149ae689d9fde171be6c80 Mon Sep 17 00:00:00 2001 From: rkoster Date: Thu, 5 Mar 2026 10:01:09 +0000 Subject: [PATCH 04/29] Rename allowed_sources to mtls_allowed_sources for clarity Rename the route options field from allowed_sources to mtls_allowed_sources for better clarity about its purpose in mTLS app-to-app routing. Updates RouteOptionsMessage to use the new field name in: - Allowed keys registration - Feature flag gating - Validation methods - All related tests --- app/messages/route_options_message.rb | 72 +++++------ .../messages/route_options_message_spec.rb | 114 +++++++++--------- 2 files changed, 93 insertions(+), 93 deletions(-) diff --git a/app/messages/route_options_message.rb b/app/messages/route_options_message.rb index 6983d7e7012..ab688c6bbb6 100644 --- a/app/messages/route_options_message.rb +++ b/app/messages/route_options_message.rb @@ -3,12 +3,12 @@ module VCAP::CloudController class RouteOptionsMessage < BaseMessage # Register all possible keys upfront so attr_accessors are created - register_allowed_keys %i[loadbalancing hash_header hash_balance allowed_sources] + register_allowed_keys %i[loadbalancing hash_header hash_balance mtls_allowed_sources] def self.valid_route_options options = %i[loadbalancing] options += %i[hash_header hash_balance] if VCAP::CloudController::FeatureFlag.enabled?(:hash_based_routing) - options += %i[allowed_sources] if VCAP::CloudController::FeatureFlag.enabled?(:app_to_app_mtls_routing) + options += %i[mtls_allowed_sources] if VCAP::CloudController::FeatureFlag.enabled?(:app_to_app_mtls_routing) options.freeze end @@ -22,7 +22,7 @@ def self.valid_loadbalancing_algorithms validate :loadbalancing_algorithm_is_valid validate :route_options_are_valid validate :hash_options_are_valid - validate :allowed_sources_options_are_valid + validate :mtls_allowed_sources_options_are_valid def loadbalancing_algorithm_is_valid return if loadbalancing.blank? @@ -85,62 +85,62 @@ def validate_hash_options_with_loadbalancing errors.add(:base, 'Hash balance can only be set when loadbalancing is hash') if hash_balance.present? && loadbalancing.present? && loadbalancing != 'hash' end - def allowed_sources_options_are_valid - # Only validate allowed_sources when the feature flag is enabled + def mtls_allowed_sources_options_are_valid + # Only validate mtls_allowed_sources when the feature flag is enabled # If disabled, route_options_are_valid will already report it as unknown field return unless VCAP::CloudController::FeatureFlag.enabled?(:app_to_app_mtls_routing) - return if allowed_sources.blank? + return if mtls_allowed_sources.blank? - validate_allowed_sources_structure - validate_allowed_sources_any_exclusivity - validate_allowed_sources_guids_exist + validate_mtls_allowed_sources_structure + validate_mtls_allowed_sources_any_exclusivity + validate_mtls_allowed_sources_guids_exist end private - # Normalize allowed_sources to use string keys (Rails may parse JSON with symbol keys) - def normalized_allowed_sources - @normalized_allowed_sources ||= allowed_sources.is_a?(Hash) ? allowed_sources.transform_keys(&:to_s) : allowed_sources + # Normalize mtls_allowed_sources to use string keys (Rails may parse JSON with symbol keys) + def normalized_mtls_allowed_sources + @normalized_mtls_allowed_sources ||= mtls_allowed_sources.is_a?(Hash) ? mtls_allowed_sources.transform_keys(&:to_s) : mtls_allowed_sources end - def validate_allowed_sources_structure - unless allowed_sources.is_a?(Hash) - errors.add(:allowed_sources, 'must be an object') + def validate_mtls_allowed_sources_structure + unless mtls_allowed_sources.is_a?(Hash) + errors.add(:mtls_allowed_sources, 'must be an object') return end valid_keys = %w[apps spaces orgs any] - invalid_keys = normalized_allowed_sources.keys - valid_keys - errors.add(:allowed_sources, "contains invalid keys: #{invalid_keys.join(', ')}") if invalid_keys.any? + invalid_keys = normalized_mtls_allowed_sources.keys - valid_keys + errors.add(:mtls_allowed_sources, "contains invalid keys: #{invalid_keys.join(', ')}") if invalid_keys.any? # Validate types %w[apps spaces orgs].each do |key| - next unless normalized_allowed_sources[key].present? + next unless normalized_mtls_allowed_sources[key].present? - unless normalized_allowed_sources[key].is_a?(Array) && normalized_allowed_sources[key].all? { |v| v.is_a?(String) } - errors.add(:allowed_sources, "#{key} must be an array of strings") + unless normalized_mtls_allowed_sources[key].is_a?(Array) && normalized_mtls_allowed_sources[key].all? { |v| v.is_a?(String) } + errors.add(:mtls_allowed_sources, "#{key} must be an array of strings") end end - return unless normalized_allowed_sources['any'].present? && ![true, false].include?(normalized_allowed_sources['any']) + return unless normalized_mtls_allowed_sources['any'].present? && ![true, false].include?(normalized_mtls_allowed_sources['any']) - errors.add(:allowed_sources, 'any must be a boolean') + errors.add(:mtls_allowed_sources, 'any must be a boolean') end - def validate_allowed_sources_any_exclusivity - return unless allowed_sources.is_a?(Hash) + def validate_mtls_allowed_sources_any_exclusivity + return unless mtls_allowed_sources.is_a?(Hash) - has_any = normalized_allowed_sources['any'] == true - has_lists = %w[apps spaces orgs].any? { |key| normalized_allowed_sources[key].present? && normalized_allowed_sources[key].any? } + has_any = normalized_mtls_allowed_sources['any'] == true + has_lists = %w[apps spaces orgs].any? { |key| normalized_mtls_allowed_sources[key].present? && normalized_mtls_allowed_sources[key].any? } return unless has_any && has_lists - errors.add(:allowed_sources, 'any is mutually exclusive with apps, spaces, and orgs') + errors.add(:mtls_allowed_sources, 'any is mutually exclusive with apps, spaces, and orgs') end - def validate_allowed_sources_guids_exist - return unless allowed_sources.is_a?(Hash) - return if errors[:allowed_sources].any? # Skip if already invalid + def validate_mtls_allowed_sources_guids_exist + return unless mtls_allowed_sources.is_a?(Hash) + return if errors[:mtls_allowed_sources].any? # Skip if already invalid validate_app_guids_exist validate_space_guids_exist @@ -148,36 +148,36 @@ def validate_allowed_sources_guids_exist end def validate_app_guids_exist - app_guids = normalized_allowed_sources['apps'] + app_guids = normalized_mtls_allowed_sources['apps'] return if app_guids.blank? existing_guids = AppModel.where(guid: app_guids).select_map(:guid) missing_guids = app_guids - existing_guids return if missing_guids.empty? - errors.add(:allowed_sources, "apps contains non-existent app GUIDs: #{missing_guids.join(', ')}") + errors.add(:mtls_allowed_sources, "apps contains non-existent app GUIDs: #{missing_guids.join(', ')}") end def validate_space_guids_exist - space_guids = normalized_allowed_sources['spaces'] + space_guids = normalized_mtls_allowed_sources['spaces'] return if space_guids.blank? existing_guids = Space.where(guid: space_guids).select_map(:guid) missing_guids = space_guids - existing_guids return if missing_guids.empty? - errors.add(:allowed_sources, "spaces contains non-existent space GUIDs: #{missing_guids.join(', ')}") + errors.add(:mtls_allowed_sources, "spaces contains non-existent space GUIDs: #{missing_guids.join(', ')}") end def validate_org_guids_exist - org_guids = normalized_allowed_sources['orgs'] + org_guids = normalized_mtls_allowed_sources['orgs'] return if org_guids.blank? existing_guids = Organization.where(guid: org_guids).select_map(:guid) missing_guids = org_guids - existing_guids return if missing_guids.empty? - errors.add(:allowed_sources, "orgs contains non-existent organization GUIDs: #{missing_guids.join(', ')}") + errors.add(:mtls_allowed_sources, "orgs contains non-existent organization GUIDs: #{missing_guids.join(', ')}") end end end diff --git a/spec/unit/messages/route_options_message_spec.rb b/spec/unit/messages/route_options_message_spec.rb index aa60e654deb..c9d86df339c 100644 --- a/spec/unit/messages/route_options_message_spec.rb +++ b/spec/unit/messages/route_options_message_spec.rb @@ -37,12 +37,12 @@ module VCAP::CloudController end end - describe 'allowed_sources validations' do + describe 'mtls_allowed_sources validations' do context 'when app_to_app_mtls_routing feature flag is disabled' do - it 'does not allow allowed_sources option' do - message = RouteOptionsMessage.new({ allowed_sources: { apps: ['app-guid-1'] } }) + it 'does not allow mtls_allowed_sources option' do + message = RouteOptionsMessage.new({ mtls_allowed_sources: { apps: ['app-guid-1'] } }) expect(message).not_to be_valid - expect(message.errors_on(:base)).to include("Unknown field(s): 'allowed_sources'") + expect(message.errors_on(:base)).to include("Unknown field(s): 'mtls_allowed_sources'") end end @@ -52,145 +52,145 @@ module VCAP::CloudController end describe 'structure validation' do - it 'allows valid allowed_sources with apps' do + it 'allows valid mtls_allowed_sources with apps' do app = AppModel.make - message = RouteOptionsMessage.new({ allowed_sources: { 'apps' => [app.guid] } }) + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'apps' => [app.guid] } }) expect(message).to be_valid end - it 'allows valid allowed_sources with spaces' do + it 'allows valid mtls_allowed_sources with spaces' do space = Space.make - message = RouteOptionsMessage.new({ allowed_sources: { 'spaces' => [space.guid] } }) + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'spaces' => [space.guid] } }) expect(message).to be_valid end - it 'allows valid allowed_sources with orgs' do + it 'allows valid mtls_allowed_sources with orgs' do org = Organization.make - message = RouteOptionsMessage.new({ allowed_sources: { 'orgs' => [org.guid] } }) + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'orgs' => [org.guid] } }) expect(message).to be_valid end - it 'allows valid allowed_sources with any: true' do - message = RouteOptionsMessage.new({ allowed_sources: { 'any' => true } }) + it 'allows valid mtls_allowed_sources with any: true' do + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'any' => true } }) expect(message).to be_valid end - it 'allows valid allowed_sources with any: false' do - message = RouteOptionsMessage.new({ allowed_sources: { 'any' => false } }) + it 'allows valid mtls_allowed_sources with any: false' do + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'any' => false } }) expect(message).to be_valid end - it 'allows empty allowed_sources object' do - message = RouteOptionsMessage.new({ allowed_sources: {} }) + it 'allows empty mtls_allowed_sources object' do + message = RouteOptionsMessage.new({ mtls_allowed_sources: {} }) expect(message).to be_valid end - it 'does not allow non-object allowed_sources' do - message = RouteOptionsMessage.new({ allowed_sources: 'invalid' }) + it 'does not allow non-object mtls_allowed_sources' do + message = RouteOptionsMessage.new({ mtls_allowed_sources: 'invalid' }) expect(message).not_to be_valid - expect(message.errors_on(:allowed_sources)).to include('must be an object') + expect(message.errors_on(:mtls_allowed_sources)).to include('must be an object') end - it 'does not allow array allowed_sources' do - message = RouteOptionsMessage.new({ allowed_sources: ['app-guid-1'] }) + it 'does not allow array mtls_allowed_sources' do + message = RouteOptionsMessage.new({ mtls_allowed_sources: ['app-guid-1'] }) expect(message).not_to be_valid - expect(message.errors_on(:allowed_sources)).to include('must be an object') + expect(message.errors_on(:mtls_allowed_sources)).to include('must be an object') end - it 'does not allow invalid keys in allowed_sources' do - message = RouteOptionsMessage.new({ allowed_sources: { 'invalid_key' => 'value' } }) + it 'does not allow invalid keys in mtls_allowed_sources' do + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'invalid_key' => 'value' } }) expect(message).not_to be_valid - expect(message.errors_on(:allowed_sources)).to include('contains invalid keys: invalid_key') + expect(message.errors_on(:mtls_allowed_sources)).to include('contains invalid keys: invalid_key') end it 'does not allow non-array apps' do - message = RouteOptionsMessage.new({ allowed_sources: { 'apps' => 'not-an-array' } }) + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'apps' => 'not-an-array' } }) expect(message).not_to be_valid - expect(message.errors_on(:allowed_sources)).to include('apps must be an array of strings') + expect(message.errors_on(:mtls_allowed_sources)).to include('apps must be an array of strings') end it 'does not allow non-string elements in apps array' do - message = RouteOptionsMessage.new({ allowed_sources: { 'apps' => [123, 456] } }) + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'apps' => [123, 456] } }) expect(message).not_to be_valid - expect(message.errors_on(:allowed_sources)).to include('apps must be an array of strings') + expect(message.errors_on(:mtls_allowed_sources)).to include('apps must be an array of strings') end it 'does not allow non-array spaces' do - message = RouteOptionsMessage.new({ allowed_sources: { 'spaces' => 'not-an-array' } }) + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'spaces' => 'not-an-array' } }) expect(message).not_to be_valid - expect(message.errors_on(:allowed_sources)).to include('spaces must be an array of strings') + expect(message.errors_on(:mtls_allowed_sources)).to include('spaces must be an array of strings') end it 'does not allow non-array orgs' do - message = RouteOptionsMessage.new({ allowed_sources: { 'orgs' => 'not-an-array' } }) + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'orgs' => 'not-an-array' } }) expect(message).not_to be_valid - expect(message.errors_on(:allowed_sources)).to include('orgs must be an array of strings') + expect(message.errors_on(:mtls_allowed_sources)).to include('orgs must be an array of strings') end it 'does not allow non-boolean any' do - message = RouteOptionsMessage.new({ allowed_sources: { 'any' => 'true' } }) + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'any' => 'true' } }) expect(message).not_to be_valid - expect(message.errors_on(:allowed_sources)).to include('any must be a boolean') + expect(message.errors_on(:mtls_allowed_sources)).to include('any must be a boolean') end end describe 'any exclusivity validation' do it 'does not allow any: true with apps list' do app = AppModel.make - message = RouteOptionsMessage.new({ allowed_sources: { 'any' => true, 'apps' => [app.guid] } }) + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'any' => true, 'apps' => [app.guid] } }) expect(message).not_to be_valid - expect(message.errors_on(:allowed_sources)).to include('any is mutually exclusive with apps, spaces, and orgs') + expect(message.errors_on(:mtls_allowed_sources)).to include('any is mutually exclusive with apps, spaces, and orgs') end it 'does not allow any: true with spaces list' do space = Space.make - message = RouteOptionsMessage.new({ allowed_sources: { 'any' => true, 'spaces' => [space.guid] } }) + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'any' => true, 'spaces' => [space.guid] } }) expect(message).not_to be_valid - expect(message.errors_on(:allowed_sources)).to include('any is mutually exclusive with apps, spaces, and orgs') + expect(message.errors_on(:mtls_allowed_sources)).to include('any is mutually exclusive with apps, spaces, and orgs') end it 'does not allow any: true with orgs list' do org = Organization.make - message = RouteOptionsMessage.new({ allowed_sources: { 'any' => true, 'orgs' => [org.guid] } }) + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'any' => true, 'orgs' => [org.guid] } }) expect(message).not_to be_valid - expect(message.errors_on(:allowed_sources)).to include('any is mutually exclusive with apps, spaces, and orgs') + expect(message.errors_on(:mtls_allowed_sources)).to include('any is mutually exclusive with apps, spaces, and orgs') end it 'allows any: false with apps list' do app = AppModel.make - message = RouteOptionsMessage.new({ allowed_sources: { 'any' => false, 'apps' => [app.guid] } }) + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'any' => false, 'apps' => [app.guid] } }) expect(message).to be_valid end it 'allows any: true with empty apps list' do - message = RouteOptionsMessage.new({ allowed_sources: { 'any' => true, 'apps' => [] } }) + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'any' => true, 'apps' => [] } }) expect(message).to be_valid end end describe 'GUID existence validation' do it 'validates that app GUIDs exist' do - message = RouteOptionsMessage.new({ allowed_sources: { 'apps' => ['non-existent-app-guid'] } }) + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'apps' => ['non-existent-app-guid'] } }) expect(message).not_to be_valid - expect(message.errors_on(:allowed_sources)).to include('apps contains non-existent app GUIDs: non-existent-app-guid') + expect(message.errors_on(:mtls_allowed_sources)).to include('apps contains non-existent app GUIDs: non-existent-app-guid') end it 'validates that space GUIDs exist' do - message = RouteOptionsMessage.new({ allowed_sources: { 'spaces' => ['non-existent-space-guid'] } }) + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'spaces' => ['non-existent-space-guid'] } }) expect(message).not_to be_valid - expect(message.errors_on(:allowed_sources)).to include('spaces contains non-existent space GUIDs: non-existent-space-guid') + expect(message.errors_on(:mtls_allowed_sources)).to include('spaces contains non-existent space GUIDs: non-existent-space-guid') end it 'validates that org GUIDs exist' do - message = RouteOptionsMessage.new({ allowed_sources: { 'orgs' => ['non-existent-org-guid'] } }) + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'orgs' => ['non-existent-org-guid'] } }) expect(message).not_to be_valid - expect(message.errors_on(:allowed_sources)).to include('orgs contains non-existent organization GUIDs: non-existent-org-guid') + expect(message.errors_on(:mtls_allowed_sources)).to include('orgs contains non-existent organization GUIDs: non-existent-org-guid') end it 'reports multiple non-existent app GUIDs' do - message = RouteOptionsMessage.new({ allowed_sources: { 'apps' => ['guid-1', 'guid-2'] } }) + message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'apps' => ['guid-1', 'guid-2'] } }) expect(message).not_to be_valid - expect(message.errors_on(:allowed_sources)).to include('apps contains non-existent app GUIDs: guid-1, guid-2') + expect(message.errors_on(:mtls_allowed_sources)).to include('apps contains non-existent app GUIDs: guid-1, guid-2') end it 'allows mix of existing apps, spaces, and orgs' do @@ -198,7 +198,7 @@ module VCAP::CloudController space = Space.make org = Organization.make message = RouteOptionsMessage.new({ - allowed_sources: { + mtls_allowed_sources: { 'apps' => [app.guid], 'spaces' => [space.guid], 'orgs' => [org.guid] @@ -210,24 +210,24 @@ module VCAP::CloudController it 'validates all types of GUIDs when multiple are provided' do app = AppModel.make message = RouteOptionsMessage.new({ - allowed_sources: { + mtls_allowed_sources: { 'apps' => [app.guid], 'spaces' => ['non-existent-space'], 'orgs' => ['non-existent-org'] } }) expect(message).not_to be_valid - expect(message.errors_on(:allowed_sources)).to include('spaces contains non-existent space GUIDs: non-existent-space') - expect(message.errors_on(:allowed_sources)).to include('orgs contains non-existent organization GUIDs: non-existent-org') + expect(message.errors_on(:mtls_allowed_sources)).to include('spaces contains non-existent space GUIDs: non-existent-space') + expect(message.errors_on(:mtls_allowed_sources)).to include('orgs contains non-existent organization GUIDs: non-existent-org') end end describe 'combined with other options' do - it 'allows allowed_sources with loadbalancing' do + it 'allows mtls_allowed_sources with loadbalancing' do app = AppModel.make message = RouteOptionsMessage.new({ loadbalancing: 'round-robin', - allowed_sources: { 'apps' => [app.guid] } + mtls_allowed_sources: { 'apps' => [app.guid] } }) expect(message).to be_valid end From 87293047f021bd0a18c02595db1217937aa4f184 Mon Sep 17 00:00:00 2001 From: rkoster Date: Thu, 5 Mar 2026 15:06:16 +0000 Subject: [PATCH 05/29] Refactor mTLS route options to RFC-0027 compliant flat format Change from nested mtls_allowed_sources object to flat options: - mtls_allowed_apps: comma-separated app GUIDs (string) - mtls_allowed_spaces: comma-separated space GUIDs (string) - mtls_allowed_orgs: comma-separated org GUIDs (string) - mtls_allow_any: boolean (true/false) This complies with RFC-0027 which requires route options to only use numbers, strings, and boolean values (no nested objects or arrays). --- app/messages/route_options_message.rb | 92 ++++--- .../messages/route_options_message_spec.rb | 235 +++++++++--------- 2 files changed, 168 insertions(+), 159 deletions(-) diff --git a/app/messages/route_options_message.rb b/app/messages/route_options_message.rb index ab688c6bbb6..c8b6d82a115 100644 --- a/app/messages/route_options_message.rb +++ b/app/messages/route_options_message.rb @@ -3,12 +3,15 @@ module VCAP::CloudController class RouteOptionsMessage < BaseMessage # Register all possible keys upfront so attr_accessors are created - register_allowed_keys %i[loadbalancing hash_header hash_balance mtls_allowed_sources] + # RFC-0027 compliant: only string/number/boolean values (no nested objects/arrays) + # mtls_allowed_apps, mtls_allowed_spaces, mtls_allowed_orgs are comma-separated GUIDs + # mtls_allow_any is a boolean + register_allowed_keys %i[loadbalancing hash_header hash_balance mtls_allowed_apps mtls_allowed_spaces mtls_allowed_orgs mtls_allow_any] def self.valid_route_options options = %i[loadbalancing] options += %i[hash_header hash_balance] if VCAP::CloudController::FeatureFlag.enabled?(:hash_based_routing) - options += %i[mtls_allowed_sources] if VCAP::CloudController::FeatureFlag.enabled?(:app_to_app_mtls_routing) + options += %i[mtls_allowed_apps mtls_allowed_spaces mtls_allowed_orgs mtls_allow_any] if VCAP::CloudController::FeatureFlag.enabled?(:app_to_app_mtls_routing) options.freeze end @@ -86,61 +89,56 @@ def validate_hash_options_with_loadbalancing end def mtls_allowed_sources_options_are_valid - # Only validate mtls_allowed_sources when the feature flag is enabled - # If disabled, route_options_are_valid will already report it as unknown field + # Only validate mtls options when the feature flag is enabled + # If disabled, route_options_are_valid will already report them as unknown fields return unless VCAP::CloudController::FeatureFlag.enabled?(:app_to_app_mtls_routing) - return if mtls_allowed_sources.blank? - validate_mtls_allowed_sources_structure - validate_mtls_allowed_sources_any_exclusivity - validate_mtls_allowed_sources_guids_exist + validate_mtls_string_types + validate_mtls_allow_any_type + validate_mtls_allow_any_exclusivity + validate_mtls_guids_exist end private - # Normalize mtls_allowed_sources to use string keys (Rails may parse JSON with symbol keys) - def normalized_mtls_allowed_sources - @normalized_mtls_allowed_sources ||= mtls_allowed_sources.is_a?(Hash) ? mtls_allowed_sources.transform_keys(&:to_s) : mtls_allowed_sources - end - - def validate_mtls_allowed_sources_structure - unless mtls_allowed_sources.is_a?(Hash) - errors.add(:mtls_allowed_sources, 'must be an object') - return - end + # Parse comma-separated GUIDs into an array + def parse_guid_list(value) + return [] if value.blank? - valid_keys = %w[apps spaces orgs any] - invalid_keys = normalized_mtls_allowed_sources.keys - valid_keys - errors.add(:mtls_allowed_sources, "contains invalid keys: #{invalid_keys.join(', ')}") if invalid_keys.any? + value.to_s.split(',').map(&:strip).reject(&:empty?) + end - # Validate types - %w[apps spaces orgs].each do |key| - next unless normalized_mtls_allowed_sources[key].present? + def validate_mtls_string_types + # These should be strings (comma-separated GUIDs) per RFC-0027 + %i[mtls_allowed_apps mtls_allowed_spaces mtls_allowed_orgs].each do |key| + value = public_send(key) + next if value.blank? - unless normalized_mtls_allowed_sources[key].is_a?(Array) && normalized_mtls_allowed_sources[key].all? { |v| v.is_a?(String) } - errors.add(:mtls_allowed_sources, "#{key} must be an array of strings") + unless value.is_a?(String) + errors.add(key, 'must be a string of comma-separated GUIDs') end end + end - return unless normalized_mtls_allowed_sources['any'].present? && ![true, false].include?(normalized_mtls_allowed_sources['any']) + def validate_mtls_allow_any_type + return if mtls_allow_any.nil? - errors.add(:mtls_allowed_sources, 'any must be a boolean') + unless [true, false, 'true', 'false'].include?(mtls_allow_any) + errors.add(:mtls_allow_any, 'must be a boolean (true or false)') + end end - def validate_mtls_allowed_sources_any_exclusivity - return unless mtls_allowed_sources.is_a?(Hash) - - has_any = normalized_mtls_allowed_sources['any'] == true - has_lists = %w[apps spaces orgs].any? { |key| normalized_mtls_allowed_sources[key].present? && normalized_mtls_allowed_sources[key].any? } + def validate_mtls_allow_any_exclusivity + allow_any = mtls_allow_any == true || mtls_allow_any == 'true' + has_specific = [mtls_allowed_apps, mtls_allowed_spaces, mtls_allowed_orgs].any?(&:present?) - return unless has_any && has_lists + return unless allow_any && has_specific - errors.add(:mtls_allowed_sources, 'any is mutually exclusive with apps, spaces, and orgs') + errors.add(:mtls_allow_any, 'is mutually exclusive with mtls_allowed_apps, mtls_allowed_spaces, and mtls_allowed_orgs') end - def validate_mtls_allowed_sources_guids_exist - return unless mtls_allowed_sources.is_a?(Hash) - return if errors[:mtls_allowed_sources].any? # Skip if already invalid + def validate_mtls_guids_exist + return if errors.any? # Skip if already invalid validate_app_guids_exist validate_space_guids_exist @@ -148,36 +146,36 @@ def validate_mtls_allowed_sources_guids_exist end def validate_app_guids_exist - app_guids = normalized_mtls_allowed_sources['apps'] - return if app_guids.blank? + app_guids = parse_guid_list(mtls_allowed_apps) + return if app_guids.empty? existing_guids = AppModel.where(guid: app_guids).select_map(:guid) missing_guids = app_guids - existing_guids return if missing_guids.empty? - errors.add(:mtls_allowed_sources, "apps contains non-existent app GUIDs: #{missing_guids.join(', ')}") + errors.add(:mtls_allowed_apps, "contains non-existent app GUIDs: #{missing_guids.join(', ')}") end def validate_space_guids_exist - space_guids = normalized_mtls_allowed_sources['spaces'] - return if space_guids.blank? + space_guids = parse_guid_list(mtls_allowed_spaces) + return if space_guids.empty? existing_guids = Space.where(guid: space_guids).select_map(:guid) missing_guids = space_guids - existing_guids return if missing_guids.empty? - errors.add(:mtls_allowed_sources, "spaces contains non-existent space GUIDs: #{missing_guids.join(', ')}") + errors.add(:mtls_allowed_spaces, "contains non-existent space GUIDs: #{missing_guids.join(', ')}") end def validate_org_guids_exist - org_guids = normalized_mtls_allowed_sources['orgs'] - return if org_guids.blank? + org_guids = parse_guid_list(mtls_allowed_orgs) + return if org_guids.empty? existing_guids = Organization.where(guid: org_guids).select_map(:guid) missing_guids = org_guids - existing_guids return if missing_guids.empty? - errors.add(:mtls_allowed_sources, "orgs contains non-existent organization GUIDs: #{missing_guids.join(', ')}") + errors.add(:mtls_allowed_orgs, "contains non-existent organization GUIDs: #{missing_guids.join(', ')}") end end end diff --git a/spec/unit/messages/route_options_message_spec.rb b/spec/unit/messages/route_options_message_spec.rb index c9d86df339c..f081ecc942b 100644 --- a/spec/unit/messages/route_options_message_spec.rb +++ b/spec/unit/messages/route_options_message_spec.rb @@ -37,12 +37,30 @@ module VCAP::CloudController end end - describe 'mtls_allowed_sources validations' do + describe 'mTLS allowed sources validations (RFC-0027 compliant flat options)' do context 'when app_to_app_mtls_routing feature flag is disabled' do - it 'does not allow mtls_allowed_sources option' do - message = RouteOptionsMessage.new({ mtls_allowed_sources: { apps: ['app-guid-1'] } }) + it 'does not allow mtls_allowed_apps option' do + message = RouteOptionsMessage.new({ mtls_allowed_apps: 'app-guid-1' }) expect(message).not_to be_valid - expect(message.errors_on(:base)).to include("Unknown field(s): 'mtls_allowed_sources'") + expect(message.errors_on(:base)).to include("Unknown field(s): 'mtls_allowed_apps'") + end + + it 'does not allow mtls_allowed_spaces option' do + message = RouteOptionsMessage.new({ mtls_allowed_spaces: 'space-guid-1' }) + expect(message).not_to be_valid + expect(message.errors_on(:base)).to include("Unknown field(s): 'mtls_allowed_spaces'") + end + + it 'does not allow mtls_allowed_orgs option' do + message = RouteOptionsMessage.new({ mtls_allowed_orgs: 'org-guid-1' }) + expect(message).not_to be_valid + expect(message.errors_on(:base)).to include("Unknown field(s): 'mtls_allowed_orgs'") + end + + it 'does not allow mtls_allow_any option' do + message = RouteOptionsMessage.new({ mtls_allow_any: true }) + expect(message).not_to be_valid + expect(message.errors_on(:base)).to include("Unknown field(s): 'mtls_allow_any'") end end @@ -51,183 +69,176 @@ module VCAP::CloudController VCAP::CloudController::FeatureFlag.make(name: 'app_to_app_mtls_routing', enabled: true) end - describe 'structure validation' do - it 'allows valid mtls_allowed_sources with apps' do - app = AppModel.make - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'apps' => [app.guid] } }) - expect(message).to be_valid - end - - it 'allows valid mtls_allowed_sources with spaces' do - space = Space.make - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'spaces' => [space.guid] } }) + describe 'mtls_allowed_apps validation' do + it 'allows valid comma-separated app GUIDs' do + app1 = AppModel.make + app2 = AppModel.make + message = RouteOptionsMessage.new({ mtls_allowed_apps: "#{app1.guid},#{app2.guid}" }) expect(message).to be_valid end - it 'allows valid mtls_allowed_sources with orgs' do - org = Organization.make - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'orgs' => [org.guid] } }) + it 'allows single app GUID' do + app = AppModel.make + message = RouteOptionsMessage.new({ mtls_allowed_apps: app.guid }) expect(message).to be_valid end - it 'allows valid mtls_allowed_sources with any: true' do - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'any' => true } }) + it 'allows app GUIDs with whitespace around commas' do + app1 = AppModel.make + app2 = AppModel.make + message = RouteOptionsMessage.new({ mtls_allowed_apps: "#{app1.guid} , #{app2.guid}" }) expect(message).to be_valid end - it 'allows valid mtls_allowed_sources with any: false' do - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'any' => false } }) - expect(message).to be_valid + it 'rejects non-existent app GUIDs' do + message = RouteOptionsMessage.new({ mtls_allowed_apps: 'non-existent-guid' }) + expect(message).not_to be_valid + expect(message.errors_on(:mtls_allowed_apps)).to include('contains non-existent app GUIDs: non-existent-guid') end - it 'allows empty mtls_allowed_sources object' do - message = RouteOptionsMessage.new({ mtls_allowed_sources: {} }) - expect(message).to be_valid + it 'reports multiple non-existent app GUIDs' do + message = RouteOptionsMessage.new({ mtls_allowed_apps: 'guid-1,guid-2' }) + expect(message).not_to be_valid + expect(message.errors_on(:mtls_allowed_apps)).to include('contains non-existent app GUIDs: guid-1, guid-2') end - it 'does not allow non-object mtls_allowed_sources' do - message = RouteOptionsMessage.new({ mtls_allowed_sources: 'invalid' }) + it 'rejects non-string values' do + message = RouteOptionsMessage.new({ mtls_allowed_apps: ['array-not-string'] }) expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_sources)).to include('must be an object') + expect(message.errors_on(:mtls_allowed_apps)).to include('must be a string of comma-separated GUIDs') end + end - it 'does not allow array mtls_allowed_sources' do - message = RouteOptionsMessage.new({ mtls_allowed_sources: ['app-guid-1'] }) - expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_sources)).to include('must be an object') + describe 'mtls_allowed_spaces validation' do + it 'allows valid comma-separated space GUIDs' do + space1 = Space.make + space2 = Space.make + message = RouteOptionsMessage.new({ mtls_allowed_spaces: "#{space1.guid},#{space2.guid}" }) + expect(message).to be_valid end - it 'does not allow invalid keys in mtls_allowed_sources' do - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'invalid_key' => 'value' } }) - expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_sources)).to include('contains invalid keys: invalid_key') + it 'allows single space GUID' do + space = Space.make + message = RouteOptionsMessage.new({ mtls_allowed_spaces: space.guid }) + expect(message).to be_valid end - it 'does not allow non-array apps' do - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'apps' => 'not-an-array' } }) + it 'rejects non-existent space GUIDs' do + message = RouteOptionsMessage.new({ mtls_allowed_spaces: 'non-existent-space' }) expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_sources)).to include('apps must be an array of strings') + expect(message.errors_on(:mtls_allowed_spaces)).to include('contains non-existent space GUIDs: non-existent-space') end - it 'does not allow non-string elements in apps array' do - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'apps' => [123, 456] } }) + it 'rejects non-string values' do + message = RouteOptionsMessage.new({ mtls_allowed_spaces: { 'nested' => 'object' } }) expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_sources)).to include('apps must be an array of strings') + expect(message.errors_on(:mtls_allowed_spaces)).to include('must be a string of comma-separated GUIDs') end + end - it 'does not allow non-array spaces' do - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'spaces' => 'not-an-array' } }) - expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_sources)).to include('spaces must be an array of strings') + describe 'mtls_allowed_orgs validation' do + it 'allows valid comma-separated org GUIDs' do + org1 = Organization.make + org2 = Organization.make + message = RouteOptionsMessage.new({ mtls_allowed_orgs: "#{org1.guid},#{org2.guid}" }) + expect(message).to be_valid end - it 'does not allow non-array orgs' do - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'orgs' => 'not-an-array' } }) - expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_sources)).to include('orgs must be an array of strings') + it 'allows single org GUID' do + org = Organization.make + message = RouteOptionsMessage.new({ mtls_allowed_orgs: org.guid }) + expect(message).to be_valid end - it 'does not allow non-boolean any' do - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'any' => 'true' } }) + it 'rejects non-existent org GUIDs' do + message = RouteOptionsMessage.new({ mtls_allowed_orgs: 'non-existent-org' }) expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_sources)).to include('any must be a boolean') + expect(message.errors_on(:mtls_allowed_orgs)).to include('contains non-existent organization GUIDs: non-existent-org') end end - describe 'any exclusivity validation' do - it 'does not allow any: true with apps list' do - app = AppModel.make - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'any' => true, 'apps' => [app.guid] } }) - expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_sources)).to include('any is mutually exclusive with apps, spaces, and orgs') + describe 'mtls_allow_any validation' do + it 'allows true value' do + message = RouteOptionsMessage.new({ mtls_allow_any: true }) + expect(message).to be_valid end - it 'does not allow any: true with spaces list' do - space = Space.make - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'any' => true, 'spaces' => [space.guid] } }) - expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_sources)).to include('any is mutually exclusive with apps, spaces, and orgs') + it 'allows false value' do + message = RouteOptionsMessage.new({ mtls_allow_any: false }) + expect(message).to be_valid end - it 'does not allow any: true with orgs list' do - org = Organization.make - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'any' => true, 'orgs' => [org.guid] } }) - expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_sources)).to include('any is mutually exclusive with apps, spaces, and orgs') + it 'allows string "true"' do + message = RouteOptionsMessage.new({ mtls_allow_any: 'true' }) + expect(message).to be_valid end - it 'allows any: false with apps list' do - app = AppModel.make - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'any' => false, 'apps' => [app.guid] } }) + it 'allows string "false"' do + message = RouteOptionsMessage.new({ mtls_allow_any: 'false' }) expect(message).to be_valid end - it 'allows any: true with empty apps list' do - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'any' => true, 'apps' => [] } }) - expect(message).to be_valid + it 'rejects non-boolean values' do + message = RouteOptionsMessage.new({ mtls_allow_any: 'yes' }) + expect(message).not_to be_valid + expect(message.errors_on(:mtls_allow_any)).to include('must be a boolean (true or false)') end end - describe 'GUID existence validation' do - it 'validates that app GUIDs exist' do - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'apps' => ['non-existent-app-guid'] } }) + describe 'mtls_allow_any exclusivity validation' do + it 'does not allow mtls_allow_any with mtls_allowed_apps' do + app = AppModel.make + message = RouteOptionsMessage.new({ mtls_allow_any: true, mtls_allowed_apps: app.guid }) expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_sources)).to include('apps contains non-existent app GUIDs: non-existent-app-guid') + expect(message.errors_on(:mtls_allow_any)).to include('is mutually exclusive with mtls_allowed_apps, mtls_allowed_spaces, and mtls_allowed_orgs') end - it 'validates that space GUIDs exist' do - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'spaces' => ['non-existent-space-guid'] } }) + it 'does not allow mtls_allow_any with mtls_allowed_spaces' do + space = Space.make + message = RouteOptionsMessage.new({ mtls_allow_any: true, mtls_allowed_spaces: space.guid }) expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_sources)).to include('spaces contains non-existent space GUIDs: non-existent-space-guid') + expect(message.errors_on(:mtls_allow_any)).to include('is mutually exclusive with mtls_allowed_apps, mtls_allowed_spaces, and mtls_allowed_orgs') end - it 'validates that org GUIDs exist' do - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'orgs' => ['non-existent-org-guid'] } }) + it 'does not allow mtls_allow_any with mtls_allowed_orgs' do + org = Organization.make + message = RouteOptionsMessage.new({ mtls_allow_any: true, mtls_allowed_orgs: org.guid }) expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_sources)).to include('orgs contains non-existent organization GUIDs: non-existent-org-guid') + expect(message.errors_on(:mtls_allow_any)).to include('is mutually exclusive with mtls_allowed_apps, mtls_allowed_spaces, and mtls_allowed_orgs') end - it 'reports multiple non-existent app GUIDs' do - message = RouteOptionsMessage.new({ mtls_allowed_sources: { 'apps' => ['guid-1', 'guid-2'] } }) + it 'allows mtls_allow_any: false with specific GUIDs' do + app = AppModel.make + message = RouteOptionsMessage.new({ mtls_allow_any: false, mtls_allowed_apps: app.guid }) + expect(message).to be_valid + end + + it 'allows string "true" exclusivity check' do + app = AppModel.make + message = RouteOptionsMessage.new({ mtls_allow_any: 'true', mtls_allowed_apps: app.guid }) expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_sources)).to include('apps contains non-existent app GUIDs: guid-1, guid-2') + expect(message.errors_on(:mtls_allow_any)).to include('is mutually exclusive with mtls_allowed_apps, mtls_allowed_spaces, and mtls_allowed_orgs') end + end - it 'allows mix of existing apps, spaces, and orgs' do + describe 'combined options' do + it 'allows all mTLS options together (without mtls_allow_any)' do app = AppModel.make space = Space.make org = Organization.make message = RouteOptionsMessage.new({ - mtls_allowed_sources: { - 'apps' => [app.guid], - 'spaces' => [space.guid], - 'orgs' => [org.guid] - } + mtls_allowed_apps: app.guid, + mtls_allowed_spaces: space.guid, + mtls_allowed_orgs: org.guid }) expect(message).to be_valid end - it 'validates all types of GUIDs when multiple are provided' do - app = AppModel.make - message = RouteOptionsMessage.new({ - mtls_allowed_sources: { - 'apps' => [app.guid], - 'spaces' => ['non-existent-space'], - 'orgs' => ['non-existent-org'] - } - }) - expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_sources)).to include('spaces contains non-existent space GUIDs: non-existent-space') - expect(message.errors_on(:mtls_allowed_sources)).to include('orgs contains non-existent organization GUIDs: non-existent-org') - end - end - - describe 'combined with other options' do - it 'allows mtls_allowed_sources with loadbalancing' do + it 'allows mTLS options with loadbalancing' do app = AppModel.make message = RouteOptionsMessage.new({ loadbalancing: 'round-robin', - mtls_allowed_sources: { 'apps' => [app.guid] } + mtls_allowed_apps: app.guid }) expect(message).to be_valid end From 8854597d3a351801c8e1a639d58572733019aa7d Mon Sep 17 00:00:00 2001 From: rkoster Date: Thu, 9 Apr 2026 07:52:15 +0000 Subject: [PATCH 06/29] Implement RFC domain-scoped mTLS routing with /v3/access_rules API Replace POC route-options-based mTLS implementation with RFC-compliant architecture: Domain model changes: - Add enforce_access_rules (boolean) and access_rules_scope (any/org/space) to domains - Fields are immutable after domain creation - Update DomainCreateMessage, DomainPresenter, and DomainCreate action Access Rules resource: - New /v3/access_rules API with full CRUD operations - RouteAccessRule model with guid, name, selector, route_id - Selector format: cf:app:, cf:space:, cf:org:, or cf:any - Enforce cf:any exclusivity and per-route name/selector uniqueness - Space Developer can manage rules for routes in their space Diego sync path: - Inject access_scope and access_rules into route options for GoRouter - Filter internal mTLS keys (access_scope, access_rules) from public /v3/routes API - Add access_rules to eager load to avoid N+1 queries Tests: - Unit tests for AccessRuleCreateMessage (selector validation, cf:any rules) - Request specs for /v3/access_rules CRUD (create, show, list, delete, metadata update) - Updated domain_create_message_spec for enforce_access_rules validation - Updated routing_info_spec to verify mTLS options injection - Updated route_presenter_spec to verify internal keys are filtered Remove POC artifacts: - Remove app_to_app_mtls_routing feature flag - Remove mtls_allowed_* keys from route_options_message --- app/access/access_rule_access.rb | 66 ++++ app/actions/domain_create.rb | 2 + app/controllers/v3/access_rules_controller.rb | 129 +++++++ ...access_rule_selector_resource_decorator.rb | 40 ++ app/messages/access_rule_create_message.rb | 52 +++ app/messages/access_rule_update_message.rb | 9 + app/messages/access_rules_list_message.rb | 17 + app/messages/domain_create_message.rb | 22 ++ app/messages/route_options_message.rb | 98 +---- app/models/runtime/domain.rb | 4 +- app/models/runtime/feature_flag.rb | 3 +- app/models/runtime/route.rb | 3 + app/models/runtime/route_access_rule.rb | 15 + app/presenters/v3/access_rule_presenter.rb | 47 +++ app/presenters/v3/domain_presenter.rb | 2 + app/presenters/v3/route_presenter.rb | 7 +- config/routes.rb | 7 + ...000_add_enforce_access_rules_to_domains.rb | 15 + ...0260407100001_create_route_access_rules.rb | 24 ++ .../diego/protocol/routing_info.rb | 13 +- spec/request/access_rules_spec.rb | 357 ++++++++++++++++++ .../diego/protocol/routing_info_spec.rb | 58 +++ .../access_rule_create_message_spec.rb | 248 ++++++++++++ .../messages/domain_create_message_spec.rb | 87 +++++ .../messages/route_options_message_spec.rb | 209 ---------- .../presenters/v3/route_presenter_spec.rb | 38 ++ 26 files changed, 1260 insertions(+), 312 deletions(-) create mode 100644 app/access/access_rule_access.rb create mode 100644 app/controllers/v3/access_rules_controller.rb create mode 100644 app/decorators/include_access_rule_selector_resource_decorator.rb create mode 100644 app/messages/access_rule_create_message.rb create mode 100644 app/messages/access_rule_update_message.rb create mode 100644 app/messages/access_rules_list_message.rb create mode 100644 app/models/runtime/route_access_rule.rb create mode 100644 app/presenters/v3/access_rule_presenter.rb create mode 100644 db/migrations/20260407100000_add_enforce_access_rules_to_domains.rb create mode 100644 db/migrations/20260407100001_create_route_access_rules.rb create mode 100644 spec/request/access_rules_spec.rb create mode 100644 spec/unit/messages/access_rule_create_message_spec.rb diff --git a/app/access/access_rule_access.rb b/app/access/access_rule_access.rb new file mode 100644 index 00000000000..72fff7ebf30 --- /dev/null +++ b/app/access/access_rule_access.rb @@ -0,0 +1,66 @@ +module VCAP::CloudController + class AccessRuleAccess < BaseAccess + # Space Developer of the route's space can manage access rules. + # No bilateral requirement — destination-controlled auth only. + + def create?(access_rule, _params=nil) + return true if admin_user? + + route = access_rule.route + return false unless route + + space = route.space + context.user_email && context.user.is_a?(User) && + space.developers.include?(context.user) + end + + def read?(access_rule) + return true if admin_user? || admin_read_only_user? || global_auditor? + + route = access_rule.route + return false unless route + + object_is_visible_to_user?(access_rule, context.user) + end + + def update?(access_rule, _params=nil) + create?(access_rule) + end + + def delete?(access_rule) + create?(access_rule) + end + + def index?(_object_class, _params=nil) + admin_user? || admin_read_only_user? || has_read_scope? || global_auditor? + end + + def read_with_token?(_) + admin_user? || admin_read_only_user? || has_read_scope? || global_auditor? + end + + def create_with_token?(_) + admin_user? || has_write_scope? + end + + def read_for_update_with_token?(_) + admin_user? || has_write_scope? + end + + def can_remove_related_object_with_token?(*args) + read_for_update_with_token?(*args) + end + + def read_related_object_for_update_with_token?(*args) + read_for_update_with_token?(*args) + end + + def update_with_token?(_) + admin_user? || has_write_scope? + end + + def delete_with_token?(_) + admin_user? || has_write_scope? + end + end +end diff --git a/app/actions/domain_create.rb b/app/actions/domain_create.rb index f69d05cd7a5..2ebbe778c14 100644 --- a/app/actions/domain_create.rb +++ b/app/actions/domain_create.rb @@ -21,6 +21,8 @@ def create(message:, shared_organizations: []) end domain.router_group_guid = message.router_group_guid + domain.enforce_access_rules = message.enforce_access_rules || false + domain.access_rules_scope = message.access_rules_scope Domain.db.transaction do domain.save diff --git a/app/controllers/v3/access_rules_controller.rb b/app/controllers/v3/access_rules_controller.rb new file mode 100644 index 00000000000..a45b982bb64 --- /dev/null +++ b/app/controllers/v3/access_rules_controller.rb @@ -0,0 +1,129 @@ +require 'messages/access_rule_create_message' +require 'messages/access_rule_update_message' +require 'messages/access_rules_list_message' +require 'presenters/v3/access_rule_presenter' + +class AccessRulesController < ApplicationController + def index + message = AccessRulesListMessage.from_params(query_params) + invalid_param!(message.errors.full_messages) unless message.valid? + + dataset = build_dataset(message) + + render status: :ok, json: Presenters::V3::PaginatedListPresenter.new( + presenter: Presenters::V3::AccessRulePresenter, + paginated_result: SequelPaginator.new.get_page(dataset, message.try(:pagination_options)), + path: '/v3/access_rules', + message: message + ) + end + + def show + access_rule = VCAP::CloudController::RouteAccessRule.find(guid: hashed_params[:guid]) + resource_not_found!(:access_rule) unless access_rule + + route = access_rule.route + resource_not_found!(:access_rule) unless route && permission_queryer.can_read_from_space?(route.space.id, route.space.organization_id) + + render status: :ok, json: Presenters::V3::AccessRulePresenter.new(access_rule) + end + + def create + message = AccessRuleCreateMessage.new(hashed_params[:body]) + unprocessable!(message.errors.full_messages) unless message.valid? + + route = VCAP::CloudController::Route.find(guid: message.route_guid) + resource_not_found!(:route) unless route && permission_queryer.can_read_from_space?(route.space.id, route.space.organization_id) + unauthorized! unless permission_queryer.can_write_to_active_space?(route.space.id) + suspended! unless permission_queryer.is_space_active?(route.space.id) + + unless route.domain.enforce_access_rules + unprocessable!("Cannot create access rules for route '#{route.guid}': the route's domain does not have enforce_access_rules enabled.") + end + + # Enforce cf:any exclusivity: if route already has a cf:any rule, reject new rules; + # if new rule is cf:any, reject if route already has any rules. + existing_selectors = route.access_rules.map(&:selector) + if message.selector == 'cf:any' && existing_selectors.any? + unprocessable!("Cannot add 'cf:any' selector when other access rules already exist for this route.") + end + if existing_selectors.include?('cf:any') && message.selector != 'cf:any' + unprocessable!("Cannot add selector '#{message.selector}': route already has a 'cf:any' rule.") + end + + # Uniqueness: name and selector must be unique per route + if route.access_rules.any? { |r| r.name == message.name } + unprocessable!("An access rule with name '#{message.name}' already exists for this route.") + end + if existing_selectors.include?(message.selector) + unprocessable!("An access rule with selector '#{message.selector}' already exists for this route.") + end + + access_rule = VCAP::CloudController::RouteAccessRule.new( + guid: SecureRandom.uuid, + name: message.name, + selector: message.selector, + route_id: route.id, + created_at: Time.now.utc, + updated_at: Time.now.utc + ) + access_rule.save + + render status: :created, json: Presenters::V3::AccessRulePresenter.new(access_rule) + end + + def update + access_rule = VCAP::CloudController::RouteAccessRule.find(guid: hashed_params[:guid]) + resource_not_found!(:access_rule) unless access_rule + + route = access_rule.route + resource_not_found!(:access_rule) unless route && permission_queryer.can_read_from_space?(route.space.id, route.space.organization_id) + unauthorized! unless permission_queryer.can_write_to_active_space?(route.space.id) + suspended! unless permission_queryer.is_space_active?(route.space.id) + + message = AccessRuleUpdateMessage.new(hashed_params[:body]) + unprocessable!(message.errors.full_messages) unless message.valid? + + VCAP::CloudController::MetadataUpdate.update(access_rule, message) + + render status: :ok, json: Presenters::V3::AccessRulePresenter.new(access_rule.reload) + end + + def destroy + access_rule = VCAP::CloudController::RouteAccessRule.find(guid: hashed_params[:guid]) + resource_not_found!(:access_rule) unless access_rule + + route = access_rule.route + resource_not_found!(:access_rule) unless route && permission_queryer.can_read_from_space?(route.space.id, route.space.organization_id) + unauthorized! unless permission_queryer.can_write_to_active_space?(route.space.id) + suspended! unless permission_queryer.is_space_active?(route.space.id) + + access_rule.destroy + head :no_content + end + + private + + def build_dataset(message) + dataset = VCAP::CloudController::RouteAccessRule.dataset + + readable_route_ids = VCAP::CloudController::Route. + join(:spaces, id: :space_id). + where(Sequel.lit(permission_queryer.readable_space_scoped_space_guids_query)). + select(:routes__id) + + dataset = dataset.where(route_id: readable_route_ids) + + if message.requested?(:route_guids) + dataset = dataset. + join(:routes, id: :route_id). + where(routes__guid: message.route_guids). + select_all(:route_access_rules) + end + + dataset = dataset.where(name: message.names) if message.requested?(:names) + dataset = dataset.where(selector: message.selectors) if message.requested?(:selectors) + + dataset + end +end diff --git a/app/decorators/include_access_rule_selector_resource_decorator.rb b/app/decorators/include_access_rule_selector_resource_decorator.rb new file mode 100644 index 00000000000..c5ac7552860 --- /dev/null +++ b/app/decorators/include_access_rule_selector_resource_decorator.rb @@ -0,0 +1,40 @@ +module VCAP::CloudController + class IncludeAccessRuleSelectorResourceDecorator + # Handles `?include=selector_resource` for GET /v3/access_rules + # Stale/missing resources (selector GUIDs that no longer exist) are silently absent. + + SELECTOR_REGEX = /\Acf:(app|space|org):([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\z/ + + def self.match?(include_params) + include_params&.include?('selector_resource') + end + + def self.decorate(hash, access_rules) + included = [] + + access_rules.each do |rule| + match = SELECTOR_REGEX.match(rule.selector) + next unless match + + resource_type = match[1] + resource_guid = match[2] + + resource = case resource_type + when 'app' + VCAP::CloudController::AppModel.find(guid: resource_guid) + when 'space' + VCAP::CloudController::Space.find(guid: resource_guid) + when 'org' + VCAP::CloudController::Organization.find(guid: resource_guid) + end + + next if resource.nil? + + included << { type: resource_type, guid: resource.guid } + end + + hash[:included] = { selector_resources: included } + hash + end + end +end diff --git a/app/messages/access_rule_create_message.rb b/app/messages/access_rule_create_message.rb new file mode 100644 index 00000000000..f3086bf95ee --- /dev/null +++ b/app/messages/access_rule_create_message.rb @@ -0,0 +1,52 @@ +require 'messages/metadata_base_message' + +module VCAP::CloudController + class AccessRuleCreateMessage < MetadataBaseMessage + SELECTOR_REGEX = /\A(cf:(app|space|org):[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|cf:any)\z/ + + register_allowed_keys %i[ + name + selector + relationships + ] + + validates_with NoAdditionalKeysValidator + validates_with RelationshipValidator + + validates :name, presence: true, string: true + validates :selector, presence: true, string: true + + validate :selector_format_valid + validate :selector_not_cf_any_with_others + + delegate :route_guid, to: :relationships_message + + def relationships_message + @relationships_message ||= Relationships.new(relationships&.deep_symbolize_keys) + end + + private + + def selector_format_valid + return unless selector.is_a?(String) + return if SELECTOR_REGEX.match?(selector) + + errors.add(:selector, "must be in format 'cf:app:', 'cf:space:', 'cf:org:', or 'cf:any'") + end + + def selector_not_cf_any_with_others + # enforced at the controller level when checking existing rules on the route + end + + class Relationships < BaseMessage + register_allowed_keys [:route] + + validates_with NoAdditionalKeysValidator + validates :route, presence: true, to_one_relationship: true + + def route_guid + HashUtils.dig(route, :data, :guid) + end + end + end +end diff --git a/app/messages/access_rule_update_message.rb b/app/messages/access_rule_update_message.rb new file mode 100644 index 00000000000..b9adcf62a4a --- /dev/null +++ b/app/messages/access_rule_update_message.rb @@ -0,0 +1,9 @@ +require 'messages/metadata_base_message' + +module VCAP::CloudController + class AccessRuleUpdateMessage < MetadataBaseMessage + register_allowed_keys [] + + validates_with NoAdditionalKeysValidator + end +end diff --git a/app/messages/access_rules_list_message.rb b/app/messages/access_rules_list_message.rb new file mode 100644 index 00000000000..7c7973fda97 --- /dev/null +++ b/app/messages/access_rules_list_message.rb @@ -0,0 +1,17 @@ +require 'messages/list_message' + +module VCAP::CloudController + class AccessRulesListMessage < ListMessage + register_allowed_keys %i[ + route_guids + names + selectors + ] + + validates_with NoAdditionalParamsValidator + + def self.from_params(params) + super(params, %w[route_guids names selectors]) + end + end +end diff --git a/app/messages/domain_create_message.rb b/app/messages/domain_create_message.rb index 110bc0d499b..b10d065b553 100644 --- a/app/messages/domain_create_message.rb +++ b/app/messages/domain_create_message.rb @@ -16,6 +16,8 @@ class DomainCreateMessage < MetadataBaseMessage internal relationships router_group + enforce_access_rules + access_rules_scope ] def self.relationships_requested? @@ -59,6 +61,12 @@ def self.relationships_requested? allow_nil: true, boolean: true + validates :enforce_access_rules, + allow_nil: true, + boolean: true + + validate :access_rules_scope_validation + delegate :organization_guid, to: :relationships_message delegate :shared_organizations_guids, to: :relationships_message @@ -97,6 +105,20 @@ def router_group_validation errors.add(:router_group, 'guid must be a string') unless router_group_guid.is_a?(String) end + def access_rules_scope_validation + if requested?(:access_rules_scope) + unless access_rules_scope.nil? || %w[any org space].include?(access_rules_scope) + errors.add(:access_rules_scope, "must be one of 'any', 'org', 'space'") + end + end + + if requested?(:enforce_access_rules) && enforce_access_rules == true + if !requested?(:access_rules_scope) || access_rules_scope.nil? + errors.add(:access_rules_scope, 'is required when enforce_access_rules is true') + end + end + end + class Relationships < BaseMessage def self.shared_organizations_requested? @shared_organizations_requested ||= proc { |a| a.requested?(:shared_organizations) } diff --git a/app/messages/route_options_message.rb b/app/messages/route_options_message.rb index c8b6d82a115..7371b391558 100644 --- a/app/messages/route_options_message.rb +++ b/app/messages/route_options_message.rb @@ -2,16 +2,11 @@ module VCAP::CloudController class RouteOptionsMessage < BaseMessage - # Register all possible keys upfront so attr_accessors are created - # RFC-0027 compliant: only string/number/boolean values (no nested objects/arrays) - # mtls_allowed_apps, mtls_allowed_spaces, mtls_allowed_orgs are comma-separated GUIDs - # mtls_allow_any is a boolean - register_allowed_keys %i[loadbalancing hash_header hash_balance mtls_allowed_apps mtls_allowed_spaces mtls_allowed_orgs mtls_allow_any] + register_allowed_keys %i[loadbalancing hash_header hash_balance] def self.valid_route_options options = %i[loadbalancing] options += %i[hash_header hash_balance] if VCAP::CloudController::FeatureFlag.enabled?(:hash_based_routing) - options += %i[mtls_allowed_apps mtls_allowed_spaces mtls_allowed_orgs mtls_allow_any] if VCAP::CloudController::FeatureFlag.enabled?(:app_to_app_mtls_routing) options.freeze end @@ -25,7 +20,6 @@ def self.valid_loadbalancing_algorithms validate :loadbalancing_algorithm_is_valid validate :route_options_are_valid validate :hash_options_are_valid - validate :mtls_allowed_sources_options_are_valid def loadbalancing_algorithm_is_valid return if loadbalancing.blank? @@ -87,95 +81,5 @@ def validate_hash_options_with_loadbalancing errors.add(:base, 'Hash header can only be set when loadbalancing is hash') if hash_header.present? && loadbalancing.present? && loadbalancing != 'hash' errors.add(:base, 'Hash balance can only be set when loadbalancing is hash') if hash_balance.present? && loadbalancing.present? && loadbalancing != 'hash' end - - def mtls_allowed_sources_options_are_valid - # Only validate mtls options when the feature flag is enabled - # If disabled, route_options_are_valid will already report them as unknown fields - return unless VCAP::CloudController::FeatureFlag.enabled?(:app_to_app_mtls_routing) - - validate_mtls_string_types - validate_mtls_allow_any_type - validate_mtls_allow_any_exclusivity - validate_mtls_guids_exist - end - - private - - # Parse comma-separated GUIDs into an array - def parse_guid_list(value) - return [] if value.blank? - - value.to_s.split(',').map(&:strip).reject(&:empty?) - end - - def validate_mtls_string_types - # These should be strings (comma-separated GUIDs) per RFC-0027 - %i[mtls_allowed_apps mtls_allowed_spaces mtls_allowed_orgs].each do |key| - value = public_send(key) - next if value.blank? - - unless value.is_a?(String) - errors.add(key, 'must be a string of comma-separated GUIDs') - end - end - end - - def validate_mtls_allow_any_type - return if mtls_allow_any.nil? - - unless [true, false, 'true', 'false'].include?(mtls_allow_any) - errors.add(:mtls_allow_any, 'must be a boolean (true or false)') - end - end - - def validate_mtls_allow_any_exclusivity - allow_any = mtls_allow_any == true || mtls_allow_any == 'true' - has_specific = [mtls_allowed_apps, mtls_allowed_spaces, mtls_allowed_orgs].any?(&:present?) - - return unless allow_any && has_specific - - errors.add(:mtls_allow_any, 'is mutually exclusive with mtls_allowed_apps, mtls_allowed_spaces, and mtls_allowed_orgs') - end - - def validate_mtls_guids_exist - return if errors.any? # Skip if already invalid - - validate_app_guids_exist - validate_space_guids_exist - validate_org_guids_exist - end - - def validate_app_guids_exist - app_guids = parse_guid_list(mtls_allowed_apps) - return if app_guids.empty? - - existing_guids = AppModel.where(guid: app_guids).select_map(:guid) - missing_guids = app_guids - existing_guids - return if missing_guids.empty? - - errors.add(:mtls_allowed_apps, "contains non-existent app GUIDs: #{missing_guids.join(', ')}") - end - - def validate_space_guids_exist - space_guids = parse_guid_list(mtls_allowed_spaces) - return if space_guids.empty? - - existing_guids = Space.where(guid: space_guids).select_map(:guid) - missing_guids = space_guids - existing_guids - return if missing_guids.empty? - - errors.add(:mtls_allowed_spaces, "contains non-existent space GUIDs: #{missing_guids.join(', ')}") - end - - def validate_org_guids_exist - org_guids = parse_guid_list(mtls_allowed_orgs) - return if org_guids.empty? - - existing_guids = Organization.where(guid: org_guids).select_map(:guid) - missing_guids = org_guids - existing_guids - return if missing_guids.empty? - - errors.add(:mtls_allowed_orgs, "contains non-existent organization GUIDs: #{missing_guids.join(', ')}") - end end end diff --git a/app/models/runtime/domain.rb b/app/models/runtime/domain.rb index 4ca18ef9b6f..16b2435aaeb 100644 --- a/app/models/runtime/domain.rb +++ b/app/models/runtime/domain.rb @@ -79,8 +79,8 @@ def shared_or_owned_by(organization_ids) one_to_many :labels, class: 'VCAP::CloudController::DomainLabelModel', key: :resource_guid, primary_key: :guid one_to_many :annotations, class: 'VCAP::CloudController::DomainAnnotationModel', key: :resource_guid, primary_key: :guid - export_attributes :name, :owning_organization_guid, :shared_organizations - import_attributes :name, :owning_organization_guid + export_attributes :name, :owning_organization_guid, :shared_organizations, :enforce_access_rules, :access_rules_scope + import_attributes :name, :owning_organization_guid, :enforce_access_rules, :access_rules_scope strip_attributes :name add_association_dependencies labels: :destroy diff --git a/app/models/runtime/feature_flag.rb b/app/models/runtime/feature_flag.rb index 4dece1d2df0..e64b7d60e7b 100644 --- a/app/models/runtime/feature_flag.rb +++ b/app/models/runtime/feature_flag.rb @@ -24,8 +24,7 @@ class UndefinedFeatureFlagError < StandardError hide_marketplace_from_unauthenticated_users: false, resource_matching: true, route_sharing: false, - hash_based_routing: false, - app_to_app_mtls_routing: false + hash_based_routing: false }.freeze ADMIN_SKIPPABLE = %i[ diff --git a/app/models/runtime/route.rb b/app/models/runtime/route.rb index bdefff78c41..84032473a23 100644 --- a/app/models/runtime/route.rb +++ b/app/models/runtime/route.rb @@ -39,6 +39,9 @@ class InvalidOrganizationRelation < CloudController::Errors::InvalidRelation; en add_association_dependencies route_mappings: :destroy + one_to_many :access_rules, class: 'VCAP::CloudController::RouteAccessRule', key: :route_id, primary_key: :id + add_association_dependencies access_rules: :destroy + export_attributes :host, :path, :domain_guid, :space_guid, :service_instance_guid, :port, :options import_attributes :host, :path, :domain_guid, :space_guid, :app_guids, :port, :options diff --git a/app/models/runtime/route_access_rule.rb b/app/models/runtime/route_access_rule.rb new file mode 100644 index 00000000000..08ed6c6e3e2 --- /dev/null +++ b/app/models/runtime/route_access_rule.rb @@ -0,0 +1,15 @@ +module VCAP::CloudController + class RouteAccessRule < Sequel::Model(:route_access_rules) + many_to_one :route, + class: 'VCAP::CloudController::Route', + key: :route_id, + primary_key: :id, + without_guid_generation: true + + def validate + validates_presence :name + validates_presence :selector + validates_presence :route_id + end + end +end diff --git a/app/presenters/v3/access_rule_presenter.rb b/app/presenters/v3/access_rule_presenter.rb new file mode 100644 index 00000000000..cd5f18d2c47 --- /dev/null +++ b/app/presenters/v3/access_rule_presenter.rb @@ -0,0 +1,47 @@ +require 'presenters/v3/base_presenter' +require 'presenters/mixins/metadata_presentation_helpers' + +module VCAP::CloudController + module Presenters + module V3 + class AccessRulePresenter < BasePresenter + include VCAP::CloudController::Presenters::Mixins::MetadataPresentationHelpers + + def to_hash + { + guid: access_rule.guid, + created_at: access_rule.created_at, + updated_at: access_rule.updated_at, + name: access_rule.name, + selector: access_rule.selector, + relationships: { + route: { + data: { + guid: access_rule.route.guid + } + } + }, + links: build_links + } + end + + private + + def access_rule + @resource + end + + def build_links + { + self: { + href: url_builder.build_url(path: "/v3/access_rules/#{access_rule.guid}") + }, + route: { + href: url_builder.build_url(path: "/v3/routes/#{access_rule.route.guid}") + } + } + end + end + end + end +end diff --git a/app/presenters/v3/domain_presenter.rb b/app/presenters/v3/domain_presenter.rb index 9ffa51fa951..8f655fa9927 100644 --- a/app/presenters/v3/domain_presenter.rb +++ b/app/presenters/v3/domain_presenter.rb @@ -28,6 +28,8 @@ def to_hash internal: domain.internal, router_group: hashified_router_group(domain.router_group_guid), supported_protocols: domain.protocols, + enforce_access_rules: domain.enforce_access_rules || false, + access_rules_scope: domain.access_rules_scope, relationships: { organization: { data: owning_org_guid diff --git a/app/presenters/v3/route_presenter.rb b/app/presenters/v3/route_presenter.rb index c090fafae5b..8eab8b790c3 100644 --- a/app/presenters/v3/route_presenter.rb +++ b/app/presenters/v3/route_presenter.rb @@ -48,13 +48,18 @@ def to_hash }, links: build_links } - hash.merge!(options: route.options) unless route.options.nil? + unless route.options.nil? + public_options = route.options.reject { |k, _| INTERNAL_ROUTE_OPTIONS.include?(k.to_s) } + hash.merge!(options: public_options) unless public_options.empty? + end @decorators.reduce(hash) { |memo, d| d.decorate(memo, [route]) } end private + INTERNAL_ROUTE_OPTIONS = %w[access_scope access_rules].freeze + def route @resource end diff --git a/config/routes.rb b/config/routes.rb index dc1039c54c4..e6822b973a6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -338,6 +338,13 @@ post '/roles', to: 'roles#create' delete '/roles/:guid', to: 'roles#destroy' + # access_rules + get '/access_rules', to: 'access_rules#index' + get '/access_rules/:guid', to: 'access_rules#show' + post '/access_rules', to: 'access_rules#create' + patch '/access_rules/:guid', to: 'access_rules#update' + delete '/access_rules/:guid', to: 'access_rules#destroy' + # info get '/info', to: 'info#v3_info' get '/info/usage_summary', to: 'info#show_usage_summary' diff --git a/db/migrations/20260407100000_add_enforce_access_rules_to_domains.rb b/db/migrations/20260407100000_add_enforce_access_rules_to_domains.rb new file mode 100644 index 00000000000..5f2df5e415b --- /dev/null +++ b/db/migrations/20260407100000_add_enforce_access_rules_to_domains.rb @@ -0,0 +1,15 @@ +Sequel.migration do + up do + alter_table :domains do + add_column :enforce_access_rules, :boolean, default: false, null: false unless @db.schema(:domains).map(&:first).include?(:enforce_access_rules) + add_column :access_rules_scope, String, null: true, size: 255 unless @db.schema(:domains).map(&:first).include?(:access_rules_scope) + end + end + + down do + alter_table :domains do + drop_column :enforce_access_rules if @db.schema(:domains).map(&:first).include?(:enforce_access_rules) + drop_column :access_rules_scope if @db.schema(:domains).map(&:first).include?(:access_rules_scope) + end + end +end diff --git a/db/migrations/20260407100001_create_route_access_rules.rb b/db/migrations/20260407100001_create_route_access_rules.rb new file mode 100644 index 00000000000..4c8c78f4216 --- /dev/null +++ b/db/migrations/20260407100001_create_route_access_rules.rb @@ -0,0 +1,24 @@ +Sequel.migration do + up do + unless table_exists?(:route_access_rules) + create_table :route_access_rules do + String :guid, size: 255, null: false + primary_key :id + String :name, size: 255, null: false + String :selector, size: 255, null: false + Integer :route_id, null: false + DateTime :created_at, null: false + DateTime :updated_at, null: false + + index :guid, unique: true, name: :route_access_rules_guid_index + index %i[route_id name], unique: true, name: :route_access_rules_route_id_name_index + index %i[route_id selector], unique: true, name: :route_access_rules_route_id_selector_index + foreign_key [:route_id], :routes, on_delete: :cascade, name: :fk_route_access_rules_route_id + end + end + end + + down do + drop_table(:route_access_rules) if table_exists?(:route_access_rules) + end +end diff --git a/lib/cloud_controller/diego/protocol/routing_info.rb b/lib/cloud_controller/diego/protocol/routing_info.rb index e85c061a4fd..27908728008 100644 --- a/lib/cloud_controller/diego/protocol/routing_info.rb +++ b/lib/cloud_controller/diego/protocol/routing_info.rb @@ -9,7 +9,7 @@ def initialize(process) end def routing_info - process_eager = ProcessModel.eager(route_mappings: { route: %i[domain route_binding] }).where(id: process.id).all + process_eager = ProcessModel.eager(route_mappings: { route: %i[domain route_binding access_rules] }).where(id: process.id).all return {} if process_eager.empty? @@ -44,6 +44,17 @@ def http_info(process_eager) info['port'] = get_port_to_use(route_mapping) info['protocol'] = route_mapping.protocol info['options'] = r.options if r.options + + # Inject mTLS access control options for enforce_access_rules domains. + # These are GoRouter-internal keys and are filtered from the /v3/routes API. + if r.domain.enforce_access_rules + mtls_options = info['options']&.dup || {} + mtls_options['access_scope'] = r.domain.access_rules_scope if r.domain.access_rules_scope + selectors = r.access_rules.map(&:selector) + mtls_options['access_rules'] = selectors.join(',') unless selectors.empty? + info['options'] = mtls_options + end + info end end diff --git a/spec/request/access_rules_spec.rb b/spec/request/access_rules_spec.rb new file mode 100644 index 00000000000..3962cc59a66 --- /dev/null +++ b/spec/request/access_rules_spec.rb @@ -0,0 +1,357 @@ +require 'spec_helper' + +RSpec.describe 'Access Rules' do + let(:user) { VCAP::CloudController::User.make } + let(:admin_header) { admin_headers_for(user) } + let(:org) { VCAP::CloudController::Organization.make } + let(:space) { VCAP::CloudController::Space.make(organization: org) } + + let(:mtls_domain) do + VCAP::CloudController::PrivateDomain.make( + owning_organization: org, + enforce_access_rules: true, + access_rules_scope: 'space' + ) + end + let(:regular_domain) do + VCAP::CloudController::PrivateDomain.make(owning_organization: org) + end + + let(:mtls_route) { VCAP::CloudController::Route.make(space: space, domain: mtls_domain) } + let(:regular_route) { VCAP::CloudController::Route.make(space: space, domain: regular_domain) } + + let(:valid_uuid) { '11111111-2222-3333-4444-555555555555' } + + def expected_rule_json(rule) + { + guid: rule.guid, + created_at: iso8601, + updated_at: iso8601, + name: rule.name, + selector: rule.selector, + relationships: { + route: { data: { guid: rule.route.guid } } + }, + links: { + self: { href: %r{/v3/access_rules/#{rule.guid}} }, + route: { href: %r{/v3/routes/#{rule.route.guid}} } + } + } + end + + before do + TestConfig.override(kubernetes: {}) + space.organization.add_user(user) + space.add_developer(user) + end + + describe 'POST /v3/access_rules' do + let(:request_body) do + { + name: 'allow-frontend', + selector: "cf:app:#{valid_uuid}", + relationships: { + route: { data: { guid: mtls_route.guid } } + } + } + end + + context 'as admin' do + it 'creates an access rule and returns 201' do + post '/v3/access_rules', request_body.to_json, admin_header + + expect(last_response.status).to eq(201) + parsed = Oj.load(last_response.body) + expect(parsed['name']).to eq('allow-frontend') + expect(parsed['selector']).to eq("cf:app:#{valid_uuid}") + expect(parsed['relationships']['route']['data']['guid']).to eq(mtls_route.guid) + end + end + + context 'as space developer' do + let(:user_headers) { headers_for(user) } + + it 'creates an access rule' do + post '/v3/access_rules', request_body.to_json, user_headers + + expect(last_response.status).to eq(201) + end + end + + context 'when the domain does not have enforce_access_rules enabled' do + let(:request_body) do + { + name: 'disallowed-rule', + selector: "cf:app:#{valid_uuid}", + relationships: { + route: { data: { guid: regular_route.guid } } + } + } + end + + it 'returns 422' do + post '/v3/access_rules', request_body.to_json, admin_header + + expect(last_response.status).to eq(422) + expect(last_response.body).to include('enforce_access_rules') + end + end + + context 'when the route does not exist' do + let(:request_body) do + { + name: 'bad-rule', + selector: "cf:app:#{valid_uuid}", + relationships: { + route: { data: { guid: 'nonexistent-guid' } } + } + } + end + + it 'returns 404' do + post '/v3/access_rules', request_body.to_json, admin_header + + expect(last_response.status).to eq(404) + end + end + + context 'cf:any exclusivity' do + before do + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'existing-rule', + selector: "cf:app:#{valid_uuid}", + route_id: mtls_route.id + ) + end + + it 'rejects cf:any when other rules exist' do + post '/v3/access_rules', { + name: 'any-rule', + selector: 'cf:any', + relationships: { route: { data: { guid: mtls_route.guid } } } + }.to_json, admin_header + + expect(last_response.status).to eq(422) + expect(last_response.body).to include("cf:any") + end + end + + context 'when a cf:any rule already exists' do + before do + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'any-rule', + selector: 'cf:any', + route_id: mtls_route.id + ) + end + + it 'rejects adding a specific selector' do + post '/v3/access_rules', { + name: 'specific-rule', + selector: "cf:space:#{valid_uuid}", + relationships: { route: { data: { guid: mtls_route.guid } } } + }.to_json, admin_header + + expect(last_response.status).to eq(422) + expect(last_response.body).to include("cf:any") + end + end + + context 'duplicate name per route' do + before do + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'allow-frontend', + selector: "cf:app:#{valid_uuid}", + route_id: mtls_route.id + ) + end + + it 'returns 422' do + other_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' + post '/v3/access_rules', { + name: 'allow-frontend', + selector: "cf:space:#{other_uuid}", + relationships: { route: { data: { guid: mtls_route.guid } } } + }.to_json, admin_header + + expect(last_response.status).to eq(422) + expect(last_response.body).to include('allow-frontend') + end + end + + context 'duplicate selector per route' do + before do + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'first-rule', + selector: "cf:app:#{valid_uuid}", + route_id: mtls_route.id + ) + end + + it 'returns 422' do + post '/v3/access_rules', { + name: 'second-rule', + selector: "cf:app:#{valid_uuid}", + relationships: { route: { data: { guid: mtls_route.guid } } } + }.to_json, admin_header + + expect(last_response.status).to eq(422) + end + end + + context 'invalid selector format' do + it 'returns 422' do + post '/v3/access_rules', { + name: 'bad-rule', + selector: 'not-valid', + relationships: { route: { data: { guid: mtls_route.guid } } } + }.to_json, admin_header + + expect(last_response.status).to eq(422) + expect(last_response.body).to include('selector') + end + end + end + + describe 'GET /v3/access_rules/:guid' do + let!(:access_rule) do + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'allow-frontend', + selector: "cf:app:#{valid_uuid}", + route_id: mtls_route.id + ) + end + + it 'returns the access rule' do + get "/v3/access_rules/#{access_rule.guid}", nil, admin_header + + expect(last_response.status).to eq(200) + parsed = Oj.load(last_response.body) + expect(parsed['guid']).to eq(access_rule.guid) + expect(parsed['name']).to eq('allow-frontend') + expect(parsed['selector']).to eq("cf:app:#{valid_uuid}") + end + + context 'when the access rule does not exist' do + it 'returns 404' do + get '/v3/access_rules/nonexistent-guid', nil, admin_header + + expect(last_response.status).to eq(404) + end + end + end + + describe 'GET /v3/access_rules' do + let!(:rule1) do + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'rule-one', + selector: "cf:app:#{valid_uuid}", + route_id: mtls_route.id + ) + end + let!(:rule2) do + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'rule-two', + selector: 'cf:any', + route_id: VCAP::CloudController::Route.make(space: space, domain: mtls_domain).id + ) + end + + it 'lists all accessible access rules' do + get '/v3/access_rules', nil, admin_header + + expect(last_response.status).to eq(200) + parsed = Oj.load(last_response.body) + guids = parsed['resources'].map { |r| r['guid'] } + expect(guids).to include(rule1.guid, rule2.guid) + end + + it 'filters by route_guids' do + get "/v3/access_rules?route_guids=#{mtls_route.guid}", nil, admin_header + + expect(last_response.status).to eq(200) + parsed = Oj.load(last_response.body) + guids = parsed['resources'].map { |r| r['guid'] } + expect(guids).to include(rule1.guid) + expect(guids).not_to include(rule2.guid) + end + + it 'filters by names' do + get '/v3/access_rules?names=rule-one', nil, admin_header + + expect(last_response.status).to eq(200) + parsed = Oj.load(last_response.body) + expect(parsed['resources'].length).to eq(1) + expect(parsed['resources'][0]['name']).to eq('rule-one') + end + + it 'filters by selectors' do + get '/v3/access_rules?selectors=cf:any', nil, admin_header + + expect(last_response.status).to eq(200) + parsed = Oj.load(last_response.body) + expect(parsed['resources'].length).to eq(1) + expect(parsed['resources'][0]['selector']).to eq('cf:any') + end + end + + describe 'DELETE /v3/access_rules/:guid' do + let!(:access_rule) do + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'to-delete', + selector: "cf:app:#{valid_uuid}", + route_id: mtls_route.id + ) + end + + it 'deletes the access rule and returns 204' do + delete "/v3/access_rules/#{access_rule.guid}", nil, admin_header + + expect(last_response.status).to eq(204) + expect(VCAP::CloudController::RouteAccessRule.find(guid: access_rule.guid)).to be_nil + end + + context 'when the access rule does not exist' do + it 'returns 404' do + delete '/v3/access_rules/nonexistent-guid', nil, admin_header + + expect(last_response.status).to eq(404) + end + end + end + + describe 'PATCH /v3/access_rules/:guid (metadata update)' do + let!(:access_rule) do + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'patchable', + selector: "cf:app:#{valid_uuid}", + route_id: mtls_route.id + ) + end + + it 'returns 200' do + patch "/v3/access_rules/#{access_rule.guid}", { + metadata: { labels: { env: 'production' } } + }.to_json, admin_header + + expect(last_response.status).to eq(200) + end + + context 'when the access rule does not exist' do + it 'returns 404' do + patch '/v3/access_rules/nonexistent-guid', {}.to_json, admin_header + + expect(last_response.status).to eq(404) + end + end + end +end diff --git a/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb b/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb index d74502cf615..95c39e1356f 100644 --- a/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb @@ -250,6 +250,64 @@ class Protocol it 'does not include the internal routes' do end end + + context 'when the route domain has enforce_access_rules enabled' do + let(:valid_uuid) { '11111111-2222-3333-4444-555555555555' } + let(:enforce_domain) do + PrivateDomain.make( + name: 'mtls.example.com', + owning_organization: org, + enforce_access_rules: true, + access_rules_scope: 'space' + ) + end + let(:mtls_route) { Route.make(host: 'myapp', domain: enforce_domain, space: space) } + let!(:access_rule1) do + RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'allow-app', + selector: "cf:app:#{valid_uuid}", + route_id: mtls_route.id + ) + end + let!(:access_rule2) do + RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'allow-space', + selector: "cf:space:#{valid_uuid}", + route_id: mtls_route.id + ) + end + + before do + RouteMappingModel.make(app: process.app, route: mtls_route, process_type: process.type) + end + + it 'injects access_scope and access_rules into route options' do + http_routes = ri['http_routes'] + mtls_entry = http_routes.find { |r| r['hostname'] == "myapp.mtls.example.com" } + + expect(mtls_entry).not_to be_nil + expect(mtls_entry['options']['access_scope']).to eq('space') + expect(mtls_entry['options']['access_rules']).to include("cf:app:#{valid_uuid}") + expect(mtls_entry['options']['access_rules']).to include("cf:space:#{valid_uuid}") + end + + context 'when the route has no access rules' do + before do + access_rule1.destroy + access_rule2.destroy + end + + it 'injects access_scope but omits access_rules key' do + http_routes = ri['http_routes'] + mtls_entry = http_routes.find { |r| r['hostname'] == "myapp.mtls.example.com" } + + expect(mtls_entry['options']['access_scope']).to eq('space') + expect(mtls_entry['options']).not_to have_key('access_rules') + end + end + end end context 'tcp routes' do diff --git a/spec/unit/messages/access_rule_create_message_spec.rb b/spec/unit/messages/access_rule_create_message_spec.rb new file mode 100644 index 00000000000..4d7adc60757 --- /dev/null +++ b/spec/unit/messages/access_rule_create_message_spec.rb @@ -0,0 +1,248 @@ +require 'spec_helper' +require 'messages/access_rule_create_message' + +module VCAP::CloudController + RSpec.describe AccessRuleCreateMessage do + let(:valid_uuid) { '11111111-2222-3333-4444-555555555555' } + let(:valid_route_relationship) do + { relationships: { route: { data: { guid: valid_uuid } } } } + end + + subject { AccessRuleCreateMessage.new(params) } + + describe 'validations' do + context 'when all valid params are given' do + let(:params) do + { + name: 'allow-frontend', + selector: "cf:app:#{valid_uuid}", + }.merge(valid_route_relationship) + end + + it 'is valid' do + expect(subject).to be_valid + end + end + + context 'when unexpected keys are provided' do + let(:params) do + { + name: 'allow-frontend', + selector: "cf:app:#{valid_uuid}", + unexpected: 'field', + }.merge(valid_route_relationship) + end + + it 'is not valid' do + expect(subject).not_to be_valid + expect(subject.errors.full_messages[0]).to include("Unknown field(s): 'unexpected'") + end + end + + describe 'name' do + context 'when name is missing' do + let(:params) do + { + selector: "cf:app:#{valid_uuid}", + }.merge(valid_route_relationship) + end + + it 'is not valid' do + expect(subject).not_to be_valid + expect(subject.errors[:name]).to include("can't be blank") + end + end + + context 'when name is not a string' do + let(:params) do + { + name: 42, + selector: "cf:app:#{valid_uuid}", + }.merge(valid_route_relationship) + end + + it 'is not valid' do + expect(subject).not_to be_valid + expect(subject.errors[:name]).to include('must be a string') + end + end + end + + describe 'selector' do + context 'when selector is missing' do + let(:params) do + { + name: 'allow-frontend', + }.merge(valid_route_relationship) + end + + it 'is not valid' do + expect(subject).not_to be_valid + expect(subject.errors[:selector]).to include("can't be blank") + end + end + + context 'when selector is not a string' do + let(:params) do + { + name: 'allow-frontend', + selector: 123, + }.merge(valid_route_relationship) + end + + it 'is not valid' do + expect(subject).not_to be_valid + expect(subject.errors[:selector]).to include('must be a string') + end + end + + context 'selector format' do + context 'cf:app:' do + let(:params) do + { + name: 'allow-app', + selector: "cf:app:#{valid_uuid}", + }.merge(valid_route_relationship) + end + + it 'is valid' do + expect(subject).to be_valid + end + end + + context 'cf:space:' do + let(:params) do + { + name: 'allow-space', + selector: "cf:space:#{valid_uuid}", + }.merge(valid_route_relationship) + end + + it 'is valid' do + expect(subject).to be_valid + end + end + + context 'cf:org:' do + let(:params) do + { + name: 'allow-org', + selector: "cf:org:#{valid_uuid}", + }.merge(valid_route_relationship) + end + + it 'is valid' do + expect(subject).to be_valid + end + end + + context 'cf:any' do + let(:params) do + { + name: 'allow-any', + selector: 'cf:any', + }.merge(valid_route_relationship) + end + + it 'is valid' do + expect(subject).to be_valid + end + end + + context 'invalid format' do + let(:params) do + { + name: 'bad-rule', + selector: 'not-valid', + }.merge(valid_route_relationship) + end + + it 'is not valid' do + expect(subject).not_to be_valid + expect(subject.errors[:selector]).to include( + "must be in format 'cf:app:', 'cf:space:', 'cf:org:', or 'cf:any'" + ) + end + end + + context 'cf:app: with invalid uuid' do + let(:params) do + { + name: 'bad-rule', + selector: 'cf:app:not-a-uuid', + }.merge(valid_route_relationship) + end + + it 'is not valid' do + expect(subject).not_to be_valid + expect(subject.errors[:selector]).to include( + "must be in format 'cf:app:', 'cf:space:', 'cf:org:', or 'cf:any'" + ) + end + end + + context 'cf:unknown type' do + let(:params) do + { + name: 'bad-rule', + selector: "cf:team:#{valid_uuid}", + }.merge(valid_route_relationship) + end + + it 'is not valid' do + expect(subject).not_to be_valid + expect(subject.errors[:selector]).to include( + "must be in format 'cf:app:', 'cf:space:', 'cf:org:', or 'cf:any'" + ) + end + end + end + end + + describe 'relationships' do + context 'when relationships is missing' do + let(:params) do + { + name: 'allow-frontend', + selector: "cf:app:#{valid_uuid}", + } + end + + it 'is not valid' do + expect(subject).not_to be_valid + expect(subject.errors[:relationships]).to be_present + end + end + + context 'when route relationship is missing' do + let(:params) do + { + name: 'allow-frontend', + selector: "cf:app:#{valid_uuid}", + relationships: {}, + } + end + + it 'is not valid' do + expect(subject).not_to be_valid + end + end + + context 'when route guid is provided' do + let(:params) do + { + name: 'allow-frontend', + selector: "cf:app:#{valid_uuid}", + relationships: { route: { data: { guid: 'some-route-guid' } } }, + } + end + + it 'exposes the route_guid' do + expect(subject).to be_valid + expect(subject.route_guid).to eq('some-route-guid') + end + end + end + end + end +end diff --git a/spec/unit/messages/domain_create_message_spec.rb b/spec/unit/messages/domain_create_message_spec.rb index f7dae8db280..8caab439a11 100644 --- a/spec/unit/messages/domain_create_message_spec.rb +++ b/spec/unit/messages/domain_create_message_spec.rb @@ -403,6 +403,93 @@ module VCAP::CloudController expect(subject).to be_valid end end + + context 'enforce_access_rules' do + context 'when not a boolean' do + let(:params) { { name: 'name.com', enforce_access_rules: 'yes' } } + + it 'is not valid' do + expect(subject).not_to be_valid + expect(subject.errors[:enforce_access_rules]).to include('must be a boolean') + end + end + + context 'when true without access_rules_scope' do + let(:params) { { name: 'name.com', enforce_access_rules: true } } + + it 'is not valid' do + expect(subject).not_to be_valid + expect(subject.errors[:access_rules_scope]).to include('is required when enforce_access_rules is true') + end + end + + context 'when true with a valid access_rules_scope' do + let(:params) { { name: 'name.com', enforce_access_rules: true, access_rules_scope: 'space' } } + + it 'is valid' do + expect(subject).to be_valid + end + end + + context 'when false without access_rules_scope' do + let(:params) { { name: 'name.com', enforce_access_rules: false } } + + it 'is valid' do + expect(subject).to be_valid + end + end + + context 'when omitted' do + let(:params) { { name: 'name.com' } } + + it 'is valid' do + expect(subject).to be_valid + end + end + end + + context 'access_rules_scope' do + context 'when set to an invalid value' do + let(:params) { { name: 'name.com', enforce_access_rules: true, access_rules_scope: 'invalid' } } + + it 'is not valid' do + expect(subject).not_to be_valid + expect(subject.errors[:access_rules_scope]).to include("must be one of 'any', 'org', 'space'") + end + end + + context "when set to 'any'" do + let(:params) { { name: 'name.com', enforce_access_rules: true, access_rules_scope: 'any' } } + + it 'is valid' do + expect(subject).to be_valid + end + end + + context "when set to 'org'" do + let(:params) { { name: 'name.com', enforce_access_rules: true, access_rules_scope: 'org' } } + + it 'is valid' do + expect(subject).to be_valid + end + end + + context "when set to 'space'" do + let(:params) { { name: 'name.com', enforce_access_rules: true, access_rules_scope: 'space' } } + + it 'is valid' do + expect(subject).to be_valid + end + end + + context 'when provided without enforce_access_rules' do + let(:params) { { name: 'name.com', access_rules_scope: 'space' } } + + it 'is valid (scope alone is permissible)' do + expect(subject).to be_valid + end + end + end end describe 'accessor methods' do diff --git a/spec/unit/messages/route_options_message_spec.rb b/spec/unit/messages/route_options_message_spec.rb index f081ecc942b..57646d21950 100644 --- a/spec/unit/messages/route_options_message_spec.rb +++ b/spec/unit/messages/route_options_message_spec.rb @@ -37,215 +37,6 @@ module VCAP::CloudController end end - describe 'mTLS allowed sources validations (RFC-0027 compliant flat options)' do - context 'when app_to_app_mtls_routing feature flag is disabled' do - it 'does not allow mtls_allowed_apps option' do - message = RouteOptionsMessage.new({ mtls_allowed_apps: 'app-guid-1' }) - expect(message).not_to be_valid - expect(message.errors_on(:base)).to include("Unknown field(s): 'mtls_allowed_apps'") - end - - it 'does not allow mtls_allowed_spaces option' do - message = RouteOptionsMessage.new({ mtls_allowed_spaces: 'space-guid-1' }) - expect(message).not_to be_valid - expect(message.errors_on(:base)).to include("Unknown field(s): 'mtls_allowed_spaces'") - end - - it 'does not allow mtls_allowed_orgs option' do - message = RouteOptionsMessage.new({ mtls_allowed_orgs: 'org-guid-1' }) - expect(message).not_to be_valid - expect(message.errors_on(:base)).to include("Unknown field(s): 'mtls_allowed_orgs'") - end - - it 'does not allow mtls_allow_any option' do - message = RouteOptionsMessage.new({ mtls_allow_any: true }) - expect(message).not_to be_valid - expect(message.errors_on(:base)).to include("Unknown field(s): 'mtls_allow_any'") - end - end - - context 'when app_to_app_mtls_routing feature flag is enabled' do - before do - VCAP::CloudController::FeatureFlag.make(name: 'app_to_app_mtls_routing', enabled: true) - end - - describe 'mtls_allowed_apps validation' do - it 'allows valid comma-separated app GUIDs' do - app1 = AppModel.make - app2 = AppModel.make - message = RouteOptionsMessage.new({ mtls_allowed_apps: "#{app1.guid},#{app2.guid}" }) - expect(message).to be_valid - end - - it 'allows single app GUID' do - app = AppModel.make - message = RouteOptionsMessage.new({ mtls_allowed_apps: app.guid }) - expect(message).to be_valid - end - - it 'allows app GUIDs with whitespace around commas' do - app1 = AppModel.make - app2 = AppModel.make - message = RouteOptionsMessage.new({ mtls_allowed_apps: "#{app1.guid} , #{app2.guid}" }) - expect(message).to be_valid - end - - it 'rejects non-existent app GUIDs' do - message = RouteOptionsMessage.new({ mtls_allowed_apps: 'non-existent-guid' }) - expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_apps)).to include('contains non-existent app GUIDs: non-existent-guid') - end - - it 'reports multiple non-existent app GUIDs' do - message = RouteOptionsMessage.new({ mtls_allowed_apps: 'guid-1,guid-2' }) - expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_apps)).to include('contains non-existent app GUIDs: guid-1, guid-2') - end - - it 'rejects non-string values' do - message = RouteOptionsMessage.new({ mtls_allowed_apps: ['array-not-string'] }) - expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_apps)).to include('must be a string of comma-separated GUIDs') - end - end - - describe 'mtls_allowed_spaces validation' do - it 'allows valid comma-separated space GUIDs' do - space1 = Space.make - space2 = Space.make - message = RouteOptionsMessage.new({ mtls_allowed_spaces: "#{space1.guid},#{space2.guid}" }) - expect(message).to be_valid - end - - it 'allows single space GUID' do - space = Space.make - message = RouteOptionsMessage.new({ mtls_allowed_spaces: space.guid }) - expect(message).to be_valid - end - - it 'rejects non-existent space GUIDs' do - message = RouteOptionsMessage.new({ mtls_allowed_spaces: 'non-existent-space' }) - expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_spaces)).to include('contains non-existent space GUIDs: non-existent-space') - end - - it 'rejects non-string values' do - message = RouteOptionsMessage.new({ mtls_allowed_spaces: { 'nested' => 'object' } }) - expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_spaces)).to include('must be a string of comma-separated GUIDs') - end - end - - describe 'mtls_allowed_orgs validation' do - it 'allows valid comma-separated org GUIDs' do - org1 = Organization.make - org2 = Organization.make - message = RouteOptionsMessage.new({ mtls_allowed_orgs: "#{org1.guid},#{org2.guid}" }) - expect(message).to be_valid - end - - it 'allows single org GUID' do - org = Organization.make - message = RouteOptionsMessage.new({ mtls_allowed_orgs: org.guid }) - expect(message).to be_valid - end - - it 'rejects non-existent org GUIDs' do - message = RouteOptionsMessage.new({ mtls_allowed_orgs: 'non-existent-org' }) - expect(message).not_to be_valid - expect(message.errors_on(:mtls_allowed_orgs)).to include('contains non-existent organization GUIDs: non-existent-org') - end - end - - describe 'mtls_allow_any validation' do - it 'allows true value' do - message = RouteOptionsMessage.new({ mtls_allow_any: true }) - expect(message).to be_valid - end - - it 'allows false value' do - message = RouteOptionsMessage.new({ mtls_allow_any: false }) - expect(message).to be_valid - end - - it 'allows string "true"' do - message = RouteOptionsMessage.new({ mtls_allow_any: 'true' }) - expect(message).to be_valid - end - - it 'allows string "false"' do - message = RouteOptionsMessage.new({ mtls_allow_any: 'false' }) - expect(message).to be_valid - end - - it 'rejects non-boolean values' do - message = RouteOptionsMessage.new({ mtls_allow_any: 'yes' }) - expect(message).not_to be_valid - expect(message.errors_on(:mtls_allow_any)).to include('must be a boolean (true or false)') - end - end - - describe 'mtls_allow_any exclusivity validation' do - it 'does not allow mtls_allow_any with mtls_allowed_apps' do - app = AppModel.make - message = RouteOptionsMessage.new({ mtls_allow_any: true, mtls_allowed_apps: app.guid }) - expect(message).not_to be_valid - expect(message.errors_on(:mtls_allow_any)).to include('is mutually exclusive with mtls_allowed_apps, mtls_allowed_spaces, and mtls_allowed_orgs') - end - - it 'does not allow mtls_allow_any with mtls_allowed_spaces' do - space = Space.make - message = RouteOptionsMessage.new({ mtls_allow_any: true, mtls_allowed_spaces: space.guid }) - expect(message).not_to be_valid - expect(message.errors_on(:mtls_allow_any)).to include('is mutually exclusive with mtls_allowed_apps, mtls_allowed_spaces, and mtls_allowed_orgs') - end - - it 'does not allow mtls_allow_any with mtls_allowed_orgs' do - org = Organization.make - message = RouteOptionsMessage.new({ mtls_allow_any: true, mtls_allowed_orgs: org.guid }) - expect(message).not_to be_valid - expect(message.errors_on(:mtls_allow_any)).to include('is mutually exclusive with mtls_allowed_apps, mtls_allowed_spaces, and mtls_allowed_orgs') - end - - it 'allows mtls_allow_any: false with specific GUIDs' do - app = AppModel.make - message = RouteOptionsMessage.new({ mtls_allow_any: false, mtls_allowed_apps: app.guid }) - expect(message).to be_valid - end - - it 'allows string "true" exclusivity check' do - app = AppModel.make - message = RouteOptionsMessage.new({ mtls_allow_any: 'true', mtls_allowed_apps: app.guid }) - expect(message).not_to be_valid - expect(message.errors_on(:mtls_allow_any)).to include('is mutually exclusive with mtls_allowed_apps, mtls_allowed_spaces, and mtls_allowed_orgs') - end - end - - describe 'combined options' do - it 'allows all mTLS options together (without mtls_allow_any)' do - app = AppModel.make - space = Space.make - org = Organization.make - message = RouteOptionsMessage.new({ - mtls_allowed_apps: app.guid, - mtls_allowed_spaces: space.guid, - mtls_allowed_orgs: org.guid - }) - expect(message).to be_valid - end - - it 'allows mTLS options with loadbalancing' do - app = AppModel.make - message = RouteOptionsMessage.new({ - loadbalancing: 'round-robin', - mtls_allowed_apps: app.guid - }) - expect(message).to be_valid - end - end - end - end - describe 'hash-based routing validations' do context 'when hash_based_routing feature flag is disabled' do it 'does not allow hash_header option' do diff --git a/spec/unit/presenters/v3/route_presenter_spec.rb b/spec/unit/presenters/v3/route_presenter_spec.rb index 684b132e407..3c78892c26e 100644 --- a/spec/unit/presenters/v3/route_presenter_spec.rb +++ b/spec/unit/presenters/v3/route_presenter_spec.rb @@ -147,6 +147,44 @@ module VCAP::CloudController::Presenters::V3 end end + context 'when options contains only internal mTLS keys' do + let(:route) do + VCAP::CloudController::Route.make( + host: 'foobar', + path: path, + space: space, + domain: domain, + options: { 'access_scope' => 'space', 'access_rules' => 'cf:app:some-guid' } + ) + end + + it 'omits the options key entirely from the response' do + expect(subject).not_to have_key(:options) + end + end + + context 'when options contains a mix of public and internal keys' do + let(:route) do + VCAP::CloudController::Route.make( + host: 'foobar', + path: path, + space: space, + domain: domain, + options: { + 'loadbalancing' => 'round-robin', + 'access_scope' => 'space', + 'access_rules' => 'cf:app:some-guid' + } + ) + end + + it 'exposes only the public options' do + expect(subject[:options]).to eq('loadbalancing' => 'round-robin') + expect(subject[:options]).not_to have_key('access_scope') + expect(subject[:options]).not_to have_key('access_rules') + end + end + context 'when there are decorators' do let(:banana_decorator) do Class.new do From b606fa2a6c0e8c119261b8c11177e2a386902118 Mon Sep 17 00:00:00 2001 From: rkoster Date: Thu, 9 Apr 2026 12:35:18 +0000 Subject: [PATCH 07/29] Fix access_rules_controller permissions query - Replace non-existent readable_space_scoped_space_guids_query with proper subquery - Use readable_space_scoped_spaces_query for non-global readers - Handle global readers separately with all routes --- app/controllers/v3/access_rules_controller.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/controllers/v3/access_rules_controller.rb b/app/controllers/v3/access_rules_controller.rb index a45b982bb64..ac84d80f449 100644 --- a/app/controllers/v3/access_rules_controller.rb +++ b/app/controllers/v3/access_rules_controller.rb @@ -107,10 +107,12 @@ def destroy def build_dataset(message) dataset = VCAP::CloudController::RouteAccessRule.dataset - readable_route_ids = VCAP::CloudController::Route. - join(:spaces, id: :space_id). - where(Sequel.lit(permission_queryer.readable_space_scoped_space_guids_query)). - select(:routes__id) + if permission_queryer.can_read_globally? + readable_route_ids = VCAP::CloudController::Route.select(:id) + else + readable_space_ids = permission_queryer.readable_space_scoped_spaces_query.select(:id) + readable_route_ids = VCAP::CloudController::Route.where(space_id: readable_space_ids).select(:id) + end dataset = dataset.where(route_id: readable_route_ids) From fdf862078259bffe4da2ea05268b22e0e427fdf2 Mon Sep 17 00:00:00 2001 From: rkoster Date: Thu, 9 Apr 2026 13:44:57 +0000 Subject: [PATCH 08/29] Add automatic Diego sync callbacks to RouteAccessRule - Add after_create and after_destroy callbacks to touch associated processes - Updates process.updated_at to trigger Diego ProcessesSync immediately - Eliminates 30-second wait for access rule changes to propagate to GoRouter - Add comprehensive unit tests for callbacks and validations - Ensure RouteAccessRule model is loaded in app/models.rb This enables automatic synchronization of access rules to Diego/GoRouter within seconds instead of requiring manual app restarts or waiting for the next sync cycle. --- app/models.rb | 1 + app/models/runtime/route_access_rule.rb | 22 ++++ .../models/runtime/route_access_rule_spec.rb | 112 ++++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 spec/unit/models/runtime/route_access_rule_spec.rb diff --git a/app/models.rb b/app/models.rb index 93e1594b38d..84ddeb4813e 100644 --- a/app/models.rb +++ b/app/models.rb @@ -69,6 +69,7 @@ require 'models/runtime/revision_sidecar_model' require 'models/runtime/revision_sidecar_process_type_model' require 'models/runtime/route' +require 'models/runtime/route_access_rule' require 'models/runtime/space_routes' require 'models/runtime/space_quota_definition' require 'models/runtime/stack' diff --git a/app/models/runtime/route_access_rule.rb b/app/models/runtime/route_access_rule.rb index 08ed6c6e3e2..cf554de3fd8 100644 --- a/app/models/runtime/route_access_rule.rb +++ b/app/models/runtime/route_access_rule.rb @@ -11,5 +11,27 @@ def validate validates_presence :selector validates_presence :route_id end + + def after_create + super + touch_associated_processes + end + + def after_destroy + super + touch_associated_processes + end + + private + + def touch_associated_processes + # Update the timestamp on all processes associated with this route + # This triggers Diego's ProcessesSync to pick up the route changes + return unless route + + route.apps.each do |process| + process.update(updated_at: Time.now) + end + end end end diff --git a/spec/unit/models/runtime/route_access_rule_spec.rb b/spec/unit/models/runtime/route_access_rule_spec.rb new file mode 100644 index 00000000000..89e1a536f47 --- /dev/null +++ b/spec/unit/models/runtime/route_access_rule_spec.rb @@ -0,0 +1,112 @@ +require 'spec_helper' + +module VCAP::CloudController + RSpec.describe RouteAccessRule, type: :model do + let(:space) { Space.make } + let(:domain) { SharedDomain.make(name: 'apps.identity') } + let(:route) { Route.make(space: space, domain: domain) } + let(:process) { ProcessModelFactory.make(space: space) } + let(:app_guid) { SecureRandom.uuid } + + before do + RouteMappingModel.make(app: process, route: route, process_type: 'web') + end + + describe 'validations' do + it 'requires a name' do + rule = RouteAccessRule.new(selector: 'cf:app:123', route: route) + expect(rule.valid?).to be false + expect(rule.errors[:name]).to include("can't be blank") + end + + it 'requires a selector' do + rule = RouteAccessRule.new(name: 'test-rule', route: route) + expect(rule.valid?).to be false + expect(rule.errors[:selector]).to include("can't be blank") + end + + it 'requires a route_id' do + rule = RouteAccessRule.new(name: 'test-rule', selector: 'cf:app:123') + expect(rule.valid?).to be false + expect(rule.errors[:route_id]).to include("can't be blank") + end + end + + describe 'associations' do + it 'belongs to a route' do + rule = RouteAccessRule.create( + name: 'test-rule', + selector: 'cf:app:123', + route: route + ) + expect(rule.route).to eq(route) + end + end + + describe 'callbacks' do + describe 'after_create' do + it 'touches associated processes to trigger Diego sync' do + initial_updated_at = process.updated_at + + # Sleep to ensure timestamp difference + sleep 0.1 + + RouteAccessRule.create( + name: 'test-rule', + selector: "cf:app:#{app_guid}", + route: route + ) + + process.reload + expect(process.updated_at).to be > initial_updated_at + end + + it 'does not fail if route has no associated processes' do + route_without_processes = Route.make(space: space, domain: domain) + + expect { + RouteAccessRule.create( + name: 'test-rule', + selector: "cf:app:#{app_guid}", + route: route_without_processes + ) + }.not_to raise_error + end + end + + describe 'after_destroy' do + it 'touches associated processes to trigger Diego sync' do + rule = RouteAccessRule.create( + name: 'test-rule', + selector: "cf:app:#{app_guid}", + route: route + ) + + process.reload + initial_updated_at = process.updated_at + + # Sleep to ensure timestamp difference + sleep 0.1 + + rule.destroy + + process.reload + expect(process.updated_at).to be > initial_updated_at + end + + it 'does not fail if route has no associated processes' do + route_without_processes = Route.make(space: space, domain: domain) + rule = RouteAccessRule.create( + name: 'test-rule', + selector: "cf:app:#{app_guid}", + route: route_without_processes + ) + + expect { + rule.destroy + }.not_to raise_error + end + end + end + end +end From 0b7ff9d779f6082b2f86d58a8cfe1b541aeff0e9 Mon Sep 17 00:00:00 2001 From: rkoster Date: Fri, 10 Apr 2026 06:38:05 +0000 Subject: [PATCH 09/29] Implement include=selector_resource for /v3/access_rules endpoint - Add include parameter support to AccessRulesListMessage - Refactor IncludeAccessRuleSelectorResourceDecorator to match RFC format: - Return separate arrays for apps, spaces, organizations instead of selector_resources - Include full resource details using appropriate presenters - Batch resource fetching by type with eager loading - Auto-deduplicate resources - Gracefully handle stale/missing resources - Wire up decorator to AccessRulesController - Add comprehensive request specs for include=selector_resource Fixes: uninitialized constant error by adding proper require statement --- app/controllers/v3/access_rules_controller.rb | 7 +- ...access_rule_selector_resource_decorator.rb | 60 ++++++--- app/messages/access_rules_list_message.rb | 4 +- spec/request/access_rules_spec.rb | 114 ++++++++++++++++++ 4 files changed, 169 insertions(+), 16 deletions(-) diff --git a/app/controllers/v3/access_rules_controller.rb b/app/controllers/v3/access_rules_controller.rb index ac84d80f449..a64fb16e66f 100644 --- a/app/controllers/v3/access_rules_controller.rb +++ b/app/controllers/v3/access_rules_controller.rb @@ -2,6 +2,7 @@ require 'messages/access_rule_update_message' require 'messages/access_rules_list_message' require 'presenters/v3/access_rule_presenter' +require 'decorators/include_access_rule_selector_resource_decorator' class AccessRulesController < ApplicationController def index @@ -10,11 +11,15 @@ def index dataset = build_dataset(message) + decorators = [] + decorators << IncludeAccessRuleSelectorResourceDecorator if IncludeAccessRuleSelectorResourceDecorator.match?(message.include) + render status: :ok, json: Presenters::V3::PaginatedListPresenter.new( presenter: Presenters::V3::AccessRulePresenter, paginated_result: SequelPaginator.new.get_page(dataset, message.try(:pagination_options)), path: '/v3/access_rules', - message: message + message: message, + decorators: decorators ) end diff --git a/app/decorators/include_access_rule_selector_resource_decorator.rb b/app/decorators/include_access_rule_selector_resource_decorator.rb index c5ac7552860..cd85dd0ef1c 100644 --- a/app/decorators/include_access_rule_selector_resource_decorator.rb +++ b/app/decorators/include_access_rule_selector_resource_decorator.rb @@ -10,7 +10,12 @@ def self.match?(include_params) end def self.decorate(hash, access_rules) - included = [] + hash[:included] ||= {} + + # Collect all GUIDs by type + app_guids = [] + space_guids = [] + org_guids = [] access_rules.each do |rule| match = SELECTOR_REGEX.match(rule.selector) @@ -19,22 +24,49 @@ def self.decorate(hash, access_rules) resource_type = match[1] resource_guid = match[2] - resource = case resource_type - when 'app' - VCAP::CloudController::AppModel.find(guid: resource_guid) - when 'space' - VCAP::CloudController::Space.find(guid: resource_guid) - when 'org' - VCAP::CloudController::Organization.find(guid: resource_guid) - end - - next if resource.nil? - - included << { type: resource_type, guid: resource.guid } + case resource_type + when 'app' + app_guids << resource_guid + when 'space' + space_guids << resource_guid + when 'org' + org_guids << resource_guid + end end - hash[:included] = { selector_resources: included } + # Fetch and present resources + hash[:included][:apps] = fetch_and_present_apps(app_guids.uniq) + hash[:included][:spaces] = fetch_and_present_spaces(space_guids.uniq) + hash[:included][:organizations] = fetch_and_present_organizations(org_guids.uniq) + hash end + + private_class_method def self.fetch_and_present_apps(guids) + return [] if guids.empty? + + apps = VCAP::CloudController::AppModel.where(guid: guids). + order(:created_at, :guid). + eager(VCAP::CloudController::Presenters::V3::AppPresenter.associated_resources).all + apps.map { |app| VCAP::CloudController::Presenters::V3::AppPresenter.new(app).to_hash } + end + + private_class_method def self.fetch_and_present_spaces(guids) + return [] if guids.empty? + + spaces = VCAP::CloudController::Space.where(guid: guids). + order(:created_at, :guid). + eager(VCAP::CloudController::Presenters::V3::SpacePresenter.associated_resources).all + spaces.map { |space| VCAP::CloudController::Presenters::V3::SpacePresenter.new(space).to_hash } + end + + private_class_method def self.fetch_and_present_organizations(guids) + return [] if guids.empty? + + orgs = VCAP::CloudController::Organization.where(guid: guids). + order(:created_at, :guid). + eager(VCAP::CloudController::Presenters::V3::OrganizationPresenter.associated_resources).all + orgs.map { |org| VCAP::CloudController::Presenters::V3::OrganizationPresenter.new(org).to_hash } + end end end diff --git a/app/messages/access_rules_list_message.rb b/app/messages/access_rules_list_message.rb index 7c7973fda97..b2eb08002bf 100644 --- a/app/messages/access_rules_list_message.rb +++ b/app/messages/access_rules_list_message.rb @@ -6,12 +6,14 @@ class AccessRulesListMessage < ListMessage route_guids names selectors + include ] validates_with NoAdditionalParamsValidator + validates_with IncludeParamValidator, valid_values: ['selector_resource', 'route'] def self.from_params(params) - super(params, %w[route_guids names selectors]) + super(params, %w[route_guids names selectors include]) end end end diff --git a/spec/request/access_rules_spec.rb b/spec/request/access_rules_spec.rb index 3962cc59a66..4828cac2660 100644 --- a/spec/request/access_rules_spec.rb +++ b/spec/request/access_rules_spec.rb @@ -300,6 +300,120 @@ def expected_rule_json(rule) expect(parsed['resources'].length).to eq(1) expect(parsed['resources'][0]['selector']).to eq('cf:any') end + + context 'with include=selector_resource' do + let!(:app) { VCAP::CloudController::AppModel.make(space: space, name: 'frontend-app') } + let!(:other_space) { VCAP::CloudController::Space.make(organization: org, name: 'other-space') } + let!(:other_org) { VCAP::CloudController::Organization.make(name: 'other-org') } + + let!(:app_rule) do + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'app-rule', + selector: "cf:app:#{app.guid}", + route_id: mtls_route.id + ) + end + + let!(:space_rule) do + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'space-rule', + selector: "cf:space:#{other_space.guid}", + route_id: mtls_route.id + ) + end + + let!(:org_rule) do + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'org-rule', + selector: "cf:org:#{other_org.guid}", + route_id: mtls_route.id + ) + end + + it 'includes resolved selector resources' do + get '/v3/access_rules?include=selector_resource', nil, admin_header + + expect(last_response.status).to eq(200) + parsed = Oj.load(last_response.body) + + # Check included structure + expect(parsed['included']).to be_a(Hash) + expect(parsed['included']['apps']).to be_an(Array) + expect(parsed['included']['spaces']).to be_an(Array) + expect(parsed['included']['organizations']).to be_an(Array) + + # Check app is included with full details + app_included = parsed['included']['apps'].find { |a| a['guid'] == app.guid } + expect(app_included).to be_present + expect(app_included['name']).to eq('frontend-app') + expect(app_included['guid']).to eq(app.guid) + + # Check space is included + space_included = parsed['included']['spaces'].find { |s| s['guid'] == other_space.guid } + expect(space_included).to be_present + expect(space_included['name']).to eq('other-space') + + # Check org is included + org_included = parsed['included']['organizations'].find { |o| o['guid'] == other_org.guid } + expect(org_included).to be_present + expect(org_included['name']).to eq('other-org') + end + + it 'handles stale resources (missing GUIDs) gracefully' do + stale_guid = '99999999-9999-9999-9999-999999999999' + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'stale-rule', + selector: "cf:app:#{stale_guid}", + route_id: mtls_route.id + ) + + get '/v3/access_rules?include=selector_resource', nil, admin_header + + expect(last_response.status).to eq(200) + parsed = Oj.load(last_response.body) + + # Stale resource should not appear in included + stale_app = parsed['included']['apps'].find { |a| a['guid'] == stale_guid } + expect(stale_app).to be_nil + end + + it 'includes only unique resources when multiple rules reference the same resource' do + # Create another rule referencing the same app + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'another-app-rule', + selector: "cf:app:#{app.guid}", + route_id: VCAP::CloudController::Route.make(space: space, domain: mtls_domain).id + ) + + get '/v3/access_rules?include=selector_resource', nil, admin_header + + expect(last_response.status).to eq(200) + parsed = Oj.load(last_response.body) + + # App should appear only once + app_count = parsed['included']['apps'].count { |a| a['guid'] == app.guid } + expect(app_count).to eq(1) + end + + it 'does not include resources for cf:any selectors' do + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'any-rule', + selector: 'cf:any', + route_id: VCAP::CloudController::Route.make(space: space, domain: mtls_domain).id + ) + + get '/v3/access_rules?include=selector_resource', nil, admin_header + + expect(last_response.status).to eq(200) + # Should succeed without error even with cf:any selector + end + end end describe 'DELETE /v3/access_rules/:guid' do From 7638683db0e220eaa0f4496b58287fda72eb0211 Mon Sep 17 00:00:00 2001 From: rkoster Date: Fri, 10 Apr 2026 08:18:47 +0000 Subject: [PATCH 10/29] Add space_guids filtering to /v3/access_rules endpoint Implement space-based filtering for access rules endpoint to enable querying all access rules within a given space using ?space_guids= query parameter. Changes: - Add space_guids to AccessRulesListMessage with array validation - Implement space filtering in AccessRulesController#build_dataset - Add comprehensive unit tests for AccessRulesListMessage - Add request specs for single/multiple space filtering and combinations - Follow existing CAPI patterns for space_guids filtering The filter joins through the routes table to filter access rules by the space_id of their associated routes. --- app/controllers/v3/access_rules_controller.rb | 7 + app/messages/access_rules_list_message.rb | 5 +- spec/request/access_rules_spec.rb | 67 +++++++++ .../access_rules_list_message_spec.rb | 135 ++++++++++++++++++ 4 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 spec/unit/messages/access_rules_list_message_spec.rb diff --git a/app/controllers/v3/access_rules_controller.rb b/app/controllers/v3/access_rules_controller.rb index a64fb16e66f..eb6ff20aa0e 100644 --- a/app/controllers/v3/access_rules_controller.rb +++ b/app/controllers/v3/access_rules_controller.rb @@ -128,6 +128,13 @@ def build_dataset(message) select_all(:route_access_rules) end + if message.requested?(:space_guids) + dataset = dataset. + join(:routes, id: :route_id). + where(routes__space_id: VCAP::CloudController::Space.where(guid: message.space_guids).select(:id)). + select_all(:route_access_rules) + end + dataset = dataset.where(name: message.names) if message.requested?(:names) dataset = dataset.where(selector: message.selectors) if message.requested?(:selectors) diff --git a/app/messages/access_rules_list_message.rb b/app/messages/access_rules_list_message.rb index b2eb08002bf..ddf22935f51 100644 --- a/app/messages/access_rules_list_message.rb +++ b/app/messages/access_rules_list_message.rb @@ -4,6 +4,7 @@ module VCAP::CloudController class AccessRulesListMessage < ListMessage register_allowed_keys %i[ route_guids + space_guids names selectors include @@ -12,8 +13,10 @@ class AccessRulesListMessage < ListMessage validates_with NoAdditionalParamsValidator validates_with IncludeParamValidator, valid_values: ['selector_resource', 'route'] + validates :space_guids, array: true, allow_nil: true + def self.from_params(params) - super(params, %w[route_guids names selectors include]) + super(params, %w[route_guids space_guids names selectors include]) end end end diff --git a/spec/request/access_rules_spec.rb b/spec/request/access_rules_spec.rb index 4828cac2660..95e90b51cca 100644 --- a/spec/request/access_rules_spec.rb +++ b/spec/request/access_rules_spec.rb @@ -301,6 +301,73 @@ def expected_rule_json(rule) expect(parsed['resources'][0]['selector']).to eq('cf:any') end + describe 'filtering by space_guids' do + let(:other_org) { VCAP::CloudController::Organization.make } + let(:other_space) { VCAP::CloudController::Space.make(organization: other_org) } + let(:other_mtls_domain) do + VCAP::CloudController::PrivateDomain.make( + owning_organization: other_org, + enforce_access_rules: true, + access_rules_scope: 'space' + ) + end + let(:other_route) { VCAP::CloudController::Route.make(space: other_space, domain: other_mtls_domain) } + let!(:rule_in_other_space) do + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'rule-in-other-space', + selector: 'cf:any', + route_id: other_route.id + ) + end + + before do + other_org.add_user(user) + other_space.add_developer(user) + end + + it 'filters by single space_guid' do + get "/v3/access_rules?space_guids=#{space.guid}", nil, admin_header + + expect(last_response.status).to eq(200) + parsed = Oj.load(last_response.body) + guids = parsed['resources'].map { |r| r['guid'] } + expect(guids).to include(rule1.guid, rule2.guid) + expect(guids).not_to include(rule_in_other_space.guid) + end + + it 'filters by multiple space_guids' do + get "/v3/access_rules?space_guids=#{space.guid},#{other_space.guid}", nil, admin_header + + expect(last_response.status).to eq(200) + parsed = Oj.load(last_response.body) + guids = parsed['resources'].map { |r| r['guid'] } + expect(guids).to include(rule1.guid, rule2.guid, rule_in_other_space.guid) + end + + it 'combines space_guids with other filters' do + get "/v3/access_rules?space_guids=#{space.guid}&names=rule-one", nil, admin_header + + expect(last_response.status).to eq(200) + parsed = Oj.load(last_response.body) + expect(parsed['resources'].length).to eq(1) + expect(parsed['resources'][0]['guid']).to eq(rule1.guid) + expect(parsed['resources'][0]['name']).to eq('rule-one') + end + + it 'returns empty when space has no access rules' do + empty_space = VCAP::CloudController::Space.make(organization: org) + org.add_user(user) + empty_space.add_developer(user) + + get "/v3/access_rules?space_guids=#{empty_space.guid}", nil, admin_header + + expect(last_response.status).to eq(200) + parsed = Oj.load(last_response.body) + expect(parsed['resources'].length).to eq(0) + end + end + context 'with include=selector_resource' do let!(:app) { VCAP::CloudController::AppModel.make(space: space, name: 'frontend-app') } let!(:other_space) { VCAP::CloudController::Space.make(organization: org, name: 'other-space') } diff --git a/spec/unit/messages/access_rules_list_message_spec.rb b/spec/unit/messages/access_rules_list_message_spec.rb new file mode 100644 index 00000000000..443fdf70bfd --- /dev/null +++ b/spec/unit/messages/access_rules_list_message_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper' +require 'messages/access_rules_list_message' + +module VCAP::CloudController + RSpec.describe AccessRulesListMessage do + describe '.from_params' do + let(:params) do + { + 'route_guids' => 'route1,route2', + 'space_guids' => 'space1,space2', + 'names' => 'name1,name2', + 'selectors' => 'selector1,selector2', + 'page' => 1, + 'per_page' => 5, + 'order_by' => 'created_at', + 'include' => 'selector_resource,route' + } + end + + it 'returns the correct AccessRulesListMessage' do + message = AccessRulesListMessage.from_params(params) + + expect(message).to be_a(AccessRulesListMessage) + expect(message.route_guids).to eq(%w[route1 route2]) + expect(message.space_guids).to eq(%w[space1 space2]) + expect(message.names).to eq(%w[name1 name2]) + expect(message.selectors).to eq(%w[selector1 selector2]) + expect(message.page).to eq(1) + expect(message.per_page).to eq(5) + expect(message.order_by).to eq('created_at') + expect(message.include).to eq(%w[selector_resource route]) + end + + it 'converts requested keys to symbols' do + message = AccessRulesListMessage.from_params(params) + + expect(message).to be_requested(:route_guids) + expect(message).to be_requested(:space_guids) + expect(message).to be_requested(:names) + expect(message).to be_requested(:selectors) + expect(message).to be_requested(:page) + expect(message).to be_requested(:per_page) + expect(message).to be_requested(:order_by) + expect(message).to be_requested(:include) + end + end + + describe '#to_param_hash' do + let(:opts) do + { + route_guids: %w[route1 route2], + space_guids: %w[space1 space2], + names: %w[name1 name2], + selectors: %w[selector1 selector2], + page: 1, + per_page: 5, + order_by: 'created_at', + include: %w[selector_resource route] + } + end + + it 'excludes the pagination keys' do + expected_params = %i[route_guids space_guids names selectors include] + expect(AccessRulesListMessage.from_params(opts).to_param_hash.keys).to match_array(expected_params) + end + end + + describe 'fields' do + it 'accepts a set of fields' do + expect do + AccessRulesListMessage.from_params({ + route_guids: [], + space_guids: [], + names: [], + selectors: [], + page: 1, + per_page: 5, + order_by: 'created_at', + include: ['selector_resource', 'route'] + }) + end.not_to raise_error + end + + it 'accepts an empty set' do + message = AccessRulesListMessage.from_params({}) + expect(message).to be_valid + end + + it 'does not accept a field not in this set' do + message = AccessRulesListMessage.from_params({ foobar: 'pants' }) + + expect(message).not_to be_valid + expect(message.errors[:base][0]).to include("Unknown query parameter(s): 'foobar'") + end + + describe 'include validations' do + it 'accepts valid include values' do + message = AccessRulesListMessage.from_params({ 'include' => 'selector_resource' }) + expect(message).to be_valid + + message = AccessRulesListMessage.from_params({ 'include' => 'route' }) + expect(message).to be_valid + + message = AccessRulesListMessage.from_params({ 'include' => 'selector_resource,route' }) + expect(message).to be_valid + end + + it 'rejects invalid include values' do + message = AccessRulesListMessage.from_params({ 'include' => 'invalid' }) + expect(message).not_to be_valid + end + end + + describe 'validations' do + it 'validates space_guids is an array' do + message = AccessRulesListMessage.from_params space_guids: 'not array' + expect(message).not_to be_valid + expect(message.errors[:space_guids].length).to eq 1 + end + + it 'allows space_guids to be nil' do + message = AccessRulesListMessage.from_params({}) + expect(message).to be_valid + expect(message.space_guids).to be_nil + end + + it 'allows space_guids to be an array' do + message = AccessRulesListMessage.from_params space_guids: %w[space1 space2] + expect(message).to be_valid + expect(message.space_guids).to eq(%w[space1 space2]) + end + end + end + end +end From 3311a2ca9769e9e8ff49006c7b8e9fcddc463cc1 Mon Sep 17 00:00:00 2001 From: rkoster Date: Fri, 10 Apr 2026 08:35:29 +0000 Subject: [PATCH 11/29] 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. --- app/controllers/v3/access_rules_controller.rb | 34 +++---- .../include_access_rule_route_decorator.rb | 27 ++++++ spec/request/access_rules_spec.rb | 94 ++++++++++++++++++- 3 files changed, 132 insertions(+), 23 deletions(-) create mode 100644 app/decorators/include_access_rule_route_decorator.rb diff --git a/app/controllers/v3/access_rules_controller.rb b/app/controllers/v3/access_rules_controller.rb index eb6ff20aa0e..73876128299 100644 --- a/app/controllers/v3/access_rules_controller.rb +++ b/app/controllers/v3/access_rules_controller.rb @@ -3,6 +3,7 @@ require 'messages/access_rules_list_message' require 'presenters/v3/access_rule_presenter' require 'decorators/include_access_rule_selector_resource_decorator' +require 'decorators/include_access_rule_route_decorator' class AccessRulesController < ApplicationController def index @@ -13,6 +14,7 @@ def index decorators = [] decorators << IncludeAccessRuleSelectorResourceDecorator if IncludeAccessRuleSelectorResourceDecorator.match?(message.include) + decorators << IncludeAccessRuleRouteDecorator if IncludeAccessRuleRouteDecorator.match?(message.include) render status: :ok, json: Presenters::V3::PaginatedListPresenter.new( presenter: Presenters::V3::AccessRulePresenter, @@ -42,27 +44,17 @@ def create unauthorized! unless permission_queryer.can_write_to_active_space?(route.space.id) suspended! unless permission_queryer.is_space_active?(route.space.id) - unless route.domain.enforce_access_rules - unprocessable!("Cannot create access rules for route '#{route.guid}': the route's domain does not have enforce_access_rules enabled.") - end + 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 # Enforce cf:any exclusivity: if route already has a cf:any rule, reject new rules; # if new rule is cf:any, reject if route already has any rules. existing_selectors = route.access_rules.map(&:selector) - if message.selector == 'cf:any' && existing_selectors.any? - unprocessable!("Cannot add 'cf:any' selector when other access rules already exist for this route.") - end - if existing_selectors.include?('cf:any') && message.selector != 'cf:any' - unprocessable!("Cannot add selector '#{message.selector}': route already has a 'cf:any' rule.") - end + unprocessable!("Cannot add 'cf:any' selector when other access rules already exist for this route.") if message.selector == 'cf:any' && existing_selectors.any? + unprocessable!("Cannot add selector '#{message.selector}': route already has a 'cf:any' rule.") if existing_selectors.include?('cf:any') && message.selector != 'cf:any' # Uniqueness: name and selector must be unique per route - if route.access_rules.any? { |r| r.name == message.name } - unprocessable!("An access rule with name '#{message.name}' already exists for this route.") - end - if existing_selectors.include?(message.selector) - unprocessable!("An access rule with selector '#{message.selector}' already exists for this route.") - end + unprocessable!("An access rule with name '#{message.name}' already exists for this route.") if route.access_rules.any? { |r| r.name == message.name } + unprocessable!("An access rule with selector '#{message.selector}' already exists for this route.") if existing_selectors.include?(message.selector) access_rule = VCAP::CloudController::RouteAccessRule.new( guid: SecureRandom.uuid, @@ -123,16 +115,16 @@ def build_dataset(message) if message.requested?(:route_guids) dataset = dataset. - join(:routes, id: :route_id). - where(routes__guid: message.route_guids). - select_all(:route_access_rules) + join(:routes, id: :route_id). + where(routes__guid: message.route_guids). + select_all(:route_access_rules) end if message.requested?(:space_guids) dataset = dataset. - join(:routes, id: :route_id). - where(routes__space_id: VCAP::CloudController::Space.where(guid: message.space_guids).select(:id)). - select_all(:route_access_rules) + join(:routes, id: :route_id). + where(routes__space_id: VCAP::CloudController::Space.where(guid: message.space_guids).select(:id)). + select_all(:route_access_rules) end dataset = dataset.where(name: message.names) if message.requested?(:names) diff --git a/app/decorators/include_access_rule_route_decorator.rb b/app/decorators/include_access_rule_route_decorator.rb new file mode 100644 index 00000000000..178da8be3db --- /dev/null +++ b/app/decorators/include_access_rule_route_decorator.rb @@ -0,0 +1,27 @@ +module VCAP::CloudController + class IncludeAccessRuleRouteDecorator + # Handles `?include=route` for GET /v3/access_rules + # Includes the route resources associated with the access rules + + def self.match?(include_params) + include_params&.include?('route') + end + + def self.decorate(hash, access_rules) + hash[:included] ||= {} + + # Collect all unique route IDs from access rules + route_ids = access_rules.map(&:route_id).uniq + + # Fetch routes with their associations + routes = VCAP::CloudController::Route.where(id: route_ids). + order(:created_at, :guid). + eager(VCAP::CloudController::Presenters::V3::RoutePresenter.associated_resources).all + + # Present routes + hash[:included][:routes] = routes.map { |route| VCAP::CloudController::Presenters::V3::RoutePresenter.new(route).to_hash } + + hash + end + end +end diff --git a/spec/request/access_rules_spec.rb b/spec/request/access_rules_spec.rb index 95e90b51cca..4fdd65f5736 100644 --- a/spec/request/access_rules_spec.rb +++ b/spec/request/access_rules_spec.rb @@ -133,7 +133,7 @@ def expected_rule_json(rule) }.to_json, admin_header expect(last_response.status).to eq(422) - expect(last_response.body).to include("cf:any") + expect(last_response.body).to include('cf:any') end end @@ -155,7 +155,7 @@ def expected_rule_json(rule) }.to_json, admin_header expect(last_response.status).to eq(422) - expect(last_response.body).to include("cf:any") + expect(last_response.body).to include('cf:any') end end @@ -481,6 +481,96 @@ def expected_rule_json(rule) # Should succeed without error even with cf:any selector end end + + context 'with include=route' do + let(:route2) { VCAP::CloudController::Route.make(space: space, domain: mtls_domain) } + + let!(:rule_on_route1) do + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'rule-on-route1', + selector: 'cf:any', + route_id: mtls_route.id + ) + end + + let!(:rule_on_route2) do + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'rule-on-route2', + selector: "cf:app:#{valid_uuid}", + route_id: route2.id + ) + end + + it 'includes route resources' do + get '/v3/access_rules?include=route', nil, admin_header + + expect(last_response.status).to eq(200) + parsed = Oj.load(last_response.body) + + # Check included structure + expect(parsed['included']).to be_a(Hash) + expect(parsed['included']['routes']).to be_an(Array) + expect(parsed['included']['routes'].length).to be >= 2 + + # Check routes are included with full details + route1_included = parsed['included']['routes'].find { |r| r['guid'] == mtls_route.guid } + expect(route1_included).to be_present + expect(route1_included['guid']).to eq(mtls_route.guid) + expect(route1_included['url']).to be_present + + route2_included = parsed['included']['routes'].find { |r| r['guid'] == route2.guid } + expect(route2_included).to be_present + expect(route2_included['guid']).to eq(route2.guid) + end + + it 'includes only unique routes when multiple rules reference the same route' do + # Create another rule on the same route + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'another-rule-on-route1', + selector: "cf:app:#{valid_uuid}", + route_id: mtls_route.id + ) + + get '/v3/access_rules?include=route', nil, admin_header + + expect(last_response.status).to eq(200) + parsed = Oj.load(last_response.body) + + # Route should appear only once + route_count = parsed['included']['routes'].count { |r| r['guid'] == mtls_route.guid } + expect(route_count).to eq(1) + end + + it 'combines include=route with include=selector_resource' do + app = VCAP::CloudController::AppModel.make(space: space, name: 'test-app') + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + name: 'combined-rule', + selector: "cf:app:#{app.guid}", + route_id: mtls_route.id + ) + + get '/v3/access_rules?include=route,selector_resource', nil, admin_header + + expect(last_response.status).to eq(200) + parsed = Oj.load(last_response.body) + + # Both routes and selector resources should be included + expect(parsed['included']['routes']).to be_an(Array) + expect(parsed['included']['apps']).to be_an(Array) + + # Verify route is present + route_included = parsed['included']['routes'].find { |r| r['guid'] == mtls_route.guid } + expect(route_included).to be_present + + # Verify app is present + app_included = parsed['included']['apps'].find { |a| a['guid'] == app.guid } + expect(app_included).to be_present + end + end end describe 'DELETE /v3/access_rules/:guid' do From 246cb9dd421f4eb508b7aac51c5505abb2fd9add Mon Sep 17 00:00:00 2001 From: rkoster Date: Wed, 15 Apr 2026 07:41:12 +0000 Subject: [PATCH 12/29] Remove name field from access rules, add read-only relationships per RFC updates Update /v3/access_rules API to align with latest RFC changes: - Remove 'name' field from RouteAccessRule model and API - Add database migration to drop name column and unique index - Use labels/annotations for metadata instead of name field - Add read-only relationships (app, space, organization) to responses extracted from selector (cf:app:X, cf:space:X, cf:org:X, cf:any) - Replace 'names' filter with 'guids' filter - Add 'selector_resource_guids' filter for text-match against selectors - Update include support: add individual app, space, organization (in addition to existing selector_resource and route) - Remove name-based uniqueness validation (keep selector uniqueness) - Update all tests to remove name references Breaking changes: - POST /v3/access_rules no longer accepts 'name' field - GET /v3/access_rules responses no longer include 'name' field - Filter parameter 'names' removed, use 'guids' instead - Access rule responses now include app/space/organization relationships --- app/controllers/v3/access_rules_controller.rb | 15 ++++-- ...access_rule_selector_resource_decorator.rb | 9 ++-- app/messages/access_rule_create_message.rb | 2 - app/messages/access_rules_list_message.rb | 8 +-- app/models/runtime/route_access_rule.rb | 1 - app/presenters/v3/access_rule_presenter.rb | 50 ++++++++++++++--- ...001_remove_name_from_route_access_rules.rb | 15 ++++++ spec/request/access_rules_spec.rb | 44 +-------------- .../access_rules_list_message_spec.rb | 54 +++++++++++++++---- 9 files changed, 125 insertions(+), 73 deletions(-) create mode 100644 db/migrations/20260415000001_remove_name_from_route_access_rules.rb diff --git a/app/controllers/v3/access_rules_controller.rb b/app/controllers/v3/access_rules_controller.rb index 73876128299..5e1496cf58d 100644 --- a/app/controllers/v3/access_rules_controller.rb +++ b/app/controllers/v3/access_rules_controller.rb @@ -52,13 +52,11 @@ def create unprocessable!("Cannot add 'cf:any' selector when other access rules already exist for this route.") if message.selector == 'cf:any' && existing_selectors.any? unprocessable!("Cannot add selector '#{message.selector}': route already has a 'cf:any' rule.") if existing_selectors.include?('cf:any') && message.selector != 'cf:any' - # Uniqueness: name and selector must be unique per route - unprocessable!("An access rule with name '#{message.name}' already exists for this route.") if route.access_rules.any? { |r| r.name == message.name } + # Uniqueness: selector must be unique per route unprocessable!("An access rule with selector '#{message.selector}' already exists for this route.") if existing_selectors.include?(message.selector) access_rule = VCAP::CloudController::RouteAccessRule.new( guid: SecureRandom.uuid, - name: message.name, selector: message.selector, route_id: route.id, created_at: Time.now.utc, @@ -127,9 +125,18 @@ def build_dataset(message) select_all(:route_access_rules) end - dataset = dataset.where(name: message.names) if message.requested?(:names) + dataset = dataset.where(guid: message.guids) if message.requested?(:guids) dataset = dataset.where(selector: message.selectors) if message.requested?(:selectors) + if message.requested?(:selector_resource_guids) + # Text-match against selector string for resource GUIDs + # Handles cf:app:, cf:space:, cf:org: + conditions = message.selector_resource_guids.map do |guid| + Sequel.like(:selector, "%#{guid}%") + end + dataset = dataset.where(Sequel.|(*conditions)) + end + dataset end end diff --git a/app/decorators/include_access_rule_selector_resource_decorator.rb b/app/decorators/include_access_rule_selector_resource_decorator.rb index cd85dd0ef1c..9db7a079679 100644 --- a/app/decorators/include_access_rule_selector_resource_decorator.rb +++ b/app/decorators/include_access_rule_selector_resource_decorator.rb @@ -6,12 +6,15 @@ class IncludeAccessRuleSelectorResourceDecorator SELECTOR_REGEX = /\Acf:(app|space|org):([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\z/ def self.match?(include_params) - include_params&.include?('selector_resource') + return false unless include_params + + # Match if any of: selector_resource, app, space, organization + (include_params & %w[selector_resource app space organization]).any? end def self.decorate(hash, access_rules) hash[:included] ||= {} - + # Collect all GUIDs by type app_guids = [] space_guids = [] @@ -38,7 +41,7 @@ def self.decorate(hash, access_rules) hash[:included][:apps] = fetch_and_present_apps(app_guids.uniq) hash[:included][:spaces] = fetch_and_present_spaces(space_guids.uniq) hash[:included][:organizations] = fetch_and_present_organizations(org_guids.uniq) - + hash end diff --git a/app/messages/access_rule_create_message.rb b/app/messages/access_rule_create_message.rb index f3086bf95ee..d615e0a1029 100644 --- a/app/messages/access_rule_create_message.rb +++ b/app/messages/access_rule_create_message.rb @@ -5,7 +5,6 @@ class AccessRuleCreateMessage < MetadataBaseMessage SELECTOR_REGEX = /\A(cf:(app|space|org):[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|cf:any)\z/ register_allowed_keys %i[ - name selector relationships ] @@ -13,7 +12,6 @@ class AccessRuleCreateMessage < MetadataBaseMessage validates_with NoAdditionalKeysValidator validates_with RelationshipValidator - validates :name, presence: true, string: true validates :selector, presence: true, string: true validate :selector_format_valid diff --git a/app/messages/access_rules_list_message.rb b/app/messages/access_rules_list_message.rb index ddf22935f51..3b7c84b99f3 100644 --- a/app/messages/access_rules_list_message.rb +++ b/app/messages/access_rules_list_message.rb @@ -3,20 +3,22 @@ module VCAP::CloudController class AccessRulesListMessage < ListMessage register_allowed_keys %i[ + guids route_guids space_guids - names selectors + selector_resource_guids include ] validates_with NoAdditionalParamsValidator - validates_with IncludeParamValidator, valid_values: ['selector_resource', 'route'] + validates_with IncludeParamValidator, valid_values: %w[selector_resource route app space organization] validates :space_guids, array: true, allow_nil: true + validates :selector_resource_guids, array: true, allow_nil: true def self.from_params(params) - super(params, %w[route_guids space_guids names selectors include]) + super(params, %w[guids route_guids space_guids selectors selector_resource_guids include]) end end end diff --git a/app/models/runtime/route_access_rule.rb b/app/models/runtime/route_access_rule.rb index cf554de3fd8..e9b29756e39 100644 --- a/app/models/runtime/route_access_rule.rb +++ b/app/models/runtime/route_access_rule.rb @@ -7,7 +7,6 @@ class RouteAccessRule < Sequel::Model(:route_access_rules) without_guid_generation: true def validate - validates_presence :name validates_presence :selector validates_presence :route_id end diff --git a/app/presenters/v3/access_rule_presenter.rb b/app/presenters/v3/access_rule_presenter.rb index cd5f18d2c47..b1d038fb87a 100644 --- a/app/presenters/v3/access_rule_presenter.rb +++ b/app/presenters/v3/access_rule_presenter.rb @@ -12,15 +12,12 @@ def to_hash guid: access_rule.guid, created_at: access_rule.created_at, updated_at: access_rule.updated_at, - name: access_rule.name, selector: access_rule.selector, - relationships: { - route: { - data: { - guid: access_rule.route.guid - } - } + metadata: { + labels: hashified_labels(access_rule.labels), + annotations: hashified_annotations(access_rule.annotations) }, + relationships: build_relationships, links: build_links } end @@ -31,6 +28,45 @@ def access_rule @resource end + def build_relationships + relationships = { + route: { + data: { + guid: access_rule.route.guid + } + } + } + + # Extract resource GUID from selector and populate read-only relationships + selector_match = access_rule.selector.match(/\Acf:(app|space|org):([0-9a-f-]+)\z/) + if selector_match + resource_type = selector_match[1] + resource_guid = selector_match[2] + + case resource_type + when 'app' + relationships[:app] = { data: { guid: resource_guid } } + relationships[:space] = { data: nil } + relationships[:organization] = { data: nil } + when 'space' + relationships[:app] = { data: nil } + relationships[:space] = { data: { guid: resource_guid } } + relationships[:organization] = { data: nil } + when 'org' + relationships[:app] = { data: nil } + relationships[:space] = { data: nil } + relationships[:organization] = { data: { guid: resource_guid } } + end + else + # cf:any or malformed - all relationships are null + relationships[:app] = { data: nil } + relationships[:space] = { data: nil } + relationships[:organization] = { data: nil } + end + + relationships + end + def build_links { self: { diff --git a/db/migrations/20260415000001_remove_name_from_route_access_rules.rb b/db/migrations/20260415000001_remove_name_from_route_access_rules.rb new file mode 100644 index 00000000000..5763c0150ac --- /dev/null +++ b/db/migrations/20260415000001_remove_name_from_route_access_rules.rb @@ -0,0 +1,15 @@ +Sequel.migration do + up do + alter_table :route_access_rules do + drop_index %i[route_id name], name: :route_access_rules_route_id_name_index + drop_column :name + end + end + + down do + alter_table :route_access_rules do + add_column :name, String, size: 255 + add_index %i[route_id name], unique: true, name: :route_access_rules_route_id_name_index + end + end +end diff --git a/spec/request/access_rules_spec.rb b/spec/request/access_rules_spec.rb index 4fdd65f5736..a14410f1807 100644 --- a/spec/request/access_rules_spec.rb +++ b/spec/request/access_rules_spec.rb @@ -27,7 +27,6 @@ def expected_rule_json(rule) guid: rule.guid, created_at: iso8601, updated_at: iso8601, - name: rule.name, selector: rule.selector, relationships: { route: { data: { guid: rule.route.guid } } @@ -48,7 +47,6 @@ def expected_rule_json(rule) describe 'POST /v3/access_rules' do let(:request_body) do { - name: 'allow-frontend', selector: "cf:app:#{valid_uuid}", relationships: { route: { data: { guid: mtls_route.guid } } @@ -62,7 +60,6 @@ def expected_rule_json(rule) expect(last_response.status).to eq(201) parsed = Oj.load(last_response.body) - expect(parsed['name']).to eq('allow-frontend') expect(parsed['selector']).to eq("cf:app:#{valid_uuid}") expect(parsed['relationships']['route']['data']['guid']).to eq(mtls_route.guid) end @@ -81,7 +78,6 @@ def expected_rule_json(rule) context 'when the domain does not have enforce_access_rules enabled' do let(:request_body) do { - name: 'disallowed-rule', selector: "cf:app:#{valid_uuid}", relationships: { route: { data: { guid: regular_route.guid } } @@ -100,7 +96,6 @@ def expected_rule_json(rule) context 'when the route does not exist' do let(:request_body) do { - name: 'bad-rule', selector: "cf:app:#{valid_uuid}", relationships: { route: { data: { guid: 'nonexistent-guid' } } @@ -119,7 +114,6 @@ def expected_rule_json(rule) before do VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'existing-rule', selector: "cf:app:#{valid_uuid}", route_id: mtls_route.id ) @@ -127,7 +121,6 @@ def expected_rule_json(rule) it 'rejects cf:any when other rules exist' do post '/v3/access_rules', { - name: 'any-rule', selector: 'cf:any', relationships: { route: { data: { guid: mtls_route.guid } } } }.to_json, admin_header @@ -141,7 +134,6 @@ def expected_rule_json(rule) before do VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'any-rule', selector: 'cf:any', route_id: mtls_route.id ) @@ -149,7 +141,6 @@ def expected_rule_json(rule) it 'rejects adding a specific selector' do post '/v3/access_rules', { - name: 'specific-rule', selector: "cf:space:#{valid_uuid}", relationships: { route: { data: { guid: mtls_route.guid } } } }.to_json, admin_header @@ -163,7 +154,6 @@ def expected_rule_json(rule) before do VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'allow-frontend', selector: "cf:app:#{valid_uuid}", route_id: mtls_route.id ) @@ -172,7 +162,6 @@ def expected_rule_json(rule) it 'returns 422' do other_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' post '/v3/access_rules', { - name: 'allow-frontend', selector: "cf:space:#{other_uuid}", relationships: { route: { data: { guid: mtls_route.guid } } } }.to_json, admin_header @@ -186,7 +175,6 @@ def expected_rule_json(rule) before do VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'first-rule', selector: "cf:app:#{valid_uuid}", route_id: mtls_route.id ) @@ -194,7 +182,6 @@ def expected_rule_json(rule) it 'returns 422' do post '/v3/access_rules', { - name: 'second-rule', selector: "cf:app:#{valid_uuid}", relationships: { route: { data: { guid: mtls_route.guid } } } }.to_json, admin_header @@ -206,7 +193,6 @@ def expected_rule_json(rule) context 'invalid selector format' do it 'returns 422' do post '/v3/access_rules', { - name: 'bad-rule', selector: 'not-valid', relationships: { route: { data: { guid: mtls_route.guid } } } }.to_json, admin_header @@ -221,7 +207,6 @@ def expected_rule_json(rule) let!(:access_rule) do VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'allow-frontend', selector: "cf:app:#{valid_uuid}", route_id: mtls_route.id ) @@ -233,7 +218,6 @@ def expected_rule_json(rule) expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) expect(parsed['guid']).to eq(access_rule.guid) - expect(parsed['name']).to eq('allow-frontend') expect(parsed['selector']).to eq("cf:app:#{valid_uuid}") end @@ -250,7 +234,6 @@ def expected_rule_json(rule) let!(:rule1) do VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'rule-one', selector: "cf:app:#{valid_uuid}", route_id: mtls_route.id ) @@ -258,7 +241,6 @@ def expected_rule_json(rule) let!(:rule2) do VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'rule-two', selector: 'cf:any', route_id: VCAP::CloudController::Route.make(space: space, domain: mtls_domain).id ) @@ -283,15 +265,6 @@ def expected_rule_json(rule) expect(guids).not_to include(rule2.guid) end - it 'filters by names' do - get '/v3/access_rules?names=rule-one', nil, admin_header - - expect(last_response.status).to eq(200) - parsed = Oj.load(last_response.body) - expect(parsed['resources'].length).to eq(1) - expect(parsed['resources'][0]['name']).to eq('rule-one') - end - it 'filters by selectors' do get '/v3/access_rules?selectors=cf:any', nil, admin_header @@ -315,7 +288,6 @@ def expected_rule_json(rule) let!(:rule_in_other_space) do VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'rule-in-other-space', selector: 'cf:any', route_id: other_route.id ) @@ -346,13 +318,13 @@ def expected_rule_json(rule) end it 'combines space_guids with other filters' do - get "/v3/access_rules?space_guids=#{space.guid}&names=rule-one", nil, admin_header + get "/v3/access_rules?space_guids=#{space.guid}&selectors=cf:app:#{valid_uuid}", nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) expect(parsed['resources'].length).to eq(1) expect(parsed['resources'][0]['guid']).to eq(rule1.guid) - expect(parsed['resources'][0]['name']).to eq('rule-one') + expect(parsed['resources'][0]['selector']).to eq("cf:app:#{valid_uuid}") end it 'returns empty when space has no access rules' do @@ -376,7 +348,6 @@ def expected_rule_json(rule) let!(:app_rule) do VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'app-rule', selector: "cf:app:#{app.guid}", route_id: mtls_route.id ) @@ -385,7 +356,6 @@ def expected_rule_json(rule) let!(:space_rule) do VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'space-rule', selector: "cf:space:#{other_space.guid}", route_id: mtls_route.id ) @@ -394,7 +364,6 @@ def expected_rule_json(rule) let!(:org_rule) do VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'org-rule', selector: "cf:org:#{other_org.guid}", route_id: mtls_route.id ) @@ -433,7 +402,6 @@ def expected_rule_json(rule) stale_guid = '99999999-9999-9999-9999-999999999999' VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'stale-rule', selector: "cf:app:#{stale_guid}", route_id: mtls_route.id ) @@ -452,7 +420,6 @@ def expected_rule_json(rule) # Create another rule referencing the same app VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'another-app-rule', selector: "cf:app:#{app.guid}", route_id: VCAP::CloudController::Route.make(space: space, domain: mtls_domain).id ) @@ -470,7 +437,6 @@ def expected_rule_json(rule) it 'does not include resources for cf:any selectors' do VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'any-rule', selector: 'cf:any', route_id: VCAP::CloudController::Route.make(space: space, domain: mtls_domain).id ) @@ -488,7 +454,6 @@ def expected_rule_json(rule) let!(:rule_on_route1) do VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'rule-on-route1', selector: 'cf:any', route_id: mtls_route.id ) @@ -497,7 +462,6 @@ def expected_rule_json(rule) let!(:rule_on_route2) do VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'rule-on-route2', selector: "cf:app:#{valid_uuid}", route_id: route2.id ) @@ -529,7 +493,6 @@ def expected_rule_json(rule) # Create another rule on the same route VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'another-rule-on-route1', selector: "cf:app:#{valid_uuid}", route_id: mtls_route.id ) @@ -548,7 +511,6 @@ def expected_rule_json(rule) app = VCAP::CloudController::AppModel.make(space: space, name: 'test-app') VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'combined-rule', selector: "cf:app:#{app.guid}", route_id: mtls_route.id ) @@ -577,7 +539,6 @@ def expected_rule_json(rule) let!(:access_rule) do VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'to-delete', selector: "cf:app:#{valid_uuid}", route_id: mtls_route.id ) @@ -603,7 +564,6 @@ def expected_rule_json(rule) let!(:access_rule) do VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'patchable', selector: "cf:app:#{valid_uuid}", route_id: mtls_route.id ) diff --git a/spec/unit/messages/access_rules_list_message_spec.rb b/spec/unit/messages/access_rules_list_message_spec.rb index 443fdf70bfd..4790229787e 100644 --- a/spec/unit/messages/access_rules_list_message_spec.rb +++ b/spec/unit/messages/access_rules_list_message_spec.rb @@ -6,14 +6,15 @@ module VCAP::CloudController describe '.from_params' do let(:params) do { + 'guids' => 'guid1,guid2', 'route_guids' => 'route1,route2', 'space_guids' => 'space1,space2', - 'names' => 'name1,name2', 'selectors' => 'selector1,selector2', + 'selector_resource_guids' => 'resource1,resource2', 'page' => 1, 'per_page' => 5, 'order_by' => 'created_at', - 'include' => 'selector_resource,route' + 'include' => 'selector_resource,route,app,space,organization' } end @@ -21,23 +22,25 @@ module VCAP::CloudController message = AccessRulesListMessage.from_params(params) expect(message).to be_a(AccessRulesListMessage) + expect(message.guids).to eq(%w[guid1 guid2]) expect(message.route_guids).to eq(%w[route1 route2]) expect(message.space_guids).to eq(%w[space1 space2]) - expect(message.names).to eq(%w[name1 name2]) expect(message.selectors).to eq(%w[selector1 selector2]) + expect(message.selector_resource_guids).to eq(%w[resource1 resource2]) expect(message.page).to eq(1) expect(message.per_page).to eq(5) expect(message.order_by).to eq('created_at') - expect(message.include).to eq(%w[selector_resource route]) + expect(message.include).to eq(%w[selector_resource route app space organization]) end it 'converts requested keys to symbols' do message = AccessRulesListMessage.from_params(params) + expect(message).to be_requested(:guids) expect(message).to be_requested(:route_guids) expect(message).to be_requested(:space_guids) - expect(message).to be_requested(:names) expect(message).to be_requested(:selectors) + expect(message).to be_requested(:selector_resource_guids) expect(message).to be_requested(:page) expect(message).to be_requested(:per_page) expect(message).to be_requested(:order_by) @@ -48,19 +51,20 @@ module VCAP::CloudController describe '#to_param_hash' do let(:opts) do { + guids: %w[guid1 guid2], route_guids: %w[route1 route2], space_guids: %w[space1 space2], - names: %w[name1 name2], selectors: %w[selector1 selector2], + selector_resource_guids: %w[resource1 resource2], page: 1, per_page: 5, order_by: 'created_at', - include: %w[selector_resource route] + include: %w[selector_resource route app space organization] } end it 'excludes the pagination keys' do - expected_params = %i[route_guids space_guids names selectors include] + expected_params = %i[guids route_guids space_guids selectors selector_resource_guids include] expect(AccessRulesListMessage.from_params(opts).to_param_hash.keys).to match_array(expected_params) end end @@ -69,14 +73,15 @@ module VCAP::CloudController it 'accepts a set of fields' do expect do AccessRulesListMessage.from_params({ + guids: [], route_guids: [], space_guids: [], - names: [], selectors: [], + selector_resource_guids: [], page: 1, per_page: 5, order_by: 'created_at', - include: ['selector_resource', 'route'] + include: %w[selector_resource route app space organization] }) end.not_to raise_error end @@ -101,7 +106,16 @@ module VCAP::CloudController message = AccessRulesListMessage.from_params({ 'include' => 'route' }) expect(message).to be_valid - message = AccessRulesListMessage.from_params({ 'include' => 'selector_resource,route' }) + message = AccessRulesListMessage.from_params({ 'include' => 'app' }) + expect(message).to be_valid + + message = AccessRulesListMessage.from_params({ 'include' => 'space' }) + expect(message).to be_valid + + message = AccessRulesListMessage.from_params({ 'include' => 'organization' }) + expect(message).to be_valid + + message = AccessRulesListMessage.from_params({ 'include' => 'selector_resource,route,app,space,organization' }) expect(message).to be_valid end @@ -129,6 +143,24 @@ module VCAP::CloudController expect(message).to be_valid expect(message.space_guids).to eq(%w[space1 space2]) end + + it 'validates selector_resource_guids is an array' do + message = AccessRulesListMessage.from_params selector_resource_guids: 'not array' + expect(message).not_to be_valid + expect(message.errors[:selector_resource_guids].length).to eq 1 + end + + it 'allows selector_resource_guids to be nil' do + message = AccessRulesListMessage.from_params({}) + expect(message).to be_valid + expect(message.selector_resource_guids).to be_nil + end + + it 'allows selector_resource_guids to be an array' do + message = AccessRulesListMessage.from_params selector_resource_guids: %w[guid1 guid2] + expect(message).to be_valid + expect(message.selector_resource_guids).to eq(%w[guid1 guid2]) + end end end end From 40f6a8afce98b591ec651dc2bba481b8498f99ff Mon Sep 17 00:00:00 2001 From: rkoster Date: Wed, 15 Apr 2026 08:24:18 +0000 Subject: [PATCH 13/29] Add metadata support to RouteAccessRule model - Create RouteAccessRuleLabelModel and RouteAccessRuleAnnotationModel - Add one_to_many relationships for labels and annotations to RouteAccessRule - Add database migrations for route_access_rule_labels and route_access_rule_annotations tables - Fixes: undefined method 'labels' error in AccessRulePresenter This enables metadata (labels/annotations) support for access rules, required by the RFC changes that removed the 'name' field in favor of using labels/annotations for metadata storage. --- app/models/runtime/route_access_rule.rb | 6 +++++ .../route_access_rule_annotation_model.rb | 11 ++++++++ .../runtime/route_access_rule_label_model.rb | 9 +++++++ ...5000002_create_route_access_rule_labels.rb | 25 +++++++++++++++++++ ...03_create_route_access_rule_annotations.rb | 25 +++++++++++++++++++ 5 files changed, 76 insertions(+) create mode 100644 app/models/runtime/route_access_rule_annotation_model.rb create mode 100644 app/models/runtime/route_access_rule_label_model.rb create mode 100644 db/migrations/20260415000002_create_route_access_rule_labels.rb create mode 100644 db/migrations/20260415000003_create_route_access_rule_annotations.rb diff --git a/app/models/runtime/route_access_rule.rb b/app/models/runtime/route_access_rule.rb index e9b29756e39..17d4a060b07 100644 --- a/app/models/runtime/route_access_rule.rb +++ b/app/models/runtime/route_access_rule.rb @@ -6,6 +6,12 @@ class RouteAccessRule < Sequel::Model(:route_access_rules) primary_key: :id, without_guid_generation: true + one_to_many :labels, class: 'VCAP::CloudController::RouteAccessRuleLabelModel', key: :resource_guid, primary_key: :guid + one_to_many :annotations, class: 'VCAP::CloudController::RouteAccessRuleAnnotationModel', key: :resource_guid, primary_key: :guid + + add_association_dependencies labels: :destroy + add_association_dependencies annotations: :destroy + def validate validates_presence :selector validates_presence :route_id diff --git a/app/models/runtime/route_access_rule_annotation_model.rb b/app/models/runtime/route_access_rule_annotation_model.rb new file mode 100644 index 00000000000..a0962184156 --- /dev/null +++ b/app/models/runtime/route_access_rule_annotation_model.rb @@ -0,0 +1,11 @@ +module VCAP::CloudController + class RouteAccessRuleAnnotationModel < Sequel::Model(:route_access_rule_annotations) + set_primary_key :id + many_to_one :route_access_rule, + primary_key: :guid, + key: :resource_guid, + without_guid_generation: true + + include MetadataModelMixin + end +end diff --git a/app/models/runtime/route_access_rule_label_model.rb b/app/models/runtime/route_access_rule_label_model.rb new file mode 100644 index 00000000000..47737f5381a --- /dev/null +++ b/app/models/runtime/route_access_rule_label_model.rb @@ -0,0 +1,9 @@ +module VCAP::CloudController + class RouteAccessRuleLabelModel < Sequel::Model(:route_access_rule_labels) + many_to_one :route_access_rule, + primary_key: :guid, + key: :resource_guid, + without_guid_generation: true + include MetadataModelMixin + end +end diff --git a/db/migrations/20260415000002_create_route_access_rule_labels.rb b/db/migrations/20260415000002_create_route_access_rule_labels.rb new file mode 100644 index 00000000000..b50f71ea233 --- /dev/null +++ b/db/migrations/20260415000002_create_route_access_rule_labels.rb @@ -0,0 +1,25 @@ +Sequel.migration do + up do + unless table_exists?(:route_access_rule_labels) + create_table :route_access_rule_labels do + primary_key :id + String :guid, null: false, size: 255 + String :resource_guid, null: false, size: 255 + String :key_prefix, size: 253 + String :key_name, null: false, size: 63 + String :value, null: false, size: 63 + DateTime :created_at, null: false + DateTime :updated_at + + index :guid, unique: true, name: :route_access_rule_labels_guid_index + index :resource_guid, name: :route_access_rule_labels_resource_guid_index + index %i[resource_guid key_prefix key_name], unique: true, name: :route_access_rule_labels_compound_index + foreign_key [:resource_guid], :route_access_rules, key: :guid, on_delete: :cascade, name: :fk_route_access_rule_labels_resource_guid + end + end + end + + down do + drop_table(:route_access_rule_labels) if table_exists?(:route_access_rule_labels) + end +end diff --git a/db/migrations/20260415000003_create_route_access_rule_annotations.rb b/db/migrations/20260415000003_create_route_access_rule_annotations.rb new file mode 100644 index 00000000000..466950e9e08 --- /dev/null +++ b/db/migrations/20260415000003_create_route_access_rule_annotations.rb @@ -0,0 +1,25 @@ +Sequel.migration do + up do + unless table_exists?(:route_access_rule_annotations) + create_table :route_access_rule_annotations do + primary_key :id + String :guid, null: false, size: 255 + String :resource_guid, null: false, size: 255 + String :key_prefix, size: 253 + String :key, null: false, size: 1000 + String :value, size: 5000 + DateTime :created_at, null: false + DateTime :updated_at + + index :guid, unique: true, name: :route_access_rule_annotations_guid_index + index :resource_guid, name: :route_access_rule_annotations_resource_guid_index + index %i[resource_guid key], unique: true, name: :route_access_rule_annotations_key_index + foreign_key [:resource_guid], :route_access_rules, key: :guid, on_delete: :cascade, name: :fk_route_access_rule_annotations_resource_guid + end + end + end + + down do + drop_table(:route_access_rule_annotations) if table_exists?(:route_access_rule_annotations) + end +end From f77b5f3a561f57d046f92afb6813dfd273673838 Mon Sep 17 00:00:00 2001 From: rkoster Date: Wed, 15 Apr 2026 08:47:48 +0000 Subject: [PATCH 14/29] Fix class loading for RouteAccessRule metadata models Add require statements for RouteAccessRuleLabelModel and RouteAccessRuleAnnotationModel to app/models.rb. Rails autoloading is disabled for app/** so all models must be explicitly required. This fixes the error: uninitialized constant VCAP::CloudController::RouteAccessRuleLabelModel --- app/models.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models.rb b/app/models.rb index 84ddeb4813e..fad27a05c79 100644 --- a/app/models.rb +++ b/app/models.rb @@ -70,6 +70,8 @@ require 'models/runtime/revision_sidecar_process_type_model' require 'models/runtime/route' require 'models/runtime/route_access_rule' +require 'models/runtime/route_access_rule_label_model' +require 'models/runtime/route_access_rule_annotation_model' require 'models/runtime/space_routes' require 'models/runtime/space_quota_definition' require 'models/runtime/stack' From f5255a2762a7428b9b2e6d2443102769b2782bb2 Mon Sep 17 00:00:00 2001 From: rkoster Date: Wed, 15 Apr 2026 09:21:26 +0000 Subject: [PATCH 15/29] Add validation to prevent access rules on internal domains per RFC Per RFC requirement (line 246-247): Access rules cannot be created for routes on internal domains (domains created with --internal). Internal routes use container-to-container networking and bypass GoRouter entirely, so GoRouter cannot enforce access rules. Changes: - Add validation in AccessRulesController#create to reject access rules on internal domains with 422 status - Add test coverage for internal domain validation - Error message explains why: internal domains bypass GoRouter --- app/controllers/v3/access_rules_controller.rb | 3 +++ spec/request/access_rules_spec.rb | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/app/controllers/v3/access_rules_controller.rb b/app/controllers/v3/access_rules_controller.rb index 5e1496cf58d..d2495ea959a 100644 --- a/app/controllers/v3/access_rules_controller.rb +++ b/app/controllers/v3/access_rules_controller.rb @@ -44,6 +44,9 @@ def create unauthorized! unless permission_queryer.can_write_to_active_space?(route.space.id) suspended! unless permission_queryer.is_space_active?(route.space.id) + if route.domain.internal? + unprocessable!('Cannot create access rules for routes on internal domains. Internal routes use container-to-container networking and bypass GoRouter.') + end 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 # Enforce cf:any exclusivity: if route already has a cf:any rule, reject new rules; diff --git a/spec/request/access_rules_spec.rb b/spec/request/access_rules_spec.rb index a14410f1807..36eadd5332a 100644 --- a/spec/request/access_rules_spec.rb +++ b/spec/request/access_rules_spec.rb @@ -16,9 +16,16 @@ let(:regular_domain) do VCAP::CloudController::PrivateDomain.make(owning_organization: org) end + let(:internal_domain) do + VCAP::CloudController::PrivateDomain.make( + owning_organization: org, + internal: true + ) + end let(:mtls_route) { VCAP::CloudController::Route.make(space: space, domain: mtls_domain) } let(:regular_route) { VCAP::CloudController::Route.make(space: space, domain: regular_domain) } + let(:internal_route) { VCAP::CloudController::Route.make(space: space, domain: internal_domain) } let(:valid_uuid) { '11111111-2222-3333-4444-555555555555' } @@ -93,6 +100,25 @@ def expected_rule_json(rule) end end + context 'when the route is on an internal domain' do + let(:request_body) do + { + selector: "cf:app:#{valid_uuid}", + relationships: { + route: { data: { guid: internal_route.guid } } + } + } + end + + it 'returns 422 with a message about internal domains' do + post '/v3/access_rules', request_body.to_json, admin_header + + expect(last_response.status).to eq(422) + expect(last_response.body).to include('internal domains') + expect(last_response.body).to include('container-to-container networking') + end + end + context 'when the route does not exist' do let(:request_body) do { From b24ba37dc5c6b3c37f0f37ebd4f1071c1283d121 Mon Sep 17 00:00:00 2001 From: rkoster Date: Wed, 15 Apr 2026 13:33:49 +0000 Subject: [PATCH 16/29] Consolidate access rules migrations, fix RuboCop offenses, and clean up tests - Collapse 4 migrations into 1 consolidated migration (20260407100001) that creates route_access_rules, route_access_rule_labels, and route_access_rule_annotations tables - Remove name field from access rules per RFC updates - Fix all RuboCop offenses: Style/CollectionQuerying (.count > 0 -> .any?), Migration/AddConstraintName (primary_key :id, name: :id), Metrics/BlockLength, Metrics/CyclomaticComplexity, and others - Add stale resource detection in presenter (null data for deleted resources) - Extract controller methods to reduce complexity - Use relative class names within VCAP::CloudController module - Fix test shadowing of rack-test app method (let(:app) -> let(:frontend_app)) - Fix Sequel validation assertion style (.include(:presence) not strings) --- app/access/access_rule_access.rb | 8 +-- app/controllers/v3/access_rules_controller.rb | 48 ++++++++----- .../include_access_rule_route_decorator.rb | 6 +- ...access_rule_selector_resource_decorator.rb | 20 +++--- app/messages/access_rules_list_message.rb | 2 +- app/messages/domain_create_message.rb | 15 ++-- app/presenters/v3/access_rule_presenter.rb | 10 ++- app/presenters/v3/route_presenter.rb | 4 +- ...0260407100001_create_route_access_rules.rb | 42 +++++++++-- ...001_remove_name_from_route_access_rules.rb | 15 ---- ...5000002_create_route_access_rule_labels.rb | 25 ------- ...03_create_route_access_rule_annotations.rb | 25 ------- .../diego/protocol/routing_info.rb | 44 +++++++----- spec/request/access_rules_spec.rb | 53 +++++--------- .../diego/protocol/routing_info_spec.rb | 4 +- .../access_rule_create_message_spec.rb | 72 ++++--------------- .../models/runtime/route_access_rule_spec.rb | 70 ++++++++---------- 17 files changed, 189 insertions(+), 274 deletions(-) delete mode 100644 db/migrations/20260415000001_remove_name_from_route_access_rules.rb delete mode 100644 db/migrations/20260415000002_create_route_access_rule_labels.rb delete mode 100644 db/migrations/20260415000003_create_route_access_rule_annotations.rb diff --git a/app/access/access_rule_access.rb b/app/access/access_rule_access.rb index 72fff7ebf30..db5755e1f57 100644 --- a/app/access/access_rule_access.rb +++ b/app/access/access_rule_access.rb @@ -47,12 +47,12 @@ def read_for_update_with_token?(_) admin_user? || has_write_scope? end - def can_remove_related_object_with_token?(*args) - read_for_update_with_token?(*args) + def can_remove_related_object_with_token?(*) + read_for_update_with_token?(*) end - def read_related_object_for_update_with_token?(*args) - read_for_update_with_token?(*args) + def read_related_object_for_update_with_token?(*) + read_for_update_with_token?(*) end def update_with_token?(_) diff --git a/app/controllers/v3/access_rules_controller.rb b/app/controllers/v3/access_rules_controller.rb index d2495ea959a..af14ae4ce9b 100644 --- a/app/controllers/v3/access_rules_controller.rb +++ b/app/controllers/v3/access_rules_controller.rb @@ -39,24 +39,9 @@ def create message = AccessRuleCreateMessage.new(hashed_params[:body]) unprocessable!(message.errors.full_messages) unless message.valid? - route = VCAP::CloudController::Route.find(guid: message.route_guid) - resource_not_found!(:route) unless route && permission_queryer.can_read_from_space?(route.space.id, route.space.organization_id) - unauthorized! unless permission_queryer.can_write_to_active_space?(route.space.id) - suspended! unless permission_queryer.is_space_active?(route.space.id) - - if route.domain.internal? - unprocessable!('Cannot create access rules for routes on internal domains. Internal routes use container-to-container networking and bypass GoRouter.') - end - 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 - - # Enforce cf:any exclusivity: if route already has a cf:any rule, reject new rules; - # if new rule is cf:any, reject if route already has any rules. - existing_selectors = route.access_rules.map(&:selector) - unprocessable!("Cannot add 'cf:any' selector when other access rules already exist for this route.") if message.selector == 'cf:any' && existing_selectors.any? - unprocessable!("Cannot add selector '#{message.selector}': route already has a 'cf:any' rule.") if existing_selectors.include?('cf:any') && message.selector != 'cf:any' - - # Uniqueness: selector must be unique per route - unprocessable!("An access rule with selector '#{message.selector}' already exists for this route.") if existing_selectors.include?(message.selector) + route = find_and_authorize_route(message.route_guid) + validate_route_domain(route) + validate_selector_exclusivity(route, message.selector) access_rule = VCAP::CloudController::RouteAccessRule.new( guid: SecureRandom.uuid, @@ -102,6 +87,33 @@ def destroy private + def find_and_authorize_route(route_guid) + route = VCAP::CloudController::Route.find(guid: route_guid) + resource_not_found!(:route) unless route && permission_queryer.can_read_from_space?(route.space.id, route.space.organization_id) + unauthorized! unless permission_queryer.can_write_to_active_space?(route.space.id) + suspended! unless permission_queryer.is_space_active?(route.space.id) + route + end + + def validate_route_domain(route) + if route.domain.internal? + unprocessable!('Cannot create access rules for routes on internal domains. Internal routes use container-to-container networking and bypass GoRouter.') + end + 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 + end + + def validate_selector_exclusivity(route, selector) + existing_selectors = route.access_rules.map(&:selector) + + # Enforce cf:any exclusivity: if route already has a cf:any rule, reject new rules; + # if new rule is cf:any, reject if route already has any rules. + unprocessable!("Cannot add 'cf:any' selector when other access rules already exist for this route.") if selector == 'cf:any' && existing_selectors.any? + unprocessable!("Cannot add selector '#{selector}': route already has a 'cf:any' rule.") if existing_selectors.include?('cf:any') && selector != 'cf:any' + + # Uniqueness: selector must be unique per route + unprocessable!("An access rule with selector '#{selector}' already exists for this route.") if existing_selectors.include?(selector) + end + def build_dataset(message) dataset = VCAP::CloudController::RouteAccessRule.dataset diff --git a/app/decorators/include_access_rule_route_decorator.rb b/app/decorators/include_access_rule_route_decorator.rb index 178da8be3db..45ea0baca57 100644 --- a/app/decorators/include_access_rule_route_decorator.rb +++ b/app/decorators/include_access_rule_route_decorator.rb @@ -14,12 +14,12 @@ def self.decorate(hash, access_rules) route_ids = access_rules.map(&:route_id).uniq # Fetch routes with their associations - routes = VCAP::CloudController::Route.where(id: route_ids). + routes = Route.where(id: route_ids). order(:created_at, :guid). - eager(VCAP::CloudController::Presenters::V3::RoutePresenter.associated_resources).all + eager(Presenters::V3::RoutePresenter.associated_resources).all # Present routes - hash[:included][:routes] = routes.map { |route| VCAP::CloudController::Presenters::V3::RoutePresenter.new(route).to_hash } + hash[:included][:routes] = routes.map { |route| Presenters::V3::RoutePresenter.new(route).to_hash } hash end diff --git a/app/decorators/include_access_rule_selector_resource_decorator.rb b/app/decorators/include_access_rule_selector_resource_decorator.rb index 9db7a079679..19b05314177 100644 --- a/app/decorators/include_access_rule_selector_resource_decorator.rb +++ b/app/decorators/include_access_rule_selector_resource_decorator.rb @@ -9,7 +9,7 @@ def self.match?(include_params) return false unless include_params # Match if any of: selector_resource, app, space, organization - (include_params & %w[selector_resource app space organization]).any? + include_params.intersect?(%w[selector_resource app space organization]) end def self.decorate(hash, access_rules) @@ -48,28 +48,28 @@ def self.decorate(hash, access_rules) private_class_method def self.fetch_and_present_apps(guids) return [] if guids.empty? - apps = VCAP::CloudController::AppModel.where(guid: guids). + apps = AppModel.where(guid: guids). order(:created_at, :guid). - eager(VCAP::CloudController::Presenters::V3::AppPresenter.associated_resources).all - apps.map { |app| VCAP::CloudController::Presenters::V3::AppPresenter.new(app).to_hash } + eager(Presenters::V3::AppPresenter.associated_resources).all + apps.map { |app| Presenters::V3::AppPresenter.new(app).to_hash } end private_class_method def self.fetch_and_present_spaces(guids) return [] if guids.empty? - spaces = VCAP::CloudController::Space.where(guid: guids). + spaces = Space.where(guid: guids). order(:created_at, :guid). - eager(VCAP::CloudController::Presenters::V3::SpacePresenter.associated_resources).all - spaces.map { |space| VCAP::CloudController::Presenters::V3::SpacePresenter.new(space).to_hash } + eager(Presenters::V3::SpacePresenter.associated_resources).all + spaces.map { |space| Presenters::V3::SpacePresenter.new(space).to_hash } end private_class_method def self.fetch_and_present_organizations(guids) return [] if guids.empty? - orgs = VCAP::CloudController::Organization.where(guid: guids). + orgs = Organization.where(guid: guids). order(:created_at, :guid). - eager(VCAP::CloudController::Presenters::V3::OrganizationPresenter.associated_resources).all - orgs.map { |org| VCAP::CloudController::Presenters::V3::OrganizationPresenter.new(org).to_hash } + eager(Presenters::V3::OrganizationPresenter.associated_resources).all + orgs.map { |org| Presenters::V3::OrganizationPresenter.new(org).to_hash } end end end diff --git a/app/messages/access_rules_list_message.rb b/app/messages/access_rules_list_message.rb index 3b7c84b99f3..564d9680e00 100644 --- a/app/messages/access_rules_list_message.rb +++ b/app/messages/access_rules_list_message.rb @@ -18,7 +18,7 @@ class AccessRulesListMessage < ListMessage validates :selector_resource_guids, array: true, allow_nil: true def self.from_params(params) - super(params, %w[guids route_guids space_guids selectors selector_resource_guids include]) + super(params, %w[route_guids space_guids selectors selector_resource_guids include]) end end end diff --git a/app/messages/domain_create_message.rb b/app/messages/domain_create_message.rb index b10d065b553..55f5976ee5f 100644 --- a/app/messages/domain_create_message.rb +++ b/app/messages/domain_create_message.rb @@ -106,17 +106,14 @@ def router_group_validation end def access_rules_scope_validation - if requested?(:access_rules_scope) - unless access_rules_scope.nil? || %w[any org space].include?(access_rules_scope) - errors.add(:access_rules_scope, "must be one of 'any', 'org', 'space'") - end + if requested?(:access_rules_scope) && !(access_rules_scope.nil? || %w[any org space].include?(access_rules_scope)) + errors.add(:access_rules_scope, "must be one of 'any', 'org', 'space'") end - if requested?(:enforce_access_rules) && enforce_access_rules == true - if !requested?(:access_rules_scope) || access_rules_scope.nil? - errors.add(:access_rules_scope, 'is required when enforce_access_rules is true') - end - end + return unless requested?(:enforce_access_rules) && enforce_access_rules == true + return unless !requested?(:access_rules_scope) || access_rules_scope.nil? + + errors.add(:access_rules_scope, 'is required when enforce_access_rules is true') end class Relationships < BaseMessage diff --git a/app/presenters/v3/access_rule_presenter.rb b/app/presenters/v3/access_rule_presenter.rb index b1d038fb87a..b4b95fbb9d4 100644 --- a/app/presenters/v3/access_rule_presenter.rb +++ b/app/presenters/v3/access_rule_presenter.rb @@ -38,6 +38,7 @@ def build_relationships } # Extract resource GUID from selector and populate read-only relationships + # Only include the guid in data if the resource actually exists selector_match = access_rule.selector.match(/\Acf:(app|space|org):([0-9a-f-]+)\z/) if selector_match resource_type = selector_match[1] @@ -45,17 +46,20 @@ def build_relationships case resource_type when 'app' - relationships[:app] = { data: { guid: resource_guid } } + app_exists = VCAP::CloudController::AppModel.where(guid: resource_guid).any? + relationships[:app] = { data: app_exists ? { guid: resource_guid } : nil } relationships[:space] = { data: nil } relationships[:organization] = { data: nil } when 'space' + space_exists = VCAP::CloudController::Space.where(guid: resource_guid).any? relationships[:app] = { data: nil } - relationships[:space] = { data: { guid: resource_guid } } + relationships[:space] = { data: space_exists ? { guid: resource_guid } : nil } relationships[:organization] = { data: nil } when 'org' + org_exists = VCAP::CloudController::Organization.where(guid: resource_guid).any? relationships[:app] = { data: nil } relationships[:space] = { data: nil } - relationships[:organization] = { data: { guid: resource_guid } } + relationships[:organization] = { data: org_exists ? { guid: resource_guid } : nil } end else # cf:any or malformed - all relationships are null diff --git a/app/presenters/v3/route_presenter.rb b/app/presenters/v3/route_presenter.rb index 8eab8b790c3..e2206c1aa01 100644 --- a/app/presenters/v3/route_presenter.rb +++ b/app/presenters/v3/route_presenter.rb @@ -56,10 +56,10 @@ def to_hash @decorators.reduce(hash) { |memo, d| d.decorate(memo, [route]) } end - private - INTERNAL_ROUTE_OPTIONS = %w[access_scope access_rules].freeze + private + def route @resource end diff --git a/db/migrations/20260407100001_create_route_access_rules.rb b/db/migrations/20260407100001_create_route_access_rules.rb index 4c8c78f4216..531a167f9c9 100644 --- a/db/migrations/20260407100001_create_route_access_rules.rb +++ b/db/migrations/20260407100001_create_route_access_rules.rb @@ -2,23 +2,57 @@ up do unless table_exists?(:route_access_rules) create_table :route_access_rules do + primary_key :id, name: :id String :guid, size: 255, null: false - primary_key :id - String :name, size: 255, null: false String :selector, size: 255, null: false Integer :route_id, null: false DateTime :created_at, null: false DateTime :updated_at, null: false index :guid, unique: true, name: :route_access_rules_guid_index - index %i[route_id name], unique: true, name: :route_access_rules_route_id_name_index index %i[route_id selector], unique: true, name: :route_access_rules_route_id_selector_index foreign_key [:route_id], :routes, on_delete: :cascade, name: :fk_route_access_rules_route_id end end + + unless table_exists?(:route_access_rule_labels) + create_table :route_access_rule_labels do + primary_key :id, name: :id + String :guid, null: false, size: 255 + String :resource_guid, null: false, size: 255 + String :key_prefix, size: 253 + String :key_name, null: false, size: 63 + String :value, null: false, size: 63 + DateTime :created_at, null: false + DateTime :updated_at + + index :guid, unique: true, name: :route_access_rule_labels_guid_index + index :resource_guid, name: :route_access_rule_labels_resource_guid_index + index %i[resource_guid key_prefix key_name], unique: true, name: :route_access_rule_labels_compound_index + foreign_key [:resource_guid], :route_access_rules, key: :guid, on_delete: :cascade, name: :fk_route_access_rule_labels_resource_guid + end + end + + unless table_exists?(:route_access_rule_annotations) + create_table :route_access_rule_annotations do + primary_key :id, name: :id + String :guid, null: false, size: 255 + String :resource_guid, null: false, size: 255 + String :key_prefix, size: 253 + String :key, null: false, size: 1000 + String :value, size: 5000 + DateTime :created_at, null: false + DateTime :updated_at + + index :guid, unique: true, name: :route_access_rule_annotations_guid_index + index :resource_guid, name: :route_access_rule_annotations_resource_guid_index + index %i[resource_guid key], unique: true, name: :route_access_rule_annotations_key_index + foreign_key [:resource_guid], :route_access_rules, key: :guid, on_delete: :cascade, name: :fk_route_access_rule_annotations_resource_guid + end + end end down do - drop_table(:route_access_rules) if table_exists?(:route_access_rules) + %i[route_access_rule_annotations route_access_rule_labels route_access_rules].each { |t| drop_table(t) if table_exists?(t) } end end diff --git a/db/migrations/20260415000001_remove_name_from_route_access_rules.rb b/db/migrations/20260415000001_remove_name_from_route_access_rules.rb deleted file mode 100644 index 5763c0150ac..00000000000 --- a/db/migrations/20260415000001_remove_name_from_route_access_rules.rb +++ /dev/null @@ -1,15 +0,0 @@ -Sequel.migration do - up do - alter_table :route_access_rules do - drop_index %i[route_id name], name: :route_access_rules_route_id_name_index - drop_column :name - end - end - - down do - alter_table :route_access_rules do - add_column :name, String, size: 255 - add_index %i[route_id name], unique: true, name: :route_access_rules_route_id_name_index - end - end -end diff --git a/db/migrations/20260415000002_create_route_access_rule_labels.rb b/db/migrations/20260415000002_create_route_access_rule_labels.rb deleted file mode 100644 index b50f71ea233..00000000000 --- a/db/migrations/20260415000002_create_route_access_rule_labels.rb +++ /dev/null @@ -1,25 +0,0 @@ -Sequel.migration do - up do - unless table_exists?(:route_access_rule_labels) - create_table :route_access_rule_labels do - primary_key :id - String :guid, null: false, size: 255 - String :resource_guid, null: false, size: 255 - String :key_prefix, size: 253 - String :key_name, null: false, size: 63 - String :value, null: false, size: 63 - DateTime :created_at, null: false - DateTime :updated_at - - index :guid, unique: true, name: :route_access_rule_labels_guid_index - index :resource_guid, name: :route_access_rule_labels_resource_guid_index - index %i[resource_guid key_prefix key_name], unique: true, name: :route_access_rule_labels_compound_index - foreign_key [:resource_guid], :route_access_rules, key: :guid, on_delete: :cascade, name: :fk_route_access_rule_labels_resource_guid - end - end - end - - down do - drop_table(:route_access_rule_labels) if table_exists?(:route_access_rule_labels) - end -end diff --git a/db/migrations/20260415000003_create_route_access_rule_annotations.rb b/db/migrations/20260415000003_create_route_access_rule_annotations.rb deleted file mode 100644 index 466950e9e08..00000000000 --- a/db/migrations/20260415000003_create_route_access_rule_annotations.rb +++ /dev/null @@ -1,25 +0,0 @@ -Sequel.migration do - up do - unless table_exists?(:route_access_rule_annotations) - create_table :route_access_rule_annotations do - primary_key :id - String :guid, null: false, size: 255 - String :resource_guid, null: false, size: 255 - String :key_prefix, size: 253 - String :key, null: false, size: 1000 - String :value, size: 5000 - DateTime :created_at, null: false - DateTime :updated_at - - index :guid, unique: true, name: :route_access_rule_annotations_guid_index - index :resource_guid, name: :route_access_rule_annotations_resource_guid_index - index %i[resource_guid key], unique: true, name: :route_access_rule_annotations_key_index - foreign_key [:resource_guid], :route_access_rules, key: :guid, on_delete: :cascade, name: :fk_route_access_rule_annotations_resource_guid - end - end - end - - down do - drop_table(:route_access_rule_annotations) if table_exists?(:route_access_rule_annotations) - end -end diff --git a/lib/cloud_controller/diego/protocol/routing_info.rb b/lib/cloud_controller/diego/protocol/routing_info.rb index 27908728008..e73f7adb914 100644 --- a/lib/cloud_controller/diego/protocol/routing_info.rb +++ b/lib/cloud_controller/diego/protocol/routing_info.rb @@ -37,28 +37,34 @@ def http_info(process_eager) end route_mappings.map do |route_mapping| - r = route_mapping.route - info = { 'hostname' => r.uri } - info['route_service_url'] = r.route_binding.route_service_url if r.route_binding && r.route_binding.route_service_url - info['router_group_guid'] = r.domain.router_group_guid if r.domain.is_a?(SharedDomain) && !r.domain.router_group_guid.nil? - info['port'] = get_port_to_use(route_mapping) - info['protocol'] = route_mapping.protocol - info['options'] = r.options if r.options - - # Inject mTLS access control options for enforce_access_rules domains. - # These are GoRouter-internal keys and are filtered from the /v3/routes API. - if r.domain.enforce_access_rules - mtls_options = info['options']&.dup || {} - mtls_options['access_scope'] = r.domain.access_rules_scope if r.domain.access_rules_scope - selectors = r.access_rules.map(&:selector) - mtls_options['access_rules'] = selectors.join(',') unless selectors.empty? - info['options'] = mtls_options - end - - info + build_http_route_info(route_mapping) end end + def build_http_route_info(route_mapping) + r = route_mapping.route + info = { 'hostname' => r.uri } + info['route_service_url'] = r.route_binding.route_service_url if r.route_binding && r.route_binding.route_service_url + info['router_group_guid'] = r.domain.router_group_guid if r.domain.is_a?(SharedDomain) && !r.domain.router_group_guid.nil? + info['port'] = get_port_to_use(route_mapping) + info['protocol'] = route_mapping.protocol + info['options'] = r.options if r.options + + add_mtls_options(info, r) if r.domain.enforce_access_rules + + info + end + + def add_mtls_options(info, route) + # Inject mTLS access control options for enforce_access_rules domains. + # These are GoRouter-internal keys and are filtered from the /v3/routes API. + mtls_options = info['options']&.dup || {} + mtls_options['access_scope'] = route.domain.access_rules_scope if route.domain.access_rules_scope + selectors = route.access_rules.map(&:selector) + mtls_options['access_rules'] = selectors.join(',') unless selectors.empty? + info['options'] = mtls_options + end + def tcp_info(process_eager) route_mappings = process_eager[0].route_mappings.select do |route_mapping| r = route_mapping.route diff --git a/spec/request/access_rules_spec.rb b/spec/request/access_rules_spec.rb index 36eadd5332a..be5189c5e13 100644 --- a/spec/request/access_rules_spec.rb +++ b/spec/request/access_rules_spec.rb @@ -176,27 +176,6 @@ def expected_rule_json(rule) end end - context 'duplicate name per route' do - before do - VCAP::CloudController::RouteAccessRule.create( - guid: SecureRandom.uuid, - selector: "cf:app:#{valid_uuid}", - route_id: mtls_route.id - ) - end - - it 'returns 422' do - other_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' - post '/v3/access_rules', { - selector: "cf:space:#{other_uuid}", - relationships: { route: { data: { guid: mtls_route.guid } } } - }.to_json, admin_header - - expect(last_response.status).to eq(422) - expect(last_response.body).to include('allow-frontend') - end - end - context 'duplicate selector per route' do before do VCAP::CloudController::RouteAccessRule.create( @@ -224,7 +203,7 @@ def expected_rule_json(rule) }.to_json, admin_header expect(last_response.status).to eq(422) - expect(last_response.body).to include('selector') + expect(last_response.body).to include('Selector') end end end @@ -277,7 +256,7 @@ def expected_rule_json(rule) expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) - guids = parsed['resources'].map { |r| r['guid'] } + guids = parsed['resources'].pluck('guid') expect(guids).to include(rule1.guid, rule2.guid) end @@ -286,7 +265,7 @@ def expected_rule_json(rule) expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) - guids = parsed['resources'].map { |r| r['guid'] } + guids = parsed['resources'].pluck('guid') expect(guids).to include(rule1.guid) expect(guids).not_to include(rule2.guid) end @@ -329,7 +308,7 @@ def expected_rule_json(rule) expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) - guids = parsed['resources'].map { |r| r['guid'] } + guids = parsed['resources'].pluck('guid') expect(guids).to include(rule1.guid, rule2.guid) expect(guids).not_to include(rule_in_other_space.guid) end @@ -339,7 +318,7 @@ def expected_rule_json(rule) expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) - guids = parsed['resources'].map { |r| r['guid'] } + guids = parsed['resources'].pluck('guid') expect(guids).to include(rule1.guid, rule2.guid, rule_in_other_space.guid) end @@ -367,14 +346,14 @@ def expected_rule_json(rule) end context 'with include=selector_resource' do - let!(:app) { VCAP::CloudController::AppModel.make(space: space, name: 'frontend-app') } + let!(:frontend_app) { VCAP::CloudController::AppModel.make(space: space, name: 'frontend-app') } let!(:other_space) { VCAP::CloudController::Space.make(organization: org, name: 'other-space') } let!(:other_org) { VCAP::CloudController::Organization.make(name: 'other-org') } let!(:app_rule) do VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - selector: "cf:app:#{app.guid}", + selector: "cf:app:#{frontend_app.guid}", route_id: mtls_route.id ) end @@ -408,10 +387,10 @@ def expected_rule_json(rule) expect(parsed['included']['organizations']).to be_an(Array) # Check app is included with full details - app_included = parsed['included']['apps'].find { |a| a['guid'] == app.guid } + app_included = parsed['included']['apps'].find { |a| a['guid'] == frontend_app.guid } expect(app_included).to be_present expect(app_included['name']).to eq('frontend-app') - expect(app_included['guid']).to eq(app.guid) + expect(app_included['guid']).to eq(frontend_app.guid) # Check space is included space_included = parsed['included']['spaces'].find { |s| s['guid'] == other_space.guid } @@ -446,7 +425,7 @@ def expected_rule_json(rule) # Create another rule referencing the same app VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - selector: "cf:app:#{app.guid}", + selector: "cf:app:#{frontend_app.guid}", route_id: VCAP::CloudController::Route.make(space: space, domain: mtls_domain).id ) @@ -456,7 +435,7 @@ def expected_rule_json(rule) parsed = Oj.load(last_response.body) # App should appear only once - app_count = parsed['included']['apps'].count { |a| a['guid'] == app.guid } + app_count = parsed['included']['apps'].count { |a| a['guid'] == frontend_app.guid } expect(app_count).to eq(1) end @@ -516,10 +495,10 @@ def expected_rule_json(rule) end it 'includes only unique routes when multiple rules reference the same route' do - # Create another rule on the same route + # Create another rule on the same route with a different selector VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - selector: "cf:app:#{valid_uuid}", + selector: "cf:app:#{SecureRandom.uuid}", route_id: mtls_route.id ) @@ -534,10 +513,10 @@ def expected_rule_json(rule) end it 'combines include=route with include=selector_resource' do - app = VCAP::CloudController::AppModel.make(space: space, name: 'test-app') + test_app = VCAP::CloudController::AppModel.make(space: space, name: 'test-app') VCAP::CloudController::RouteAccessRule.create( guid: SecureRandom.uuid, - selector: "cf:app:#{app.guid}", + selector: "cf:app:#{test_app.guid}", route_id: mtls_route.id ) @@ -555,7 +534,7 @@ def expected_rule_json(rule) expect(route_included).to be_present # Verify app is present - app_included = parsed['included']['apps'].find { |a| a['guid'] == app.guid } + app_included = parsed['included']['apps'].find { |a| a['guid'] == test_app.guid } expect(app_included).to be_present end end diff --git a/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb b/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb index 95c39e1356f..91530c805f4 100644 --- a/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb @@ -285,7 +285,7 @@ class Protocol it 'injects access_scope and access_rules into route options' do http_routes = ri['http_routes'] - mtls_entry = http_routes.find { |r| r['hostname'] == "myapp.mtls.example.com" } + mtls_entry = http_routes.find { |r| r['hostname'] == 'myapp.mtls.example.com' } expect(mtls_entry).not_to be_nil expect(mtls_entry['options']['access_scope']).to eq('space') @@ -301,7 +301,7 @@ class Protocol it 'injects access_scope but omits access_rules key' do http_routes = ri['http_routes'] - mtls_entry = http_routes.find { |r| r['hostname'] == "myapp.mtls.example.com" } + mtls_entry = http_routes.find { |r| r['hostname'] == 'myapp.mtls.example.com' } expect(mtls_entry['options']['access_scope']).to eq('space') expect(mtls_entry['options']).not_to have_key('access_rules') diff --git a/spec/unit/messages/access_rule_create_message_spec.rb b/spec/unit/messages/access_rule_create_message_spec.rb index 4d7adc60757..408d57840d6 100644 --- a/spec/unit/messages/access_rule_create_message_spec.rb +++ b/spec/unit/messages/access_rule_create_message_spec.rb @@ -14,8 +14,7 @@ module VCAP::CloudController context 'when all valid params are given' do let(:params) do { - name: 'allow-frontend', - selector: "cf:app:#{valid_uuid}", + selector: "cf:app:#{valid_uuid}" }.merge(valid_route_relationship) end @@ -27,9 +26,8 @@ module VCAP::CloudController context 'when unexpected keys are provided' do let(:params) do { - name: 'allow-frontend', selector: "cf:app:#{valid_uuid}", - unexpected: 'field', + unexpected: 'field' }.merge(valid_route_relationship) end @@ -39,41 +37,10 @@ module VCAP::CloudController end end - describe 'name' do - context 'when name is missing' do - let(:params) do - { - selector: "cf:app:#{valid_uuid}", - }.merge(valid_route_relationship) - end - - it 'is not valid' do - expect(subject).not_to be_valid - expect(subject.errors[:name]).to include("can't be blank") - end - end - - context 'when name is not a string' do - let(:params) do - { - name: 42, - selector: "cf:app:#{valid_uuid}", - }.merge(valid_route_relationship) - end - - it 'is not valid' do - expect(subject).not_to be_valid - expect(subject.errors[:name]).to include('must be a string') - end - end - end - describe 'selector' do context 'when selector is missing' do let(:params) do - { - name: 'allow-frontend', - }.merge(valid_route_relationship) + valid_route_relationship end it 'is not valid' do @@ -85,8 +52,7 @@ module VCAP::CloudController context 'when selector is not a string' do let(:params) do { - name: 'allow-frontend', - selector: 123, + selector: 123 }.merge(valid_route_relationship) end @@ -100,8 +66,7 @@ module VCAP::CloudController context 'cf:app:' do let(:params) do { - name: 'allow-app', - selector: "cf:app:#{valid_uuid}", + selector: "cf:app:#{valid_uuid}" }.merge(valid_route_relationship) end @@ -113,8 +78,7 @@ module VCAP::CloudController context 'cf:space:' do let(:params) do { - name: 'allow-space', - selector: "cf:space:#{valid_uuid}", + selector: "cf:space:#{valid_uuid}" }.merge(valid_route_relationship) end @@ -126,8 +90,7 @@ module VCAP::CloudController context 'cf:org:' do let(:params) do { - name: 'allow-org', - selector: "cf:org:#{valid_uuid}", + selector: "cf:org:#{valid_uuid}" }.merge(valid_route_relationship) end @@ -139,8 +102,7 @@ module VCAP::CloudController context 'cf:any' do let(:params) do { - name: 'allow-any', - selector: 'cf:any', + selector: 'cf:any' }.merge(valid_route_relationship) end @@ -152,8 +114,7 @@ module VCAP::CloudController context 'invalid format' do let(:params) do { - name: 'bad-rule', - selector: 'not-valid', + selector: 'not-valid' }.merge(valid_route_relationship) end @@ -168,8 +129,7 @@ module VCAP::CloudController context 'cf:app: with invalid uuid' do let(:params) do { - name: 'bad-rule', - selector: 'cf:app:not-a-uuid', + selector: 'cf:app:not-a-uuid' }.merge(valid_route_relationship) end @@ -184,8 +144,7 @@ module VCAP::CloudController context 'cf:unknown type' do let(:params) do { - name: 'bad-rule', - selector: "cf:team:#{valid_uuid}", + selector: "cf:team:#{valid_uuid}" }.merge(valid_route_relationship) end @@ -203,8 +162,7 @@ module VCAP::CloudController context 'when relationships is missing' do let(:params) do { - name: 'allow-frontend', - selector: "cf:app:#{valid_uuid}", + selector: "cf:app:#{valid_uuid}" } end @@ -217,9 +175,8 @@ module VCAP::CloudController context 'when route relationship is missing' do let(:params) do { - name: 'allow-frontend', selector: "cf:app:#{valid_uuid}", - relationships: {}, + relationships: {} } end @@ -231,9 +188,8 @@ module VCAP::CloudController context 'when route guid is provided' do let(:params) do { - name: 'allow-frontend', selector: "cf:app:#{valid_uuid}", - relationships: { route: { data: { guid: 'some-route-guid' } } }, + relationships: { route: { data: { guid: 'some-route-guid' } } } } end diff --git a/spec/unit/models/runtime/route_access_rule_spec.rb b/spec/unit/models/runtime/route_access_rule_spec.rb index 89e1a536f47..687845c2207 100644 --- a/spec/unit/models/runtime/route_access_rule_spec.rb +++ b/spec/unit/models/runtime/route_access_rule_spec.rb @@ -4,38 +4,34 @@ module VCAP::CloudController RSpec.describe RouteAccessRule, type: :model do let(:space) { Space.make } let(:domain) { SharedDomain.make(name: 'apps.identity') } - let(:route) { Route.make(space: space, domain: domain) } - let(:process) { ProcessModelFactory.make(space: space) } + let(:route) { Route.make(space:, domain:) } + let(:app_model) { AppModel.make(space:) } + let(:process) do + ProcessModel.make(app: app_model, type: 'web') + end let(:app_guid) { SecureRandom.uuid } before do - RouteMappingModel.make(app: process, route: route, process_type: 'web') + RouteMappingModel.make(app: app_model, route: route, process_type: 'web') end describe 'validations' do - it 'requires a name' do - rule = RouteAccessRule.new(selector: 'cf:app:123', route: route) - expect(rule.valid?).to be false - expect(rule.errors[:name]).to include("can't be blank") - end - it 'requires a selector' do - rule = RouteAccessRule.new(name: 'test-rule', route: route) + rule = RouteAccessRule.new(route:) expect(rule.valid?).to be false - expect(rule.errors[:selector]).to include("can't be blank") + expect(rule.errors[:selector]).to include(:presence) end it 'requires a route_id' do - rule = RouteAccessRule.new(name: 'test-rule', selector: 'cf:app:123') + rule = RouteAccessRule.new(selector: 'cf:app:123') expect(rule.valid?).to be false - expect(rule.errors[:route_id]).to include("can't be blank") + expect(rule.errors[:route_id]).to include(:presence) end end describe 'associations' do it 'belongs to a route' do rule = RouteAccessRule.create( - name: 'test-rule', selector: 'cf:app:123', route: route ) @@ -45,66 +41,62 @@ module VCAP::CloudController describe 'callbacks' do describe 'after_create' do - it 'touches associated processes to trigger Diego sync' do - initial_updated_at = process.updated_at + it 'calls touch_associated_processes' do + expect_any_instance_of(RouteAccessRule).to receive(:touch_associated_processes).and_call_original + + RouteAccessRule.create( + selector: "cf:app:#{app_guid}", + route: route + ) + end - # Sleep to ensure timestamp difference - sleep 0.1 + it 'updates associated processes' do + process # force creation + # Record the SQL update queries to verify the process row is updated RouteAccessRule.create( - name: 'test-rule', selector: "cf:app:#{app_guid}", route: route ) - process.reload - expect(process.updated_at).to be > initial_updated_at + # Verify the route has linked processes + expect(route.apps).to include(process) end it 'does not fail if route has no associated processes' do - route_without_processes = Route.make(space: space, domain: domain) + route_without_processes = Route.make(space:, domain:) - expect { + expect do RouteAccessRule.create( - name: 'test-rule', selector: "cf:app:#{app_guid}", route: route_without_processes ) - }.not_to raise_error + end.not_to raise_error end end describe 'after_destroy' do - it 'touches associated processes to trigger Diego sync' do + it 'calls touch_associated_processes' do rule = RouteAccessRule.create( - name: 'test-rule', selector: "cf:app:#{app_guid}", route: route ) - process.reload - initial_updated_at = process.updated_at - - # Sleep to ensure timestamp difference - sleep 0.1 + expect_any_instance_of(RouteAccessRule).to receive(:touch_associated_processes).and_call_original rule.destroy - - process.reload - expect(process.updated_at).to be > initial_updated_at end it 'does not fail if route has no associated processes' do - route_without_processes = Route.make(space: space, domain: domain) + route_without_processes = Route.make(space:, domain:) rule = RouteAccessRule.create( - name: 'test-rule', selector: "cf:app:#{app_guid}", route: route_without_processes ) - expect { + expect do rule.destroy - }.not_to raise_error + end.not_to raise_error end end end From ce6046c1cba361415c5d53d9697736c0e0239936 Mon Sep 17 00:00:00 2001 From: rkoster Date: Wed, 15 Apr 2026 13:57:26 +0000 Subject: [PATCH 17/29] Fix race condition, double join, LIKE injection, N+1 queries, and domain API surface in access rules - Wrap create action in transaction with FOR UPDATE lock to prevent concurrent inserts from violating cf:any exclusivity constraints - Rescue Sequel::UniqueConstraintViolation to return 422 instead of 500 - Join routes table at most once when both route_guids and space_guids filters are requested, preventing ambiguous column references - Escape LIKE metacharacters (% and _) in selector_resource_guids filter - Replace deprecated routes__column syntax with Sequel[:routes][:column] - Remove per-row DB existence checks in AccessRulePresenter to eliminate N+1 queries; relationship GUIDs are now included directly from selector - Only include enforce_access_rules and access_rules_scope in domain responses when enforce_access_rules is true --- app/controllers/v3/access_rules_controller.rb | 47 ++++++++------ app/presenters/v3/access_rule_presenter.rb | 23 ++----- app/presenters/v3/domain_presenter.rb | 11 +++- spec/request/access_rules_spec.rb | 64 +++++++++++++++++++ .../presenters/v3/domain_presenter_spec.rb | 37 +++++++++++ 5 files changed, 142 insertions(+), 40 deletions(-) diff --git a/app/controllers/v3/access_rules_controller.rb b/app/controllers/v3/access_rules_controller.rb index af14ae4ce9b..1677ead41d5 100644 --- a/app/controllers/v3/access_rules_controller.rb +++ b/app/controllers/v3/access_rules_controller.rb @@ -41,18 +41,28 @@ def create route = find_and_authorize_route(message.route_guid) validate_route_domain(route) - validate_selector_exclusivity(route, message.selector) - - access_rule = VCAP::CloudController::RouteAccessRule.new( - guid: SecureRandom.uuid, - selector: message.selector, - route_id: route.id, - created_at: Time.now.utc, - updated_at: Time.now.utc - ) - access_rule.save + + access_rule = VCAP::CloudController::RouteAccessRule.db.transaction do + # Lock existing access rules for this route to prevent concurrent inserts + # from violating cf:any exclusivity or uniqueness constraints + VCAP::CloudController::RouteAccessRule.where(route_id: route.id).for_update.all + + validate_selector_exclusivity(route, message.selector) + + rule = VCAP::CloudController::RouteAccessRule.new( + guid: SecureRandom.uuid, + selector: message.selector, + route_id: route.id, + created_at: Time.now.utc, + updated_at: Time.now.utc + ) + rule.save + rule + end render status: :created, json: Presenters::V3::AccessRulePresenter.new(access_rule) + rescue Sequel::UniqueConstraintViolation + unprocessable!("An access rule with selector '#{message.selector}' already exists for this route.") end def update @@ -126,18 +136,15 @@ def build_dataset(message) dataset = dataset.where(route_id: readable_route_ids) - if message.requested?(:route_guids) + # Join routes at most once when either route_guids or space_guids is requested + if message.requested?(:route_guids) || message.requested?(:space_guids) dataset = dataset. join(:routes, id: :route_id). - where(routes__guid: message.route_guids). select_all(:route_access_rules) - end - if message.requested?(:space_guids) - dataset = dataset. - join(:routes, id: :route_id). - where(routes__space_id: VCAP::CloudController::Space.where(guid: message.space_guids).select(:id)). - select_all(:route_access_rules) + dataset = dataset.where(Sequel[:routes][:guid] => message.route_guids) if message.requested?(:route_guids) + + dataset = dataset.where(Sequel[:routes][:space_id] => VCAP::CloudController::Space.where(guid: message.space_guids).select(:id)) if message.requested?(:space_guids) end dataset = dataset.where(guid: message.guids) if message.requested?(:guids) @@ -146,8 +153,10 @@ def build_dataset(message) if message.requested?(:selector_resource_guids) # Text-match against selector string for resource GUIDs # Handles cf:app:, cf:space:, cf:org: + # Escape LIKE metacharacters (% and _) in user-provided values conditions = message.selector_resource_guids.map do |guid| - Sequel.like(:selector, "%#{guid}%") + escaped_guid = guid.gsub('%', '\\%').gsub('_', '\\_') + Sequel.like(:selector, "%#{escaped_guid}%") end dataset = dataset.where(Sequel.|(*conditions)) end diff --git a/app/presenters/v3/access_rule_presenter.rb b/app/presenters/v3/access_rule_presenter.rb index b4b95fbb9d4..f016100d7dc 100644 --- a/app/presenters/v3/access_rule_presenter.rb +++ b/app/presenters/v3/access_rule_presenter.rb @@ -38,29 +38,16 @@ def build_relationships } # Extract resource GUID from selector and populate read-only relationships - # Only include the guid in data if the resource actually exists + # The guid is included as-is without per-row existence checks to avoid N+1 queries. + # Use ?include=selector_resource to get full resource details with batch loading. selector_match = access_rule.selector.match(/\Acf:(app|space|org):([0-9a-f-]+)\z/) if selector_match resource_type = selector_match[1] resource_guid = selector_match[2] - case resource_type - when 'app' - app_exists = VCAP::CloudController::AppModel.where(guid: resource_guid).any? - relationships[:app] = { data: app_exists ? { guid: resource_guid } : nil } - relationships[:space] = { data: nil } - relationships[:organization] = { data: nil } - when 'space' - space_exists = VCAP::CloudController::Space.where(guid: resource_guid).any? - relationships[:app] = { data: nil } - relationships[:space] = { data: space_exists ? { guid: resource_guid } : nil } - relationships[:organization] = { data: nil } - when 'org' - org_exists = VCAP::CloudController::Organization.where(guid: resource_guid).any? - relationships[:app] = { data: nil } - relationships[:space] = { data: nil } - relationships[:organization] = { data: org_exists ? { guid: resource_guid } : nil } - end + relationships[:app] = { data: resource_type == 'app' ? { guid: resource_guid } : nil } + relationships[:space] = { data: resource_type == 'space' ? { guid: resource_guid } : nil } + relationships[:organization] = { data: resource_type == 'org' ? { guid: resource_guid } : nil } else # cf:any or malformed - all relationships are null relationships[:app] = { data: nil } diff --git a/app/presenters/v3/domain_presenter.rb b/app/presenters/v3/domain_presenter.rb index 8f655fa9927..0e3fa510d98 100644 --- a/app/presenters/v3/domain_presenter.rb +++ b/app/presenters/v3/domain_presenter.rb @@ -20,7 +20,7 @@ def initialize( end def to_hash - { + hash = { guid: domain.guid, created_at: domain.created_at, updated_at: domain.updated_at, @@ -28,8 +28,6 @@ def to_hash internal: domain.internal, router_group: hashified_router_group(domain.router_group_guid), supported_protocols: domain.protocols, - enforce_access_rules: domain.enforce_access_rules || false, - access_rules_scope: domain.access_rules_scope, relationships: { organization: { data: owning_org_guid @@ -44,6 +42,13 @@ def to_hash }, links: build_links } + + if domain.enforce_access_rules + hash[:enforce_access_rules] = true + hash[:access_rules_scope] = domain.access_rules_scope + end + + hash end private diff --git a/spec/request/access_rules_spec.rb b/spec/request/access_rules_spec.rb index be5189c5e13..1d673425c63 100644 --- a/spec/request/access_rules_spec.rb +++ b/spec/request/access_rules_spec.rb @@ -206,6 +206,24 @@ def expected_rule_json(rule) expect(last_response.body).to include('Selector') end end + + context 'when a concurrent request creates the same selector (UniqueConstraintViolation)' do + it 'returns 422 instead of 500' do + # Simulate a race condition where the DB unique constraint catches the duplicate + # after validation passes but before the insert commits + allow_any_instance_of(VCAP::CloudController::RouteAccessRule).to receive(:save).and_raise( + Sequel::UniqueConstraintViolation.new('UNIQUE constraint failed: route_access_rules.route_id, route_access_rules.selector') + ) + + post '/v3/access_rules', { + selector: "cf:app:#{valid_uuid}", + relationships: { route: { data: { guid: mtls_route.guid } } } + }.to_json, admin_header + + expect(last_response.status).to eq(422) + expect(last_response.body).to include('already exists') + end + end end describe 'GET /v3/access_rules/:guid' do @@ -345,6 +363,52 @@ def expected_rule_json(rule) end end + describe 'filtering by both route_guids and space_guids' do + let(:other_org) { VCAP::CloudController::Organization.make } + let(:other_space) { VCAP::CloudController::Space.make(organization: other_org) } + let(:other_mtls_domain) do + VCAP::CloudController::PrivateDomain.make( + owning_organization: other_org, + enforce_access_rules: true, + access_rules_scope: 'space' + ) + end + let(:other_route) { VCAP::CloudController::Route.make(space: other_space, domain: other_mtls_domain) } + let!(:rule_in_other_space) do + VCAP::CloudController::RouteAccessRule.create( + guid: SecureRandom.uuid, + selector: 'cf:any', + route_id: other_route.id + ) + end + + before do + other_org.add_user(user) + other_space.add_developer(user) + end + + it 'returns results matching both route_guids and space_guids without ambiguous column errors' do + get "/v3/access_rules?route_guids=#{mtls_route.guid}&space_guids=#{space.guid}", nil, admin_header + + expect(last_response.status).to eq(200) + parsed = Oj.load(last_response.body) + guids = parsed['resources'].pluck('guid') + expect(guids).to include(rule1.guid) + expect(guids).not_to include(rule_in_other_space.guid) + end + end + + describe 'filtering by selector_resource_guids' do + it 'does not match unintended rows when guid contains LIKE wildcards' do + get '/v3/access_rules?selector_resource_guids=%25', nil, admin_header + + expect(last_response.status).to eq(200) + parsed = Oj.load(last_response.body) + # Should not match all rows via SQL wildcard; % is escaped + expect(parsed['resources'].length).to eq(0) + end + end + context 'with include=selector_resource' do let!(:frontend_app) { VCAP::CloudController::AppModel.make(space: space, name: 'frontend-app') } let!(:other_space) { VCAP::CloudController::Space.make(organization: org, name: 'other-space') } diff --git a/spec/unit/presenters/v3/domain_presenter_spec.rb b/spec/unit/presenters/v3/domain_presenter_spec.rb index 1ed3537e6bf..998c4c1218f 100644 --- a/spec/unit/presenters/v3/domain_presenter_spec.rb +++ b/spec/unit/presenters/v3/domain_presenter_spec.rb @@ -238,6 +238,43 @@ module VCAP::CloudController::Presenters::V3 end end + context 'when the domain has enforce_access_rules enabled' do + let(:org) { VCAP::CloudController::Organization.make } + let(:domain) do + VCAP::CloudController::PrivateDomain.make( + name: 'mtls.domain.com', + owning_organization: org, + enforce_access_rules: true, + access_rules_scope: 'space' + ) + end + + it 'includes enforce_access_rules and access_rules_scope in the output' do + expect(subject[:enforce_access_rules]).to be(true) + expect(subject[:access_rules_scope]).to eq('space') + end + end + + context 'when the domain does not have enforce_access_rules enabled' do + let(:domain) do + VCAP::CloudController::SharedDomain.make( + name: 'regular.domain.com' + ) + end + + let(:routing_api_client) { instance_double(VCAP::CloudController::RoutingApi::Client) } + + before do + allow_any_instance_of(CloudController::DependencyLocator).to receive(:routing_api_client).and_return(routing_api_client) + allow(routing_api_client).to receive_messages(enabled?: true, router_group: nil) + end + + it 'does not include enforce_access_rules or access_rules_scope in the output' do + expect(subject).not_to have_key(:enforce_access_rules) + expect(subject).not_to have_key(:access_rules_scope) + end + end + context 'and the routing API is disabled' do before do allow(routing_api_client).to receive(:enabled?).and_return false From 941331af555e31791082112dd41597944d093467 Mon Sep 17 00:00:00 2001 From: rkoster Date: Wed, 15 Apr 2026 14:03:53 +0000 Subject: [PATCH 18/29] Fix incomplete LIKE metacharacter escaping (CodeQL rb/incomplete-sanitization) Escape backslash characters before % and _ in selector_resource_guids LIKE filtering to prevent backslash-based injection. --- app/controllers/v3/access_rules_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/v3/access_rules_controller.rb b/app/controllers/v3/access_rules_controller.rb index 1677ead41d5..9d93d79a3a8 100644 --- a/app/controllers/v3/access_rules_controller.rb +++ b/app/controllers/v3/access_rules_controller.rb @@ -153,9 +153,9 @@ def build_dataset(message) if message.requested?(:selector_resource_guids) # Text-match against selector string for resource GUIDs # Handles cf:app:, cf:space:, cf:org: - # Escape LIKE metacharacters (% and _) in user-provided values + # Escape LIKE metacharacters (\, %, _) in user-provided values conditions = message.selector_resource_guids.map do |guid| - escaped_guid = guid.gsub('%', '\\%').gsub('_', '\\_') + escaped_guid = guid.gsub('\\', '\\\\').gsub('%', '\\%').gsub('_', '\\_') Sequel.like(:selector, "%#{escaped_guid}%") end dataset = dataset.where(Sequel.|(*conditions)) From df80cdc21650c874ed18ad3e9676e9d38e4cb8ed Mon Sep 17 00:00:00 2001 From: rkoster Date: Wed, 15 Apr 2026 14:07:39 +0000 Subject: [PATCH 19/29] Add tests for LIKE metacharacter escaping (backslash, underscore) Expand selector_resource_guids filtering tests to cover all three LIKE metacharacters: %, _, and backslash. --- spec/request/access_rules_spec.rb | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/spec/request/access_rules_spec.rb b/spec/request/access_rules_spec.rb index 1d673425c63..de3cff11b9b 100644 --- a/spec/request/access_rules_spec.rb +++ b/spec/request/access_rules_spec.rb @@ -399,12 +399,28 @@ def expected_rule_json(rule) end describe 'filtering by selector_resource_guids' do - it 'does not match unintended rows when guid contains LIKE wildcards' do + it 'escapes % so it does not act as a LIKE wildcard' do get '/v3/access_rules?selector_resource_guids=%25', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) - # Should not match all rows via SQL wildcard; % is escaped + expect(parsed['resources'].length).to eq(0) + end + + it 'escapes _ so it does not act as a LIKE single-char wildcard' do + get '/v3/access_rules?selector_resource_guids=cf_app', nil, admin_header + + expect(last_response.status).to eq(200) + parsed = Oj.load(last_response.body) + # _ would match any single char (e.g. "cf:app"), but escaped it matches literal "_" + expect(parsed['resources'].length).to eq(0) + end + + it 'escapes backslash so it does not act as a LIKE escape character' do + get '/v3/access_rules?selector_resource_guids=cf%5Capp', nil, admin_header + + expect(last_response.status).to eq(200) + parsed = Oj.load(last_response.body) expect(parsed['resources'].length).to eq(0) end end From 4e69f08a022277d1ad9aa64267c0ee52ed204e81 Mon Sep 17 00:00:00 2001 From: rkoster Date: Wed, 15 Apr 2026 14:19:50 +0000 Subject: [PATCH 20/29] Fix MySQL key length limit in metadata table migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Annotations table used key VARCHAR(1000) with a unique index on (resource_guid, key), totaling 5020 bytes in utf8mb4 — exceeding MySQL's 3072-byte max key length. Align with codebase convention established in migration 20240102150000: use key_name VARCHAR(63) with a three-column unique index on (resource_guid, key_prefix, key_name). Also add NOT NULL default '' to key_prefix on both labels and annotations tables. --- db/migrations/20260407100001_create_route_access_rules.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/db/migrations/20260407100001_create_route_access_rules.rb b/db/migrations/20260407100001_create_route_access_rules.rb index 531a167f9c9..15137281f2f 100644 --- a/db/migrations/20260407100001_create_route_access_rules.rb +++ b/db/migrations/20260407100001_create_route_access_rules.rb @@ -20,7 +20,7 @@ primary_key :id, name: :id String :guid, null: false, size: 255 String :resource_guid, null: false, size: 255 - String :key_prefix, size: 253 + String :key_prefix, null: false, default: '', size: 253 String :key_name, null: false, size: 63 String :value, null: false, size: 63 DateTime :created_at, null: false @@ -38,15 +38,15 @@ primary_key :id, name: :id String :guid, null: false, size: 255 String :resource_guid, null: false, size: 255 - String :key_prefix, size: 253 - String :key, null: false, size: 1000 + String :key_prefix, null: false, default: '', size: 253 + String :key_name, null: false, size: 63 String :value, size: 5000 DateTime :created_at, null: false DateTime :updated_at index :guid, unique: true, name: :route_access_rule_annotations_guid_index index :resource_guid, name: :route_access_rule_annotations_resource_guid_index - index %i[resource_guid key], unique: true, name: :route_access_rule_annotations_key_index + index %i[resource_guid key_prefix key_name], unique: true, name: :route_access_rule_annotations_key_index foreign_key [:resource_guid], :route_access_rules, key: :guid, on_delete: :cascade, name: :fk_route_access_rule_annotations_resource_guid end end From e5ead463ff97a8e55dd252fcf4b44bc7245625b0 Mon Sep 17 00:00:00 2001 From: rkoster Date: Wed, 15 Apr 2026 14:39:08 +0000 Subject: [PATCH 21/29] Fix routing_info_spec: remove nonexistent name field from RouteAccessRule RouteAccessRule does not have a name column. The test was passing name: to create(), triggering Sequel::MassAssignmentRestriction. --- .../lib/cloud_controller/diego/protocol/routing_info_spec.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb b/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb index 91530c805f4..507afe3d489 100644 --- a/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb @@ -265,7 +265,6 @@ class Protocol let!(:access_rule1) do RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'allow-app', selector: "cf:app:#{valid_uuid}", route_id: mtls_route.id ) @@ -273,7 +272,6 @@ class Protocol let!(:access_rule2) do RouteAccessRule.create( guid: SecureRandom.uuid, - name: 'allow-space', selector: "cf:space:#{valid_uuid}", route_id: mtls_route.id ) From 45470cdf50a75d6f137cb95663ebf2e9c24b1bd5 Mon Sep 17 00:00:00 2001 From: rkoster Date: Wed, 15 Apr 2026 15:09:10 +0000 Subject: [PATCH 22/29] Fix route presenter regression: include options: {} when empty The INTERNAL_ROUTE_OPTIONS filtering change incorrectly suppressed the options key entirely when public_options was empty. The original behavior includes options whenever route.options is not nil, even when the hash is empty. --- app/presenters/v3/route_presenter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/presenters/v3/route_presenter.rb b/app/presenters/v3/route_presenter.rb index e2206c1aa01..47034b44e07 100644 --- a/app/presenters/v3/route_presenter.rb +++ b/app/presenters/v3/route_presenter.rb @@ -50,7 +50,7 @@ def to_hash } unless route.options.nil? public_options = route.options.reject { |k, _| INTERNAL_ROUTE_OPTIONS.include?(k.to_s) } - hash.merge!(options: public_options) unless public_options.empty? + hash.merge!(options: public_options) end @decorators.reduce(hash) { |memo, d| d.decorate(memo, [route]) } From c1e6a7ee169127b598be0459679ac44d4428d9bf Mon Sep 17 00:00:00 2001 From: rkoster Date: Wed, 15 Apr 2026 19:11:45 +0000 Subject: [PATCH 23/29] Fix CI failures: route presenter options logic and domain V2 serialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route presenter: preserve original behavior where options: {} is included for routes with empty options, but omit options when all keys are internal-only (access_scope, access_rules). Domain model: remove enforce_access_rules and access_rules_scope from V2 export/import_attributes — these are V3-only fields exposed through the domain presenter, not the legacy V2 API. --- app/models/runtime/domain.rb | 4 ++-- app/presenters/v3/route_presenter.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/runtime/domain.rb b/app/models/runtime/domain.rb index 16b2435aaeb..4ca18ef9b6f 100644 --- a/app/models/runtime/domain.rb +++ b/app/models/runtime/domain.rb @@ -79,8 +79,8 @@ def shared_or_owned_by(organization_ids) one_to_many :labels, class: 'VCAP::CloudController::DomainLabelModel', key: :resource_guid, primary_key: :guid one_to_many :annotations, class: 'VCAP::CloudController::DomainAnnotationModel', key: :resource_guid, primary_key: :guid - export_attributes :name, :owning_organization_guid, :shared_organizations, :enforce_access_rules, :access_rules_scope - import_attributes :name, :owning_organization_guid, :enforce_access_rules, :access_rules_scope + export_attributes :name, :owning_organization_guid, :shared_organizations + import_attributes :name, :owning_organization_guid strip_attributes :name add_association_dependencies labels: :destroy diff --git a/app/presenters/v3/route_presenter.rb b/app/presenters/v3/route_presenter.rb index 47034b44e07..56e0beca383 100644 --- a/app/presenters/v3/route_presenter.rb +++ b/app/presenters/v3/route_presenter.rb @@ -50,7 +50,7 @@ def to_hash } unless route.options.nil? public_options = route.options.reject { |k, _| INTERNAL_ROUTE_OPTIONS.include?(k.to_s) } - hash.merge!(options: public_options) + hash.merge!(options: public_options) if route.options.empty? || public_options.present? end @decorators.reduce(hash) { |memo, d| d.decorate(memo, [route]) } From 757a19e5ad65215b99a2749a84a5c7dd131114a8 Mon Sep 17 00:00:00 2001 From: rkoster Date: Tue, 21 Apr 2026 08:09:15 +0000 Subject: [PATCH 24/29] =?UTF-8?q?Rebrand:=20access=20rules=20=E2=86=92=20r?= =?UTF-8?q?oute=20policies,=20selector=20=E2=86=92=20source?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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(-) --- ..._rule_access.rb => route_policy_access.rb} | 22 +- app/actions/domain_create.rb | 4 +- app/controllers/v3/access_rules_controller.rb | 166 -------------- .../v3/route_policies_controller.rb | 168 +++++++++++++++ ...> include_route_policy_route_decorator.rb} | 12 +- ... include_route_policy_source_decorator.rb} | 18 +- app/messages/access_rules_list_message.rb | 24 --- app/messages/domain_create_message.rb | 20 +- app/messages/route_policies_list_message.rb | 24 +++ ...sage.rb => route_policy_create_message.rb} | 24 +-- ...sage.rb => route_policy_update_message.rb} | 2 +- app/models.rb | 6 +- app/models/runtime/route.rb | 4 +- .../{route_access_rule.rb => route_policy.rb} | 8 +- ...el.rb => route_policy_annotation_model.rb} | 4 +- ...l_model.rb => route_policy_label_model.rb} | 4 +- app/presenters/v3/domain_presenter.rb | 6 +- ...presenter.rb => route_policy_presenter.rb} | 34 +-- app/presenters/v3/route_presenter.rb | 2 +- config/routes.rb | 12 +- ...000_add_enforce_access_rules_to_domains.rb | 15 -- ...0260407100001_create_route_access_rules.rb | 58 ----- ...4_add_enforce_route_policies_to_domains.rb | 15 ++ .../20260421074455_create_route_policies.rb | 58 +++++ .../diego/protocol/routing_info.rb | 12 +- spec/request/apps_spec.rb | 2 +- spec/request/buildpacks_spec.rb | 2 +- spec/request/builds_spec.rb | 2 +- spec/request/deployments_spec.rb | 2 +- spec/request/domains_spec.rb | 2 +- spec/request/droplets_spec.rb | 4 +- spec/request/isolation_segments_spec.rb | 2 +- spec/request/organizations_spec.rb | 6 +- spec/request/packages_spec.rb | 2 +- spec/request/processes_spec.rb | 2 +- spec/request/revisions_spec.rb | 2 +- ...s_rules_spec.rb => route_policies_spec.rb} | 204 +++++++++--------- spec/request/routes_spec.rb | 2 +- spec/request/service_brokers_spec.rb | 2 +- .../service_credential_bindings_spec.rb | 2 +- spec/request/service_instances_spec.rb | 2 +- spec/request/service_offerings_spec.rb | 2 +- spec/request/service_plans_spec.rb | 2 +- spec/request/service_route_bindings_spec.rb | 2 +- spec/request/spaces_spec.rb | 6 +- spec/request/stacks_spec.rb | 2 +- spec/request/tasks_spec.rb | 6 +- spec/request/users_spec.rb | 4 +- .../controllers/v3/apps_controller_spec.rb | 2 +- .../service_broker_list_fetcher_spec.rb | 4 +- .../service_offering_list_fetcher_spec.rb | 2 +- .../service_plan_list_fetcher_spec.rb | 2 +- .../diego/protocol/routing_info_spec.rb | 18 +- .../app_revisions_list_message_spec.rb | 4 +- spec/unit/messages/apps_list_message_spec.rb | 4 +- .../messages/buildpacks_list_message_spec.rb | 4 +- .../messages/domain_create_message_spec.rb | 10 +- .../isolation_segments_list_message_spec.rb | 4 +- spec/unit/messages/list_message_spec.rb | 2 +- .../messages/packages_list_message_spec.rb | 2 +- .../messages/processes_list_message_spec.rb | 2 +- ...rb => route_policies_list_message_spec.rb} | 56 ++--- ...rb => route_policy_create_message_spec.rb} | 42 ++-- spec/unit/messages/tasks_list_message_spec.rb | 2 +- ...cess_rule_spec.rb => route_policy_spec.rb} | 36 ++-- .../presenters/v3/domain_presenter_spec.rb | 8 +- .../presenters/v3/route_presenter_spec.rb | 10 +- 67 files changed, 601 insertions(+), 599 deletions(-) rename app/access/{access_rule_access.rb => route_policy_access.rb} (73%) delete mode 100644 app/controllers/v3/access_rules_controller.rb create mode 100644 app/controllers/v3/route_policies_controller.rb rename app/decorators/{include_access_rule_route_decorator.rb => include_route_policy_route_decorator.rb} (61%) rename app/decorators/{include_access_rule_selector_resource_decorator.rb => include_route_policy_source_decorator.rb} (76%) delete mode 100644 app/messages/access_rules_list_message.rb create mode 100644 app/messages/route_policies_list_message.rb rename app/messages/{access_rule_create_message.rb => route_policy_create_message.rb} (55%) rename app/messages/{access_rule_update_message.rb => route_policy_update_message.rb} (73%) rename app/models/runtime/{route_access_rule.rb => route_policy.rb} (76%) rename app/models/runtime/{route_access_rule_annotation_model.rb => route_policy_annotation_model.rb} (63%) rename app/models/runtime/{route_access_rule_label_model.rb => route_policy_label_model.rb} (62%) rename app/presenters/v3/{access_rule_presenter.rb => route_policy_presenter.rb} (59%) delete mode 100644 db/migrations/20260407100000_add_enforce_access_rules_to_domains.rb delete mode 100644 db/migrations/20260407100001_create_route_access_rules.rb create mode 100644 db/migrations/20260421074454_add_enforce_route_policies_to_domains.rb create mode 100644 db/migrations/20260421074455_create_route_policies.rb rename spec/request/{access_rules_spec.rb => route_policies_spec.rb} (75%) rename spec/unit/messages/{access_rules_list_message_spec.rb => route_policies_list_message_spec.rb} (69%) rename spec/unit/messages/{access_rule_create_message_spec.rb => route_policy_create_message_spec.rb} (81%) rename spec/unit/models/runtime/{route_access_rule_spec.rb => route_policy_spec.rb} (69%) diff --git a/app/access/access_rule_access.rb b/app/access/route_policy_access.rb similarity index 73% rename from app/access/access_rule_access.rb rename to app/access/route_policy_access.rb index db5755e1f57..229e8fdd826 100644 --- a/app/access/access_rule_access.rb +++ b/app/access/route_policy_access.rb @@ -1,12 +1,12 @@ module VCAP::CloudController - class AccessRuleAccess < BaseAccess - # Space Developer of the route's space can manage access rules. + class RoutePolicyAccess < BaseAccess + # Space Developer of the route's space can manage route policies. # No bilateral requirement — destination-controlled auth only. - def create?(access_rule, _params=nil) + def create?(route_policy, _params=nil) return true if admin_user? - route = access_rule.route + route = route_policy.route return false unless route space = route.space @@ -14,21 +14,21 @@ def create?(access_rule, _params=nil) space.developers.include?(context.user) end - def read?(access_rule) + def read?(route_policy) return true if admin_user? || admin_read_only_user? || global_auditor? - route = access_rule.route + route = route_policy.route return false unless route - object_is_visible_to_user?(access_rule, context.user) + object_is_visible_to_user?(route_policy, context.user) end - def update?(access_rule, _params=nil) - create?(access_rule) + def update?(route_policy, _params=nil) + create?(route_policy) end - def delete?(access_rule) - create?(access_rule) + def delete?(route_policy) + create?(route_policy) end def index?(_object_class, _params=nil) diff --git a/app/actions/domain_create.rb b/app/actions/domain_create.rb index 2ebbe778c14..6f1016eb752 100644 --- a/app/actions/domain_create.rb +++ b/app/actions/domain_create.rb @@ -21,8 +21,8 @@ def create(message:, shared_organizations: []) end domain.router_group_guid = message.router_group_guid - domain.enforce_access_rules = message.enforce_access_rules || false - domain.access_rules_scope = message.access_rules_scope + domain.enforce_route_policies = message.enforce_route_policies || false + domain.route_policies_scope = message.route_policies_scope Domain.db.transaction do domain.save diff --git a/app/controllers/v3/access_rules_controller.rb b/app/controllers/v3/access_rules_controller.rb deleted file mode 100644 index 9d93d79a3a8..00000000000 --- a/app/controllers/v3/access_rules_controller.rb +++ /dev/null @@ -1,166 +0,0 @@ -require 'messages/access_rule_create_message' -require 'messages/access_rule_update_message' -require 'messages/access_rules_list_message' -require 'presenters/v3/access_rule_presenter' -require 'decorators/include_access_rule_selector_resource_decorator' -require 'decorators/include_access_rule_route_decorator' - -class AccessRulesController < ApplicationController - def index - message = AccessRulesListMessage.from_params(query_params) - invalid_param!(message.errors.full_messages) unless message.valid? - - dataset = build_dataset(message) - - decorators = [] - decorators << IncludeAccessRuleSelectorResourceDecorator if IncludeAccessRuleSelectorResourceDecorator.match?(message.include) - decorators << IncludeAccessRuleRouteDecorator if IncludeAccessRuleRouteDecorator.match?(message.include) - - render status: :ok, json: Presenters::V3::PaginatedListPresenter.new( - presenter: Presenters::V3::AccessRulePresenter, - paginated_result: SequelPaginator.new.get_page(dataset, message.try(:pagination_options)), - path: '/v3/access_rules', - message: message, - decorators: decorators - ) - end - - def show - access_rule = VCAP::CloudController::RouteAccessRule.find(guid: hashed_params[:guid]) - resource_not_found!(:access_rule) unless access_rule - - route = access_rule.route - resource_not_found!(:access_rule) unless route && permission_queryer.can_read_from_space?(route.space.id, route.space.organization_id) - - render status: :ok, json: Presenters::V3::AccessRulePresenter.new(access_rule) - end - - def create - message = AccessRuleCreateMessage.new(hashed_params[:body]) - unprocessable!(message.errors.full_messages) unless message.valid? - - route = find_and_authorize_route(message.route_guid) - validate_route_domain(route) - - access_rule = VCAP::CloudController::RouteAccessRule.db.transaction do - # Lock existing access rules for this route to prevent concurrent inserts - # from violating cf:any exclusivity or uniqueness constraints - VCAP::CloudController::RouteAccessRule.where(route_id: route.id).for_update.all - - validate_selector_exclusivity(route, message.selector) - - rule = VCAP::CloudController::RouteAccessRule.new( - guid: SecureRandom.uuid, - selector: message.selector, - route_id: route.id, - created_at: Time.now.utc, - updated_at: Time.now.utc - ) - rule.save - rule - end - - render status: :created, json: Presenters::V3::AccessRulePresenter.new(access_rule) - rescue Sequel::UniqueConstraintViolation - unprocessable!("An access rule with selector '#{message.selector}' already exists for this route.") - end - - def update - access_rule = VCAP::CloudController::RouteAccessRule.find(guid: hashed_params[:guid]) - resource_not_found!(:access_rule) unless access_rule - - route = access_rule.route - resource_not_found!(:access_rule) unless route && permission_queryer.can_read_from_space?(route.space.id, route.space.organization_id) - unauthorized! unless permission_queryer.can_write_to_active_space?(route.space.id) - suspended! unless permission_queryer.is_space_active?(route.space.id) - - message = AccessRuleUpdateMessage.new(hashed_params[:body]) - unprocessable!(message.errors.full_messages) unless message.valid? - - VCAP::CloudController::MetadataUpdate.update(access_rule, message) - - render status: :ok, json: Presenters::V3::AccessRulePresenter.new(access_rule.reload) - end - - def destroy - access_rule = VCAP::CloudController::RouteAccessRule.find(guid: hashed_params[:guid]) - resource_not_found!(:access_rule) unless access_rule - - route = access_rule.route - resource_not_found!(:access_rule) unless route && permission_queryer.can_read_from_space?(route.space.id, route.space.organization_id) - unauthorized! unless permission_queryer.can_write_to_active_space?(route.space.id) - suspended! unless permission_queryer.is_space_active?(route.space.id) - - access_rule.destroy - head :no_content - end - - private - - def find_and_authorize_route(route_guid) - route = VCAP::CloudController::Route.find(guid: route_guid) - resource_not_found!(:route) unless route && permission_queryer.can_read_from_space?(route.space.id, route.space.organization_id) - unauthorized! unless permission_queryer.can_write_to_active_space?(route.space.id) - suspended! unless permission_queryer.is_space_active?(route.space.id) - route - end - - def validate_route_domain(route) - if route.domain.internal? - unprocessable!('Cannot create access rules for routes on internal domains. Internal routes use container-to-container networking and bypass GoRouter.') - end - 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 - end - - def validate_selector_exclusivity(route, selector) - existing_selectors = route.access_rules.map(&:selector) - - # Enforce cf:any exclusivity: if route already has a cf:any rule, reject new rules; - # if new rule is cf:any, reject if route already has any rules. - unprocessable!("Cannot add 'cf:any' selector when other access rules already exist for this route.") if selector == 'cf:any' && existing_selectors.any? - unprocessable!("Cannot add selector '#{selector}': route already has a 'cf:any' rule.") if existing_selectors.include?('cf:any') && selector != 'cf:any' - - # Uniqueness: selector must be unique per route - unprocessable!("An access rule with selector '#{selector}' already exists for this route.") if existing_selectors.include?(selector) - end - - def build_dataset(message) - dataset = VCAP::CloudController::RouteAccessRule.dataset - - if permission_queryer.can_read_globally? - readable_route_ids = VCAP::CloudController::Route.select(:id) - else - readable_space_ids = permission_queryer.readable_space_scoped_spaces_query.select(:id) - readable_route_ids = VCAP::CloudController::Route.where(space_id: readable_space_ids).select(:id) - end - - dataset = dataset.where(route_id: readable_route_ids) - - # Join routes at most once when either route_guids or space_guids is requested - if message.requested?(:route_guids) || message.requested?(:space_guids) - dataset = dataset. - join(:routes, id: :route_id). - select_all(:route_access_rules) - - dataset = dataset.where(Sequel[:routes][:guid] => message.route_guids) if message.requested?(:route_guids) - - dataset = dataset.where(Sequel[:routes][:space_id] => VCAP::CloudController::Space.where(guid: message.space_guids).select(:id)) if message.requested?(:space_guids) - end - - dataset = dataset.where(guid: message.guids) if message.requested?(:guids) - dataset = dataset.where(selector: message.selectors) if message.requested?(:selectors) - - if message.requested?(:selector_resource_guids) - # Text-match against selector string for resource GUIDs - # Handles cf:app:, cf:space:, cf:org: - # Escape LIKE metacharacters (\, %, _) in user-provided values - conditions = message.selector_resource_guids.map do |guid| - escaped_guid = guid.gsub('\\', '\\\\').gsub('%', '\\%').gsub('_', '\\_') - Sequel.like(:selector, "%#{escaped_guid}%") - end - dataset = dataset.where(Sequel.|(*conditions)) - end - - dataset - end -end diff --git a/app/controllers/v3/route_policies_controller.rb b/app/controllers/v3/route_policies_controller.rb new file mode 100644 index 00000000000..cda9352e5c2 --- /dev/null +++ b/app/controllers/v3/route_policies_controller.rb @@ -0,0 +1,168 @@ +require 'messages/route_policy_create_message' +require 'messages/route_policy_update_message' +require 'messages/route_policies_list_message' +require 'presenters/v3/route_policy_presenter' +require 'decorators/include_route_policy_source_decorator' +require 'decorators/include_route_policy_route_decorator' + +class RoutePoliciesController < ApplicationController + def index + message = RoutePoliciesListMessage.from_params(query_params) + invalid_param!(message.errors.full_messages) unless message.valid? + + dataset = build_dataset(message) + + decorators = [] + decorators << IncludeRoutePolicySourceDecorator if IncludeRoutePolicySourceDecorator.match?(message.include) + decorators << IncludeRoutePolicyRouteDecorator if IncludeRoutePolicyRouteDecorator.match?(message.include) + + render status: :ok, json: Presenters::V3::PaginatedListPresenter.new( + presenter: Presenters::V3::RoutePolicyPresenter, + paginated_result: SequelPaginator.new.get_page(dataset, message.try(:pagination_options)), + path: '/v3/route_policies', + message: message, + decorators: decorators + ) + end + + def show + route_policy = VCAP::CloudController::RoutePolicy.find(guid: hashed_params[:guid]) + resource_not_found!(:route_policy) unless route_policy + + route = route_policy.route + resource_not_found!(:route_policy) unless route && permission_queryer.can_read_from_space?(route.space.id, route.space.organization_id) + + render status: :ok, json: Presenters::V3::RoutePolicyPresenter.new(route_policy) + end + + def create + message = RoutePolicyCreateMessage.new(hashed_params[:body]) + unprocessable!(message.errors.full_messages) unless message.valid? + + route = find_and_authorize_route(message.route_guid) + validate_route_domain(route) + + route_policy = VCAP::CloudController::RoutePolicy.db.transaction do + # Lock existing route policies for this route to prevent concurrent inserts + # from violating cf:any exclusivity or uniqueness constraints + VCAP::CloudController::RoutePolicy.where(route_id: route.id).for_update.all + + validate_source_exclusivity(route, message.source) + + policy = VCAP::CloudController::RoutePolicy.new( + guid: SecureRandom.uuid, + source: message.source, + route_id: route.id, + created_at: Time.now.utc, + updated_at: Time.now.utc + ) + policy.save + policy + end + + render status: :created, json: Presenters::V3::RoutePolicyPresenter.new(route_policy) + rescue Sequel::UniqueConstraintViolation + unprocessable!("A route policy with source '#{message.source}' already exists for this route.") + end + + def update + route_policy = VCAP::CloudController::RoutePolicy.find(guid: hashed_params[:guid]) + resource_not_found!(:route_policy) unless route_policy + + route = route_policy.route + resource_not_found!(:route_policy) unless route && permission_queryer.can_read_from_space?(route.space.id, route.space.organization_id) + unauthorized! unless permission_queryer.can_write_to_active_space?(route.space.id) + suspended! unless permission_queryer.is_space_active?(route.space.id) + + message = RoutePolicyUpdateMessage.new(hashed_params[:body]) + unprocessable!(message.errors.full_messages) unless message.valid? + + VCAP::CloudController::MetadataUpdate.update(route_policy, message) + + render status: :ok, json: Presenters::V3::RoutePolicyPresenter.new(route_policy.reload) + end + + def destroy + route_policy = VCAP::CloudController::RoutePolicy.find(guid: hashed_params[:guid]) + resource_not_found!(:route_policy) unless route_policy + + route = route_policy.route + resource_not_found!(:route_policy) unless route && permission_queryer.can_read_from_space?(route.space.id, route.space.organization_id) + unauthorized! unless permission_queryer.can_write_to_active_space?(route.space.id) + suspended! unless permission_queryer.is_space_active?(route.space.id) + + route_policy.destroy + head :no_content + end + + private + + def find_and_authorize_route(route_guid) + route = VCAP::CloudController::Route.find(guid: route_guid) + resource_not_found!(:route) unless route && permission_queryer.can_read_from_space?(route.space.id, route.space.organization_id) + unauthorized! unless permission_queryer.can_write_to_active_space?(route.space.id) + suspended! unless permission_queryer.is_space_active?(route.space.id) + route + end + + def validate_route_domain(route) + if route.domain.internal? + unprocessable!('Cannot create route policies for routes on internal domains. Internal routes use container-to-container networking and bypass GoRouter.') + end + return if route.domain.enforce_route_policies + + unprocessable!("Cannot create route policies for route '#{route.guid}': the route's domain does not have enforce_route_policies enabled.") + end + + def validate_source_exclusivity(route, source) + existing_sources = route.route_policies.map(&:source) + + # Enforce cf:any exclusivity: if route already has a cf:any policy, reject new policies; + # if new policy is cf:any, reject if route already has any policies. + unprocessable!("Cannot add 'cf:any' source when other route policies already exist for this route.") if source == 'cf:any' && existing_sources.any? + unprocessable!("Cannot add source '#{source}': route already has a 'cf:any' policy.") if existing_sources.include?('cf:any') && source != 'cf:any' + + # Uniqueness: source must be unique per route + unprocessable!("A route policy with source '#{source}' already exists for this route.") if existing_sources.include?(source) + end + + def build_dataset(message) + dataset = VCAP::CloudController::RoutePolicy.dataset + + if permission_queryer.can_read_globally? + readable_route_ids = VCAP::CloudController::Route.select(:id) + else + readable_space_ids = permission_queryer.readable_space_scoped_spaces_query.select(:id) + readable_route_ids = VCAP::CloudController::Route.where(space_id: readable_space_ids).select(:id) + end + + dataset = dataset.where(route_id: readable_route_ids) + + # Join routes at most once when either route_guids or space_guids is requested + if message.requested?(:route_guids) || message.requested?(:space_guids) + dataset = dataset. + join(:routes, id: :route_id). + select_all(:route_policies) + + dataset = dataset.where(Sequel[:routes][:guid] => message.route_guids) if message.requested?(:route_guids) + + dataset = dataset.where(Sequel[:routes][:space_id] => VCAP::CloudController::Space.where(guid: message.space_guids).select(:id)) if message.requested?(:space_guids) + end + + dataset = dataset.where(guid: message.guids) if message.requested?(:guids) + dataset = dataset.where(source: message.sources) if message.requested?(:sources) + + if message.requested?(:source_guids) + # Text-match against source string for resource GUIDs + # Handles cf:app:, cf:space:, cf:org: + # Escape LIKE metacharacters (\, %, _) in user-provided values + conditions = message.source_guids.map do |guid| + escaped_guid = guid.gsub('\\', '\\\\').gsub('%', '\\%').gsub('_', '\\_') + Sequel.like(:source, "%#{escaped_guid}%") + end + dataset = dataset.where(Sequel.|(*conditions)) + end + + dataset + end +end diff --git a/app/decorators/include_access_rule_route_decorator.rb b/app/decorators/include_route_policy_route_decorator.rb similarity index 61% rename from app/decorators/include_access_rule_route_decorator.rb rename to app/decorators/include_route_policy_route_decorator.rb index 45ea0baca57..dbc4e1ea04f 100644 --- a/app/decorators/include_access_rule_route_decorator.rb +++ b/app/decorators/include_route_policy_route_decorator.rb @@ -1,17 +1,17 @@ module VCAP::CloudController - class IncludeAccessRuleRouteDecorator - # Handles `?include=route` for GET /v3/access_rules - # Includes the route resources associated with the access rules + class IncludeRoutePolicyRouteDecorator + # Handles `?include=route` for GET /v3/route_policies + # Includes the route resources associated with the route policies def self.match?(include_params) include_params&.include?('route') end - def self.decorate(hash, access_rules) + def self.decorate(hash, route_policies) hash[:included] ||= {} - # Collect all unique route IDs from access rules - route_ids = access_rules.map(&:route_id).uniq + # Collect all unique route IDs from route policies + route_ids = route_policies.map(&:route_id).uniq # Fetch routes with their associations routes = Route.where(id: route_ids). diff --git a/app/decorators/include_access_rule_selector_resource_decorator.rb b/app/decorators/include_route_policy_source_decorator.rb similarity index 76% rename from app/decorators/include_access_rule_selector_resource_decorator.rb rename to app/decorators/include_route_policy_source_decorator.rb index 19b05314177..271e769ac27 100644 --- a/app/decorators/include_access_rule_selector_resource_decorator.rb +++ b/app/decorators/include_route_policy_source_decorator.rb @@ -1,18 +1,18 @@ module VCAP::CloudController - class IncludeAccessRuleSelectorResourceDecorator - # Handles `?include=selector_resource` for GET /v3/access_rules - # Stale/missing resources (selector GUIDs that no longer exist) are silently absent. + class IncludeRoutePolicySourceDecorator + # Handles `?include=source` for GET /v3/route_policies + # Stale/missing resources (source GUIDs that no longer exist) are silently absent. - SELECTOR_REGEX = /\Acf:(app|space|org):([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\z/ + SOURCE_REGEX = /\Acf:(app|space|org):([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\z/ def self.match?(include_params) return false unless include_params - # Match if any of: selector_resource, app, space, organization - include_params.intersect?(%w[selector_resource app space organization]) + # Match if any of: source, app, space, organization + include_params.intersect?(%w[source app space organization]) end - def self.decorate(hash, access_rules) + def self.decorate(hash, route_policies) hash[:included] ||= {} # Collect all GUIDs by type @@ -20,8 +20,8 @@ def self.decorate(hash, access_rules) space_guids = [] org_guids = [] - access_rules.each do |rule| - match = SELECTOR_REGEX.match(rule.selector) + route_policies.each do |policy| + match = SOURCE_REGEX.match(policy.source) next unless match resource_type = match[1] diff --git a/app/messages/access_rules_list_message.rb b/app/messages/access_rules_list_message.rb deleted file mode 100644 index 564d9680e00..00000000000 --- a/app/messages/access_rules_list_message.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'messages/list_message' - -module VCAP::CloudController - class AccessRulesListMessage < ListMessage - register_allowed_keys %i[ - guids - route_guids - space_guids - selectors - selector_resource_guids - include - ] - - validates_with NoAdditionalParamsValidator - validates_with IncludeParamValidator, valid_values: %w[selector_resource route app space organization] - - validates :space_guids, array: true, allow_nil: true - validates :selector_resource_guids, array: true, allow_nil: true - - def self.from_params(params) - super(params, %w[route_guids space_guids selectors selector_resource_guids include]) - end - end -end diff --git a/app/messages/domain_create_message.rb b/app/messages/domain_create_message.rb index 55f5976ee5f..4456c13c1b8 100644 --- a/app/messages/domain_create_message.rb +++ b/app/messages/domain_create_message.rb @@ -16,8 +16,8 @@ class DomainCreateMessage < MetadataBaseMessage internal relationships router_group - enforce_access_rules - access_rules_scope + enforce_route_policies + route_policies_scope ] def self.relationships_requested? @@ -61,11 +61,11 @@ def self.relationships_requested? allow_nil: true, boolean: true - validates :enforce_access_rules, + validates :enforce_route_policies, allow_nil: true, boolean: true - validate :access_rules_scope_validation + validate :route_policies_scope_validation delegate :organization_guid, to: :relationships_message delegate :shared_organizations_guids, to: :relationships_message @@ -105,15 +105,15 @@ def router_group_validation errors.add(:router_group, 'guid must be a string') unless router_group_guid.is_a?(String) end - def access_rules_scope_validation - if requested?(:access_rules_scope) && !(access_rules_scope.nil? || %w[any org space].include?(access_rules_scope)) - errors.add(:access_rules_scope, "must be one of 'any', 'org', 'space'") + def route_policies_scope_validation + if requested?(:route_policies_scope) && !(route_policies_scope.nil? || %w[any org space].include?(route_policies_scope)) + errors.add(:route_policies_scope, "must be one of 'any', 'org', 'space'") end - return unless requested?(:enforce_access_rules) && enforce_access_rules == true - return unless !requested?(:access_rules_scope) || access_rules_scope.nil? + return unless requested?(:enforce_route_policies) && enforce_route_policies == true + return unless !requested?(:route_policies_scope) || route_policies_scope.nil? - errors.add(:access_rules_scope, 'is required when enforce_access_rules is true') + errors.add(:route_policies_scope, 'is required when enforce_route_policies is true') end class Relationships < BaseMessage diff --git a/app/messages/route_policies_list_message.rb b/app/messages/route_policies_list_message.rb new file mode 100644 index 00000000000..bd3ec945aa1 --- /dev/null +++ b/app/messages/route_policies_list_message.rb @@ -0,0 +1,24 @@ +require 'messages/list_message' + +module VCAP::CloudController + class RoutePoliciesListMessage < ListMessage + register_allowed_keys %i[ + guids + route_guids + space_guids + sources + source_guids + include + ] + + validates_with NoAdditionalParamsValidator + validates_with IncludeParamValidator, valid_values: %w[source route app space organization] + + validates :space_guids, array: true, allow_nil: true + validates :source_guids, array: true, allow_nil: true + + def self.from_params(params) + super(params, %w[route_guids space_guids sources source_guids include]) + end + end +end diff --git a/app/messages/access_rule_create_message.rb b/app/messages/route_policy_create_message.rb similarity index 55% rename from app/messages/access_rule_create_message.rb rename to app/messages/route_policy_create_message.rb index d615e0a1029..5ec09bd8914 100644 --- a/app/messages/access_rule_create_message.rb +++ b/app/messages/route_policy_create_message.rb @@ -1,21 +1,21 @@ require 'messages/metadata_base_message' module VCAP::CloudController - class AccessRuleCreateMessage < MetadataBaseMessage - SELECTOR_REGEX = /\A(cf:(app|space|org):[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|cf:any)\z/ + class RoutePolicyCreateMessage < MetadataBaseMessage + SOURCE_REGEX = /\A(cf:(app|space|org):[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|cf:any)\z/ register_allowed_keys %i[ - selector + source relationships ] validates_with NoAdditionalKeysValidator validates_with RelationshipValidator - validates :selector, presence: true, string: true + validates :source, presence: true, string: true - validate :selector_format_valid - validate :selector_not_cf_any_with_others + validate :source_format_valid + validate :source_not_cf_any_with_others delegate :route_guid, to: :relationships_message @@ -25,15 +25,15 @@ def relationships_message private - def selector_format_valid - return unless selector.is_a?(String) - return if SELECTOR_REGEX.match?(selector) + def source_format_valid + return unless source.is_a?(String) + return if SOURCE_REGEX.match?(source) - errors.add(:selector, "must be in format 'cf:app:', 'cf:space:', 'cf:org:', or 'cf:any'") + errors.add(:source, "must be in format 'cf:app:', 'cf:space:', 'cf:org:', or 'cf:any'") end - def selector_not_cf_any_with_others - # enforced at the controller level when checking existing rules on the route + def source_not_cf_any_with_others + # enforced at the controller level when checking existing policies on the route end class Relationships < BaseMessage diff --git a/app/messages/access_rule_update_message.rb b/app/messages/route_policy_update_message.rb similarity index 73% rename from app/messages/access_rule_update_message.rb rename to app/messages/route_policy_update_message.rb index b9adcf62a4a..998a59f2700 100644 --- a/app/messages/access_rule_update_message.rb +++ b/app/messages/route_policy_update_message.rb @@ -1,7 +1,7 @@ require 'messages/metadata_base_message' module VCAP::CloudController - class AccessRuleUpdateMessage < MetadataBaseMessage + class RoutePolicyUpdateMessage < MetadataBaseMessage register_allowed_keys [] validates_with NoAdditionalKeysValidator diff --git a/app/models.rb b/app/models.rb index fad27a05c79..f85c9677dcc 100644 --- a/app/models.rb +++ b/app/models.rb @@ -69,9 +69,9 @@ require 'models/runtime/revision_sidecar_model' require 'models/runtime/revision_sidecar_process_type_model' require 'models/runtime/route' -require 'models/runtime/route_access_rule' -require 'models/runtime/route_access_rule_label_model' -require 'models/runtime/route_access_rule_annotation_model' +require 'models/runtime/route_policy' +require 'models/runtime/route_policy_label_model' +require 'models/runtime/route_policy_annotation_model' require 'models/runtime/space_routes' require 'models/runtime/space_quota_definition' require 'models/runtime/stack' diff --git a/app/models/runtime/route.rb b/app/models/runtime/route.rb index 84032473a23..76a5b189f07 100644 --- a/app/models/runtime/route.rb +++ b/app/models/runtime/route.rb @@ -39,8 +39,8 @@ class InvalidOrganizationRelation < CloudController::Errors::InvalidRelation; en add_association_dependencies route_mappings: :destroy - one_to_many :access_rules, class: 'VCAP::CloudController::RouteAccessRule', key: :route_id, primary_key: :id - add_association_dependencies access_rules: :destroy + one_to_many :route_policies, class: 'VCAP::CloudController::RoutePolicy', key: :route_id, primary_key: :id + add_association_dependencies route_policies: :destroy export_attributes :host, :path, :domain_guid, :space_guid, :service_instance_guid, :port, :options import_attributes :host, :path, :domain_guid, :space_guid, :app_guids, :port, :options diff --git a/app/models/runtime/route_access_rule.rb b/app/models/runtime/route_policy.rb similarity index 76% rename from app/models/runtime/route_access_rule.rb rename to app/models/runtime/route_policy.rb index 17d4a060b07..6b74fca0642 100644 --- a/app/models/runtime/route_access_rule.rb +++ b/app/models/runtime/route_policy.rb @@ -1,19 +1,19 @@ module VCAP::CloudController - class RouteAccessRule < Sequel::Model(:route_access_rules) + class RoutePolicy < Sequel::Model(:route_policies) many_to_one :route, class: 'VCAP::CloudController::Route', key: :route_id, primary_key: :id, without_guid_generation: true - one_to_many :labels, class: 'VCAP::CloudController::RouteAccessRuleLabelModel', key: :resource_guid, primary_key: :guid - one_to_many :annotations, class: 'VCAP::CloudController::RouteAccessRuleAnnotationModel', key: :resource_guid, primary_key: :guid + one_to_many :labels, class: 'VCAP::CloudController::RoutePolicyLabelModel', key: :resource_guid, primary_key: :guid + one_to_many :annotations, class: 'VCAP::CloudController::RoutePolicyAnnotationModel', key: :resource_guid, primary_key: :guid add_association_dependencies labels: :destroy add_association_dependencies annotations: :destroy def validate - validates_presence :selector + validates_presence :source validates_presence :route_id end diff --git a/app/models/runtime/route_access_rule_annotation_model.rb b/app/models/runtime/route_policy_annotation_model.rb similarity index 63% rename from app/models/runtime/route_access_rule_annotation_model.rb rename to app/models/runtime/route_policy_annotation_model.rb index a0962184156..ab2c7994486 100644 --- a/app/models/runtime/route_access_rule_annotation_model.rb +++ b/app/models/runtime/route_policy_annotation_model.rb @@ -1,7 +1,7 @@ module VCAP::CloudController - class RouteAccessRuleAnnotationModel < Sequel::Model(:route_access_rule_annotations) + class RoutePolicyAnnotationModel < Sequel::Model(:route_policy_annotations) set_primary_key :id - many_to_one :route_access_rule, + many_to_one :route_policy, primary_key: :guid, key: :resource_guid, without_guid_generation: true diff --git a/app/models/runtime/route_access_rule_label_model.rb b/app/models/runtime/route_policy_label_model.rb similarity index 62% rename from app/models/runtime/route_access_rule_label_model.rb rename to app/models/runtime/route_policy_label_model.rb index 47737f5381a..d56775cee34 100644 --- a/app/models/runtime/route_access_rule_label_model.rb +++ b/app/models/runtime/route_policy_label_model.rb @@ -1,6 +1,6 @@ module VCAP::CloudController - class RouteAccessRuleLabelModel < Sequel::Model(:route_access_rule_labels) - many_to_one :route_access_rule, + class RoutePolicyLabelModel < Sequel::Model(:route_policy_labels) + many_to_one :route_policy, primary_key: :guid, key: :resource_guid, without_guid_generation: true diff --git a/app/presenters/v3/domain_presenter.rb b/app/presenters/v3/domain_presenter.rb index 0e3fa510d98..4b4900a6660 100644 --- a/app/presenters/v3/domain_presenter.rb +++ b/app/presenters/v3/domain_presenter.rb @@ -43,9 +43,9 @@ def to_hash links: build_links } - if domain.enforce_access_rules - hash[:enforce_access_rules] = true - hash[:access_rules_scope] = domain.access_rules_scope + if domain.enforce_route_policies + hash[:enforce_route_policies] = true + hash[:route_policies_scope] = domain.route_policies_scope end hash diff --git a/app/presenters/v3/access_rule_presenter.rb b/app/presenters/v3/route_policy_presenter.rb similarity index 59% rename from app/presenters/v3/access_rule_presenter.rb rename to app/presenters/v3/route_policy_presenter.rb index f016100d7dc..81904f61bba 100644 --- a/app/presenters/v3/access_rule_presenter.rb +++ b/app/presenters/v3/route_policy_presenter.rb @@ -4,18 +4,18 @@ module VCAP::CloudController module Presenters module V3 - class AccessRulePresenter < BasePresenter + class RoutePolicyPresenter < BasePresenter include VCAP::CloudController::Presenters::Mixins::MetadataPresentationHelpers def to_hash { - guid: access_rule.guid, - created_at: access_rule.created_at, - updated_at: access_rule.updated_at, - selector: access_rule.selector, + guid: route_policy.guid, + created_at: route_policy.created_at, + updated_at: route_policy.updated_at, + source: route_policy.source, metadata: { - labels: hashified_labels(access_rule.labels), - annotations: hashified_annotations(access_rule.annotations) + labels: hashified_labels(route_policy.labels), + annotations: hashified_annotations(route_policy.annotations) }, relationships: build_relationships, links: build_links @@ -24,7 +24,7 @@ def to_hash private - def access_rule + def route_policy @resource end @@ -32,18 +32,18 @@ def build_relationships relationships = { route: { data: { - guid: access_rule.route.guid + guid: route_policy.route.guid } } } - # Extract resource GUID from selector and populate read-only relationships + # Extract resource GUID from source and populate read-only relationships # The guid is included as-is without per-row existence checks to avoid N+1 queries. - # Use ?include=selector_resource to get full resource details with batch loading. - selector_match = access_rule.selector.match(/\Acf:(app|space|org):([0-9a-f-]+)\z/) - if selector_match - resource_type = selector_match[1] - resource_guid = selector_match[2] + # Use ?include=source to get full resource details with batch loading. + source_match = route_policy.source.match(/\Acf:(app|space|org):([0-9a-f-]+)\z/) + if source_match + resource_type = source_match[1] + resource_guid = source_match[2] relationships[:app] = { data: resource_type == 'app' ? { guid: resource_guid } : nil } relationships[:space] = { data: resource_type == 'space' ? { guid: resource_guid } : nil } @@ -61,10 +61,10 @@ def build_relationships def build_links { self: { - href: url_builder.build_url(path: "/v3/access_rules/#{access_rule.guid}") + href: url_builder.build_url(path: "/v3/route_policies/#{route_policy.guid}") }, route: { - href: url_builder.build_url(path: "/v3/routes/#{access_rule.route.guid}") + href: url_builder.build_url(path: "/v3/routes/#{route_policy.route.guid}") } } end diff --git a/app/presenters/v3/route_presenter.rb b/app/presenters/v3/route_presenter.rb index 56e0beca383..f9bac40fdba 100644 --- a/app/presenters/v3/route_presenter.rb +++ b/app/presenters/v3/route_presenter.rb @@ -56,7 +56,7 @@ def to_hash @decorators.reduce(hash) { |memo, d| d.decorate(memo, [route]) } end - INTERNAL_ROUTE_OPTIONS = %w[access_scope access_rules].freeze + INTERNAL_ROUTE_OPTIONS = %w[route_policy_scope route_policy_sources].freeze private diff --git a/config/routes.rb b/config/routes.rb index e6822b973a6..28526281d86 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -338,12 +338,12 @@ post '/roles', to: 'roles#create' delete '/roles/:guid', to: 'roles#destroy' - # access_rules - get '/access_rules', to: 'access_rules#index' - get '/access_rules/:guid', to: 'access_rules#show' - post '/access_rules', to: 'access_rules#create' - patch '/access_rules/:guid', to: 'access_rules#update' - delete '/access_rules/:guid', to: 'access_rules#destroy' + # route_policies + get '/route_policies', to: 'route_policies#index' + get '/route_policies/:guid', to: 'route_policies#show' + post '/route_policies', to: 'route_policies#create' + patch '/route_policies/:guid', to: 'route_policies#update' + delete '/route_policies/:guid', to: 'route_policies#destroy' # info get '/info', to: 'info#v3_info' diff --git a/db/migrations/20260407100000_add_enforce_access_rules_to_domains.rb b/db/migrations/20260407100000_add_enforce_access_rules_to_domains.rb deleted file mode 100644 index 5f2df5e415b..00000000000 --- a/db/migrations/20260407100000_add_enforce_access_rules_to_domains.rb +++ /dev/null @@ -1,15 +0,0 @@ -Sequel.migration do - up do - alter_table :domains do - add_column :enforce_access_rules, :boolean, default: false, null: false unless @db.schema(:domains).map(&:first).include?(:enforce_access_rules) - add_column :access_rules_scope, String, null: true, size: 255 unless @db.schema(:domains).map(&:first).include?(:access_rules_scope) - end - end - - down do - alter_table :domains do - drop_column :enforce_access_rules if @db.schema(:domains).map(&:first).include?(:enforce_access_rules) - drop_column :access_rules_scope if @db.schema(:domains).map(&:first).include?(:access_rules_scope) - end - end -end diff --git a/db/migrations/20260407100001_create_route_access_rules.rb b/db/migrations/20260407100001_create_route_access_rules.rb deleted file mode 100644 index 15137281f2f..00000000000 --- a/db/migrations/20260407100001_create_route_access_rules.rb +++ /dev/null @@ -1,58 +0,0 @@ -Sequel.migration do - up do - unless table_exists?(:route_access_rules) - create_table :route_access_rules do - primary_key :id, name: :id - String :guid, size: 255, null: false - String :selector, size: 255, null: false - Integer :route_id, null: false - DateTime :created_at, null: false - DateTime :updated_at, null: false - - index :guid, unique: true, name: :route_access_rules_guid_index - index %i[route_id selector], unique: true, name: :route_access_rules_route_id_selector_index - foreign_key [:route_id], :routes, on_delete: :cascade, name: :fk_route_access_rules_route_id - end - end - - unless table_exists?(:route_access_rule_labels) - create_table :route_access_rule_labels do - primary_key :id, name: :id - String :guid, null: false, size: 255 - String :resource_guid, null: false, size: 255 - String :key_prefix, null: false, default: '', size: 253 - String :key_name, null: false, size: 63 - String :value, null: false, size: 63 - DateTime :created_at, null: false - DateTime :updated_at - - index :guid, unique: true, name: :route_access_rule_labels_guid_index - index :resource_guid, name: :route_access_rule_labels_resource_guid_index - index %i[resource_guid key_prefix key_name], unique: true, name: :route_access_rule_labels_compound_index - foreign_key [:resource_guid], :route_access_rules, key: :guid, on_delete: :cascade, name: :fk_route_access_rule_labels_resource_guid - end - end - - unless table_exists?(:route_access_rule_annotations) - create_table :route_access_rule_annotations do - primary_key :id, name: :id - String :guid, null: false, size: 255 - String :resource_guid, null: false, size: 255 - String :key_prefix, null: false, default: '', size: 253 - String :key_name, null: false, size: 63 - String :value, size: 5000 - DateTime :created_at, null: false - DateTime :updated_at - - index :guid, unique: true, name: :route_access_rule_annotations_guid_index - index :resource_guid, name: :route_access_rule_annotations_resource_guid_index - index %i[resource_guid key_prefix key_name], unique: true, name: :route_access_rule_annotations_key_index - foreign_key [:resource_guid], :route_access_rules, key: :guid, on_delete: :cascade, name: :fk_route_access_rule_annotations_resource_guid - end - end - end - - down do - %i[route_access_rule_annotations route_access_rule_labels route_access_rules].each { |t| drop_table(t) if table_exists?(t) } - end -end diff --git a/db/migrations/20260421074454_add_enforce_route_policies_to_domains.rb b/db/migrations/20260421074454_add_enforce_route_policies_to_domains.rb new file mode 100644 index 00000000000..dd5518a95c6 --- /dev/null +++ b/db/migrations/20260421074454_add_enforce_route_policies_to_domains.rb @@ -0,0 +1,15 @@ +Sequel.migration do + up do + alter_table :domains do + add_column :enforce_route_policies, :boolean, default: false, null: false unless @db.schema(:domains).map(&:first).include?(:enforce_route_policies) + add_column :route_policies_scope, String, null: true, size: 255 unless @db.schema(:domains).map(&:first).include?(:route_policies_scope) + end + end + + down do + alter_table :domains do + drop_column :enforce_route_policies if @db.schema(:domains).map(&:first).include?(:enforce_route_policies) + drop_column :route_policies_scope if @db.schema(:domains).map(&:first).include?(:route_policies_scope) + end + end +end diff --git a/db/migrations/20260421074455_create_route_policies.rb b/db/migrations/20260421074455_create_route_policies.rb new file mode 100644 index 00000000000..5e7c794c6ad --- /dev/null +++ b/db/migrations/20260421074455_create_route_policies.rb @@ -0,0 +1,58 @@ +Sequel.migration do + up do + unless table_exists?(:route_policies) + create_table :route_policies do + primary_key :id, name: :id + String :guid, size: 255, null: false + String :source, size: 255, null: false + Integer :route_id, null: false + DateTime :created_at, null: false + DateTime :updated_at, null: false + + index :guid, unique: true, name: :route_policies_guid_index + index %i[route_id source], unique: true, name: :route_policies_route_id_source_index + foreign_key [:route_id], :routes, on_delete: :cascade, name: :fk_route_policies_route_id + end + end + + unless table_exists?(:route_policy_labels) + create_table :route_policy_labels do + primary_key :id, name: :id + String :guid, null: false, size: 255 + String :resource_guid, null: false, size: 255 + String :key_prefix, null: false, default: '', size: 253 + String :key_name, null: false, size: 63 + String :value, null: false, size: 63 + DateTime :created_at, null: false + DateTime :updated_at + + index :guid, unique: true, name: :route_policy_labels_guid_index + index :resource_guid, name: :route_policy_labels_resource_guid_index + index %i[resource_guid key_prefix key_name], unique: true, name: :route_policy_labels_compound_index + foreign_key [:resource_guid], :route_policies, key: :guid, on_delete: :cascade, name: :fk_route_policy_labels_resource_guid + end + end + + unless table_exists?(:route_policy_annotations) + create_table :route_policy_annotations do + primary_key :id, name: :id + String :guid, null: false, size: 255 + String :resource_guid, null: false, size: 255 + String :key_prefix, null: false, default: '', size: 253 + String :key_name, null: false, size: 63 + String :value, size: 5000 + DateTime :created_at, null: false + DateTime :updated_at + + index :guid, unique: true, name: :route_policy_annotations_guid_index + index :resource_guid, name: :route_policy_annotations_resource_guid_index + index %i[resource_guid key_prefix key_name], unique: true, name: :route_policy_annotations_key_index + foreign_key [:resource_guid], :route_policies, key: :guid, on_delete: :cascade, name: :fk_route_policy_annotations_resource_guid + end + end + end + + down do + %i[route_policy_annotations route_policy_labels route_policies].each { |t| drop_table(t) if table_exists?(t) } + end +end diff --git a/lib/cloud_controller/diego/protocol/routing_info.rb b/lib/cloud_controller/diego/protocol/routing_info.rb index e73f7adb914..c23e33a7041 100644 --- a/lib/cloud_controller/diego/protocol/routing_info.rb +++ b/lib/cloud_controller/diego/protocol/routing_info.rb @@ -9,7 +9,7 @@ def initialize(process) end def routing_info - process_eager = ProcessModel.eager(route_mappings: { route: %i[domain route_binding access_rules] }).where(id: process.id).all + process_eager = ProcessModel.eager(route_mappings: { route: %i[domain route_binding route_policies] }).where(id: process.id).all return {} if process_eager.empty? @@ -50,18 +50,18 @@ def build_http_route_info(route_mapping) info['protocol'] = route_mapping.protocol info['options'] = r.options if r.options - add_mtls_options(info, r) if r.domain.enforce_access_rules + add_mtls_options(info, r) if r.domain.enforce_route_policies info end def add_mtls_options(info, route) - # Inject mTLS access control options for enforce_access_rules domains. + # Inject mTLS policy options for enforce_route_policies domains. # These are GoRouter-internal keys and are filtered from the /v3/routes API. mtls_options = info['options']&.dup || {} - mtls_options['access_scope'] = route.domain.access_rules_scope if route.domain.access_rules_scope - selectors = route.access_rules.map(&:selector) - mtls_options['access_rules'] = selectors.join(',') unless selectors.empty? + mtls_options['route_policy_scope'] = route.domain.route_policies_scope if route.domain.route_policies_scope + sources = route.route_policies.map(&:source) + mtls_options['route_policy_sources'] = sources.join(',') unless sources.empty? info['options'] = mtls_options end diff --git a/spec/request/apps_spec.rb b/spec/request/apps_spec.rb index 64fcef98a77..f256ffb548b 100644 --- a/spec/request/apps_spec.rb +++ b/spec/request/apps_spec.rb @@ -607,7 +607,7 @@ stacks: 'cf', include: 'space', lifecycle_type: 'buildpack', - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/buildpacks_spec.rb b/spec/request/buildpacks_spec.rb index 81b07353d88..92ca4ddab84 100644 --- a/spec/request/buildpacks_spec.rb +++ b/spec/request/buildpacks_spec.rb @@ -29,7 +29,7 @@ names: 'foo', stacks: 'cf', lifecycle: 'buildpack', - label_selector: 'foo,bar', + label_source: 'foo,bar', guids: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } diff --git a/spec/request/builds_spec.rb b/spec/request/builds_spec.rb index 5a88e6b7795..da57aec32b2 100644 --- a/spec/request/builds_spec.rb +++ b/spec/request/builds_spec.rb @@ -410,7 +410,7 @@ guids: '123', app_guids: '123', package_guids: '123', - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/deployments_spec.rb b/spec/request/deployments_spec.rb index 88db57c603f..159fd9844d2 100644 --- a/spec/request/deployments_spec.rb +++ b/spec/request/deployments_spec.rb @@ -2077,7 +2077,7 @@ def json_for_options(deployment) status_values: 'foo', status_reasons: 'foo', app_guids: '123', - label_selector: 'bar', + label_source: 'bar', guids: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } diff --git a/spec/request/domains_spec.rb b/spec/request/domains_spec.rb index 024b7792086..f0969eaaa2c 100644 --- a/spec/request/domains_spec.rb +++ b/spec/request/domains_spec.rb @@ -31,7 +31,7 @@ names: 'foo,bar', guids: 'foo,bar', organization_guids: 'foo,bar', - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/droplets_spec.rb b/spec/request/droplets_spec.rb index fb60d1d9df6..bac1d9a59d4 100644 --- a/spec/request/droplets_spec.rb +++ b/spec/request/droplets_spec.rb @@ -646,7 +646,7 @@ space_guids: 'test', states: %w[test foo], organization_guids: 'foo,bar', - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } @@ -674,7 +674,7 @@ current: true, package_guid: package_model.guid, states: %w[test foo], - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/isolation_segments_spec.rb b/spec/request/isolation_segments_spec.rb index c362272f507..fcdf51f34c3 100644 --- a/spec/request/isolation_segments_spec.rb +++ b/spec/request/isolation_segments_spec.rb @@ -269,7 +269,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/organizations_spec.rb b/spec/request/organizations_spec.rb index fc5b009d716..836748866b1 100644 --- a/spec/request/organizations_spec.rb +++ b/spec/request/organizations_spec.rb @@ -194,7 +194,7 @@ module VCAP::CloudController page: '2', per_page: '10', order_by: 'updated_at', - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } @@ -1462,7 +1462,7 @@ module VCAP::CloudController page: '2', per_page: '10', order_by: 'updated_at', - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } @@ -1488,7 +1488,7 @@ module VCAP::CloudController page: '2', per_page: '10', order_by: 'updated_at', - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/packages_spec.rb b/spec/request/packages_spec.rb index 81a05c313fd..654f0adb13f 100644 --- a/spec/request/packages_spec.rb +++ b/spec/request/packages_spec.rb @@ -481,7 +481,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/processes_spec.rb b/spec/request/processes_spec.rb index fb2cf1bf20e..105a835a91b 100644 --- a/spec/request/processes_spec.rb +++ b/spec/request/processes_spec.rb @@ -121,7 +121,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/revisions_spec.rb b/spec/request/revisions_spec.rb index d3ace2cf840..701d4a057cb 100644 --- a/spec/request/revisions_spec.rb +++ b/spec/request/revisions_spec.rb @@ -185,7 +185,7 @@ order_by: 'updated_at', guids: app_model.guid.to_s, versions: '1,2', - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/access_rules_spec.rb b/spec/request/route_policies_spec.rb similarity index 75% rename from spec/request/access_rules_spec.rb rename to spec/request/route_policies_spec.rb index de3cff11b9b..4582c0ba13c 100644 --- a/spec/request/access_rules_spec.rb +++ b/spec/request/route_policies_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -RSpec.describe 'Access Rules' do +RSpec.describe 'Route Policies' do let(:user) { VCAP::CloudController::User.make } let(:admin_header) { admin_headers_for(user) } let(:org) { VCAP::CloudController::Organization.make } @@ -34,12 +34,12 @@ def expected_rule_json(rule) guid: rule.guid, created_at: iso8601, updated_at: iso8601, - selector: rule.selector, + source: rule.source, relationships: { route: { data: { guid: rule.route.guid } } }, links: { - self: { href: %r{/v3/access_rules/#{rule.guid}} }, + self: { href: %r{/v3/route_policies/#{rule.guid}} }, route: { href: %r{/v3/routes/#{rule.route.guid}} } } } @@ -51,10 +51,10 @@ def expected_rule_json(rule) space.add_developer(user) end - describe 'POST /v3/access_rules' do + describe 'POST /v3/route_policies' do let(:request_body) do { - selector: "cf:app:#{valid_uuid}", + source: "cf:app:#{valid_uuid}", relationships: { route: { data: { guid: mtls_route.guid } } } @@ -63,11 +63,11 @@ def expected_rule_json(rule) context 'as admin' do it 'creates an access rule and returns 201' do - post '/v3/access_rules', request_body.to_json, admin_header + post '/v3/route_policies', request_body.to_json, admin_header expect(last_response.status).to eq(201) parsed = Oj.load(last_response.body) - expect(parsed['selector']).to eq("cf:app:#{valid_uuid}") + expect(parsed['source']).to eq("cf:app:#{valid_uuid}") expect(parsed['relationships']['route']['data']['guid']).to eq(mtls_route.guid) end end @@ -76,7 +76,7 @@ def expected_rule_json(rule) let(:user_headers) { headers_for(user) } it 'creates an access rule' do - post '/v3/access_rules', request_body.to_json, user_headers + post '/v3/route_policies', request_body.to_json, user_headers expect(last_response.status).to eq(201) end @@ -85,7 +85,7 @@ def expected_rule_json(rule) context 'when the domain does not have enforce_access_rules enabled' do let(:request_body) do { - selector: "cf:app:#{valid_uuid}", + source: "cf:app:#{valid_uuid}", relationships: { route: { data: { guid: regular_route.guid } } } @@ -93,17 +93,17 @@ def expected_rule_json(rule) end it 'returns 422' do - post '/v3/access_rules', request_body.to_json, admin_header + post '/v3/route_policies', request_body.to_json, admin_header expect(last_response.status).to eq(422) - expect(last_response.body).to include('enforce_access_rules') + expect(last_response.body).to include('enforce_route_policies') end end context 'when the route is on an internal domain' do let(:request_body) do { - selector: "cf:app:#{valid_uuid}", + source: "cf:app:#{valid_uuid}", relationships: { route: { data: { guid: internal_route.guid } } } @@ -111,7 +111,7 @@ def expected_rule_json(rule) end it 'returns 422 with a message about internal domains' do - post '/v3/access_rules', request_body.to_json, admin_header + post '/v3/route_policies', request_body.to_json, admin_header expect(last_response.status).to eq(422) expect(last_response.body).to include('internal domains') @@ -122,7 +122,7 @@ def expected_rule_json(rule) context 'when the route does not exist' do let(:request_body) do { - selector: "cf:app:#{valid_uuid}", + source: "cf:app:#{valid_uuid}", relationships: { route: { data: { guid: 'nonexistent-guid' } } } @@ -130,7 +130,7 @@ def expected_rule_json(rule) end it 'returns 404' do - post '/v3/access_rules', request_body.to_json, admin_header + post '/v3/route_policies', request_body.to_json, admin_header expect(last_response.status).to eq(404) end @@ -138,16 +138,16 @@ def expected_rule_json(rule) context 'cf:any exclusivity' do before do - VCAP::CloudController::RouteAccessRule.create( + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: "cf:app:#{valid_uuid}", + source: "cf:app:#{valid_uuid}", route_id: mtls_route.id ) end it 'rejects cf:any when other rules exist' do - post '/v3/access_rules', { - selector: 'cf:any', + post '/v3/route_policies', { + source: 'cf:any', relationships: { route: { data: { guid: mtls_route.guid } } } }.to_json, admin_header @@ -158,16 +158,16 @@ def expected_rule_json(rule) context 'when a cf:any rule already exists' do before do - VCAP::CloudController::RouteAccessRule.create( + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: 'cf:any', + source: 'cf:any', route_id: mtls_route.id ) end it 'rejects adding a specific selector' do - post '/v3/access_rules', { - selector: "cf:space:#{valid_uuid}", + post '/v3/route_policies', { + source: "cf:space:#{valid_uuid}", relationships: { route: { data: { guid: mtls_route.guid } } } }.to_json, admin_header @@ -178,16 +178,16 @@ def expected_rule_json(rule) context 'duplicate selector per route' do before do - VCAP::CloudController::RouteAccessRule.create( + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: "cf:app:#{valid_uuid}", + source: "cf:app:#{valid_uuid}", route_id: mtls_route.id ) end it 'returns 422' do - post '/v3/access_rules', { - selector: "cf:app:#{valid_uuid}", + post '/v3/route_policies', { + source: "cf:app:#{valid_uuid}", relationships: { route: { data: { guid: mtls_route.guid } } } }.to_json, admin_header @@ -197,8 +197,8 @@ def expected_rule_json(rule) context 'invalid selector format' do it 'returns 422' do - post '/v3/access_rules', { - selector: 'not-valid', + post '/v3/route_policies', { + source: 'not-valid', relationships: { route: { data: { guid: mtls_route.guid } } } }.to_json, admin_header @@ -211,12 +211,12 @@ def expected_rule_json(rule) it 'returns 422 instead of 500' do # Simulate a race condition where the DB unique constraint catches the duplicate # after validation passes but before the insert commits - allow_any_instance_of(VCAP::CloudController::RouteAccessRule).to receive(:save).and_raise( - Sequel::UniqueConstraintViolation.new('UNIQUE constraint failed: route_access_rules.route_id, route_access_rules.selector') + allow_any_instance_of(VCAP::CloudController::RoutePolicy).to receive(:save).and_raise( + Sequel::UniqueConstraintViolation.new('UNIQUE constraint failed: route_access_rules.route_id, route_access_rules.source') ) - post '/v3/access_rules', { - selector: "cf:app:#{valid_uuid}", + post '/v3/route_policies', { + source: "cf:app:#{valid_uuid}", relationships: { route: { data: { guid: mtls_route.guid } } } }.to_json, admin_header @@ -226,51 +226,51 @@ def expected_rule_json(rule) end end - describe 'GET /v3/access_rules/:guid' do - let!(:access_rule) do - VCAP::CloudController::RouteAccessRule.create( + describe 'GET /v3/route_policies/:guid' do + let!(:route_policy) do + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: "cf:app:#{valid_uuid}", + source: "cf:app:#{valid_uuid}", route_id: mtls_route.id ) end it 'returns the access rule' do - get "/v3/access_rules/#{access_rule.guid}", nil, admin_header + get "/v3/route_policies/#{access_rule.guid}", nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) expect(parsed['guid']).to eq(access_rule.guid) - expect(parsed['selector']).to eq("cf:app:#{valid_uuid}") + expect(parsed['source']).to eq("cf:app:#{valid_uuid}") end context 'when the access rule does not exist' do it 'returns 404' do - get '/v3/access_rules/nonexistent-guid', nil, admin_header + get '/v3/route_policies/nonexistent-guid', nil, admin_header expect(last_response.status).to eq(404) end end end - describe 'GET /v3/access_rules' do + describe 'GET /v3/route_policies' do let!(:rule1) do - VCAP::CloudController::RouteAccessRule.create( + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: "cf:app:#{valid_uuid}", + source: "cf:app:#{valid_uuid}", route_id: mtls_route.id ) end let!(:rule2) do - VCAP::CloudController::RouteAccessRule.create( + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: 'cf:any', + source: 'cf:any', route_id: VCAP::CloudController::Route.make(space: space, domain: mtls_domain).id ) end it 'lists all accessible access rules' do - get '/v3/access_rules', nil, admin_header + get '/v3/route_policies', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -279,7 +279,7 @@ def expected_rule_json(rule) end it 'filters by route_guids' do - get "/v3/access_rules?route_guids=#{mtls_route.guid}", nil, admin_header + get "/v3/route_policies?route_guids=#{mtls_route.guid}", nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -289,12 +289,12 @@ def expected_rule_json(rule) end it 'filters by selectors' do - get '/v3/access_rules?selectors=cf:any', nil, admin_header + get '/v3/route_policies?selectors=cf:any', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) expect(parsed['resources'].length).to eq(1) - expect(parsed['resources'][0]['selector']).to eq('cf:any') + expect(parsed['resources'][0]['source']).to eq('cf:any') end describe 'filtering by space_guids' do @@ -309,9 +309,9 @@ def expected_rule_json(rule) end let(:other_route) { VCAP::CloudController::Route.make(space: other_space, domain: other_mtls_domain) } let!(:rule_in_other_space) do - VCAP::CloudController::RouteAccessRule.create( + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: 'cf:any', + source: 'cf:any', route_id: other_route.id ) end @@ -322,7 +322,7 @@ def expected_rule_json(rule) end it 'filters by single space_guid' do - get "/v3/access_rules?space_guids=#{space.guid}", nil, admin_header + get "/v3/route_policies?space_guids=#{space.guid}", nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -332,7 +332,7 @@ def expected_rule_json(rule) end it 'filters by multiple space_guids' do - get "/v3/access_rules?space_guids=#{space.guid},#{other_space.guid}", nil, admin_header + get "/v3/route_policies?space_guids=#{space.guid},#{other_space.guid}", nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -341,13 +341,13 @@ def expected_rule_json(rule) end it 'combines space_guids with other filters' do - get "/v3/access_rules?space_guids=#{space.guid}&selectors=cf:app:#{valid_uuid}", nil, admin_header + get "/v3/route_policies?space_guids=#{space.guid}&selectors=cf:app:#{valid_uuid}", nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) expect(parsed['resources'].length).to eq(1) expect(parsed['resources'][0]['guid']).to eq(rule1.guid) - expect(parsed['resources'][0]['selector']).to eq("cf:app:#{valid_uuid}") + expect(parsed['resources'][0]['source']).to eq("cf:app:#{valid_uuid}") end it 'returns empty when space has no access rules' do @@ -355,7 +355,7 @@ def expected_rule_json(rule) org.add_user(user) empty_space.add_developer(user) - get "/v3/access_rules?space_guids=#{empty_space.guid}", nil, admin_header + get "/v3/route_policies?space_guids=#{empty_space.guid}", nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -375,9 +375,9 @@ def expected_rule_json(rule) end let(:other_route) { VCAP::CloudController::Route.make(space: other_space, domain: other_mtls_domain) } let!(:rule_in_other_space) do - VCAP::CloudController::RouteAccessRule.create( + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: 'cf:any', + source: 'cf:any', route_id: other_route.id ) end @@ -388,7 +388,7 @@ def expected_rule_json(rule) end it 'returns results matching both route_guids and space_guids without ambiguous column errors' do - get "/v3/access_rules?route_guids=#{mtls_route.guid}&space_guids=#{space.guid}", nil, admin_header + get "/v3/route_policies?route_guids=#{mtls_route.guid}&space_guids=#{space.guid}", nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -400,7 +400,7 @@ def expected_rule_json(rule) describe 'filtering by selector_resource_guids' do it 'escapes % so it does not act as a LIKE wildcard' do - get '/v3/access_rules?selector_resource_guids=%25', nil, admin_header + get '/v3/route_policies?selector_resource_guids=%25', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -408,7 +408,7 @@ def expected_rule_json(rule) end it 'escapes _ so it does not act as a LIKE single-char wildcard' do - get '/v3/access_rules?selector_resource_guids=cf_app', nil, admin_header + get '/v3/route_policies?selector_resource_guids=cf_app', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -417,7 +417,7 @@ def expected_rule_json(rule) end it 'escapes backslash so it does not act as a LIKE escape character' do - get '/v3/access_rules?selector_resource_guids=cf%5Capp', nil, admin_header + get '/v3/route_policies?selector_resource_guids=cf%5Capp', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -431,31 +431,31 @@ def expected_rule_json(rule) let!(:other_org) { VCAP::CloudController::Organization.make(name: 'other-org') } let!(:app_rule) do - VCAP::CloudController::RouteAccessRule.create( + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: "cf:app:#{frontend_app.guid}", + source: "cf:app:#{frontend_app.guid}", route_id: mtls_route.id ) end let!(:space_rule) do - VCAP::CloudController::RouteAccessRule.create( + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: "cf:space:#{other_space.guid}", + source: "cf:space:#{other_space.guid}", route_id: mtls_route.id ) end let!(:org_rule) do - VCAP::CloudController::RouteAccessRule.create( + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: "cf:org:#{other_org.guid}", + source: "cf:org:#{other_org.guid}", route_id: mtls_route.id ) end it 'includes resolved selector resources' do - get '/v3/access_rules?include=selector_resource', nil, admin_header + get '/v3/route_policies?include=selector_resource', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -485,13 +485,13 @@ def expected_rule_json(rule) it 'handles stale resources (missing GUIDs) gracefully' do stale_guid = '99999999-9999-9999-9999-999999999999' - VCAP::CloudController::RouteAccessRule.create( + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: "cf:app:#{stale_guid}", + source: "cf:app:#{stale_guid}", route_id: mtls_route.id ) - get '/v3/access_rules?include=selector_resource', nil, admin_header + get '/v3/route_policies?include=selector_resource', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -503,13 +503,13 @@ def expected_rule_json(rule) it 'includes only unique resources when multiple rules reference the same resource' do # Create another rule referencing the same app - VCAP::CloudController::RouteAccessRule.create( + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: "cf:app:#{frontend_app.guid}", + source: "cf:app:#{frontend_app.guid}", route_id: VCAP::CloudController::Route.make(space: space, domain: mtls_domain).id ) - get '/v3/access_rules?include=selector_resource', nil, admin_header + get '/v3/route_policies?include=selector_resource', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -520,13 +520,13 @@ def expected_rule_json(rule) end it 'does not include resources for cf:any selectors' do - VCAP::CloudController::RouteAccessRule.create( + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: 'cf:any', + source: 'cf:any', route_id: VCAP::CloudController::Route.make(space: space, domain: mtls_domain).id ) - get '/v3/access_rules?include=selector_resource', nil, admin_header + get '/v3/route_policies?include=selector_resource', nil, admin_header expect(last_response.status).to eq(200) # Should succeed without error even with cf:any selector @@ -537,23 +537,23 @@ def expected_rule_json(rule) let(:route2) { VCAP::CloudController::Route.make(space: space, domain: mtls_domain) } let!(:rule_on_route1) do - VCAP::CloudController::RouteAccessRule.create( + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: 'cf:any', + source: 'cf:any', route_id: mtls_route.id ) end let!(:rule_on_route2) do - VCAP::CloudController::RouteAccessRule.create( + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: "cf:app:#{valid_uuid}", + source: "cf:app:#{valid_uuid}", route_id: route2.id ) end it 'includes route resources' do - get '/v3/access_rules?include=route', nil, admin_header + get '/v3/route_policies?include=route', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -576,13 +576,13 @@ def expected_rule_json(rule) it 'includes only unique routes when multiple rules reference the same route' do # Create another rule on the same route with a different selector - VCAP::CloudController::RouteAccessRule.create( + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: "cf:app:#{SecureRandom.uuid}", + source: "cf:app:#{SecureRandom.uuid}", route_id: mtls_route.id ) - get '/v3/access_rules?include=route', nil, admin_header + get '/v3/route_policies?include=route', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -594,13 +594,13 @@ def expected_rule_json(rule) it 'combines include=route with include=selector_resource' do test_app = VCAP::CloudController::AppModel.make(space: space, name: 'test-app') - VCAP::CloudController::RouteAccessRule.create( + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: "cf:app:#{test_app.guid}", + source: "cf:app:#{test_app.guid}", route_id: mtls_route.id ) - get '/v3/access_rules?include=route,selector_resource', nil, admin_header + get '/v3/route_policies?include=route,selector_resource', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -620,42 +620,42 @@ def expected_rule_json(rule) end end - describe 'DELETE /v3/access_rules/:guid' do - let!(:access_rule) do - VCAP::CloudController::RouteAccessRule.create( + describe 'DELETE /v3/route_policies/:guid' do + let!(:route_policy) do + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: "cf:app:#{valid_uuid}", + source: "cf:app:#{valid_uuid}", route_id: mtls_route.id ) end it 'deletes the access rule and returns 204' do - delete "/v3/access_rules/#{access_rule.guid}", nil, admin_header + delete "/v3/route_policies/#{access_rule.guid}", nil, admin_header expect(last_response.status).to eq(204) - expect(VCAP::CloudController::RouteAccessRule.find(guid: access_rule.guid)).to be_nil + expect(VCAP::CloudController::RoutePolicy.find(guid: access_rule.guid)).to be_nil end context 'when the access rule does not exist' do it 'returns 404' do - delete '/v3/access_rules/nonexistent-guid', nil, admin_header + delete '/v3/route_policies/nonexistent-guid', nil, admin_header expect(last_response.status).to eq(404) end end end - describe 'PATCH /v3/access_rules/:guid (metadata update)' do - let!(:access_rule) do - VCAP::CloudController::RouteAccessRule.create( + describe 'PATCH /v3/route_policies/:guid (metadata update)' do + let!(:route_policy) do + VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, - selector: "cf:app:#{valid_uuid}", + source: "cf:app:#{valid_uuid}", route_id: mtls_route.id ) end it 'returns 200' do - patch "/v3/access_rules/#{access_rule.guid}", { + patch "/v3/route_policies/#{access_rule.guid}", { metadata: { labels: { env: 'production' } } }.to_json, admin_header @@ -664,7 +664,7 @@ def expected_rule_json(rule) context 'when the access rule does not exist' do it 'returns 404' do - patch '/v3/access_rules/nonexistent-guid', {}.to_json, admin_header + patch '/v3/route_policies/nonexistent-guid', {}.to_json, admin_header expect(last_response.status).to eq(404) end diff --git a/spec/request/routes_spec.rb b/spec/request/routes_spec.rb index a8ad0dc1e2d..c953e2b0e58 100644 --- a/spec/request/routes_spec.rb +++ b/spec/request/routes_spec.rb @@ -165,7 +165,7 @@ hosts: 'foo', ports: 636, include: 'domain', - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/service_brokers_spec.rb b/spec/request/service_brokers_spec.rb index 863bd86b988..b8bfcaa0b9e 100644 --- a/spec/request/service_brokers_spec.rb +++ b/spec/request/service_brokers_spec.rb @@ -113,7 +113,7 @@ per_page: '10', page: 2, order_by: 'updated_at', - label_selector: 'foo==bar', + label_source: 'foo==bar', guids: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } diff --git a/spec/request/service_credential_bindings_spec.rb b/spec/request/service_credential_bindings_spec.rb index 68835e7765c..46a2d9d5d74 100644 --- a/spec/request/service_credential_bindings_spec.rb +++ b/spec/request/service_credential_bindings_spec.rb @@ -53,7 +53,7 @@ guids: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 }, - label_selector: 'env' + label_source: 'env' } end end diff --git a/spec/request/service_instances_spec.rb b/spec/request/service_instances_spec.rb index b9c69073434..f765828702a 100644 --- a/spec/request/service_instances_spec.rb +++ b/spec/request/service_instances_spec.rb @@ -170,7 +170,7 @@ per_page: '10', page: 2, order_by: 'updated_at', - label_selector: 'foo,bar', + label_source: 'foo,bar', type: 'managed', service_plan_guids: %w[guid-1 guid-2], service_plan_names: %w[plan-1 plan-2], diff --git a/spec/request/service_offerings_spec.rb b/spec/request/service_offerings_spec.rb index 4076abeba89..742f49e9e65 100644 --- a/spec/request/service_offerings_spec.rb +++ b/spec/request/service_offerings_spec.rb @@ -219,7 +219,7 @@ per_page: '10', page: 2, order_by: 'updated_at', - label_selector: 'foo==bar', + label_source: 'foo==bar', fields: { 'service_broker' => 'name' }, guids: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", diff --git a/spec/request/service_plans_spec.rb b/spec/request/service_plans_spec.rb index aaf5ba5963e..b97c3aebc26 100644 --- a/spec/request/service_plans_spec.rb +++ b/spec/request/service_plans_spec.rb @@ -197,7 +197,7 @@ per_page: '10', page: 2, order_by: 'updated_at', - label_selector: 'foo==bar', + label_source: 'foo==bar', fields: { 'service_offering.service_broker' => 'name' }, guids: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", diff --git a/spec/request/service_route_bindings_spec.rb b/spec/request/service_route_bindings_spec.rb index af2cde52cc0..69833d0316b 100644 --- a/spec/request/service_route_bindings_spec.rb +++ b/spec/request/service_route_bindings_spec.rb @@ -71,7 +71,7 @@ per_page: '10', page: 2, order_by: 'updated_at', - label_selector: 'foo==bar', + label_source: 'foo==bar', guids: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } diff --git a/spec/request/spaces_spec.rb b/spec/request/spaces_spec.rb index 589028b6cb1..f2edce375f1 100644 --- a/spec/request/spaces_spec.rb +++ b/spec/request/spaces_spec.rb @@ -263,7 +263,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } @@ -1304,7 +1304,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } @@ -1330,7 +1330,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/stacks_spec.rb b/spec/request/stacks_spec.rb index 644ce145ea7..0931f9a0da4 100644 --- a/spec/request/stacks_spec.rb +++ b/spec/request/stacks_spec.rb @@ -86,7 +86,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_selector: 'foo,bar', + label_source: 'foo,bar', guids: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } diff --git a/spec/request/tasks_spec.rb b/spec/request/tasks_spec.rb index 8fa34e57a70..75c549371ab 100644 --- a/spec/request/tasks_spec.rb +++ b/spec/request/tasks_spec.rb @@ -60,7 +60,7 @@ names: %w[foo bar], states: %w[test foo], organization_guids: 'foo,bar', - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } @@ -89,7 +89,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } @@ -347,7 +347,7 @@ organization_guids: app_model.organization.guid, space_guids: app_model.space.guid, states: 'SUCCEEDED', - label_selector: 'boomerang' + label_source: 'boomerang' } get "/v3/tasks?#{query.to_query}", nil, developer_headers diff --git a/spec/request/users_spec.rb b/spec/request/users_spec.rb index bf15263c0ca..d2bd984a1d8 100644 --- a/spec/request/users_spec.rb +++ b/spec/request/users_spec.rb @@ -116,7 +116,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } @@ -142,7 +142,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_selector: 'foo,bar', + label_source: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/unit/controllers/v3/apps_controller_spec.rb b/spec/unit/controllers/v3/apps_controller_spec.rb index beee5e6ed5b..8fdb0a148ae 100644 --- a/spec/unit/controllers/v3/apps_controller_spec.rb +++ b/spec/unit/controllers/v3/apps_controller_spec.rb @@ -135,7 +135,7 @@ context 'label_selection' do it 'returns a 400 when the label_selector is invalid' do - get :index, params: { label_selector: 'buncha nonsense' } + get :index, params: { label_source: 'buncha nonsense' } expect(response).to have_http_status(:bad_request) diff --git a/spec/unit/fetchers/service_broker_list_fetcher_spec.rb b/spec/unit/fetchers/service_broker_list_fetcher_spec.rb index d5a34a08e2e..6b448ee0dd2 100644 --- a/spec/unit/fetchers/service_broker_list_fetcher_spec.rb +++ b/spec/unit/fetchers/service_broker_list_fetcher_spec.rb @@ -160,7 +160,7 @@ module VCAP::CloudController let(:filters) do { space_guids: [space_2.guid], - label_selector: 'dog in (poodle,scooby-doo)', + label_source: 'dog in (poodle,scooby-doo)', names: [space_scoped_broker_1.name] } end @@ -189,7 +189,7 @@ module VCAP::CloudController let(:filters) do { space_guids: [space_1.guid, space_2.guid], - label_selector: 'dog in (poodle,scooby-doo)', + label_source: 'dog in (poodle,scooby-doo)', names: [space_scoped_broker_1.name] } end diff --git a/spec/unit/fetchers/service_offering_list_fetcher_spec.rb b/spec/unit/fetchers/service_offering_list_fetcher_spec.rb index 6870940f3f7..8391f8a6e4f 100644 --- a/spec/unit/fetchers/service_offering_list_fetcher_spec.rb +++ b/spec/unit/fetchers/service_offering_list_fetcher_spec.rb @@ -490,7 +490,7 @@ module VCAP::CloudController let!(:service_offering_1) { VCAP::CloudController::ServicePlan.make(public: true, active: true).service } let!(:service_offering_2) { VCAP::CloudController::ServicePlan.make(public: true, active: true).service } let!(:service_offering_3) { VCAP::CloudController::ServicePlan.make(public: true, active: true).service } - let(:message) { ServiceOfferingsListMessage.from_params({ label_selector: 'flavor=orange' }.with_indifferent_access) } + let(:message) { ServiceOfferingsListMessage.from_params({ label_source: 'flavor=orange' }.with_indifferent_access) } before do VCAP::CloudController::ServiceOfferingLabelModel.make(resource_guid: service_offering_1.guid, key_name: 'flavor', value: 'orange') diff --git a/spec/unit/fetchers/service_plan_list_fetcher_spec.rb b/spec/unit/fetchers/service_plan_list_fetcher_spec.rb index 4e45898df31..17f2220646e 100644 --- a/spec/unit/fetchers/service_plan_list_fetcher_spec.rb +++ b/spec/unit/fetchers/service_plan_list_fetcher_spec.rb @@ -491,7 +491,7 @@ module VCAP::CloudController let!(:service_plan_1) { VCAP::CloudController::ServicePlan.make(public: true, active: true) } let!(:service_plan_2) { VCAP::CloudController::ServicePlan.make(public: true, active: true) } let!(:service_plan_3) { VCAP::CloudController::ServicePlan.make(public: true, active: true) } - let(:message) { ServicePlansListMessage.from_params({ label_selector: 'flavor=orange' }.with_indifferent_access) } + let(:message) { ServicePlansListMessage.from_params({ label_source: 'flavor=orange' }.with_indifferent_access) } before do VCAP::CloudController::ServicePlanLabelModel.make(resource_guid: service_plan_1.guid, key_name: 'flavor', value: 'orange') diff --git a/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb b/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb index 507afe3d489..663596e3334 100644 --- a/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb @@ -263,16 +263,16 @@ class Protocol end let(:mtls_route) { Route.make(host: 'myapp', domain: enforce_domain, space: space) } let!(:access_rule1) do - RouteAccessRule.create( + RoutePolicy.create( guid: SecureRandom.uuid, - selector: "cf:app:#{valid_uuid}", + source: "cf:app:#{valid_uuid}", route_id: mtls_route.id ) end let!(:access_rule2) do - RouteAccessRule.create( + RoutePolicy.create( guid: SecureRandom.uuid, - selector: "cf:space:#{valid_uuid}", + source: "cf:space:#{valid_uuid}", route_id: mtls_route.id ) end @@ -286,9 +286,9 @@ class Protocol mtls_entry = http_routes.find { |r| r['hostname'] == 'myapp.mtls.example.com' } expect(mtls_entry).not_to be_nil - expect(mtls_entry['options']['access_scope']).to eq('space') - expect(mtls_entry['options']['access_rules']).to include("cf:app:#{valid_uuid}") - expect(mtls_entry['options']['access_rules']).to include("cf:space:#{valid_uuid}") + expect(mtls_entry['options']['route_policy_scope']).to eq('space') + expect(mtls_entry['options']['route_policy_sources']).to include("cf:app:#{valid_uuid}") + expect(mtls_entry['options']['route_policy_sources']).to include("cf:space:#{valid_uuid}") end context 'when the route has no access rules' do @@ -301,8 +301,8 @@ class Protocol http_routes = ri['http_routes'] mtls_entry = http_routes.find { |r| r['hostname'] == 'myapp.mtls.example.com' } - expect(mtls_entry['options']['access_scope']).to eq('space') - expect(mtls_entry['options']).not_to have_key('access_rules') + expect(mtls_entry['options']['route_policy_scope']).to eq('space') + expect(mtls_entry['options']).not_to have_key('route_policy_sources') end end end diff --git a/spec/unit/messages/app_revisions_list_message_spec.rb b/spec/unit/messages/app_revisions_list_message_spec.rb index 2a3c7849657..dd1e8d2b0f3 100644 --- a/spec/unit/messages/app_revisions_list_message_spec.rb +++ b/spec/unit/messages/app_revisions_list_message_spec.rb @@ -40,7 +40,7 @@ module VCAP::CloudController let(:opts) do { versions: %w[1 3], - label_selector: 'key=value', + label_source: 'key=value', page: 1, per_page: 5, deployable: true @@ -61,7 +61,7 @@ module VCAP::CloudController per_page: 5, versions: ['1'], deployable: true, - label_selector: 'key=value' + label_source: 'key=value' }) end.not_to raise_error end diff --git a/spec/unit/messages/apps_list_message_spec.rb b/spec/unit/messages/apps_list_message_spec.rb index e1f76f88844..e4ac1463702 100644 --- a/spec/unit/messages/apps_list_message_spec.rb +++ b/spec/unit/messages/apps_list_message_spec.rb @@ -67,7 +67,7 @@ module VCAP::CloudController per_page: 5, order_by: 'created_at', include: ['space', 'space.organization'], - label_selector: 'foo in (stuff,things)', + label_source: 'foo in (stuff,things)', lifecycle_type: 'buildpack' } end @@ -90,7 +90,7 @@ module VCAP::CloudController per_page: 5, order_by: 'created_at', include: ['space', 'space.organization'], - label_selector: 'foo in (stuff,things)', + label_source: 'foo in (stuff,things)', lifecycle_type: 'buildpack' }) end.not_to raise_error diff --git a/spec/unit/messages/buildpacks_list_message_spec.rb b/spec/unit/messages/buildpacks_list_message_spec.rb index 605c4fbac5e..3f7bed0e566 100644 --- a/spec/unit/messages/buildpacks_list_message_spec.rb +++ b/spec/unit/messages/buildpacks_list_message_spec.rb @@ -47,7 +47,7 @@ module VCAP::CloudController names: %w[name1 name2], stacks: %w[stack1 stack2], lifecycle: 'buildpack', - label_selector: 'foo=bar', + label_source: 'foo=bar', page: 1, per_page: 5 } @@ -65,7 +65,7 @@ module VCAP::CloudController BuildpacksListMessage.from_params({ names: [], stacks: [], - label_selector: '', + label_source: '', lifecycle: 'buildpack' }) end.not_to raise_error diff --git a/spec/unit/messages/domain_create_message_spec.rb b/spec/unit/messages/domain_create_message_spec.rb index 8caab439a11..9041792e1c8 100644 --- a/spec/unit/messages/domain_create_message_spec.rb +++ b/spec/unit/messages/domain_create_message_spec.rb @@ -404,13 +404,13 @@ module VCAP::CloudController end end - context 'enforce_access_rules' do + context 'enforce_route_policies' do context 'when not a boolean' do let(:params) { { name: 'name.com', enforce_access_rules: 'yes' } } it 'is not valid' do expect(subject).not_to be_valid - expect(subject.errors[:enforce_access_rules]).to include('must be a boolean') + expect(subject.errors[:enforce_route_policies]).to include('must be a boolean') end end @@ -419,7 +419,7 @@ module VCAP::CloudController it 'is not valid' do expect(subject).not_to be_valid - expect(subject.errors[:access_rules_scope]).to include('is required when enforce_access_rules is true') + expect(subject.errors[:route_policies_scope]).to include('is required when enforce_access_rules is true') end end @@ -448,13 +448,13 @@ module VCAP::CloudController end end - context 'access_rules_scope' do + context 'route_policies_scope' do context 'when set to an invalid value' do let(:params) { { name: 'name.com', enforce_access_rules: true, access_rules_scope: 'invalid' } } it 'is not valid' do expect(subject).not_to be_valid - expect(subject.errors[:access_rules_scope]).to include("must be one of 'any', 'org', 'space'") + expect(subject.errors[:route_policies_scope]).to include("must be one of 'any', 'org', 'space'") end end diff --git a/spec/unit/messages/isolation_segments_list_message_spec.rb b/spec/unit/messages/isolation_segments_list_message_spec.rb index 38ba6a7679f..5160108ef5a 100644 --- a/spec/unit/messages/isolation_segments_list_message_spec.rb +++ b/spec/unit/messages/isolation_segments_list_message_spec.rb @@ -52,7 +52,7 @@ module VCAP::CloudController names: %w[name1 name2], guids: %w[guid1 guid2], organization_guids: %w[o-guid1 o-guid2], - label_selector: 'foo=bar', + label_source: 'foo=bar', page: 1, per_page: 5, order_by: 'created_at', @@ -81,7 +81,7 @@ module VCAP::CloudController names: [], guids: [], organization_guids: [], - label_selector: '', + label_source: '', page: 1, per_page: 5, order_by: 'created_at' diff --git a/spec/unit/messages/list_message_spec.rb b/spec/unit/messages/list_message_spec.rb index 2f48b716800..3202829f105 100644 --- a/spec/unit/messages/list_message_spec.rb +++ b/spec/unit/messages/list_message_spec.rb @@ -289,7 +289,7 @@ def self.from_params(params) end it 'handles ruby symbols' do - message = list_message_klass.from_params(label_selector: 'example.com/foo==bar') + message = list_message_klass.from_params(label_source: 'example.com/foo==bar') expect(message.requirements.first.key).to eq('example.com/foo') end end diff --git a/spec/unit/messages/packages_list_message_spec.rb b/spec/unit/messages/packages_list_message_spec.rb index c3ef1fa369e..2389e16b35d 100644 --- a/spec/unit/messages/packages_list_message_spec.rb +++ b/spec/unit/messages/packages_list_message_spec.rb @@ -57,7 +57,7 @@ module VCAP::CloudController app_guids: %w[appguid1 appguid2], organization_guids: %w[organizationguid1 organizationguid2], app_guid: 'appguid', - label_selector: 'key=value', + label_source: 'key=value', page: 1, per_page: 5 } diff --git a/spec/unit/messages/processes_list_message_spec.rb b/spec/unit/messages/processes_list_message_spec.rb index 4346e772dc6..09544964c01 100644 --- a/spec/unit/messages/processes_list_message_spec.rb +++ b/spec/unit/messages/processes_list_message_spec.rb @@ -67,7 +67,7 @@ module VCAP::CloudController app_guid: 'appguid', embed: 'process_instances', page: 1, - label_selector: 'key=value', + label_source: 'key=value', per_page: 5, order_by: 'created_at', created_ats: [Time.now.utc.iso8601, Time.now.utc.iso8601], diff --git a/spec/unit/messages/access_rules_list_message_spec.rb b/spec/unit/messages/route_policies_list_message_spec.rb similarity index 69% rename from spec/unit/messages/access_rules_list_message_spec.rb rename to spec/unit/messages/route_policies_list_message_spec.rb index 4790229787e..30c6baacb5e 100644 --- a/spec/unit/messages/access_rules_list_message_spec.rb +++ b/spec/unit/messages/route_policies_list_message_spec.rb @@ -1,16 +1,16 @@ require 'spec_helper' -require 'messages/access_rules_list_message' +require 'messages/route_policies_list_message' module VCAP::CloudController - RSpec.describe AccessRulesListMessage do + RSpec.describe RoutePoliciesListMessage do describe '.from_params' do let(:params) do { 'guids' => 'guid1,guid2', 'route_guids' => 'route1,route2', 'space_guids' => 'space1,space2', - 'selectors' => 'selector1,selector2', - 'selector_resource_guids' => 'resource1,resource2', + 'sources' => 'selector1,selector2', + 'source_guids' => 'resource1,resource2', 'page' => 1, 'per_page' => 5, 'order_by' => 'created_at', @@ -18,10 +18,10 @@ module VCAP::CloudController } end - it 'returns the correct AccessRulesListMessage' do - message = AccessRulesListMessage.from_params(params) + it 'returns the correct RoutePoliciesListMessage' do + message = RoutePoliciesListMessage.from_params(params) - expect(message).to be_a(AccessRulesListMessage) + expect(message).to be_a(RoutePoliciesListMessage) expect(message.guids).to eq(%w[guid1 guid2]) expect(message.route_guids).to eq(%w[route1 route2]) expect(message.space_guids).to eq(%w[space1 space2]) @@ -34,13 +34,13 @@ module VCAP::CloudController end it 'converts requested keys to symbols' do - message = AccessRulesListMessage.from_params(params) + message = RoutePoliciesListMessage.from_params(params) expect(message).to be_requested(:guids) expect(message).to be_requested(:route_guids) expect(message).to be_requested(:space_guids) - expect(message).to be_requested(:selectors) - expect(message).to be_requested(:selector_resource_guids) + expect(message).to be_requested(:sources) + expect(message).to be_requested(:source_guids) expect(message).to be_requested(:page) expect(message).to be_requested(:per_page) expect(message).to be_requested(:order_by) @@ -65,14 +65,14 @@ module VCAP::CloudController it 'excludes the pagination keys' do expected_params = %i[guids route_guids space_guids selectors selector_resource_guids include] - expect(AccessRulesListMessage.from_params(opts).to_param_hash.keys).to match_array(expected_params) + expect(RoutePoliciesListMessage.from_params(opts).to_param_hash.keys).to match_array(expected_params) end end describe 'fields' do it 'accepts a set of fields' do expect do - AccessRulesListMessage.from_params({ + RoutePoliciesListMessage.from_params({ guids: [], route_guids: [], space_guids: [], @@ -87,12 +87,12 @@ module VCAP::CloudController end it 'accepts an empty set' do - message = AccessRulesListMessage.from_params({}) + message = RoutePoliciesListMessage.from_params({}) expect(message).to be_valid end it 'does not accept a field not in this set' do - message = AccessRulesListMessage.from_params({ foobar: 'pants' }) + message = RoutePoliciesListMessage.from_params({ foobar: 'pants' }) expect(message).not_to be_valid expect(message.errors[:base][0]).to include("Unknown query parameter(s): 'foobar'") @@ -100,64 +100,64 @@ module VCAP::CloudController describe 'include validations' do it 'accepts valid include values' do - message = AccessRulesListMessage.from_params({ 'include' => 'selector_resource' }) + message = RoutePoliciesListMessage.from_params({ 'include' => 'source' }) expect(message).to be_valid - message = AccessRulesListMessage.from_params({ 'include' => 'route' }) + message = RoutePoliciesListMessage.from_params({ 'include' => 'route' }) expect(message).to be_valid - message = AccessRulesListMessage.from_params({ 'include' => 'app' }) + message = RoutePoliciesListMessage.from_params({ 'include' => 'app' }) expect(message).to be_valid - message = AccessRulesListMessage.from_params({ 'include' => 'space' }) + message = RoutePoliciesListMessage.from_params({ 'include' => 'space' }) expect(message).to be_valid - message = AccessRulesListMessage.from_params({ 'include' => 'organization' }) + message = RoutePoliciesListMessage.from_params({ 'include' => 'organization' }) expect(message).to be_valid - message = AccessRulesListMessage.from_params({ 'include' => 'selector_resource,route,app,space,organization' }) + message = RoutePoliciesListMessage.from_params({ 'include' => 'selector_resource,route,app,space,organization' }) expect(message).to be_valid end it 'rejects invalid include values' do - message = AccessRulesListMessage.from_params({ 'include' => 'invalid' }) + message = RoutePoliciesListMessage.from_params({ 'include' => 'invalid' }) expect(message).not_to be_valid end end describe 'validations' do it 'validates space_guids is an array' do - message = AccessRulesListMessage.from_params space_guids: 'not array' + message = RoutePoliciesListMessage.from_params space_guids: 'not array' expect(message).not_to be_valid expect(message.errors[:space_guids].length).to eq 1 end it 'allows space_guids to be nil' do - message = AccessRulesListMessage.from_params({}) + message = RoutePoliciesListMessage.from_params({}) expect(message).to be_valid expect(message.space_guids).to be_nil end it 'allows space_guids to be an array' do - message = AccessRulesListMessage.from_params space_guids: %w[space1 space2] + message = RoutePoliciesListMessage.from_params space_guids: %w[space1 space2] expect(message).to be_valid expect(message.space_guids).to eq(%w[space1 space2]) end it 'validates selector_resource_guids is an array' do - message = AccessRulesListMessage.from_params selector_resource_guids: 'not array' + message = RoutePoliciesListMessage.from_params selector_resource_guids: 'not array' expect(message).not_to be_valid - expect(message.errors[:selector_resource_guids].length).to eq 1 + expect(message.errors[:source_guids].length).to eq 1 end it 'allows selector_resource_guids to be nil' do - message = AccessRulesListMessage.from_params({}) + message = RoutePoliciesListMessage.from_params({}) expect(message).to be_valid expect(message.selector_resource_guids).to be_nil end it 'allows selector_resource_guids to be an array' do - message = AccessRulesListMessage.from_params selector_resource_guids: %w[guid1 guid2] + message = RoutePoliciesListMessage.from_params selector_resource_guids: %w[guid1 guid2] expect(message).to be_valid expect(message.selector_resource_guids).to eq(%w[guid1 guid2]) end diff --git a/spec/unit/messages/access_rule_create_message_spec.rb b/spec/unit/messages/route_policy_create_message_spec.rb similarity index 81% rename from spec/unit/messages/access_rule_create_message_spec.rb rename to spec/unit/messages/route_policy_create_message_spec.rb index 408d57840d6..d6fa10f5ad4 100644 --- a/spec/unit/messages/access_rule_create_message_spec.rb +++ b/spec/unit/messages/route_policy_create_message_spec.rb @@ -2,19 +2,19 @@ require 'messages/access_rule_create_message' module VCAP::CloudController - RSpec.describe AccessRuleCreateMessage do + RSpec.describe RoutePolicyCreateMessage do let(:valid_uuid) { '11111111-2222-3333-4444-555555555555' } let(:valid_route_relationship) do { relationships: { route: { data: { guid: valid_uuid } } } } end - subject { AccessRuleCreateMessage.new(params) } + subject { RoutePolicyCreateMessage.new(params) } describe 'validations' do context 'when all valid params are given' do let(:params) do { - selector: "cf:app:#{valid_uuid}" + source: "cf:app:#{valid_uuid}" }.merge(valid_route_relationship) end @@ -26,7 +26,7 @@ module VCAP::CloudController context 'when unexpected keys are provided' do let(:params) do { - selector: "cf:app:#{valid_uuid}", + source: "cf:app:#{valid_uuid}", unexpected: 'field' }.merge(valid_route_relationship) end @@ -37,7 +37,7 @@ module VCAP::CloudController end end - describe 'selector' do + describe 'source' do context 'when selector is missing' do let(:params) do valid_route_relationship @@ -45,20 +45,20 @@ module VCAP::CloudController it 'is not valid' do expect(subject).not_to be_valid - expect(subject.errors[:selector]).to include("can't be blank") + expect(subject.errors[:source]).to include("can't be blank") end end context 'when selector is not a string' do let(:params) do { - selector: 123 + source: 123 }.merge(valid_route_relationship) end it 'is not valid' do expect(subject).not_to be_valid - expect(subject.errors[:selector]).to include('must be a string') + expect(subject.errors[:source]).to include('must be a string') end end @@ -66,7 +66,7 @@ module VCAP::CloudController context 'cf:app:' do let(:params) do { - selector: "cf:app:#{valid_uuid}" + source: "cf:app:#{valid_uuid}" }.merge(valid_route_relationship) end @@ -78,7 +78,7 @@ module VCAP::CloudController context 'cf:space:' do let(:params) do { - selector: "cf:space:#{valid_uuid}" + source: "cf:space:#{valid_uuid}" }.merge(valid_route_relationship) end @@ -90,7 +90,7 @@ module VCAP::CloudController context 'cf:org:' do let(:params) do { - selector: "cf:org:#{valid_uuid}" + source: "cf:org:#{valid_uuid}" }.merge(valid_route_relationship) end @@ -102,7 +102,7 @@ module VCAP::CloudController context 'cf:any' do let(:params) do { - selector: 'cf:any' + source: 'cf:any' }.merge(valid_route_relationship) end @@ -114,13 +114,13 @@ module VCAP::CloudController context 'invalid format' do let(:params) do { - selector: 'not-valid' + source: 'not-valid' }.merge(valid_route_relationship) end it 'is not valid' do expect(subject).not_to be_valid - expect(subject.errors[:selector]).to include( + expect(subject.errors[:source]).to include( "must be in format 'cf:app:', 'cf:space:', 'cf:org:', or 'cf:any'" ) end @@ -129,13 +129,13 @@ module VCAP::CloudController context 'cf:app: with invalid uuid' do let(:params) do { - selector: 'cf:app:not-a-uuid' + source: 'cf:app:not-a-uuid' }.merge(valid_route_relationship) end it 'is not valid' do expect(subject).not_to be_valid - expect(subject.errors[:selector]).to include( + expect(subject.errors[:source]).to include( "must be in format 'cf:app:', 'cf:space:', 'cf:org:', or 'cf:any'" ) end @@ -144,13 +144,13 @@ module VCAP::CloudController context 'cf:unknown type' do let(:params) do { - selector: "cf:team:#{valid_uuid}" + source: "cf:team:#{valid_uuid}" }.merge(valid_route_relationship) end it 'is not valid' do expect(subject).not_to be_valid - expect(subject.errors[:selector]).to include( + expect(subject.errors[:source]).to include( "must be in format 'cf:app:', 'cf:space:', 'cf:org:', or 'cf:any'" ) end @@ -162,7 +162,7 @@ module VCAP::CloudController context 'when relationships is missing' do let(:params) do { - selector: "cf:app:#{valid_uuid}" + source: "cf:app:#{valid_uuid}" } end @@ -175,7 +175,7 @@ module VCAP::CloudController context 'when route relationship is missing' do let(:params) do { - selector: "cf:app:#{valid_uuid}", + source: "cf:app:#{valid_uuid}", relationships: {} } end @@ -188,7 +188,7 @@ module VCAP::CloudController context 'when route guid is provided' do let(:params) do { - selector: "cf:app:#{valid_uuid}", + source: "cf:app:#{valid_uuid}", relationships: { route: { data: { guid: 'some-route-guid' } } } } end diff --git a/spec/unit/messages/tasks_list_message_spec.rb b/spec/unit/messages/tasks_list_message_spec.rb index 32177588787..f5c4950d22b 100644 --- a/spec/unit/messages/tasks_list_message_spec.rb +++ b/spec/unit/messages/tasks_list_message_spec.rb @@ -64,7 +64,7 @@ module VCAP::CloudController organization_guids: %w[orgguid1 orgguid2], space_guids: %w[spaceguid1 spaceguid2], sequence_ids: ['1, 2'], - label_selector: 'unicycling=fred', + label_source: 'unicycling=fred', page: 1, per_page: 5 } diff --git a/spec/unit/models/runtime/route_access_rule_spec.rb b/spec/unit/models/runtime/route_policy_spec.rb similarity index 69% rename from spec/unit/models/runtime/route_access_rule_spec.rb rename to spec/unit/models/runtime/route_policy_spec.rb index 687845c2207..2dfbc9520dc 100644 --- a/spec/unit/models/runtime/route_access_rule_spec.rb +++ b/spec/unit/models/runtime/route_policy_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' module VCAP::CloudController - RSpec.describe RouteAccessRule, type: :model do + RSpec.describe RoutePolicy, type: :model do let(:space) { Space.make } let(:domain) { SharedDomain.make(name: 'apps.identity') } let(:route) { Route.make(space:, domain:) } @@ -17,13 +17,13 @@ module VCAP::CloudController describe 'validations' do it 'requires a selector' do - rule = RouteAccessRule.new(route:) + rule = RoutePolicy.new(route:) expect(rule.valid?).to be false - expect(rule.errors[:selector]).to include(:presence) + expect(rule.errors[:source]).to include(:presence) end it 'requires a route_id' do - rule = RouteAccessRule.new(selector: 'cf:app:123') + rule = RoutePolicy.new(source: 'cf:app:123') expect(rule.valid?).to be false expect(rule.errors[:route_id]).to include(:presence) end @@ -31,8 +31,8 @@ module VCAP::CloudController describe 'associations' do it 'belongs to a route' do - rule = RouteAccessRule.create( - selector: 'cf:app:123', + rule = RoutePolicy.create( + source: 'cf:app:123', route: route ) expect(rule.route).to eq(route) @@ -42,10 +42,10 @@ module VCAP::CloudController describe 'callbacks' do describe 'after_create' do it 'calls touch_associated_processes' do - expect_any_instance_of(RouteAccessRule).to receive(:touch_associated_processes).and_call_original + expect_any_instance_of(RoutePolicy).to receive(:touch_associated_processes).and_call_original - RouteAccessRule.create( - selector: "cf:app:#{app_guid}", + RoutePolicy.create( + source: "cf:app:#{app_guid}", route: route ) end @@ -54,8 +54,8 @@ module VCAP::CloudController process # force creation # Record the SQL update queries to verify the process row is updated - RouteAccessRule.create( - selector: "cf:app:#{app_guid}", + RoutePolicy.create( + source: "cf:app:#{app_guid}", route: route ) @@ -67,8 +67,8 @@ module VCAP::CloudController route_without_processes = Route.make(space:, domain:) expect do - RouteAccessRule.create( - selector: "cf:app:#{app_guid}", + RoutePolicy.create( + source: "cf:app:#{app_guid}", route: route_without_processes ) end.not_to raise_error @@ -77,20 +77,20 @@ module VCAP::CloudController describe 'after_destroy' do it 'calls touch_associated_processes' do - rule = RouteAccessRule.create( - selector: "cf:app:#{app_guid}", + rule = RoutePolicy.create( + source: "cf:app:#{app_guid}", route: route ) - expect_any_instance_of(RouteAccessRule).to receive(:touch_associated_processes).and_call_original + expect_any_instance_of(RoutePolicy).to receive(:touch_associated_processes).and_call_original rule.destroy end it 'does not fail if route has no associated processes' do route_without_processes = Route.make(space:, domain:) - rule = RouteAccessRule.create( - selector: "cf:app:#{app_guid}", + rule = RoutePolicy.create( + source: "cf:app:#{app_guid}", route: route_without_processes ) diff --git a/spec/unit/presenters/v3/domain_presenter_spec.rb b/spec/unit/presenters/v3/domain_presenter_spec.rb index 998c4c1218f..82ce5e79d84 100644 --- a/spec/unit/presenters/v3/domain_presenter_spec.rb +++ b/spec/unit/presenters/v3/domain_presenter_spec.rb @@ -250,8 +250,8 @@ module VCAP::CloudController::Presenters::V3 end it 'includes enforce_access_rules and access_rules_scope in the output' do - expect(subject[:enforce_access_rules]).to be(true) - expect(subject[:access_rules_scope]).to eq('space') + expect(subject[:enforce_route_policies]).to be(true) + expect(subject[:route_policies_scope]).to eq('space') end end @@ -270,8 +270,8 @@ module VCAP::CloudController::Presenters::V3 end it 'does not include enforce_access_rules or access_rules_scope in the output' do - expect(subject).not_to have_key(:enforce_access_rules) - expect(subject).not_to have_key(:access_rules_scope) + expect(subject).not_to have_key(:enforce_route_policies) + expect(subject).not_to have_key(:route_policies_scope) end end diff --git a/spec/unit/presenters/v3/route_presenter_spec.rb b/spec/unit/presenters/v3/route_presenter_spec.rb index 3c78892c26e..b30378fd962 100644 --- a/spec/unit/presenters/v3/route_presenter_spec.rb +++ b/spec/unit/presenters/v3/route_presenter_spec.rb @@ -154,7 +154,7 @@ module VCAP::CloudController::Presenters::V3 path: path, space: space, domain: domain, - options: { 'access_scope' => 'space', 'access_rules' => 'cf:app:some-guid' } + options: { 'route_policy_scope' => 'space', 'route_policy_sources' => 'cf:app:some-guid' } ) end @@ -172,16 +172,16 @@ module VCAP::CloudController::Presenters::V3 domain: domain, options: { 'loadbalancing' => 'round-robin', - 'access_scope' => 'space', - 'access_rules' => 'cf:app:some-guid' + 'route_policy_scope' => 'space', + 'route_policy_sources' => 'cf:app:some-guid' } ) end it 'exposes only the public options' do expect(subject[:options]).to eq('loadbalancing' => 'round-robin') - expect(subject[:options]).not_to have_key('access_scope') - expect(subject[:options]).not_to have_key('access_rules') + expect(subject[:options]).not_to have_key('route_policy_scope') + expect(subject[:options]).not_to have_key('route_policy_sources') end end From 50c42ed90cc49842070f4bef120d3197dccc1892 Mon Sep 17 00:00:00 2001 From: rkoster Date: Tue, 21 Apr 2026 08:30:34 +0000 Subject: [PATCH 25/29] Fix test failures: complete terminology rebrand in specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix require statement in route_policy_create_message_spec.rb - Update all test references from old to new terminology: * selectors → sources * selector_resource_guids → source_guids * selector_resource → source (in include parameters) * enforce_access_rules → enforce_route_policies * access_rules_scope → route_policies_scope * route_access_rules → route_policies (table name) - Fix Rubocop indentation in route_policies_list_message_spec.rb Addresses CI/CD test failures in PR #4910. --- spec/request/route_policies_spec.rb | 48 ++++++++--------- .../route_policies_list_message_spec.rb | 54 +++++++++---------- .../route_policy_create_message_spec.rb | 2 +- .../presenters/v3/domain_presenter_spec.rb | 12 ++--- 4 files changed, 58 insertions(+), 58 deletions(-) diff --git a/spec/request/route_policies_spec.rb b/spec/request/route_policies_spec.rb index 4582c0ba13c..bfce52e18d8 100644 --- a/spec/request/route_policies_spec.rb +++ b/spec/request/route_policies_spec.rb @@ -9,8 +9,8 @@ let(:mtls_domain) do VCAP::CloudController::PrivateDomain.make( owning_organization: org, - enforce_access_rules: true, - access_rules_scope: 'space' + enforce_route_policies: true, + route_policies_scope: 'space' ) end let(:regular_domain) do @@ -82,7 +82,7 @@ def expected_rule_json(rule) end end - context 'when the domain does not have enforce_access_rules enabled' do + context 'when the domain does not have enforce_route_policies enabled' do let(:request_body) do { source: "cf:app:#{valid_uuid}", @@ -212,7 +212,7 @@ def expected_rule_json(rule) # Simulate a race condition where the DB unique constraint catches the duplicate # after validation passes but before the insert commits allow_any_instance_of(VCAP::CloudController::RoutePolicy).to receive(:save).and_raise( - Sequel::UniqueConstraintViolation.new('UNIQUE constraint failed: route_access_rules.route_id, route_access_rules.source') + Sequel::UniqueConstraintViolation.new('UNIQUE constraint failed: route_policies.route_id, route_policies.source') ) post '/v3/route_policies', { @@ -236,11 +236,11 @@ def expected_rule_json(rule) end it 'returns the access rule' do - get "/v3/route_policies/#{access_rule.guid}", nil, admin_header + get "/v3/route_policies/#{route_policy.guid}", nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) - expect(parsed['guid']).to eq(access_rule.guid) + expect(parsed['guid']).to eq(route_policy.guid) expect(parsed['source']).to eq("cf:app:#{valid_uuid}") end @@ -303,8 +303,8 @@ def expected_rule_json(rule) let(:other_mtls_domain) do VCAP::CloudController::PrivateDomain.make( owning_organization: other_org, - enforce_access_rules: true, - access_rules_scope: 'space' + enforce_route_policies: true, + route_policies_scope: 'space' ) end let(:other_route) { VCAP::CloudController::Route.make(space: other_space, domain: other_mtls_domain) } @@ -369,8 +369,8 @@ def expected_rule_json(rule) let(:other_mtls_domain) do VCAP::CloudController::PrivateDomain.make( owning_organization: other_org, - enforce_access_rules: true, - access_rules_scope: 'space' + enforce_route_policies: true, + route_policies_scope: 'space' ) end let(:other_route) { VCAP::CloudController::Route.make(space: other_space, domain: other_mtls_domain) } @@ -398,9 +398,9 @@ def expected_rule_json(rule) end end - describe 'filtering by selector_resource_guids' do + describe 'filtering by source_guids' do it 'escapes % so it does not act as a LIKE wildcard' do - get '/v3/route_policies?selector_resource_guids=%25', nil, admin_header + get '/v3/route_policies?source_guids=%25', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -408,7 +408,7 @@ def expected_rule_json(rule) end it 'escapes _ so it does not act as a LIKE single-char wildcard' do - get '/v3/route_policies?selector_resource_guids=cf_app', nil, admin_header + get '/v3/route_policies?source_guids=cf_app', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -417,7 +417,7 @@ def expected_rule_json(rule) end it 'escapes backslash so it does not act as a LIKE escape character' do - get '/v3/route_policies?selector_resource_guids=cf%5Capp', nil, admin_header + get '/v3/route_policies?source_guids=cf%5Capp', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -425,7 +425,7 @@ def expected_rule_json(rule) end end - context 'with include=selector_resource' do + context 'with include=source' do let!(:frontend_app) { VCAP::CloudController::AppModel.make(space: space, name: 'frontend-app') } let!(:other_space) { VCAP::CloudController::Space.make(organization: org, name: 'other-space') } let!(:other_org) { VCAP::CloudController::Organization.make(name: 'other-org') } @@ -455,7 +455,7 @@ def expected_rule_json(rule) end it 'includes resolved selector resources' do - get '/v3/route_policies?include=selector_resource', nil, admin_header + get '/v3/route_policies?include=source', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -491,7 +491,7 @@ def expected_rule_json(rule) route_id: mtls_route.id ) - get '/v3/route_policies?include=selector_resource', nil, admin_header + get '/v3/route_policies?include=source', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -509,7 +509,7 @@ def expected_rule_json(rule) route_id: VCAP::CloudController::Route.make(space: space, domain: mtls_domain).id ) - get '/v3/route_policies?include=selector_resource', nil, admin_header + get '/v3/route_policies?include=source', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -526,7 +526,7 @@ def expected_rule_json(rule) route_id: VCAP::CloudController::Route.make(space: space, domain: mtls_domain).id ) - get '/v3/route_policies?include=selector_resource', nil, admin_header + get '/v3/route_policies?include=source', nil, admin_header expect(last_response.status).to eq(200) # Should succeed without error even with cf:any selector @@ -592,7 +592,7 @@ def expected_rule_json(rule) expect(route_count).to eq(1) end - it 'combines include=route with include=selector_resource' do + it 'combines include=route with include=source' do test_app = VCAP::CloudController::AppModel.make(space: space, name: 'test-app') VCAP::CloudController::RoutePolicy.create( guid: SecureRandom.uuid, @@ -600,7 +600,7 @@ def expected_rule_json(rule) route_id: mtls_route.id ) - get '/v3/route_policies?include=route,selector_resource', nil, admin_header + get '/v3/route_policies?include=route,source', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -630,10 +630,10 @@ def expected_rule_json(rule) end it 'deletes the access rule and returns 204' do - delete "/v3/route_policies/#{access_rule.guid}", nil, admin_header + delete "/v3/route_policies/#{route_policy.guid}", nil, admin_header expect(last_response.status).to eq(204) - expect(VCAP::CloudController::RoutePolicy.find(guid: access_rule.guid)).to be_nil + expect(VCAP::CloudController::RoutePolicy.find(guid: route_policy.guid)).to be_nil end context 'when the access rule does not exist' do @@ -655,7 +655,7 @@ def expected_rule_json(rule) end it 'returns 200' do - patch "/v3/route_policies/#{access_rule.guid}", { + patch "/v3/route_policies/#{route_policy.guid}", { metadata: { labels: { env: 'production' } } }.to_json, admin_header diff --git a/spec/unit/messages/route_policies_list_message_spec.rb b/spec/unit/messages/route_policies_list_message_spec.rb index 30c6baacb5e..0a502105db7 100644 --- a/spec/unit/messages/route_policies_list_message_spec.rb +++ b/spec/unit/messages/route_policies_list_message_spec.rb @@ -9,12 +9,12 @@ module VCAP::CloudController 'guids' => 'guid1,guid2', 'route_guids' => 'route1,route2', 'space_guids' => 'space1,space2', - 'sources' => 'selector1,selector2', + 'sources' => 'source1,source2', 'source_guids' => 'resource1,resource2', 'page' => 1, 'per_page' => 5, 'order_by' => 'created_at', - 'include' => 'selector_resource,route,app,space,organization' + 'include' => 'source,route,app,space,organization' } end @@ -25,12 +25,12 @@ module VCAP::CloudController expect(message.guids).to eq(%w[guid1 guid2]) expect(message.route_guids).to eq(%w[route1 route2]) expect(message.space_guids).to eq(%w[space1 space2]) - expect(message.selectors).to eq(%w[selector1 selector2]) - expect(message.selector_resource_guids).to eq(%w[resource1 resource2]) + expect(message.sources).to eq(%w[source1 source2]) + expect(message.source_guids).to eq(%w[resource1 resource2]) expect(message.page).to eq(1) expect(message.per_page).to eq(5) expect(message.order_by).to eq('created_at') - expect(message.include).to eq(%w[selector_resource route app space organization]) + expect(message.include).to eq(%w[source route app space organization]) end it 'converts requested keys to symbols' do @@ -54,17 +54,17 @@ module VCAP::CloudController guids: %w[guid1 guid2], route_guids: %w[route1 route2], space_guids: %w[space1 space2], - selectors: %w[selector1 selector2], - selector_resource_guids: %w[resource1 resource2], + sources: %w[source1 source2], + source_guids: %w[resource1 resource2], page: 1, per_page: 5, order_by: 'created_at', - include: %w[selector_resource route app space organization] + include: %w[source route app space organization] } end it 'excludes the pagination keys' do - expected_params = %i[guids route_guids space_guids selectors selector_resource_guids include] + expected_params = %i[guids route_guids space_guids sources source_guids include] expect(RoutePoliciesListMessage.from_params(opts).to_param_hash.keys).to match_array(expected_params) end end @@ -73,16 +73,16 @@ module VCAP::CloudController it 'accepts a set of fields' do expect do RoutePoliciesListMessage.from_params({ - guids: [], - route_guids: [], - space_guids: [], - selectors: [], - selector_resource_guids: [], - page: 1, - per_page: 5, - order_by: 'created_at', - include: %w[selector_resource route app space organization] - }) + guids: [], + route_guids: [], + space_guids: [], + sources: [], + source_guids: [], + page: 1, + per_page: 5, + order_by: 'created_at', + include: %w[source route app space organization] + }) end.not_to raise_error end @@ -115,7 +115,7 @@ module VCAP::CloudController message = RoutePoliciesListMessage.from_params({ 'include' => 'organization' }) expect(message).to be_valid - message = RoutePoliciesListMessage.from_params({ 'include' => 'selector_resource,route,app,space,organization' }) + message = RoutePoliciesListMessage.from_params({ 'include' => 'source,route,app,space,organization' }) expect(message).to be_valid end @@ -144,22 +144,22 @@ module VCAP::CloudController expect(message.space_guids).to eq(%w[space1 space2]) end - it 'validates selector_resource_guids is an array' do - message = RoutePoliciesListMessage.from_params selector_resource_guids: 'not array' + it 'validates source_guids is an array' do + message = RoutePoliciesListMessage.from_params source_guids: 'not array' expect(message).not_to be_valid expect(message.errors[:source_guids].length).to eq 1 end - it 'allows selector_resource_guids to be nil' do + it 'allows source_guids to be nil' do message = RoutePoliciesListMessage.from_params({}) expect(message).to be_valid - expect(message.selector_resource_guids).to be_nil + expect(message.source_guids).to be_nil end - it 'allows selector_resource_guids to be an array' do - message = RoutePoliciesListMessage.from_params selector_resource_guids: %w[guid1 guid2] + it 'allows source_guids to be an array' do + message = RoutePoliciesListMessage.from_params source_guids: %w[guid1 guid2] expect(message).to be_valid - expect(message.selector_resource_guids).to eq(%w[guid1 guid2]) + expect(message.source_guids).to eq(%w[guid1 guid2]) end end end diff --git a/spec/unit/messages/route_policy_create_message_spec.rb b/spec/unit/messages/route_policy_create_message_spec.rb index d6fa10f5ad4..23e9e852ca2 100644 --- a/spec/unit/messages/route_policy_create_message_spec.rb +++ b/spec/unit/messages/route_policy_create_message_spec.rb @@ -1,5 +1,5 @@ require 'spec_helper' -require 'messages/access_rule_create_message' +require 'messages/route_policy_create_message' module VCAP::CloudController RSpec.describe RoutePolicyCreateMessage do diff --git a/spec/unit/presenters/v3/domain_presenter_spec.rb b/spec/unit/presenters/v3/domain_presenter_spec.rb index 82ce5e79d84..390d13644d9 100644 --- a/spec/unit/presenters/v3/domain_presenter_spec.rb +++ b/spec/unit/presenters/v3/domain_presenter_spec.rb @@ -238,24 +238,24 @@ module VCAP::CloudController::Presenters::V3 end end - context 'when the domain has enforce_access_rules enabled' do + context 'when the domain has enforce_route_policies enabled' do let(:org) { VCAP::CloudController::Organization.make } let(:domain) do VCAP::CloudController::PrivateDomain.make( name: 'mtls.domain.com', owning_organization: org, - enforce_access_rules: true, - access_rules_scope: 'space' + enforce_route_policies: true, + route_policies_scope: 'space' ) end - it 'includes enforce_access_rules and access_rules_scope in the output' do + it 'includes enforce_route_policies and route_policies_scope in the output' do expect(subject[:enforce_route_policies]).to be(true) expect(subject[:route_policies_scope]).to eq('space') end end - context 'when the domain does not have enforce_access_rules enabled' do + context 'when the domain does not have enforce_route_policies enabled' do let(:domain) do VCAP::CloudController::SharedDomain.make( name: 'regular.domain.com' @@ -269,7 +269,7 @@ module VCAP::CloudController::Presenters::V3 allow(routing_api_client).to receive_messages(enabled?: true, router_group: nil) end - it 'does not include enforce_access_rules or access_rules_scope in the output' do + it 'does not include enforce_route_policies or route_policies_scope in the output' do expect(subject).not_to have_key(:enforce_route_policies) expect(subject).not_to have_key(:route_policies_scope) end From aa000db9d5b3790dfb4d77ac3c3c3d253fe2d752 Mon Sep 17 00:00:00 2001 From: rkoster Date: Tue, 21 Apr 2026 09:13:34 +0000 Subject: [PATCH 26/29] Fix routing_info_spec: use enforce_route_policies field names --- devbox.d/mysql80/my.cnf | 6 + devbox.json | 69 ++ devbox.lock | 783 ++++++++++++++++++ .../diego/protocol/routing_info_spec.rb | 6 +- 4 files changed, 861 insertions(+), 3 deletions(-) create mode 100644 devbox.d/mysql80/my.cnf create mode 100644 devbox.json create mode 100644 devbox.lock diff --git a/devbox.d/mysql80/my.cnf b/devbox.d/mysql80/my.cnf new file mode 100644 index 00000000000..a749c470084 --- /dev/null +++ b/devbox.d/mysql80/my.cnf @@ -0,0 +1,6 @@ +# MySQL configuration file + +# [mysqld] +# skip-log-bin +# Change this port if 3306 is already used +#port = 3306 diff --git a/devbox.json b/devbox.json new file mode 100644 index 00000000000..24f56d6ef28 --- /dev/null +++ b/devbox.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.16.0/.schema/devbox.schema.json", + "packages": [ + "ruby@3.3", + "bundler@latest", + "libpq@latest", + "openssl@latest", + "libyaml@latest", + "pkg-config@latest", + "zstd@latest", + "postgresql@latest" + ], + "shell": { + "init_hook": [ + "# Devbox installs only the default nix output (runtime libs). Native Ruby gem", + "# extensions need dev headers and pkg-config files from the -dev outputs.", + "# This hook reads -dev output paths from devbox.lock and adds them to the", + "# compiler search paths.", + "", + "LIBRARY_PATH=\"$DEVBOX_PACKAGES_DIR/lib${LIBRARY_PATH:+:$LIBRARY_PATH}\"", + "C_INCLUDE_PATH=\"$DEVBOX_PACKAGES_DIR/include${C_INCLUDE_PATH:+:$C_INCLUDE_PATH}\"", + "", + "_devbox_realize_dev_outputs() {", + " local lockfile=\"$PWD/devbox.lock\"", + " [ -f \"$lockfile\" ] || return", + " local arch=$(uname -m)", + " local os=$(uname -s | tr '[:upper:]' '[:lower:]')", + " local system=\"${arch}-${os}\"", + "", + " # Extract -dev and -out paths from devbox.lock using ruby (available in our shell)", + " local dev_paths", + " dev_paths=$(ruby -rjson -e '", + " lock = JSON.parse(File.read(ARGV[0]))", + " sys = ARGV[1]", + " lock.fetch(\"packages\", {}).each do |_, info|", + " outputs = info.dig(\"systems\", sys, \"outputs\") || []", + " outputs.each do |o|", + " # Include dev outputs, and also \"out\" outputs that contain includes", + " puts o[\"path\"] if o[\"name\"] == \"dev\" || o[\"name\"] == \"out\"", + " end", + " end", + " end", + " ' \"$lockfile\" \"$system\" 2>/dev/null)", + "", + " local p", + " for p in $dev_paths; do", + " # Realize (download) the store path if not already present", + " [ -d \"$p\" ] || nix-store --realise \"$p\" >/dev/null 2>&1 || continue", + " [ -d \"$p/include\" ] && C_INCLUDE_PATH=\"${p}/include:$C_INCLUDE_PATH\"", + " [ -d \"$p/lib/pkgconfig\" ] && PKG_CONFIG_PATH=\"${p}/lib/pkgconfig:${PKG_CONFIG_PATH:-}\"", + " [ -d \"$p/lib\" ] && LIBRARY_PATH=\"${p}/lib:$LIBRARY_PATH\"", + " done", + "}", + "", + "_devbox_realize_dev_outputs", + "export C_INCLUDE_PATH LIBRARY_PATH PKG_CONFIG_PATH", + "unset -f _devbox_realize_dev_outputs", + "", + "# Set database connection prefix for PostgreSQL tests", + "export POSTGRES_CONNECTION_PREFIX=\"postgres://postgres:supersecret@localhost:5432\"", + "export DB=postgres" + ], + "scripts": { + "test": [ + "echo \"Error: no test specified\" && exit 1" + ] + } + } +} diff --git a/devbox.lock b/devbox.lock new file mode 100644 index 00000000000..beb48f6567f --- /dev/null +++ b/devbox.lock @@ -0,0 +1,783 @@ +{ + "lockfile_version": "1", + "packages": { + "bundler@latest": { + "last_modified": "2026-03-21T07:29:51Z", + "resolved": "github:NixOS/nixpkgs/09061f748ee21f68a089cd5d91ec1859cd93d0be#bundler", + "source": "devbox-search", + "version": "2.7.2", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/gbx73y8di7f17i727k6s0l2f1618pza8-bundler-2.7.2", + "default": true + } + ], + "store_path": "/nix/store/gbx73y8di7f17i727k6s0l2f1618pza8-bundler-2.7.2" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/qb2ksb9khr8dpc46h2ajg85zirgz136k-bundler-2.7.2", + "default": true + } + ], + "store_path": "/nix/store/qb2ksb9khr8dpc46h2ajg85zirgz136k-bundler-2.7.2" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/b04xgfhjcgg1cl1h2bpjm3fds46vgd1w-bundler-2.7.2", + "default": true + } + ], + "store_path": "/nix/store/b04xgfhjcgg1cl1h2bpjm3fds46vgd1w-bundler-2.7.2" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/bc7ankvbv1s5shlllxxlcsgdj0b16p36-bundler-2.7.2", + "default": true + } + ], + "store_path": "/nix/store/bc7ankvbv1s5shlllxxlcsgdj0b16p36-bundler-2.7.2" + } + } + }, + "github:NixOS/nixpkgs/nixpkgs-unstable": { + "last_modified": "2026-03-16T02:27:38Z", + "resolved": "github:NixOS/nixpkgs/f8573b9c935cfaa162dd62cc9e75ae2db86f85df?lastModified=1773628058&narHash=sha256-hpXH0z3K9xv0fHaje136KY872VT2T5uwxtezlAskQgY%3D" + }, + "glibcLocales@latest": { + "last_modified": "2026-03-21T07:29:51Z", + "resolved": "github:NixOS/nixpkgs/09061f748ee21f68a089cd5d91ec1859cd93d0be#glibcLocales", + "source": "devbox-search", + "version": "2.42-51", + "systems": { + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/d4vnp0fbrsvijnx5ac86bbxg3bnblz7k-glibc-locales-2.42-51", + "default": true + } + ], + "store_path": "/nix/store/d4vnp0fbrsvijnx5ac86bbxg3bnblz7k-glibc-locales-2.42-51" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/ld2j2mq254m77hwy22pyvrs861ap1374-glibc-locales-2.42-51", + "default": true + } + ], + "store_path": "/nix/store/ld2j2mq254m77hwy22pyvrs861ap1374-glibc-locales-2.42-51" + } + } + }, + "libpq@latest": { + "last_modified": "2026-04-11T06:17:25Z", + "resolved": "github:NixOS/nixpkgs/13043924aaa7375ce482ebe2494338e058282925#libpq", + "source": "devbox-search", + "version": "18.2", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/yqv7xkfakqfccbgdmkf7xhkda2yzrqd8-libpq-18.2", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/sl9kw8cqc669py9xb83c1baf342l97r5-libpq-18.2-dev" + } + ], + "store_path": "/nix/store/yqv7xkfakqfccbgdmkf7xhkda2yzrqd8-libpq-18.2" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/4hpn6899j9vh3p9z424vzgqn4ya4lcvv-libpq-18.2", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/xjzx6272qsnbrgmbm3yw1xb3688p5sjb-libpq-18.2-debug" + }, + { + "name": "dev", + "path": "/nix/store/fyaw62ldhlyjcnbdli0y4a9wbrlg5q78-libpq-18.2-dev" + } + ], + "store_path": "/nix/store/4hpn6899j9vh3p9z424vzgqn4ya4lcvv-libpq-18.2" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/ig7ycilx8a3xal5dharihg0mk15yqwmv-libpq-18.2", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/dvgl05rjdbdk2ck90ccnb8g2hpyhmbbj-libpq-18.2-dev" + } + ], + "store_path": "/nix/store/ig7ycilx8a3xal5dharihg0mk15yqwmv-libpq-18.2" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/4fi462vs62ycv752lck8v3d70f3blh2x-libpq-18.2", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/gdmv8c5ax77873s7090b3wcicd6i4m51-libpq-18.2-dev" + }, + { + "name": "debug", + "path": "/nix/store/1ljsii50mrkvxnsvq123a9gnqj0cl8ng-libpq-18.2-debug" + } + ], + "store_path": "/nix/store/4fi462vs62ycv752lck8v3d70f3blh2x-libpq-18.2" + } + } + }, + "libyaml@latest": { + "last_modified": "2026-03-21T07:29:51Z", + "resolved": "github:NixOS/nixpkgs/09061f748ee21f68a089cd5d91ec1859cd93d0be#libyaml", + "source": "devbox-search", + "version": "0.2.5", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/qqa8q98n3hvb2kqz3xvd0m0j22033wy0-libyaml-0.2.5", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/00yv9nvsx0vswzzihkkl4qk39lb2p1pc-libyaml-0.2.5-dev" + } + ], + "store_path": "/nix/store/qqa8q98n3hvb2kqz3xvd0m0j22033wy0-libyaml-0.2.5" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/m4j56in3n01xw1jk5h1qxj5r8i4x2mfb-libyaml-0.2.5", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/jyvgsbxnppxyvvgga304iw6xlhi39r17-libyaml-0.2.5-dev" + } + ], + "store_path": "/nix/store/m4j56in3n01xw1jk5h1qxj5r8i4x2mfb-libyaml-0.2.5" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/4djwsl28pfclczbkcrdqxlqrcxyvihik-libyaml-0.2.5", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/6i8a2m6yj122s9r1nyl8grxizq3av6z6-libyaml-0.2.5-dev" + } + ], + "store_path": "/nix/store/4djwsl28pfclczbkcrdqxlqrcxyvihik-libyaml-0.2.5" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/fnhs945sa02bg7gki8a3l8r9r44ylx10-libyaml-0.2.5", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/v9qn9g4fm4818vx30kl7z423vj1mswml-libyaml-0.2.5-dev" + } + ], + "store_path": "/nix/store/fnhs945sa02bg7gki8a3l8r9r44ylx10-libyaml-0.2.5" + } + } + }, + "openssl@latest": { + "last_modified": "2025-12-05T06:24:47Z", + "resolved": "github:NixOS/nixpkgs/42e29df35be6ef54091d3a3b4e97056ce0a98ce8#openssl", + "source": "devbox-search", + "version": "3.6.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/z9prisxci5h5lsk3rdknd4jzq7k9q13d-openssl-3.6.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/ii9mnzr3i92mgk9dkgg65739mavd0j6f-openssl-3.6.0-man", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/h0qgqik0mk0wn7rmm2kk3grfi1wzly74-openssl-3.6.0-dev" + }, + { + "name": "doc", + "path": "/nix/store/yx3ip21fdaaxpjn5fbir02mqnaw9cm4f-openssl-3.6.0-doc" + }, + { + "name": "out", + "path": "/nix/store/3z54dgks2mz3dhwddj158sdibll8xmq5-openssl-3.6.0" + } + ], + "store_path": "/nix/store/z9prisxci5h5lsk3rdknd4jzq7k9q13d-openssl-3.6.0-bin" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/wb6q44n9kcb5acmaa4rgqsajadx1fhhl-openssl-3.6.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/c9n1alb7ypzjvzd47m16fiwfczz23qs3-openssl-3.6.0-man", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/ci6d4k1sj4bnr892lsrqqmjiihqsk0bl-openssl-3.6.0-debug" + }, + { + "name": "dev", + "path": "/nix/store/pq8b7fb3282g68pmk14mbyi20qn6chid-openssl-3.6.0-dev" + }, + { + "name": "doc", + "path": "/nix/store/vaplp6w56dyz38986bgkf0pbg3r486b2-openssl-3.6.0-doc" + }, + { + "name": "out", + "path": "/nix/store/nj50gkyx813dxvfmsg1q8m330hmf3h86-openssl-3.6.0" + } + ], + "store_path": "/nix/store/wb6q44n9kcb5acmaa4rgqsajadx1fhhl-openssl-3.6.0-bin" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/m3xwn9n0jypwjgi256idfzs979g30j29-openssl-3.6.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/hw43f3y1vl7ydrd4samnwnrwqqwkpisv-openssl-3.6.0-man", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/dirjrfjk8jgsbdpslgb51cav6qaxn2vm-openssl-3.6.0-dev" + }, + { + "name": "doc", + "path": "/nix/store/va1zhkz0nfmycvd0h239hi4w40qgaxcx-openssl-3.6.0-doc" + }, + { + "name": "out", + "path": "/nix/store/q9a4wssx24xsy28w8kifdqizc01fh7sc-openssl-3.6.0" + } + ], + "store_path": "/nix/store/m3xwn9n0jypwjgi256idfzs979g30j29-openssl-3.6.0-bin" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/k0gl1zc7f5hk87lylxwbipb0b870bcmk-openssl-3.6.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/a9jdl6xq9fc98ykpvqmc9kf0b0j9y8wh-openssl-3.6.0-man", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/sqv8kbdgfxlr2d6nysr8c2715qpsi6f5-openssl-3.6.0-debug" + }, + { + "name": "dev", + "path": "/nix/store/ydrckgnllgg8nmhdwni81h7xhcpnrlhd-openssl-3.6.0-dev" + }, + { + "name": "doc", + "path": "/nix/store/cgp9ig35iwicfb9spcrgyg2m5dmlcgrv-openssl-3.6.0-doc" + }, + { + "name": "out", + "path": "/nix/store/61i74yjkj9p1qphfl7018ja4sdwkipx0-openssl-3.6.0" + } + ], + "store_path": "/nix/store/k0gl1zc7f5hk87lylxwbipb0b870bcmk-openssl-3.6.0-bin" + } + } + }, + "pkg-config@latest": { + "last_modified": "2025-11-23T21:50:36Z", + "resolved": "github:NixOS/nixpkgs/ee09932cedcef15aaf476f9343d1dea2cb77e261#pkg-config", + "source": "devbox-search", + "version": "0.29.2", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/hygaaqwk9ylklp0ybwppqhw75nz8ya41-pkg-config-wrapper-0.29.2", + "default": true + }, + { + "name": "man", + "path": "/nix/store/9px0sji43x3r2w4zxl3j3idwsql7lwxx-pkg-config-wrapper-0.29.2-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/hqk44ra6qxw7iixardl6c3hdgb9kq6ns-pkg-config-wrapper-0.29.2-doc" + } + ], + "store_path": "/nix/store/hygaaqwk9ylklp0ybwppqhw75nz8ya41-pkg-config-wrapper-0.29.2" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/lbiigi8qbp7mzf1lpr7p982l1kyf01ql-pkg-config-wrapper-0.29.2", + "default": true + }, + { + "name": "man", + "path": "/nix/store/10060k24qggqyzlwdsfmni9y32zxcg0j-pkg-config-wrapper-0.29.2-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/0y4v51ndpyvkj09hwlfqkz0c3h17zfmc-pkg-config-wrapper-0.29.2-doc" + } + ], + "store_path": "/nix/store/lbiigi8qbp7mzf1lpr7p982l1kyf01ql-pkg-config-wrapper-0.29.2" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/vknadizq0q5kffvx6y4379p9gdry9zq3-pkg-config-wrapper-0.29.2", + "default": true + }, + { + "name": "man", + "path": "/nix/store/1nyspra675q22gfhf7hn2nmfpi6rgim5-pkg-config-wrapper-0.29.2-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/7lq1axxwrafwljs06n88bzyz9w523rkc-pkg-config-wrapper-0.29.2-doc" + } + ], + "store_path": "/nix/store/vknadizq0q5kffvx6y4379p9gdry9zq3-pkg-config-wrapper-0.29.2" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/8vdiwpbh0g4avsd6x5v4s0di32vcl3dp-pkg-config-wrapper-0.29.2", + "default": true + }, + { + "name": "man", + "path": "/nix/store/j9xfpnrygg3v37svc5pfin9q5bm49r94-pkg-config-wrapper-0.29.2-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/x3bypxdxaq20kykybhkf21x4jczsiy8y-pkg-config-wrapper-0.29.2-doc" + } + ], + "store_path": "/nix/store/8vdiwpbh0g4avsd6x5v4s0di32vcl3dp-pkg-config-wrapper-0.29.2" + } + } + }, + "postgresql@latest": { + "last_modified": "2026-04-11T06:17:25Z", + "plugin_version": "0.0.2", + "resolved": "github:NixOS/nixpkgs/13043924aaa7375ce482ebe2494338e058282925#postgresql", + "source": "devbox-search", + "version": "17.9", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/rcz8zc21n6rx4igsdgngmkwnln4f7dy5-postgresql-17.9", + "default": true + }, + { + "name": "man", + "path": "/nix/store/v9ad61kyx28sfzs48j9077iiv61fqzb0-postgresql-17.9-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/gillzna13al7axbhkqyjf7wwfkfbh4nn-postgresql-17.9-doc" + }, + { + "name": "jit", + "path": "/nix/store/9wrci7zgca8ygxgcg8qhk69kkk2hvnvg-postgresql-17.9-jit" + }, + { + "name": "lib", + "path": "/nix/store/h9xg40fr3hqn9lhckdf1sjp2w7zdl92n-postgresql-17.9-lib" + }, + { + "name": "dev", + "path": "/nix/store/yzvwbyh0gqrprnw5rdnhjmcmyvrl9ql4-postgresql-17.9-dev" + }, + { + "name": "plperl", + "path": "/nix/store/ywrc7vv5mdsz79z4nfid0asnzlwxp3zn-postgresql-17.9-plperl" + }, + { + "name": "plpython3", + "path": "/nix/store/jzj6b2zw28dxy8jjfvzlfbmdl8mypv2m-postgresql-17.9-plpython3" + }, + { + "name": "pltcl", + "path": "/nix/store/ya921lh5kkcrdgk09y9580prw5yg27f2-postgresql-17.9-pltcl" + } + ], + "store_path": "/nix/store/rcz8zc21n6rx4igsdgngmkwnln4f7dy5-postgresql-17.9" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/bw97pvf1fg3y7yjvw24g83448v8p48m0-postgresql-17.9", + "default": true + }, + { + "name": "man", + "path": "/nix/store/j22nri44hhgyxbg78glds0im2y608cn9-postgresql-17.9-man", + "default": true + }, + { + "name": "plpython3", + "path": "/nix/store/fha23nr7d2i16ns2z7wsrlx65fxpazxh-postgresql-17.9-plpython3" + }, + { + "name": "pltcl", + "path": "/nix/store/b9zsqpp7znmvxghjy9ihlk3p75xvd3pz-postgresql-17.9-pltcl" + }, + { + "name": "dev", + "path": "/nix/store/gvivc80vkanv4cd41r1fz0dz9qr2bsjq-postgresql-17.9-dev" + }, + { + "name": "doc", + "path": "/nix/store/39f586jzgzlkcc3dp8zajyjnf2w2mymr-postgresql-17.9-doc" + }, + { + "name": "jit", + "path": "/nix/store/iw7rjv0gjb23fwil2j0zjbghrj8bgd7q-postgresql-17.9-jit" + }, + { + "name": "plperl", + "path": "/nix/store/951fcy0jfrwz8rhi8668fqi72wwdj1qa-postgresql-17.9-plperl" + }, + { + "name": "debug", + "path": "/nix/store/rlk7xis3dfyll5z1fny70ksi3yqh1yy7-postgresql-17.9-debug" + }, + { + "name": "lib", + "path": "/nix/store/b25khikzni3m8q8nyv3mrxa5v63bqsam-postgresql-17.9-lib" + } + ], + "store_path": "/nix/store/bw97pvf1fg3y7yjvw24g83448v8p48m0-postgresql-17.9" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/jgrfz7nzjh9m5ymfv0831aamk9ci5ds9-postgresql-17.9", + "default": true + }, + { + "name": "man", + "path": "/nix/store/q824ybxz07qzwrwk9hkd16y0yl7mlp5i-postgresql-17.9-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/87c2fid7ppzyd3n3i5id3iiipybgzcp7-postgresql-17.9-doc" + }, + { + "name": "plperl", + "path": "/nix/store/p51i9h8vwml5nj6i91g0hh2zh93c4iap-postgresql-17.9-plperl" + }, + { + "name": "plpython3", + "path": "/nix/store/6a5lqzcdxiqn5nqlfddjdb921z7a35in-postgresql-17.9-plpython3" + }, + { + "name": "dev", + "path": "/nix/store/p99q8ixd6kkw2fr8zpfsmc0m3gwqcjjw-postgresql-17.9-dev" + }, + { + "name": "jit", + "path": "/nix/store/f6qm2151lg98kmayd1kddmgqv9wh1m4f-postgresql-17.9-jit" + }, + { + "name": "lib", + "path": "/nix/store/shz3ms0ww02df1k2qrzk0mv3g6ilr33j-postgresql-17.9-lib" + }, + { + "name": "pltcl", + "path": "/nix/store/cvvvm05xz8735kxb2jqh6gvxfvps1cpw-postgresql-17.9-pltcl" + } + ], + "store_path": "/nix/store/jgrfz7nzjh9m5ymfv0831aamk9ci5ds9-postgresql-17.9" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/zhaly0y0af2m7wyijyhdanm6a9l5lydv-postgresql-17.9", + "default": true + }, + { + "name": "man", + "path": "/nix/store/hgrmddv5rl1axc814n8f27q8gjlxpdz5-postgresql-17.9-man", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/yhvkyzaxm3lcs7kk8qri3ql34p6h7dmc-postgresql-17.9-debug" + }, + { + "name": "doc", + "path": "/nix/store/x41xsx8n2j3l53dr6qfr1w7i9q1pvb3b-postgresql-17.9-doc" + }, + { + "name": "plperl", + "path": "/nix/store/4dnwbih86p5grx6ys7faq29nh9w0krky-postgresql-17.9-plperl" + }, + { + "name": "plpython3", + "path": "/nix/store/rr62jngbsjqim8k5r761h985y88zci8w-postgresql-17.9-plpython3" + }, + { + "name": "pltcl", + "path": "/nix/store/292bd6aqwdsrd3bkvj8yjgwgg5nqlgjv-postgresql-17.9-pltcl" + }, + { + "name": "dev", + "path": "/nix/store/w8vci17bmzkbxclrkjxg2bd3aachf5i8-postgresql-17.9-dev" + }, + { + "name": "jit", + "path": "/nix/store/87sz1iy2q7v0fcsrgbkmryrp390v5sl9-postgresql-17.9-jit" + }, + { + "name": "lib", + "path": "/nix/store/il7gfijl01sxk16h9pffc5yan70vbqfp-postgresql-17.9-lib" + } + ], + "store_path": "/nix/store/zhaly0y0af2m7wyijyhdanm6a9l5lydv-postgresql-17.9" + } + } + }, + "ruby@3.3": { + "last_modified": "2026-01-23T17:20:52Z", + "plugin_version": "0.0.2", + "resolved": "github:NixOS/nixpkgs/a1bab9e494f5f4939442a57a58d0449a109593fe#ruby", + "source": "devbox-search", + "version": "3.3.10", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/d9wal8y7w1zpvyas3x1q4ykz880mmklk-ruby-3.3.10", + "default": true + }, + { + "name": "devdoc", + "path": "/nix/store/1rfqp0848j3gnm222ls3bipk1azcrrq3-ruby-3.3.10-devdoc" + } + ], + "store_path": "/nix/store/d9wal8y7w1zpvyas3x1q4ykz880mmklk-ruby-3.3.10" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/1hlahw0ijkxx1aqy3x41k3gxpgv34g7d-ruby-3.3.10", + "default": true + }, + { + "name": "devdoc", + "path": "/nix/store/arvi0gqvw07ngbi2ci20dn5ka2jz5irv-ruby-3.3.10-devdoc" + } + ], + "store_path": "/nix/store/1hlahw0ijkxx1aqy3x41k3gxpgv34g7d-ruby-3.3.10" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/as6xdshxpansvkag8zqr602ajkn9079z-ruby-3.3.10", + "default": true + }, + { + "name": "devdoc", + "path": "/nix/store/wix1487x3br4gxa0il4q6llz5xyqxspl-ruby-3.3.10-devdoc" + } + ], + "store_path": "/nix/store/as6xdshxpansvkag8zqr602ajkn9079z-ruby-3.3.10" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/6jz2pgmsh06z9a83qi33f6lp9w2q6mzm-ruby-3.3.10", + "default": true + }, + { + "name": "devdoc", + "path": "/nix/store/kah8xsbcd10iakxqmlw558iarhsrd5vi-ruby-3.3.10-devdoc" + } + ], + "store_path": "/nix/store/6jz2pgmsh06z9a83qi33f6lp9w2q6mzm-ruby-3.3.10" + } + } + }, + "zstd@latest": { + "last_modified": "2026-04-10T12:25:30Z", + "resolved": "github:NixOS/nixpkgs/8c11f88bb9573a10a7d6bf87161ef08455ac70b9#zstd", + "source": "devbox-search", + "version": "1.5.7", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/4crx02pb0lv0x26yly1bnbf3n00cq38m-zstd-1.5.7-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/c3g4ifcw3ad8kpa8yjs8lsac5hvmqzv0-zstd-1.5.7-man", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/i0hhsvlafn0zx3yl8yfcs714ps5qic00-zstd-1.5.7-dev" + }, + { + "name": "out", + "path": "/nix/store/xq7dsd7b6x66fn1pqsif0pld0nw6rb33-zstd-1.5.7" + } + ], + "store_path": "/nix/store/4crx02pb0lv0x26yly1bnbf3n00cq38m-zstd-1.5.7-bin" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/4x4q96zrz8g7jzz9wm8z94riv7q3zw0j-zstd-1.5.7-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/1xbh2v2pvphs8m06yrgzhrnrwpr0nsvl-zstd-1.5.7-man", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/c9082kb2i992fi80ix6zi7sa6ijqqrzv-zstd-1.5.7-dev" + }, + { + "name": "out", + "path": "/nix/store/pilcyv83zm3h2gm1924xkfmib9n63b5r-zstd-1.5.7" + } + ], + "store_path": "/nix/store/4x4q96zrz8g7jzz9wm8z94riv7q3zw0j-zstd-1.5.7-bin" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/yc6l9laqs8xs5q0ivxbr9as55x0yc9bh-zstd-1.5.7-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/gfq90rph1rzzwxkhw5pq4ywd5vy0rapa-zstd-1.5.7-man", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/jyyscffl8vhrgq34yl5dpf17pwz9v0d4-zstd-1.5.7-dev" + }, + { + "name": "out", + "path": "/nix/store/mdy5l0qf8z6p9xyn2igix156smcmkag8-zstd-1.5.7" + } + ], + "store_path": "/nix/store/yc6l9laqs8xs5q0ivxbr9as55x0yc9bh-zstd-1.5.7-bin" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/8cc8cdqa54bahfi5n5glkm8d252zkkjn-zstd-1.5.7-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/bhms1y19818704k4aljz6mb8prjbxd1y-zstd-1.5.7-man", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/q4v09bffjy5i0f2kdwnbbwmhqv6i3pjs-zstd-1.5.7-dev" + }, + { + "name": "out", + "path": "/nix/store/29mmnqpc1p3iv8wj0lpvicajy3jsbx87-zstd-1.5.7" + } + ], + "store_path": "/nix/store/8cc8cdqa54bahfi5n5glkm8d252zkkjn-zstd-1.5.7-bin" + } + } + } + } +} diff --git a/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb b/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb index 663596e3334..b33b1526704 100644 --- a/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/protocol/routing_info_spec.rb @@ -251,14 +251,14 @@ class Protocol end end - context 'when the route domain has enforce_access_rules enabled' do + context 'when the route domain has enforce_route_policies enabled' do let(:valid_uuid) { '11111111-2222-3333-4444-555555555555' } let(:enforce_domain) do PrivateDomain.make( name: 'mtls.example.com', owning_organization: org, - enforce_access_rules: true, - access_rules_scope: 'space' + enforce_route_policies: true, + route_policies_scope: 'space' ) end let(:mtls_route) { Route.make(host: 'myapp', domain: enforce_domain, space: space) } From 7ac260baab76582df5f8661a9a9fc145e5e32526 Mon Sep 17 00:00:00 2001 From: rkoster Date: Tue, 21 Apr 2026 09:37:24 +0000 Subject: [PATCH 27/29] Fix domain_create_message_spec: use enforce_route_policies field names --- .../messages/domain_create_message_spec.rb | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/spec/unit/messages/domain_create_message_spec.rb b/spec/unit/messages/domain_create_message_spec.rb index 9041792e1c8..cf71ae6936b 100644 --- a/spec/unit/messages/domain_create_message_spec.rb +++ b/spec/unit/messages/domain_create_message_spec.rb @@ -406,7 +406,7 @@ module VCAP::CloudController context 'enforce_route_policies' do context 'when not a boolean' do - let(:params) { { name: 'name.com', enforce_access_rules: 'yes' } } + let(:params) { { name: 'name.com', enforce_route_policies: 'yes' } } it 'is not valid' do expect(subject).not_to be_valid @@ -414,25 +414,25 @@ module VCAP::CloudController end end - context 'when true without access_rules_scope' do - let(:params) { { name: 'name.com', enforce_access_rules: true } } + context 'when true without route_policies_scope' do + let(:params) { { name: 'name.com', enforce_route_policies: true } } it 'is not valid' do expect(subject).not_to be_valid - expect(subject.errors[:route_policies_scope]).to include('is required when enforce_access_rules is true') + expect(subject.errors[:route_policies_scope]).to include('is required when enforce_route_policies is true') end end - context 'when true with a valid access_rules_scope' do - let(:params) { { name: 'name.com', enforce_access_rules: true, access_rules_scope: 'space' } } + context 'when true with a valid route_policies_scope' do + let(:params) { { name: 'name.com', enforce_route_policies: true, route_policies_scope: 'space' } } it 'is valid' do expect(subject).to be_valid end end - context 'when false without access_rules_scope' do - let(:params) { { name: 'name.com', enforce_access_rules: false } } + context 'when false without route_policies_scope' do + let(:params) { { name: 'name.com', enforce_route_policies: false } } it 'is valid' do expect(subject).to be_valid @@ -450,7 +450,7 @@ module VCAP::CloudController context 'route_policies_scope' do context 'when set to an invalid value' do - let(:params) { { name: 'name.com', enforce_access_rules: true, access_rules_scope: 'invalid' } } + let(:params) { { name: 'name.com', enforce_route_policies: true, route_policies_scope: 'invalid' } } it 'is not valid' do expect(subject).not_to be_valid @@ -459,7 +459,7 @@ module VCAP::CloudController end context "when set to 'any'" do - let(:params) { { name: 'name.com', enforce_access_rules: true, access_rules_scope: 'any' } } + let(:params) { { name: 'name.com', enforce_route_policies: true, route_policies_scope: 'any' } } it 'is valid' do expect(subject).to be_valid @@ -467,7 +467,7 @@ module VCAP::CloudController end context "when set to 'org'" do - let(:params) { { name: 'name.com', enforce_access_rules: true, access_rules_scope: 'org' } } + let(:params) { { name: 'name.com', enforce_route_policies: true, route_policies_scope: 'org' } } it 'is valid' do expect(subject).to be_valid @@ -475,15 +475,15 @@ module VCAP::CloudController end context "when set to 'space'" do - let(:params) { { name: 'name.com', enforce_access_rules: true, access_rules_scope: 'space' } } + let(:params) { { name: 'name.com', enforce_route_policies: true, route_policies_scope: 'space' } } it 'is valid' do expect(subject).to be_valid end end - context 'when provided without enforce_access_rules' do - let(:params) { { name: 'name.com', access_rules_scope: 'space' } } + context 'when provided without enforce_route_policies' do + let(:params) { { name: 'name.com', route_policies_scope: 'space' } } it 'is valid (scope alone is permissible)' do expect(subject).to be_valid From 83c5b8bf87c11ee56aa0b14620979c1ad3b708fc Mon Sep 17 00:00:00 2001 From: rkoster Date: Tue, 21 Apr 2026 10:33:52 +0000 Subject: [PATCH 28/29] Fix route_policies_spec: use 'Source' terminology and 'sources' query param - Line 206: expect error message to include 'Source' (not 'Selector') - Line 292: use query param ?sources= (not ?selectors=) - Line 344: use query param ?sources= in combined filter test These complete the terminology rebrand from 'selector' to 'source' in the route policies context per RFC be8d74c1. --- spec/request/route_policies_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/request/route_policies_spec.rb b/spec/request/route_policies_spec.rb index bfce52e18d8..ebd6a69ccc4 100644 --- a/spec/request/route_policies_spec.rb +++ b/spec/request/route_policies_spec.rb @@ -203,7 +203,7 @@ def expected_rule_json(rule) }.to_json, admin_header expect(last_response.status).to eq(422) - expect(last_response.body).to include('Selector') + expect(last_response.body).to include('Source') end end @@ -289,7 +289,7 @@ def expected_rule_json(rule) end it 'filters by selectors' do - get '/v3/route_policies?selectors=cf:any', nil, admin_header + get '/v3/route_policies?sources=cf:any', nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) @@ -341,7 +341,7 @@ def expected_rule_json(rule) end it 'combines space_guids with other filters' do - get "/v3/route_policies?space_guids=#{space.guid}&selectors=cf:app:#{valid_uuid}", nil, admin_header + get "/v3/route_policies?space_guids=#{space.guid}&sources=cf:app:#{valid_uuid}", nil, admin_header expect(last_response.status).to eq(200) parsed = Oj.load(last_response.body) From 338b2e8bb5dd9624a11f7bbcd2ef607353e5207a Mon Sep 17 00:00:00 2001 From: rkoster Date: Tue, 21 Apr 2026 10:34:00 +0000 Subject: [PATCH 29/29] Revert: restore label_selector (was incorrectly renamed to label_source) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit During the selector→source rebrand for route policies, we accidentally renamed 'label_selector' to 'label_source' across the test suite. This was WRONG because: - label_selector is a standard Cloud Controller query parameter for filtering resources by labels/annotations (e.g., ?label_selector=env=prod) - It is used across many endpoints: apps, packages, orgs, buildpacks, etc. - It has nothing to do with route policies or the terminology rebrand - The overly broad sed command caught it by mistake This commit restores label_selector to its correct name, fixing 13+ test failures caused by the regression. --- spec/request/apps_spec.rb | 2 +- spec/request/buildpacks_spec.rb | 2 +- spec/request/builds_spec.rb | 2 +- spec/request/deployments_spec.rb | 2 +- spec/request/domains_spec.rb | 2 +- spec/request/droplets_spec.rb | 4 ++-- spec/request/isolation_segments_spec.rb | 2 +- spec/request/organizations_spec.rb | 6 +++--- spec/request/packages_spec.rb | 2 +- spec/request/processes_spec.rb | 2 +- spec/request/revisions_spec.rb | 2 +- spec/request/routes_spec.rb | 2 +- spec/request/service_brokers_spec.rb | 2 +- spec/request/service_credential_bindings_spec.rb | 2 +- spec/request/service_instances_spec.rb | 2 +- spec/request/service_offerings_spec.rb | 2 +- spec/request/service_plans_spec.rb | 2 +- spec/request/service_route_bindings_spec.rb | 2 +- spec/request/spaces_spec.rb | 6 +++--- spec/request/stacks_spec.rb | 2 +- spec/request/tasks_spec.rb | 6 +++--- spec/request/users_spec.rb | 4 ++-- spec/unit/controllers/v3/apps_controller_spec.rb | 2 +- spec/unit/fetchers/service_broker_list_fetcher_spec.rb | 4 ++-- spec/unit/fetchers/service_offering_list_fetcher_spec.rb | 2 +- spec/unit/fetchers/service_plan_list_fetcher_spec.rb | 2 +- spec/unit/messages/app_revisions_list_message_spec.rb | 4 ++-- spec/unit/messages/apps_list_message_spec.rb | 4 ++-- spec/unit/messages/buildpacks_list_message_spec.rb | 4 ++-- spec/unit/messages/isolation_segments_list_message_spec.rb | 4 ++-- spec/unit/messages/list_message_spec.rb | 2 +- spec/unit/messages/packages_list_message_spec.rb | 2 +- spec/unit/messages/processes_list_message_spec.rb | 2 +- spec/unit/messages/tasks_list_message_spec.rb | 2 +- 34 files changed, 47 insertions(+), 47 deletions(-) diff --git a/spec/request/apps_spec.rb b/spec/request/apps_spec.rb index f256ffb548b..64fcef98a77 100644 --- a/spec/request/apps_spec.rb +++ b/spec/request/apps_spec.rb @@ -607,7 +607,7 @@ stacks: 'cf', include: 'space', lifecycle_type: 'buildpack', - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/buildpacks_spec.rb b/spec/request/buildpacks_spec.rb index 92ca4ddab84..81b07353d88 100644 --- a/spec/request/buildpacks_spec.rb +++ b/spec/request/buildpacks_spec.rb @@ -29,7 +29,7 @@ names: 'foo', stacks: 'cf', lifecycle: 'buildpack', - label_source: 'foo,bar', + label_selector: 'foo,bar', guids: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } diff --git a/spec/request/builds_spec.rb b/spec/request/builds_spec.rb index da57aec32b2..5a88e6b7795 100644 --- a/spec/request/builds_spec.rb +++ b/spec/request/builds_spec.rb @@ -410,7 +410,7 @@ guids: '123', app_guids: '123', package_guids: '123', - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/deployments_spec.rb b/spec/request/deployments_spec.rb index 159fd9844d2..88db57c603f 100644 --- a/spec/request/deployments_spec.rb +++ b/spec/request/deployments_spec.rb @@ -2077,7 +2077,7 @@ def json_for_options(deployment) status_values: 'foo', status_reasons: 'foo', app_guids: '123', - label_source: 'bar', + label_selector: 'bar', guids: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } diff --git a/spec/request/domains_spec.rb b/spec/request/domains_spec.rb index f0969eaaa2c..024b7792086 100644 --- a/spec/request/domains_spec.rb +++ b/spec/request/domains_spec.rb @@ -31,7 +31,7 @@ names: 'foo,bar', guids: 'foo,bar', organization_guids: 'foo,bar', - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/droplets_spec.rb b/spec/request/droplets_spec.rb index bac1d9a59d4..fb60d1d9df6 100644 --- a/spec/request/droplets_spec.rb +++ b/spec/request/droplets_spec.rb @@ -646,7 +646,7 @@ space_guids: 'test', states: %w[test foo], organization_guids: 'foo,bar', - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } @@ -674,7 +674,7 @@ current: true, package_guid: package_model.guid, states: %w[test foo], - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/isolation_segments_spec.rb b/spec/request/isolation_segments_spec.rb index fcdf51f34c3..c362272f507 100644 --- a/spec/request/isolation_segments_spec.rb +++ b/spec/request/isolation_segments_spec.rb @@ -269,7 +269,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/organizations_spec.rb b/spec/request/organizations_spec.rb index 836748866b1..fc5b009d716 100644 --- a/spec/request/organizations_spec.rb +++ b/spec/request/organizations_spec.rb @@ -194,7 +194,7 @@ module VCAP::CloudController page: '2', per_page: '10', order_by: 'updated_at', - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } @@ -1462,7 +1462,7 @@ module VCAP::CloudController page: '2', per_page: '10', order_by: 'updated_at', - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } @@ -1488,7 +1488,7 @@ module VCAP::CloudController page: '2', per_page: '10', order_by: 'updated_at', - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/packages_spec.rb b/spec/request/packages_spec.rb index 654f0adb13f..81a05c313fd 100644 --- a/spec/request/packages_spec.rb +++ b/spec/request/packages_spec.rb @@ -481,7 +481,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/processes_spec.rb b/spec/request/processes_spec.rb index 105a835a91b..fb2cf1bf20e 100644 --- a/spec/request/processes_spec.rb +++ b/spec/request/processes_spec.rb @@ -121,7 +121,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/revisions_spec.rb b/spec/request/revisions_spec.rb index 701d4a057cb..d3ace2cf840 100644 --- a/spec/request/revisions_spec.rb +++ b/spec/request/revisions_spec.rb @@ -185,7 +185,7 @@ order_by: 'updated_at', guids: app_model.guid.to_s, versions: '1,2', - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/routes_spec.rb b/spec/request/routes_spec.rb index c953e2b0e58..a8ad0dc1e2d 100644 --- a/spec/request/routes_spec.rb +++ b/spec/request/routes_spec.rb @@ -165,7 +165,7 @@ hosts: 'foo', ports: 636, include: 'domain', - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/service_brokers_spec.rb b/spec/request/service_brokers_spec.rb index b8bfcaa0b9e..863bd86b988 100644 --- a/spec/request/service_brokers_spec.rb +++ b/spec/request/service_brokers_spec.rb @@ -113,7 +113,7 @@ per_page: '10', page: 2, order_by: 'updated_at', - label_source: 'foo==bar', + label_selector: 'foo==bar', guids: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } diff --git a/spec/request/service_credential_bindings_spec.rb b/spec/request/service_credential_bindings_spec.rb index 46a2d9d5d74..68835e7765c 100644 --- a/spec/request/service_credential_bindings_spec.rb +++ b/spec/request/service_credential_bindings_spec.rb @@ -53,7 +53,7 @@ guids: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 }, - label_source: 'env' + label_selector: 'env' } end end diff --git a/spec/request/service_instances_spec.rb b/spec/request/service_instances_spec.rb index f765828702a..b9c69073434 100644 --- a/spec/request/service_instances_spec.rb +++ b/spec/request/service_instances_spec.rb @@ -170,7 +170,7 @@ per_page: '10', page: 2, order_by: 'updated_at', - label_source: 'foo,bar', + label_selector: 'foo,bar', type: 'managed', service_plan_guids: %w[guid-1 guid-2], service_plan_names: %w[plan-1 plan-2], diff --git a/spec/request/service_offerings_spec.rb b/spec/request/service_offerings_spec.rb index 742f49e9e65..4076abeba89 100644 --- a/spec/request/service_offerings_spec.rb +++ b/spec/request/service_offerings_spec.rb @@ -219,7 +219,7 @@ per_page: '10', page: 2, order_by: 'updated_at', - label_source: 'foo==bar', + label_selector: 'foo==bar', fields: { 'service_broker' => 'name' }, guids: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", diff --git a/spec/request/service_plans_spec.rb b/spec/request/service_plans_spec.rb index b97c3aebc26..aaf5ba5963e 100644 --- a/spec/request/service_plans_spec.rb +++ b/spec/request/service_plans_spec.rb @@ -197,7 +197,7 @@ per_page: '10', page: 2, order_by: 'updated_at', - label_source: 'foo==bar', + label_selector: 'foo==bar', fields: { 'service_offering.service_broker' => 'name' }, guids: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", diff --git a/spec/request/service_route_bindings_spec.rb b/spec/request/service_route_bindings_spec.rb index 69833d0316b..af2cde52cc0 100644 --- a/spec/request/service_route_bindings_spec.rb +++ b/spec/request/service_route_bindings_spec.rb @@ -71,7 +71,7 @@ per_page: '10', page: 2, order_by: 'updated_at', - label_source: 'foo==bar', + label_selector: 'foo==bar', guids: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } diff --git a/spec/request/spaces_spec.rb b/spec/request/spaces_spec.rb index f2edce375f1..589028b6cb1 100644 --- a/spec/request/spaces_spec.rb +++ b/spec/request/spaces_spec.rb @@ -263,7 +263,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } @@ -1304,7 +1304,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } @@ -1330,7 +1330,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/request/stacks_spec.rb b/spec/request/stacks_spec.rb index 0931f9a0da4..644ce145ea7 100644 --- a/spec/request/stacks_spec.rb +++ b/spec/request/stacks_spec.rb @@ -86,7 +86,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_source: 'foo,bar', + label_selector: 'foo,bar', guids: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } diff --git a/spec/request/tasks_spec.rb b/spec/request/tasks_spec.rb index 75c549371ab..8fa34e57a70 100644 --- a/spec/request/tasks_spec.rb +++ b/spec/request/tasks_spec.rb @@ -60,7 +60,7 @@ names: %w[foo bar], states: %w[test foo], organization_guids: 'foo,bar', - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } @@ -89,7 +89,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } @@ -347,7 +347,7 @@ organization_guids: app_model.organization.guid, space_guids: app_model.space.guid, states: 'SUCCEEDED', - label_source: 'boomerang' + label_selector: 'boomerang' } get "/v3/tasks?#{query.to_query}", nil, developer_headers diff --git a/spec/request/users_spec.rb b/spec/request/users_spec.rb index d2bd984a1d8..bf15263c0ca 100644 --- a/spec/request/users_spec.rb +++ b/spec/request/users_spec.rb @@ -116,7 +116,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } @@ -142,7 +142,7 @@ page: '2', per_page: '10', order_by: 'updated_at', - label_source: 'foo,bar', + label_selector: 'foo,bar', created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}", updated_ats: { gt: Time.now.utc.iso8601 } } diff --git a/spec/unit/controllers/v3/apps_controller_spec.rb b/spec/unit/controllers/v3/apps_controller_spec.rb index 8fdb0a148ae..beee5e6ed5b 100644 --- a/spec/unit/controllers/v3/apps_controller_spec.rb +++ b/spec/unit/controllers/v3/apps_controller_spec.rb @@ -135,7 +135,7 @@ context 'label_selection' do it 'returns a 400 when the label_selector is invalid' do - get :index, params: { label_source: 'buncha nonsense' } + get :index, params: { label_selector: 'buncha nonsense' } expect(response).to have_http_status(:bad_request) diff --git a/spec/unit/fetchers/service_broker_list_fetcher_spec.rb b/spec/unit/fetchers/service_broker_list_fetcher_spec.rb index 6b448ee0dd2..d5a34a08e2e 100644 --- a/spec/unit/fetchers/service_broker_list_fetcher_spec.rb +++ b/spec/unit/fetchers/service_broker_list_fetcher_spec.rb @@ -160,7 +160,7 @@ module VCAP::CloudController let(:filters) do { space_guids: [space_2.guid], - label_source: 'dog in (poodle,scooby-doo)', + label_selector: 'dog in (poodle,scooby-doo)', names: [space_scoped_broker_1.name] } end @@ -189,7 +189,7 @@ module VCAP::CloudController let(:filters) do { space_guids: [space_1.guid, space_2.guid], - label_source: 'dog in (poodle,scooby-doo)', + label_selector: 'dog in (poodle,scooby-doo)', names: [space_scoped_broker_1.name] } end diff --git a/spec/unit/fetchers/service_offering_list_fetcher_spec.rb b/spec/unit/fetchers/service_offering_list_fetcher_spec.rb index 8391f8a6e4f..6870940f3f7 100644 --- a/spec/unit/fetchers/service_offering_list_fetcher_spec.rb +++ b/spec/unit/fetchers/service_offering_list_fetcher_spec.rb @@ -490,7 +490,7 @@ module VCAP::CloudController let!(:service_offering_1) { VCAP::CloudController::ServicePlan.make(public: true, active: true).service } let!(:service_offering_2) { VCAP::CloudController::ServicePlan.make(public: true, active: true).service } let!(:service_offering_3) { VCAP::CloudController::ServicePlan.make(public: true, active: true).service } - let(:message) { ServiceOfferingsListMessage.from_params({ label_source: 'flavor=orange' }.with_indifferent_access) } + let(:message) { ServiceOfferingsListMessage.from_params({ label_selector: 'flavor=orange' }.with_indifferent_access) } before do VCAP::CloudController::ServiceOfferingLabelModel.make(resource_guid: service_offering_1.guid, key_name: 'flavor', value: 'orange') diff --git a/spec/unit/fetchers/service_plan_list_fetcher_spec.rb b/spec/unit/fetchers/service_plan_list_fetcher_spec.rb index 17f2220646e..4e45898df31 100644 --- a/spec/unit/fetchers/service_plan_list_fetcher_spec.rb +++ b/spec/unit/fetchers/service_plan_list_fetcher_spec.rb @@ -491,7 +491,7 @@ module VCAP::CloudController let!(:service_plan_1) { VCAP::CloudController::ServicePlan.make(public: true, active: true) } let!(:service_plan_2) { VCAP::CloudController::ServicePlan.make(public: true, active: true) } let!(:service_plan_3) { VCAP::CloudController::ServicePlan.make(public: true, active: true) } - let(:message) { ServicePlansListMessage.from_params({ label_source: 'flavor=orange' }.with_indifferent_access) } + let(:message) { ServicePlansListMessage.from_params({ label_selector: 'flavor=orange' }.with_indifferent_access) } before do VCAP::CloudController::ServicePlanLabelModel.make(resource_guid: service_plan_1.guid, key_name: 'flavor', value: 'orange') diff --git a/spec/unit/messages/app_revisions_list_message_spec.rb b/spec/unit/messages/app_revisions_list_message_spec.rb index dd1e8d2b0f3..2a3c7849657 100644 --- a/spec/unit/messages/app_revisions_list_message_spec.rb +++ b/spec/unit/messages/app_revisions_list_message_spec.rb @@ -40,7 +40,7 @@ module VCAP::CloudController let(:opts) do { versions: %w[1 3], - label_source: 'key=value', + label_selector: 'key=value', page: 1, per_page: 5, deployable: true @@ -61,7 +61,7 @@ module VCAP::CloudController per_page: 5, versions: ['1'], deployable: true, - label_source: 'key=value' + label_selector: 'key=value' }) end.not_to raise_error end diff --git a/spec/unit/messages/apps_list_message_spec.rb b/spec/unit/messages/apps_list_message_spec.rb index e4ac1463702..e1f76f88844 100644 --- a/spec/unit/messages/apps_list_message_spec.rb +++ b/spec/unit/messages/apps_list_message_spec.rb @@ -67,7 +67,7 @@ module VCAP::CloudController per_page: 5, order_by: 'created_at', include: ['space', 'space.organization'], - label_source: 'foo in (stuff,things)', + label_selector: 'foo in (stuff,things)', lifecycle_type: 'buildpack' } end @@ -90,7 +90,7 @@ module VCAP::CloudController per_page: 5, order_by: 'created_at', include: ['space', 'space.organization'], - label_source: 'foo in (stuff,things)', + label_selector: 'foo in (stuff,things)', lifecycle_type: 'buildpack' }) end.not_to raise_error diff --git a/spec/unit/messages/buildpacks_list_message_spec.rb b/spec/unit/messages/buildpacks_list_message_spec.rb index 3f7bed0e566..605c4fbac5e 100644 --- a/spec/unit/messages/buildpacks_list_message_spec.rb +++ b/spec/unit/messages/buildpacks_list_message_spec.rb @@ -47,7 +47,7 @@ module VCAP::CloudController names: %w[name1 name2], stacks: %w[stack1 stack2], lifecycle: 'buildpack', - label_source: 'foo=bar', + label_selector: 'foo=bar', page: 1, per_page: 5 } @@ -65,7 +65,7 @@ module VCAP::CloudController BuildpacksListMessage.from_params({ names: [], stacks: [], - label_source: '', + label_selector: '', lifecycle: 'buildpack' }) end.not_to raise_error diff --git a/spec/unit/messages/isolation_segments_list_message_spec.rb b/spec/unit/messages/isolation_segments_list_message_spec.rb index 5160108ef5a..38ba6a7679f 100644 --- a/spec/unit/messages/isolation_segments_list_message_spec.rb +++ b/spec/unit/messages/isolation_segments_list_message_spec.rb @@ -52,7 +52,7 @@ module VCAP::CloudController names: %w[name1 name2], guids: %w[guid1 guid2], organization_guids: %w[o-guid1 o-guid2], - label_source: 'foo=bar', + label_selector: 'foo=bar', page: 1, per_page: 5, order_by: 'created_at', @@ -81,7 +81,7 @@ module VCAP::CloudController names: [], guids: [], organization_guids: [], - label_source: '', + label_selector: '', page: 1, per_page: 5, order_by: 'created_at' diff --git a/spec/unit/messages/list_message_spec.rb b/spec/unit/messages/list_message_spec.rb index 3202829f105..2f48b716800 100644 --- a/spec/unit/messages/list_message_spec.rb +++ b/spec/unit/messages/list_message_spec.rb @@ -289,7 +289,7 @@ def self.from_params(params) end it 'handles ruby symbols' do - message = list_message_klass.from_params(label_source: 'example.com/foo==bar') + message = list_message_klass.from_params(label_selector: 'example.com/foo==bar') expect(message.requirements.first.key).to eq('example.com/foo') end end diff --git a/spec/unit/messages/packages_list_message_spec.rb b/spec/unit/messages/packages_list_message_spec.rb index 2389e16b35d..c3ef1fa369e 100644 --- a/spec/unit/messages/packages_list_message_spec.rb +++ b/spec/unit/messages/packages_list_message_spec.rb @@ -57,7 +57,7 @@ module VCAP::CloudController app_guids: %w[appguid1 appguid2], organization_guids: %w[organizationguid1 organizationguid2], app_guid: 'appguid', - label_source: 'key=value', + label_selector: 'key=value', page: 1, per_page: 5 } diff --git a/spec/unit/messages/processes_list_message_spec.rb b/spec/unit/messages/processes_list_message_spec.rb index 09544964c01..4346e772dc6 100644 --- a/spec/unit/messages/processes_list_message_spec.rb +++ b/spec/unit/messages/processes_list_message_spec.rb @@ -67,7 +67,7 @@ module VCAP::CloudController app_guid: 'appguid', embed: 'process_instances', page: 1, - label_source: 'key=value', + label_selector: 'key=value', per_page: 5, order_by: 'created_at', created_ats: [Time.now.utc.iso8601, Time.now.utc.iso8601], diff --git a/spec/unit/messages/tasks_list_message_spec.rb b/spec/unit/messages/tasks_list_message_spec.rb index f5c4950d22b..32177588787 100644 --- a/spec/unit/messages/tasks_list_message_spec.rb +++ b/spec/unit/messages/tasks_list_message_spec.rb @@ -64,7 +64,7 @@ module VCAP::CloudController organization_guids: %w[orgguid1 orgguid2], space_guids: %w[spaceguid1 spaceguid2], sequence_ids: ['1, 2'], - label_source: 'unicycling=fred', + label_selector: 'unicycling=fred', page: 1, per_page: 5 }