Add support for read/write permissions.

This commit is contained in:
Tom Hacohen 2020-12-28 12:12:00 +02:00
parent 6c05a7898a
commit b081d0129f
5 changed files with 79 additions and 43 deletions

View File

@ -14,7 +14,17 @@ from .authentication import get_authenticated_user
from .exceptions import HttpError, transform_validation_error, PermissionDenied from .exceptions import HttpError, transform_validation_error, PermissionDenied
from .msgpack import MsgpackRoute from .msgpack import MsgpackRoute
from .stoken_handler import filter_by_stoken_and_limit, filter_by_stoken, get_stoken_obj, get_queryset_stoken from .stoken_handler import filter_by_stoken_and_limit, filter_by_stoken, get_stoken_obj, get_queryset_stoken
from .utils import get_object_or_404, Context, Prefetch, PrefetchQuery, is_collection_admin, BaseModel, permission_responses from .utils import (
get_object_or_404,
Context,
Prefetch,
PrefetchQuery,
is_collection_admin,
BaseModel,
permission_responses,
PERMISSIONS_READ,
PERMISSIONS_READWRITE,
)
User = get_user_model() User = get_user_model()
collection_router = APIRouter(route_class=MsgpackRoute, responses=permission_responses) collection_router = APIRouter(route_class=MsgpackRoute, responses=permission_responses)
@ -228,7 +238,13 @@ def has_write_access(
# paths # paths
@collection_router.post("/list_multi/", response_model=CollectionListResponse, response_model_exclude_unset=True)
@collection_router.post(
"/list_multi/",
response_model=CollectionListResponse,
response_model_exclude_unset=True,
dependencies=PERMISSIONS_READ,
)
async def list_multi( async def list_multi(
data: ListMulti, data: ListMulti,
stoken: t.Optional[str] = None, stoken: t.Optional[str] = None,
@ -245,7 +261,7 @@ async def list_multi(
return await collection_list_common(queryset, user, stoken, limit, prefetch) return await collection_list_common(queryset, user, stoken, limit, prefetch)
@collection_router.get("/", response_model=CollectionListResponse) @collection_router.get("/", response_model=CollectionListResponse, dependencies=PERMISSIONS_READ)
async def collection_list( async def collection_list(
stoken: t.Optional[str] = None, stoken: t.Optional[str] = None,
limit: int = 50, limit: int = 50,
@ -321,16 +337,16 @@ def _create(data: CollectionIn, user: User):
).save() ).save()
@collection_router.post("/", status_code=status.HTTP_201_CREATED) @collection_router.post("/", status_code=status.HTTP_201_CREATED, dependencies=PERMISSIONS_READWRITE)
async def create(data: CollectionIn, user: User = Depends(get_authenticated_user)): async def create(data: CollectionIn, user: User = Depends(get_authenticated_user)):
await sync_to_async(_create)(data, user) await sync_to_async(_create)(data, user)
@collection_router.get("/{collection_uid}/", response_model=CollectionOut) @collection_router.get("/{collection_uid}/", response_model=CollectionOut, dependencies=PERMISSIONS_READ)
def collection_get( def collection_get(
obj: models.Collection = Depends(get_collection), obj: models.Collection = Depends(get_collection),
user: User = Depends(get_authenticated_user), user: User = Depends(get_authenticated_user),
prefetch: Prefetch = PrefetchQuery prefetch: Prefetch = PrefetchQuery,
): ):
return CollectionOut.from_orm_context(obj, Context(user, prefetch)) return CollectionOut.from_orm_context(obj, Context(user, prefetch))
@ -375,11 +391,12 @@ def item_create(item_model: CollectionItemIn, collection: models.Collection, val
return instance return instance
@item_router.get("/item/{item_uid}/", response_model=CollectionItemOut) @item_router.get("/item/{item_uid}/", response_model=CollectionItemOut, dependencies=PERMISSIONS_READ)
def item_get( def item_get(
item_uid: str, item_uid: str,
queryset: QuerySet = Depends(get_item_queryset), queryset: QuerySet = Depends(get_item_queryset),
user: User = Depends(get_authenticated_user), prefetch: Prefetch = PrefetchQuery, user: User = Depends(get_authenticated_user),
prefetch: Prefetch = PrefetchQuery,
): ):
obj = queryset.get(uid=item_uid) obj = queryset.get(uid=item_uid)
return CollectionItemOut.from_orm_context(obj, Context(user, prefetch)) return CollectionItemOut.from_orm_context(obj, Context(user, prefetch))
@ -402,7 +419,7 @@ def item_list_common(
return CollectionItemListResponse(data=data, stoken=new_stoken, done=done) return CollectionItemListResponse(data=data, stoken=new_stoken, done=done)
@item_router.get("/item/", response_model=CollectionItemListResponse) @item_router.get("/item/", response_model=CollectionItemListResponse, dependencies=PERMISSIONS_READ)
async def item_list( async def item_list(
queryset: QuerySet = Depends(get_item_queryset), queryset: QuerySet = Depends(get_item_queryset),
stoken: t.Optional[str] = None, stoken: t.Optional[str] = None,
@ -434,7 +451,9 @@ def item_bulk_common(data: ItemBatchIn, user: User, stoken: t.Optional[str], uid
return None return None
@item_router.get("/item/{item_uid}/revision/", response_model=CollectionItemRevisionListResponse) @item_router.get(
"/item/{item_uid}/revision/", response_model=CollectionItemRevisionListResponse, dependencies=PERMISSIONS_READ
)
def item_revisions( def item_revisions(
item_uid: str, item_uid: str,
limit: int = 50, limit: int = 50,
@ -469,7 +488,7 @@ def item_revisions(
) )
@item_router.post("/item/fetch_updates/", response_model=CollectionItemListResponse) @item_router.post("/item/fetch_updates/", response_model=CollectionItemListResponse, dependencies=PERMISSIONS_READ)
def fetch_updates( def fetch_updates(
data: t.List[CollectionItemBulkGetIn], data: t.List[CollectionItemBulkGetIn],
stoken: t.Optional[str] = None, stoken: t.Optional[str] = None,
@ -502,14 +521,14 @@ def fetch_updates(
) )
@item_router.post("/item/transaction/", dependencies=[Depends(has_write_access)]) @item_router.post("/item/transaction/", dependencies=[Depends(has_write_access), *PERMISSIONS_READWRITE])
def item_transaction( def item_transaction(
collection_uid: str, data: ItemBatchIn, stoken: t.Optional[str] = None, user: User = Depends(get_authenticated_user) collection_uid: str, data: ItemBatchIn, stoken: t.Optional[str] = None, user: User = Depends(get_authenticated_user)
): ):
return item_bulk_common(data, user, stoken, collection_uid, validate_etag=True) return item_bulk_common(data, user, stoken, collection_uid, validate_etag=True)
@item_router.post("/item/batch/", dependencies=[Depends(has_write_access)]) @item_router.post("/item/batch/", dependencies=[Depends(has_write_access), *PERMISSIONS_READWRITE])
def item_batch( def item_batch(
collection_uid: str, data: ItemBatchIn, stoken: t.Optional[str] = None, user: User = Depends(get_authenticated_user) collection_uid: str, data: ItemBatchIn, stoken: t.Optional[str] = None, user: User = Depends(get_authenticated_user)
): ):
@ -519,7 +538,11 @@ def item_batch(
# Chunks # Chunks
@item_router.put("/item/{item_uid}/chunk/{chunk_uid}/", dependencies=[Depends(has_write_access)], status_code=status.HTTP_201_CREATED) @item_router.put(
"/item/{item_uid}/chunk/{chunk_uid}/",
dependencies=[Depends(has_write_access), *PERMISSIONS_READWRITE],
status_code=status.HTTP_201_CREATED,
)
def chunk_update( def chunk_update(
limit: int = 50, limit: int = 50,
iterator: t.Optional[str] = None, iterator: t.Optional[str] = None,
@ -539,6 +562,4 @@ def chunk_update(
try: try:
serializer.save(collection=col) serializer.save(collection=col)
except IntegrityError: except IntegrityError:
return Response( return Response({"code": "chunk_exists", "detail": "Chunk already exists."}, status=status.HTTP_409_CONFLICT)
{"code": "chunk_exists", "detail": "Chunk already exists."}, status=status.HTTP_409_CONFLICT
)

View File

@ -10,7 +10,15 @@ from django_etebase.utils import get_user_queryset, CallbackContext
from .authentication import get_authenticated_user from .authentication import get_authenticated_user
from .exceptions import HttpError, PermissionDenied from .exceptions import HttpError, PermissionDenied
from .msgpack import MsgpackRoute from .msgpack import MsgpackRoute
from .utils import get_object_or_404, Context, is_collection_admin, BaseModel, permission_responses from .utils import (
get_object_or_404,
Context,
is_collection_admin,
BaseModel,
permission_responses,
PERMISSIONS_READ,
PERMISSIONS_READWRITE,
)
User = get_user_model() User = get_user_model()
invitation_incoming_router = APIRouter(route_class=MsgpackRoute, responses=permission_responses) invitation_incoming_router = APIRouter(route_class=MsgpackRoute, responses=permission_responses)
@ -108,7 +116,7 @@ def list_common(
) )
@invitation_incoming_router.get("/", response_model=InvitationListResponse) @invitation_incoming_router.get("/", response_model=InvitationListResponse, dependencies=PERMISSIONS_READ)
def incoming_list( def incoming_list(
iterator: t.Optional[str] = None, iterator: t.Optional[str] = None,
limit: int = 50, limit: int = 50,
@ -117,7 +125,9 @@ def incoming_list(
return list_common(queryset, iterator, limit) return list_common(queryset, iterator, limit)
@invitation_incoming_router.get("/{invitation_uid}/", response_model=CollectionInvitationOut) @invitation_incoming_router.get(
"/{invitation_uid}/", response_model=CollectionInvitationOut, dependencies=PERMISSIONS_READ
)
def incoming_get( def incoming_get(
invitation_uid: str, invitation_uid: str,
queryset: QuerySet = Depends(get_incoming_queryset), queryset: QuerySet = Depends(get_incoming_queryset),
@ -126,7 +136,9 @@ def incoming_get(
return CollectionInvitationOut.from_orm(obj) return CollectionInvitationOut.from_orm(obj)
@invitation_incoming_router.delete("/{invitation_uid}/", status_code=status.HTTP_204_NO_CONTENT) @invitation_incoming_router.delete(
"/{invitation_uid}/", status_code=status.HTTP_204_NO_CONTENT, dependencies=PERMISSIONS_READWRITE
)
def incoming_delete( def incoming_delete(
invitation_uid: str, invitation_uid: str,
queryset: QuerySet = Depends(get_incoming_queryset), queryset: QuerySet = Depends(get_incoming_queryset),
@ -135,7 +147,9 @@ def incoming_delete(
obj.delete() obj.delete()
@invitation_incoming_router.post("/{invitation_uid}/accept/", status_code=status.HTTP_201_CREATED) @invitation_incoming_router.post(
"/{invitation_uid}/accept/", status_code=status.HTTP_201_CREATED, dependencies=PERMISSIONS_READWRITE
)
def incoming_accept( def incoming_accept(
invitation_uid: str, invitation_uid: str,
data: CollectionInvitationAcceptIn, data: CollectionInvitationAcceptIn,
@ -161,7 +175,7 @@ def incoming_accept(
invitation.delete() invitation.delete()
@invitation_outgoing_router.post("/", status_code=status.HTTP_201_CREATED) @invitation_outgoing_router.post("/", status_code=status.HTTP_201_CREATED, dependencies=PERMISSIONS_READWRITE)
def outgoing_create( def outgoing_create(
data: CollectionInvitationIn, data: CollectionInvitationIn,
request: Request, request: Request,
@ -189,7 +203,7 @@ def outgoing_create(
raise HttpError("invitation_exists", "Invitation already exists") raise HttpError("invitation_exists", "Invitation already exists")
@invitation_outgoing_router.get("/", response_model=InvitationListResponse) @invitation_outgoing_router.get("/", response_model=InvitationListResponse, dependencies=PERMISSIONS_READ)
def outgoing_list( def outgoing_list(
iterator: t.Optional[str] = None, iterator: t.Optional[str] = None,
limit: int = 50, limit: int = 50,
@ -198,7 +212,9 @@ def outgoing_list(
return list_common(queryset, iterator, limit) return list_common(queryset, iterator, limit)
@invitation_outgoing_router.delete("/{invitation_uid}/", status_code=status.HTTP_204_NO_CONTENT) @invitation_outgoing_router.delete(
"/{invitation_uid}/", status_code=status.HTTP_204_NO_CONTENT, dependencies=PERMISSIONS_READWRITE
)
def outgoing_delete( def outgoing_delete(
invitation_uid: str, invitation_uid: str,
queryset: QuerySet = Depends(get_outgoing_queryset), queryset: QuerySet = Depends(get_outgoing_queryset),
@ -207,7 +223,7 @@ def outgoing_delete(
obj.delete() obj.delete()
@invitation_outgoing_router.get("/fetch_user_profile/", response_model=UserInfoOut) @invitation_outgoing_router.get("/fetch_user_profile/", response_model=UserInfoOut, dependencies=PERMISSIONS_READ)
def outgoing_fetch_user_profile( def outgoing_fetch_user_profile(
username: str, username: str,
request: Request, request: Request,

View File

@ -8,7 +8,7 @@ from fastapi import APIRouter, Depends, status
from django_etebase import models from django_etebase import models
from .authentication import get_authenticated_user from .authentication import get_authenticated_user
from .msgpack import MsgpackRoute from .msgpack import MsgpackRoute
from .utils import get_object_or_404, BaseModel, permission_responses from .utils import get_object_or_404, BaseModel, permission_responses, PERMISSIONS_READ, PERMISSIONS_READWRITE
from .stoken_handler import filter_by_stoken_and_limit from .stoken_handler import filter_by_stoken_and_limit
from .collection import get_collection, verify_collection_admin from .collection import get_collection, verify_collection_admin
@ -48,7 +48,9 @@ class MemberListResponse(BaseModel):
done: bool done: bool
@member_router.get("/member/", response_model=MemberListResponse, dependencies=[Depends(verify_collection_admin)]) @member_router.get(
"/member/", response_model=MemberListResponse, dependencies=[Depends(verify_collection_admin), *PERMISSIONS_READ]
)
def member_list( def member_list(
iterator: t.Optional[str] = None, iterator: t.Optional[str] = None,
limit: int = 50, limit: int = 50,
@ -70,7 +72,7 @@ def member_list(
@member_router.delete( @member_router.delete(
"/member/{username}/", "/member/{username}/",
status_code=status.HTTP_204_NO_CONTENT, status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(verify_collection_admin)], dependencies=[Depends(verify_collection_admin), *PERMISSIONS_READWRITE],
) )
def member_delete( def member_delete(
obj: models.CollectionMember = Depends(get_member), obj: models.CollectionMember = Depends(get_member),
@ -81,7 +83,7 @@ def member_delete(
@member_router.patch( @member_router.patch(
"/member/{username}/", "/member/{username}/",
status_code=status.HTTP_204_NO_CONTENT, status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(verify_collection_admin)], dependencies=[Depends(verify_collection_admin), *PERMISSIONS_READWRITE],
) )
def member_patch( def member_patch(
data: CollectionMemberModifyAccessLevelIn, data: CollectionMemberModifyAccessLevelIn,
@ -95,10 +97,7 @@ def member_patch(
instance.save() instance.save()
@member_router.post( @member_router.post("/member/leave/", status_code=status.HTTP_204_NO_CONTENT, dependencies=PERMISSIONS_READ)
"/member/leave/",
status_code=status.HTTP_204_NO_CONTENT,
)
def member_leave(user: User = Depends(get_authenticated_user), collection: models.Collection = Depends(get_collection)): def member_leave(user: User = Depends(get_authenticated_user), collection: models.Collection = Depends(get_collection)):
obj = get_object_or_404(collection.members, user=user) obj = get_object_or_404(collection.members, user=user)
obj.revoke() obj.revoke()

View File

@ -1,13 +1,14 @@
import dataclasses import dataclasses
import typing as t import typing as t
from fastapi import status, Query from fastapi import status, Query, Depends
from pydantic import BaseModel as PyBaseModel from pydantic import BaseModel as PyBaseModel
from django.db.models import QuerySet from django.db.models import QuerySet
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django_etebase import app_settings
from django_etebase.models import AccessLevels from django_etebase.models import AccessLevels
from .exceptions import HttpError, HttpErrorOut from .exceptions import HttpError, HttpErrorOut
@ -43,5 +44,9 @@ def is_collection_admin(collection, user):
return (member is not None) and (member.accessLevel == AccessLevels.ADMIN) return (member is not None) and (member.accessLevel == AccessLevels.ADMIN)
PERMISSIONS_READ = [Depends(x) for x in app_settings.API_PERMISSIONS_READ]
PERMISSIONS_READWRITE = PERMISSIONS_READ + [Depends(x) for x in app_settings.API_PERMISSIONS_WRITE]
response_model_dict = {"model": HttpErrorOut} response_model_dict = {"model": HttpErrorOut}
permission_responses = {401: response_model_dict, 403: response_model_dict} permission_responses = {401: response_model_dict, 403: response_model_dict}

View File

@ -166,11 +166,6 @@ if any(os.path.isfile(x) for x in config_locations):
if "database" in config: if "database" in config:
DATABASES = {"default": {x.upper(): y for x, y in config.items("database")}} DATABASES = {"default": {x.upper(): y for x, y in config.items("database")}}
ETEBASE_API_PERMISSIONS = ("rest_framework.permissions.IsAuthenticated",)
ETEBASE_API_AUTHENTICATORS = (
"django_etebase.token_auth.authentication.TokenAuthentication",
"rest_framework.authentication.SessionAuthentication",
)
ETEBASE_CREATE_USER_FUNC = "django_etebase.utils.create_user_blocked" ETEBASE_CREATE_USER_FUNC = "django_etebase.utils.create_user_blocked"
# Efficient file streaming (for large files) # Efficient file streaming (for large files)