Skip to content
87 changes: 87 additions & 0 deletions docs/user/rest-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -961,3 +961,90 @@ DELETE
^^^^^^

Deletes a RADIUS group identified by its UUID.

RADIUS User Groups
++++++++++++++++++

.. code-block:: text

/api/v1/users/user/<user_pk>/radius-group/

GET
^^^

Returns the list of RADIUS group assignments for the specified user.
Pagination is provided using page number pagination; default page size is
20 and can be overridden with the ``page_size`` parameter (maximum 100).

.. code-block:: text

GET /api/v1/users/user/<user_pk>/radius-group/

It supports filtering by organization id and organization slug.

Filters
"""""""

========================= ============================
Filter Parameter Description
========================= ============================
group__organization Filter organizations by id
group__organization__slug Filter organizations by slug
========================= ============================

POST
^^^^

Creates a RADIUS user group assignment for the specified user.

======== ==============================================
Param Description
======== ==============================================
group UUID of the RADIUS group to assign (required)
priority Priority of the assignment (optional, integer)
======== ==============================================

.. note::

The provided ``group`` must belong to the same organization as the
user; attempting to assign a group from another organization will
return a ``400`` error with ``does_not_exist`` for the ``group``
field.

.. code-block:: text

/api/v1/users/user/<user_pk>/radius-group/<uuid>/

GET (detail)
^^^^^^^^^^^^

Returns a single RADIUS user group assignment by its UUID.

PUT
^^^

Fully updates the RADIUS user group assignment.

======== ==============================================
Param Description
======== ==============================================
group UUID of the RADIUS group to assign (optional)
priority Priority of the assignment (optional, integer)
======== ==============================================

PATCH
^^^^^

Partially updates a RADIUS user group assignment.

======== ==============================================
Param Description
======== ==============================================
group UUID of the RADIUS group to assign (optional)
priority Priority of the assignment (optional, integer)
======== ==============================================

DELETE
^^^^^^

Deletes the RADIUS user group assignment identified by the UUID.
37 changes: 37 additions & 0 deletions openwisp_radius/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,43 @@ def validate(self, data):
return data


class RadiusUserGroupSerializer(FilterSerializerByOrgManaged, ValidatedModelSerializer):
class Meta:
model = RadiusUserGroup
fields = ("id", "group", "priority", "created", "modified")
read_only_fields = ("id", "created", "modified")

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
view = self.context.get("view")
if (
view
and getattr(view, "get_parent_queryset", None)
and not getattr(view, "swagger_fake_view", False)
):
self._user = view.get_parent_queryset().first()
else:
self._user = None
if self._user and view and getattr(view.request, "user", None):
# Restrict available groups to organizations that the request user manages
# and that the edited user belongs to. This prevents assigning groups from
# organizations outside the request user's management scope.
self.fields["group"].queryset = self.fields["group"].queryset.filter(
Q(organization__in=view.request.user.organizations_managed)
& Q(organization__in=self._user.organizations_dict.keys())
)
else:
self.fields["group"].queryset = self.fields["group"].queryset.none()

def validate(self, data):
if self._user:
if "username" not in data:
data["username"] = self._user.username
if "user" not in data:
data["user"] = self._user
return super().validate(data)


class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = Group
Expand Down
10 changes: 10 additions & 0 deletions openwisp_radius/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ def get_api_urls(api_views=None):
api_views.radius_group_detail,
name="radius_group_detail",
),
path(
"users/user/<str:user_pk>/radius-group/",
api_views.radius_user_group_list,
name="radius_user_group_list",
),
path(
"users/user/<str:user_pk>/radius-group/<uuid:pk>/",
api_views.radius_user_group_detail,
name="radius_user_group_detail",
),
]
else:
return []
127 changes: 126 additions & 1 deletion openwisp_radius/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@
from openwisp_radius.api.serializers import RadiusUserSerializer
from openwisp_users.api.authentication import BearerAuthentication, SesameAuthentication
from openwisp_users.api.filters import OrganizationManagedFilter
from openwisp_users.api.mixins import FilterByOrganizationManaged, ProtectedAPIMixin
from openwisp_users.api.mixins import (
FilterByOrganizationManaged,
FilterByParentManaged,
ProtectedAPIMixin,
)
from openwisp_users.api.permissions import IsOrganizationManager
from openwisp_users.api.views import ChangePasswordView as BasePasswordChangeView
from openwisp_users.backends import UsersAuthenticationBackend
Expand All @@ -69,6 +73,7 @@
RadiusAccountingSerializer,
RadiusBatchSerializer,
RadiusGroupSerializer,
RadiusUserGroupSerializer,
UserRadiusUsageSerializer,
ValidatePhoneTokenSerializer,
)
Expand Down Expand Up @@ -936,3 +941,123 @@ class RadiusGroupDetailView(


radius_group_detail = RadiusGroupDetailView.as_view()


class BaseRadiusUserGroupView(ProtectedAPIMixin, FilterByParentManaged):
"""
Base view for RadiusUserGroup management.
Provides user parent filtering and queryset logic.
"""

serializer_class = RadiusUserGroupSerializer
queryset = RadiusUserGroup.objects.select_related("group", "user").order_by(
"-created"
)

def get_queryset(self):
if getattr(self, "swagger_fake_view", False):
return self.queryset.none()
qs = (
super()
.get_queryset()
.filter(
user_id=self.kwargs["user_pk"],
)
)
if self.request.user.is_superuser:
return qs
return qs.filter(
group__organization__in=self.request.user.organizations_managed
)

def get_parent_queryset(self):
return User.objects.filter(pk=self.kwargs["user_pk"])

def get_organization_queryset(self, qs):
"""Filter users by organizations the request user manages."""
orgs = self.request.user.organizations_managed
app_label = User._meta.app_config.label
filter_kwargs = {
# exclude superusers
"is_superuser": False,
# ensure user is member of the org
f"{app_label}_organizationuser__organization_id__in": orgs,
}
return qs.filter(**filter_kwargs).distinct()


class RadiusUserGroupFilter(OrganizationManagedFilter, filters.FilterSet):
"""
Filter RADIUS groups by organizations managed by the user.
"""

# Disable parent's organization_slug; use group__organization__slug instead
organization_slug = None

class Meta(OrganizationManagedFilter.Meta):
model = RadiusUserGroup
fields = ["group__organization", "group__organization__slug"]


@method_decorator(
name="get",
decorator=swagger_auto_schema(
operation_description="""
Returns the list of RADIUS user groups for a specific user.
""",
),
)
@method_decorator(
name="post",
decorator=swagger_auto_schema(
operation_description="""
Creates a new RADIUS user group assignment for the user.
""",
),
)
class RadiusUserGroupListCreateView(BaseRadiusUserGroupView, ListCreateAPIView):
pagination_class = RadiusGroupPaginator
filter_backends = [DjangoFilterBackend]
filterset_class = RadiusUserGroupFilter


radius_user_group_list = RadiusUserGroupListCreateView.as_view()


@method_decorator(
name="get",
decorator=swagger_auto_schema(
operation_description="""
Returns a single RADIUS user group by its UUID.
""",
),
)
@method_decorator(
name="put",
decorator=swagger_auto_schema(
operation_description="""
Updates a RADIUS user group identified by its UUID.
""",
),
)
@method_decorator(
name="patch",
decorator=swagger_auto_schema(
operation_description="""
Partially updates a RADIUS user group identified by its UUID.
""",
),
)
@method_decorator(
name="delete",
decorator=swagger_auto_schema(
operation_description="""
Deletes a RADIUS user group identified by its UUID.
""",
),
)
class RadiusUserGroupDetailView(BaseRadiusUserGroupView, RetrieveUpdateDestroyAPIView):
organization_field = "group__organization"


radius_user_group_detail = RadiusUserGroupDetailView.as_view()
Loading
Loading