Collection invitation: implement creating and manipulating collections invitations.
This commit is contained in:
parent
edd88427b0
commit
8d1c02dcb9
31
django_etesync/migrations/0003_collectioninvitation.py
Normal file
31
django_etesync/migrations/0003_collectioninvitation.py
Normal 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')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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!!!
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user