Skip to content

Commit 906d973

Browse files
committed
Add realm user group membership change views
1 parent d644696 commit 906d973

File tree

8 files changed

+217
-5
lines changed

8 files changed

+217
-5
lines changed

server/realms/forms.py

+16
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,19 @@ def get_queryset(self):
6262
if realm_group:
6363
qs = qs.filter(groups=realm_group)
6464
return qs
65+
66+
67+
class AddRealmUserToGroupForm(forms.Form):
68+
realm_group = forms.ModelChoiceField(label="Realm Group", queryset=RealmGroup.objects.for_update(), required=True)
69+
70+
def __init__(self, *args, **kwargs):
71+
self.realm_user = kwargs.pop("realm_user")
72+
super().__init__(*args, **kwargs)
73+
self.fields["realm_group"].queryset = self.fields["realm_group"].queryset.filter(
74+
realm=self.realm_user.realm
75+
).exclude(
76+
pk__in=[g.pk for g in self.realm_user.groups.all()]
77+
).order_by("display_name")
78+
79+
def save(self):
80+
self.realm_user.groups.add(self.cleaned_data["realm_group"])

server/realms/models.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,10 @@ def serialize_for_event(self, keys_only=False):
105105

106106
class RealmGroupManager(models.Manager):
107107
def for_deletion(self):
108-
return self.filter(scim_external_id__isnull=True)
108+
return self.filter(scim_managed=False)
109109

110110
def for_update(self):
111-
return self.filter(scim_external_id__isnull=True)
111+
return self.filter(scim_managed=False)
112112

113113

114114
class RealmGroup(models.Model):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{% extends 'base.html' %}
2+
3+
{% block content %}
4+
<ol class="breadcrumb">
5+
<li class="breadcrumb-item"><a href="/">Home</a></li>
6+
<li class="breadcrumb-item"><a href="{% url 'realms:index' %}">SSO</a></li>
7+
<li class="breadcrumb-item"><a href="{% url 'realms:users' %}">Users</a></li>
8+
<li class="breadcrumb-item"><a href="{{ realm_user.get_absolute_url }}">{{ realm_user }}</a></li>
9+
<li class="breadcrumb-item active">add to group</li>
10+
</ol>
11+
12+
<h2>Add <i>{{ realm_user }}</i> to group</h2>
13+
14+
<form method="POST">{% csrf_token %}
15+
{{ form }}
16+
<p>
17+
<a class="btn btn-outline-secondary" href="{{ realm_user.get_absolute_url }}">
18+
Cancel
19+
</a>
20+
<button class="btn btn-primary" type="submit">Add</button>
21+
</p>
22+
</form>
23+
{% endblock %}

server/realms/templates/realms/realmuser_detail.html

+13-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,14 @@ <h3 class="m-0 fs-5 text-secondary">Realm User</i></h3>
6363
</tr>
6464
{% with object.groups_with_types as groups_with_types %}
6565
<tr>
66-
<td>Group{{ groups_with_types|length|pluralize }} ({{ groups_with_types|length }})</td>
66+
<td>
67+
Group{{ groups_with_types|length|pluralize }} ({{ groups_with_types|length }})
68+
{% if not request.realm_authentication_session.is_remote and perms.realms.change_realmgroup %}
69+
<a class="btn btn-link" href="{% url 'realms:add_user_to_group' object.pk %}">
70+
<i class="bi bi-plus-circle"></i>
71+
</a>
72+
{% endif %}
73+
</td>
6774
<td>
6875
{% if groups_with_types %}
6976
<ul class="list-unstyled">
@@ -74,6 +81,11 @@ <h3 class="m-0 fs-5 text-secondary">Realm User</i></h3>
7481
{% else %}
7582
{{ group }} ({{ type }})
7683
{% endif %}
84+
{% if not request.realm_authentication_session.is_remote and perms.realms.change_realmgroup and not group.scim_managed %}
85+
<a class="btn btn-link" href="{% url 'realms:remove_user_from_group' object.pk group.pk %}">
86+
<i class="bi bi-trash"></i>
87+
</a>
88+
{% endif %}
7789
</li>
7890
{% endfor %}
7991
</ul>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{% extends 'base.html' %}
2+
3+
{% block content %}
4+
<ol class="breadcrumb">
5+
<li class="breadcrumb-item"><a href="/">Home</a></li>
6+
<li class="breadcrumb-item"><a href="{% url 'realms:index' %}">SSO</a></li>
7+
<li class="breadcrumb-item"><a href="{% url 'realms:users' %}">Users</a></li>
8+
<li class="breadcrumb-item"><a href="{{ realm_user.get_absolute_url }}">{{ realm_user }}</a></li>
9+
<li class="breadcrumb-item active">remove from group</li>
10+
</ol>
11+
12+
<h2>Remove <i>{{ realm_user }}</i> from group</h2>
13+
14+
<form method="POST">{% csrf_token %}
15+
<p>Do you really want to remove this user from the <i>{{ realm_group }}</i> group?</p>
16+
<p>
17+
<a class="btn btn-outline-secondary" href="{{ realm_user.get_absolute_url }}">
18+
Cancel
19+
</a>
20+
<button class="btn btn-danger" type="submit">Remove</button>
21+
</p>
22+
</form>
23+
24+
{% endblock %}

server/realms/urls.py

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
# users
1111
path('users/', views.RealmUserListView.as_view(), name='users'),
1212
path('users/<uuid:pk>/', views.RealmUserView.as_view(), name='user'),
13+
path('users/<uuid:pk>/groups/add/', views.AddRealmUserToGroupView.as_view(),
14+
name='add_user_to_group'),
15+
path('users/<uuid:pk>/groups/<uuid:group_pk>/remove/', views.RemoveRealmUserFromGroupView.as_view(),
16+
name='remove_user_from_group'),
1317

1418
# groups
1519
path('groups/', views.RealmGroupListView.as_view(), name='groups'),

server/realms/views.py

+55-2
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
from django.http import Http404, HttpResponseRedirect
99
from django.shortcuts import get_object_or_404, redirect
1010
from django.urls import reverse, reverse_lazy
11-
from django.views.generic import CreateView, DeleteView, DetailView, ListView, TemplateView, UpdateView, View
11+
from django.views.generic import CreateView, DeleteView, DetailView, FormView, ListView, TemplateView, UpdateView, View
1212
from zentral.conf import settings as zentral_settings
1313
from zentral.utils.views import UserPaginationListView
1414
from .backends.registry import backend_classes
15-
from .forms import RealmGroupSearchForm, RealmUserSearchForm
15+
from .forms import AddRealmUserToGroupForm, RealmGroupSearchForm, RealmUserSearchForm
1616
from .models import (Realm, RealmAuthenticationSession,
1717
RealmGroup, RealmGroupMapping, RoleMapping,
1818
RealmUser, RealmUserGroupMembership,
@@ -273,6 +273,59 @@ def get_context_data(self, **kwargs):
273273
return ctx
274274

275275

276+
class AddRealmUserToGroupView(LocalUserRequiredMixin, PermissionRequiredMixin, FormView):
277+
permission_required = "realms.change_realmgroup"
278+
template_name = "realms/realmuser_add_to_group.html"
279+
form_class = AddRealmUserToGroupForm
280+
281+
def get_object(self):
282+
self.realm_user = get_object_or_404(RealmUser, pk=self.kwargs["pk"])
283+
284+
def get_context_data(self, **kwargs):
285+
self.get_object()
286+
ctx = super().get_context_data(**kwargs)
287+
ctx["realm_user"] = self.realm_user
288+
return ctx
289+
290+
def get_form_kwargs(self):
291+
self.get_object()
292+
kwargs = super().get_form_kwargs()
293+
kwargs["realm_user"] = self.realm_user
294+
return kwargs
295+
296+
def form_valid(self, form):
297+
form.save()
298+
realm_group_members_updated.send_robust(self.__class__, realm=self.realm_user.realm, request=self.request)
299+
return redirect(self.realm_user)
300+
301+
302+
class RemoveRealmUserFromGroupView(LocalUserRequiredMixin, PermissionRequiredMixin, TemplateView):
303+
permission_required = "realms.change_realmgroup"
304+
template_name = "realms/realmuser_remove_from_group.html"
305+
306+
def get_objects(self):
307+
self.realm_user = get_object_or_404(RealmUser, pk=self.kwargs["pk"])
308+
self.realm_group = get_object_or_404(
309+
RealmGroup,
310+
pk=self.kwargs["group_pk"],
311+
realm=self.realm_user.realm,
312+
scim_managed=False,
313+
)
314+
315+
def get_context_data(self, **kwargs):
316+
self.get_objects()
317+
ctx = super().get_context_data(**kwargs)
318+
ctx["realm_user"] = self.realm_user
319+
ctx["realm_group"] = self.realm_group
320+
return ctx
321+
322+
def post(self, request, *args, **kwargs):
323+
self.get_objects()
324+
self.realm_user.groups.remove(self.realm_group)
325+
realm_group_members_updated.send_robust(self.__class__, realm=self.realm_user.realm, request=self.request)
326+
return redirect(self.realm_user)
327+
328+
276329
# realm group mappings
277330

278331

tests/server_realms/test_realm_views.py

+80
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,86 @@ def test_realm_user_one_zentral_no_user_link(self):
11391139
self.assertNotContains(response, user.get_absolute_url())
11401140
self.assertContains(response, "Zentral user (1)")
11411141

1142+
# add realm user to group
1143+
1144+
def test_add_realm_user_to_group_login_redirect(self):
1145+
_, user = force_realm_user()
1146+
self.login_redirect("add_user_to_group", user.pk)
1147+
1148+
def test_add_realm_user_to_group_permission_denied(self):
1149+
_, user = force_realm_user()
1150+
self.login("realms.view_realmuser")
1151+
response = self.client.get(reverse("realms:add_user_to_group", args=(user.pk,)))
1152+
self.assertEqual(response.status_code, 403)
1153+
1154+
def test_add_realm_user_to_group_get(self):
1155+
existing_group = force_realm_group()
1156+
realm, user = force_realm_user(realm=existing_group.realm, group=existing_group)
1157+
available_group = force_realm_group(realm=realm)
1158+
scim_group = force_realm_group(realm=realm)
1159+
scim_group.scim_managed = True
1160+
scim_group.save()
1161+
other_group = force_realm_group()
1162+
self.login("realms.change_realmgroup")
1163+
response = self.client.get(reverse("realms:add_user_to_group", args=(user.pk,)))
1164+
self.assertEqual(response.status_code, 200)
1165+
self.assertTemplateUsed(response, "realms/realmuser_add_to_group.html")
1166+
self.assertNotContains(response, existing_group.display_name)
1167+
self.assertContains(response, available_group.display_name)
1168+
self.assertNotContains(response, scim_group.display_name)
1169+
self.assertNotContains(response, other_group.display_name)
1170+
1171+
@patch("realms.views.realm_group_members_updated.send_robust")
1172+
def test_add_realm_user_to_group_post(self, send_robust):
1173+
scim_group = force_realm_group()
1174+
scim_group.scim_managed = True
1175+
scim_group.save()
1176+
realm, user = force_realm_user(realm=scim_group.realm, group=scim_group)
1177+
group = force_realm_group(realm=realm)
1178+
self.login("realms.change_realmgroup", "realms.view_realmuser")
1179+
response = self.client.post(reverse("realms:add_user_to_group", args=(user.pk,)),
1180+
{"realm_group": str(group.pk)},
1181+
follow=True)
1182+
self.assertEqual(response.status_code, 200)
1183+
self.assertTemplateUsed(response, "realms/realmuser_detail.html")
1184+
self.assertNotContains(response, reverse("realms:remove_user_from_group", args=(user.pk, scim_group.pk)))
1185+
self.assertContains(response, reverse("realms:remove_user_from_group", args=(user.pk, group.pk)))
1186+
send_robust.assert_called_once()
1187+
1188+
# remove realm user from group
1189+
1190+
def test_remove_realm_user_from_group_redirect(self):
1191+
group = force_realm_group()
1192+
_, user = force_realm_user(realm=group.realm, group=group)
1193+
self.login_redirect("remove_user_from_group", user.pk, group.pk)
1194+
1195+
def test_remove_realm_user_from_group_permission_denied(self):
1196+
group = force_realm_group()
1197+
_, user = force_realm_user(realm=group.realm, group=group)
1198+
self.login("realms.view_realmuser")
1199+
response = self.client.get(reverse("realms:remove_user_from_group", args=(user.pk, group.pk)))
1200+
self.assertEqual(response.status_code, 403)
1201+
1202+
def test_remove_realm_user_from_group_get(self):
1203+
group = force_realm_group()
1204+
_, user = force_realm_user(realm=group.realm, group=group)
1205+
self.login("realms.change_realmgroup")
1206+
response = self.client.get(reverse("realms:remove_user_from_group", args=(user.pk, group.pk)))
1207+
self.assertEqual(response.status_code, 200)
1208+
self.assertTemplateUsed(response, "realms/realmuser_remove_from_group.html")
1209+
1210+
@patch("realms.views.realm_group_members_updated.send_robust")
1211+
def test_remove_realm_user_from_group_post(self, send_robust):
1212+
group = force_realm_group()
1213+
_, user = force_realm_user(realm=group.realm, group=group)
1214+
self.login("realms.change_realmgroup", "realms.view_realmuser")
1215+
response = self.client.post(reverse("realms:remove_user_from_group", args=(user.pk, group.pk)),
1216+
follow=True)
1217+
self.assertEqual(response.status_code, 200)
1218+
self.assertTemplateUsed(response, "realms/realmuser_detail.html")
1219+
self.assertNotContains(response, group.display_name)
1220+
send_robust.assert_called_once()
1221+
11421222
# test realm
11431223

11441224
def test_realm_permission_denied(self):

0 commit comments

Comments
 (0)