Skip to content

refactor: consolidate RBAC into dojo/authorization package#14764

Open
Maffooch wants to merge 7 commits intoDefectDojo:devfrom
Maffooch:perm-cleanup
Open

refactor: consolidate RBAC into dojo/authorization package#14764
Maffooch wants to merge 7 commits intoDefectDojo:devfrom
Maffooch:perm-cleanup

Conversation

@Maffooch
Copy link
Copy Markdown
Contributor

Description

Consolidate every RBAC / authorization concern into a single dojo/authorization/ package. Before this PR, authorization code lived in seven different places: dojo/models.py (RBAC models), 14 per-app queries.py files (get_authorized_*), every view file (@user_is_authorized decorators), dojo/api_v2/permissions.py, dojo/location/api/permissions.py, and dojo/templatetags/authorization_tags.py. After this PR, it all lives in dojo/authorization/.

Net diff: 85 files changed, +3015 / −2113.

What moves into dojo/authorization/

  • models.py — 7 RBAC models (Role, Global_Role, Dojo_Group_Member, Product_Member, Product_Group, Product_Type_Member, Product_Type_Group) extracted from dojo/models.py. app_label='dojo' is preserved so no migrations are needed; ~47 import sites are updated.
  • api_permissions.pydojo/api_v2/permissions.py and dojo/location/api/permissions.py merged into one module; originals deleted; consumers and tests rewritten.
  • template_filters.py — filter functions extracted from dojo/templatetags/authorization_tags.py. The templatetags module becomes a thin registration proxy.
  • query_filters.py + query_registrations.py — registry pattern (~1.9k lines of RBAC filter logic) extracted from 14 per-app queries.py files. Each get_authorized_* becomes a thin wrapper that defers to the registry and falls back to unfiltered querysets when no RBAC backend is registered.
  • url_permissions.py + middleware.py — map ~198 URL names to permission checks and enforce them via AuthorizationMiddleware.process_view. Removes @user_is_authorized, @user_has_global_permission, and @user_is_configuration_authorized from 26 view files.
  • __init__.py — exports the public surface and triggers query-filter registration at app startup.

Why

  • Single source of truth for "is this allowed?" — checks live next to the models, registry, and middleware that enforce them.
  • No more per-view decorators — one URL-permission map replaces ~200 decorator lines scattered across 26 view files.
  • Decouples dojo.models from authorization — RBAC models live in their own module, removing circular-import pressure.
  • Pluggableget_authorized_* falls back to unfiltered querysets when no RBAC backend is registered, so non-RBAC deployments keep working.

Behavioral changes

None intended. Authorization checks for the ~198 mapped URLs now run in middleware (process_view) instead of inside view bodies, but produce the same allow/deny outcome.

Test results

  • Full Django unit-test suite (Rest Framework × 4 platform combos) — green.
  • K8s deployment smoke (kubernetes 1.33.11, 1.35.4) — green.
  • Full Selenium UI matrix — green.
  • New unittests/test_permissions_audit.py exercises the URL-permission map for completeness.

Move every RBAC / authorization concern into a single dojo/authorization/
package. Before this change authorization code lived in seven different
places: dojo/models.py (RBAC models), 14 per-app queries.py files
(get_authorized_*), every view file (@user_is_authorized decorators),
dojo/api_v2/permissions.py, dojo/location/api/permissions.py, and
dojo/templatetags/authorization_tags.py.

Changes
- Move 7 RBAC models (Role, Global_Role, Dojo_Group_Member,
  Product_Member, Product_Group, Product_Type_Member,
  Product_Type_Group) from dojo/models.py to
  dojo/authorization/models.py. app_label='dojo' is preserved so no
  migrations are needed; ~47 import sites are updated.
- Merge dojo/api_v2/permissions.py and
  dojo/location/api/permissions.py into
  dojo/authorization/api_permissions.py.
- Extract template-tag logic from
  dojo/templatetags/authorization_tags.py into
  dojo/authorization/template_filters.py; the templatetags module
  becomes a thin registration proxy.
- Add dojo/authorization/query_filters.py (registry) and
  dojo/authorization/query_registrations.py (~1.9k lines of RBAC
  filter logic extracted from 14 per-app queries.py files). Each
  get_authorized_* becomes a thin wrapper that defers to the registry
  and falls back to unfiltered querysets when no RBAC backend is
  registered.
- Add dojo/authorization/url_permissions.py mapping ~198 URL names to
  permission checks plus dojo/authorization/middleware.py with
  AuthorizationMiddleware enforcing them via process_view. Removes
  @user_is_authorized, @user_has_global_permission, and
  @user_is_configuration_authorized from 26 view files.
- Update dojo/authorization/__init__.py exports and trigger
  query-filter registration at app startup.

Behavior is unchanged: authorization checks for the ~198 mapped URLs
now run in middleware (process_view) instead of view bodies, but
produce the same allow/deny outcome. Non-RBAC deployments keep working
because get_authorized_* falls back to unfiltered querysets.

Tests: unittests/test_permissions_audit.py exercises the URL-permission
map for completeness; existing API/UI suites pass.
@Maffooch Maffooch requested a review from mtesauro as a code owner April 27, 2026 22:06
@github-actions github-actions Bot added settings_changes Needs changes to settings.py based on changes in settings.dist.py included in this PR apiv2 unittests ui labels Apr 27, 2026
@Maffooch Maffooch added this to the 2.58.0 milestone Apr 27, 2026
Copy link
Copy Markdown
Member

@valentijnscholten valentijnscholten left a comment

Choose a reason for hiding this comment

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

No more per-view decorators — one URL-permission map replaces ~200 decorator lines scattered across 26 view files.

I don't think I like this. It requires jumping back and forth to the mapping to find what mapping is in place and it increases the chance someone forgets a mapping altogheter or leaves an old mapping behind. Why is this being done?

@Maffooch
Copy link
Copy Markdown
Contributor Author

The hope is to make permissions easier the manage in a single location. This will likely make reviews easier as well since we only have to look out for a single point of failure for authorization rather than having it spread out

@github-actions
Copy link
Copy Markdown
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

Copy link
Copy Markdown
Contributor

@mtesauro mtesauro left a comment

Choose a reason for hiding this comment

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

Approved

(assumes the merge conflict is fixed first)

# Conflicts:
#	dojo/forms.py
#	dojo/github/ui/views.py
@github-actions
Copy link
Copy Markdown
Contributor

Conflicts have been resolved. A maintainer will review the pull request shortly.

Maffooch and others added 2 commits April 29, 2026 21:38
The merge from upstream brought in dojo/notifications/api/views.py
referencing dojo.api_v2.permissions, which this branch already moved
to dojo.authorization.api_permissions. The stale import broke
URLConf resolution, failing every test that boots the app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dryrunsecurity
Copy link
Copy Markdown

dryrunsecurity Bot commented Apr 30, 2026

DryRun Security

This pull request introduces several medium-severity authorization fallbacks that can fail open and expose engagements, findings, credentials, and finding groups if filter registration is missing, plus multiple high-severity IDOR risks where views fetch objects directly from user-controlled IDs without visible in-view permission checks. The proposed middleware/URL-permission mitigations are not shown as enabled in Django settings, so the scanner cannot confirm they actually protect these endpoints.

🟠 Potential IDOR Vulnerability in dojo/endpoint/views.py (drs_e35d13e2)
Vulnerability Potential IDOR Vulnerability
Description The endpoint report views retrieve an Endpoint directly using the user-controlled eid route parameter. The shown view code does not scope the lookup to the current user and does not perform an in-function object permission check before generating the report. Although the PR adds a URL-permission middleware mapping for these routes, the provided full patch does not show that new middleware being installed in Django's MIDDLEWARE setting, so that mitigation is not demonstrably active.

endpoint = get_object_or_404(Endpoint, id=eid)
return generate_report(request, endpoint, host_view=False)

🟠 Potential IDOR Vulnerability in dojo/engagement/views.py (drs_fa0aa793)
Vulnerability Potential IDOR Vulnerability
Description The view retrieves an Engagement using the user-controlled eid path parameter via Engagement.objects.get(id=eid) without a user-scoped query or an in-view permission check. Although the patch adds a centralized URL permission mapping for add_tests, the provided full PR patch does not show the new AuthorizationMiddleware being registered/enabled, so that mitigation is not proven to execute.

eng = Engagement.objects.get(id=eid)
cred_form = CredMappingForm()

🟠 Potential IDOR Vulnerability in dojo/benchmark/views.py (drs_63e7210f)
Vulnerability Potential IDOR Vulnerability
Description The code retrieves a Product record directly by the user-supplied pid parameter using get_object_or_404(Product, id=pid). Although the patch adds a centralized authorization middleware and a URL permission entry for delete_product_benchmark, the provided full patch does not show that the new middleware is registered/enabled in Django settings, so the object-level permission check is not proven to execute before this view.

product = get_object_or_404(Product, id=pid)
benchmark_product_summary = get_object_or_404(

🟠 Potential IDOR Vulnerability in dojo/endpoint/views.py (drs_711dfe2c)
Vulnerability Potential IDOR Vulnerability
Description The view fetches a Product using the user-controlled pid route parameter without an authorization-scoped query or an in-view permission check. Although the patch adds a URL_PERMISSIONS entry and AuthorizationMiddleware, the provided full patch does not show that this middleware is registered/enabled, so the added mapping is not evidence of enforcement for this request.

product = get_object_or_404(Product, id=pid)
form = ImportEndpointMetaForm()

🟠 Potential IDOR Vulnerability in dojo/finding/views.py (drs_c59c4d11)
Vulnerability Potential IDOR Vulnerability
Description The view uses the URL-controlled fid directly to fetch a Finding by primary key. Although the patch introduces a central URL permission map and middleware intended to authorize copy_finding, the provided full patch does not show that the new AuthorizationMiddleware is registered in Django's middleware stack, so that mitigation is not proven to execute before this view. No direct ownership-scoped query or in-function permission check is visible for copy_finding.

finding = get_object_or_404(Finding, id=fid)
product = finding.test.engagement.product

🟠 Potential IDOR Vulnerability in dojo/engagement/views.py (drs_5072d780)
Vulnerability Potential IDOR Vulnerability
Description The view fetches an Engagement directly by the user-controlled eid URL parameter without any ownership-scoped query or in-view permission check. Although the patch adds a URL_PERMISSIONS entry and AuthorizationMiddleware that would check copy_engagement, the provided full PR patch does not show that new middleware being registered/enabled in Django settings, so there is no clear active authorization mitigation for this direct object lookup.

engagement = get_object_or_404(Engagement, id=eid)
product = engagement.product

🟠 Potential IDOR Vulnerability in dojo/finding/views.py (drs_32bd50eb)
Vulnerability Potential IDOR Vulnerability
Description The view retrieves a Finding using the user-controlled fid parameter and the only apparent authorization mitigation is a newly added URL-permission middleware that is not shown as registered in Django settings in the provided full patch, so the object-level permission check would not execute before the view.

finding = get_object_or_404(Finding, id=fid)
finding.active = True

🟠 Potential IDOR Vulnerability in dojo/finding/views.py (drs_b7f5be3e)
Vulnerability Potential IDOR Vulnerability
Description The view fetches a Finding directly from the user-controlled fid path parameter and the patch removes/does not show an effective per-object authorization check in the view. Although the PR adds a URL permission mapping and AuthorizationMiddleware, the provided full patch does not register that middleware in Django's MIDDLEWARE settings, so the central object check is not shown to execute.

finding = get_object_or_404(Finding, id=fid)

🟡 Potential authorization bypass due to fallback queryset return in engagement queries in dojo/engagement/queries.py (drs_330bba8a)
Vulnerability Potential authorization bypass due to fallback queryset return in engagement queries
Description The engagement query helper get_authorized_engagements defaults to returning Engagement.objects.all() if no authorization filter is registered for the key 'engagement.get_authorized_engagements'. While the system is designed to initialize authorization filters at startup by importing dojo.authorization.query_registrations in dojo/authorization/__init__.py, this relies on correct import sequencing. If the import system fails or circular dependencies occur (causing the registration to be bypassed), engagement records would be globally exposed to any user, bypassing all product-level and group-level authorization scopes.

return Engagement.objects.all().order_by("id")

🟡 Insecure Authorization Fallback in dojo/finding/queries.py (drs_f5050390)
Vulnerability Insecure Authorization Fallback
Description The authorization query helpers in dojo/finding/queries.py employ an insecure-by-default design pattern. When an authorization filter implementation is not found in the _AUTH_FILTER_REGISTRY, the helpers fall back to returning the full objects.all() queryset. This design assumes that the registry will always be populated, creating a risk where any failure in the registration process—such as a circular import, an uninitialized module, or a race condition—results in an immediate, global authorization bypass. Given that these helpers serve as the primary access control mechanism for all finding-related data in the application, including sensitive vulnerability information, a broad fallback is dangerous. Furthermore, multiple views rely on these helpers to enforce data scoping, and while some downstream views implement additional object-level checks, many rely solely on the initial queryset filtering provided by these helpers.

return Finding.objects.all().order_by("id")
def get_authorized_findings_for_queryset(permission, queryset, user=None):

🟡 Potential authorization bypass via fallback queryset return in credential queries in dojo/cred/queries.py (drs_a13aca38)
Vulnerability Potential authorization bypass via fallback queryset return in credential queries
Description The vulnerability involves credential query helper functions (get_authorized_cred_mappings and get_authorized_cred_mappings_for_queryset) that default to returning the full, unfiltered querysets if the authorization filter implementation is not registered. While the dojo/authorization/__init__.py attempts to trigger registration of filters at startup, relying on this side-effect for security-critical authorization introduces a race condition or failure point. If get_auth_filter returns None (for example, due to an import failure or improper execution context), the application fails open, returning all credentials to the caller. Given that credential mapping records contain sensitive information (like usernames and environment associations), exposing them to unauthorized users is a significant security risk.

if impl:
return impl(permission)
return Cred_Mapping.objects.all().order_by("id")

🟡 Authorization Bypass in Finding Group Queries in dojo/finding_group/queries.py (drs_bdfdbd4a)
Vulnerability Authorization Bypass in Finding Group Queries
Description The query helpers in dojo/finding_group/queries.py fallback to returning all Finding_Group objects (Finding_Group.objects.all()) or an unmodified queryset if the authorization filter implementation is not retrieved. While dojo/authorization/__init__.py imports query_registrations to ensure registration at startup, if for any reason the registry lookup fails or is uninitialized (e.g., due to circular imports or improper initialization), the system fails open by granting full access. This bypasses the intended Role-Based Access Control (RBAC).

try:
from dojo.authorization.query_filters import get_auth_filter
except ImportError:
def get_auth_filter(key): return None
from dojo.models import Finding_Group
from dojo.request_cache import cache_for_request
# Cached: all parameters are hashable, no dynamic queryset filtering
@cache_for_request
def get_authorized_finding_groups(permission, user=None):
impl = get_auth_filter("finding_group.get_authorized_finding_groups")
if impl:
return impl(permission, user=user)
return Finding_Group.objects.all()
def get_authorized_finding_groups_for_queryset(permission, queryset, user=None):
impl = get_auth_filter("finding_group.get_authorized_finding_groups_for_queryset")
if impl:
return impl(permission, queryset, user=user)
return queryset


Comment to provide feedback on these findings.

Report false positive: @dryrunsecurity fp [FINDING ID] [FEEDBACK]
Report low-impact: @dryrunsecurity nit [FINDING ID] [FEEDBACK]

Example: @dryrunsecurity fp drs_90eda195 This code is not user-facing

All finding details can be found in the DryRun Security Dashboard.

# Conflicts:
#	unittests/test_remote_user.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

apiv2 settings_changes Needs changes to settings.py based on changes in settings.dist.py included in this PR ui unittests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants