Skip to content

✨ web-api: share tags #6998

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.template.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"**/requirements/*.txt": "pip-requirements",
"*logs.txt": "log",
"*Makefile": "makefile",
"*sql.*": "sql",
"*.sql": "sql",
"docker-compose*.yml": "dockercompose",
"Dockerfile*": "dockerfile"
},
Expand Down
1 change: 1 addition & 0 deletions api/specs/web-server/_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
@router.post(
"/tags",
response_model=Envelope[TagGet],
status_code=status.HTTP_201_CREATED,
)
async def create_tag(_body: TagCreate):
...
Expand Down
15 changes: 10 additions & 5 deletions api/specs/web-server/_tags_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@

from fastapi import APIRouter, Depends, status
from models_library.generics import Envelope
from models_library.rest_error import EnvelopedError
from simcore_service_webserver._meta import API_VTAG
from simcore_service_webserver.tags._rest import _TO_HTTP_ERROR_MAP
from simcore_service_webserver.tags.schemas import (
TagGet,
TagGroupCreate,
Expand All @@ -23,6 +25,9 @@
"tags",
"groups",
],
responses={
i.status_code: {"model": EnvelopedError} for i in _TO_HTTP_ERROR_MAP.values()
},
)


Expand All @@ -31,7 +36,7 @@
response_model=Envelope[list[TagGroupGet]],
)
async def list_tag_groups(_path_params: Annotated[TagPathParams, Depends()]):
...
"""Lists all groups associated to this tag"""


@router.post(
Expand All @@ -42,22 +47,22 @@ async def list_tag_groups(_path_params: Annotated[TagPathParams, Depends()]):
async def create_tag_group(
_path_params: Annotated[TagGroupPathParams, Depends()], _body: TagGroupCreate
):
...
"""Shares tag `tag_id` with an organization or user with `group_id` providing access-rights to it"""


@router.put(
"/tags/{tag_id}/groups/{group_id}",
response_model=Envelope[list[TagGroupGet]],
)
async def replace_tag_groups(
async def replace_tag_group(
_path_params: Annotated[TagGroupPathParams, Depends()], _body: TagGroupCreate
):
...
"""Replace access rights on tag for associated organization or user with `group_id`"""


@router.delete(
"/tags/{tag_id}/groups/{group_id}",
status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_tag_group(_path_params: Annotated[TagGroupPathParams, Depends()]):
...
"""Delete access rights on tag to an associated organization or user with `group_id`"""
7 changes: 7 additions & 0 deletions packages/common-library/src/common_library/groups_dicts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from typing_extensions import TypedDict


class AccessRightsDict(TypedDict):
read: bool
write: bool
delete: bool
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
""" Repository pattern, errors and data structures for models.tags
"""

from typing import TypedDict

from common_library.errors_classes import OsparcErrorMixin
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine
from typing_extensions import TypedDict

from .utils_repos import pass_or_acquire_connection, transaction_context
from .utils_tags_sql import (
TagAccessRightsDict,
count_groups_with_given_access_rights_stmt,
create_tag_stmt,
delete_tag_access_rights_stmt,
delete_tag_stmt,
get_tag_stmt,
has_access_rights_stmt,
list_tag_group_access_stmt,
list_tags_stmt,
set_tag_access_rights_stmt,
update_tag_stmt,
upsert_tags_access_rights_stmt,
)

__all__: tuple[str, ...] = ("TagAccessRightsDict",)


#
# Errors
#
class BaseTagError(Exception):
pass
class _BaseTagError(OsparcErrorMixin, Exception):
msg_template = "Tag repo error on tag {tag_id}"


class TagNotFoundError(BaseTagError):
class TagNotFoundError(_BaseTagError):
pass


class TagOperationNotAllowedError(BaseTagError): # maps to AccessForbidden
class TagOperationNotAllowedError(_BaseTagError): # maps to AccessForbidden
pass


Expand Down Expand Up @@ -108,7 +113,7 @@ async def create(
assert tag # nosec

# take tag ownership
access_stmt = set_tag_access_rights_stmt(
access_stmt = upsert_tags_access_rights_stmt(
tag_id=tag.id,
user_id=user_id,
read=read,
Expand Down Expand Up @@ -163,8 +168,7 @@ async def get(
result = await conn.execute(stmt_get)
row = result.first()
if not row:
msg = f"{tag_id=} not found: either no access or does not exists"
raise TagNotFoundError(msg)
raise TagNotFoundError(operation="get", tag_id=tag_id, user_id=user_id)
return TagDict(
id=row.id,
name=row.name,
Expand Down Expand Up @@ -198,8 +202,9 @@ async def update(
result = await conn.execute(update_stmt)
row = result.first()
if not row:
msg = f"{tag_id=} not updated: either no access or not found"
raise TagOperationNotAllowedError(msg)
raise TagOperationNotAllowedError(
operation="update", tag_id=tag_id, user_id=user_id
)

return TagDict(
id=row.id,
Expand All @@ -222,44 +227,95 @@ async def delete(
async with transaction_context(self.engine, connection) as conn:
deleted = await conn.scalar(stmt_delete)
if not deleted:
msg = f"Could not delete {tag_id=}. Not found or insuficient access."
raise TagOperationNotAllowedError(msg)
raise TagOperationNotAllowedError(
operation="delete", tag_id=tag_id, user_id=user_id
)

#
# ACCESS RIGHTS
#

async def create_access_rights(
async def has_access_rights(
self,
connection: AsyncConnection | None = None,
*,
user_id: int,
tag_id: int,
group_id: int,
read: bool,
write: bool,
delete: bool,
):
raise NotImplementedError
read: bool = False,
write: bool = False,
delete: bool = False,
) -> bool:
async with pass_or_acquire_connection(self.engine, connection) as conn:
group_id_or_none = await conn.scalar(
has_access_rights_stmt(
tag_id=tag_id,
caller_user_id=user_id,
read=read,
write=write,
delete=delete,
)
)
return bool(group_id_or_none)

async def update_access_rights(
async def list_access_rights(
self,
connection: AsyncConnection | None = None,
*,
tag_id: int,
) -> list[TagAccessRightsDict]:
async with pass_or_acquire_connection(self.engine, connection) as conn:
result = await conn.execute(list_tag_group_access_stmt(tag_id=tag_id))
return [
TagAccessRightsDict(
tag_id=row.tag_id,
group_id=row.group_id,
read=row.read,
write=row.write,
delete=row.delete,
)
for row in result.fetchall()
]

async def create_or_update_access_rights(
self,
connection: AsyncConnection | None = None,
*,
user_id: int,
tag_id: int,
group_id: int,
read: bool,
write: bool,
delete: bool,
):
raise NotImplementedError
) -> TagAccessRightsDict:
async with transaction_context(self.engine, connection) as conn:
result = await conn.execute(
upsert_tags_access_rights_stmt(
tag_id=tag_id,
group_id=group_id,
read=read,
write=write,
delete=delete,
)
)
row = result.first()
assert row is not None

return TagAccessRightsDict(
tag_id=row.tag_id,
group_id=row.group_id,
read=row.read,
write=row.write,
delete=row.delete,
)

async def delete_access_rights(
self,
connection: AsyncConnection | None = None,
*,
user_id: int,
tag_id: int,
):
raise NotImplementedError
group_id: int,
) -> bool:
async with transaction_context(self.engine, connection) as conn:
deleted: bool = await conn.scalar(
delete_tag_access_rights_stmt(tag_id=tag_id, group_id=group_id)
)
return deleted
Loading
Loading