Skip to content

Conversation

@nofaralfasi
Copy link
Collaborator

@nofaralfasi nofaralfasi commented Jan 8, 2026

Requires #1135.

What are the changes introduced in this pull request?

Add permission enforcement for Insights API forwarding.

Implement permission checks in InsightsApiForwarder before proxying
requests to Insights cloud. Users without appropriate view/edit
permissions for Vulnerability or Advisor apps receive 403 Forbidden.

Add ForemanRhCloudPermissionsHelper to map Foreman permissions to
Insights Chrome API format for frontend UI access control.

Includes comprehensive tests for all protected endpoints.

Considerations taken when implementing this change?

This is Part 2 of a 3-part PR series.

What are the testing steps for this pull request?

This PR should be tested together with the related PRs. For now, please ensure that all Ruby tests pass.

Summary by Sourcery

Enforce Foreman role-based permissions when forwarding Insights-related API requests and expose corresponding Insights Chrome permissions for the frontend.

Enhancements:

  • Add permission-aware routing in InsightsApiForwarder so specific inventory, vulnerability, and advisor endpoints require appropriate view/edit permissions before proxying.
  • Extend RH Cloud plugin to define dedicated view/edit permissions for Vulnerability and Advisor applications and include them in the default plugin role.
  • Introduce a helper to translate Foreman Vulnerability and Advisor permissions into Insights Chrome API permission strings for UI access control.

Tests:

  • Add extensive unit coverage for InsightsApiForwarder permission handling, including per-endpoint permission resolution and request authorization behavior.
  • Add unit tests for the permissions helper to verify correct mapping, aggregation, and Chrome API structure of exposed Insights permissions.

@sourcery-ai
Copy link

sourcery-ai bot commented Jan 8, 2026

Reviewer's Guide

Implements permission-scoped forwarding for Insights API requests by enforcing Foreman-level Vulnerability and Advisor permissions in the InsightsApiForwarder and exposing a helper that maps those permissions to Insights Chrome API permission strings for frontend access control, with comprehensive unit tests for both the forwarder and the helper plus plugin permission wiring.

Sequence diagram for permission-scoped Insights API forwarding

sequenceDiagram
    actor User
    participant ForemanController
    participant InsightsApiForwarder
    participant UserModel as User
    participant InsightsCloud

    User ->> ForemanController: HTTP request (method, path)
    ForemanController ->> InsightsApiForwarder: forward_request(original_request, path, controller_name, user, organization, location)

    InsightsApiForwarder ->> InsightsApiForwarder: required_permission_for(path, original_request.request_method)
    alt Permission required
        InsightsApiForwarder ->> InsightsApiForwarder: user_has_permission?(user, permission)
        alt User has permission
            InsightsApiForwarder ->> InsightsApiForwarder: scope_request?(original_request, path)
            alt Scoped request
                InsightsApiForwarder ->> TagsAuth: new(user, organization, location, logger)
                TagsAuth ->> TagsAuth: update_tag
            end
            InsightsApiForwarder ->> InsightsApiForwarder: prepare_forward_params(original_request, path, user, organization, location)
            InsightsApiForwarder ->> InsightsCloud: Forward proxied request
            InsightsCloud -->> InsightsApiForwarder: Response
            InsightsApiForwarder -->> ForemanController: Response
            ForemanController -->> User: HTTP response
        else User lacks permission
            InsightsApiForwarder ->> InsightsApiForwarder: logger.warn("User login lacks permission ...")
            InsightsApiForwarder ->> ForemanController: Raise Foreman::Exception (Forbidden)
            ForemanController -->> User: 403 Forbidden
        end
    else No permission required
        InsightsApiForwarder ->> InsightsApiForwarder: scope_request?(original_request, path)
        alt Scoped request
            InsightsApiForwarder ->> TagsAuth: new(user, organization, location, logger)
            TagsAuth ->> TagsAuth: update_tag
        end
        InsightsApiForwarder ->> InsightsCloud: Forward proxied request
        InsightsCloud -->> InsightsApiForwarder: Response
        InsightsApiForwarder -->> ForemanController: Response
        ForemanController -->> User: HTTP response
    end
Loading

Updated class diagram for InsightsApiForwarder and permissions helper

classDiagram
    class ForemanRhCloud_InsightsApiForwarder {
      <<service>>
      SCOPED_REQUESTS
      +forward_request(original_request, path, controller_name, user, organization, location)
      +scope_request?(original_request, path)
      +prepare_forward_params(original_request, path, user, organization, location)
      +http_user_agent(original_request)
      +logger()
      +required_permission_for(path, http_method)
      +user_has_permission?(user, permission)
    }

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

    class User {
      +login : String
      +can?(permission) Bool
    }

    class TagsAuth {
      +TagsAuth(user, organization, location, logger)
      +update_tag()
    }

    class ForemanRhCloudPlugin {
      <<plugin>>
      +register()
      rh_cloud_permissions : Array~Symbol~
      insights_permissions : Array~Symbol~
      plugin_permissions : Array~Symbol~
    }

    ForemanRhCloud_InsightsApiForwarder --> User : uses_for_permission_checks
    ForemanRhCloud_InsightsApiForwarder --> TagsAuth : creates_and_uses
    ForemanRhCloudPermissionsHelper --> User : uses_current_user
    ForemanRhCloudPlugin ..> ForemanRhCloud_InsightsApiForwarder : defines_permissions_for
    ForemanRhCloudPlugin ..> ForemanRhCloudPermissionsHelper : defines_permissions_for
Loading

File-Level Changes

Change Details Files
Enforce Foreman Vulnerability and Advisor permissions on forwarded Insights API requests before executing cloud calls.
  • Extend SCOPED_REQUESTS to include per-path, per-HTTP-method permission metadata for inventory, vulnerability, and insights endpoints while preserving tag scoping behavior.
  • Add a permission check at the start of forward_request that derives the required permission via required_permission_for and raises a Foreman::Exception when the user lacks it, logging a warning with user, method, and path.
  • Introduce private helpers required_permission_for and user_has_permission? that resolve the required permission from SCOPED_REQUESTS and call user.can? safely when a user is present.
app/services/foreman_rh_cloud/insights_api_forwarder.rb
Define and wire new Foreman permissions for Insights Vulnerability and Advisor features into the plugin role.
  • Register four new permissions (view_vulnerability, edit_vulnerability, view_advisor, edit_advisor) scoped to the ForemanRhCloud resource type in the plugin.
  • Refactor plugin permission lists into rh_cloud_permissions and insights_permissions and build the ForemanRhCloud role from their concatenation.
lib/foreman_rh_cloud/plugin.rb
Expose a helper that maps Foreman permissions to Insights Chrome API-style permission objects for frontend usage, with full test coverage.
  • Add ForemanRhCloudPermissionsHelper with a PERMISSION_MAPPING constant mapping each new Foreman permission to the corresponding Insights permission strings.
  • Implement insights_user_permissions to read User.current, test each mapped Foreman permission with can?, and emit an array of {:permission, :resourceDefinitions => []} hashes for all granted Insights permissions.
  • Provide permissions_to_insights_format to get the Insights permission strings for a given Foreman permission symbol.
  • Add comprehensive unit tests covering PERMISSION_MAPPING contents, all mapping combinations, empty user handling, Chrome API object shape, and behavior for unknown permissions.
app/helpers/foreman_rh_cloud_permissions_helper.rb
test/unit/helpers/foreman_rh_cloud_permissions_helper_test.rb
Add exhaustive unit tests for permission enforcement behavior in InsightsApiForwarder for both Vulnerability and Advisor endpoints.
  • Add tests that build ActionDispatch::Request instances for each protected path/method pair and assert that requests succeed when the user has the required permission and raise Foreman::Exception when they do not.
  • Verify that unprotected endpoints such as vulnerability dashbar still forward without permission checks but continue to trigger tag updates and execute_cloud_request.
  • Test required_permission_for and user_has_permission? helpers directly for all mapped endpoints, unprotected endpoints, unknown paths, and nil user cases, including Advisor-specific mappings (view_advisor/edit_advisor).
test/unit/services/foreman_rh_cloud/insights_api_forwarder_test.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 forward_request, the log line User #{user.login}... will raise if user is ever nil (while user_has_permission? explicitly supports a nil user); consider guarding or using safe navigation to avoid a NoMethodError.
  • The permission-enforcement tests build many similar ActionDispatch::Request objects inline; extracting a small helper/factory for common cases (GET with query params, POST/PATCH with body, etc.) would make the tests shorter and easier to maintain.
  • Tests currently rely on the RAW_POST_DATA key when setting up request bodies, which is deprecated in modern Rack/Rails; consider switching to writing the body to rack.input (e.g., StringIO.new(post_data)) to better match real request handling.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `forward_request`, the log line `User #{user.login}...` will raise if `user` is ever nil (while `user_has_permission?` explicitly supports a nil user); consider guarding or using safe navigation to avoid a `NoMethodError`.
- The permission-enforcement tests build many similar `ActionDispatch::Request` objects inline; extracting a small helper/factory for common cases (GET with query params, POST/PATCH with body, etc.) would make the tests shorter and easier to maintain.
- Tests currently rely on the `RAW_POST_DATA` key when setting up request bodies, which is deprecated in modern Rack/Rails; consider switching to writing the body to `rack.input` (e.g., `StringIO.new(post_data)`) to better match real request handling.

## Individual Comments

### Comment 1
<location> `app/services/foreman_rh_cloud/insights_api_forwarder.rb:12-21` </location>
<code_context>
+    # view_vulnerability   | GET /api/vulnerability/v1/*
</code_context>

<issue_to_address>
**🚨 issue (security):** Permission table comments for vulnerability GET endpoints don't match the actual enforcement logic.

The header states that `view_vulnerability` is required for all `GET /api/vulnerability/v1/*`, but `SCOPED_REQUESTS` only applies permissions to specific POST/PATCH routes and otherwise only tags these requests. As a result, vulnerability GET endpoints (e.g. CVE list, reports) are not actually gated by `required_permission_for('view_vulnerability')`. Either add an explicit pattern to enforce `view_vulnerability` on these GET routes (similar to `view_advisor` for `GET /api/insights/v1/.*`) or update the comment to describe the narrower enforcement that currently exists.
</issue_to_address>

### Comment 2
<location> `app/services/foreman_rh_cloud/insights_api_forwarder.rb:28-31` </location>
<code_context>
     SCOPED_REQUESTS = [
-      { test: %r{api/vulnerability/v1/vulnerabilities/cves}, tag_name: :tags },
+      # Inventory hosts - requires view_vulnerability for GET
+      {
+        test: %r{api/inventory/v1/hosts(/.*)?$},
+        tag_name: :tags,
+        permissions: {
+          'GET' => :view_vulnerability,
+        },
</code_context>

<issue_to_address>
**issue (bug_risk):** GET requests to ack/hostack endpoints bypass the view_advisor check due to pattern ordering.

Because `required_permission_for` matches the specific `ack`/`hostack` patterns first, `GET /api/insights/v1/ack/...` and `GET /api/insights/v1/hostack/...` end up with `permissions[http_method] == nil`, so no permission is enforced, and they never fall through to the generic `api/insights/v1/.*` rule that requires `view_advisor`. If these GETs are intended to require `view_advisor`, either add `'GET' => :view_advisor` to the `ack`/`hostack` patterns or change `required_permission_for` so that it prefers patterns that define permissions for the current method.
</issue_to_address>

### Comment 3
<location> `test/unit/services/foreman_rh_cloud/insights_api_forwarder_test.rb:342-358` </location>
<code_context>
+  end
+
+  # PATCH /api/vulnerability/v1/systems/opt_out requires edit_vulnerability
+  test 'should allow PATCH request to systems opt_out when user has edit_vulnerability permission' do
+    patch_data = '{"opt_out": true}'
+    req = ActionDispatch::Request.new(
+      'REQUEST_URI' => '/api/vulnerability/v1/systems/opt_out',
+      'REQUEST_METHOD' => 'PATCH',
+      'rack.input' => ::Puma::NullIO.new,
+      'RAW_POST_DATA' => patch_data,
+      "action_dispatch.request.path_parameters" => { :format => "json" }
+    )
+
+    @user.stubs(:can?).with(:edit_vulnerability).returns(true)
+    @forwarder.expects(:execute_cloud_request).returns(true)
+
+    @forwarder.forward_request(req, 'api/vulnerability/v1/systems/opt_out', 'test_controller', @user, @organization, @location)
+  end
+
</code_context>

<issue_to_address>
**suggestion (testing):** Add a negative test case for PATCH systems opt_out when the user lacks edit_vulnerability

Please add a complementary test that stubs `@user.can?(:edit_vulnerability)` to `false` and expects a `Foreman::Exception`, matching the pattern used for other protected endpoints to verify the permission check is enforced.

```suggestion
  # PATCH /api/vulnerability/v1/systems/opt_out requires edit_vulnerability
  test 'should allow PATCH request to systems opt_out when user has edit_vulnerability permission' do
    patch_data = '{"opt_out": true}'
    req = ActionDispatch::Request.new(
      'REQUEST_URI' => '/api/vulnerability/v1/systems/opt_out',
      'REQUEST_METHOD' => 'PATCH',
      'rack.input' => ::Puma::NullIO.new,
      'RAW_POST_DATA' => patch_data,
      "action_dispatch.request.path_parameters" => { :format => "json" }
    )

    @user.stubs(:can?).with(:edit_vulnerability).returns(true)
    @forwarder.expects(:execute_cloud_request).returns(true)

    @forwarder.forward_request(req, 'api/vulnerability/v1/systems/opt_out', 'test_controller', @user, @organization, @location)
  end

  test 'should deny PATCH request to systems opt_out when user lacks edit_vulnerability permission' do
    patch_data = '{"opt_out": true}'
    req = ActionDispatch::Request.new(
      'REQUEST_URI' => '/api/vulnerability/v1/systems/opt_out',
      'REQUEST_METHOD' => 'PATCH',
      'rack.input' => ::Puma::NullIO.new,
      'RAW_POST_DATA' => patch_data,
      "action_dispatch.request.path_parameters" => { :format => "json" }
    )

    @user.stubs(:can?).with(:edit_vulnerability).returns(false)

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

```
</issue_to_address>

### Comment 4
<location> `test/unit/helpers/foreman_rh_cloud_permissions_helper_test.rb:143-140` </location>
<code_context>
+  test 'insights_user_permissions should return all permissions when user has all' do
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding a test to ensure no duplicate Chrome permissions are emitted

Since `insights_user_permissions` aggregation looks good, it would still be helpful to assert that the resulting `permission_strings` list contains no duplicates (either in this test or a dedicated one). That way, if `PERMISSION_MAPPING` changes in the future (e.g., overlapping Foreman permissions), we’ll catch any accidental multiple mappings of the same Insights permission.
</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 +12 to +21
# 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}/
Copy link

Choose a reason for hiding this comment

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

🚨 issue (security): Permission table comments for vulnerability GET endpoints don't match the actual enforcement logic.

The header states that view_vulnerability is required for all GET /api/vulnerability/v1/*, but SCOPED_REQUESTS only applies permissions to specific POST/PATCH routes and otherwise only tags these requests. As a result, vulnerability GET endpoints (e.g. CVE list, reports) are not actually gated by required_permission_for('view_vulnerability'). Either add an explicit pattern to enforce view_vulnerability on these GET routes (similar to view_advisor for GET /api/insights/v1/.*) or update the comment to describe the narrower enforcement that currently exists.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We do need to make sure these tables match reality (I think this will be easier with AI). Or another option is to remove them, but I do think they are nice to have..

assert_equal 1, permissions.length

permission_strings = permissions.map { |p| p[:permission] }
assert_includes permission_strings, 'advisor:disable-recommendations:write'
Copy link

Choose a reason for hiding this comment

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

suggestion (testing): Consider adding a test to ensure no duplicate Chrome permissions are emitted

Since insights_user_permissions aggregation looks good, it would still be helpful to assert that the resulting permission_strings list contains no duplicates (either in this test or a dedicated one). That way, if PERMISSION_MAPPING changes in the future (e.g., overlapping Foreman permissions), we’ll catch any accidental multiple mappings of the same Insights permission.

Copy link
Collaborator

@jeremylenz jeremylenz left a comment

Choose a reason for hiding this comment

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

Thanks @nofaralfasi !

some initial minor comments below, haven't tested yet

Comment on lines +12 to +21
# 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}/
Copy link
Collaborator

Choose a reason for hiding this comment

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

We do need to make sure these tables match reality (I think this will be easier with AI). Or another option is to remove them, but I do think they are nice to have..

@nofaralfasi nofaralfasi force-pushed the SAT-40631 branch 3 times, most recently from 2b1c088 to 05a9904 Compare January 8, 2026 16:36
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.

2 participants