diff --git a/djangoldp_circle/migrations/0008_circlemember_is_admin.py b/djangoldp_circle/migrations/0008_circlemember_is_admin.py new file mode 100644 index 0000000000000000000000000000000000000000..9fdccd485dd3f125b252a67b40b563947657eca1 --- /dev/null +++ b/djangoldp_circle/migrations/0008_circlemember_is_admin.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2019-11-21 16:56 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangoldp_circle', '0007_auto_20191112_1251'), + ] + + operations = [ + migrations.AddField( + model_name='circlemember', + name='is_admin', + field=models.BooleanField(default=False), + ), + ] diff --git a/djangoldp_circle/models.py b/djangoldp_circle/models.py index 9ea673086e10e0f1849b92a4db6e9e34adc8e8d9..ab22d577b8b29141d5b628ece46d40726ec3f924 100644 --- a/djangoldp_circle/models.py +++ b/djangoldp_circle/models.py @@ -5,6 +5,7 @@ from django.db import models from django.db.models.signals import pre_save, post_save from django.dispatch import receiver from djangoldp.models import Model +from .permissions import CirclePermissions, CircleMemberPermissions STATUS_CHOICES = [ ('Public', 'Public'), @@ -27,6 +28,7 @@ class Circle(Model): auto_author = 'owner' owner_field = 'owner' nested_fields = ["team", 'members'] + permission_classes = [CirclePermissions] anonymous_perms = ["view"] authenticated_perms = ["inherit", "add"] owner_perms = ["inherit", "change", "delete"] @@ -36,17 +38,32 @@ class Circle(Model): def __str__(self): return self.name + def get_admins(self): + return self.members.filter(is_admin=True) + class CircleMember(Model): circle = models.ForeignKey(Circle, on_delete=models.CASCADE, related_name='members') user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="circles") + is_admin = models.BooleanField(default=False) def __str__(self): return str(self.circle.name) + " - " + str(self.user.name()) + def save(self, *args, **kwargs): + # cannot be duplicated CircleMembers + if not self.pk and CircleMember.objects.filter(circle=self.circle, user=self.user).exists(): + return + + # owners must be an admin + if not self.is_admin and self.circle.owner == self.user: + self.is_admin = True + + super(CircleMember, self).save(*args, **kwargs) + class Meta: - # TODO: As a auth I can still add someone else. container_path = "circle-members/" + permission_classes = [CircleMemberPermissions] authenticated_perms = ["view", "add"] owner_perms = ["view", "add", "delete"] # auto_author = "user" @@ -75,6 +92,7 @@ def set_jabberid(sender, instance, **kwargs): instance.jabberRoom = True @receiver(post_save, sender=Circle) -def set_ower_as_member(instance, created, **kwargs): +def set_owner_as_member(instance, created, **kwargs): + # add owner as an admin member, if they've not already been added if created and not instance.members.filter(user=instance.owner).exists(): - CircleMember.objects.create(user=instance.owner, circle=instance) + CircleMember.objects.create(user=instance.owner, circle=instance, is_admin=True) \ No newline at end of file diff --git a/djangoldp_circle/permissions.py b/djangoldp_circle/permissions.py new file mode 100644 index 0000000000000000000000000000000000000000..869d1224aefd031a455e726d64819f3fcd38fa81 --- /dev/null +++ b/djangoldp_circle/permissions.py @@ -0,0 +1,87 @@ +from djangoldp.permissions import LDPPermissions + + +# auxiliary function tests user is an admin for specified circle +def is_user_admin_of_circle(user, circle): + from .models import CircleMember + + if user.is_anonymous: + return False + + try: + circle_member = CircleMember.objects.get(user=user, circle=circle) + return circle_member.is_admin + + except: + return False + + +class CirclePermissions(LDPPermissions): + def has_permission(self, request, view): + # request on an existing resource - this will be reviewed by has_object_permission + if request.method == 'PATCH' or request.method == 'DELETE' or request.method == 'PUT': + return True + + return super().has_permission(request, view) + + def has_object_permission(self, request, view, obj): + from .models import CircleMember + + # admins have full permissions + if is_user_admin_of_circle(request.user, obj): + return True + + # other members can perform GET only + if obj.status != 'Public': + if request.user.is_anonymous: + return False + + if not CircleMember.objects.filter(user=request.user, circle=obj).exists(): + return False + + if request.method != 'GET': + return False + + return super().has_object_permission(request, view, obj) + + +class CircleMemberPermissions(LDPPermissions): + def has_permission(self, request, view): + # request on an existing resource - this will be reviewed by has_object_permission + if request.method == 'PATCH' or request.method == 'DELETE' or request.method == 'PUT': + return True + + return super().has_permission(request, view) + + def has_object_permission(self, request, view, obj): + from .models import CircleMember + + # admins have full permissions + if is_user_admin_of_circle(request.user, obj.circle): + if request.method == 'DELETE': + # I cannot remove myself if I am the last admin + if obj.pk == request.user.pk: + if obj.circle.get_admins().count() == 1: + return False + + # I cannot remove another admin + elif obj.is_admin: + return False + + return True + + # I can remove myself + if obj.user.pk == request.user.pk: + return True + + # anyone can perform GET if it's public + if obj.circle.status == 'Public': + if request.method == 'GET': + return True + + # private circles, members can GET/POST + elif obj.circle.status == 'Private': + if request.method == 'GET' or request.method == 'POST': + return CircleMember.objects.filter(user=request.user, circle=obj.circle).exists() + + return super().has_object_permission(request, view, obj)