Skip to content

Commit 4d36014

Browse files
committed
added permission and test to remove CIA issue
1 parent a85b3a8 commit 4d36014

File tree

3 files changed

+113
-85
lines changed

3 files changed

+113
-85
lines changed

services/web/server/src/simcore_service_webserver/security/_access_roles.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class PermissionDict(TypedDict, total=False):
3535
"project.update",
3636
"storage.locations.*",
3737
"storage.files.*",
38+
"user.notifications.read",
39+
"user.notifications.update",
3840
"groups.read",
3941
"project.open",
4042
"project.read",
@@ -63,6 +65,7 @@ class PermissionDict(TypedDict, total=False):
6365
"project.tag.*",
6466
"user.profile.update",
6567
"user.apikey.*",
68+
"user.notifications.write",
6669
"user.tokens.*",
6770
"groups.*",
6871
"tag.crud.*",

services/web/server/src/simcore_service_webserver/users/_handlers.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ async def _get_user_notifications(
147147

148148
@routes.get(f"/{API_VTAG}/me/notifications", name="list_user_notifications")
149149
@login_required
150+
@permission_required("user.notifications.read")
150151
async def list_user_notifications(request: web.Request) -> web.Response:
151152
redis_client = get_redis_user_notifications_client(request.app)
152153
req_ctx = _RequestContext.parse_obj(request)
@@ -156,6 +157,7 @@ async def list_user_notifications(request: web.Request) -> web.Response:
156157

157158
@routes.post(f"/{API_VTAG}/me/notifications", name="create_user_notification")
158159
@login_required
160+
@permission_required("user.notifications.write")
159161
async def create_user_notification(request: web.Request) -> web.Response:
160162
req_ctx = _RequestContext.parse_obj(request)
161163
# body includes the updated notification
@@ -178,9 +180,11 @@ class _NotificationPathParams(BaseModel):
178180

179181

180182
@routes.patch(
181-
f"/{API_VTAG}/notifications/{{notification_id}}", name="mark_notification_as_read"
183+
f"/{API_VTAG}/me/notifications/{{notification_id}}",
184+
name="mark_notification_as_read",
182185
)
183186
@login_required
187+
@permission_required("user.notifications.update")
184188
async def mark_notification_as_read(request: web.Request) -> web.Response:
185189
redis_client = get_redis_user_notifications_client(request.app)
186190
req_ctx = _RequestContext.parse_obj(request)

services/web/server/tests/unit/with_dbs/03/test_users.py

Lines changed: 105 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
MAX_NOTIFICATIONS_FOR_USER_TO_SHOW,
5353
NotificationCategory,
5454
UserNotification,
55+
UserNotificationCreate,
5556
get_notification_key,
5657
)
5758
from simcore_service_webserver.users.plugin import setup_users
@@ -229,6 +230,7 @@ async def test_create_token(
229230
logged_user: UserInfoDict,
230231
tokens_db,
231232
expected: type[web.HTTPException],
233+
faker: Faker,
232234
):
233235
assert client.app
234236

@@ -237,8 +239,8 @@ async def test_create_token(
237239

238240
token = {
239241
"service": "pennsieve",
240-
"token_key": "4k9lyzBTS",
241-
"token_secret": "my secret",
242+
"token_key": faker.uuid4(),
243+
"token_secret": faker.uuid4(),
242244
}
243245

244246
resp = await client.post(f"{url}", json=token)
@@ -288,46 +290,6 @@ async def test_read_token(
288290
assert data == expected_token, "list and read item are both read operations"
289291

290292

291-
@pytest.mark.parametrize(
292-
"user_role,expected",
293-
[
294-
(UserRole.ANONYMOUS, web.HTTPUnauthorized),
295-
(UserRole.GUEST, web.HTTPForbidden),
296-
(UserRole.USER, web.HTTPNoContent),
297-
(UserRole.TESTER, web.HTTPNoContent),
298-
],
299-
)
300-
async def test_update_token(
301-
client: TestClient,
302-
logged_user: UserInfoDict,
303-
tokens_db,
304-
fake_tokens,
305-
expected: type[web.HTTPException],
306-
):
307-
assert client.app
308-
309-
selected = random.choice(fake_tokens)
310-
sid = selected["service"]
311-
312-
url = client.app.router["get_token"].url_for(service=sid)
313-
assert "/v0/me/tokens/%s" % sid == f"{url}"
314-
315-
resp = await client.put(
316-
f"{url}", json={"token_secret": "some completely new secret"}
317-
)
318-
data, error = await assert_status(resp, expected)
319-
320-
if not error:
321-
# check in db
322-
token_in_db = await get_token_from_db(tokens_db, token_service=sid)
323-
assert token_in_db
324-
assert token_in_db["token_data"]["token_secret"] == "some completely new secret"
325-
assert token_in_db["token_data"]["token_secret"] != selected["token_secret"]
326-
327-
selected["token_secret"] = "some completely new secret"
328-
assert token_in_db["token_data"] == selected
329-
330-
331293
@pytest.mark.parametrize(
332294
"user_role,expected",
333295
[
@@ -401,7 +363,7 @@ async def test_get_profile_with_failing_db_connection(
401363
mock_failing_connection.call_count == NUM_RETRY
402364
), "Expected mock failure raised in AuthorizationPolicy.authorized_userid after severals"
403365

404-
data, error = await assert_status(resp, expected)
366+
await assert_status(resp, expected)
405367

406368

407369
@pytest.fixture
@@ -423,14 +385,16 @@ async def _create_notifications(
423385

424386
user_notifications: list[UserNotification] = [
425387
UserNotification.create_from_request_data(
426-
{
427-
"user_id": user_id,
428-
"category": random.choice(notification_categories),
429-
"actionable_path": "a/path",
430-
"title": "test_title",
431-
"text": "text_text",
432-
"date": datetime.now(timezone.utc).isoformat(),
433-
}
388+
UserNotificationCreate.parse_obj(
389+
{
390+
"user_id": user_id,
391+
"category": random.choice(notification_categories),
392+
"actionable_path": "a/path",
393+
"title": "test_title",
394+
"text": "text_text",
395+
"date": datetime.now(timezone.utc).isoformat(),
396+
}
397+
)
434398
)
435399
for _ in range(count)
436400
]
@@ -445,7 +409,15 @@ async def _create_notifications(
445409
await redis_client.flushall()
446410

447411

448-
@pytest.mark.parametrize("user_role", [UserRole.USER])
412+
@pytest.mark.parametrize(
413+
"user_role,expected_response",
414+
[
415+
(UserRole.ANONYMOUS, web.HTTPUnauthorized),
416+
(UserRole.GUEST, web.HTTPOk),
417+
(UserRole.USER, web.HTTPOk),
418+
(UserRole.TESTER, web.HTTPOk),
419+
],
420+
)
449421
@pytest.mark.parametrize(
450422
"notification_count",
451423
[
@@ -455,30 +427,44 @@ async def _create_notifications(
455427
MAX_NOTIFICATIONS_FOR_USER_TO_SHOW + 1,
456428
],
457429
)
458-
async def test_get_user_notifications(
430+
async def test_list_user_notifications(
459431
logged_user: UserInfoDict,
460432
notification_redis_client: aioredis.Redis,
461433
client: TestClient,
462434
notification_count: int,
435+
expected_response: type[web.HTTPException],
463436
):
464437
assert client.app
465-
url = client.app.router["get_user_notifications"].url_for()
438+
url = client.app.router["list_user_notifications"].url_for()
466439
assert str(url) == "/v0/me/notifications"
467-
468-
async with _create_notifications(
469-
notification_redis_client, logged_user, notification_count
470-
) as created_notifications:
471-
response = await client.get(f"{url}")
472-
json_response = await response.json()
473-
474-
result = parse_obj_as(list[UserNotification], json_response["data"])
475-
assert len(result) <= MAX_NOTIFICATIONS_FOR_USER_TO_SHOW
476-
assert result == list(
477-
reversed(created_notifications[:MAX_NOTIFICATIONS_FOR_USER_TO_SHOW])
478-
)
440+
response = await client.get(f"{url}")
441+
data, error = await assert_status(response, expected_response)
442+
if data:
443+
assert data == []
444+
assert not error
445+
446+
async with _create_notifications(
447+
notification_redis_client, logged_user, notification_count
448+
) as created_notifications:
449+
response = await client.get(f"{url}")
450+
json_response = await response.json()
451+
452+
result = parse_obj_as(list[UserNotification], json_response["data"])
453+
assert len(result) <= MAX_NOTIFICATIONS_FOR_USER_TO_SHOW
454+
assert result == list(
455+
reversed(created_notifications[:MAX_NOTIFICATIONS_FOR_USER_TO_SHOW])
456+
)
479457

480458

481-
@pytest.mark.parametrize("user_role", [UserRole.USER])
459+
@pytest.mark.parametrize(
460+
"user_role,expected_response",
461+
[
462+
(UserRole.ANONYMOUS, web.HTTPUnauthorized),
463+
(UserRole.GUEST, web.HTTPForbidden),
464+
(UserRole.USER, web.HTTPNoContent),
465+
(UserRole.TESTER, web.HTTPNoContent),
466+
],
467+
)
482468
@pytest.mark.parametrize(
483469
"notification_dict",
484470
[
@@ -508,31 +494,36 @@ async def test_get_user_notifications(
508494
),
509495
],
510496
)
511-
async def test_post_user_notification(
497+
async def test_create_user_notification(
512498
logged_user: UserInfoDict,
513499
notification_redis_client: aioredis.Redis,
514500
client: TestClient,
515501
notification_dict: dict[str, Any],
502+
expected_response: type[web.HTTPException],
516503
):
517504
assert client.app
518-
url = client.app.router["post_user_notification"].url_for()
505+
url = client.app.router["create_user_notification"].url_for()
519506
assert str(url) == "/v0/me/notifications"
520507
notification_dict["user_id"] = logged_user["id"]
521508
resp = await client.post(f"{url}", json=notification_dict)
522-
assert resp.status == web.HTTPNoContent.status_code, await resp.text()
509+
data, error = await assert_status(resp, expected_response)
510+
assert data is None # 204...
523511

524-
user_id = logged_user["id"]
525-
user_notifications = await _get_user_notifications(
526-
notification_redis_client, user_id
527-
)
528-
assert len(user_notifications) == 1
529-
# these are always generated and overwritten, even if provided by the user, since
530-
# we do not want to overwrite existing ones
531-
assert user_notifications[0].read is False
532-
assert user_notifications[0].id != notification_dict.get("id", None)
512+
if not error:
513+
user_id = logged_user["id"]
514+
user_notifications = await _get_user_notifications(
515+
notification_redis_client, user_id
516+
)
517+
assert len(user_notifications) == 1
518+
# these are always generated and overwritten, even if provided by the user, since
519+
# we do not want to overwrite existing ones
520+
assert user_notifications[0].read is False
521+
assert user_notifications[0].id != notification_dict.get("id", None)
522+
else:
523+
assert error is not None
533524

534525

535-
@pytest.mark.parametrize("user_role", [UserRole.USER])
526+
@pytest.mark.parametrize("user_role", [(UserRole.USER)])
536527
@pytest.mark.parametrize(
537528
"notification_count",
538529
[
@@ -543,14 +534,14 @@ async def test_post_user_notification(
543534
MAX_NOTIFICATIONS_FOR_USER_TO_KEEP * 10,
544535
],
545536
)
546-
async def test_post_user_notification_capped_list_length(
537+
async def test_create_user_notification_capped_list_length(
547538
logged_user: UserInfoDict,
548539
notification_redis_client: aioredis.Redis,
549540
client: TestClient,
550541
notification_count: int,
551542
):
552543
assert client.app
553-
url = client.app.router["post_user_notification"].url_for()
544+
url = client.app.router["create_user_notification"].url_for()
554545
assert str(url) == "/v0/me/notifications"
555546

556547
notifications_create_results = await asyncio.gather(
@@ -584,6 +575,36 @@ async def test_post_user_notification_capped_list_length(
584575
assert len(user_notifications) <= MAX_NOTIFICATIONS_FOR_USER_TO_KEEP
585576

586577

578+
@pytest.mark.parametrize(
579+
"user_role,expected_response",
580+
[
581+
(UserRole.ANONYMOUS, web.HTTPUnauthorized),
582+
(UserRole.GUEST, web.HTTPNoContent),
583+
(UserRole.USER, web.HTTPNoContent),
584+
(UserRole.TESTER, web.HTTPNoContent),
585+
],
586+
)
587+
async def test_update_user_notification(
588+
logged_user: UserInfoDict,
589+
notification_redis_client: aioredis.Redis,
590+
client: TestClient,
591+
expected_response: type[web.HTTPException],
592+
):
593+
async with _create_notifications(
594+
notification_redis_client, logged_user, 1
595+
) as created_notifications:
596+
assert client.app
597+
for notification in created_notifications:
598+
url = client.app.router["mark_notification_as_read"].url_for(
599+
notification_id=f"{notification.id}"
600+
)
601+
assert str(url) == f"/v0/me/notifications/{notification.id}"
602+
assert notification.read is False
603+
604+
resp = await client.patch(f"{url}", json={"read": True})
605+
await assert_status(resp, expected_response)
606+
607+
587608
@pytest.mark.parametrize("user_role", [UserRole.USER])
588609
@pytest.mark.parametrize(
589610
"notification_count",
@@ -619,8 +640,8 @@ def _marked_as_read(
619640
) as created_notifications:
620641
notifications_before_update = await _get_stored_notifications()
621642
for notification in created_notifications:
622-
url = client.app.router["update_user_notification"].url_for(
623-
id=f"{notification.id}"
643+
url = client.app.router["mark_notification_as_read"].url_for(
644+
notification_id=f"{notification.id}"
624645
)
625646
assert str(url) == f"/v0/me/notifications/{notification.id}"
626647
assert notification.read is False

0 commit comments

Comments
 (0)