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)
|
||||
|
||||
|
||||
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):
|
||||
owner = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, primary_key=True)
|
||||
version = models.PositiveSmallIntegerField(default=1)
|
||||
|
@ -262,6 +262,45 @@ class CollectionMemberSerializer(serializers.ModelSerializer):
|
||||
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 Meta:
|
||||
model = User
|
||||
|
@ -34,7 +34,7 @@ import nacl.secret
|
||||
import nacl.hash
|
||||
|
||||
from . import app_settings, permissions
|
||||
from .models import Collection, CollectionItem, CollectionItemRevision, CollectionMember
|
||||
from .models import Collection, CollectionItem, CollectionItemRevision, CollectionMember, CollectionInvitation
|
||||
from .serializers import (
|
||||
b64encode,
|
||||
AuthenticationSignupSerializer,
|
||||
@ -48,6 +48,7 @@ from .serializers import (
|
||||
CollectionItemRevisionSerializer,
|
||||
CollectionItemChunkSerializer,
|
||||
CollectionMemberSerializer,
|
||||
CollectionInvitationSerializer,
|
||||
UserSerializer,
|
||||
)
|
||||
|
||||
@ -423,6 +424,38 @@ class CollectionMemberViewSet(BaseViewSet):
|
||||
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):
|
||||
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
|
||||
request.user.collection_set.all().delete()
|
||||
request.user.incoming_invitations.all().delete()
|
||||
|
||||
# FIXME: also delete chunk files!!!
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user