Skip to content

Commit 35f22ea

Browse files
authored
Merge branch 'master' into enh/servicelib
2 parents acd73d2 + 1999416 commit 35f22ea

File tree

4 files changed

+172
-20
lines changed

4 files changed

+172
-20
lines changed

services/storage/src/simcore_service_storage/simcore_s3_dsm.py

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ async def list_files( # noqa C901
223223

224224
async def get_file(self, user_id: UserID, file_id: StorageFileID) -> FileMetaData:
225225
async with self.engine.acquire() as conn, conn.begin():
226-
can: AccessRights | None = await get_file_access_rights(
226+
can: AccessRights = await get_file_access_rights(
227227
conn, int(user_id), file_id
228228
)
229229
if can.read:
@@ -249,9 +249,7 @@ async def create_file_upload_links(
249249
is_directory: bool,
250250
) -> UploadLinks:
251251
async with self.engine.acquire() as conn, conn.begin() as transaction:
252-
can: AccessRights | None = await get_file_access_rights(
253-
conn, user_id, file_id
254-
)
252+
can: AccessRights = await get_file_access_rights(conn, user_id, file_id)
255253
if not can.write:
256254
raise FileAccessRightError(access_right="write", file_id=file_id)
257255

@@ -263,7 +261,14 @@ async def create_file_upload_links(
263261
)
264262

265263
# ensure file is deleted first in case it already exists
266-
await self.delete_file(user_id=user_id, file_id=file_id)
264+
await self.delete_file(
265+
user_id=user_id,
266+
file_id=file_id,
267+
# NOTE: bypassing check since the project access rights don't play well
268+
# with collaborators
269+
# SEE https://github.com/ITISFoundation/osparc-simcore/issues/5159
270+
enforce_access_rights=False,
271+
)
267272

268273
# initiate the file meta data table
269274
fmd = await self._create_fmd_for_upload(
@@ -331,7 +336,7 @@ async def abort_file_upload(
331336
file_id: StorageFileID,
332337
) -> None:
333338
async with self.engine.acquire() as conn, conn.begin():
334-
can: AccessRights | None = await get_file_access_rights(
339+
can: AccessRights = await get_file_access_rights(
335340
conn, int(user_id), file_id
336341
)
337342
if not can.delete or not can.write:
@@ -367,7 +372,7 @@ async def complete_file_upload(
367372
uploaded_parts: list[UploadedPart],
368373
) -> FileMetaData:
369374
async with self.engine.acquire() as conn:
370-
can: AccessRights | None = await get_file_access_rights(
375+
can: AccessRights = await get_file_access_rights(
371376
conn, int(user_id), file_id
372377
)
373378
if not can.write:
@@ -427,9 +432,7 @@ async def create_file_download_link(
427432
async def __ensure_read_access_rights(
428433
conn: SAConnection, user_id: UserID, storage_file_id: StorageFileID
429434
) -> None:
430-
can: AccessRights | None = await get_file_access_rights(
431-
conn, user_id, storage_file_id
432-
)
435+
can: AccessRights = await get_file_access_rights(conn, user_id, storage_file_id)
433436
if not can.read:
434437
# NOTE: this is tricky. A user with read access can download and data!
435438
# If write permission would be required, then shared projects as views cannot
@@ -486,13 +489,29 @@ async def _get_link_for_directory_fmd(
486489
raise S3KeyNotFoundError(key=file_id, bucket=self.simcore_bucket_name)
487490
return await self.__get_link(parse_obj_as(SimcoreS3FileID, file_id), link_type)
488491

489-
async def delete_file(self, user_id: UserID, file_id: StorageFileID):
492+
async def delete_file(
493+
self,
494+
user_id: UserID,
495+
file_id: StorageFileID,
496+
*,
497+
enforce_access_rights: bool = True,
498+
):
499+
# _ _ ___ _____ _____
500+
# | \ | |/ _ \_ _| ____|
501+
# | \| | | | || | | _|
502+
# | |\ | |_| || | | |___
503+
# |_| \_|\___/ |_| |_____|
504+
# NOTE: (a very big one)
505+
# `enforce_access_rights` is set to False because permissions are based on "project access rights"
506+
# they should be based on data access rights (which is currently not present)
507+
# Only use this in those circumstances where a collaborator requires to delete a file (the current
508+
# permissions model will not allow him to do so, even though this is a legitimate action)
509+
# SEE https://github.com/ITISFoundation/osparc-simcore/issues/5159
490510
async with self.engine.acquire() as conn, conn.begin():
491-
can: AccessRights | None = await get_file_access_rights(
492-
conn, user_id, file_id
493-
)
494-
if not can.delete:
495-
raise FileAccessRightError(access_right="delete", file_id=file_id)
511+
if enforce_access_rights:
512+
can: AccessRights = await get_file_access_rights(conn, user_id, file_id)
513+
if not can.delete:
514+
raise FileAccessRightError(access_right="delete", file_id=file_id)
496515

497516
with suppress(FileMetaDataNotFoundError):
498517
file: FileMetaDataAtDB = await db_file_meta_data.get(

services/storage/tests/fixtures/data_models.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
# pylint:disable=redefined-outer-name
44

55

6+
from contextlib import asynccontextmanager
67
from typing import Any, AsyncIterator, Awaitable, Callable
78

89
import pytest
910
import sqlalchemy as sa
11+
from aiopg.sa.connection import SAConnection
1012
from aiopg.sa.engine import Engine
1113
from faker import Faker
1214
from models_library.projects import ProjectID
@@ -16,16 +18,16 @@
1618
from simcore_postgres_database.storage_models import projects, users
1719

1820

19-
@pytest.fixture
20-
async def user_id(aiopg_engine: Engine) -> AsyncIterator[UserID]:
21+
@asynccontextmanager
22+
async def user_context(aiopg_engine: Engine, *, name: str) -> UserID:
2123
# inject a random user in db
2224

2325
# NOTE: Ideally this (and next fixture) should be done via webserver API but at this point
2426
# in time, the webserver service would bring more dependencies to other services
2527
# which would turn this test too complex.
2628

2729
# pylint: disable=no-value-for-parameter
28-
stmt = users.insert().values(**random_user(name="test")).returning(users.c.id)
30+
stmt = users.insert().values(**random_user(name=name)).returning(users.c.id)
2931
print(str(stmt))
3032
async with aiopg_engine.acquire() as conn:
3133
result = await conn.execute(stmt)
@@ -38,6 +40,12 @@ async def user_id(aiopg_engine: Engine) -> AsyncIterator[UserID]:
3840
await conn.execute(users.delete().where(users.c.id == row.id))
3941

4042

43+
@pytest.fixture
44+
async def user_id(aiopg_engine: Engine) -> AsyncIterator[UserID]:
45+
async with user_context(aiopg_engine, name="test") as new_user_id:
46+
yield new_user_id
47+
48+
4149
@pytest.fixture
4250
async def create_project(
4351
user_id: UserID, aiopg_engine: Engine
@@ -74,6 +82,59 @@ async def project_id(
7482
return ProjectID(project["uuid"])
7583

7684

85+
@pytest.fixture
86+
async def collaborator_id(aiopg_engine: Engine) -> AsyncIterator[UserID]:
87+
async with user_context(aiopg_engine, name="collaborator") as new_user_id:
88+
yield new_user_id
89+
90+
91+
@pytest.fixture
92+
def share_with_collaborator(
93+
aiopg_engine: Engine,
94+
collaborator_id: UserID,
95+
user_id: UserID,
96+
project_id: ProjectID,
97+
) -> Callable[[], Awaitable[None]]:
98+
async def _get_user_group(conn: SAConnection, query_user: int) -> int:
99+
result = await conn.execute(
100+
sa.select(users.c.primary_gid).where(users.c.id == query_user)
101+
)
102+
row = await result.fetchone()
103+
assert row
104+
primary_gid: int = row[users.c.primary_gid]
105+
return primary_gid
106+
107+
async def _() -> None:
108+
async with aiopg_engine.acquire() as conn:
109+
result = await conn.execute(
110+
sa.select(projects.c.access_rights).where(
111+
projects.c.uuid == f"{project_id}"
112+
)
113+
)
114+
row = await result.fetchone()
115+
assert row
116+
access_rights: dict[str, Any] = row[projects.c.access_rights]
117+
118+
access_rights[await _get_user_group(conn, user_id)] = {
119+
"read": True,
120+
"write": True,
121+
"delete": True,
122+
}
123+
access_rights[await _get_user_group(conn, collaborator_id)] = {
124+
"read": True,
125+
"write": True,
126+
"delete": False,
127+
}
128+
129+
await conn.execute(
130+
projects.update()
131+
.where(projects.c.uuid == f"{project_id}")
132+
.values(access_rights=access_rights)
133+
)
134+
135+
return _
136+
137+
77138
@pytest.fixture
78139
async def create_project_node(
79140
user_id: UserID, aiopg_engine: Engine, faker: Faker

services/storage/tests/unit/test_dsm_dsmcleaner.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
from pytest_simcore.helpers.utils_parametrizations import byte_size_ids
2424
from simcore_postgres_database.storage_models import file_meta_data
2525
from simcore_service_storage import db_file_meta_data
26-
from simcore_service_storage.exceptions import FileMetaDataNotFoundError
26+
from simcore_service_storage.exceptions import (
27+
FileAccessRightError,
28+
FileMetaDataNotFoundError,
29+
)
2730
from simcore_service_storage.models import (
2831
FileMetaData,
2932
MultiPartUploadLinks,
@@ -86,6 +89,72 @@ async def test_clean_expired_uploads_aborts_dangling_multipart_uploads(
8689
assert not await storage_s3_client.list_ongoing_multipart_uploads(storage_s3_bucket)
8790

8891

92+
@pytest.mark.parametrize(
93+
"file_size",
94+
[ByteSize(0), parse_obj_as(ByteSize, "10Mib"), parse_obj_as(ByteSize, "100Mib")],
95+
ids=byte_size_ids,
96+
)
97+
@pytest.mark.parametrize(
98+
"link_type, is_directory",
99+
[
100+
# NOTE: directories are handled only as LinkType.S3
101+
(LinkType.S3, True),
102+
(LinkType.S3, False),
103+
(LinkType.PRESIGNED, False),
104+
],
105+
)
106+
@pytest.mark.parametrize("checksum", [None, _faker.sha256()])
107+
async def test_regression_collaborator_creates_file_upload_links(
108+
disabled_dsm_cleaner_task,
109+
aiopg_engine: Engine,
110+
simcore_s3_dsm: SimcoreS3DataManager,
111+
simcore_file_id: SimcoreS3FileID,
112+
simcore_directory_id: SimcoreS3FileID,
113+
user_id: UserID,
114+
link_type: LinkType,
115+
file_size: ByteSize,
116+
is_directory: bool,
117+
storage_s3_client: StorageS3Client,
118+
storage_s3_bucket: S3BucketName,
119+
checksum: SHA256Str | None,
120+
collaborator_id: UserID,
121+
share_with_collaborator: Callable[[], Awaitable[None]],
122+
):
123+
file_or_directory_id = simcore_directory_id if is_directory else simcore_file_id
124+
125+
await simcore_s3_dsm.create_file_upload_links(
126+
user_id,
127+
file_or_directory_id,
128+
link_type,
129+
file_size,
130+
sha256_checksum=checksum,
131+
is_directory=is_directory,
132+
)
133+
134+
# collaborators don't have access
135+
with pytest.raises(FileAccessRightError):
136+
await simcore_s3_dsm.create_file_upload_links(
137+
collaborator_id,
138+
file_or_directory_id,
139+
link_type,
140+
file_size,
141+
sha256_checksum=checksum,
142+
is_directory=is_directory,
143+
)
144+
145+
await share_with_collaborator()
146+
147+
# collaborator have access
148+
await simcore_s3_dsm.create_file_upload_links(
149+
collaborator_id,
150+
file_or_directory_id,
151+
link_type,
152+
file_size,
153+
sha256_checksum=checksum,
154+
is_directory=is_directory,
155+
)
156+
157+
89158
@pytest.mark.parametrize(
90159
"file_size",
91160
[ByteSize(0), parse_obj_as(ByteSize, "10Mib"), parse_obj_as(ByteSize, "100Mib")],

services/web/server/tests/integration/02/scicrunch/test_scicrunch__resolver.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
from simcore_service_webserver.scicrunch.settings import SciCrunchSettings
1717

1818

19+
@pytest.mark.skip(
20+
"requires a fix see case https://github.com/ITISFoundation/osparc-simcore/issues/5160"
21+
)
1922
@pytest.mark.parametrize(
2023
"name,rrid",
2124
TOOL_CITATIONS + ANTIBODY_CITATIONS + PLAMID_CITATIONS + ORGANISM_CITATIONS,

0 commit comments

Comments
 (0)