mirror of https://gitlab.crans.org/bde/nk20
Handle permissions (and it seems working!)
This commit is contained in:
parent
112d4b6c5a
commit
057f42fdb6
|
@ -1,14 +1,15 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.filters import SearchFilter
|
||||
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer
|
||||
from ..models import ActivityType, Activity, Guest
|
||||
|
||||
|
||||
class ActivityTypeViewSet(viewsets.ModelViewSet):
|
||||
class ActivityTypeViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -20,7 +21,7 @@ class ActivityTypeViewSet(viewsets.ModelViewSet):
|
|||
filterset_fields = ['name', 'can_invite', ]
|
||||
|
||||
|
||||
class ActivityViewSet(viewsets.ModelViewSet):
|
||||
class ActivityViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -32,7 +33,7 @@ class ActivityViewSet(viewsets.ModelViewSet):
|
|||
filterset_fields = ['name', 'description', 'activity_type', ]
|
||||
|
||||
|
||||
class GuestViewSet(viewsets.ModelViewSet):
|
||||
class GuestViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer,
|
||||
|
|
|
@ -5,12 +5,16 @@ from django.conf.urls import url, include
|
|||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import routers, serializers, viewsets
|
||||
from rest_framework import routers, serializers
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
|
||||
from activity.api.urls import register_activity_urls
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
from member.api.urls import register_members_urls
|
||||
from note.api.urls import register_note_urls
|
||||
from logs.api.urls import register_logs_urls
|
||||
from permission.api.urls import register_permission_urls
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
|
@ -39,7 +43,7 @@ class ContentTypeSerializer(serializers.ModelSerializer):
|
|||
fields = '__all__'
|
||||
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
class UserViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -52,7 +56,8 @@ class UserViewSet(viewsets.ModelViewSet):
|
|||
search_fields = ['$username', '$first_name', '$last_name', ]
|
||||
|
||||
|
||||
class ContentTypeViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
# This ViewSet is the only one that is accessible from all authenticated users!
|
||||
class ContentTypeViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -70,6 +75,7 @@ router.register('user', UserViewSet)
|
|||
register_members_urls(router, 'members')
|
||||
register_activity_urls(router, 'activity')
|
||||
register_note_urls(router, 'note')
|
||||
register_permission_urls(router, 'permission')
|
||||
register_logs_urls(router, 'logs')
|
||||
|
||||
app_name = 'api'
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from member.backends import PermissionBackend
|
||||
from rest_framework import viewsets
|
||||
|
||||
|
||||
class ReadProtectedModelViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
Protect a ModelViewSet by filtering the objects that the user cannot see.
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
model = ContentType.objects.get_for_model(self.serializer_class.Meta.model)
|
||||
return super().get_queryset().filter(PermissionBackend().filter_queryset(self.request.user, model, "view"))
|
||||
|
||||
|
||||
class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
Protect a ReadOnlyModelViewSet by filtering the objects that the user cannot see.
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
model = ContentType.objects.get_for_model(self.serializer_class.Meta.model)
|
||||
return super().get_queryset().filter(PermissionBackend().filter_queryset(self.request.user, model, "view"))
|
|
@ -2,14 +2,14 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.filters import OrderingFilter
|
||||
|
||||
from api.viewsets import ReadOnlyProtectedModelViewSet
|
||||
from .serializers import ChangelogSerializer
|
||||
from ..models import Changelog
|
||||
|
||||
|
||||
class ChangelogViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class ChangelogViewSet(ReadOnlyProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.filters import SearchFilter
|
||||
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer
|
||||
from ..models import Profile, Club, Role, Membership
|
||||
|
||||
|
||||
class ProfileViewSet(viewsets.ModelViewSet):
|
||||
class ProfileViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Profile` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -18,7 +18,7 @@ class ProfileViewSet(viewsets.ModelViewSet):
|
|||
serializer_class = ProfileSerializer
|
||||
|
||||
|
||||
class ClubViewSet(viewsets.ModelViewSet):
|
||||
class ClubViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -30,7 +30,7 @@ class ClubViewSet(viewsets.ModelViewSet):
|
|||
search_fields = ['$name', ]
|
||||
|
||||
|
||||
class RoleViewSet(viewsets.ModelViewSet):
|
||||
class RoleViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Role` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -42,7 +42,7 @@ class RoleViewSet(viewsets.ModelViewSet):
|
|||
search_fields = ['$name', ]
|
||||
|
||||
|
||||
class MembershipViewSet(viewsets.ModelViewSet):
|
||||
class MembershipViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer,
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from member.models import Club, Membership, RolePermissions
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import Q, F
|
||||
|
||||
from note.models import Note, NoteUser, NoteClub, NoteSpecial
|
||||
from .models import Membership, RolePermissions, Club
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
|
||||
|
@ -14,21 +19,61 @@ class PermissionBackend(ModelBackend):
|
|||
for membership in Membership.objects.filter(user=user).all():
|
||||
if not membership.valid() or membership.roles is None:
|
||||
continue
|
||||
|
||||
for role_permissions in RolePermissions.objects.filter(role=membership.roles).all():
|
||||
for permission in role_permissions.permissions.all():
|
||||
permission = permission.about(user=user, club=membership.club)
|
||||
permission = permission.about(
|
||||
user=user,
|
||||
club=membership.club,
|
||||
User=User,
|
||||
Club=Club,
|
||||
Membership=Membership,
|
||||
Note=Note,
|
||||
NoteUser=NoteUser,
|
||||
NoteClub=NoteClub,
|
||||
NoteSpecial=NoteSpecial,
|
||||
F=F,
|
||||
Q=Q
|
||||
)
|
||||
yield permission
|
||||
|
||||
def filter_queryset(self, user, model, type, field=None):
|
||||
"""
|
||||
Filter a queryset by considering the permissions of a given user.
|
||||
:param user: The owner of the permissions that are fetched
|
||||
:param model: The concerned model of the queryset
|
||||
:param type: The type of modification (view, add, change, delete)
|
||||
:param field: The field of the model to test, if concerned
|
||||
:return: A query that corresponds to the filter to give to a queryset
|
||||
"""
|
||||
|
||||
if user.is_superuser:
|
||||
# Superusers have all rights
|
||||
return Q()
|
||||
|
||||
# Never satisfied
|
||||
query = Q(pk=-1)
|
||||
for perm in self.permissions(user):
|
||||
if field and field != perm.field:
|
||||
continue
|
||||
if perm.model != model or perm.type != type:
|
||||
continue
|
||||
query = query | perm.query
|
||||
return query
|
||||
|
||||
def has_perm(self, user_obj, perm, obj=None):
|
||||
if user_obj.is_superuser:
|
||||
return True
|
||||
|
||||
if obj is None:
|
||||
return False
|
||||
perm = perm.split('_', 3)
|
||||
perm_type = perm[1]
|
||||
return True
|
||||
|
||||
perm = perm.split('.')[-1].split('_', 2)
|
||||
perm_type = perm[0]
|
||||
perm_field = perm[2] if len(perm) == 3 else None
|
||||
return any(permission.applies(obj, perm_type, perm_field) for permission in self.permissions(user_obj))
|
||||
if any(permission.applies(obj, perm_type, perm_field) for permission in self.permissions(user_obj)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_module_perms(self, user_obj, app_label):
|
||||
return False
|
||||
|
|
|
@ -203,7 +203,6 @@ class DeleteAliasView(LoginRequiredMixin, DeleteView):
|
|||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
print(self.request)
|
||||
return reverse_lazy('member:user_alias', kwargs={'pk': self.object.note.user.pk})
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
|
|
@ -88,6 +88,9 @@ class NotePolymorphicSerializer(PolymorphicSerializer):
|
|||
NoteSpecial: NoteSpecialSerializer
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = Note
|
||||
|
||||
|
||||
class TemplateCategorySerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
|
@ -162,3 +165,6 @@ class TransactionPolymorphicSerializer(PolymorphicSerializer):
|
|||
MembershipTransaction: MembershipTransactionSerializer,
|
||||
SpecialTransaction: SpecialTransactionSerializer,
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = Transaction
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
|
||||
from django.db.models import Q
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \
|
||||
NoteUserSerializer, AliasSerializer, \
|
||||
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
|
||||
|
@ -13,7 +13,7 @@ from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
|
|||
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
|
||||
|
||||
|
||||
class NoteViewSet(viewsets.ModelViewSet):
|
||||
class NoteViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Note` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -23,7 +23,7 @@ class NoteViewSet(viewsets.ModelViewSet):
|
|||
serializer_class = NoteSerializer
|
||||
|
||||
|
||||
class NoteClubViewSet(viewsets.ModelViewSet):
|
||||
class NoteClubViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `NoteClub` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -33,7 +33,7 @@ class NoteClubViewSet(viewsets.ModelViewSet):
|
|||
serializer_class = NoteClubSerializer
|
||||
|
||||
|
||||
class NoteSpecialViewSet(viewsets.ModelViewSet):
|
||||
class NoteSpecialViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `NoteSpecial` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -43,7 +43,7 @@ class NoteSpecialViewSet(viewsets.ModelViewSet):
|
|||
serializer_class = NoteSpecialSerializer
|
||||
|
||||
|
||||
class NoteUserViewSet(viewsets.ModelViewSet):
|
||||
class NoteUserViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `NoteUser` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -53,7 +53,7 @@ class NoteUserViewSet(viewsets.ModelViewSet):
|
|||
serializer_class = NoteUserSerializer
|
||||
|
||||
|
||||
class NotePolymorphicViewSet(viewsets.ModelViewSet):
|
||||
class NotePolymorphicViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer,
|
||||
|
@ -70,7 +70,7 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet):
|
|||
Parse query and apply filters.
|
||||
:return: The filtered set of requested notes
|
||||
"""
|
||||
queryset = Note.objects.all()
|
||||
queryset = super().get_queryset()
|
||||
|
||||
alias = self.request.query_params.get("alias", ".*")
|
||||
queryset = queryset.filter(
|
||||
|
@ -92,7 +92,7 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet):
|
|||
return queryset.distinct()
|
||||
|
||||
|
||||
class AliasViewSet(viewsets.ModelViewSet):
|
||||
class AliasViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -110,7 +110,7 @@ class AliasViewSet(viewsets.ModelViewSet):
|
|||
:return: The filtered set of requested aliases
|
||||
"""
|
||||
|
||||
queryset = Alias.objects.all()
|
||||
queryset = super().get_queryset()
|
||||
|
||||
alias = self.request.query_params.get("alias", ".*")
|
||||
queryset = queryset.filter(
|
||||
|
@ -138,7 +138,7 @@ class AliasViewSet(viewsets.ModelViewSet):
|
|||
return queryset
|
||||
|
||||
|
||||
class TemplateCategoryViewSet(viewsets.ModelViewSet):
|
||||
class TemplateCategoryViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `TemplateCategory` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -150,7 +150,7 @@ class TemplateCategoryViewSet(viewsets.ModelViewSet):
|
|||
search_fields = ['$name', ]
|
||||
|
||||
|
||||
class TransactionTemplateViewSet(viewsets.ModelViewSet):
|
||||
class TransactionTemplateViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -162,7 +162,7 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet):
|
|||
filterset_fields = ['name', 'amount', 'display', 'category', ]
|
||||
|
||||
|
||||
class TransactionViewSet(viewsets.ModelViewSet):
|
||||
class TransactionViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer,
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'permission.apps.PermissionConfig'
|
|
@ -0,0 +1,17 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..models import Permission
|
||||
|
||||
|
||||
class PermissionSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Permission types.
|
||||
The djangorestframework plugin will analyse the model `Permission` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Permission
|
||||
fields = '__all__'
|
|
@ -0,0 +1,11 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import PermissionViewSet
|
||||
|
||||
|
||||
def register_permission_urls(router, path):
|
||||
"""
|
||||
Configure router for permission REST API.
|
||||
"""
|
||||
router.register(path, PermissionViewSet)
|
|
@ -0,0 +1,20 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
|
||||
from api.viewsets import ReadOnlyProtectedModelViewSet
|
||||
from .serializers import PermissionSerializer
|
||||
from ..models import Permission
|
||||
|
||||
|
||||
class PermissionViewSet(ReadOnlyProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/logs/
|
||||
"""
|
||||
queryset = Permission.objects.all()
|
||||
serializer_class = PermissionSerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ['model', 'type', ]
|
|
@ -2,7 +2,14 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db.models.signals import pre_save, pre_delete
|
||||
|
||||
|
||||
class PermissionConfig(AppConfig):
|
||||
name = 'permission'
|
||||
|
||||
def ready(self):
|
||||
# noinspection PyUnresolvedReferences
|
||||
from . import signals
|
||||
pre_save.connect(signals.pre_save_object)
|
||||
pre_delete.connect(signals.pre_delete_object)
|
||||
|
|
|
@ -27,12 +27,13 @@ class InstancedPermission:
|
|||
"""
|
||||
if self.type == 'add':
|
||||
if permission_type == self.type:
|
||||
return obj in self.model.modelclass().objects.get(self.query)
|
||||
return self.query(obj)
|
||||
|
||||
if ContentType.objects.get_for_model(obj) != self.model:
|
||||
# The permission does not apply to the model
|
||||
return False
|
||||
if permission_type == self.type:
|
||||
if field_name and field_name != self.field:
|
||||
if self.field and field_name != self.field:
|
||||
return False
|
||||
return obj in self.model.model_class().objects.filter(self.query).all()
|
||||
else:
|
||||
|
@ -91,6 +92,7 @@ class Permission(models.Model):
|
|||
unique_together = ('model', 'query', 'type', 'field')
|
||||
|
||||
def clean(self):
|
||||
self.query = json.dumps(json.loads(self.query))
|
||||
if self.field and self.type not in {'view', 'change'}:
|
||||
raise ValidationError(_("Specifying field applies only to view and change permission types."))
|
||||
|
||||
|
@ -101,21 +103,45 @@ class Permission(models.Model):
|
|||
@staticmethod
|
||||
def compute_f(oper, **kwargs):
|
||||
if isinstance(oper, list):
|
||||
if len(oper) == 1:
|
||||
return kwargs[oper[0]].pk
|
||||
elif len(oper) >= 2:
|
||||
if oper[0] == 'ADD':
|
||||
return functools.reduce(operator.add, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]])
|
||||
elif oper[0] == 'SUB':
|
||||
return Permission.compute_f(oper[1], **kwargs) - Permission.compute_f(oper[2], **kwargs)
|
||||
elif oper[0] == 'MUL':
|
||||
return functools.reduce(operator.mul, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]])
|
||||
elif oper[0] == 'F':
|
||||
return F(oper[1])
|
||||
if oper[0] == 'ADD':
|
||||
return functools.reduce(operator.add, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]])
|
||||
elif oper[0] == 'SUB':
|
||||
return Permission.compute_f(oper[1], **kwargs) - Permission.compute_f(oper[2], **kwargs)
|
||||
elif oper[0] == 'MUL':
|
||||
return functools.reduce(operator.mul, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]])
|
||||
elif oper[0] == 'F':
|
||||
return F(oper[1])
|
||||
else:
|
||||
field = kwargs[oper[0]]
|
||||
for i in range(1, len(oper)):
|
||||
field = getattr(field, oper[i])
|
||||
return field
|
||||
else:
|
||||
return oper
|
||||
# TODO: find a better way to crash here
|
||||
raise Exception("F is wrong")
|
||||
|
||||
@staticmethod
|
||||
def compute_param(value, **kwargs):
|
||||
if not isinstance(value, list):
|
||||
return value
|
||||
|
||||
field = kwargs[value[0]]
|
||||
for i in range(1, len(value)):
|
||||
if isinstance(value[i], list):
|
||||
field = getattr(field, value[i][0])
|
||||
params = []
|
||||
call_kwargs = {}
|
||||
for j in range(1, len(value[i])):
|
||||
param = Permission.compute_param(value[i][j], **kwargs)
|
||||
if isinstance(param, dict):
|
||||
for key in param:
|
||||
val = Permission.compute_param(param[key], **kwargs)
|
||||
call_kwargs[key] = val
|
||||
else:
|
||||
params.append(param)
|
||||
field = field(*params, **call_kwargs)
|
||||
else:
|
||||
field = getattr(field, value[i])
|
||||
return field
|
||||
|
||||
def _about(self, query, **kwargs):
|
||||
if self.type == 'add':
|
||||
|
@ -124,8 +150,8 @@ class Permission(models.Model):
|
|||
if len(query) == 0:
|
||||
# The query is either [] or {} and
|
||||
# applies to all objects of the model
|
||||
# to represent this we return None
|
||||
return None
|
||||
# to represent this we return a trivial request
|
||||
return Q(pk=F("pk"))
|
||||
if isinstance(query, list):
|
||||
if query[0] == 'AND':
|
||||
return functools.reduce(operator.and_, [self._about(query, **kwargs) for query in query[1:]])
|
||||
|
@ -138,11 +164,11 @@ class Permission(models.Model):
|
|||
for key in query:
|
||||
value = query[key]
|
||||
if isinstance(value, list):
|
||||
# It is a parameter we query its primary key
|
||||
q_kwargs[key] = kwargs[value[0]].pk
|
||||
# It is a parameter we query its return value
|
||||
q_kwargs[key] = Permission.compute_param(value, **kwargs)
|
||||
elif isinstance(value, dict):
|
||||
# It is an F object
|
||||
q_kwargs[key] = Permission.compute_f(query['F'], **kwargs)
|
||||
q_kwargs[key] = Permission.compute_f(value['F'], **kwargs)
|
||||
else:
|
||||
q_kwargs[key] = value
|
||||
return Q(**q_kwargs)
|
||||
|
@ -167,7 +193,7 @@ class Permission(models.Model):
|
|||
value = query[key]
|
||||
if isinstance(value, list):
|
||||
# It is a parameter we query its primary key
|
||||
q_kwargs[key] = kwargs[value[0]].pk
|
||||
q_kwargs[key] = Permission.compute_param(value, **kwargs)
|
||||
elif isinstance(value, dict):
|
||||
# It is an F object
|
||||
q_kwargs[key] = Permission.compute_f(query['F'], **kwargs)
|
||||
|
@ -176,7 +202,7 @@ class Permission(models.Model):
|
|||
def func(obj):
|
||||
nonlocal q_kwargs
|
||||
for arg in q_kwargs:
|
||||
if getattr(obj, arg) != q_kwargs(arg):
|
||||
if getattr(obj, arg) != q_kwargs[arg]:
|
||||
return False
|
||||
return True
|
||||
return func
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework.permissions import DjangoObjectPermissions
|
||||
|
||||
SAFE_METHODS = ('HEAD', 'OPTIONS', )
|
||||
|
||||
|
||||
class StrongDjangoObjectPermissions(DjangoObjectPermissions):
|
||||
perms_map = {
|
||||
'GET': ['%(app_label)s.view_%(model_name)s'],
|
||||
'OPTIONS': [],
|
||||
'HEAD': [],
|
||||
'POST': ['%(app_label)s.add_%(model_name)s'],
|
||||
'PUT': ['%(app_label)s.change_%(model_name)s'],
|
||||
'PATCH': ['%(app_label)s.change_%(model_name)s'],
|
||||
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
|
||||
}
|
||||
|
||||
def get_required_object_permissions(self, method, model_cls):
|
||||
kwargs = {
|
||||
'app_label': model_cls._meta.app_label,
|
||||
'model_name': model_cls._meta.model_name
|
||||
}
|
||||
|
||||
if method not in self.perms_map:
|
||||
from rest_framework import exceptions
|
||||
raise exceptions.MethodNotAllowed(method)
|
||||
|
||||
return [perm % kwargs for perm in self.perms_map[method]]
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
# authentication checks have already executed via has_permission
|
||||
queryset = self._queryset(view)
|
||||
model_cls = queryset.model
|
||||
user = request.user
|
||||
|
||||
perms = self.get_required_object_permissions(request.method, model_cls)
|
||||
|
||||
if not user.has_perms(perms, obj):
|
||||
# If the user does not have permissions we need to determine if
|
||||
# they have read permissions to see 403, or not, and simply see
|
||||
# a 404 response.
|
||||
from django.http import Http404
|
||||
|
||||
if request.method in SAFE_METHODS:
|
||||
# Read permissions already checked and failed, no need
|
||||
# to make another lookup.
|
||||
raise Http404
|
||||
|
||||
read_perms = self.get_required_object_permissions('GET', model_cls)
|
||||
if not user.has_perms(read_perms, obj):
|
||||
raise Http404
|
||||
|
||||
# Has read permissions.
|
||||
return False
|
||||
|
||||
return True
|
|
@ -0,0 +1,75 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from logs.middlewares import get_current_authenticated_user
|
||||
|
||||
|
||||
EXCLUDED = [
|
||||
'cas_server.proxygrantingticket',
|
||||
'cas_server.proxyticket',
|
||||
'cas_server.serviceticket',
|
||||
'cas_server.user',
|
||||
'cas_server.userattributes',
|
||||
'contenttypes.contenttype',
|
||||
'logs.changelog',
|
||||
'migrations.migration',
|
||||
'sessions.session',
|
||||
]
|
||||
|
||||
|
||||
def pre_save_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Before a model get saved, we check the permissions
|
||||
"""
|
||||
# noinspection PyProtectedMember
|
||||
if instance._meta.label_lower in EXCLUDED:
|
||||
return
|
||||
|
||||
user = get_current_authenticated_user()
|
||||
if user is None:
|
||||
# Action performed on shell is always granted
|
||||
return
|
||||
|
||||
qs = sender.objects.filter(pk=instance.pk).all()
|
||||
model_name_full = instance._meta.label_lower.split(".")
|
||||
app_label = model_name_full[0]
|
||||
model_name = model_name_full[1]
|
||||
|
||||
if qs.exists():
|
||||
if user.has_perm(app_label + ".change_" + model_name, instance):
|
||||
return
|
||||
|
||||
previous = qs.get()
|
||||
for field in instance._meta.fields:
|
||||
field_name = field.name
|
||||
old_value = getattr(previous, field.name)
|
||||
new_value = getattr(instance, field.name)
|
||||
if old_value == new_value:
|
||||
continue
|
||||
if not user.has_perm(app_label + ".change_" + model_name + "_" + field_name, instance):
|
||||
raise PermissionDenied
|
||||
else:
|
||||
if not user.has_perm(app_label + ".add_" + model_name, instance):
|
||||
raise PermissionDenied
|
||||
|
||||
|
||||
def pre_delete_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Before a model get deleted, we check the permissions
|
||||
"""
|
||||
# noinspection PyProtectedMember
|
||||
if instance._meta.label_lower in EXCLUDED:
|
||||
return
|
||||
|
||||
user = get_current_authenticated_user()
|
||||
if user is None:
|
||||
# Action performed on shell is always granted
|
||||
return
|
||||
|
||||
model_name_full = instance._meta.label_lower.split(".")
|
||||
app_label = model_name_full[0]
|
||||
model_name = model_name_full[1]
|
||||
|
||||
if not user.has_perm(app_label + ".delete_" + model_name, instance):
|
||||
raise PermissionDenied
|
|
@ -139,8 +139,7 @@ REST_FRAMEWORK = {
|
|||
# Use Django's standard `django.contrib.auth` permissions,
|
||||
# or allow read-only access for unauthenticated users.
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
# TODO Maybe replace it with our custom permissions system
|
||||
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
|
||||
'permission.permissions.StrongDjangoObjectPermissions',
|
||||
],
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
|
|
Loading…
Reference in New Issue