mirror of
https://github.com/ae-utbm/sith.git
synced 2025-07-09 19:40:19 +00:00
Move core auth mixins to their own file
This commit is contained in:
0
core/auth/__init__.py
Normal file
0
core/auth/__init__.py
Normal file
138
core/auth/api_permissions.py
Normal file
138
core/auth/api_permissions.py
Normal file
@ -0,0 +1,138 @@
|
||||
"""Permission classes to be used within ninja-extra controllers.
|
||||
|
||||
Some permissions are global (like `IsInGroup` or `IsRoot`),
|
||||
and some others are per-object (like `CanView` or `CanEdit`).
|
||||
|
||||
Examples:
|
||||
# restrict all the routes of this controller
|
||||
# to subscribed users
|
||||
@api_controller("/foo", permissions=[IsSubscriber])
|
||||
class FooController(ControllerBase):
|
||||
@route.get("/bar")
|
||||
def bar_get(self):
|
||||
# This route inherits the permissions of the controller
|
||||
# ...
|
||||
|
||||
@route.bar("/bar/{bar_id}", permissions=[CanView])
|
||||
def bar_get_one(self, bar_id: int):
|
||||
# per-object permission resolution happens
|
||||
# when calling either the `get_object_or_exception`
|
||||
# or `get_object_or_none` method.
|
||||
bar = self.get_object_or_exception(Counter, pk=bar_id)
|
||||
|
||||
# you can also call the `check_object_permission` manually
|
||||
other_bar = Counter.objects.first()
|
||||
self.check_object_permissions(other_bar)
|
||||
|
||||
# ...
|
||||
|
||||
# This route is restricted to counter admins and root users
|
||||
@route.delete(
|
||||
"/bar/{bar_id}",
|
||||
permissions=[IsRoot | IsInGroup(settings.SITH_GROUP_COUNTER_ADMIN_ID)
|
||||
]
|
||||
def bar_delete(self, bar_id: int):
|
||||
# ...
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from django.http import HttpRequest
|
||||
from ninja_extra import ControllerBase
|
||||
from ninja_extra.permissions import BasePermission
|
||||
|
||||
from counter.models import Counter
|
||||
|
||||
|
||||
class IsInGroup(BasePermission):
|
||||
"""Check that the user is in the group whose primary key is given."""
|
||||
|
||||
def __init__(self, group_pk: int):
|
||||
self._group_pk = group_pk
|
||||
|
||||
def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
|
||||
return request.user.is_in_group(pk=self._group_pk)
|
||||
|
||||
|
||||
class IsRoot(BasePermission):
|
||||
"""Check that the user is root."""
|
||||
|
||||
def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
|
||||
return request.user.is_root
|
||||
|
||||
|
||||
class IsSubscriber(BasePermission):
|
||||
"""Check that the user is currently subscribed."""
|
||||
|
||||
def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
|
||||
return request.user.is_subscribed
|
||||
|
||||
|
||||
class IsOldSubscriber(BasePermission):
|
||||
"""Check that the user has at least one subscription in its history."""
|
||||
|
||||
def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
|
||||
return request.user.was_subscribed
|
||||
|
||||
|
||||
class CanView(BasePermission):
|
||||
"""Check that this user has the permission to view the object of this route.
|
||||
|
||||
Wrap the `user.can_view(obj)` method.
|
||||
To see an example, look at the example in the module docstring.
|
||||
"""
|
||||
|
||||
def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
|
||||
return True
|
||||
|
||||
def has_object_permission(
|
||||
self, request: HttpRequest, controller: ControllerBase, obj: Any
|
||||
) -> bool:
|
||||
return request.user.can_view(obj)
|
||||
|
||||
|
||||
class CanEdit(BasePermission):
|
||||
"""Check that this user has the permission to edit the object of this route.
|
||||
|
||||
Wrap the `user.can_edit(obj)` method.
|
||||
To see an example, look at the example in the module docstring.
|
||||
"""
|
||||
|
||||
def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
|
||||
return True
|
||||
|
||||
def has_object_permission(
|
||||
self, request: HttpRequest, controller: ControllerBase, obj: Any
|
||||
) -> bool:
|
||||
return request.user.can_edit(obj)
|
||||
|
||||
|
||||
class IsOwner(BasePermission):
|
||||
"""Check that this user owns the object of this route.
|
||||
|
||||
Wrap the `user.is_owner(obj)` method.
|
||||
To see an example, look at the example in the module docstring.
|
||||
"""
|
||||
|
||||
def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
|
||||
return True
|
||||
|
||||
def has_object_permission(
|
||||
self, request: HttpRequest, controller: ControllerBase, obj: Any
|
||||
) -> bool:
|
||||
return request.user.is_owner(obj)
|
||||
|
||||
|
||||
class IsLoggedInCounter(BasePermission):
|
||||
"""Check that a user is logged in a counter."""
|
||||
|
||||
def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
|
||||
if "/counter/" not in request.META.get("HTTP_REFERER", ""):
|
||||
return False
|
||||
token = request.session.get("counter_token")
|
||||
if not token:
|
||||
return False
|
||||
return Counter.objects.filter(token=token).exists()
|
||||
|
||||
|
||||
CanAccessLookup = IsOldSubscriber | IsRoot | IsLoggedInCounter
|
42
core/auth/backends.py
Normal file
42
core/auth/backends.py
Normal file
@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
from core.models import Group
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.models import User
|
||||
|
||||
|
||||
class SithModelBackend(ModelBackend):
|
||||
"""Custom auth backend for the Sith.
|
||||
|
||||
In fact, it's the exact same backend as `django.contrib.auth.backend.ModelBackend`,
|
||||
with the exception that group permissions are fetched slightly differently.
|
||||
Indeed, django tries by default to fetch the permissions associated
|
||||
with all the `django.contrib.auth.models.Group` of a user ;
|
||||
however, our User model overrides that, so the actual linked group model
|
||||
is [core.models.Group][].
|
||||
Instead of having the relation `auth_perm --> auth_group <-- core_user`,
|
||||
we have `auth_perm --> auth_group <-- core_group <-- core_user`.
|
||||
|
||||
Thus, this backend make the small tweaks necessary to make
|
||||
our custom models interact with the django auth.
|
||||
"""
|
||||
|
||||
def _get_group_permissions(self, user_obj: User):
|
||||
# union of querysets doesn't work if the queryset is ordered.
|
||||
# The empty `order_by` here are actually there to *remove*
|
||||
# any default ordering defined in managers or model Meta
|
||||
groups = user_obj.groups.order_by()
|
||||
if user_obj.is_subscribed:
|
||||
groups = groups.union(
|
||||
Group.objects.filter(pk=settings.SITH_GROUP_SUBSCRIBERS_ID).order_by()
|
||||
)
|
||||
return Permission.objects.filter(
|
||||
group__group__in=groups.values_list("pk", flat=True)
|
||||
)
|
212
core/auth/mixins.py
Normal file
212
core/auth/mixins.py
Normal file
@ -0,0 +1,212 @@
|
||||
#
|
||||
# Copyright 2016,2017
|
||||
# - Skia <skia@libskia.so>
|
||||
# - Sli <antoine@bartuccio.fr>
|
||||
#
|
||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||
# http://ae.utbm.fr.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License a published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
|
||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
#
|
||||
|
||||
import types
|
||||
from typing import Any
|
||||
|
||||
from django.contrib.auth.mixins import AccessMixin
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.views.generic.base import View
|
||||
|
||||
from core.models import User
|
||||
|
||||
|
||||
def can_edit_prop(obj: Any, user: User) -> bool:
|
||||
"""Can the user edit the properties of the object.
|
||||
|
||||
Args:
|
||||
obj: Object to test for permission
|
||||
user: core.models.User to test permissions against
|
||||
|
||||
Returns:
|
||||
True if user is authorized to edit object properties else False
|
||||
|
||||
Examples:
|
||||
```python
|
||||
if not can_edit_prop(self.object ,request.user):
|
||||
raise PermissionDenied
|
||||
```
|
||||
"""
|
||||
return obj is None or user.is_owner(obj)
|
||||
|
||||
|
||||
def can_edit(obj: Any, user: User) -> bool:
|
||||
"""Can the user edit the object.
|
||||
|
||||
Args:
|
||||
obj: Object to test for permission
|
||||
user: core.models.User to test permissions against
|
||||
|
||||
Returns:
|
||||
True if user is authorized to edit object else False
|
||||
|
||||
Examples:
|
||||
```python
|
||||
if not can_edit(self.object, request.user):
|
||||
raise PermissionDenied
|
||||
```
|
||||
"""
|
||||
if obj is None or user.can_edit(obj):
|
||||
return True
|
||||
return can_edit_prop(obj, user)
|
||||
|
||||
|
||||
def can_view(obj: Any, user: User) -> bool:
|
||||
"""Can the user see the object.
|
||||
|
||||
Args:
|
||||
obj: Object to test for permission
|
||||
user: core.models.User to test permissions against
|
||||
|
||||
Returns:
|
||||
True if user is authorized to see object else False
|
||||
|
||||
Examples:
|
||||
```python
|
||||
if not can_view(self.object ,request.user):
|
||||
raise PermissionDenied
|
||||
```
|
||||
"""
|
||||
if obj is None or user.can_view(obj):
|
||||
return True
|
||||
return can_edit(obj, user)
|
||||
|
||||
|
||||
class GenericContentPermissionMixinBuilder(View):
|
||||
"""Used to build permission mixins.
|
||||
|
||||
This view protect any child view that would be showing an object that is restricted based
|
||||
on two properties.
|
||||
|
||||
Attributes:
|
||||
raised_error: permission to be raised
|
||||
"""
|
||||
|
||||
raised_error = PermissionDenied
|
||||
|
||||
@staticmethod
|
||||
def permission_function(obj: Any, user: User) -> bool:
|
||||
"""Function to test permission with."""
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_permission_function(cls, obj, user):
|
||||
return cls.permission_function(obj, user)
|
||||
|
||||
def dispatch(self, request, *arg, **kwargs):
|
||||
if hasattr(self, "get_object") and callable(self.get_object):
|
||||
self.object = self.get_object()
|
||||
if not self.get_permission_function(self.object, request.user):
|
||||
raise self.raised_error
|
||||
return super().dispatch(request, *arg, **kwargs)
|
||||
|
||||
# If we get here, it's a ListView
|
||||
|
||||
queryset = self.get_queryset()
|
||||
l_id = [o.id for o in queryset if self.get_permission_function(o, request.user)]
|
||||
if not l_id and queryset.count() != 0:
|
||||
raise self.raised_error
|
||||
self._get_queryset = self.get_queryset
|
||||
|
||||
def get_qs(self2):
|
||||
return self2._get_queryset().filter(id__in=l_id)
|
||||
|
||||
self.get_queryset = types.MethodType(get_qs, self)
|
||||
return super().dispatch(request, *arg, **kwargs)
|
||||
|
||||
|
||||
class CanCreateMixin(View):
|
||||
"""Protect any child view that would create an object.
|
||||
|
||||
Raises:
|
||||
PermissionDenied:
|
||||
If the user has not the necessary permission
|
||||
to create the object of the view.
|
||||
"""
|
||||
|
||||
def dispatch(self, request, *arg, **kwargs):
|
||||
res = super().dispatch(request, *arg, **kwargs)
|
||||
if not request.user.is_authenticated:
|
||||
raise PermissionDenied
|
||||
return res
|
||||
|
||||
def form_valid(self, form):
|
||||
obj = form.instance
|
||||
if can_edit_prop(obj, self.request.user):
|
||||
return super().form_valid(form)
|
||||
raise PermissionDenied
|
||||
|
||||
|
||||
class CanEditPropMixin(GenericContentPermissionMixinBuilder):
|
||||
"""Ensure the user has owner permissions on the child view object.
|
||||
|
||||
In other word, you can make a view with this view as parent,
|
||||
and it will be retricted to the users that are in the
|
||||
object's owner_group or that pass the `obj.can_be_viewed_by` test.
|
||||
|
||||
Raises:
|
||||
PermissionDenied: If the user cannot see the object
|
||||
"""
|
||||
|
||||
permission_function = can_edit_prop
|
||||
|
||||
|
||||
class CanEditMixin(GenericContentPermissionMixinBuilder):
|
||||
"""Ensure the user has permission to edit this view's object.
|
||||
|
||||
Raises:
|
||||
PermissionDenied: if the user cannot edit this view's object.
|
||||
"""
|
||||
|
||||
permission_function = can_edit
|
||||
|
||||
|
||||
class CanViewMixin(GenericContentPermissionMixinBuilder):
|
||||
"""Ensure the user has permission to view this view's object.
|
||||
|
||||
Raises:
|
||||
PermissionDenied: if the user cannot edit this view's object.
|
||||
"""
|
||||
|
||||
permission_function = can_view
|
||||
|
||||
|
||||
class FormerSubscriberMixin(AccessMixin):
|
||||
"""Check if the user was at least an old subscriber.
|
||||
|
||||
Raises:
|
||||
PermissionDenied: if the user never subscribed.
|
||||
"""
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.user.was_subscribed:
|
||||
raise PermissionDenied
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class SubscriberMixin(AccessMixin):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.user.is_subscribed:
|
||||
return self.handle_no_permission()
|
||||
return super().dispatch(request, *args, **kwargs)
|
Reference in New Issue
Block a user