Collection invitation: implement creating and manipulating collections invitations.

This commit is contained in:
Tom Hacohen 2020-05-20 14:30:09 +03:00
parent edd88427b0
commit 8d1c02dcb9
5 changed files with 145 additions and 1 deletions

View File

@ -0,0 +1,31 @@
# Generated by Django 3.0.3 on 2020-05-20 11:03
from django.conf import settings
import django.core.validators
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', '0002_userinfo'),
]
operations = [
migrations.CreateModel(
name='CollectionInvitation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uid', models.CharField(db_index=True, max_length=44, validators=[django.core.validators.RegexValidator(message='Expected a 256bit base64url.', regex='^[a-zA-Z0-9\\-_]{43}$')])),
('signedEncryptionKey', models.BinaryField()),
('accessLevel', models.CharField(choices=[('adm', 'Admin'), ('rw', 'Read Write'), ('ro', 'Read Only')], default='ro', max_length=3)),
('fromMember', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_etesync.CollectionMember')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='incoming_invitations', to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'fromMember')},
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.3 on 2020-05-21 14:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_etesync', '0003_collectioninvitation'),
]
operations = [
migrations.AddField(
model_name='collectioninvitation',
name='version',
field=models.PositiveSmallIntegerField(default=1),
),
]

View File

@ -144,6 +144,28 @@ class CollectionMember(models.Model):
return '{} {}'.format(self.collection.uid, self.user) return '{} {}'.format(self.collection.uid, self.user)
class CollectionInvitation(models.Model):
uid = models.CharField(db_index=True, blank=False, null=False,
max_length=44, validators=[Base64Url256BitValidator])
version = models.PositiveSmallIntegerField(default=1)
fromMember = models.ForeignKey(CollectionMember, on_delete=models.CASCADE)
# FIXME: make sure to delete all invitations for the same collection once one is accepted
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='incoming_invitations', on_delete=models.CASCADE)
signedEncryptionKey = models.BinaryField(editable=False, blank=False, null=False)
accessLevel = models.CharField(
max_length=3,
choices=AccessLevels.choices,
default=AccessLevels.READ_ONLY,
)
class Meta:
unique_together = ('user', 'fromMember')
def __str__(self):
return '{} {}'.format(self.fromMember.collection.uid, self.user)
class UserInfo(models.Model): class UserInfo(models.Model):
owner = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, primary_key=True) owner = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, primary_key=True)
version = models.PositiveSmallIntegerField(default=1) version = models.PositiveSmallIntegerField(default=1)

View File

@ -262,6 +262,45 @@ class CollectionMemberSerializer(serializers.ModelSerializer):
return instance return instance
class CollectionInvitationSerializer(serializers.ModelSerializer):
username = serializers.SlugRelatedField(
source='user',
slug_field=User.USERNAME_FIELD,
queryset=User.objects
)
collection = serializers.SlugRelatedField(
source='fromMember__collection',
slug_field='uid',
read_only=True,
)
fromPubkey = BinaryBase64Field(source='fromMember__user__userinfo__pubkey', read_only=True)
signedEncryptionKey = BinaryBase64Field()
class Meta:
model = models.CollectionInvitation
fields = ('username', 'uid', 'collection', 'signedEncryptionKey', 'accessLevel', 'fromPubkey', 'version')
def create(self, validated_data):
collection = self.context['collection']
request = self.context['request']
if request.user == validated_data.get('user'):
raise serializers.ValidationError('Inviting yourself is not allowed')
member = collection.members.get(user=request.user)
with transaction.atomic():
return type(self).Meta.model.objects.create(**validated_data, fromMember=member)
def update(self, instance, validated_data):
with transaction.atomic():
instance.accessLevel = validated_data.pop('accessLevel')
instance.signedEncryptionKey = validated_data.pop('signedEncryptionKey')
instance.save()
return instance
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User

View File

@ -34,7 +34,7 @@ import nacl.secret
import nacl.hash import nacl.hash
from . import app_settings, permissions from . import app_settings, permissions
from .models import Collection, CollectionItem, CollectionItemRevision, CollectionMember from .models import Collection, CollectionItem, CollectionItemRevision, CollectionMember, CollectionInvitation
from .serializers import ( from .serializers import (
b64encode, b64encode,
AuthenticationSignupSerializer, AuthenticationSignupSerializer,
@ -48,6 +48,7 @@ from .serializers import (
CollectionItemRevisionSerializer, CollectionItemRevisionSerializer,
CollectionItemChunkSerializer, CollectionItemChunkSerializer,
CollectionMemberSerializer, CollectionMemberSerializer,
CollectionInvitationSerializer,
UserSerializer, UserSerializer,
) )
@ -423,6 +424,38 @@ class CollectionMemberViewSet(BaseViewSet):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
class CollectionInvitationViewSet(BaseViewSet):
allowed_methods = ['GET', 'POST', 'PUT', 'DELETE']
permission_classes = BaseViewSet.permission_classes + (permissions.IsCollectionAdmin, )
queryset = CollectionInvitation.objects.all()
serializer_class = CollectionInvitationSerializer
lookup_field = 'uid'
lookup_url_kwarg = 'invitation_uid'
def get_serializer_context(self):
context = super().get_serializer_context()
collection_uid = self.kwargs['collection_uid']
try:
collection = self.get_collection_queryset(Collection.objects).get(uid=collection_uid)
except Collection.DoesNotExist:
raise Http404('Collection does not exist')
context.update({'request': self.request, 'collection': collection})
return context
def get_queryset(self, queryset=None):
collection_uid = self.kwargs['collection_uid']
try:
collection = self.get_collection_queryset(Collection.objects).get(uid=collection_uid)
except Collection.DoesNotExist:
raise Http404('Collection does not exist')
if queryset is None:
queryset = type(self).queryset
return queryset.filter(fromMember__collection=collection)
class AuthenticationViewSet(viewsets.ViewSet): class AuthenticationViewSet(viewsets.ViewSet):
allowed_methods = ['POST'] allowed_methods = ['POST']
@ -561,6 +594,7 @@ class TestAuthenticationViewSet(viewsets.ViewSet):
# Delete all of the journal data for this user for a clear test env # Delete all of the journal data for this user for a clear test env
request.user.collection_set.all().delete() request.user.collection_set.all().delete()
request.user.incoming_invitations.all().delete()
# FIXME: also delete chunk files!!! # FIXME: also delete chunk files!!!