Class: ForemanRhCloud::InsightsApiForwarder

Inherits:
Object
  • Object
show all
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/cves

edit_vulnerability | PATCH /api/vulnerability/v1/status edit_vulnerability | PATCH /api/vulnerability/v1/cves/status

| PATCH /api/vulnerability/v1/cves/business_risk

edit_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

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

#execute_cloud_request

Instance Method Details

#core_app_nameObject



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_versionObject



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
  permission = required_permission_for(path, original_request.request_method)
  if permission && !user&.can?(permission)
    logger.warn("User #{user&. || 'anonymous'} lacks permission #{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

#loggerObject



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 += prepare_tags(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 prepare_tags(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.

Parameters:

  • path (String)

    The request path

  • http_method (String)

    The HTTP method (GET, POST, etc.)

Returns:

  • (Symbol, nil)

    The required permission symbol or nil if no permission required



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 required_permission_for(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 }

  permissions = request_pattern[:permissions]
  return nil unless permissions

  permissions[http_method]
end

#scope_request?(original_request, path) ⇒ Boolean

Returns:

  • (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