Skip to content

🐛 adjust storage user project permission based on new logic with workspaces #6337

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

90 changes: 83 additions & 7 deletions services/storage/src/simcore_service_storage/db_access_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
from models_library.users import GroupID, UserID
from simcore_postgres_database.models.project_to_groups import project_to_groups
from simcore_postgres_database.models.projects import projects
from simcore_postgres_database.models.workspaces_access_rights import (
workspaces_access_rights,
)
from simcore_postgres_database.storage_models import file_meta_data, user_to_groups

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -142,6 +145,26 @@ def assemble_array_groups(user_group_ids: list[GroupID]) -> str:
).subquery("access_rights_subquery")


workspace_access_rights_subquery = (
sa.select(
workspaces_access_rights.c.workspace_id,
sa.func.jsonb_object_agg(
workspaces_access_rights.c.gid,
sa.func.jsonb_build_object(
"read",
workspaces_access_rights.c.read,
"write",
workspaces_access_rights.c.write,
"delete",
workspaces_access_rights.c.delete,
),
)
.filter(workspaces_access_rights.c.read)
.label("access_rights"),
).group_by(workspaces_access_rights.c.workspace_id)
).subquery("workspace_access_rights_subquery")


async def list_projects_access_rights(
conn: SAConnection, user_id: UserID
) -> dict[ProjectID, AccessRights]:
Expand All @@ -151,23 +174,50 @@ async def list_projects_access_rights(

user_group_ids: list[GroupID] = await _get_user_groups_ids(conn, user_id)

query = (
private_workspace_query = (
sa.select(
projects.c.uuid,
access_rights_subquery.c.access_rights,
)
.select_from(projects.join(access_rights_subquery, isouter=True))
.where(
(projects.c.prj_owner == user_id)
| sa.text(
f"jsonb_exists_any(access_rights_subquery.access_rights, {assemble_array_groups(user_group_ids)})"
(
(projects.c.prj_owner == user_id)
| sa.text(
f"jsonb_exists_any(access_rights_subquery.access_rights, {assemble_array_groups(user_group_ids)})"
)
)
& (projects.c.workspace_id.is_(None))
)
)

shared_workspace_query = (
sa.select(
projects.c.uuid,
workspace_access_rights_subquery.c.access_rights,
)
.select_from(
projects.join(
workspace_access_rights_subquery,
projects.c.workspace_id
== workspace_access_rights_subquery.c.workspace_id,
)
)
.where(
(
sa.text(
f"jsonb_exists_any(workspace_access_rights_subquery.access_rights, {assemble_array_groups(user_group_ids)})"
)
)
& (projects.c.workspace_id.is_not(None))
)
)

combined_query = sa.union_all(private_workspace_query, shared_workspace_query)

projects_access_rights = {}

async for row in conn.execute(query):
async for row in conn.execute(combined_query):
assert isinstance(row.access_rights, dict) # nosec
assert isinstance(row.uuid, str) # nosec

Expand All @@ -193,7 +243,7 @@ async def get_project_access_rights(
"""
user_group_ids: list[GroupID] = await _get_user_groups_ids(conn, user_id)

query = (
private_workspace_query = (
sa.select(
projects.c.prj_owner,
access_rights_subquery.c.access_rights,
Expand All @@ -207,10 +257,36 @@ async def get_project_access_rights(
f"jsonb_exists_any(access_rights_subquery.access_rights, {assemble_array_groups(user_group_ids)})"
)
)
& (projects.c.workspace_id.is_(None))
)
)

shared_workspace_query = (
sa.select(
projects.c.prj_owner,
workspace_access_rights_subquery.c.access_rights,
)
.select_from(
projects.join(
workspace_access_rights_subquery,
projects.c.workspace_id
== workspace_access_rights_subquery.c.workspace_id,
)
)
.where(
(projects.c.uuid == f"{project_id}")
& (
sa.text(
f"jsonb_exists_any(workspace_access_rights_subquery.access_rights, {assemble_array_groups(user_group_ids)})"
)
)
& (projects.c.workspace_id.is_not(None))
)
)

result: ResultProxy = await conn.execute(query)
combined_query = sa.union_all(private_workspace_query, shared_workspace_query)

result: ResultProxy = await conn.execute(combined_query)
row: RowProxy | None = await result.first()

if not row:
Expand Down
55 changes: 55 additions & 0 deletions services/storage/tests/unit/test_db_access_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@
# pylint: disable=too-many-arguments


import pytest
import sqlalchemy as sa
from aiopg.sa.engine import Engine
from models_library.projects import ProjectID
from models_library.users import UserID
from simcore_postgres_database.models.projects import projects
from simcore_postgres_database.models.users import users
from simcore_postgres_database.models.workspaces import workspaces
from simcore_service_storage.db_access_layer import (
AccessRights,
get_file_access_rights,
Expand All @@ -28,3 +33,53 @@ async def test_access_rights_on_owned_project(
conn, user_id, f"{project_id}/node_id/not-in-file-metadata-table.txt"
)
assert access == AccessRights.all()


@pytest.fixture
async def prepare_db(user_id: UserID, project_id: ProjectID, aiopg_engine: Engine):
async with aiopg_engine.acquire() as conn:
result = await conn.execute(
sa.select(users.c.primary_gid).where(users.c.id == user_id)
)
row = await result.first()
user_primary_id = row[0]

result = await conn.execute(
workspaces.insert()
.values(
name="test",
description=None,
owner_primary_gid=user_primary_id,
thumbnail=None,
created=sa.func.now(),
modified=sa.func.now(),
product_name="osparc",
)
.returning(workspaces.c.workspace_id)
)
row = await result.first()
workspace_id = row[0]

await conn.execute(
projects.update()
.values(workspace_id=workspace_id)
.where(projects.c.uuid == f"{project_id}")
)

yield

await conn.execute(workspaces.delete())


async def test_access_rights_based_on_workspace(
user_id: UserID, project_id: ProjectID, aiopg_engine: Engine, prepare_db
):
async with aiopg_engine.acquire() as conn:
access = await get_project_access_rights(conn, user_id, project_id)
assert access == AccessRights.all()

# still NOT registered in file_meta_data BUT with prefix {project_id} owned by user
access = await get_file_access_rights(
conn, user_id, f"{project_id}/node_id/not-in-file-metadata-table.txt"
)
assert access == AccessRights.all()
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,6 @@ async def list_projects( # pylint: disable=too-many-arguments

async with self.engine.acquire() as conn:

# if workspace_is_private:
access_rights_subquery = (
sa.select(
project_to_groups.c.project_uuid,
Expand All @@ -373,29 +372,6 @@ async def list_projects( # pylint: disable=too-many-arguments
.label("access_rights"),
).group_by(project_to_groups.c.project_uuid)
).subquery("access_rights_subquery")
# else:
# access_rights_subquery = (
# sa.select(
# workspaces_access_rights.c.workspace_id,
# sa.func.jsonb_object_agg(
# workspaces_access_rights.c.gid,
# sa.func.jsonb_build_object(
# "read",
# workspaces_access_rights.c.read,
# "write",
# workspaces_access_rights.c.write,
# "delete",
# workspaces_access_rights.c.delete,
# ),
# )
# .filter(
# workspaces_access_rights.c.read # Filters out entries where "read" is False
# )
# .label("access_rights"),
# )
# .where(workspaces_access_rights.c.workspace_id == workspace_id)
# .group_by(workspaces_access_rights.c.workspace_id)
# ).subquery("access_rights_subquery")

_join_query = (
projects.join(projects_to_products, isouter=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,9 @@ async def get_workspace_for_user(
access_rights_subquery.c.access_rights,
my_access_rights_subquery.c.my_access_rights,
)
.select_from(workspaces.join(my_access_rights_subquery))
.select_from(
workspaces.join(access_rights_subquery).join(my_access_rights_subquery)
)
.where(
(workspaces.c.workspace_id == workspace_id)
& (workspaces.c.product_name == product_name)
Expand Down
Loading