Skip to content

chore: rbac v1 cleanup#2385

Merged
jdobes merged 1 commit into
RedHatInsights:masterfrom
pindo696:cleanup-rbac-v1
Jun 17, 2026
Merged

chore: rbac v1 cleanup#2385
jdobes merged 1 commit into
RedHatInsights:masterfrom
pindo696:cleanup-rbac-v1

Conversation

@pindo696

@pindo696 pindo696 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor
  • Removed rbac v1 and its leftovers including decorators and related config
  • kept shared parts
  • moved rbac exception to separate file due to circular imports
  • adapted some tests
  • RHINENG-26714

Good luck with PR

Secure Coding Practices Checklist GitHub Link

Secure Coding Checklist

  • Input Validation
  • Output Encoding
  • Authentication and Password Management
  • Session Management
  • Access Control
  • Cryptographic Practices
  • Error Handling and Logging
  • Data Protection
  • Communication Security
  • System Configuration
  • Database Security
  • File Management
  • Memory Management
  • General Coding Practices

Summary by Sourcery

Remove legacy RBAC v1 usage in favor of Kessel-based RBAC v2 and centralize RBAC exceptions.

Bug Fixes:

  • Ensure RBAC v2 decorators inject empty permissions metadata into responses when Kessel is disabled instead of bypassing metadata handling.

Enhancements:

  • Inline the RBAC permission model and route/filter permission groupings into the RBAC v2 manager, including Kessel permission conversion helpers.
  • Simplify authentication metadata by dropping pre-fetched RBAC permissions and relying on group IDs and principal IDs for Kessel-based checks.
  • Adjust handlers and tests to depend solely on RBAC v2 decorators and to validate permission metadata and workspace behavior accordingly.
  • Remove unused feature flag for omitting permission metadata from responses and default permission metadata to an empty list.

Build:

  • Remove obsolete RBAC-related environment variables and configuration parameters from clowdapp and application config.

Tests:

  • Port and extend RBAC permission and decorator tests to exercise the new RBAC v2 permission model, Kessel conversion helpers, and behavior when Kessel is disabled.

@sourcery-ai

sourcery-ai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

Removes legacy RBAC v1 support and centralizes all RBAC concepts in the v2 (Kessel) manager, while ensuring responses consistently include empty permission metadata and updating handlers, configuration, mocks, and tests accordingly.

File-Level Changes

Change Details Files
Inline the remaining RBAC permission model into rbac_v2_manager and introduce Kessel permission conversion helpers.
  • Define RbacApp, RbacResource, and RbacAction StrEnums for applications, resources, and actions.
  • Implement a standalone RbacPermission class with string representation, wildcard-aware equality, and to_kessel_permission conversion logic.
  • Introduce RbacRoutePermissions and RbacFilterRoutePermissions in rbac_v2_manager to replace the old v1 permission groupings.
  • Add helper functions _convert_permissions_to_kessel, _convert_filter_to_kessel, and _add_empty_permissions, and adjust decorators to use them when KESSEL is disabled.
manager/rbac_v2_manager.py
tests/manager_tests/test_rbac_v2_manager.py
Remove RBAC v1 manager, decorators, filters, and related configuration/env wiring.
  • Delete rbac_manager.py, rbac_filters.py, and their tests, and drop the platform mock RbacHandler and route registration.
  • Remove v1 RBAC decorators from system, CVE, vulnerabilities, dashboard, dashbar, report, status, and feature handlers so only v2 (workspace) decorators remain.
  • Drop RBAC-related environment variables, config fields, and feature flags (DISABLE_RBAC, GRANULAR_RBAC, RBAC_TIMEOUT, OMIT_PERMISSION_METADATA_FROM_RESPONSE_FEATURE).
manager/rbac_manager.py
manager/rbac_filters.py
tests/manager_tests/test_rbac_manager.py
platform_mock/platform_mock.py
deploy/clowdapp.yaml
common/config.py
common/feature_flags.py
manager/system_handler.py
manager/cve_handler.py
manager/vulnerabilities_handler.py
manager/dashbar_handler.py
manager/dashboard_handler.py
manager/report_handler.py
manager/status_handler.py
manager/feature_handler.py
Move RBAC-specific exceptions into their own module and stop fetching v1 permissions during auth.
  • Create manager/rbac_exceptions.py with a dedicated RbacException class and update imports across the codebase to use it.
  • Update auth_common in manager/base.py to no longer construct RbacManager or fetch rbac_perms; only group_ids and principal_id are propagated.
  • Adjust main.py and tests to import RbacException from the new module and remove toggling of disable_rbac in the vuln_testcase fixture.
manager/rbac_exceptions.py
manager/base.py
manager/main.py
tests/manager_tests/vuln_testcase.py
Align tests and expected responses with v2 behavior and the new permission metadata semantics.
  • Extend test_rbac_v2_manager to cover the new permission model, Kessel conversion helpers, and _add_empty_permissions behavior.
  • Update RBAC v2 decorators tests to assert that when Kessel is disabled, handlers still run but responses are wrapped with meta.permissions = [].
  • Change report handler tests to expect an empty permissions list in meta instead of legacy wildcard permissions.
tests/manager_tests/test_rbac_v2_manager.py
tests/manager_tests/test_report_handler.py

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

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 manager/rbac_exceptions.RbacException, super().__init__(self) should probably be super().__init__(message) so that the base Exception stores the human-readable message rather than the instance object.
  • In manager/rbac_v2_manager.RbacPermission.eq, consider adding an isinstance check (or returning NotImplemented) before accessing other.app/resource/action to avoid AttributeError when compared with non-RbacPermission objects.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In manager/rbac_exceptions.RbacException, `super().__init__(self)` should probably be `super().__init__(message)` so that the base Exception stores the human-readable message rather than the instance object.
- In manager/rbac_v2_manager.RbacPermission.__eq__, consider adding an `isinstance` check (or returning NotImplemented) before accessing `other.app/resource/action` to avoid AttributeError when compared with non-RbacPermission objects.

## Individual Comments

### Comment 1
<location path="manager/rbac_exceptions.py" line_range="4-9" />
<code_context>
+"""RBAC-related exceptions"""
+
+
+class RbacException(Exception):
+    """Represents RBAC-related exception"""
+
+    def __init__(self, message):
+        self.message = message
+        super().__init__(self)
</code_context>
<issue_to_address>
**issue (bug_risk):** RbacException should pass the message to Exception instead of `self`.

`super().__init__(self)` sets the base `Exception` argument to the instance instead of the message, which changes `str(e)`, logging output, and `e.args`. Please pass `message` to the base class (`super().__init__(message)`) and consider removing `self.message` if it's unnecessary.
</issue_to_address>

### Comment 2
<location path="manager/rbac_v2_manager.py" line_range="78-82" />
<code_context>
-    def __repr__(self):
-        return self.__str__()
-
-    def __eq__(self, other):
-        app = self.app == other.app
-        resource = self.resource == other.resource or (self.resource == RbacResource.ANY) or (other.resource == RbacResource.ANY)
-        action = self.action == other.action or (self.action == RbacAction.ANY) or (other.action == RbacAction.ANY)
-        return app and resource and action
-
-    def to_kessel_permission(self):
</code_context>
<issue_to_address>
**suggestion (bug_risk):** RbacPermission.__eq__ should guard against comparisons with non-RbacPermission objects.

The implementation assumes `other` has `.app`, `.resource`, and `.action` attributes, so comparing to a different type (including `None`) will raise `AttributeError` instead of returning `False`. Adding an `isinstance(other, RbacPermission)` guard or returning `NotImplemented` for non-matching types would avoid this and make equality behave predictably.

```suggestion
    def __eq__(self, other):
        if not isinstance(other, RbacPermission):
            return NotImplemented

        app = self.app == other.app
        resource = (
            self.resource == other.resource
            or (self.resource == RbacResource.ANY)
            or (other.resource == RbacResource.ANY)
        )
        action = (
            self.action == other.action
            or (self.action == RbacAction.ANY)
            or (other.action == RbacAction.ANY)
        )
        return app and resource and action
```
</issue_to_address>

### Comment 3
<location path="tests/manager_tests/test_report_handler.py" line_range="27" />
<code_context>
     },
     "top_cves": [],
     "top_rules": [],
-    "meta": {"cache_used": False, "permissions": ["vulnerability:*:*", "inventory:hosts:read"]},
+    "meta": {"cache_used": False, "permissions": []},
 }

</code_context>
<issue_to_address>
**suggestion (testing):** Extend the report handler tests to assert the presence and type of the `permissions` field rather than only its exact value.

The expected response now uses `"permissions": []`, which matches the new behavior. To make the tests more resilient, consider asserting that `permissions` exists and is a list (e.g. `assert isinstance(result["meta"]["permissions"], list)`), and optionally keep a separate assertion for its contents (currently an empty list).

Suggested implementation:

```python
    },
    "top_cves": [],
    "top_rules": [],
    "meta": {"cache_used": False, "permissions": []},
}

```

To fully implement the suggestion, update the test that checks this response structure (where this expected dict is used) as follows:

1. After obtaining the actual result (e.g. `result = ...` or `response.json()`), add an assertion that verifies the existence and type of the `permissions` field:
   ```python
   assert "meta" in result
   assert "permissions" in result["meta"]
   assert isinstance(result["meta"]["permissions"], list)
   ```
2. Optionally keep or add a separate assertion for the specific contents, which currently should be an empty list:
   ```python
   assert result["meta"]["permissions"] == []
   ```
3. If the test currently performs a full deep equality comparison between the result and the expected dict shown in the snippet (e.g. `assert result == EXPECTED_RESPONSE`), you can keep that assertion, since `"permissions": []` still matches the new behavior; the new type-focused assertions will make the test resilient if the precise contents of `permissions` change in the future while remaining a list.

Place these new assertions in the relevant test(s) immediately after you obtain `result`, before or alongside any existing equality checks.
</issue_to_address>

### Comment 4
<location path="manager/rbac_v2_manager.py" line_range="64" />
<code_context>
-    ANY = "*"
-
-
-class RbacPermission:
-    """Class represents single permission"""
-
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring the new RBAC permission enums, mappings, and route/filter permission structures to make wildcard behavior, permission translation, and configuration data more explicit and readable while preserving current behavior.

You can keep the new behavior but reduce complexity in a few focused spots.

### 1. `RbacPermission.to_kessel_permission` string mangling

Right now `StrEnum` values are treated as strings and transformed via `replace`, which couples behavior to the literal string and makes the enum abstraction thin.

You can make this more explicit and safer by:

* Using `.value` instead of bare enum instances
* Replacing ad‑hoc `.replace` calls with small mappings

```python
class RbacApp(StrEnum):
    VULNERABILITY = "vulnerability"
    INVENTORY = "inventory"
    ANY = "all"  # optional, if this matches Kessel semantics
```

```python
class RbacPermission:
    # ...

    def to_kessel_permission(self) -> str:
        """Convert RBACv1 permission to Kessel v2 permission string format."""
        action_mapping = {
            RbacAction.READ: "view",
            RbacAction.WRITE: "edit",
            RbacAction.ANY: "all",
        }

        app_mapping = {
            RbacApp.VULNERABILITY: "vulnerability",
            RbacApp.INVENTORY: "inventory",
            RbacApp.ANY: "all",
        }

        resource_mapping = {
            RbacResource.VULNERABILITY_RESULTS: "vulnerability_results",
            RbacResource.CVE_BUSINESS_RISK_AND_STATUS: "cve_business_risk_and_status",
            RbacResource.SYSTEM_CVE_STATUS: "system_cve_status",
            RbacResource.ADVANCED_REPORT: "advanced_report",
            RbacResource.SYSTEM_OPT_OUT: "system_opt_out",
            RbacResource.EXPORT_AND_REPORT: "report_and_export",
            RbacResource.HOSTS: "hosts",
            RbacResource.GROUPS: "groups",
            RbacResource.TOGGLE_CVES_WITHOUT_ERRATA: "toggle_cves_without_errata",
            RbacResource.ANY: "all",
        }

        app = app_mapping[self.app]
        resource = resource_mapping[self.resource]
        action = action_mapping[self.action]

        return f"{app}_{resource}_{action}"
```

This keeps functionality but makes the mapping explicit and decoupled from the literal enum string format.

### 2. Wildcard matching vs equality

Overloading `__eq__` to do wildcard matching is surprising. You can keep the behavior but introduce a dedicated method and start moving callers over when convenient, without breaking anything now:

```python
class RbacPermission:
    # ...

    def matches(self, other: "RbacPermission") -> bool:
        app = self.app == other.app
        resource = (
            self.resource == other.resource
            or self.resource == RbacResource.ANY
            or other.resource == RbacResource.ANY
        )
        action = (
            self.action == other.action
            or self.action == RbacAction.ANY
            or other.action == RbacAction.ANY
        )
        return app and resource and action

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, RbacPermission):
            return NotImplemented
        # Keep current behavior for now
        return self.matches(other)
```

Then in any new code (or when you touch existing call sites), you can use `perm.matches(other_perm)` instead of `==`, making intent clearer.

### 3. Route permission nesting readability

The `[[perm, INVENTORY_READ]]` pattern is not self‑describing. A tiny helper clarifies semantics without changing behavior:

```python
def _perm_with_inventory(*perms: RbacPermission) -> list[list[RbacPermission]]:
    # OR-of-AND: single group requiring given perms and inventory read
    return [[*perms, RbacRoutePermissions.INVENTORY_READ]]
```

```python
class RbacRoutePermissions:
    VULNERABILITY_ANY = RbacPermission(RbacApp.VULNERABILITY, RbacResource.ANY, RbacAction.ANY)
    INVENTORY_READ = RbacPermission(RbacApp.INVENTORY, RbacResource.HOSTS, RbacAction.READ)

    BASE_OPT_OUT_READ = RbacPermission(RbacApp.VULNERABILITY, RbacResource.SYSTEM_OPT_OUT, RbacAction.READ)

    VULNERABILITY_RESULTS = _perm_with_inventory(
        RbacPermission(RbacApp.VULNERABILITY, RbacResource.VULNERABILITY_RESULTS, RbacAction.READ)
    )
    CVE_BR_AND_STATUS_EDIT = _perm_with_inventory(
        RbacPermission(RbacApp.VULNERABILITY, RbacResource.CVE_BUSINESS_RISK_AND_STATUS, RbacAction.WRITE)
    )
    SYSTEM_CVE_STATUS_EDIT = _perm_with_inventory(
        RbacPermission(RbacApp.VULNERABILITY, RbacResource.SYSTEM_CVE_STATUS, RbacAction.WRITE)
    )
    SYSTEM_OPT_OUT_READ = _perm_with_inventory(BASE_OPT_OUT_READ)
    SYSTEM_OPT_OUT_EDIT = _perm_with_inventory(
        RbacPermission(RbacApp.VULNERABILITY, RbacResource.SYSTEM_OPT_OUT, RbacAction.WRITE)
    )
    ADVANCED_REPORTING = _perm_with_inventory(
        RbacPermission(RbacApp.VULNERABILITY, RbacResource.ADVANCED_REPORT, RbacAction.READ)
    )
    REPORT_AND_EXPORT = _perm_with_inventory(
        RbacPermission(RbacApp.VULNERABILITY, RbacResource.EXPORT_AND_REPORT, RbacAction.READ)
    )
    TOGGLE_CVES_WITHOUT_ERRATA = _perm_with_inventory(
        RbacPermission(RbacApp.VULNERABILITY, RbacResource.TOGGLE_CVES_WITHOUT_ERRATA, RbacAction.WRITE)
    )
```

This preserves the nested list shape but makes the “AND with inventory read” intent obvious.

### 4. `RbacFilterRoutePermissions` dict construction

The `copy()` + `update()` cascade makes it harder to see what each endpoint actually allows. You can keep the shared pieces but use literal unpacking for clarity:

```python
class RbacFilterRoutePermissions:
    OPT_OUT_VALUE = {"excluded": (RbacRoutePermissions.SYSTEM_OPT_OUT_READ, True)}
    CSV_FORMAT_VALUE = {"data_format": (RbacRoutePermissions.REPORT_AND_EXPORT, "csv")}
    REPORT_VALUE = {"report": (RbacRoutePermissions.REPORT_AND_EXPORT, True)}
    ADVANCED_REPORT_VALUE = {"advanced_report": (RbacRoutePermissions.ADVANCED_REPORTING, True)}

    CSV_FORMAT_ENDPOINTS = {
        **CSV_FORMAT_VALUE,
    }

    REPORTABLE_ENDPOINTS = {
        **CSV_FORMAT_VALUE,
        **REPORT_VALUE,
    }

    VULNERABILITIES_CVES_ENDPOINT = {
        **CSV_FORMAT_VALUE,
        **REPORT_VALUE,
        **ADVANCED_REPORT_VALUE,
    }

    SYSTEMS_ENDPOINT_VALUES = {
        **CSV_FORMAT_VALUE,
        **REPORT_VALUE,
        **OPT_OUT_VALUE,
    }
```

Same resulting dicts, but each one’s content is visible at the point of definition.

### 5. Make “constants” clearly immutable

If these route/filter permission collections are intended as static configuration, consider using tuples for nested permission lists to avoid accidental mutation:

```python
from typing import Tuple

Permission = RbacPermission
PermissionGroup = Tuple[Permission, ...]
PermissionAlternatives = Tuple[PermissionGroup, ...]

class RbacRoutePermissions:
    # ...
    VULNERABILITY_RESULTS: PermissionAlternatives = (
        (
            RbacPermission(RbacApp.VULNERABILITY, RbacResource.VULNERABILITY_RESULTS, RbacAction.READ),
            INVENTORY_READ,
        ),
    )
```

You don’t have to change all call sites immediately, but even just changing the inner lists to tuples removes one class of accidental mutations while keeping behavior the same.
</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 thread manager/rbac_exceptions.py Outdated
Comment on lines +78 to +82
def __eq__(self, other):
app = self.app == other.app
resource = self.resource == other.resource or (self.resource == RbacResource.ANY) or (other.resource == RbacResource.ANY)
action = self.action == other.action or (self.action == RbacAction.ANY) or (other.action == RbacAction.ANY)
return app and resource and action

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): RbacPermission.eq should guard against comparisons with non-RbacPermission objects.

The implementation assumes other has .app, .resource, and .action attributes, so comparing to a different type (including None) will raise AttributeError instead of returning False. Adding an isinstance(other, RbacPermission) guard or returning NotImplemented for non-matching types would avoid this and make equality behave predictably.

Suggested change
def __eq__(self, other):
app = self.app == other.app
resource = self.resource == other.resource or (self.resource == RbacResource.ANY) or (other.resource == RbacResource.ANY)
action = self.action == other.action or (self.action == RbacAction.ANY) or (other.action == RbacAction.ANY)
return app and resource and action
def __eq__(self, other):
if not isinstance(other, RbacPermission):
return NotImplemented
app = self.app == other.app
resource = (
self.resource == other.resource
or (self.resource == RbacResource.ANY)
or (other.resource == RbacResource.ANY)
)
action = (
self.action == other.action
or (self.action == RbacAction.ANY)
or (other.action == RbacAction.ANY)
)
return app and resource and action

Comment thread tests/manager_tests/test_report_handler.py
ANY = "*"


class RbacPermission:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (complexity): Consider refactoring the new RBAC permission enums, mappings, and route/filter permission structures to make wildcard behavior, permission translation, and configuration data more explicit and readable while preserving current behavior.

You can keep the new behavior but reduce complexity in a few focused spots.

1. RbacPermission.to_kessel_permission string mangling

Right now StrEnum values are treated as strings and transformed via replace, which couples behavior to the literal string and makes the enum abstraction thin.

You can make this more explicit and safer by:

  • Using .value instead of bare enum instances
  • Replacing ad‑hoc .replace calls with small mappings
class RbacApp(StrEnum):
    VULNERABILITY = "vulnerability"
    INVENTORY = "inventory"
    ANY = "all"  # optional, if this matches Kessel semantics
class RbacPermission:
    # ...

    def to_kessel_permission(self) -> str:
        """Convert RBACv1 permission to Kessel v2 permission string format."""
        action_mapping = {
            RbacAction.READ: "view",
            RbacAction.WRITE: "edit",
            RbacAction.ANY: "all",
        }

        app_mapping = {
            RbacApp.VULNERABILITY: "vulnerability",
            RbacApp.INVENTORY: "inventory",
            RbacApp.ANY: "all",
        }

        resource_mapping = {
            RbacResource.VULNERABILITY_RESULTS: "vulnerability_results",
            RbacResource.CVE_BUSINESS_RISK_AND_STATUS: "cve_business_risk_and_status",
            RbacResource.SYSTEM_CVE_STATUS: "system_cve_status",
            RbacResource.ADVANCED_REPORT: "advanced_report",
            RbacResource.SYSTEM_OPT_OUT: "system_opt_out",
            RbacResource.EXPORT_AND_REPORT: "report_and_export",
            RbacResource.HOSTS: "hosts",
            RbacResource.GROUPS: "groups",
            RbacResource.TOGGLE_CVES_WITHOUT_ERRATA: "toggle_cves_without_errata",
            RbacResource.ANY: "all",
        }

        app = app_mapping[self.app]
        resource = resource_mapping[self.resource]
        action = action_mapping[self.action]

        return f"{app}_{resource}_{action}"

This keeps functionality but makes the mapping explicit and decoupled from the literal enum string format.

2. Wildcard matching vs equality

Overloading __eq__ to do wildcard matching is surprising. You can keep the behavior but introduce a dedicated method and start moving callers over when convenient, without breaking anything now:

class RbacPermission:
    # ...

    def matches(self, other: "RbacPermission") -> bool:
        app = self.app == other.app
        resource = (
            self.resource == other.resource
            or self.resource == RbacResource.ANY
            or other.resource == RbacResource.ANY
        )
        action = (
            self.action == other.action
            or self.action == RbacAction.ANY
            or other.action == RbacAction.ANY
        )
        return app and resource and action

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, RbacPermission):
            return NotImplemented
        # Keep current behavior for now
        return self.matches(other)

Then in any new code (or when you touch existing call sites), you can use perm.matches(other_perm) instead of ==, making intent clearer.

3. Route permission nesting readability

The [[perm, INVENTORY_READ]] pattern is not self‑describing. A tiny helper clarifies semantics without changing behavior:

def _perm_with_inventory(*perms: RbacPermission) -> list[list[RbacPermission]]:
    # OR-of-AND: single group requiring given perms and inventory read
    return [[*perms, RbacRoutePermissions.INVENTORY_READ]]
class RbacRoutePermissions:
    VULNERABILITY_ANY = RbacPermission(RbacApp.VULNERABILITY, RbacResource.ANY, RbacAction.ANY)
    INVENTORY_READ = RbacPermission(RbacApp.INVENTORY, RbacResource.HOSTS, RbacAction.READ)

    BASE_OPT_OUT_READ = RbacPermission(RbacApp.VULNERABILITY, RbacResource.SYSTEM_OPT_OUT, RbacAction.READ)

    VULNERABILITY_RESULTS = _perm_with_inventory(
        RbacPermission(RbacApp.VULNERABILITY, RbacResource.VULNERABILITY_RESULTS, RbacAction.READ)
    )
    CVE_BR_AND_STATUS_EDIT = _perm_with_inventory(
        RbacPermission(RbacApp.VULNERABILITY, RbacResource.CVE_BUSINESS_RISK_AND_STATUS, RbacAction.WRITE)
    )
    SYSTEM_CVE_STATUS_EDIT = _perm_with_inventory(
        RbacPermission(RbacApp.VULNERABILITY, RbacResource.SYSTEM_CVE_STATUS, RbacAction.WRITE)
    )
    SYSTEM_OPT_OUT_READ = _perm_with_inventory(BASE_OPT_OUT_READ)
    SYSTEM_OPT_OUT_EDIT = _perm_with_inventory(
        RbacPermission(RbacApp.VULNERABILITY, RbacResource.SYSTEM_OPT_OUT, RbacAction.WRITE)
    )
    ADVANCED_REPORTING = _perm_with_inventory(
        RbacPermission(RbacApp.VULNERABILITY, RbacResource.ADVANCED_REPORT, RbacAction.READ)
    )
    REPORT_AND_EXPORT = _perm_with_inventory(
        RbacPermission(RbacApp.VULNERABILITY, RbacResource.EXPORT_AND_REPORT, RbacAction.READ)
    )
    TOGGLE_CVES_WITHOUT_ERRATA = _perm_with_inventory(
        RbacPermission(RbacApp.VULNERABILITY, RbacResource.TOGGLE_CVES_WITHOUT_ERRATA, RbacAction.WRITE)
    )

This preserves the nested list shape but makes the “AND with inventory read” intent obvious.

4. RbacFilterRoutePermissions dict construction

The copy() + update() cascade makes it harder to see what each endpoint actually allows. You can keep the shared pieces but use literal unpacking for clarity:

class RbacFilterRoutePermissions:
    OPT_OUT_VALUE = {"excluded": (RbacRoutePermissions.SYSTEM_OPT_OUT_READ, True)}
    CSV_FORMAT_VALUE = {"data_format": (RbacRoutePermissions.REPORT_AND_EXPORT, "csv")}
    REPORT_VALUE = {"report": (RbacRoutePermissions.REPORT_AND_EXPORT, True)}
    ADVANCED_REPORT_VALUE = {"advanced_report": (RbacRoutePermissions.ADVANCED_REPORTING, True)}

    CSV_FORMAT_ENDPOINTS = {
        **CSV_FORMAT_VALUE,
    }

    REPORTABLE_ENDPOINTS = {
        **CSV_FORMAT_VALUE,
        **REPORT_VALUE,
    }

    VULNERABILITIES_CVES_ENDPOINT = {
        **CSV_FORMAT_VALUE,
        **REPORT_VALUE,
        **ADVANCED_REPORT_VALUE,
    }

    SYSTEMS_ENDPOINT_VALUES = {
        **CSV_FORMAT_VALUE,
        **REPORT_VALUE,
        **OPT_OUT_VALUE,
    }

Same resulting dicts, but each one’s content is visible at the point of definition.

5. Make “constants” clearly immutable

If these route/filter permission collections are intended as static configuration, consider using tuples for nested permission lists to avoid accidental mutation:

from typing import Tuple

Permission = RbacPermission
PermissionGroup = Tuple[Permission, ...]
PermissionAlternatives = Tuple[PermissionGroup, ...]

class RbacRoutePermissions:
    # ...
    VULNERABILITY_RESULTS: PermissionAlternatives = (
        (
            RbacPermission(RbacApp.VULNERABILITY, RbacResource.VULNERABILITY_RESULTS, RbacAction.READ),
            INVENTORY_READ,
        ),
    )

You don’t have to change all call sites immediately, but even just changing the inner lists to tuples removes one class of accidental mutations while keeping behavior the same.

Removed rbac v1 and its leftovers, kept shared parts RHINENG-26714
@jdobes
jdobes merged commit 3b328ea into RedHatInsights:master Jun 17, 2026
7 of 9 checks passed
@jdobes jdobes mentioned this pull request Jun 17, 2026
14 tasks
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