Permissions support fully OAuth2 scopes

Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
This commit is contained in:
Yohann D'ANELLO 2021-06-15 15:50:36 +02:00
parent ea092803d7
commit 8be16e7b58
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
5 changed files with 56 additions and 24 deletions

View File

@ -24,7 +24,6 @@ class ReadProtectedModelViewSet(ModelViewSet):
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class() self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
def get_queryset(self): def get_queryset(self):
self.request.session.setdefault("permission_mask", 42)
return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct() return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct()
@ -38,7 +37,6 @@ class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class() self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
def get_queryset(self): def get_queryset(self):
self.request.session.setdefault("permission_mask", 42)
return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct() return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct()

View File

@ -83,7 +83,7 @@ def save_object(sender, instance, **kwargs):
ip = request.META.get('REMOTE_ADDR') ip = request.META.get('REMOTE_ADDR')
if not user.is_authenticated: if not user.is_authenticated:
# For registration purposes # For registration and OAuth2 purposes
user = None user = None
# noinspection PyProtectedMember # noinspection PyProtectedMember
@ -160,6 +160,10 @@ def delete_object(sender, instance, **kwargs):
else: else:
ip = request.META.get('REMOTE_ADDR') ip = request.META.get('REMOTE_ADDR')
if not user.is_authenticated:
# For registration and OAuth2 purposes
user = None
# On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles # On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles
class CustomSerializer(ModelSerializer): class CustomSerializer(ModelSerializer):
class Meta: class Meta:

View File

@ -39,7 +39,6 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
Parse query and apply filters. Parse query and apply filters.
:return: The filtered set of requested notes :return: The filtered set of requested notes
""" """
self.request.session.setdefault("permission_mask", 42)
queryset = self.queryset.filter(PermissionBackend.filter_queryset(self.request, Note, "view") queryset = self.queryset.filter(PermissionBackend.filter_queryset(self.request, Note, "view")
| PermissionBackend.filter_queryset(self.request, NoteUser, "view") | PermissionBackend.filter_queryset(self.request, NoteUser, "view")
| PermissionBackend.filter_queryset(self.request, NoteClub, "view") | PermissionBackend.filter_queryset(self.request, NoteClub, "view")
@ -204,6 +203,5 @@ class TransactionViewSet(ReadProtectedModelViewSet):
ordering_fields = ['created_at', 'amount', ] ordering_fields = ['created_at', 'amount', ]
def get_queryset(self): def get_queryset(self):
self.request.session.setdefault("permission_mask", 42)
return self.model.objects.filter(PermissionBackend.filter_queryset(self.request, self.model, "view"))\ return self.model.objects.filter(PermissionBackend.filter_queryset(self.request, self.model, "view"))\
.order_by("created_at", "id") .order_by("created_at", "id")

View File

@ -26,14 +26,31 @@ class PermissionBackend(ModelBackend):
@staticmethod @staticmethod
@memoize @memoize
def get_raw_permissions(user, t): def get_raw_permissions(request, t):
""" """
Query permissions of a certain type for a user, then memoize it. Query permissions of a certain type for a user, then memoize it.
:param user: The owner of the permissions :param request: The current request
:param t: The type of the permissions: view, change, add or delete :param t: The type of the permissions: view, change, add or delete
:return: The queryset of the permissions of the user (memoized) grouped by clubs :return: The queryset of the permissions of the user (memoized) grouped by clubs
""" """
if not user.is_authenticated: if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
# OAuth2 Authentication
user = request.auth.user
def permission_filter(membership_obj):
query = Q(pk=-1)
for scope in request.auth.scope.split(' '):
permission_id, club_id = scope.split('_')
if int(club_id) == membership_obj.club_id:
query |= Q(pk=permission_id)
return query
else:
user = request.user
def permission_filter(membership_obj):
return Q(mask__rank__lte=request.session.get("permission_mask", -1))
if user.is_anonymous:
# Unauthenticated users have no permissions # Unauthenticated users have no permissions
return Permission.objects.none() return Permission.objects.none()
@ -43,8 +60,7 @@ class PermissionBackend(ModelBackend):
for membership in memberships: for membership in memberships:
for role in membership.roles.all(): for role in membership.roles.all():
for perm in role.permissions.filter( for perm in role.permissions.filter(permission_filter(membership), type=t).all():
type=t, mask__rank__lte=get_current_request().session.get("permission_mask", -1)).all():
if not perm.permanent: if not perm.permanent:
if membership.date_start > date.today() or membership.date_end < date.today(): if membership.date_start > date.today() or membership.date_end < date.today():
continue continue
@ -53,16 +69,22 @@ class PermissionBackend(ModelBackend):
return perms return perms
@staticmethod @staticmethod
def permissions(user, model, type): def permissions(request, model, type):
""" """
List all permissions of the given user that applies to a given model and a give type List all permissions of the given user that applies to a given model and a give type
:param user: The owner of the permissions :param request: The current request
:param model: The model that the permissions shoud apply :param model: The model that the permissions shoud apply
:param type: The type of the permissions: view, change, add or delete :param type: The type of the permissions: view, change, add or delete
:return: A generator of the requested permissions :return: A generator of the requested permissions
""" """
for permission in PermissionBackend.get_raw_permissions(user, type): if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
# OAuth2 Authentication
user = request.auth.user
else:
user = request.user
for permission in PermissionBackend.get_raw_permissions(request, type):
if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.membership: if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.membership:
continue continue
@ -98,13 +120,17 @@ class PermissionBackend(ModelBackend):
:param field: The field of the model to test, if concerned :param field: The field of the model to test, if concerned
:return: A query that corresponds to the filter to give to a queryset :return: A query that corresponds to the filter to give to a queryset
""" """
if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
# OAuth2 Authentication
user = request.auth.user
else:
user = request.user user = request.user
if user is None or not user.is_authenticated: if user is None or user.is_anonymous:
# Anonymous users can't do anything # Anonymous users can't do asetdefaultnything
return Q(pk=-1) return Q(pk=-1)
if user.is_superuser and get_current_request().session.get("permission_mask", -1) >= 42: if user.is_superuser and request.session.get("permission_mask", -1) >= 42:
# Superusers have all rights # Superusers have all rights
return Q() return Q()
@ -113,7 +139,7 @@ class PermissionBackend(ModelBackend):
# Never satisfied # Never satisfied
query = Q(pk=-1) query = Q(pk=-1)
perms = PermissionBackend.permissions(user, model, t) perms = PermissionBackend.permissions(request, model, t)
for perm in perms: for perm in perms:
if perm.field and field != perm.field: if perm.field and field != perm.field:
continue continue
@ -134,12 +160,15 @@ class PermissionBackend(ModelBackend):
(e.g. for a transaction, the balance of the user could change) (e.g. for a transaction, the balance of the user could change)
""" """
user_obj = request.user user_obj = request.user
if user_obj is None or not user_obj.is_authenticated:
return False
sess = request.session sess = request.session
if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
# OAuth2 Authentication
user_obj = request.auth.user
if user_obj is None or user_obj.is_anonymous:
return False
if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42: if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42:
return True return True
@ -152,7 +181,7 @@ class PermissionBackend(ModelBackend):
ct = ContentType.objects.get_for_model(obj) ct = ContentType.objects.get_for_model(obj)
if any(permission.applies(obj, perm_type, perm_field) if any(permission.applies(obj, perm_type, perm_field)
for permission in PermissionBackend.permissions(user_obj, ct, perm_type)): for permission in PermissionBackend.permissions(request, ct, perm_type)):
return True return True
return False return False
@ -167,4 +196,4 @@ class PermissionBackend(ModelBackend):
def get_all_permissions(self, user_obj, obj=None): def get_all_permissions(self, user_obj, obj=None):
ct = ContentType.objects.get_for_model(obj) ct = ContentType.objects.get_for_model(obj)
return list(self.permissions(user_obj, ct, "view")) return list(self.permissions(get_current_request(), ct, "view"))

View File

@ -16,6 +16,9 @@ EXCLUDED = [
'contenttypes.contenttype', 'contenttypes.contenttype',
'logs.changelog', 'logs.changelog',
'migrations.migration', 'migrations.migration',
'oauth2_provider.accesstoken',
'oauth2_provider.grant',
'oauth2_provider.refreshtoken',
'sessions.session', 'sessions.session',
] ]