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_compliance | GET /api/inventory/v1/hosts(/*) view_compliance | GET /api/compliance/v2/systems view_compliance | GET /api/compliance/v2/systems/system_id view_compliance | GET /api/compliance/v2/systems/os_versions view_compliance | GET /api/compliance/v2/systems/system_id/policies view_compliance | GET /api/compliance/v2/systems/system_id/reports view_compliance | GET /api/compliance/v2/reports/report_id/systems view_compliance | GET /api/compliance/v2/reports/report_id/systems/system_id view_compliance | GET /api/compliance/v2/reports/report_id/systems/os_versions view_compliance | GET /api/compliance/v2/policies/policy_id/systems/os_versions view_compliance | GET /api/compliance/v2/* edit_compliance | POST /api/compliance/v2/policies edit_compliance | DELETE /api/compliance/v2/policies/policy_id edit_compliance | PATCH /api/compliance/v2/policies/policy_id edit_compliance | POST /api/compliance/v2/policies/policy_id/systems edit_compliance | DELETE /api/compliance/v2/policies/policy_id/systems/system_id edit_compliance | PATCH /api/compliance/v2/policies/policy_id/systems/system_id edit_compliance | POST /api/compliance/v2/policies/policy_id/tailorings edit_compliance | PATCH /api/compliance/v2/policies/policy_id/tailorings/tailoring_id edit_compliance | POST /api/compliance/v2/policies/policy_id/tailorings/tailoring_id/rules edit_compliance | DELETE /api/compliance/v2/policies/policy_id/tailorings/tailoring_id/rules/rule_id edit_compliance | PATCH /api/compliance/v2/policies/policy_id/tailorings/tailoring_id/rules/rule_id edit_compliance | DELETE /api/compliance/v2/reports/report_id
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 - shared dependency; # - requires view_compliance or view_vulnerability for GET { test: %r{api/inventory/v1/hosts(/.*)?$}, tag_name: :tags, permissions: { 'GET' => [:view_compliance, :view_vulnerability], }, }, # Policies - requires view_compliance for GET, edit_compliance for POST/DELETE/PATCH { test: %r{api/compliance/v2/policies(/[^/]*)?$}, permissions: { 'GET' => :view_compliance, 'POST' => :edit_compliance, 'DELETE' => :edit_compliance, 'PATCH' => :edit_compliance, }, }, # System and policies assigned to them - requires view_compliance for GET, edit_compliance for POST/DELETE/PATCH { test: %r{api/compliance/v2/policies/[^/]+/systems(/[^/]*)?$}, tag_name: :tags, permissions: { 'GET' => :view_compliance, 'POST' => :edit_compliance, 'DELETE' => :edit_compliance, 'PATCH' => :edit_compliance, }, }, # Tailorings of assigned policies - requires view_compliance for GET, edit_compliance for POST/PATCH { test: %r{api/compliance/v2/policies/[^/]+/tailorings(/[^/]*)?$}, permissions: { 'GET' => :view_compliance, 'POST' => :edit_compliance, 'PATCH' => :edit_compliance, }, }, # Rules of Tailorings - requires view_compliance for GET, edit_compliance for POST/DELETE/PATCH { test: %r{api/compliance/v2/policies/[^/]+/tailorings/[^/]+/rules(/[^/]*)?$}, permissions: { 'GET' => :view_compliance, 'POST' => :edit_compliance, 'DELETE' => :edit_compliance, 'PATCH' => :edit_compliance, }, }, # Compliance Reports - requires view_compliance for GET, edit_compliance for DELETE { test: %r{api/compliance/v2/reports(/[^/]*)?$}, permissions: { 'GET' => :view_compliance, 'DELETE' => :edit_compliance, }, }, # Systems assigned to a report - requires view_compliance for GET { test: %r{api/compliance/v2/reports/[^/]+/systems$}, tag_name: :tags, permissions: { 'GET' => :view_compliance, }, }, # Individual system in a report - requires view_compliance for GET { test: %r{api/compliance/v2/reports/[^/]+/systems/[^/]+$}, tag_name: :tags, permissions: { 'GET' => :view_compliance, }, }, # OS versions for systems in a report - requires view_compliance for GET { test: %r{api/compliance/v2/reports/[^/]+/systems/os_versions$}, tag_name: :tags, permissions: { 'GET' => :view_compliance, }, }, # OS versions for systems in a policy - requires view_compliance for GET { test: %r{api/compliance/v2/policies/[^/]+/systems/os_versions$}, tag_name: :tags, permissions: { 'GET' => :view_compliance, }, }, # Compliance Systems list - requires view_compliance for GET { test: %r{api/compliance/v2/systems$}, tag_name: :tags, permissions: { 'GET' => :view_compliance, }, }, # Individual compliance system - requires view_compliance for GET { test: %r{api/compliance/v2/systems/[^/]+$}, tag_name: :tags, permissions: { 'GET' => :view_compliance, }, }, # OS versions for all systems - requires view_compliance for GET { test: %r{api/compliance/v2/systems/os_versions$}, tag_name: :tags, permissions: { 'GET' => :view_compliance, }, }, # Policies for a specific system - requires view_compliance for GET { test: %r{api/compliance/v2/systems/[^/]+/policies$}, tag_name: :tags, permissions: { 'GET' => :view_compliance, }, }, # Reports for a specific system - requires view_compliance for GET { test: %r{api/compliance/v2/systems/[^/]+/reports$}, tag_name: :tags, permissions: { 'GET' => :view_compliance, }, }, # Other compliance endpoints - GET requires view_compliance { test: %r{api/compliance/v2/.*}, permissions: { 'GET' => :view_compliance, }, }, # 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
397 398 399 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 397 def core_app_name BranchInfo.new.core_app_name end |
#core_app_version ⇒ Object
401 402 403 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 401 def core_app_version BranchInfo.new.core_app_version end |
#forward_request(original_request, path, controller_name, user, organization, location) ⇒ Object
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 289 def forward_request(original_request, path, controller_name, user, organization, location) # Check permissions before forwarding = (path, original_request.request_method) if && Array().none? { |p| user&.can?(p) } logger.warn("User #{user&.login || 'anonymous'} lacks permissions #{Array().join(', ')} 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
405 406 407 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 405 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
409 410 411 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 409 def logger Foreman::Logging.logger('app') end |
#original_headers(original_request) ⇒ Object
366 367 368 369 370 371 372 373 374 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 366 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
360 361 362 363 364 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 360 def path_params(path) { url: "#{InsightsCloud.ui_base_url}/#{path}", } end |
#prepare_forward_params(original_request, path, user:, organization:, location:) ⇒ Object
351 352 353 354 355 356 357 358 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 351 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
338 339 340 341 342 343 344 345 346 347 348 349 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 338 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
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 321 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
315 316 317 318 319 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 315 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.
420 421 422 423 424 425 426 427 428 429 430 431 432 433 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 420 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
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 |
# File 'app/services/foreman_rh_cloud/insights_api_forwarder.rb', line 376 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 |