-
Notifications
You must be signed in to change notification settings - Fork 369
RFC0055 Identity-Aware Routing #4910
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rkoster
wants to merge
38
commits into
main
Choose a base branch
from
feature/app-to-app-mtls-routing
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
840a966
Add allowed_sources support for mTLS app-to-app routing
rkoster 07c4ec3
Add unit tests for allowed_sources validation
rkoster a323763
Fix allowed_sources validation to handle symbol keys
rkoster 17cb3df
Rename allowed_sources to mtls_allowed_sources for clarity
rkoster 609e1b7
Refactor mTLS route options to RFC-0027 compliant flat format
rkoster 9050ec9
Implement RFC domain-scoped mTLS routing with /v3/access_rules API
rkoster 25210fe
Fix access_rules_controller permissions query
rkoster 8cbee5a
Add automatic Diego sync callbacks to RouteAccessRule
rkoster 5bcbb43
Implement include=selector_resource for /v3/access_rules endpoint
rkoster ec93b4d
Add space_guids filtering to /v3/access_rules endpoint
rkoster 5e3f091
Implement include=route for /v3/access_rules endpoint
rkoster e59a2f6
Remove name field from access rules, add read-only relationships per …
rkoster b512e8f
Add metadata support to RouteAccessRule model
rkoster 2769a08
Fix class loading for RouteAccessRule metadata models
rkoster a401085
Add validation to prevent access rules on internal domains per RFC
rkoster d50084e
Consolidate access rules migrations, fix RuboCop offenses, and clean …
rkoster 026cbef
Fix race condition, double join, LIKE injection, N+1 queries, and dom…
rkoster 5e1ca21
Fix incomplete LIKE metacharacter escaping (CodeQL rb/incomplete-sani…
rkoster 10aa245
Add tests for LIKE metacharacter escaping (backslash, underscore)
rkoster 3928be3
Fix MySQL key length limit in metadata table migration
rkoster 75839c1
Fix routing_info_spec: remove nonexistent name field from RouteAccess…
rkoster e4d154e
Fix route presenter regression: include options: {} when empty
rkoster bc46b52
Fix CI failures: route presenter options logic and domain V2 serializ…
rkoster 7bc622d
Rebrand: access rules → route policies, selector → source
rkoster 7452e46
Fix test failures: complete terminology rebrand in specs
rkoster 8954435
Fix routing_info_spec: use enforce_route_policies field names
rkoster 0a61328
Fix domain_create_message_spec: use enforce_route_policies field names
rkoster f25162c
Fix route_policies_spec: use 'Source' terminology and 'sources' query…
rkoster 7279201
Revert: restore label_selector (was incorrectly renamed to label_source)
rkoster ea18715
Add API documentation for Route Policies
rkoster bec99af
Add include support to route policies show endpoint
rkoster c80727b
Remove devbox configuration files from branch
rkoster d5029ca
Address philippthun's review feedback
rkoster 99d0135
Rename add_mtls_options to add_route_policy_options and refactor stru…
rkoster 765e923
Limit route policy includes to 'route' and 'source' only
rkoster 3dad6e1
Prevent enforce_route_policies on internal and router_group domains
rkoster 1c2e882
Fix RuboCop offenses in domain_create_message and route_policies_list…
rkoster c058620
Add include=route_policies support on routes endpoints
rkoster File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| module VCAP::CloudController | ||
| class RoutePolicyCreate | ||
| class Error < StandardError | ||
| end | ||
|
|
||
| def create(route:, message:) | ||
| RoutePolicy.db.transaction do | ||
| # Lock existing route policies for this route to prevent concurrent inserts | ||
| # from violating cf:any exclusivity or uniqueness constraints | ||
| locked_policies = RoutePolicy.where(route_id: route.id).for_update.all | ||
|
|
||
| validate_source_exclusivity(locked_policies, message.source) | ||
|
|
||
| RoutePolicy.create( | ||
| source: message.source, | ||
| route_id: route.id | ||
| ) | ||
| end | ||
| rescue Sequel::UniqueConstraintViolation | ||
| raise Error.new("A route policy with source '#{message.source}' already exists for this route.") | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def validate_source_exclusivity(locked_policies, source) | ||
| existing_sources = locked_policies.map(&:source) | ||
|
|
||
| # Enforce cf:any exclusivity: if new policy is cf:any, reject if route already has any policies; | ||
| # if route already has a cf:any policy, reject new policies. | ||
| raise Error.new("Cannot add 'cf:any' source when other route policies already exist for this route.") if source == 'cf:any' && existing_sources.any? | ||
| raise Error.new("Cannot add source '#{source}': route already has a 'cf:any' policy.") if existing_sources.include?('cf:any') | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| require 'messages/route_policy_create_message' | ||
| require 'messages/route_policy_update_message' | ||
| require 'messages/route_policies_list_message' | ||
| require 'messages/route_policy_show_message' | ||
| require 'presenters/v3/route_policy_presenter' | ||
| require 'decorators/include_route_policy_source_decorator' | ||
| require 'decorators/include_route_policy_route_decorator' | ||
| require 'actions/route_policy_create' | ||
|
|
||
| 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 | ||
| message = RoutePolicyShowMessage.from_params(query_params) | ||
| unprocessable!(message.errors.full_messages) unless message.valid? | ||
|
|
||
| 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) | ||
|
|
||
|
rkoster marked this conversation as resolved.
|
||
| decorators = [] | ||
| decorators << IncludeRoutePolicySourceDecorator if IncludeRoutePolicySourceDecorator.match?(message.include) | ||
| decorators << IncludeRoutePolicyRouteDecorator if IncludeRoutePolicyRouteDecorator.match?(message.include) | ||
|
|
||
| render status: :ok, json: Presenters::V3::RoutePolicyPresenter.new(route_policy, decorators: decorators) | ||
| 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::RoutePolicyCreate.new.create(route: route, message: message) | ||
|
|
||
| render status: :created, json: Presenters::V3::RoutePolicyPresenter.new(route_policy) | ||
| rescue VCAP::CloudController::RoutePolicyCreate::Error => e | ||
| unprocessable!(e.message) | ||
| end | ||
|
|
||
| def update | ||
| route_policy = VCAP::CloudController::RoutePolicy.find(guid: hashed_params[:guid]) | ||
| resource_not_found!(:route_policy) unless route_policy | ||
|
|
||
| find_and_authorize_route_for_policy(route_policy) | ||
|
|
||
| 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 | ||
|
|
||
| find_and_authorize_route_for_policy(route_policy) | ||
|
|
||
| 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 find_and_authorize_route_for_policy(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) | ||
| 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 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:<guid>, cf:space:<guid>, cf:org:<guid> | ||
| # 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| module VCAP::CloudController | ||
| class IncludeRoutePoliciesDecorator | ||
| class << self | ||
| def match?(include) | ||
| include&.any? { |i| %w[route_policies].include?(i) } | ||
| end | ||
|
|
||
| def decorate(hash, routes) | ||
| hash[:included] ||= {} | ||
| route_ids = routes.map(&:id).uniq | ||
| route_policies = RoutePolicy.where(route_id: route_ids). | ||
| eager(:route, :labels, :annotations).all | ||
|
|
||
| hash[:included][:route_policies] = route_policies.map { |rp| Presenters::V3::RoutePolicyPresenter.new(rp).to_hash } | ||
| hash | ||
| end | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| module VCAP::CloudController | ||
| 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, route_policies) | ||
| hash[:included] ||= {} | ||
|
|
||
| # 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). | ||
| order(:created_at, :guid). | ||
| eager(Presenters::V3::RoutePresenter.associated_resources).all | ||
|
|
||
| # Present routes | ||
| hash[:included][:routes] = routes.map { |route| Presenters::V3::RoutePresenter.new(route).to_hash } | ||
|
|
||
| hash | ||
| end | ||
| end | ||
| end |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.