Skip to content

Commit 4341733

Browse files
🐛storage: fixes query to file metadata (#6184)
Co-authored-by: matusdrobuliak66 <[email protected]>
1 parent 5cd7d13 commit 4341733

File tree

7 files changed

+326
-71
lines changed

7 files changed

+326
-71
lines changed

services/storage/src/simcore_service_storage/db_access_layer.py

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
from models_library.projects import ProjectID
4646
from models_library.projects_nodes_io import StorageFileID
4747
from models_library.users import GroupID, UserID
48+
from simcore_postgres_database.models.project_to_groups import project_to_groups
49+
from simcore_postgres_database.models.projects import projects
4850
from simcore_postgres_database.storage_models import file_meta_data, user_to_groups
4951

5052
logger = logging.getLogger(__name__)
@@ -112,31 +114,58 @@ def _aggregate_access_rights(
112114
return AccessRights.none()
113115

114116

117+
def assemble_array_groups(user_group_ids: list[GroupID]) -> str:
118+
return (
119+
"array[]::text[]"
120+
if len(user_group_ids) == 0
121+
else f"""array[{', '.join(f"'{group_id}'" for group_id in user_group_ids)}]"""
122+
)
123+
124+
125+
access_rights_subquery = (
126+
sa.select(
127+
project_to_groups.c.project_uuid,
128+
sa.func.jsonb_object_agg(
129+
project_to_groups.c.gid,
130+
sa.func.jsonb_build_object(
131+
"read",
132+
project_to_groups.c.read,
133+
"write",
134+
project_to_groups.c.write,
135+
"delete",
136+
project_to_groups.c.delete,
137+
),
138+
).label("access_rights"),
139+
).group_by(project_to_groups.c.project_uuid)
140+
).subquery("access_rights_subquery")
141+
142+
115143
async def list_projects_access_rights(
116144
conn: SAConnection, user_id: UserID
117145
) -> dict[ProjectID, AccessRights]:
118146
"""
119147
Returns access-rights of user (user_id) over all OWNED or SHARED projects
120148
"""
121149

122-
user_group_ids: list[int] = await _get_user_groups_ids(conn, user_id)
150+
user_group_ids: list[GroupID] = await _get_user_groups_ids(conn, user_id)
123151

124-
smt = sa.DDL(
125-
f"""\
126-
SELECT uuid, access_rights
127-
FROM projects
128-
WHERE (
129-
prj_owner = {user_id}
130-
OR jsonb_exists_any( access_rights, (
131-
SELECT ARRAY( SELECT gid::TEXT FROM user_to_groups WHERE uid = {user_id} )
152+
query = (
153+
sa.select(
154+
projects.c.uuid,
155+
access_rights_subquery.c.access_rights,
156+
)
157+
.select_from(projects.join(access_rights_subquery, isouter=True))
158+
.where(
159+
(projects.c.prj_owner == user_id)
160+
| sa.text(
161+
f"jsonb_exists_any(access_rights_subquery.access_rights, {assemble_array_groups(user_group_ids)})"
132162
)
133163
)
134164
)
135-
"""
136-
)
165+
137166
projects_access_rights = {}
138167

139-
async for row in conn.execute(smt):
168+
async for row in conn.execute(query):
140169
assert isinstance(row.access_rights, dict) # nosec
141170
assert isinstance(row.uuid, str) # nosec
142171

@@ -160,25 +189,26 @@ async def get_project_access_rights(
160189
"""
161190
Returns access-rights of user (user_id) over a project resource (project_id)
162191
"""
163-
user_group_ids: list[int] = await _get_user_groups_ids(conn, user_id)
164-
165-
stmt = sa.DDL(
166-
f"""\
167-
SELECT prj_owner, access_rights
168-
FROM projects
169-
WHERE (
170-
( uuid = '{project_id}' ) AND (
171-
prj_owner = {user_id}
172-
OR jsonb_exists_any( access_rights, (
173-
SELECT ARRAY( SELECT gid::TEXT FROM user_to_groups WHERE uid = {user_id} )
174-
)
192+
user_group_ids: list[GroupID] = await _get_user_groups_ids(conn, user_id)
193+
194+
query = (
195+
sa.select(
196+
projects.c.prj_owner,
197+
access_rights_subquery.c.access_rights,
198+
)
199+
.select_from(projects.join(access_rights_subquery, isouter=True))
200+
.where(
201+
(projects.c.uuid == f"{project_id}")
202+
& (
203+
(projects.c.prj_owner == user_id)
204+
| sa.text(
205+
f"jsonb_exists_any(access_rights_subquery.access_rights, {assemble_array_groups(user_group_ids)})"
175206
)
176207
)
177208
)
178-
"""
179209
)
180210

181-
result: ResultProxy = await conn.execute(stmt)
211+
result: ResultProxy = await conn.execute(query)
182212
row: RowProxy | None = await result.first()
183213

184214
if not row:

services/storage/src/simcore_service_storage/db_file_meta_data.py

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,7 @@ async def get(conn: SAConnection, file_id: SimcoreS3FileID) -> FileMetaDataAtDB:
6464
raise FileMetaDataNotFoundError(file_id=file_id)
6565

6666

67-
async def list_filter_with_partial_file_id(
68-
conn: SAConnection,
67+
def _list_filter_with_partial_file_id_stmt(
6968
*,
7069
user_or_project_filter: UserOrProjectFilter,
7170
file_id_prefix: str | None,
@@ -74,15 +73,21 @@ async def list_filter_with_partial_file_id(
7473
only_files: bool,
7574
limit: int | None = None,
7675
offset: int | None = None,
77-
) -> list[FileMetaDataAtDB]:
78-
conditions = []
79-
80-
# user_or_project_filter
81-
if user_id := user_or_project_filter.user_id:
82-
conditions.append(file_meta_data.c.user_id == f"{user_id}")
83-
elif project_ids := user_or_project_filter.project_ids:
84-
# Check if project_ids is not empty and add condition
85-
conditions.append(file_meta_data.c.project_id.in_(f"{_}" for _ in project_ids))
76+
):
77+
conditions: list = []
78+
79+
# Checks access rights (project can be owned or shared)
80+
user_id = user_or_project_filter.user_id
81+
if user_id is not None:
82+
project_ids = user_or_project_filter.project_ids
83+
conditions.append(
84+
sa.or_(
85+
file_meta_data.c.user_id == f"{user_id}",
86+
file_meta_data.c.project_id.in_(f"{_}" for _ in project_ids)
87+
if project_ids
88+
else False,
89+
)
90+
)
8691

8792
# Optional filters
8893
if file_id_prefix:
@@ -94,19 +99,36 @@ async def list_filter_with_partial_file_id(
9499
if sha256_checksum:
95100
conditions.append(file_meta_data.c.sha256_checksum == sha256_checksum)
96101

97-
where_clause = sa.and_(*conditions)
98-
99-
stmt = (
100-
sa.select(file_meta_data).where(where_clause)
101-
# sorted as oldest first
102-
.order_by(file_meta_data.c.created_at.asc())
102+
return (
103+
sa.select(file_meta_data)
104+
.where(sa.and_(*conditions))
105+
.order_by(file_meta_data.c.created_at.asc()) # sorted as oldest first
106+
.offset(offset)
107+
.limit(limit)
103108
)
104109

105-
# Apply limit and offset if specified
106-
if limit is not None:
107-
stmt = stmt.limit(limit)
108-
if offset is not None:
109-
stmt = stmt.offset(offset)
110+
111+
async def list_filter_with_partial_file_id(
112+
conn: SAConnection,
113+
*,
114+
user_or_project_filter: UserOrProjectFilter,
115+
file_id_prefix: str | None,
116+
partial_file_id: str | None,
117+
sha256_checksum: SHA256Str | None,
118+
only_files: bool,
119+
limit: int | None = None,
120+
offset: int | None = None,
121+
) -> list[FileMetaDataAtDB]:
122+
123+
stmt = _list_filter_with_partial_file_id_stmt(
124+
user_or_project_filter=user_or_project_filter,
125+
file_id_prefix=file_id_prefix,
126+
partial_file_id=partial_file_id,
127+
sha256_checksum=sha256_checksum,
128+
only_files=only_files,
129+
limit=limit,
130+
offset=offset,
131+
)
110132

111133
return [FileMetaDataAtDB.from_orm(row) async for row in await conn.execute(stmt)]
112134

services/storage/src/simcore_service_storage/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ def unquote(cls, v):
259259

260260

261261
class UserOrProjectFilter(NamedTuple):
262-
user_id: UserID | None
262+
user_id: UserID | None # = None disables filter
263263
project_ids: list[ProjectID]
264264

265265

services/storage/tests/fixtures/data_models.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
from pydantic import ByteSize, parse_obj_as
2222
from pytest_simcore.helpers.faker_factories import random_project, random_user
2323
from servicelib.utils import limited_gather
24+
from simcore_postgres_database.models.project_to_groups import project_to_groups
2425
from simcore_postgres_database.storage_models import projects, users
26+
from sqlalchemy.dialects.postgresql import insert as pg_insert
2527

2628
from ..helpers.utils import get_updated_project
2729

@@ -51,7 +53,13 @@ async def _user_context(aiopg_engine: Engine, *, name: str) -> AsyncIterator[Use
5153

5254
@pytest.fixture
5355
async def user_id(aiopg_engine: Engine) -> AsyncIterator[UserID]:
54-
async with _user_context(aiopg_engine, name="test") as new_user_id:
56+
async with _user_context(aiopg_engine, name="test-user") as new_user_id:
57+
yield new_user_id
58+
59+
60+
@pytest.fixture
61+
async def other_user_id(aiopg_engine: Engine) -> AsyncIterator[UserID]:
62+
async with _user_context(aiopg_engine, name="test-other-user") as new_user_id:
5563
yield new_user_id
5664

5765

@@ -83,6 +91,52 @@ async def _creator(**kwargs) -> dict[str, Any]:
8391
)
8492

8593

94+
@pytest.fixture
95+
async def create_project_access_rights(
96+
aiopg_engine: Engine,
97+
) -> AsyncIterator[Callable[[ProjectID, UserID, bool, bool, bool], Awaitable[None]]]:
98+
_created = []
99+
100+
async def _creator(
101+
project_id: ProjectID, user_id: UserID, read: bool, write: bool, delete: bool
102+
) -> None:
103+
async with aiopg_engine.acquire() as conn:
104+
result = await conn.execute(
105+
project_to_groups.insert()
106+
.values(
107+
project_uuid=f"{project_id}",
108+
gid=sa.select(users.c.primary_gid)
109+
.where(users.c.id == f"{user_id}")
110+
.scalar_subquery(),
111+
read=read,
112+
write=write,
113+
delete=delete,
114+
)
115+
.returning(sa.literal_column("*"))
116+
)
117+
row = await result.fetchone()
118+
assert row
119+
_created.append(
120+
(row[project_to_groups.c.project_uuid], row[project_to_groups.c.gid])
121+
)
122+
123+
yield _creator
124+
125+
# cleanup
126+
async with aiopg_engine.acquire() as conn:
127+
await conn.execute(
128+
project_to_groups.delete().where(
129+
sa.or_(
130+
*(
131+
(project_to_groups.c.project_uuid == pid)
132+
& (project_to_groups.c.gid == gid)
133+
for pid, gid in _created
134+
)
135+
)
136+
)
137+
)
138+
139+
86140
@pytest.fixture
87141
async def project_id(
88142
create_project: Callable[[], Awaitable[dict[str, Any]]]
@@ -142,6 +196,31 @@ async def _() -> None:
142196
.values(access_rights=access_rights)
143197
)
144198

199+
# project_to_groups needs to be updated
200+
for group_id, permissions in access_rights.items():
201+
insert_stmt = pg_insert(project_to_groups).values(
202+
project_uuid=f"{project_id}",
203+
gid=int(group_id),
204+
read=permissions["read"],
205+
write=permissions["write"],
206+
delete=permissions["delete"],
207+
created=sa.func.now(),
208+
modified=sa.func.now(),
209+
)
210+
on_update_stmt = insert_stmt.on_conflict_do_update(
211+
index_elements=[
212+
project_to_groups.c.project_uuid,
213+
project_to_groups.c.gid,
214+
],
215+
set_={
216+
"read": insert_stmt.excluded.read,
217+
"write": insert_stmt.excluded.write,
218+
"delete": insert_stmt.excluded.delete,
219+
"modified": sa.func.now(),
220+
},
221+
)
222+
await conn.execute(on_update_stmt)
223+
145224
return _
146225

147226

services/storage/tests/unit/test_access_layer.py renamed to services/storage/tests/unit/test_db_access_layer.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
# pylint:disable=unused-variable
2-
# pylint:disable=unused-argument
3-
# pylint:disable=redefined-outer-name
1+
# pylint: disable=redefined-outer-name
2+
# pylint: disable=unused-argument
3+
# pylint: disable=unused-variable
4+
# pylint: disable=too-many-arguments
45

56

6-
from typing import Iterable
7-
8-
import pytest
97
from aiopg.sa.engine import Engine
108
from models_library.projects import ProjectID
119
from models_library.users import UserID
@@ -18,13 +16,6 @@
1816
pytest_simcore_core_services_selection = ["postgres"]
1917

2018

21-
@pytest.fixture
22-
async def filemeta_id(
23-
user_id: UserID, project_id: ProjectID, aiopg_engine: Engine
24-
) -> Iterable[str]:
25-
raise NotImplementedError()
26-
27-
2819
async def test_access_rights_on_owned_project(
2920
user_id: UserID, project_id: ProjectID, aiopg_engine: Engine
3021
):

0 commit comments

Comments
 (0)