Skip to content

Commit

Permalink
Is765/tags response include access-rights (#3591)
Browse files Browse the repository at this point in the history
pcrespov authored Nov 23, 2022

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 6bce043 commit c05eddd
Showing 16 changed files with 1,082 additions and 736 deletions.
44 changes: 0 additions & 44 deletions api/specs/webserver/components/schemas/tag.yaml

This file was deleted.

78 changes: 67 additions & 11 deletions api/specs/webserver/openapi-tags.yaml
Original file line number Diff line number Diff line change
@@ -6,54 +6,110 @@ paths:
summary: List all tags for the current user
operationId: list_tags
responses:
'200':
"200":
description: List of tags
content:
application/json:
schema:
$ref: './components/schemas/tag.yaml#/TagListEnveloped'
$ref: "#/components/schemas/TagListEnveloped"
default:
$ref: './openapi.yaml#/components/responses/DefaultErrorResponse'
$ref: "./openapi.yaml#/components/responses/DefaultErrorResponse"
post:
tags:
- tag
summary: Creates a new tag
operationId: create_tag
responses:
'200':
"200":
description: The created tag
content:
application/json:
schema:
$ref: './components/schemas/tag.yaml#/TagEnveloped'
$ref: "#/components/schemas/TagEnveloped"
default:
$ref: './openapi.yaml#/components/responses/DefaultErrorResponse'
$ref: "./openapi.yaml#/components/responses/DefaultErrorResponse"
/tags/{tag_id}:
parameters:
- name: tag_id
in: path
required: true
schema:
type: integer
put:
patch:
tags:
- tag
summary: Updates a tag
operationId: update_tag
responses:
'200':
"200":
description: The updated tag
content:
application/json:
schema:
$ref: './components/schemas/tag.yaml#/TagEnveloped'
$ref: "#/components/schemas/TagEnveloped"
default:
$ref: './openapi.yaml#/components/responses/DefaultErrorResponse'
$ref: "./openapi.yaml#/components/responses/DefaultErrorResponse"
delete:
tags:
- tag
summary: Deletes an existing tag
operationId: delete_tag
responses:
'204':
"204":
description: The tag has been successfully deleted
components:
schemas:
Tag:
type: object
required:
- name
- color
- accessRights
properties:
id:
type: integer
name:
type: string
description:
type: string
color:
type: string
pattern: "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"
accessRights:
type: object
properties:
read:
type: boolean
write:
type: boolean
delete:
type: boolean

TagListEnveloped:
type: object
required:
- data
properties:
data:
type: object
required:
- tags
properties:
tags:
type: array
items:
$ref: "#/components/schemas/Tag"
error:
nullable: true
default: null

TagEnveloped:
type: object
required:
- data
properties:
data:
$ref: "#/components/schemas/Tag"
error:
nullable: true
default: null
2 changes: 1 addition & 1 deletion api/specs/webserver/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.0.0
info:
title: "osparc-simcore web API"
version: 0.10.0
version: 0.11.0
description: "API designed for the front-end app"
contact:
name: IT'IS Foundation
Original file line number Diff line number Diff line change
@@ -99,6 +99,21 @@ def downgrade():
"""
)
)
op.alter_column("tags", "user_id", nullable=False)

# WARNING: downgrade with data loss
#
# With this migration upgrade, tags can now be associated to both users AND groups.
# Therefore, if in the new table contains group tags, the downgrade CANNOT map that tag
# to a user, resulting in user_id = null.
#
# In order to reduce the data loss, if any of the tags are assigned
# to groups, the non-nullable condition of user_id is dropped in exchange
# for not deleting group tags in the tags table.
#
# We tried to catch the `sa.exc.IntegrityError`` (column "user_id" of relation "tags" contains null values)
# and relax this condition only when the data requires it but the problem is that once there is failure
# the whole transaction gets cancelled.
#
# NOTE: this context can be reproduced in services/web/server/tests/unit/with_dbs/03/tags/test_tags.py::test_read_tags

op.drop_table("tags_to_groups")
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
"""

import functools
import itertools
from dataclasses import dataclass
from typing import Optional, TypedDict

@@ -30,15 +31,34 @@ class TagOperationNotAllowed(BaseTagError): # maps to AccessForbidden
#
# Repository: interface layer over pg database
#


_TAG_COLUMNS = [
tags.c.id,
tags.c.name,
tags.c.description,
tags.c.color,
]

_ACCESS_COLUMNS = [
tags_to_groups.c.read,
tags_to_groups.c.write,
tags_to_groups.c.delete,
]


_COLUMNS = _TAG_COLUMNS + _ACCESS_COLUMNS


class TagDict(TypedDict, total=True):
# NOTE: ONLY used as returned value, otherwise used
id: int
name: int
name: str
description: str
color: str


_TAG_COLUMNS = [tags.c.id, tags.c.name, tags.c.description, tags.c.color]
# access rights
read: bool
write: bool
delete: bool


@dataclass(frozen=True)
@@ -135,36 +155,42 @@ async def create(
# insert new tag
insert_stmt = tags.insert().values(**values).returning(*_TAG_COLUMNS)
result = await conn.execute(insert_stmt)
row = await result.first()
assert row # nosec
tag = await result.first()
assert tag # nosec

# take tag ownership
scalar_subq = (
sa.select(users.c.primary_gid)
.where(users.c.id == self.user_id)
.scalar_subquery()
)
await conn.execute(
tags_to_groups.insert().values(
tag_id=row.id,
result = await conn.execute(
tags_to_groups.insert()
.values(
tag_id=tag.id,
group_id=scalar_subq,
read=read,
write=write,
delete=delete,
)
.returning(*_ACCESS_COLUMNS)
)
return TagDict(row.items()) # type: ignore
access = await result.first()
assert access

return TagDict(itertools.chain(tag.items(), access.items())) # type: ignore

async def list(self, conn: SAConnection) -> list[TagDict]:
select_stmt = (
sa.select(_TAG_COLUMNS)
sa.select(_COLUMNS)
.select_from(self._join_user_to_tags(tags_to_groups.c.read == True))
.order_by(tags.c.id)
)

return [TagDict(row.items()) async for row in conn.execute(select_stmt)] # type: ignore

async def get(self, conn: SAConnection, tag_id: int) -> TagDict:
select_stmt = sa.select(_TAG_COLUMNS).select_from(
select_stmt = sa.select(_COLUMNS).select_from(
self._join_user_to_given_tag(tags_to_groups.c.read == True, tag_id=tag_id)
)

@@ -205,7 +231,7 @@ async def update(
& (user_to_groups.c.uid == self.user_id)
)
.values(**updates)
.returning(*_TAG_COLUMNS)
.returning(*_COLUMNS)
)

result = await conn.execute(update_stmt)
140 changes: 76 additions & 64 deletions packages/postgres-database/tests/test_utils_tags.py
Original file line number Diff line number Diff line change
@@ -10,7 +10,8 @@
from aiopg.sa.connection import SAConnection
from aiopg.sa.engine import Engine
from aiopg.sa.result import RowProxy
from simcore_postgres_database.models.tags import tags, tags_to_groups
from pytest_simcore.helpers.utils_tags import create_tag, create_tag_access
from simcore_postgres_database.models.tags import tags_to_groups
from simcore_postgres_database.models.users import UserRole, UserStatus
from simcore_postgres_database.utils_tags import (
TagNotFoundError,
@@ -68,54 +69,13 @@ async def other_user(
return user_


async def _create_tag_access(
conn: SAConnection,
*,
tag_id,
group_id,
read,
write,
delete,
) -> int:
await conn.execute(
tags_to_groups.insert().values(
tag_id=tag_id, group_id=group_id, read=read, write=write, delete=delete
)
)
return tag_id


async def _create_tag(
conn: SAConnection,
*,
name,
description,
color,
group_id,
read,
write,
delete,
) -> int:
"""helper to create a tab by inserting rows in two different tables"""
tag_id = await conn.scalar(
tags.insert()
.values(name=name, description=description, color=color)
.returning(tags.c.id)
)
assert tag_id
await _create_tag_access(
conn, tag_id=tag_id, group_id=group_id, read=read, write=write, delete=delete
)
return tag_id


async def test_tags_access_with_primary_groups(
connection: SAConnection, user: RowProxy, group: RowProxy, other_user: RowProxy
):
conn = connection

(tag_id, other_tag_id) = [
await _create_tag(
await create_tag(
conn,
name="T1",
description="tag 1",
@@ -125,7 +85,7 @@ async def test_tags_access_with_primary_groups(
write=True,
delete=True,
),
await _create_tag(
await create_tag(
conn,
name="T2",
description="tag for other_user",
@@ -175,7 +135,7 @@ async def test_tags_access_with_multiple_groups(
conn = connection

(tag_id, other_tag_id, group_tag_id, everyone_tag_id) = [
await _create_tag(
await create_tag(
conn,
name="T1",
description="tag 1",
@@ -185,7 +145,7 @@ async def test_tags_access_with_multiple_groups(
write=True,
delete=True,
),
await _create_tag(
await create_tag(
conn,
name="T2",
description="tag for other_user",
@@ -195,7 +155,7 @@ async def test_tags_access_with_multiple_groups(
write=True,
delete=True,
),
await _create_tag(
await create_tag(
conn,
name="TG",
description="read-write tag shared in a GROUP ( currently only user)",
@@ -205,7 +165,7 @@ async def test_tags_access_with_multiple_groups(
write=True,
delete=False,
),
await _create_tag(
await create_tag(
conn,
name="TE",
description="read-only tag shared with EVERYONE",
@@ -244,7 +204,7 @@ async def test_tags_access_with_multiple_groups(

# now group adds read for all tags
for t in (tag_id, other_tag_id, everyone_tag_id):
await _create_tag_access(
await create_tag_access(
conn,
group_id=group.gid,
tag_id=t,
@@ -270,7 +230,7 @@ async def test_tags_repo_list_and_get(

# (2) one tag
expected_tags_ids = [
await _create_tag(
await create_tag(
conn,
name="T1",
description=f"tag for {user.id}",
@@ -288,7 +248,7 @@ async def test_tags_repo_list_and_get(

# (3) another tag via its standard group
expected_tags_ids.append(
await _create_tag(
await create_tag(
conn,
name="T2",
description="tag via std group",
@@ -304,7 +264,7 @@ async def test_tags_repo_list_and_get(
assert {t["id"] for t in listed_tags} == set(expected_tags_ids)

# (4) add another tag from a differnt user
await _create_tag(
await create_tag(
conn,
name="T3",
description=f"tag for {other_user.id}",
@@ -321,7 +281,7 @@ async def test_tags_repo_list_and_get(
assert listed_tags == prev_listed_tags

# (5) add a global tag
tag_id = await _create_tag(
tag_id = await create_tag(
conn,
name="TG",
description="tag for EVERYBODY",
@@ -334,15 +294,55 @@ async def test_tags_repo_list_and_get(

listed_tags = await tags_repo.list(conn)
assert listed_tags == [
{"id": 1, "name": "T1", "description": "tag for 1", "color": "blue"},
{"id": 2, "name": "T2", "description": "tag via std group", "color": "red"},
{"id": 4, "name": "TG", "description": "tag for EVERYBODY", "color": "pink"},
{
"id": 1,
"name": "T1",
"description": "tag for 1",
"color": "blue",
"read": True,
"write": False,
"delete": False,
},
{
"id": 2,
"name": "T2",
"description": "tag via std group",
"color": "red",
"read": True,
"write": False,
"delete": False,
},
{
"id": 4,
"name": "TG",
"description": "tag for EVERYBODY",
"color": "pink",
"read": True,
"write": False,
"delete": False,
},
]

other_repo = TagsRepo(user_id=other_user.id)
assert await other_repo.list(conn) == [
{"id": 3, "name": "T3", "description": "tag for 2", "color": "green"},
{"id": 4, "name": "TG", "description": "tag for EVERYBODY", "color": "pink"},
{
"id": 3,
"name": "T3",
"description": "tag for 2",
"color": "green",
"read": True,
"write": False,
"delete": False,
},
{
"id": 4,
"name": "TG",
"description": "tag for EVERYBODY",
"color": "pink",
"read": True,
"write": False,
"delete": False,
},
]

# exclusive to user
@@ -351,6 +351,9 @@ async def test_tags_repo_list_and_get(
"name": "T2",
"description": "tag via std group",
"color": "red",
"read": True,
"write": False,
"delete": False,
}

# exclusive ot other user
@@ -362,6 +365,9 @@ async def test_tags_repo_list_and_get(
"name": "T3",
"description": "tag for 2",
"color": "green",
"read": True,
"write": False,
"delete": False,
}

# a common tag
@@ -376,7 +382,7 @@ async def test_tags_repo_update(

# Tags with different access rights
readonly_tid, readwrite_tid, other_tid = [
await _create_tag(
await create_tag(
conn,
name="T1",
description="read only",
@@ -386,7 +392,7 @@ async def test_tags_repo_update(
write=False, # <--- read only
delete=False,
),
await _create_tag(
await create_tag(
conn,
name="T2",
description="read/write",
@@ -396,7 +402,7 @@ async def test_tags_repo_update(
write=True, # <--- can write
delete=False,
),
await _create_tag(
await create_tag(
conn,
name="T3",
description="read/write but a other user",
@@ -418,6 +424,9 @@ async def test_tags_repo_update(
"name": "T2",
"description": "modified",
"color": "green",
"read": True,
"write": True, # <--- can write
"delete": False,
}

with pytest.raises(TagOperationNotAllowed):
@@ -432,7 +441,7 @@ async def test_tags_repo_delete(

# Tags with different access rights
readonly_tid, delete_tid, other_tid = [
await _create_tag(
await create_tag(
conn,
name="T1",
description="read only",
@@ -442,7 +451,7 @@ async def test_tags_repo_delete(
write=False, # <--- read only
delete=False,
),
await _create_tag(
await create_tag(
conn,
name="T2",
description="read/write",
@@ -452,7 +461,7 @@ async def test_tags_repo_delete(
write=True,
delete=True, # <-- can delete
),
await _create_tag(
await create_tag(
conn,
name="T3",
description="read/write but a other user",
@@ -500,6 +509,9 @@ async def test_tags_repo_create(
"name": "T1",
"description": "my first tag",
"color": "pink",
"read": True,
"write": True,
"delete": True,
}

# assigned primary group
53 changes: 53 additions & 0 deletions packages/pytest-simcore/src/pytest_simcore/helpers/utils_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# pylint: disable=redefined-outer-name
# pylint: disable=unused-argument
# pylint: disable=unused-variable
# pylint: disable=too-many-arguments


from aiopg.sa.connection import SAConnection
from simcore_postgres_database.models.tags import tags, tags_to_groups


async def create_tag_access(
conn: SAConnection,
*,
tag_id,
group_id,
read,
write,
delete,
) -> int:
await conn.execute(
tags_to_groups.insert().values(
tag_id=tag_id, group_id=group_id, read=read, write=write, delete=delete
)
)
return tag_id


async def create_tag(
conn: SAConnection,
*,
name,
description,
color,
group_id,
read,
write,
delete,
) -> int:
"""helper to create a tab by inserting rows in two different tables"""
tag_id = await conn.scalar(
tags.insert()
.values(name=name, description=description, color=color)
.returning(tags.c.id)
)
assert tag_id
await create_tag_access(
conn, tag_id=tag_id, group_id=group_id, read=read, write=write, delete=delete
)
return tag_id


async def delete_tag(conn: SAConnection, tag_id: int):
await conn.execute(tags.delete().where(tags.c.id == tag_id))
Original file line number Diff line number Diff line change
@@ -712,7 +712,7 @@ qx.Class.define("osparc.data.Resources", {
url: statics.API + "/tags"
},
put: {
method: "PUT",
method: "PATCH",
url: statics.API + "/tags/{tagId}"
},
delete: {
2 changes: 1 addition & 1 deletion services/web/server/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.10.0
0.11.0
4 changes: 2 additions & 2 deletions services/web/server/setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.10.0
current_version = 0.11.0
commit = True
message = services/webserver api version: {current_version} → {new_version}
tag = False
@@ -14,6 +14,6 @@ commit_args = --no-verify
[tool:pytest]
addopts = --strict-markers
asyncio_mode = auto
markers =
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
acceptance_test: "marks tests as 'acceptance tests' i.e. does the system do what the user expects? Typically those are workflows."
1,088 changes: 549 additions & 539 deletions services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions services/web/server/src/simcore_service_webserver/db.py
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
"""

import logging
from typing import Any, AsyncIterator, Dict, Optional
from typing import Any, AsyncIterator, Optional

from aiohttp import web
from aiopg.sa import Engine, create_engine
@@ -82,13 +82,17 @@ async def is_service_responsive(app: web.Application):
return is_responsive


def get_engine_state(app: web.Application) -> Dict[str, Any]:
def get_engine_state(app: web.Application) -> dict[str, Any]:
engine: Optional[Engine] = app.get(APP_DB_ENGINE_KEY)
if engine:
return get_pg_engine_stateinfo(engine)
return {}


def get_database_engine(app: web.Application) -> Engine:
return app[APP_DB_ENGINE_KEY]


@app_module_setup(
__name__, ModuleCategory.ADDON, settings_name="WEBSERVER_DB", logger=log
)
15 changes: 1 addition & 14 deletions services/web/server/src/simcore_service_webserver/tags.py
Original file line number Diff line number Diff line change
@@ -6,14 +6,8 @@
from aiohttp import web
from servicelib.aiohttp.application_keys import APP_SETTINGS_KEY
from servicelib.aiohttp.application_setup import ModuleCategory, app_module_setup
from servicelib.aiohttp.rest_routing import (
get_handlers_from_namespace,
iter_path_operations,
map_handlers_with_operations,
)

from . import tags_handlers
from ._constants import APP_OPENAPI_SPECS_KEY

logger = logging.getLogger(__name__)

@@ -27,11 +21,4 @@
)
def setup_tags(app: web.Application):
assert app[APP_SETTINGS_KEY].WEBSERVER_TAGS # nosec
# routes
specs = app[APP_OPENAPI_SPECS_KEY]
routes = map_handlers_with_operations(
get_handlers_from_namespace(tags_handlers),
filter(lambda o: "tag" in o[3], iter_path_operations(specs)),
strict=True,
)
app.router.add_routes(routes)
app.router.add_routes(tags_handlers.routes)
174 changes: 138 additions & 36 deletions services/web/server/src/simcore_service_webserver/tags_handlers.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
import functools
from typing import Optional

from aiohttp import web
from aiopg.sa.engine import Engine
from models_library.users import UserID
from pydantic import BaseModel, Extra, Field, PositiveInt, constr
from servicelib.aiohttp.application_keys import APP_DB_ENGINE_KEY
from servicelib.aiohttp.requests_validation import (
parse_request_body_as,
parse_request_path_parameters_as,
)
from servicelib.aiohttp.typing_extension import Handler
from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON
from simcore_postgres_database.utils_tags import (
TagDict,
TagNotFoundError,
TagOperationNotAllowed,
TagsRepo,
)

from ._meta import api_version_prefix as VTAG
from .login.decorators import RQT_USERID_KEY, login_required
from .security_api import check_permission
from .security_decorators import permission_required


def _handle_tags_exceptions(handler: Handler):
@@ -20,13 +30,6 @@ async def wrapper(request: web.Request) -> web.Response:
try:
return await handler(request)

except (KeyError, TypeError, ValueError) as exc:
# NOTE: will be replaced by more robust pydantic-based validation
# Bad match_info[*] -> KeyError
# Bad int(param) -> ValueError
# Bad update(**tag_update) -> TypeError
raise web.HTTPBadRequest(reason=f"{exc}") from exc

except TagNotFoundError as exc:
raise web.HTTPNotFound(reason=f"{exc}") from exc

@@ -36,54 +39,153 @@ async def wrapper(request: web.Request) -> web.Response:
return wrapper


#
# API components/schemas
#


class RequestContext(BaseModel):
user_id: UserID = Field(..., alias=RQT_USERID_KEY)


class _InputSchema(BaseModel):
class Config:
allow_population_by_field_name = False
extra = Extra.forbid
allow_mutations = False


ColorStr = constr(regex=r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")


class TagPathParams(_InputSchema):
tag_id: PositiveInt


class TagUpdate(_InputSchema):
name: Optional[str] = None
description: Optional[str] = None
color: Optional[ColorStr] = None


class TagCreate(_InputSchema):
name: str
description: Optional[str] = None
color: ColorStr


class _OutputSchema(BaseModel):
class Config:
allow_population_by_field_name = True
extra = Extra.ignore
allow_mutations = False


class TagAccessRights(_OutputSchema):
# NOTE: analogous to GroupAccessRights
read: bool
write: bool
delete: bool


class TagGet(_OutputSchema):
id: PositiveInt
name: str
description: Optional[str] = None
color: str

# analogous to UsersGroup
access_rights: TagAccessRights = Field(..., alias="accessRights")

@classmethod
def from_db(cls, tag: TagDict) -> "TagGet":
# NOTE: cls(access_rights=tag, **tag) would also work because of Config
return cls(
id=tag["id"],
name=tag["name"],
description=tag["description"],
color=tag["color"],
accessRights=TagAccessRights(
read=tag["read"],
write=tag["write"],
delete=tag["delete"],
),
)


#
# API handlers
#

routes = web.RouteTableDef()


@routes.post(f"/{VTAG}/tags", name="create_tag")
@login_required
@permission_required("tag.crud.*")
@_handle_tags_exceptions
async def list_tags(request: web.Request):
await check_permission(request, "tag.crud.*")
uid, engine = request[RQT_USERID_KEY], request.app[APP_DB_ENGINE_KEY]
async def create_tag(request: web.Request):
engine: Engine = request.app[APP_DB_ENGINE_KEY]
req_ctx = RequestContext.parse_obj(request)
tag_data = await parse_request_body_as(TagCreate, request)

repo = TagsRepo(user_id=uid)
repo = TagsRepo(user_id=req_ctx.user_id)
async with engine.acquire() as conn:
tags = await repo.list(conn)
return tags


tag = await repo.create(
conn,
read=True,
write=True,
delete=True,
**tag_data.dict(exclude_unset=True),
)
model = TagGet.from_db(tag)
return model.dict(by_alias=True)


@routes.get(f"/{VTAG}/tags", name="list_tags")
@login_required
@permission_required("tag.crud.*")
@_handle_tags_exceptions
async def update_tag(request: web.Request):
await check_permission(request, "tag.crud.*")
uid, engine = request[RQT_USERID_KEY], request.app[APP_DB_ENGINE_KEY]
tag_id = int(request.match_info["tag_id"])
tag_data = await request.json()
async def list_tags(request: web.Request):
engine: Engine = request.app[APP_DB_ENGINE_KEY]
req_ctx = RequestContext.parse_obj(request)

repo = TagsRepo(user_id=uid)
repo = TagsRepo(user_id=req_ctx.user_id)
async with engine.acquire() as conn:
tag = await repo.update(conn, tag_id, **tag_data)
return tag
tags = await repo.list(conn)
return [TagGet.from_db(t).dict(by_alias=True) for t in tags]


@routes.patch(f"/{VTAG}/tags/{{tag_id}}", name="update_tag")
@login_required
@permission_required("tag.crud.*")
@_handle_tags_exceptions
async def create_tag(request: web.Request):
await check_permission(request, "tag.crud.*")
uid, engine = request[RQT_USERID_KEY], request.app[APP_DB_ENGINE_KEY]
tag_data = await request.json()
async def update_tag(request: web.Request):
engine: Engine = request.app[APP_DB_ENGINE_KEY]
req_ctx = RequestContext.parse_obj(request)
query_params = parse_request_path_parameters_as(TagPathParams, request)
tag_data = await parse_request_body_as(TagUpdate, request)

repo = TagsRepo(user_id=uid)
repo = TagsRepo(user_id=req_ctx.user_id)
async with engine.acquire() as conn:
tag = await repo.create(conn, read=True, write=True, delete=True, **tag_data)
return tag
tag = await repo.update(
conn, query_params.tag_id, **tag_data.dict(exclude_unset=True)
)
model = TagGet.from_db(tag)
return model.dict(by_alias=True)


@routes.delete(f"/{VTAG}/tags/{{tag_id}}", name="delete_tag")
@login_required
@permission_required("tag.crud.*")
@_handle_tags_exceptions
async def delete_tag(request: web.Request):
await check_permission(request, "tag.crud.*")
uid, engine = request[RQT_USERID_KEY], request.app[APP_DB_ENGINE_KEY]
tag_id = int(request.match_info["tag_id"])
engine: Engine = request.app[APP_DB_ENGINE_KEY]
req_ctx = RequestContext.parse_obj(request)
query_params = parse_request_path_parameters_as(TagPathParams, request)

repo = TagsRepo(user_id=uid)
repo = TagsRepo(user_id=req_ctx.user_id)
async with engine.acquire() as conn:
await repo.delete(conn, tag_id=tag_id)
await repo.delete(conn, tag_id=query_params.tag_id)

raise web.HTTPNoContent(content_type=MIMETYPE_APPLICATION_JSON)
3 changes: 3 additions & 0 deletions services/web/server/tests/integration/02/test_rabbit.py
Original file line number Diff line number Diff line change
@@ -389,6 +389,7 @@ async def rabbit_exchanges(
]


@pytest.mark.flaky(max_runs=3)
@pytest.mark.parametrize("user_role", USER_ROLES)
async def test_publish_to_other_user(
not_logged_user_id: UserID,
@@ -416,6 +417,7 @@ async def test_publish_to_other_user(
socketio_subscriber_handlers.mock_event.assert_not_called()


@pytest.mark.flaky(max_runs=3)
@pytest.mark.parametrize("user_role", USER_ROLES)
async def test_publish_to_user(
logged_user: UserInfoDict,
@@ -456,6 +458,7 @@ async def test_publish_to_user(


@pytest.mark.parametrize("user_role", USER_ROLES)
@pytest.mark.flaky(max_runs=3)
async def test_publish_about_users_project(
logged_user: UserInfoDict,
user_project: dict[str, Any],
134 changes: 128 additions & 6 deletions services/web/server/tests/unit/with_dbs/03/tags/test_tags.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# pylint:disable=unused-variable
# pylint:disable=unused-argument
# pylint:disable=redefined-outer-name
# pylint: disable=redefined-outer-name
# pylint: disable=unused-argument
# pylint: disable=unused-variable
# pylint: disable=too-many-arguments


from pathlib import Path
from typing import Any, Callable
from typing import Any, AsyncIterator, Callable

import pytest
from aiohttp import web
from aiohttp.test_utils import TestClient
from faker import Faker
from models_library.projects_state import (
ProjectLocked,
ProjectRunningState,
@@ -17,14 +18,39 @@
RunningState,
)
from models_library.utils.fastapi_encoders import jsonable_encoder
from openapi_core.schema.specs.models import Spec as OpenApiSpecs
from pytest_simcore.helpers.utils_assert import assert_status
from pytest_simcore.helpers.utils_login import UserInfoDict
from pytest_simcore.helpers.utils_projects import assert_get_same_project
from pytest_simcore.helpers.utils_tags import create_tag, delete_tag
from simcore_service_webserver import tags_handlers
from simcore_service_webserver._meta import api_version_prefix
from simcore_service_webserver.db import get_database_engine
from simcore_service_webserver.db_models import UserRole


@pytest.mark.parametrize(
"route",
tags_handlers.routes,
ids=lambda r: f"{r.method.upper()} {r.path}",
)
def test_tags_route_against_openapi_specs(route, openapi_specs: OpenApiSpecs):

assert route.path.startswith(f"/{api_version_prefix}")
path = route.path.replace(f"/{api_version_prefix}", "")

assert (
route.method.lower() in openapi_specs.paths[path].operations
), f"operation {route.method} undefined in OAS"

assert (
openapi_specs.paths[path].operations[route.method.lower()].operation_id
== route.kwargs["name"]
), "route's name differs from OAS operation_id"


@pytest.fixture
def fake_tags(fake_data_dir: Path) -> list[dict[str, Any]]:
def fake_tags(faker: Faker) -> list[dict[str, Any]]:
return [
{"name": "tag1", "description": "description1", "color": "#f00"},
{"name": "tag2", "description": "description2", "color": "#00f"},
@@ -98,3 +124,99 @@ async def test_tags_to_studies(
url = client.app.router["delete_tag"].url_for(tag_id=str(added_tags[1].get("id")))
resp = await client.delete(f"{url}")
await assert_status(resp, web.HTTPNoContent)


@pytest.fixture
async def everybody_tag_id(client: TestClient) -> AsyncIterator[int]:
assert client.app
engine = get_database_engine(client.app)
assert engine

async with engine.acquire() as conn:
tag_id = await create_tag(
conn,
name="TG",
description="tag for EVERYBODY",
color="#f00",
group_id=1,
read=True, # <--- READ ONLY
write=False,
delete=False,
)

yield tag_id

await delete_tag(conn, tag_id=tag_id)


@pytest.fixture
def user_role() -> UserRole:
return UserRole.USER


async def test_read_tags(
client: TestClient,
logged_user: UserInfoDict,
user_role: UserRole,
everybody_tag_id: int,
):
assert client.app

assert user_role == UserRole.USER

url = client.app.router["list_tags"].url_for()
resp = await client.get(f"{url}")
datas, _ = await assert_status(resp, web.HTTPOk)

assert datas == [
{
"id": everybody_tag_id,
"name": "TG",
"description": "tag for EVERYBODY",
"color": "#f00",
"accessRights": {"read": True, "write": False, "delete": False},
}
]


async def test_create_and_update_tags(
client: TestClient,
logged_user: UserInfoDict,
user_role: UserRole,
everybody_tag_id: int,
):
assert client.app

assert user_role == UserRole.USER

resp = await client.post(
f"{client.app.router['create_tag'].url_for()}",
json={"name": "T", "color": "#f00"},
)
created, _ = await assert_status(resp, web.HTTPOk)

assert created == {
"id": created["id"],
"name": "T",
"description": None,
"color": "#f00",
"accessRights": {"read": True, "write": True, "delete": True},
}

url = client.app.router["update_tag"].url_for(tag_id="2")
resp = await client.patch(
f"{url}",
json={"description": "This is my tag"},
)

updated, _ = await assert_status(resp, web.HTTPOk)
created.update(description="This is my tag")
assert updated == created

url = client.app.router["update_tag"].url_for(tag_id=f"{everybody_tag_id}")
resp = await client.patch(
f"{url}",
json={"description": "I have NO WRITE ACCESS TO THIS TAG"},
)
_, error = await assert_status(resp, web.HTTPUnauthorized)
assert error

0 comments on commit c05eddd

Please sign in to comment.