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.

Summary by Sourcery

Enforce Foreman RBAC for Insights, Vulnerability, and Advisor traffic and expose mapped permissions to Scalprum-based frontend apps.

New Features:

  • Expose Insights-style permissions to Scalprum providers via a configurable createProviderOptions helper and a useInsightsPermissions hook backed by ForemanContext.

Enhancements:

  • Add fine-grained permission mapping in InsightsApiForwarder to require specific Foreman permissions per Insights, Vulnerability, and Inventory API endpoint before proxying requests.
  • Return explicit 403 responses with an error payload from the UI requests controller when permission checks fail.
  • Extend the RH Cloud plugin with dedicated view/edit permissions for Vulnerability and Advisor and include them in the default plugin role.
  • Preserve backwards compatibility for existing Scalprum usage by providing default provider options with no permissions.

Tests:

  • Add extensive unit tests covering permission resolution in InsightsApiForwarder for Vulnerability, Inventory, and Advisor endpoints, including helper behavior and allowed/denied scenarios.

@sourcery-ai
Copy link

sourcery-ai bot commented Jan 8, 2026

Reviewer's Guide

Implements frontend and backend RBAC wiring for Scalprum-based Insights apps (vulnerability-ui, advisor) by mapping new Foreman plugin permissions to Insights-style permission strings, exposing them via chrome.auth.getUserPermissions, and enforcing them on forwarded API requests, with corresponding tests and UI error handling.

Sequence diagram for frontend Insights permission propagation via getUserPermissions

sequenceDiagram
  actor User
  participant ForemanApp as ForemanReactApp
  participant ForemanContext as ForemanContext
  participant Wrapper as ScalprumWrapperComponent
  participant Hook as useInsightsPermissions
  participant Scalprum as ScalprumProvider
  participant ChromeAuth as chrome_auth
  participant InsightsApp as InsightsMicroFrontend

  User->>ForemanApp: Navigate to Insights page
  ForemanApp->>ForemanContext: Initialize context with metadata.permissions
  ForemanApp->>Wrapper: Render wrapper component

  Wrapper->>Hook: useInsightsPermissions()
  Hook->>ForemanContext: Read metadata.permissions (Set)
  ForemanContext-->>Hook: User Foreman permissions
  Hook->>Hook: Map via PERMISSION_MAPPING
  Hook-->>Wrapper: insightsPermissions array

  Wrapper->>Scalprum: createProviderOptions(insightsPermissions)
  Scaprum->>ChromeAuth: Configure getUserPermissions(permissions)
  Wrapper->>Scalprum: Render ScalprumProvider
  Scalprum->>InsightsApp: Mount scoped module

  InsightsApp->>ChromeAuth: getUserPermissions(app)
  ChromeAuth->>ChromeAuth: Filter permissions by app prefix
  ChromeAuth-->>InsightsApp: Filtered permissions
  InsightsApp->>InsightsApp: Show/hide UI actions based on permissions
Loading

Sequence diagram for backend permission enforcement in InsightsApiForwarder

sequenceDiagram
  participant InsightsApp as InsightsMicroFrontend
  participant Browser
  participant UiController as UiRequestsController
  participant Forwarder as InsightsApiForwarder
  participant User
  participant RemoteAPI as RemoteInsightsAPI

  InsightsApp->>Browser: Invoke API call (method, path)
  Browser->>UiController: HTTP request

  UiController->>Forwarder: forward_request(original_request, path, controller_name, user, organization, location)

  Forwarder->>Forwarder: required_permission_for(path, http_method)
  alt Permission required
    Forwarder->>User: can?(permission)
    alt User allowed
      User-->>Forwarder: true
      Forwarder->>Forwarder: TagsAuth.update_tag if scope_request?
      Forwarder->>RemoteAPI: Forward HTTP request
      RemoteAPI-->>Forwarder: Response
      Forwarder-->>UiController: Forwarded response
      UiController-->>Browser: HTTP response (2xx/4xx/5xx)
    else User denied
      User-->>Forwarder: false
      Forwarder->>Forwarder: logger.warn("lacks permission")
      Forwarder->>UiController: Raise Foreman::Exception
      UiController->>UiController: Rescue Foreman::Exception
      UiController->>UiController: logger.warn("Permission denied for forwarding request")
      UiController-->>Browser: HTTP 403 { message, error }
    end
  else No permission required
    Forwarder->>Forwarder: TagsAuth.update_tag if scope_request?
    Forwarder->>RemoteAPI: Forward HTTP request
    RemoteAPI-->>Forwarder: Response
    Forwarder-->>UiController: Forwarded response
    UiController-->>Browser: HTTP response
  end
Loading

Updated class diagram for Insights RBAC wiring (frontend and backend)

classDiagram
  class InsightsApiForwarder {
    <<RubyService>>
    SCOPED_REQUESTS
    forward_request(original_request, path, controller_name, user, organization, location)
    required_permission_for(path, http_method)
    scope_request?(original_request, path)
    http_user_agent(original_request)
    logger()
  }

  class UiRequestsController {
    <<RailsController>>
    forward_request()
  }

  class ForemanRhCloudPlugin {
    <<ForemanPlugin>>
    register()
    rh_cloud_permissions
    insights_permissions
  }

  class useInsightsPermissions {
    <<ReactHook>>
    +PERMISSION_MAPPING
    +useInsightsPermissions()
  }

  class ForemanContext {
    metadata
    metadata.permissions : Set
    metadata.foreman_rh_cloud
  }

  class ScalprumContext {
    +createGetUserPermissions(permissions)
    +createProviderOptions(permissions)
    +providerOptions
  }

  class ScalprumWrapperComponent {
    +useInsightsPermissions()
    +createProviderOptions(permissions)
  }

  InsightsApiForwarder ..> ForemanRhCloudPlugin : uses plugin permissions
  UiRequestsController --> InsightsApiForwarder : calls forward_request
  ForemanRhCloudPlugin --> ForemanContext : registers app_metadata_registry

  useInsightsPermissions --> ForemanContext : reads metadata.permissions
  ScalprumContext --> useInsightsPermissions : expects permissions format
  ScalprumWrapperComponent --> useInsightsPermissions : calls hook
  ScalprumWrapperComponent --> ScalprumContext : passes permissions to createProviderOptions
Loading

File-Level Changes

Change Details Files
Enforce Foreman RBAC on forwarded Insights, Vulnerability, and Inventory API requests.
  • Extend SCOPED_REQUESTS with HTTP-method-specific permission mappings for inventory, vulnerability, and insights endpoints.
  • Introduce required_permission_for(path, method) helper to resolve needed permissions from SCOPED_REQUESTS.
  • In forward_request, check user.can?(permission) before proxying and raise a Foreman::Exception on violation.
  • Update or add tests to cover allowed/denied cases and helper behavior for all protected endpoints.
app/services/foreman_rh_cloud/insights_api_forwarder.rb
test/unit/services/foreman_rh_cloud/insights_api_forwarder_test.rb
Define dedicated Foreman plugin permissions for Insights Vulnerability and Advisor and include them in the default plugin role.
  • Register view_vulnerability, edit_vulnerability, view_advisor, and edit_advisor permissions with resource_type 'ForemanRhCloud'.
  • Refactor existing plugin permission list into rh_cloud_permissions and insights_permissions arrays and combine them.
  • Register static plugin metadata under :foreman_rh_cloud for ForemanContext usage.
lib/foreman_rh_cloud/plugin.rb
Expose Insights-style permissions to Scalprum apps through a new hook and provider options factory.
  • Add useInsightsPermissions hook that reads ForemanContext.metadata.permissions and maps them to Insights Chrome permission strings via PERMISSION_MAPPING.
  • Introduce createProviderOptions(permissions) in ScalprumContext that wires chrome.auth.getUserPermissions to the mapped permissions and returns a configured providerOptions object.
  • Keep a default providerOptions export that uses createProviderOptions([]) for backwards compatibility.
  • Add unit tests for ScalprumContext behavior (file mentioned but contents not shown).
webpack/common/Hooks/PermissionsHooks.js
webpack/common/ScalprumModule/ScalprumContext.js
webpack/common/ScalprumModule/__tests__/ScalprumContext.test.js
Update all Scalprum-based Insights wrapper components to pass mapped permissions into ScalprumProvider.
  • Replace direct providerOptions usage with createProviderOptions(useInsightsPermissions()) in all wrapper components that mount advisor or vulnerability Scalprum apps.
  • Ensure permissions are computed once per wrapper via the hook before passing into ScalprumProvider.
webpack/CVEsHostDetailsTab/CVEsHostDetailsTab.js
webpack/InsightsCloudSync/InsightsCloudSync.js
webpack/InsightsHostDetailsTab/NewHostDetailsTab.js
webpack/InsightsVulnerability/InsightsVulnerabilityListPage.js
webpack/IopRecommendationDetails/IopRecommendationDetails.js
webpack/CveDetailsPage/CveDetailsPage.js
Return proper 403 responses from UI forwarding controller when permission checks fail.
  • Rescue Foreman::Exception in Insights UIRequestsController#forward_request, log a warning, and render a JSON error payload with HTTP 403 status.
  • Keep existing timeout handling behavior intact.
app/controllers/insights_cloud/ui_requests_controller.rb

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.

@nofaralfasi nofaralfasi marked this pull request as draft January 8, 2026 14:13

// 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 4 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
@nofaralfasi nofaralfasi marked this pull request as ready for review January 18, 2026 16:40
@nofaralfasi nofaralfasi changed the title [WIP] Expose Insights permissions to Scalprum apps via ForemanContext Expose Insights permissions to Scalprum apps via ForemanContext Jan 18, 2026
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 2 issues, and left some high level feedback:

  • The frontend and backend permission mappings are currently duplicated (PERMISSION_MAPPING in PermissionsHooks.js vs SCOPED_REQUESTS in InsightsApiForwarder); consider centralizing these mappings or generating one from the other to avoid drift when endpoints or permissions change.
  • forward_request now raises a generic Foreman::Exception for RBAC failures which is caught and converted to 403 in the UI controller; it may be clearer and safer to introduce a dedicated permission error type so other call sites can distinguish RBAC failures from other exceptional conditions.
  • required_permission_for relies on the ordering of SCOPED_REQUESTS (e.g., inventory hosts and specific insights endpoints before broader regexes); adding a brief comment near SCOPED_REQUESTS about this ordering requirement would help future maintainers avoid subtle bugs when extending the list.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The frontend and backend permission mappings are currently duplicated (PERMISSION_MAPPING in PermissionsHooks.js vs SCOPED_REQUESTS in InsightsApiForwarder); consider centralizing these mappings or generating one from the other to avoid drift when endpoints or permissions change.
- forward_request now raises a generic Foreman::Exception for RBAC failures which is caught and converted to 403 in the UI controller; it may be clearer and safer to introduce a dedicated permission error type so other call sites can distinguish RBAC failures from other exceptional conditions.
- required_permission_for relies on the ordering of SCOPED_REQUESTS (e.g., inventory hosts and specific insights endpoints before broader regexes); adding a brief comment near SCOPED_REQUESTS about this ordering requirement would help future maintainers avoid subtle bugs when extending the list.

## Individual Comments

### Comment 1
<location> `app/services/foreman_rh_cloud/insights_api_forwarder.rb:120-122` </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&.can?(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):** Guard against nil `user` when logging missing permission

The permission guard uses `user&.can?`, but the log line calls `user.login` unconditionally. If `user` is nil, this will raise `NoMethodError` instead of returning a 403. Use something like `user&.login || 'anonymous'` (or similar) in the log message.
</issue_to_address>

### Comment 2
<location> `app/controllers/insights_cloud/ui_requests_controller.rb:21-24` </location>
<code_context>
           @organization,
           @location
         )
+      rescue ::Foreman::Exception => e
+        logger.warn("Permission denied for forwarding request: #{e}")
+        message = e.message
+        return render json: { message: message, error: message }, status: :forbidden
       rescue RestClient::Exceptions::Timeout => e
         response_obj = e.response.presence || e.exception
</code_context>

<issue_to_address>
**issue (bug_risk):** Rescuing all `Foreman::Exception` as 403 may mask unrelated errors

`Foreman::Exception` is now used for permission issues, but other (current or future) call paths in `forward_request` may also raise it for non-RBAC reasons. Rescuing the base class and always returning 403 risks misclassifying real server errors as authorization failures. Consider rescuing a more specific subclass (e.g. a dedicated permission error) or checking the exception details to distinguish permission failures from other cases.
</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.

@nofaralfasi nofaralfasi force-pushed the SAT-40629 branch 5 times, most recently from 4720a17 to 381c64d Compare January 20, 2026 16:22
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