1
0
mirror of https://github.com/etesync/server synced 2024-11-22 08:48:07 +00:00

Collection membership: implement leaving/revoking access.

This commit is contained in:
Tom Hacohen 2020-05-27 16:03:16 +03:00
parent d93a5d3f06
commit 6e7fd5d0dd
4 changed files with 83 additions and 4 deletions

View File

@ -0,0 +1,28 @@
# Generated by Django 3.0.3 on 2020-05-27 11:29
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('django_etesync', '0012_auto_20200527_0743'),
]
operations = [
migrations.CreateModel(
name='CollectionMemberRemoved',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='removed_members', to='django_etesync.Collection')),
('stoken', models.OneToOneField(null=True, on_delete=django.db.models.deletion.PROTECT, to='django_etesync.Stoken')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'collection')},
},
),
]

View File

@ -14,7 +14,7 @@
from pathlib import Path from pathlib import Path
from django.db import models from django.db import models, transaction
from django.conf import settings from django.conf import settings
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db.models import Q from django.db.models import Q
@ -158,6 +158,29 @@ class CollectionMember(models.Model):
def __str__(self): def __str__(self):
return '{} {}'.format(self.collection.uid, self.user) return '{} {}'.format(self.collection.uid, self.user)
def revoke(self):
with transaction.atomic():
CollectionMemberRemoved.objects.update_or_create(
collection=self.collection, user=self.user,
defaults={
'stoken': Stoken.objects.create(),
},
)
self.delete()
class CollectionMemberRemoved(models.Model):
stoken = models.OneToOneField(Stoken, on_delete=models.PROTECT, null=True)
collection = models.ForeignKey(Collection, related_name='removed_members', on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
class Meta:
unique_together = ('user', 'collection')
def __str__(self):
return '{} {}'.format(self.collection.uid, self.user)
class CollectionInvitation(models.Model): class CollectionInvitation(models.Model):
uid = models.CharField(db_index=True, blank=False, null=False, uid = models.CharField(db_index=True, blank=False, null=False,

View File

@ -247,11 +247,10 @@ class CollectionMemberSerializer(serializers.ModelSerializer):
slug_field=User.USERNAME_FIELD, slug_field=User.USERNAME_FIELD,
queryset=User.objects queryset=User.objects
) )
encryptionKey = BinaryBase64Field()
class Meta: class Meta:
model = models.CollectionMember model = models.CollectionMember
fields = ('username', 'encryptionKey', 'accessLevel') fields = ('username', 'accessLevel')
def create(self, validated_data): def create(self, validated_data):
raise NotImplementedError() raise NotImplementedError()
@ -324,6 +323,9 @@ class InvitationAcceptSerializer(serializers.Serializer):
encryptionKey=encryption_key, encryptionKey=encryption_key,
) )
models.CollectionMemberRemoved.objects.filter(
user=invitation.user, collection=invitation.collection).delete()
invitation.delete() invitation.delete()
return member return member

View File

@ -42,6 +42,7 @@ from .models import (
CollectionItem, CollectionItem,
CollectionItemRevision, CollectionItemRevision,
CollectionMember, CollectionMember,
CollectionMemberRemoved,
CollectionInvitation, CollectionInvitation,
Stoken, Stoken,
UserInfo, UserInfo,
@ -181,6 +182,15 @@ class CollectionViewSet(BaseViewSet):
'data': serializer.data, 'data': serializer.data,
'stoken': new_stoken, 'stoken': new_stoken,
} }
stoken_obj = self.get_stoken_obj(request)
if stoken_obj is not None:
# FIXME: honour limit? (the limit should be combined for data and this because of stoken)
remed = CollectionMemberRemoved.objects.filter(user=request.user, stoken__id__gt=stoken_obj.id) \
.values_list('collection__uid', flat=True)
if len(remed) > 0:
ret['removedMemberships'] = [{'uid': x} for x in remed]
return Response(ret) return Response(ret)
@ -417,7 +427,8 @@ class CollectionItemChunkViewSet(viewsets.ViewSet):
class CollectionMemberViewSet(BaseViewSet): class CollectionMemberViewSet(BaseViewSet):
allowed_methods = ['GET', 'PUT', 'DELETE'] allowed_methods = ['GET', 'PUT', 'DELETE']
permission_classes = BaseViewSet.permission_classes + (permissions.IsCollectionAdmin, ) our_base_permission_classes = BaseViewSet.permission_classes
permission_classes = our_base_permission_classes + (permissions.IsCollectionAdmin, )
queryset = CollectionMember.objects.all() queryset = CollectionMember.objects.all()
serializer_class = CollectionMemberSerializer serializer_class = CollectionMemberSerializer
lookup_field = 'user__' + User.USERNAME_FIELD lookup_field = 'user__' + User.USERNAME_FIELD
@ -441,6 +452,21 @@ class CollectionMemberViewSet(BaseViewSet):
def create(self, request): def create(self, request):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
# FIXME: block leaving if we are the last admins - should be deleted / assigned in this case depending if there
# are other memebers.
def perform_destroy(self, instance):
instance.revoke()
@action_decorator(detail=False, methods=['POST'], permission_classes=our_base_permission_classes)
def leave(self, request, collection_uid=None):
collection_uid = self.kwargs['collection_uid']
col = get_object_or_404(self.get_collection_queryset(Collection.objects), uid=collection_uid)
member = col.members.get(user=request.user)
self.perform_destroy(member)
return Response({})
class InvitationOutgoingViewSet(BaseViewSet): class InvitationOutgoingViewSet(BaseViewSet):
allowed_methods = ['GET', 'POST', 'PUT', 'DELETE'] allowed_methods = ['GET', 'POST', 'PUT', 'DELETE']