Skip to content

Conversation

@nofaralfasi
Copy link
Collaborator

@nofaralfasi nofaralfasi commented Jan 8, 2026

ToDo: Improve the displayed error message on the Insights page if the user does not have the necessary permissions.

Requires #1136.

What are the changes introduced in this pull request?

Add Chrome API getUserPermissions() implementation for vulnerability-ui
and advisor apps to query user permissions and control UI accordingly.

Implements frontend RBAC for Scalprum-loaded Insights applications
(vulnerability-ui, advisor) by exposing user permissions through
the Chrome API getUserPermissions() interface.

Changes:

  • Add useInsightsPermissions() hook that reads Foreman permissions
    from ForemanContext (PR #10338) and converts them to Chrome API format
  • Add createProviderOptions() factory to pass permissions to ScalprumProvider
  • Update all Scalprum wrapper components to use the new permission flow
  • Add permission denied handling in UI requests controller (403 response)

Permission mapping (Foreman → Chrome API):
view_vulnerability → inventory:hosts:read, vulnerability::read
edit_vulnerability → vulnerability:
:write
view_advisor → advisor::read
edit_advisor → advisor:
:write

This enables Insights frontend apps to check user permissions and
show/hide UI elements based on RBAC, complementing the existing
backend permission enforcement in the API forwarder.

Considerations taken when implementing this change?

  1. Chrome API Compatibility: Implemented getUserPermissions() to match the Insights Chrome API specification, returning permissions in the expected format ({ permission: string, resourceDefinitions: [] }). This allows Scalprum-loaded Insights apps (vulnerability-ui, advisor) to use their existing RBAC logic without modification.
  2. Synchronous Availability: Permissions are read from ForemanContext, which is initialized when the React app mounts. This ensures permissions are available before Scalprum components render, avoiding race conditions or loading states in the permission checks.
  3. Using Foreman's Native Permission System: Rather than creating a custom permission mechanism, we leverage Foreman's context-based permission system introduced in PR #10338. This ensures consistency with Foreman's RBAC architecture and avoids duplicating permission logic.
  4. Chrome API Compatibility: Scalprum-loaded Insights apps (vulnerability-ui, advisor) expect permissions in Chrome API format (app:resource:action). The implementation includes a mapping layer that converts Foreman permission names (e.g., view_vulnerability) to the Chrome API format (e.g., vulnerability:vulnerability_results:read).
  5. Frontend vs Backend Enforcement: This change implements frontend RBAC only - it controls UI visibility and user experience. The authoritative permission enforcement remains on the backend via SCOPED_REQUESTS in the API forwarder. Frontend permissions are supplementary and should never be the sole security mechanism.
  6. Performance - Memoization: The useInsightsPermissions hook uses useMemo to prevent rebuilding the permissions array on every React render, reducing unnecessary computation.

What are the testing steps for this pull request?

  1. Verify that all tests pass successfully. (tesing is still in progress)

Prerequisites
Newer Advisor frontend that respects RBAC - The advisor-ui must be a version that queries chrome.auth.getUserPermissions() and respects the returned permissions for UI access control

  1. Test user with view-only permissions:

    • Create a role with only view permissions (no edit)
    • Assign role to a test user and log in
    • Navigate to Insights → Recommendations/Vulnerability
    • Verify page loads and displays recommendations
    • Verify edit actions are hidden
  2. Test user with edit permissions:

    • Create a role with both view and edit permissions
    • Assign role to a test user and log in
    • Navigate to Insights → Recommendations/Vulnerability
    • Verify all actions are enabled

Summary by Sourcery

Expose Insights RBAC permissions from Foreman to Scalprum-based frontends and enforce them on forwarded Insights and Vulnerability API requests.

New Features:

  • Expose Insights-style user permissions to Scalprum frontends via chrome.auth.getUserPermissions backed by backend-injected permission data.
  • Introduce a permissions helper that maps Foreman RBAC permissions to Insights permission strings for Vulnerability and Advisor applications.
  • Add hidden HTML injection of serialized user permissions that are initialized into a global window object for frontend consumption.

Enhancements:

  • Enforce Foreman RBAC on forwarded Vulnerability, Inventory, and Advisor API requests by mapping HTTP paths and methods to required permissions before proxying.
  • Extend the RH Cloud plugin with dedicated Vulnerability and Advisor view/edit permissions and include them in the default plugin role.
  • Return explicit 403 responses from the UI forwarding controller when permission checks fail to inform frontend clients.

Tests:

  • Add comprehensive unit tests covering permission mapping, Insights permission formatting, and combinations of user capabilities.
  • Add unit tests verifying permission enforcement for Insights and Vulnerability API forwarding, including allowed and denied cases and helper behavior.

@sourcery-ai
Copy link

sourcery-ai bot commented Jan 8, 2026

Reviewer's Guide

Implements Insights-style RBAC exposure and enforcement for Vulnerability and Advisor UIs by mapping Foreman permissions to Insights permissions, injecting them into the HTML for Scalprum/Chrome getUserPermissions(), and enforcing those permissions on proxied Insights/Vulnerability API calls with corresponding tests and controller handling.

Sequence diagram for chrome.auth.getUserPermissions flow in Scalprum

sequenceDiagram
  actor User
  participant Browser as Browser
  participant FC as ForemanRhCloudController
  participant FHP as ForemanRhCloudPermissionsHelper
  participant VIEW as _permissions_data.html.erb
  participant PI as PermissionsInit.js
  participant GF as window.__foreman.permissions
  participant SC as ScalprumContext.auth
  participant FE as Insights frontend app

  User->>Browser: Navigate to Insights page
  Browser->>FC: HTTP GET /foreman_rh_cloud
  FC->>FHP: insights_user_permissions()
  FHP-->>FC: Array of {permission, resourceDefinitions}
  FC->>VIEW: Render partial with data-permissions attribute
  VIEW-->>Browser: HTML with div#foreman-rh-cloud-permissions

  Browser->>PI: Execute on page load
  PI->>PI: Read data-permissions from permissions div
  PI->>GF: Parse JSON and set permissions array

  User->>FE: Interact with Vulnerability/Advisor UI
  FE->>SC: chrome.auth.getUserPermissions('vulnerability')
  SC->>GF: getUserPermissions('vulnerability')
  GF-->>SC: Filtered permissions starting with vulnerability:
  SC-->>FE: Promise.resolve(filtered permissions)
  FE->>FE: Enable/disable UI actions based on permissions
Loading

Sequence diagram for permission-enforced forwarding of Insights/Vulnerability API calls

sequenceDiagram
  actor User
  participant FE as Insights frontend app
  participant Browser as Browser
  participant UIC as UiRequestsController
  participant IAF as InsightsApiForwarder
  participant USER as User
  participant VA as Vulnerability/Advisor/Inventory APIs

  User->>FE: Trigger action (e.g. change CVE status)
  FE->>Browser: XHR to Foreman proxy endpoint
  Browser->>UIC: HTTP PATCH /insights_cloud/ui_request (path + method)
  UIC->>IAF: forward_request(original_request, path, controller_name, user, organization, location)

  IAF->>IAF: required_permission_for(path, http_method)
  alt Permission required
    IAF->>USER: user_has_permission?(user, permission)
    alt User has permission
      IAF->>VA: Forward request with tags
      VA-->>IAF: API response
      IAF-->>UIC: Proxied response
      UIC-->>Browser: HTTP 200/2xx with data
      Browser-->>FE: Resolve XHR
    else User lacks permission
      IAF->>UIC: Raise Foreman::Exception (permission denied)
      UIC-->>Browser: HTTP 403 { message, error }
      Browser-->>FE: Reject XHR (forbidden)
    end
  else No permission required
    IAF->>VA: Forward request with tags
    VA-->>IAF: API response
    IAF-->>UIC: Proxied response
    UIC-->>Browser: HTTP 2xx
  end
Loading

Class diagram for Foreman RH Cloud RBAC mapping and enforcement

classDiagram
  class ForemanRhCloudController {
    +index()
    +recommendations()
  }

  class ForemanRhCloudPermissionsHelper {
    <<module>>
    +PERMISSION_MAPPING
    +insights_user_permissions() Array
    +permissions_to_insights_format(permission Symbol) Array
  }

  class InsightsApiForwarder {
    +SCOPED_REQUESTS
    +forward_request(original_request, path, controller_name, user, organization, location)
    +required_permission_for(path String, http_method String) Symbol
    +user_has_permission?(user User, permission Symbol) Boolean
    +http_user_agent(original_request)
    +logger()
  }

  class UiRequestsController {
    +forward_request()
  }

  class User {
    +login
    +can?(permission Symbol) Boolean
  }

  ForemanRhCloudController ..|> ApplicationController
  ForemanRhCloudController ..> ForemanRhCloudPermissionsHelper : includes
  ForemanRhCloudController ..> UiRequestsController : routes to

  ForemanRhCloudPermissionsHelper ..> User : uses User.current

  UiRequestsController ..> InsightsApiForwarder : calls forward_request

  InsightsApiForwarder ..> User : calls user_has_permission?
  InsightsApiForwarder ..> SCOPED_REQUESTS : uses for required_permission_for
Loading

File-Level Changes

Change Details Files
Add permission-aware forwarding for Insights and Vulnerability API requests based on Foreman RBAC.
  • Extend SCOPED_REQUESTS to include per-path/per-method permission mappings for inventory, vulnerability, and insights endpoints
  • Before forwarding, resolve required permission from path+HTTP method and raise a Foreman::Exception if the user lacks it
  • Add helpers required_permission_for and user_has_permission? to encapsulate permission lookup and checks
  • Keep existing tag-scoping behavior intact while layering permission checks on top
app/services/foreman_rh_cloud/insights_api_forwarder.rb
test/unit/services/foreman_rh_cloud/insights_api_forwarder_test.rb
Introduce Foreman plugin-level permissions for Vulnerability and Advisor and map them to Insights Chrome permission strings.
  • Define new permissions :view_vulnerability, :edit_vulnerability, :view_advisor, :edit_advisor in the plugin with ForemanRhCloud resource_type
  • Extend the default ForemanRhCloud role to include the new Insights-related permissions
  • Create a ForemanRhCloudPermissionsHelper with a PERMISSION_MAPPING from Foreman to Insights permissions and helpers to emit Chrome API–shaped permission objects
  • Thoroughly test PERMISSION_MAPPING and insights_user_permissions/permissions_to_insights_format behavior for all combinations of permissions
lib/foreman_rh_cloud/plugin.rb
app/helpers/foreman_rh_cloud_permissions_helper.rb
test/unit/helpers/foreman_rh_cloud_permissions_helper_test.rb
Expose user permissions to Scalprum/Chrome frontend via HTML injection and a getUserPermissions() implementation.
  • Add PermissionsInit.js that reads JSON-encoded permissions from a hidden DOM element and populates window.__foreman.permissions
  • Add a Rails partial that renders the hidden permissions div with data-permissions set from insights_user_permissions in Chrome API format
  • Wire ForemanRhCloudPermissionsHelper into the ForemanRhCloudController and views so the helper is available when rendering the React layout
  • Update ScalprumContext to implement chrome.auth.getUserPermissions by reading and optionally filtering window.__foreman.permissions
webpack/PermissionsInit.js
app/views/foreman_rh_cloud/_permissions_data.html.erb
app/controllers/foreman_rh_cloud/foreman_rh_cloud_controller.rb
webpack/common/ScalprumModule/ScalprumContext.js
Improve UI request error handling so permission failures become proper 403 responses for frontend consumers.
  • Rescue Foreman::Exception in ui_requests_controller.forward_request and translate it into a 403 JSON response with message and error fields
  • Log permission-denied events for troubleshooting
app/controllers/insights_cloud/ui_requests_controller.rb
Backfill and expand automated tests for new permission behavior and Scalprum integration.
  • Add extensive unit tests for insights_api_forwarder to cover allow/deny behavior for each mapped endpoint and helper methods
  • Add unit tests for ForemanRhCloudPermissionsHelper mapping and output structure
  • Add (or extend) Jest tests around ScalprumContext to validate getUserPermissions behavior (file referenced but diff contents not shown)
test/unit/services/foreman_rh_cloud/insights_api_forwarder_test.rb
test/unit/helpers/foreman_rh_cloud_permissions_helper_test.rb
webpack/common/ScalprumModule/__tests__/ScalprumContext.test.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues, and left some high level feedback:

  • In InsightsApiForwarder#forward_request, the permission-denied log message unconditionally calls user.login, which will raise if user is nil; consider guarding this (e.g., user&.login || 'anonymous') or requiring a non-nil user to avoid unexpected errors.
  • The permission mapping between endpoints and Foreman permissions in SCOPED_REQUESTS and the mapping from Foreman to Insights permissions in ForemanRhCloudPermissionsHelper::PERMISSION_MAPPING are tightly coupled but maintained separately; consider centralizing or reusing these definitions to reduce the risk of the mappings drifting out of sync.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `InsightsApiForwarder#forward_request`, the permission-denied log message unconditionally calls `user.login`, which will raise if `user` is nil; consider guarding this (e.g., `user&.login || 'anonymous'`) or requiring a non-nil user to avoid unexpected errors.
- The permission mapping between endpoints and Foreman permissions in `SCOPED_REQUESTS` and the mapping from Foreman to Insights permissions in `ForemanRhCloudPermissionsHelper::PERMISSION_MAPPING` are tightly coupled but maintained separately; consider centralizing or reusing these definitions to reduce the risk of the mappings drifting out of sync.

## Individual Comments

### Comment 1
<location> `app/services/foreman_rh_cloud/insights_api_forwarder.rb:120-123` </location>
<code_context>

     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_has_permission?(user, permission)
+        logger.warn("User #{user.login} lacks permission #{permission} for #{original_request.request_method} #{path}")
+        raise ::Foreman::Exception.new(N_("You do not have permission to perform this action"))
+      end
+
</code_context>

<issue_to_address>
**issue (bug_risk):** Use a more specific or HTTP-aware exception type for authorization failures.

Raising a generic `Foreman::Exception` here means only `InsightsCloud::UiRequestsController` currently maps this to a 403; any other caller that doesn’t rescue it will return a 500 instead. Either use a more specific/HTTP-aware exception (e.g., one that implies 403) or handle the mapping to HTTP status at this layer so all callers get consistent authorization behavior.
</issue_to_address>

### Comment 2
<location> `app/services/foreman_rh_cloud/insights_api_forwarder.rb:232-239` </location>
<code_context>
+    # @param path [String] The request path
+    # @param http_method [String] The HTTP method (GET, POST, etc.)
+    # @return [Symbol, nil] The required permission symbol or nil if no permission required
+    def required_permission_for(path, http_method)
+      request_pattern = SCOPED_REQUESTS.find { |pattern| pattern[:test].match?(path) }
+      return nil unless request_pattern
+
+      permissions = request_pattern[:permissions]
+      return nil unless permissions
+
+      permissions[http_method]
+    end
+
</code_context>

<issue_to_address>
**🚨 suggestion (security):** Relying on first regex match in SCOPED_REQUESTS makes permission resolution order-dependent.

Because `required_permission_for` returns the first `SCOPED_REQUESTS` entry whose `:test` matches the path, overlapping patterns (e.g., `api/insights/v1/ack` vs `api/insights/v1/.*`) make the permission depend on array order. A later insertion of a broader pattern above a specific one could silently change which permission is enforced. Consider either prioritizing the most specific matcher (e.g., by length or constraints) or restructuring patterns so they don’t overlap and authorization can’t be altered by reordering.

Suggested implementation:

```ruby
    # 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.
    # @param path [String] The request path
    # @param http_method [String] The HTTP method (GET, POST, etc.)
    # @return [Symbol, nil] The required permission symbol or nil if no permission required

```

```ruby
    # Returns the required permission for the given path and HTTP method
    # @param path [String] The request path
    # @param http_method [String] The HTTP method (GET, POST, etc.)
    # @return [Symbol, nil] The required permission symbol or nil if no permission required
    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

```
</issue_to_address>

### Comment 3
<location> `webpack/common/ScalprumModule/ScalprumContext.js:58-78` </location>
<code_context>
+ * @param {boolean} _bypassCache - Optional flag to bypass cache (not used in Foreman)
+ * @returns {Promise<Array<{permission: string, resourceDefinitions: Array}>>} Array of permission objects
+ */
+const getUserPermissions = async (app, _bypassCache) => {
+  // Get all permissions from backend (already in Insights format)
+  const allPermissions = window.__foreman?.permissions || [];
+
+  // If app is specified, filter by app prefix
+  if (app) {
+    const filtered = allPermissions.filter(
+      p => p.permission && p.permission.startsWith(`${app}:`)
+    );
+    return filtered;
+  }
+
+  // Return all permissions
+  return allPermissions;
+};
+
</code_context>

<issue_to_address>
**suggestion:** Be mindful of initialization order between getUserPermissions and PermissionsInit.js.

This relies on `window.__foreman.permissions` being populated by `PermissionsInit.js`. If `chrome.auth.getUserPermissions()` is called before that script runs, `allPermissions` will be `[]` and permissions will appear missing for the session unless callers requery. Please ensure `PermissionsInit.js` is loaded before this context, or clearly document that `getUserPermissions` assumes `window.__foreman.permissions` has already been initialized.

```suggestion
 * @see https://github.com/RedHatInsights/frontend-components/blob/master/docs/chrome/chrome-api.md#getuserpermissions
 * @param {string} app - Optional app name to filter permissions (e.g., 'vulnerability', 'inventory')
 * @param {boolean} _bypassCache - Optional flag to bypass cache (not used in Foreman)
 * @returns {Promise<Array<{permission: string, resourceDefinitions: Array}>>} Array of permission objects
 * @throws {Error} If called before `window.__foreman.permissions` is initialized and strict initialization is enforced.
 *
 * Note:
 * This function assumes that `PermissionsInit.js` has already run and initialized
 * `window.__foreman.permissions`. If `chrome.auth.getUserPermissions()` is invoked
 * before that initialization, it will return an empty array and log a warning.
 * Callers should ensure `PermissionsInit.js` is loaded first, or be prepared to
 * requery after initialization.
 */
+let hasWarnedAboutMissingForemanPermissions = false;
+
+const getUserPermissions = async (app, _bypassCache) => {
+  const permissionsInitialized =
+    typeof window !== 'undefined' &&
+    window.__foreman &&
+    Array.isArray(window.__foreman.permissions);
+
+  // Warn (once) if permissions have not yet been initialized by PermissionsInit.js
+  if (!permissionsInitialized && !hasWarnedAboutMissingForemanPermissions) {
+    hasWarnedAboutMissingForemanPermissions = true;
+    // eslint-disable-next-line no-console
+    console.warn(
+      '[ScalprumContext] getUserPermissions called before PermissionsInit.js initialized window.__foreman.permissions. Returning empty permissions array.'
+    );
+  }
+
+  // Get all permissions from backend (already in Insights format) or fallback to empty array
+  const allPermissions = (permissionsInitialized && window.__foreman.permissions) || [];
+
+  // If app is specified, filter by app prefix
+  if (app) {
+    return allPermissions.filter(
+      (p) => p.permission && p.permission.startsWith(`${app}:`)
+    );
+  }
+
+  // Return all permissions
+  return allPermissions;
+};
+
```
</issue_to_address>

### Comment 4
<location> `test/unit/services/foreman_rh_cloud/insights_api_forwarder_test.rb:240-216` </location>
<code_context>
+    @forwarder.forward_request(req, 'api/inventory/v1/hosts', 'test_controller', @user, @organization, @location)
+  end
+
+  test 'should deny GET request to inventory hosts when user lacks view_vulnerability permission' do
+    user_agent = { :foo => :bar }
+    params = {}
+
+    req = ActionDispatch::Request.new(
+      'REQUEST_URI' => '/api/inventory/v1/hosts',
+      'REQUEST_METHOD' => 'GET',
+      'HTTP_USER_AGENT' => user_agent,
+      'rack.input' => ::Puma::NullIO.new,
+      'action_dispatch.request.query_parameters' => params
+    )
+
+    @user.stubs(:can?).with(:view_vulnerability).returns(false)
+
+    assert_raises(::Foreman::Exception) do
+      @forwarder.forward_request(req, 'api/inventory/v1/hosts', 'test_controller', @user, @organization, @location)
+    end
+  end
+
</code_context>

<issue_to_address>
**suggestion (testing):** Add expectations that `execute_cloud_request` is not called when permission checks fail

To better validate the permission enforcement, also assert that `execute_cloud_request` is never called when permissions are missing, e.g.:

```ruby
@forwarder.expects(:execute_cloud_request).never
assert_raises(::Foreman::Exception) do
  @forwarder.forward_request(...)
end
```

Applying this to the other deny-tests (vulnerability and advisor endpoints) will ensure we don’t accidentally forward a request after a failed permission check.

Suggested implementation:

```ruby
  test 'should deny GET request to inventory hosts when user lacks view_vulnerability permission' do
    user_agent = { :foo => :bar }
    params = {}

    req = ActionDispatch::Request.new(
      'REQUEST_URI' => '/api/inventory/v1/hosts',
      'REQUEST_METHOD' => 'GET',
      'HTTP_USER_AGENT' => user_agent,
      'rack.input' => ::Puma::NullIO.new,
      'action_dispatch.request.query_parameters' => params
    )

    @user.stubs(:can?).with(:view_vulnerability).returns(false)
    @forwarder.expects(:execute_cloud_request).never

    assert_raises(::Foreman::Exception) do
      @forwarder.forward_request(req, 'api/inventory/v1/hosts', 'test_controller', @user, @organization, @location)
    end
  end

```

To fully implement your suggestion for all deny-tests:
1. Locate the other tests that assert denial when permissions are missing for the vulnerability and advisor endpoints (e.g., "should deny ... when user lacks view_vulnerability" / "view_advisor" or similar).
2. In each of those tests, right after the `@user.stubs(:can?).with(...).returns(false)` line, add:
   `@forwarder.expects(:execute_cloud_request).never`
3. Ensure this expectation appears **before** the `assert_raises` block so that Mocha verifies `execute_cloud_request` is not called when permission checks fail.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +120 to +123
permission = required_permission_for(path, original_request.request_method)
if permission && !user_has_permission?(user, permission)
logger.warn("User #{user.login} lacks permission #{permission} for #{original_request.request_method} #{path}")
raise ::Foreman::Exception.new(N_("You do not have permission to perform this action"))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Use a more specific or HTTP-aware exception type for authorization failures.

Raising a generic Foreman::Exception here means only InsightsCloud::UiRequestsController currently maps this to a 403; any other caller that doesn’t rescue it will return a 500 instead. Either use a more specific/HTTP-aware exception (e.g., one that implies 403) or handle the mapping to HTTP status at this layer so all callers get consistent authorization behavior.

Comment on lines +232 to +239
def required_permission_for(path, http_method)
request_pattern = SCOPED_REQUESTS.find { |pattern| pattern[:test].match?(path) }
return nil unless request_pattern

permissions = request_pattern[:permissions]
return nil unless permissions

permissions[http_method]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 suggestion (security): Relying on first regex match in SCOPED_REQUESTS makes permission resolution order-dependent.

Because required_permission_for returns the first SCOPED_REQUESTS entry whose :test matches the path, overlapping patterns (e.g., api/insights/v1/ack vs api/insights/v1/.*) make the permission depend on array order. A later insertion of a broader pattern above a specific one could silently change which permission is enforced. Consider either prioritizing the most specific matcher (e.g., by length or constraints) or restructuring patterns so they don’t overlap and authorization can’t be altered by reordering.

Suggested implementation:

    # 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.
    # @param path [String] The request path
    # @param http_method [String] The HTTP method (GET, POST, etc.)
    # @return [Symbol, nil] The required permission symbol or nil if no permission required
    # Returns the required permission for the given path and HTTP method
    # @param path [String] The request path
    # @param http_method [String] The HTTP method (GET, POST, etc.)
    # @return [Symbol, nil] The required permission symbol or nil if no permission required
    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

@nofaralfasi nofaralfasi marked this pull request as draft January 8, 2026 14:13
@@ -0,0 +1,4 @@
<%# Injects user permissions for Scalprum/Chrome API integration %>
<%# Consumed by PermissionsInit.js to populate window.__foreman.permissions %>
<div id="foreman-rh-cloud-permissions" data-permissions="<%= insights_user_permissions.to_json %>" style="display:none;"></div>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting data in the DOM seems hacky and IMO should be a last resort. Have you considered registering the permissions in ForemanContext? https://github.com/theforeman/foreman/blob/4181accb7f4efd93a4aebbd84de57f4120decc9b/developer_docs/foreman-context.asciidoc#adding-a-value-to-the-context-from-a-plugin

Or another pattern we use widely is to have an include_permissions param on some API endpoint, but not sure if that's relevant here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, seems like this open Foreman PR is super-relevant! theforeman/foreman#10338

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@@ -0,0 +1,4 @@
<%# Injects user permissions for Scalprum/Chrome API integration %>
<%# Consumed by PermissionsInit.js to populate window.__foreman.permissions %>
<div id="foreman-rh-cloud-permissions" data-permissions="<%= insights_user_permissions.to_json %>" style="display:none;"></div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1


// Return all permissions
return allPermissions;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: can be written functionally with ternary (instead of procedurally)

@nofaralfasi nofaralfasi force-pushed the SAT-40629 branch 2 times, most recently from 999aaaa to 90fdf56 Compare January 13, 2026 15:20
@nofaralfasi nofaralfasi force-pushed the SAT-40629 branch 2 times, most recently from 9167d2a to f2c8b54 Compare January 13, 2026 17:13
@nofaralfasi nofaralfasi changed the title Expose Insights user permissions to Scalprum frontend components [WIP] Expose Insights user permissions to Scalprum frontend components Jan 14, 2026
@nofaralfasi nofaralfasi changed the title [WIP] Expose Insights user permissions to Scalprum frontend components [WIP] Expose Insights permissions to Scalprum apps via ForemanContext Jan 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants