Class: ForemanRhCloud::InsightsApiForwarder
- Inherits:
-
Object
- Object
- ForemanRhCloud::InsightsApiForwarder
- Includes:
- CertAuth
- Defined in:
- app/services/foreman_rh_cloud/insights_api_forwarder.rb
Constant Summary collapse
- SCOPED_REQUESTS =
Permission mapping for API paths:
Foreman Permission | Paths ———————|————————————————– view_vulnerability | GET /api/inventory/v1/hosts(/*) view_vulnerability | GET /api/vulnerability/v1/*
| POST /api/vulnerability/v1/vulnerabilities/cvesedit_vulnerability | PATCH /api/vulnerability/v1/status edit_vulnerability | PATCH /api/vulnerability/v1/cves/status
| PATCH /api/vulnerability/v1/cves/business_riskedit_vulnerability | PATCH /api/vulnerability/v1/systems/opt_out
view_advisor | GET /api/insights/v1/* edit_advisor | POST /api/insights/v1/ack/
| DELETE /api/insights/v1/ack/{rule_id}/ | POST /api/insights/v1/hostack/ | DELETE /api/insights/v1/hostack/{id}/ | POST /api/insights/v1/rule/{rule_id}/unack_hosts/ [ # Inventory hosts - requires view_vulnerability for GET { test: %r{api/inventory/v1/hosts(/.*)?$}, tag_name: :tags, permissions: { 'GET' => :view_vulnerability, }, }, # Vulnerability CVEs list - POST requires view_vulnerability (no tags support per OpenAPI spec) { test: %r{api/vulnerability/v1/vulnerabilities/cves}, permissions: { 'POST' => :view_vulnerability, }, }, # Vulnerability status - PATCH requires edit_vulnerability # Note: GET /status does not support tags parameter per OpenAPI spec { test: %r{api/vulnerability/v1/status}, permissions: { 'PATCH' => :edit_vulnerability, }, }, # CVE status - PATCH requires edit_vulnerability (no GET endpoint) { test: %r{api/vulnerability/v1/cves/status}, permissions: { 'PATCH' => :edit_vulnerability, }, }, # CVE business risk - PATCH requires edit_vulnerability (no GET endpoint) { test: %r{api/vulnerability/v1/cves/business_risk}, permissions: { 'PATCH' => :edit_vulnerability, }, }, # Systems opt out - PATCH requires edit_vulnerability (no GET endpoint) { test: %r{api/vulnerability/v1/systems/opt_out}, permissions: { 'PATCH' => :edit_vulnerability, }, }, # Endpoints without tags support (per OpenAPI spec) - still require view_vulnerability for GET { test: %r{api/vulnerability/v1/(apistatus|version|business_risk|announcement)$}, permissions: { 'GET' => :view_vulnerability, }, }, { test: %r{api/vulnerability/v1/cves/[^/]+$}, permissions: { 'GET' => :view_vulnerability, }, }, { test: %r{api/vulnerability/v1/(playbooks|report)/}, permissions: { 'GET' => :view_vulnerability, }, }, # Other vulnerability endpoints - GET requires view_vulnerability (with tags support) { test: %r{api/vulnerability/v1/.*}, tag_name: :tags, permissions: { 'GET' => :view_vulnerability, }, }, # Advisor ack endpoints - POST/DELETE require edit_advisor { test: %r{api/insights/v1/ack(/[^/]*)?$}, tag_name: :tags, permissions: { 'POST' => :edit_advisor, 'DELETE' => :edit_advisor, }, }, # Advisor hostack endpoints - POST/DELETE require edit_advisor { test: %r{api/insights/v1/hostack(/[^/]*)?$}, tag_name: :tags, permissions: { 'POST' => :edit_advisor, 'DELETE' => :edit_advisor, }, }, # Advisor rule unack_hosts - POST requires edit_advisor { test: %r{api/insights/v1/rule/[^/]+/unack_hosts}, tag_name: :tags, permissions: { 'POST' => :edit_advisor, }, }, # Other Advisor/Insights endpoints - GET requires view_advisor { test: %r{api/insights/v1/.*}, tag_name: :tags, permissions: { 'GET' => :view_advisor, }, }, # Other API endpoints (tagging only, no permission enforcement) { test: %r{api/inventory/.*}, tag_name: :tags }, { test: %r{api/tasks/.*}, tag_name: :tags }, ].freeze
Instance Method Summary collapse
- #core_app_name ⇒ Object
- #core_app_version ⇒ Object
- #forward_request(original_request, path, controller_name, user, organization, location) ⇒ Object
- #http_user_agent(original_request) ⇒ Object
- #logger ⇒ Object
- #original_headers(original_request) ⇒ Object
- #path_params(path) ⇒ Object
- #prepare_forward_params(original_request, path, user:, organization:, location:) ⇒ Object
- #prepare_forward_payload(original_request, controller_name) ⇒ Object
- #prepare_request_opts(original_request, path, forward_payload, forward_params) ⇒ Object
- #prepare_tags(user, organization, location, tag_name) ⇒ Object
-
#required_permission_for(path, http_method) ⇒ Symbol?
Returns the required permission for the given path and HTTP method Resolves overlapping patterns by choosing the most specific matcher, defined as the one with the longest regex source that matches the path.
- #scope_request?(original_request, path) ⇒ Boolean
Methods included from CertAuth
#cert_auth_available?, #execute_cloud_request, #foreman_certificate
Methods included from InsightsCloud::CandlepinCache
#candlepin_id_cert, #cp_owner_id, #upstream_owner
Methods included from CloudRequest
Instance Method Details
#core_app_name ⇒ Object
245 246 247 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 245 def core_app_name BranchInfo.new.core_app_name end |
#core_app_version ⇒ Object
249 250 251 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 249 def core_app_version BranchInfo.new.core_app_version end |
#forward_request(original_request, path, controller_name, user, organization, location) ⇒ Object
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 137 def forward_request(original_request, path, controller_name, user, organization, location) # Check permissions before forwarding = (path, original_request.request_method) if && !user&.can?() logger.warn("User #{user&.login || 'anonymous'} lacks permission #{} for #{original_request.request_method} #{path}") raise ::Foreman::PermissionMissingException.new(N_("You do not have permission to perform this action")) end TagsAuth.new(user, organization, location, logger).update_tag if scope_request?(original_request, path) forward_params = prepare_forward_params(original_request, path, user: user, organization: organization, location: location).to_a logger.debug("Request parameters for UI request: #{forward_params}") forward_payload = prepare_forward_payload(original_request, controller_name) logger.debug("User agent for UI is: #{http_user_agent(original_request)}") request_opts = prepare_request_opts(original_request, path, forward_payload, forward_params) request_opts[:organization] = organization logger.debug("Sending request to: #{request_opts[:url]}") execute_cloud_request(request_opts) end |
#http_user_agent(original_request) ⇒ Object
253 254 255 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 253 def http_user_agent(original_request) "#{core_app_name}/#{core_app_version};#{ForemanRhCloud::Engine.engine_name}/#{ForemanRhCloud::VERSION};#{original_request.env['HTTP_USER_AGENT']}" end |
#logger ⇒ Object
257 258 259 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 257 def logger Foreman::Logging.logger('app') end |
#original_headers(original_request) ⇒ Object
214 215 216 217 218 219 220 221 222 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 214 def original_headers(original_request) headers = { if_none_match: original_request.if_none_match, if_modified_since: original_request.if_modified_since, }.compact logger.debug("Sending headers: #{headers}") headers end |
#path_params(path) ⇒ Object
208 209 210 211 212 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 208 def path_params(path) { url: "#{InsightsCloud.ui_base_url}/#{path}", } end |
#prepare_forward_params(original_request, path, user:, organization:, location:) ⇒ Object
199 200 201 202 203 204 205 206 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 199 def prepare_forward_params(original_request, path, user:, organization:, location:) forward_params = original_request.query_parameters.to_a tag_name = scope_request?(original_request, path) forward_params += (user, organization, location, tag_name) if tag_name forward_params end |
#prepare_forward_payload(original_request, controller_name) ⇒ Object
186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 186 def prepare_forward_payload(original_request, controller_name) forward_payload = original_request.request_parameters[controller_name] forward_payload = original_request.raw_post.clone if (original_request.post? || original_request.patch?) && original_request.raw_post forward_payload = original_request.body.read if original_request.put? forward_payload = original_request.params.slice(:file, :metadata) if original_request.params[:file] # fix rails behaviour for http PATCH: forward_payload = forward_payload.to_json if original_request.format.json? && original_request.patch? && forward_payload && !forward_payload.is_a?(String) forward_payload end |
#prepare_request_opts(original_request, path, forward_payload, forward_params) ⇒ Object
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 169 def prepare_request_opts(original_request, path, forward_payload, forward_params) base_params = { method: original_request.method, payload: forward_payload, headers: original_headers(original_request).merge( { params: RestClient::ParamsArray.new(forward_params), user_agent: http_user_agent(original_request), content_type: original_request.media_type.presence || original_request.format.to_s, } ), } params = path_params(path) base_params.merge(params) end |
#prepare_tags(user, organization, location, tag_name) ⇒ Object
163 164 165 166 167 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 163 def (user, organization, location, tag_name) [ TagsAuth.auth_tag_for(user, organization, location), ].map { |tag_value| [tag_name, tag_value] } end |
#required_permission_for(path, http_method) ⇒ Symbol?
Returns the required permission for the given path and HTTP method Resolves overlapping patterns by choosing the most specific matcher, defined as the one with the longest regex source that matches the path. This avoids permission changes caused by reordering SCOPED_REQUESTS.
268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 268 def (path, http_method) # Collect all matching patterns matching_patterns = SCOPED_REQUESTS.select { |pattern| pattern[:test].match?(path) } return nil if matching_patterns.empty? # Choose the most specific pattern: longest regex source wins. # This makes overlapping patterns deterministic and independent of array order. request_pattern = matching_patterns.max_by { |pattern| pattern[:test].source.length } = request_pattern[:permissions] return nil unless [http_method] end |
#scope_request?(original_request, path) ⇒ Boolean
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 224 def scope_request?(original_request, path) return nil unless original_request.get? # Find patterns that match this path AND are relevant for GET requests. # A pattern is relevant if it either: # - Has tag_name defined (supports scoping) # - Has GET permissions defined (explicitly handles GET, even without tags) # This ensures patterns like api/vulnerability/v1/cves/[^/]+$ (GET without tags) # take precedence over general patterns, while POST-only patterns are ignored. matching_patterns = SCOPED_REQUESTS.select do |pattern| pattern[:test].match?(path) && (pattern[:tag_name] || pattern.dig(:permissions, 'GET')) end return nil if matching_patterns.empty? # Choose the most specific pattern by regex source length request_pattern = matching_patterns.max_by { |pattern| pattern[:test].source.length } # Return the tag_name (may be nil if the most specific pattern doesn't support tags) request_pattern[:tag_name] end |