Skip to content

Commit d34a5bd

Browse files
devGregAclaude
andcommitted
feat(authorization): add Authorized Users UI for legacy access management
Replaces the inert Members/Groups panels (which silently failed under legacy authorization — they wrote to Product_Member rows that legacy auth ignores) with a working Authorized Users panel that reads/writes the actual M2M that legacy authorization checks: Product.authorized_users and Product_Type.authorized_users. Both classic-Django UI trees (Tailwind and Classic Bootstrap) get the new panel and lose the dead Members/Groups blocks. The OS templates leave empty {% block rbac_members_panel %} / {% block rbac_groups_panel %} hooks for Pro to override (Pro renders RBAC panels via its own template overrides). Same treatment for the left-nav Groups link, which wraps the removed link in a {% block groups_submenu_link %} hook. Endpoints (gated on Permissions.{Product,Product_Type}_Manage_Members which maps to Action.StaffOnly under legacy): * /product/<pid>/authorized_users/add * /product/<pid>/authorized_users/<user_id>/delete * /product/type/<ptid>/authorized_users/add * /product/type/<ptid>/authorized_users/<user_id>/delete Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a6dcc5d commit d34a5bd

16 files changed

Lines changed: 487 additions & 558 deletions

dojo/asset/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,12 @@
263263
name="edit_product_member"),
264264
re_path(r"^product/member/(?P<memberid>\d+)/delete$", views.delete_product_member,
265265
name="delete_product_member"),
266+
re_path(r"^product/(?P<pid>\d+)/authorized_users/add$",
267+
views.add_product_authorized_users,
268+
name="add_product_authorized_users"),
269+
re_path(r"^product/(?P<pid>\d+)/authorized_users/(?P<user_id>\d+)/delete$",
270+
views.delete_product_authorized_user,
271+
name="delete_product_authorized_user"),
266272
re_path(r"^product/(?P<pid>\d+)/add_api_scan_configuration$", views.add_api_scan_configuration,
267273
name="add_api_scan_configuration"),
268274
re_path(r"^product/(?P<pid>\d+)/view_api_scan_configurations$", views.view_api_scan_configurations,

dojo/forms.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,23 @@ class Meta:
306306
fields = ["product_type", "users", "role"]
307307

308308

309+
class Add_Product_Type_AuthorizedUsersForm(forms.Form):
310+
users = forms.ModelMultipleChoiceField(
311+
queryset=Dojo_User.objects.none(), required=True, label="Users",
312+
)
313+
314+
def __init__(self, *args, product_type=None, **kwargs):
315+
super().__init__(*args, **kwargs)
316+
self.product_type = product_type
317+
current = product_type.authorized_users.values_list("pk", flat=True)
318+
self.fields["users"].queryset = (
319+
Dojo_User.objects.filter(is_active=True)
320+
.exclude(is_superuser=True)
321+
.exclude(pk__in=current)
322+
.order_by("first_name", "last_name")
323+
)
324+
325+
309326
class Add_Product_Type_Member_UserForm(forms.ModelForm):
310327
product_types = forms.ModelMultipleChoiceField(queryset=Product_Type.objects.none(), required=True,
311328
label=labels.ORG_PLURAL_LABEL)
@@ -476,6 +493,23 @@ class Meta:
476493
fields = ["product", "users", "role"]
477494

478495

496+
class Add_Product_AuthorizedUsersForm(forms.Form):
497+
users = forms.ModelMultipleChoiceField(
498+
queryset=Dojo_User.objects.none(), required=True, label="Users",
499+
)
500+
501+
def __init__(self, *args, product=None, **kwargs):
502+
super().__init__(*args, **kwargs)
503+
self.product = product
504+
current = product.authorized_users.values_list("pk", flat=True)
505+
self.fields["users"].queryset = (
506+
Dojo_User.objects.filter(is_active=True)
507+
.exclude(is_superuser=True)
508+
.exclude(pk__in=current)
509+
.order_by("first_name", "last_name")
510+
)
511+
512+
479513
class Add_Product_Member_UserForm(forms.ModelForm):
480514
products = forms.ModelMultipleChoiceField(queryset=Product.objects.none(), required=True,
481515
label=labels.ASSET_PLURAL_LABEL)

dojo/organization/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@
103103
name="edit_product_type_member"),
104104
re_path(r"^product/type/member/(?P<memberid>\d+)/delete$", views.delete_product_type_member,
105105
name="delete_product_type_member"),
106+
re_path(r"^product/type/(?P<ptid>\d+)/authorized_users/add$",
107+
views.add_product_type_authorized_users,
108+
name="add_product_type_authorized_users"),
109+
re_path(r"^product/type/(?P<ptid>\d+)/authorized_users/(?P<user_id>\d+)/delete$",
110+
views.delete_product_type_authorized_user,
111+
name="delete_product_type_authorized_user"),
106112
re_path(r"^product/type/(?P<ptid>\d+)/add_group$", views.add_product_type_group,
107113
name="add_product_type_group"),
108114
re_path(r"^product/type/group/(?P<groupid>\d+)/edit$", views.edit_product_type_group,

dojo/product/views.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
Product_Group,
3232
Product_Member,
3333
)
34+
from dojo.authorization.roles_permissions import Permissions
3435
from dojo.components.sql_group_concat import Sql_GroupConcat
3536
from dojo.filters import (
3637
EngagementFilter,
@@ -46,6 +47,7 @@
4647
ProductFilterWithoutObjectLookups,
4748
)
4849
from dojo.forms import (
50+
Add_Product_AuthorizedUsersForm,
4951
Add_Product_GroupForm,
5052
Add_Product_MemberForm,
5153
AdHocFindingForm,
@@ -78,6 +80,7 @@
7880
Benchmark_Product_Summary,
7981
Benchmark_Type,
8082
BurpRawRequestResponse,
83+
Dojo_User,
8184
DojoMeta,
8285
Endpoint,
8386
Endpoint_Status,
@@ -252,6 +255,9 @@ def view_product(request, pid):
252255
.prefetch_related("members") \
253256
.prefetch_related("prod_type__members")
254257
prod = get_object_or_404(prod_query, id=pid)
258+
authorized_users = prod.authorized_users.order_by("first_name", "last_name", "username")
259+
# kept for Pro template override `{% block rbac_members_panel %}` /
260+
# `{% block rbac_groups_panel %}` at pro/templates/dojo/view_product_details.html
255261
product_members = get_authorized_members_for_product(prod, "view")
256262
global_product_members = get_authorized_global_members_for_product(prod, "view")
257263
product_type_members = get_authorized_members_for_product_type(prod.prod_type, "view")
@@ -333,6 +339,7 @@ def view_product(request, pid):
333339
"benchmarks_percents": benchAndPercent,
334340
"benchmarks": benchmarks,
335341
"benchmark_type": product_tab.benchmark_type,
342+
"authorized_users": authorized_users,
336343
"product_members": product_members,
337344
"global_product_members": global_product_members,
338345
"product_type_members": product_type_members,
@@ -1778,6 +1785,46 @@ def delete_product_member(request, memberid):
17781785
})
17791786

17801787

1788+
def add_product_authorized_users(request, pid):
1789+
product = get_object_or_404(Product, pk=pid)
1790+
user_has_permission_or_403(request.user, product, Permissions.Product_Manage_Members)
1791+
page_name = _("Add Authorized Users")
1792+
form = Add_Product_AuthorizedUsersForm(product=product)
1793+
if request.method == "POST":
1794+
form = Add_Product_AuthorizedUsersForm(request.POST, product=product)
1795+
if form.is_valid():
1796+
users = form.cleaned_data["users"]
1797+
product.authorized_users.add(*users)
1798+
messages.add_message(
1799+
request, messages.SUCCESS,
1800+
_("Added %(count)d user(s) to authorized users.") % {"count": len(users)},
1801+
extra_tags="alert-success",
1802+
)
1803+
return HttpResponseRedirect(reverse("view_product", args=(pid,)))
1804+
product_tab = Product_Tab(product, title=page_name, tab="settings")
1805+
return render(request, "dojo/new_product_authorized_users.html", {
1806+
"name": page_name,
1807+
"product": product,
1808+
"form": form,
1809+
"product_tab": product_tab,
1810+
})
1811+
1812+
1813+
def delete_product_authorized_user(request, pid, user_id):
1814+
product = get_object_or_404(Product, pk=pid)
1815+
user_has_permission_or_403(request.user, product, Permissions.Product_Manage_Members)
1816+
if request.method != "POST":
1817+
raise PermissionDenied
1818+
user = get_object_or_404(Dojo_User, pk=user_id)
1819+
product.authorized_users.remove(user)
1820+
messages.add_message(
1821+
request, messages.SUCCESS,
1822+
_("Removed %(username)s from authorized users.") % {"username": user.username},
1823+
extra_tags="alert-success",
1824+
)
1825+
return HttpResponseRedirect(reverse("view_product", args=(pid,)))
1826+
1827+
17811828
def add_api_scan_configuration(request, pid):
17821829
product = get_object_or_404(Product, id=pid)
17831830
if request.method == "POST":

dojo/product_type/views.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from django.contrib import messages
55
from django.contrib.admin.utils import NestedObjects
6+
from django.core.exceptions import PermissionDenied
67
from django.db import DEFAULT_DB_ALIAS
78
from django.db.models import OuterRef, Value
89
from django.db.models.functions import Coalesce
@@ -12,10 +13,12 @@
1213
from django.urls import reverse
1314
from django.utils.translation import gettext as _
1415

15-
from dojo.authorization.authorization import user_has_permission
16+
from dojo.authorization.authorization import user_has_permission, user_has_permission_or_403
1617
from dojo.authorization.models import Product_Type_Group, Product_Type_Member, Role
18+
from dojo.authorization.roles_permissions import Permissions
1719
from dojo.filters import ProductFilter, ProductFilterWithoutObjectLookups, ProductTypeFilter
1820
from dojo.forms import (
21+
Add_Product_Type_AuthorizedUsersForm,
1922
Add_Product_Type_GroupForm,
2023
Add_Product_Type_MemberForm,
2124
Delete_Product_Type_GroupForm,
@@ -26,7 +29,7 @@
2629
Product_TypeForm,
2730
)
2831
from dojo.labels import get_labels
29-
from dojo.models import Finding, Product, Product_Type
32+
from dojo.models import Dojo_User, Finding, Product, Product_Type
3033
from dojo.product.queries import get_authorized_products
3134
from dojo.product_type.queries import (
3235
get_authorized_global_groups_for_product_type,
@@ -125,6 +128,9 @@ def add_product_type(request):
125128
def view_product_type(request, ptid):
126129
page_name = str(labels.ORG_READ_LABEL)
127130
pt = get_object_or_404(Product_Type, pk=ptid)
131+
authorized_users = pt.authorized_users.order_by("first_name", "last_name", "username")
132+
# kept for Pro template override `{% block rbac_members_panel %}` /
133+
# `{% block rbac_groups_panel %}` at pro/templates/dojo/view_product_type.html
128134
members = get_authorized_members_for_product_type(pt, "view")
129135
global_members = get_authorized_global_members_for_product_type(pt, "view")
130136
groups = get_authorized_groups_for_product_type(pt, "view")
@@ -141,6 +147,7 @@ def view_product_type(request, ptid):
141147
"pt": pt,
142148
"products": products,
143149
"prod_filter": prod_filter,
150+
"authorized_users": authorized_users,
144151
"groups": groups,
145152
"members": members,
146153
"global_groups": global_groups,
@@ -319,6 +326,45 @@ def delete_product_type_member(request, memberid):
319326
})
320327

321328

329+
def add_product_type_authorized_users(request, ptid):
330+
pt = get_object_or_404(Product_Type, pk=ptid)
331+
user_has_permission_or_403(request.user, pt, Permissions.Product_Type_Manage_Members)
332+
page_name = _("Add Authorized Users")
333+
form = Add_Product_Type_AuthorizedUsersForm(product_type=pt)
334+
if request.method == "POST":
335+
form = Add_Product_Type_AuthorizedUsersForm(request.POST, product_type=pt)
336+
if form.is_valid():
337+
users = form.cleaned_data["users"]
338+
pt.authorized_users.add(*users)
339+
messages.add_message(
340+
request, messages.SUCCESS,
341+
_("Added %(count)d user(s) to authorized users.") % {"count": len(users)},
342+
extra_tags="alert-success",
343+
)
344+
return HttpResponseRedirect(reverse("view_product_type", args=(ptid,)))
345+
add_breadcrumb(title=page_name, top_level=False, request=request)
346+
return render(request, "dojo/new_product_type_authorized_users.html", {
347+
"name": page_name,
348+
"pt": pt,
349+
"form": form,
350+
})
351+
352+
353+
def delete_product_type_authorized_user(request, ptid, user_id):
354+
pt = get_object_or_404(Product_Type, pk=ptid)
355+
user_has_permission_or_403(request.user, pt, Permissions.Product_Type_Manage_Members)
356+
if request.method != "POST":
357+
raise PermissionDenied
358+
user = get_object_or_404(Dojo_User, pk=user_id)
359+
pt.authorized_users.remove(user)
360+
messages.add_message(
361+
request, messages.SUCCESS,
362+
_("Removed %(username)s from authorized users.") % {"username": user.username},
363+
extra_tags="alert-success",
364+
)
365+
return HttpResponseRedirect(reverse("view_product_type", args=(ptid,)))
366+
367+
322368
def add_product_type_group(request, ptid):
323369
page_name = str(labels.ORG_GROUPS_ADD_LABEL)
324370
pt = get_object_or_404(Product_Type, pk=ptid)

dojo/templates/base.html

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@
290290
</div>
291291

292292
<!-- Users -->
293-
{% if "auth.view_user"|has_configuration_permission:request or "auth.view_group"|has_configuration_permission:request %}
293+
{% if "auth.view_user"|has_configuration_permission:request %}
294294
<div x-data="{ open: false }">
295295
<a href="{% url 'users' %}" @click.prevent="open = !open"
296296
class="flex items-center gap-3 px-4 py-2 text-gray-300 hover:text-white hover:bg-white/10 transition-colors cursor-pointer">
@@ -302,9 +302,8 @@
302302
{% if "auth.view_user"|has_configuration_permission:request %}
303303
<a href="{% url 'users' %}" class="block py-1.5 pl-12 pr-4 text-sm text-gray-400 hover:text-white hover:bg-white/10">{% trans "Users" %}</a>
304304
{% endif %}
305-
{% if "auth.view_group"|has_configuration_permission:request %}
306-
<a href="{% url 'groups' %}" class="block py-1.5 pl-12 pr-4 text-sm text-gray-400 hover:text-white hover:bg-white/10">{% trans "Groups" %}</a>
307-
{% endif %}
305+
{# Pro restores the Groups link by overriding this block. #}
306+
{% block groups_submenu_link %}{% endblock %}
308307
</div>
309308
</div>
310309
{% endif %}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{% extends "base.html" %}
2+
{% load i18n %}
3+
{% block content %}
4+
{{ block.super }}
5+
<h3>{{ name }}</h3>
6+
<form class="" action="{% url 'add_product_authorized_users' product.id %}" method="post">{% csrf_token %}
7+
{% include "dojo/form_fields.html" with form=form %}
8+
<div class="form-group">
9+
<div class="col-sm-offset-2 col-sm-10">
10+
<input class="btn btn-primary" type="submit" value="{% trans "Submit" %}"/>
11+
</div>
12+
</div>
13+
</form>
14+
{% endblock %}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{% extends "base.html" %}
2+
{% load i18n %}
3+
{% block content %}
4+
{{ block.super }}
5+
<h3>{{ name }}</h3>
6+
<form class="" action="{% url 'add_product_type_authorized_users' pt.id %}" method="post">{% csrf_token %}
7+
{% include "dojo/form_fields.html" with form=form %}
8+
<div class="form-group">
9+
<div class="col-sm-offset-2 col-sm-10">
10+
<input class="btn btn-primary" type="submit" value="{% trans "Submit" %}"/>
11+
</div>
12+
</div>
13+
</form>
14+
{% endblock %}

0 commit comments

Comments
 (0)