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