Skip to content

Commit 0761daa

Browse files
committed
Drafts group_or_role_permission_required
1 parent 5410700 commit 0761daa

File tree

4 files changed

+77
-4
lines changed

4 files changed

+77
-4
lines changed

services/web/server/src/simcore_service_webserver/products/_controller/rest.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010

1111
from ..._meta import API_VTAG as VTAG
1212
from ...login.decorators import login_required
13-
from ...security.decorators import permission_required
13+
from ...security.decorators import (
14+
group_or_role_permission_required,
15+
permission_required,
16+
)
1417
from ...utils_aiohttp import envelope_json_response
1518
from .. import _service, products_web
1619
from .._repository import ProductRepository
@@ -46,7 +49,7 @@ async def _get_current_product_price(request: web.Request):
4649

4750
@routes.get(f"/{VTAG}/products/{{product_name}}", name="get_product")
4851
@login_required
49-
@permission_required("product.details.*")
52+
@group_or_role_permission_required("product.details.*")
5053
@handle_rest_requests_exceptions
5154
async def _get_product(request: web.Request):
5255
req_ctx = ProductsRequestContext.model_validate(request)

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,14 @@ class PermissionDict(TypedDict, total=False):
133133
assert set(ROLES_PERMISSIONS) == set( # nosec
134134
UserRole
135135
), "All user roles must be part define permissions" # nosec
136+
137+
138+
# Group-based permissions for support groups
139+
# Maps group type to list of permissions that group members can perform
140+
GROUP_PERMISSIONS: dict[str, list[str]] = {
141+
"support_group": [
142+
"product.details.*",
143+
"admin.users.read",
144+
],
145+
# NOTE: Future group types can be added here
146+
}

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
from contextlib import suppress
12
from functools import wraps
23

4+
import aiohttp_security.api # type: ignore[import-untyped]
35
from aiohttp import web
46
from servicelib.aiohttp.typing_extension import Handler
57

8+
from ..products.products_web import get_current_product
9+
from ._authz_access_model import AuthContextDict
10+
from ._authz_access_roles import GROUP_PERMISSIONS
11+
from ._authz_web import check_user_authorized
612
from .security_web import check_user_permission
713

814

@@ -26,3 +32,53 @@ async def _wrapped(request: web.Request):
2632
return _wrapped
2733

2834
return _decorator
35+
36+
37+
def group_or_role_permission_required(permission: str):
38+
"""Decorator that checks user permissions via both roles AND groups (OR logic).
39+
40+
User gets access if they have permission via role OR group membership.
41+
42+
If user is not authorized - raises HTTPUnauthorized,
43+
if user is authorized but lacks both role and group permissions - raises HTTPForbidden.
44+
"""
45+
46+
def _decorator(handler: Handler):
47+
@wraps(handler)
48+
async def _wrapped(request: web.Request):
49+
context = AuthContextDict()
50+
context["authorized_uid"] = await check_user_authorized(request)
51+
52+
# Check role-based permissions first
53+
role_allowed = await aiohttp_security.api.permits(
54+
request, permission, context
55+
)
56+
if role_allowed:
57+
return await handler(request)
58+
59+
# Check group-based permissions
60+
with suppress(
61+
Exception
62+
# If product or group check fails, continue to deny access
63+
# NOTE: Logging omitted to avoid exposing internal errors
64+
):
65+
66+
product = get_current_product(request)
67+
68+
if product.support_standard_group_id is not None:
69+
# FIXME: Group membership API will be implemented later
70+
# For now, always returns False
71+
is_member = False # Placeholder
72+
73+
if is_member:
74+
group_permissions = GROUP_PERMISSIONS.get("support_group", [])
75+
if permission in group_permissions:
76+
return await handler(request)
77+
78+
# Neither role nor group permissions granted
79+
msg = f"You do not have sufficient access rights for {permission}"
80+
raise web.HTTPForbidden(text=msg)
81+
82+
return _wrapped
83+
84+
return _decorator

services/web/server/src/simcore_service_webserver/users/_controller/rest/accounts_rest.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
from ...._meta import API_VTAG
2727
from ....invitations import api as invitations_service
2828
from ....login.decorators import login_required
29-
from ....security.decorators import permission_required
29+
from ....security.decorators import (
30+
group_or_role_permission_required,
31+
permission_required,
32+
)
3033
from ....utils_aiohttp import create_json_response_from_page, envelope_json_response
3134
from ... import _accounts_service
3235
from ._rest_exceptions import handle_rest_requests_exceptions
@@ -43,7 +46,7 @@
4346

4447
@routes.get(f"/{API_VTAG}/admin/user-accounts", name="list_users_accounts")
4548
@login_required
46-
@permission_required("admin.users.read")
49+
@group_or_role_permission_required("admin.users.read")
4750
@handle_rest_requests_exceptions
4851
async def list_users_accounts(request: web.Request) -> web.Response:
4952
req_ctx = UsersRequestContext.model_validate(request)

0 commit comments

Comments
 (0)