Skip to content

Commit eef0edf

Browse files
authored
[feature] Added API endpoint to manage RADIUS groups of users #676
Closes #676
1 parent eb34b91 commit eef0edf

6 files changed

Lines changed: 532 additions & 2 deletions

File tree

docs/user/rest-api.rst

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,3 +961,90 @@ DELETE
961961
^^^^^^
962962

963963
Deletes a RADIUS group identified by its UUID.
964+
965+
RADIUS User Groups
966+
++++++++++++++++++
967+
968+
.. code-block:: text
969+
970+
/api/v1/users/user/<user_pk>/radius-group/
971+
972+
GET
973+
^^^
974+
975+
Returns the list of RADIUS group assignments for the specified user.
976+
Pagination is provided using page number pagination; default page size is
977+
20 and can be overridden with the ``page_size`` parameter (maximum 100).
978+
979+
.. code-block:: text
980+
981+
GET /api/v1/users/user/<user_pk>/radius-group/
982+
983+
It supports filtering by organization id and organization slug.
984+
985+
Filters
986+
"""""""
987+
988+
========================= ============================
989+
Filter Parameter Description
990+
========================= ============================
991+
group__organization Filter organizations by id
992+
group__organization__slug Filter organizations by slug
993+
========================= ============================
994+
995+
POST
996+
^^^^
997+
998+
Creates a RADIUS user group assignment for the specified user.
999+
1000+
======== ==============================================
1001+
Param Description
1002+
======== ==============================================
1003+
group UUID of the RADIUS group to assign (required)
1004+
priority Priority of the assignment (optional, integer)
1005+
======== ==============================================
1006+
1007+
.. note::
1008+
1009+
The provided ``group`` must belong to the same organization as the
1010+
user; attempting to assign a group from another organization will
1011+
return a ``400`` error with ``does_not_exist`` for the ``group``
1012+
field.
1013+
1014+
.. code-block:: text
1015+
1016+
/api/v1/users/user/<user_pk>/radius-group/<uuid>/
1017+
1018+
GET (detail)
1019+
^^^^^^^^^^^^
1020+
1021+
Returns a single RADIUS user group assignment by its UUID.
1022+
1023+
PUT
1024+
^^^
1025+
1026+
Fully updates the RADIUS user group assignment.
1027+
1028+
======== ==============================================
1029+
Param Description
1030+
======== ==============================================
1031+
group UUID of the RADIUS group to assign (optional)
1032+
priority Priority of the assignment (optional, integer)
1033+
======== ==============================================
1034+
1035+
PATCH
1036+
^^^^^
1037+
1038+
Partially updates a RADIUS user group assignment.
1039+
1040+
======== ==============================================
1041+
Param Description
1042+
======== ==============================================
1043+
group UUID of the RADIUS group to assign (optional)
1044+
priority Priority of the assignment (optional, integer)
1045+
======== ==============================================
1046+
1047+
DELETE
1048+
^^^^^^
1049+
1050+
Deletes the RADIUS user group assignment identified by the UUID.

openwisp_radius/api/serializers.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,43 @@ def validate(self, data):
365365
return data
366366

367367

368+
class RadiusUserGroupSerializer(FilterSerializerByOrgManaged, ValidatedModelSerializer):
369+
class Meta:
370+
model = RadiusUserGroup
371+
fields = ("id", "group", "priority", "created", "modified")
372+
read_only_fields = ("id", "created", "modified")
373+
374+
def __init__(self, *args, **kwargs):
375+
super().__init__(*args, **kwargs)
376+
view = self.context.get("view")
377+
if (
378+
view
379+
and getattr(view, "get_parent_queryset", None)
380+
and not getattr(view, "swagger_fake_view", False)
381+
):
382+
self._user = view.get_parent_queryset().first()
383+
else:
384+
self._user = None
385+
if self._user and view and getattr(view.request, "user", None):
386+
# Restrict available groups to organizations that the request user manages
387+
# and that the edited user belongs to. This prevents assigning groups from
388+
# organizations outside the request user's management scope.
389+
self.fields["group"].queryset = self.fields["group"].queryset.filter(
390+
Q(organization__in=view.request.user.organizations_managed)
391+
& Q(organization__in=self._user.organizations_dict.keys())
392+
)
393+
else:
394+
self.fields["group"].queryset = self.fields["group"].queryset.none()
395+
396+
def validate(self, data):
397+
if self._user:
398+
if "username" not in data:
399+
data["username"] = self._user.username
400+
if "user" not in data:
401+
data["user"] = self._user
402+
return super().validate(data)
403+
404+
368405
class GroupSerializer(serializers.ModelSerializer):
369406
class Meta:
370407
model = Group

openwisp_radius/api/urls.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,16 @@ def get_api_urls(api_views=None):
9898
api_views.radius_group_detail,
9999
name="radius_group_detail",
100100
),
101+
path(
102+
"users/user/<str:user_pk>/radius-group/",
103+
api_views.radius_user_group_list,
104+
name="radius_user_group_list",
105+
),
106+
path(
107+
"users/user/<str:user_pk>/radius-group/<uuid:pk>/",
108+
api_views.radius_user_group_detail,
109+
name="radius_user_group_detail",
110+
),
101111
]
102112
else:
103113
return []

openwisp_radius/api/views.py

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@
4848
from openwisp_radius.api.serializers import RadiusUserSerializer
4949
from openwisp_users.api.authentication import BearerAuthentication, SesameAuthentication
5050
from openwisp_users.api.filters import OrganizationManagedFilter
51-
from openwisp_users.api.mixins import FilterByOrganizationManaged, ProtectedAPIMixin
51+
from openwisp_users.api.mixins import (
52+
FilterByOrganizationManaged,
53+
FilterByParentManaged,
54+
ProtectedAPIMixin,
55+
)
5256
from openwisp_users.api.permissions import IsOrganizationManager
5357
from openwisp_users.api.views import ChangePasswordView as BasePasswordChangeView
5458
from openwisp_users.backends import UsersAuthenticationBackend
@@ -69,6 +73,7 @@
6973
RadiusAccountingSerializer,
7074
RadiusBatchSerializer,
7175
RadiusGroupSerializer,
76+
RadiusUserGroupSerializer,
7277
UserRadiusUsageSerializer,
7378
ValidatePhoneTokenSerializer,
7479
)
@@ -936,3 +941,123 @@ class RadiusGroupDetailView(
936941

937942

938943
radius_group_detail = RadiusGroupDetailView.as_view()
944+
945+
946+
class BaseRadiusUserGroupView(ProtectedAPIMixin, FilterByParentManaged):
947+
"""
948+
Base view for RadiusUserGroup management.
949+
Provides user parent filtering and queryset logic.
950+
"""
951+
952+
serializer_class = RadiusUserGroupSerializer
953+
queryset = RadiusUserGroup.objects.select_related("group", "user").order_by(
954+
"-created"
955+
)
956+
957+
def get_queryset(self):
958+
if getattr(self, "swagger_fake_view", False):
959+
return self.queryset.none()
960+
qs = (
961+
super()
962+
.get_queryset()
963+
.filter(
964+
user_id=self.kwargs["user_pk"],
965+
)
966+
)
967+
if self.request.user.is_superuser:
968+
return qs
969+
return qs.filter(
970+
group__organization__in=self.request.user.organizations_managed
971+
)
972+
973+
def get_parent_queryset(self):
974+
return User.objects.filter(pk=self.kwargs["user_pk"])
975+
976+
def get_organization_queryset(self, qs):
977+
"""Filter users by organizations the request user manages."""
978+
orgs = self.request.user.organizations_managed
979+
app_label = User._meta.app_config.label
980+
filter_kwargs = {
981+
# exclude superusers
982+
"is_superuser": False,
983+
# ensure user is member of the org
984+
f"{app_label}_organizationuser__organization_id__in": orgs,
985+
}
986+
return qs.filter(**filter_kwargs).distinct()
987+
988+
989+
class RadiusUserGroupFilter(OrganizationManagedFilter, filters.FilterSet):
990+
"""
991+
Filter RADIUS groups by organizations managed by the user.
992+
"""
993+
994+
# Disable parent's organization_slug; use group__organization__slug instead
995+
organization_slug = None
996+
997+
class Meta(OrganizationManagedFilter.Meta):
998+
model = RadiusUserGroup
999+
fields = ["group__organization", "group__organization__slug"]
1000+
1001+
1002+
@method_decorator(
1003+
name="get",
1004+
decorator=swagger_auto_schema(
1005+
operation_description="""
1006+
Returns the list of RADIUS user groups for a specific user.
1007+
""",
1008+
),
1009+
)
1010+
@method_decorator(
1011+
name="post",
1012+
decorator=swagger_auto_schema(
1013+
operation_description="""
1014+
Creates a new RADIUS user group assignment for the user.
1015+
""",
1016+
),
1017+
)
1018+
class RadiusUserGroupListCreateView(BaseRadiusUserGroupView, ListCreateAPIView):
1019+
pagination_class = RadiusGroupPaginator
1020+
filter_backends = [DjangoFilterBackend]
1021+
filterset_class = RadiusUserGroupFilter
1022+
1023+
1024+
radius_user_group_list = RadiusUserGroupListCreateView.as_view()
1025+
1026+
1027+
@method_decorator(
1028+
name="get",
1029+
decorator=swagger_auto_schema(
1030+
operation_description="""
1031+
Returns a single RADIUS user group by its UUID.
1032+
""",
1033+
),
1034+
)
1035+
@method_decorator(
1036+
name="put",
1037+
decorator=swagger_auto_schema(
1038+
operation_description="""
1039+
Updates a RADIUS user group identified by its UUID.
1040+
""",
1041+
),
1042+
)
1043+
@method_decorator(
1044+
name="patch",
1045+
decorator=swagger_auto_schema(
1046+
operation_description="""
1047+
Partially updates a RADIUS user group identified by its UUID.
1048+
""",
1049+
),
1050+
)
1051+
@method_decorator(
1052+
name="delete",
1053+
decorator=swagger_auto_schema(
1054+
operation_description="""
1055+
Deletes a RADIUS user group identified by its UUID.
1056+
""",
1057+
),
1058+
)
1059+
class RadiusUserGroupDetailView(BaseRadiusUserGroupView, RetrieveUpdateDestroyAPIView):
1060+
organization_field = "group__organization"
1061+
1062+
1063+
radius_user_group_detail = RadiusUserGroupDetailView.as_view()

0 commit comments

Comments
 (0)