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
|
# 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,
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
# 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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
# 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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
# 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',
|
||||||
|
|
Loading…
Reference in New Issue