etesync-server/etebase_fastapi/invitation.py
Tom Hacohen ca7f2ec73c When converting from ORM convert binary fields to bytes.
The problem is that some ORMs return memoryview which are more efficient but
are not supported by pydantic at the moment.
2020-12-28 16:42:39 +02:00

240 lines
7.4 KiB
Python

import typing as t
from django.contrib.auth import get_user_model
from django.db import transaction, IntegrityError
from django.db.models import QuerySet
from fastapi import APIRouter, Depends, status, Request
from django_etebase import models
from django_etebase.utils import get_user_queryset, CallbackContext
from .authentication import get_authenticated_user
from .exceptions import HttpError, PermissionDenied
from .msgpack import MsgpackRoute
from .utils import (
get_object_or_404,
Context,
is_collection_admin,
BaseModel,
permission_responses,
PERMISSIONS_READ,
PERMISSIONS_READWRITE,
)
User = get_user_model()
invitation_incoming_router = APIRouter(route_class=MsgpackRoute, responses=permission_responses)
invitation_outgoing_router = APIRouter(route_class=MsgpackRoute, responses=permission_responses)
default_queryset: QuerySet = models.CollectionInvitation.objects.all()
class UserInfoOut(BaseModel):
pubkey: bytes
class Config:
orm_mode = True
@classmethod
def from_orm(cls: t.Type["UserInfoOut"], obj: models.UserInfo) -> "UserInfoOut":
return cls(pubkey=bytes(obj.pubkey))
class CollectionInvitationAcceptIn(BaseModel):
collectionType: bytes
encryptionKey: bytes
class CollectionInvitationCommon(BaseModel):
uid: str
version: int
accessLevel: models.AccessLevels
username: str
collection: str
signedEncryptionKey: bytes
class CollectionInvitationIn(CollectionInvitationCommon):
def validate_db(self, context: Context):
if context.user.username == self.username.lower():
raise HttpError("no_self_invite", "Inviting yourself is not allowed")
class CollectionInvitationOut(CollectionInvitationCommon):
fromUsername: str
fromPubkey: bytes
class Config:
orm_mode = True
@classmethod
def from_orm(cls: t.Type["CollectionInvitationOut"], obj: models.CollectionInvitation) -> "CollectionInvitationOut":
return cls(
uid=obj.uid,
version=obj.version,
accessLevel=obj.accessLevel,
username=obj.user.username,
collection=obj.collection.uid,
fromUsername=obj.fromMember.user.username,
fromPubkey=bytes(obj.fromMember.user.userinfo.pubkey),
signedEncryptionKey=bytes(obj.signedEncryptionKey),
)
class InvitationListResponse(BaseModel):
data: t.List[CollectionInvitationOut]
iterator: t.Optional[str]
done: bool
def get_incoming_queryset(user: User = Depends(get_authenticated_user)):
return default_queryset.filter(user=user)
def get_outgoing_queryset(user: User = Depends(get_authenticated_user)):
return default_queryset.filter(fromMember__user=user)
def list_common(
queryset: QuerySet,
iterator: t.Optional[str],
limit: int,
) -> InvitationListResponse:
queryset = queryset.order_by("id")
if iterator is not None:
iterator_obj = get_object_or_404(queryset, uid=iterator)
queryset = queryset.filter(id__gt=iterator_obj.id)
result = list(queryset[: limit + 1])
if len(result) < limit + 1:
done = True
else:
done = False
result = result[:-1]
ret_data = result
iterator = ret_data[-1].uid if len(result) > 0 else None
return InvitationListResponse(
data=ret_data,
iterator=iterator,
done=done,
)
@invitation_incoming_router.get("/", response_model=InvitationListResponse, dependencies=PERMISSIONS_READ)
def incoming_list(
iterator: t.Optional[str] = None,
limit: int = 50,
queryset: QuerySet = Depends(get_incoming_queryset),
):
return list_common(queryset, iterator, limit)
@invitation_incoming_router.get(
"/{invitation_uid}/", response_model=CollectionInvitationOut, dependencies=PERMISSIONS_READ
)
def incoming_get(
invitation_uid: str,
queryset: QuerySet = Depends(get_incoming_queryset),
):
obj = get_object_or_404(queryset, uid=invitation_uid)
return CollectionInvitationOut.from_orm(obj)
@invitation_incoming_router.delete(
"/{invitation_uid}/", status_code=status.HTTP_204_NO_CONTENT, dependencies=PERMISSIONS_READWRITE
)
def incoming_delete(
invitation_uid: str,
queryset: QuerySet = Depends(get_incoming_queryset),
):
obj = get_object_or_404(queryset, uid=invitation_uid)
obj.delete()
@invitation_incoming_router.post(
"/{invitation_uid}/accept/", status_code=status.HTTP_201_CREATED, dependencies=PERMISSIONS_READWRITE
)
def incoming_accept(
invitation_uid: str,
data: CollectionInvitationAcceptIn,
queryset: QuerySet = Depends(get_incoming_queryset),
):
invitation = get_object_or_404(queryset, uid=invitation_uid)
with transaction.atomic():
user = invitation.user
collection_type_obj, _ = models.CollectionType.objects.get_or_create(uid=data.collectionType, owner=user)
models.CollectionMember.objects.create(
collection=invitation.collection,
stoken=models.Stoken.objects.create(),
user=user,
accessLevel=invitation.accessLevel,
encryptionKey=data.encryptionKey,
collectionType=collection_type_obj,
)
models.CollectionMemberRemoved.objects.filter(user=invitation.user, collection=invitation.collection).delete()
invitation.delete()
@invitation_outgoing_router.post("/", status_code=status.HTTP_201_CREATED, dependencies=PERMISSIONS_READWRITE)
def outgoing_create(
data: CollectionInvitationIn,
request: Request,
user: User = Depends(get_authenticated_user),
):
collection = get_object_or_404(models.Collection.objects, uid=data.collection)
to_user = get_object_or_404(
get_user_queryset(User.objects.all(), CallbackContext(request.path_params)), username=data.username
)
context = Context(user, None)
data.validate_db(context)
if not is_collection_admin(collection, user):
raise PermissionDenied("admin_access_required", "User is not an admin of this collection")
member = collection.members.get(user=user)
with transaction.atomic():
try:
ret = models.CollectionInvitation.objects.create(
**data.dict(exclude={"collection", "username"}), user=to_user, fromMember=member
)
except IntegrityError:
raise HttpError("invitation_exists", "Invitation already exists")
@invitation_outgoing_router.get("/", response_model=InvitationListResponse, dependencies=PERMISSIONS_READ)
def outgoing_list(
iterator: t.Optional[str] = None,
limit: int = 50,
queryset: QuerySet = Depends(get_outgoing_queryset),
):
return list_common(queryset, iterator, limit)
@invitation_outgoing_router.delete(
"/{invitation_uid}/", status_code=status.HTTP_204_NO_CONTENT, dependencies=PERMISSIONS_READWRITE
)
def outgoing_delete(
invitation_uid: str,
queryset: QuerySet = Depends(get_outgoing_queryset),
):
obj = get_object_or_404(queryset, uid=invitation_uid)
obj.delete()
@invitation_outgoing_router.get("/fetch_user_profile/", response_model=UserInfoOut, dependencies=PERMISSIONS_READ)
def outgoing_fetch_user_profile(
username: str,
request: Request,
user: User = Depends(get_authenticated_user),
):
kwargs = {User.USERNAME_FIELD: username.lower()}
user = get_object_or_404(get_user_queryset(User.objects.all(), CallbackContext(request.path_params)), **kwargs)
user_info = get_object_or_404(models.UserInfo.objects.all(), owner=user)
return UserInfoOut.from_orm(user_info)