Handle permissions (and it seems working!)

This commit is contained in:
Yohann D'ANELLO 2020-03-18 14:42:35 +01:00
parent 112d4b6c5a
commit 057f42fdb6
19 changed files with 357 additions and 57 deletions

View File

@ -1,14 +1,15 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
from rest_framework.filters import SearchFilter from rest_framework.filters import SearchFilter
from api.viewsets import ReadProtectedModelViewSet
from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer
from ..models import ActivityType, Activity, Guest from ..models import ActivityType, Activity, Guest
class ActivityTypeViewSet(viewsets.ModelViewSet): class ActivityTypeViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer, 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', ] filterset_fields = ['name', 'can_invite', ]
class ActivityViewSet(viewsets.ModelViewSet): class ActivityViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer, 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', ] filterset_fields = ['name', 'description', 'activity_type', ]
class GuestViewSet(viewsets.ModelViewSet): class GuestViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer,

View File

@ -5,12 +5,16 @@ from django.conf.urls import url, include
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django_filters.rest_framework import DjangoFilterBackend 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.filters import SearchFilter
from rest_framework.viewsets import ReadOnlyModelViewSet
from activity.api.urls import register_activity_urls from activity.api.urls import register_activity_urls
from api.viewsets import ReadProtectedModelViewSet
from member.api.urls import register_members_urls from member.api.urls import register_members_urls
from note.api.urls import register_note_urls from note.api.urls import register_note_urls
from logs.api.urls import register_logs_urls from logs.api.urls import register_logs_urls
from permission.api.urls import register_permission_urls
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
@ -39,7 +43,7 @@ class ContentTypeSerializer(serializers.ModelSerializer):
fields = '__all__' fields = '__all__'
class UserViewSet(viewsets.ModelViewSet): class UserViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer, 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', ] 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. REST API View set.
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer, 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_members_urls(router, 'members')
register_activity_urls(router, 'activity') register_activity_urls(router, 'activity')
register_note_urls(router, 'note') register_note_urls(router, 'note')
register_permission_urls(router, 'permission')
register_logs_urls(router, 'logs') register_logs_urls(router, 'logs')
app_name = 'api' app_name = 'api'

26
apps/api/viewsets.py Normal file
View File

@ -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"))

View File

@ -2,14 +2,14 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
from rest_framework.filters import OrderingFilter from rest_framework.filters import OrderingFilter
from api.viewsets import ReadOnlyProtectedModelViewSet
from .serializers import ChangelogSerializer from .serializers import ChangelogSerializer
from ..models import Changelog from ..models import Changelog
class ChangelogViewSet(viewsets.ReadOnlyModelViewSet): class ChangelogViewSet(ReadOnlyProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,

View File

@ -1,14 +1,14 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import viewsets
from rest_framework.filters import SearchFilter from rest_framework.filters import SearchFilter
from api.viewsets import ReadProtectedModelViewSet
from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer
from ..models import Profile, Club, Role, Membership from ..models import Profile, Club, Role, Membership
class ProfileViewSet(viewsets.ModelViewSet): class ProfileViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `Profile` objects, serialize it to JSON with the given serializer, 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 serializer_class = ProfileSerializer
class ClubViewSet(viewsets.ModelViewSet): class ClubViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer, 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', ] search_fields = ['$name', ]
class RoleViewSet(viewsets.ModelViewSet): class RoleViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `Role` objects, serialize it to JSON with the given serializer, 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', ] search_fields = ['$name', ]
class MembershipViewSet(viewsets.ModelViewSet): class MembershipViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer,

View File

@ -1,7 +1,12 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # 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 from django.contrib.auth.backends import ModelBackend
@ -14,21 +19,61 @@ class PermissionBackend(ModelBackend):
for membership in Membership.objects.filter(user=user).all(): for membership in Membership.objects.filter(user=user).all():
if not membership.valid() or membership.roles is None: if not membership.valid() or membership.roles is None:
continue continue
for role_permissions in RolePermissions.objects.filter(role=membership.roles).all(): for role_permissions in RolePermissions.objects.filter(role=membership.roles).all():
for permission in role_permissions.permissions.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 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): def has_perm(self, user_obj, perm, obj=None):
if user_obj.is_superuser: if user_obj.is_superuser:
return True return True
if obj is None: if obj is None:
return False return True
perm = perm.split('_', 3)
perm_type = perm[1] perm = perm.split('.')[-1].split('_', 2)
perm_type = perm[0]
perm_field = perm[2] if len(perm) == 3 else None 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): def has_module_perms(self, user_obj, app_label):
return False return False

View File

@ -203,7 +203,6 @@ class DeleteAliasView(LoginRequiredMixin, DeleteView):
return HttpResponseRedirect(self.get_success_url()) return HttpResponseRedirect(self.get_success_url())
def get_success_url(self): def get_success_url(self):
print(self.request)
return reverse_lazy('member:user_alias', kwargs={'pk': self.object.note.user.pk}) return reverse_lazy('member:user_alias', kwargs={'pk': self.object.note.user.pk})
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):

View File

@ -88,6 +88,9 @@ class NotePolymorphicSerializer(PolymorphicSerializer):
NoteSpecial: NoteSpecialSerializer NoteSpecial: NoteSpecialSerializer
} }
class Meta:
model = Note
class TemplateCategorySerializer(serializers.ModelSerializer): class TemplateCategorySerializer(serializers.ModelSerializer):
""" """
@ -162,3 +165,6 @@ class TransactionPolymorphicSerializer(PolymorphicSerializer):
MembershipTransaction: MembershipTransactionSerializer, MembershipTransaction: MembershipTransactionSerializer,
SpecialTransaction: SpecialTransactionSerializer, SpecialTransaction: SpecialTransactionSerializer,
} }
class Meta:
model = Transaction

View File

@ -3,9 +3,9 @@
from django.db.models import Q from django.db.models import Q
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.filters import OrderingFilter, SearchFilter
from api.viewsets import ReadProtectedModelViewSet
from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \ from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \
NoteUserSerializer, AliasSerializer, \ NoteUserSerializer, AliasSerializer, \
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
@ -13,7 +13,7 @@ from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
class NoteViewSet(viewsets.ModelViewSet): class NoteViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `Note` objects, serialize it to JSON with the given serializer, 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 serializer_class = NoteSerializer
class NoteClubViewSet(viewsets.ModelViewSet): class NoteClubViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `NoteClub` objects, serialize it to JSON with the given serializer, 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 serializer_class = NoteClubSerializer
class NoteSpecialViewSet(viewsets.ModelViewSet): class NoteSpecialViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `NoteSpecial` objects, serialize it to JSON with the given serializer, 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 serializer_class = NoteSpecialSerializer
class NoteUserViewSet(viewsets.ModelViewSet): class NoteUserViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `NoteUser` objects, serialize it to JSON with the given serializer, 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 serializer_class = NoteUserSerializer
class NotePolymorphicViewSet(viewsets.ModelViewSet): class NotePolymorphicViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer, 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. Parse query and apply filters.
:return: The filtered set of requested notes :return: The filtered set of requested notes
""" """
queryset = Note.objects.all() queryset = super().get_queryset()
alias = self.request.query_params.get("alias", ".*") alias = self.request.query_params.get("alias", ".*")
queryset = queryset.filter( queryset = queryset.filter(
@ -92,7 +92,7 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet):
return queryset.distinct() return queryset.distinct()
class AliasViewSet(viewsets.ModelViewSet): class AliasViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer, 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 :return: The filtered set of requested aliases
""" """
queryset = Alias.objects.all() queryset = super().get_queryset()
alias = self.request.query_params.get("alias", ".*") alias = self.request.query_params.get("alias", ".*")
queryset = queryset.filter( queryset = queryset.filter(
@ -138,7 +138,7 @@ class AliasViewSet(viewsets.ModelViewSet):
return queryset return queryset
class TemplateCategoryViewSet(viewsets.ModelViewSet): class TemplateCategoryViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `TemplateCategory` objects, serialize it to JSON with the given serializer, 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', ] search_fields = ['$name', ]
class TransactionTemplateViewSet(viewsets.ModelViewSet): class TransactionTemplateViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer, 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', ] filterset_fields = ['name', 'amount', 'display', 'category', ]
class TransactionViewSet(viewsets.ModelViewSet): class TransactionViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer,

View File

@ -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'

View File

View File

@ -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__'

View File

@ -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)

View File

@ -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', ]

View File

@ -2,7 +2,14 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig from django.apps import AppConfig
from django.db.models.signals import pre_save, pre_delete
class PermissionConfig(AppConfig): class PermissionConfig(AppConfig):
name = 'permission' name = 'permission'
def ready(self):
# noinspection PyUnresolvedReferences
from . import signals
pre_save.connect(signals.pre_save_object)
pre_delete.connect(signals.pre_delete_object)

View File

@ -27,12 +27,13 @@ class InstancedPermission:
""" """
if self.type == 'add': if self.type == 'add':
if permission_type == self.type: 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: if ContentType.objects.get_for_model(obj) != self.model:
# The permission does not apply to the model # The permission does not apply to the model
return False return False
if permission_type == self.type: if permission_type == self.type:
if field_name and field_name != self.field: if self.field and field_name != self.field:
return False return False
return obj in self.model.model_class().objects.filter(self.query).all() return obj in self.model.model_class().objects.filter(self.query).all()
else: else:
@ -91,6 +92,7 @@ class Permission(models.Model):
unique_together = ('model', 'query', 'type', 'field') unique_together = ('model', 'query', 'type', 'field')
def clean(self): def clean(self):
self.query = json.dumps(json.loads(self.query))
if self.field and self.type not in {'view', 'change'}: if self.field and self.type not in {'view', 'change'}:
raise ValidationError(_("Specifying field applies only to view and change permission types.")) raise ValidationError(_("Specifying field applies only to view and change permission types."))
@ -101,9 +103,6 @@ class Permission(models.Model):
@staticmethod @staticmethod
def compute_f(oper, **kwargs): def compute_f(oper, **kwargs):
if isinstance(oper, list): if isinstance(oper, list):
if len(oper) == 1:
return kwargs[oper[0]].pk
elif len(oper) >= 2:
if oper[0] == 'ADD': if oper[0] == 'ADD':
return functools.reduce(operator.add, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]]) return functools.reduce(operator.add, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]])
elif oper[0] == 'SUB': elif oper[0] == 'SUB':
@ -112,10 +111,37 @@ class Permission(models.Model):
return functools.reduce(operator.mul, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]]) return functools.reduce(operator.mul, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]])
elif oper[0] == 'F': elif oper[0] == 'F':
return F(oper[1]) return F(oper[1])
else:
field = kwargs[oper[0]]
for i in range(1, len(oper)):
field = getattr(field, oper[i])
return field
else: else:
return oper 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): def _about(self, query, **kwargs):
if self.type == 'add': if self.type == 'add':
@ -124,8 +150,8 @@ class Permission(models.Model):
if len(query) == 0: if len(query) == 0:
# The query is either [] or {} and # The query is either [] or {} and
# applies to all objects of the model # applies to all objects of the model
# to represent this we return None # to represent this we return a trivial request
return None return Q(pk=F("pk"))
if isinstance(query, list): if isinstance(query, list):
if query[0] == 'AND': if query[0] == 'AND':
return functools.reduce(operator.and_, [self._about(query, **kwargs) for query in query[1:]]) 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: for key in query:
value = query[key] value = query[key]
if isinstance(value, list): if isinstance(value, list):
# It is a parameter we query its primary key # It is a parameter we query its return value
q_kwargs[key] = kwargs[value[0]].pk q_kwargs[key] = Permission.compute_param(value, **kwargs)
elif isinstance(value, dict): elif isinstance(value, dict):
# It is an F object # It is an F object
q_kwargs[key] = Permission.compute_f(query['F'], **kwargs) q_kwargs[key] = Permission.compute_f(value['F'], **kwargs)
else: else:
q_kwargs[key] = value q_kwargs[key] = value
return Q(**q_kwargs) return Q(**q_kwargs)
@ -167,7 +193,7 @@ class Permission(models.Model):
value = query[key] value = query[key]
if isinstance(value, list): if isinstance(value, list):
# It is a parameter we query its primary key # 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): elif isinstance(value, dict):
# It is an F object # It is an F object
q_kwargs[key] = Permission.compute_f(query['F'], **kwargs) q_kwargs[key] = Permission.compute_f(query['F'], **kwargs)
@ -176,7 +202,7 @@ class Permission(models.Model):
def func(obj): def func(obj):
nonlocal q_kwargs nonlocal q_kwargs
for arg in q_kwargs: for arg in q_kwargs:
if getattr(obj, arg) != q_kwargs(arg): if getattr(obj, arg) != q_kwargs[arg]:
return False return False
return True return True
return func return func

View File

@ -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

View File

@ -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

View File

@ -139,8 +139,7 @@ REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions, # Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users. # or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [ 'DEFAULT_PERMISSION_CLASSES': [
# TODO Maybe replace it with our custom permissions system 'permission.permissions.StrongDjangoObjectPermissions',
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
], ],
'DEFAULT_AUTHENTICATION_CLASSES': [ 'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.SessionAuthentication',