From b081d0129fafb02434fe94be69e3216f3ad74ac7 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 28 Dec 2020 12:12:00 +0200 Subject: [PATCH] Add support for read/write permissions. --- etebase_fastapi/collection.py | 61 +++++++++++++++++++++++------------ etebase_fastapi/invitation.py | 34 +++++++++++++------ etebase_fastapi/member.py | 15 ++++----- etebase_fastapi/utils.py | 7 +++- etebase_server/settings.py | 5 --- 5 files changed, 79 insertions(+), 43 deletions(-) diff --git a/etebase_fastapi/collection.py b/etebase_fastapi/collection.py index 6757ac3..e6c10c3 100644 --- a/etebase_fastapi/collection.py +++ b/etebase_fastapi/collection.py @@ -14,7 +14,17 @@ from .authentication import get_authenticated_user from .exceptions import HttpError, transform_validation_error, PermissionDenied from .msgpack import MsgpackRoute 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() collection_router = APIRouter(route_class=MsgpackRoute, responses=permission_responses) @@ -228,7 +238,13 @@ def has_write_access( # 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( data: ListMulti, stoken: t.Optional[str] = None, @@ -245,7 +261,7 @@ async def list_multi( 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( stoken: t.Optional[str] = None, limit: int = 50, @@ -321,17 +337,17 @@ def _create(data: CollectionIn, user: User): ).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)): 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( - obj: models.Collection = Depends(get_collection), - user: User = Depends(get_authenticated_user), - prefetch: Prefetch = PrefetchQuery - ): + obj: models.Collection = Depends(get_collection), + user: User = Depends(get_authenticated_user), + prefetch: Prefetch = PrefetchQuery, +): 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 -@item_router.get("/item/{item_uid}/", response_model=CollectionItemOut) +@item_router.get("/item/{item_uid}/", response_model=CollectionItemOut, dependencies=PERMISSIONS_READ) def item_get( item_uid: str, 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) 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) -@item_router.get("/item/", response_model=CollectionItemListResponse) +@item_router.get("/item/", response_model=CollectionItemListResponse, dependencies=PERMISSIONS_READ) async def item_list( queryset: QuerySet = Depends(get_item_queryset), stoken: t.Optional[str] = None, @@ -434,7 +451,9 @@ def item_bulk_common(data: ItemBatchIn, user: User, stoken: t.Optional[str], uid 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( item_uid: str, 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( data: t.List[CollectionItemBulkGetIn], 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( 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) -@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( collection_uid: str, data: ItemBatchIn, stoken: t.Optional[str] = None, user: User = Depends(get_authenticated_user) ): @@ -519,7 +538,11 @@ def item_batch( # 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( limit: int = 50, iterator: t.Optional[str] = None, @@ -539,6 +562,4 @@ def chunk_update( try: serializer.save(collection=col) except IntegrityError: - return Response( - {"code": "chunk_exists", "detail": "Chunk already exists."}, status=status.HTTP_409_CONFLICT - ) + return Response({"code": "chunk_exists", "detail": "Chunk already exists."}, status=status.HTTP_409_CONFLICT) diff --git a/etebase_fastapi/invitation.py b/etebase_fastapi/invitation.py index 1d8df94..39460a9 100644 --- a/etebase_fastapi/invitation.py +++ b/etebase_fastapi/invitation.py @@ -10,7 +10,15 @@ 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 +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) @@ -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( iterator: t.Optional[str] = None, limit: int = 50, @@ -117,7 +125,9 @@ def incoming_list( 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( invitation_uid: str, queryset: QuerySet = Depends(get_incoming_queryset), @@ -126,7 +136,9 @@ def incoming_get( 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( invitation_uid: str, queryset: QuerySet = Depends(get_incoming_queryset), @@ -135,7 +147,9 @@ def incoming_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( invitation_uid: str, data: CollectionInvitationAcceptIn, @@ -161,7 +175,7 @@ def incoming_accept( 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( data: CollectionInvitationIn, request: Request, @@ -189,7 +203,7 @@ def outgoing_create( 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( iterator: t.Optional[str] = None, limit: int = 50, @@ -198,7 +212,9 @@ def outgoing_list( 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( invitation_uid: str, queryset: QuerySet = Depends(get_outgoing_queryset), @@ -207,7 +223,7 @@ def outgoing_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( username: str, request: Request, diff --git a/etebase_fastapi/member.py b/etebase_fastapi/member.py index 8ffed9d..725d44b 100644 --- a/etebase_fastapi/member.py +++ b/etebase_fastapi/member.py @@ -8,7 +8,7 @@ from fastapi import APIRouter, Depends, status from django_etebase import models from .authentication import get_authenticated_user 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 .collection import get_collection, verify_collection_admin @@ -48,7 +48,9 @@ class MemberListResponse(BaseModel): 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( iterator: t.Optional[str] = None, limit: int = 50, @@ -70,7 +72,7 @@ def member_list( @member_router.delete( "/member/{username}/", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(verify_collection_admin)], + dependencies=[Depends(verify_collection_admin), *PERMISSIONS_READWRITE], ) def member_delete( obj: models.CollectionMember = Depends(get_member), @@ -81,7 +83,7 @@ def member_delete( @member_router.patch( "/member/{username}/", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(verify_collection_admin)], + dependencies=[Depends(verify_collection_admin), *PERMISSIONS_READWRITE], ) def member_patch( data: CollectionMemberModifyAccessLevelIn, @@ -95,10 +97,7 @@ def member_patch( instance.save() -@member_router.post( - "/member/leave/", - status_code=status.HTTP_204_NO_CONTENT, -) +@member_router.post("/member/leave/", status_code=status.HTTP_204_NO_CONTENT, dependencies=PERMISSIONS_READ) 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.revoke() diff --git a/etebase_fastapi/utils.py b/etebase_fastapi/utils.py index 2ee3700..165163a 100644 --- a/etebase_fastapi/utils.py +++ b/etebase_fastapi/utils.py @@ -1,13 +1,14 @@ import dataclasses import typing as t -from fastapi import status, Query +from fastapi import status, Query, Depends from pydantic import BaseModel as PyBaseModel from django.db.models import QuerySet from django.core.exceptions import ObjectDoesNotExist from django.contrib.auth import get_user_model +from django_etebase import app_settings from django_etebase.models import AccessLevels 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) +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} permission_responses = {401: response_model_dict, 403: response_model_dict} diff --git a/etebase_server/settings.py b/etebase_server/settings.py index 325dca9..46ad3c9 100644 --- a/etebase_server/settings.py +++ b/etebase_server/settings.py @@ -166,11 +166,6 @@ if any(os.path.isfile(x) for x in config_locations): if "database" in config: 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" # Efficient file streaming (for large files)