mirror of
https://github.com/etesync/server
synced 2024-11-22 00:38:07 +00:00
Add support for read/write permissions.
This commit is contained in:
parent
6c05a7898a
commit
b081d0129f
@ -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,17 +337,17 @@ 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
|
|
||||||
)
|
|
||||||
|
@ -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,
|
||||||
|
@ -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()
|
||||||
|
@ -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}
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user