Skip to content

fix(rbac): resolve product-scoped users for the engagement Testing Lead selector#15063

Draft
valentijnscholten wants to merge 4 commits into
DefectDojo:bugfixfrom
valentijnscholten:fix-authorized-users-for-product
Draft

fix(rbac): resolve product-scoped users for the engagement Testing Lead selector#15063
valentijnscholten wants to merge 4 commits into
DefectDojo:bugfixfrom
valentijnscholten:fix-authorized-users-for-product

Conversation

@valentijnscholten

@valentijnscholten valentijnscholten commented Jun 23, 2026

Copy link
Copy Markdown
Member

Fixes #15062

The engagement Testing Lead selector (a required field) is empty for any user who was granted access to a product via the product's authorized users section, which blocks them from creating an engagement.

Root cause

dojo/forms.py populates the lead field from get_authorized_users_for_product_and_product_type(None, product, "view"). In the v3 OS authorization layer (dojo/authorization/query_registrations.py) the two impls are incomplete:

def _get_authorized_users_for_product_and_product_type(users, product, permission):
    ...
    if _is_unrestricted(...) or user.is_staff:
        return users
    return users.none()      # <-- product argument ignored; non-staff caller gets nothing

They ignore the product/product_type argument and return users.none() for every non-staff/non-superuser caller, so a product-scoped user gets an empty lead list. (Granting full/global access "fixes" it only because that flips the user into the _is_unrestricted branch — which is why an admin never sees the bug.)

Behaviour in 2.58.4 (what this restores)

In 2.58.4 the same function was a real query and was not gated on the calling user — any caller, including a non-staff product member, received the users authorized on that product/type:

def get_authorized_users_for_product_and_product_type(users, product, permission):
    if users is None:
        users = Dojo_User.objects.filter(is_active=True)
    roles = get_roles_for_permission(permission)
    return users.filter(
        Q(id__in=Subquery(product_member_users))        # Product_Member of product
        | Q(id__in=Subquery(product_type_member_users)) # Product_Type_Member of product.prod_type
        | Q(id__in=Subquery(group_member_users))        # via Product_Group / Product_Type_Group / Global_Role groups
        | Q(global_role__role__in=roles)                # users with a matching Global_Role
        | Q(is_superuser=True),                         # superusers
    )

The v3 rewrite introduced two behaviours that weren't in 2.58.4: it now branches on the calling user, and for any non-staff caller it returns nothing — ignoring product entirely. That is the regression.

Fix

Resolve the users authorized on the given product / product type via the authorized_users M2M — the inverse of the existing _authorized_product_ids helper, consistent with the rest of the OS layer. Superuser/staff still bypass; anonymous and None object still return none.

  • _get_authorized_users_for_product_type → users in product_type.authorized_users plus superusers
  • _get_authorized_users_for_product_and_product_type → users in product.authorized_users or product.prod_type.authorized_users plus superusers

This is the OS-layer equivalent of the 2.58.4 query: v3 split the model so that the RBAC carrier tables (Product_Member, Product_Type_Group, Global_Role, …) moved to Pro, while OS membership is the authorized_users M2M. The is_superuser clause is carried over from 2.58.4 (which always surfaced superusers as candidates). Pro layers the member/group/global-role resolution back on via its own registration; global-role is Pro-only in v3 (roles are inert in OS), so it is intentionally not resolved here.

It also fixes the same empty-selector symptom in the Test form lead, finding bulk-edit reviewer/assignee, and notification recipient resolution, which call the same functions.

Also in this PR — get_authorized_users (same regression, sibling function)

_get_authorized_users had the identical v2→v3 regression: for any non-staff/non-superuser caller it returned only the caller themselves (pk=user.pk), whereas 2.58.4 returned the caller's collaborators — users sharing the caller's authorized products/types — plus global-role users and superusers. This collapsed several user dropdowns to self-only for scoped users:

  • dojo/engagement/views.py and dojo/test/views.py — the engagement/test "users" lists
  • dojo/auditlog/filters.py — the audit-log actor / user filter dropdowns
  • dojo/api_v2/serializers.pyget_authorized_users("edit", ...)

Fix: resolve collaborators via the authorized_users M2M (users sharing the caller's authorized products / product types) plus superusers — the OS-applicable part of 2.58.4. Carrier roles (Product_Member, Global_Role, …) remain inert in OS; global-role resolution stays a Pro concern.

Tests

unittests/test_user_queries.py:

  • new TestGetAuthorizedUsersViaAuthorizedUsers — a product-authorized user gets a non-empty list containing the product + product-type authorized users and superusers (and excludes unrelated users); product-type-authorized user resolution
  • new test_user_collaborators_via_authorized_usersget_authorized_users returns co-members of the caller's authorized product plus superusers
  • the carrier-table tests were updated for the new behaviour: carrier roles remain inert in OS, but a non-staff caller now correctly sees superusers (previously asserted self-only / none)

🤖 Generated with Claude Code

…efectDojo#15062)

The OS auth-filter impls _get_authorized_users_for_product_type and
_get_authorized_users_for_product_and_product_type ignored their
product_type/product argument and returned users.none() for any
non-staff/non-superuser caller. This emptied the (mandatory) engagement
'Testing Lead' selector for users granted access to a product via the
'authorized users' section, blocking engagement creation (DefectDojo#15062).

Resolve the users authorized on the given product/product type via the
authorized_users M2M (the inverse of _authorized_product_ids), mirroring
the rest of the OS authorization layer. Superuser/staff still bypass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@valentijnscholten valentijnscholten added this to the 3.0.200 milestone Jun 23, 2026
valentijnscholten and others added 3 commits June 23, 2026 22:36
…8.4 parity)

2.58.4's get_authorized_users_for_product_and_product_type always included
is_superuser users (and global-role users) as candidates. Mirror the OS-
applicable part here so admins remain pickable as a Testing Lead for a
product-scoped caller. Global-role resolution stays a Pro concern (roles are
inert in OS).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
….58.4 parity)

The OS _get_authorized_users returned only the calling user (pk=user.pk) for
any non-staff/non-superuser caller, vs 2.58.4 which returned co-members of the
caller's authorized products/types plus global-role users and superusers. This
collapsed the engagement/test 'users' lists and the audit-log actor/user
filters to self-only for scoped users.

Resolve collaborators via the authorized_users M2M (users sharing the caller's
authorized products/product types) plus superusers, mirroring the OS-applicable
part of 2.58.4. Updated the carrier-table 'legacy' tests accordingly (carrier
roles remain inert in OS; superusers now surface) and added a collaborator test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…solution

The get_authorized_users_for_product_and_product_type / _for_product_type
resolution now executes the authorized_users subqueries (the DefectDojo#15062 fix),
adding a small fixed overhead (+1 async / +3 sync) to the import notification
path. Recalibrate both the V2 (TestDojoImporterPerformanceSmall) and V3
(TestDojoImporterPerformanceSmallLocations) expected query counts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants