Skip to content

Commit eee5848

Browse files
committed
✨ Refactor authentication decorators: move login_required to login_auth and clean up unused code
1 parent 5b18532 commit eee5848

File tree

4 files changed

+80
-90
lines changed

4 files changed

+80
-90
lines changed

services/web/server/src/simcore_service_webserver/login/_controller/rest/auth.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -287,20 +287,3 @@ async def logout(request: web.Request) -> web.Response:
287287
await security_web.forget_identity(request, response)
288288

289289
return response
290-
291-
292-
@routes.get(f"/{API_VTAG}/auth:check", name="check_auth")
293-
@login_required
294-
async def check_auth(request: web.Request) -> web.Response:
295-
"""Lightweight endpoint for checking if users are authenticated & authorized to this product
296-
297-
Used primarily by Traefik auth middleware to verify session cookies
298-
SEE https://doc.traefik.io/traefik/middlewares/http/forwardauth
299-
"""
300-
# NOTE: for future development
301-
# if database access is added here, services like jupyter-math
302-
# which load a lot of resources will have a big performance hit
303-
# consider caching some properties required by this endpoint or rely on Redis
304-
assert request # nosec
305-
306-
return web.json_response(status=status.HTTP_204_NO_CONTENT)
Lines changed: 6 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,7 @@
1-
import functools
2-
import inspect
3-
from typing import cast
1+
from ..login_auth.decorators import get_user_id, login_required
42

5-
from aiohttp import web
6-
from models_library.users import UserID
7-
from servicelib.aiohttp.typing_extension import HandlerAnyReturn
8-
from servicelib.request_keys import RQT_USERID_KEY
9-
10-
from ..products import products_web
11-
from ..security import security_web
12-
13-
14-
def login_required(handler: HandlerAnyReturn) -> HandlerAnyReturn:
15-
"""Decorator that restrict access only for authorized users with permissions to access a given product
16-
17-
- User is considered authorized if check_authorized(request) raises no exception
18-
- If authorized, it injects user_id in request[RQT_USERID_KEY]
19-
- Use this decorator instead of aiohttp_security.api.login_required!
20-
21-
WARNING: Add always @router. decorator FIRST, e.g.
22-
23-
@router.get("/foo")
24-
@login_required
25-
async def get_foo(request: web.Request):
26-
...
27-
28-
and NOT as
29-
30-
@login_required
31-
@router.get("/foo")
32-
async def get_foo(request: web.Request):
33-
...
34-
35-
since the latter will register in `router` get_foo **without** `login_required`
36-
"""
37-
assert set(inspect.signature(handler).parameters.values()) == { # nosec
38-
inspect.Parameter(
39-
name="request",
40-
kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
41-
annotation=web.Request,
42-
)
43-
}, f"Expected {handler.__name__} with request as signature, got {handler.__annotations__}"
44-
45-
@functools.wraps(handler)
46-
async def _wrapper(request: web.Request):
47-
"""
48-
Raises:
49-
HTTPUnauthorized: if unauthorized user
50-
HTTPForbidden: if user not allowed in product
51-
"""
52-
# WARNING: note that check_authorized is patched in some tests.
53-
# Careful when changing the function signature
54-
user_id = await security_web.check_user_authorized(request)
55-
product_name = products_web.get_product_name(request)
56-
57-
await security_web.check_user_permission(
58-
request,
59-
security_web.PERMISSION_PRODUCT_LOGIN_KEY,
60-
context=security_web.AuthContextDict(
61-
product_name=product_name,
62-
authorized_uid=user_id,
63-
),
64-
)
65-
66-
request[RQT_USERID_KEY] = user_id
67-
return await handler(request)
68-
69-
return _wrapper
70-
71-
72-
def get_user_id(request: web.Request) -> UserID:
73-
return cast(UserID, request[RQT_USERID_KEY])
3+
__all__: tuple[str, ...] = (
4+
"get_user_id",
5+
"login_required",
6+
)
7+
# nopycln: file

services/web/server/src/simcore_service_webserver/login_auth/_controller_rest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from servicelib.aiohttp import status
66

77
from .._meta import API_VTAG
8-
from ..login.decorators import login_required # FIXME: move this here
8+
from .decorators import login_required
99

1010
_logger = logging.getLogger(__name__)
1111

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import functools
2+
import inspect
3+
from typing import cast
4+
5+
from aiohttp import web
6+
from models_library.users import UserID
7+
from servicelib.aiohttp.typing_extension import HandlerAnyReturn
8+
from servicelib.request_keys import RQT_USERID_KEY
9+
10+
from ..products import products_web
11+
from ..security import security_web
12+
13+
14+
def login_required(handler: HandlerAnyReturn) -> HandlerAnyReturn:
15+
"""Decorator that restrict access only for authorized users with permissions to access a given product
16+
17+
- User is considered authorized if check_authorized(request) raises no exception
18+
- If authorized, it injects user_id in request[RQT_USERID_KEY]
19+
- Use this decorator instead of aiohttp_security.api.login_required!
20+
21+
WARNING: Add always @router. decorator FIRST, e.g.
22+
23+
@router.get("/foo")
24+
@login_required
25+
async def get_foo(request: web.Request):
26+
...
27+
28+
and NOT as
29+
30+
@login_required
31+
@router.get("/foo")
32+
async def get_foo(request: web.Request):
33+
...
34+
35+
since the latter will register in `router` get_foo **without** `login_required`
36+
"""
37+
assert set(inspect.signature(handler).parameters.values()) == { # nosec
38+
inspect.Parameter(
39+
name="request",
40+
kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
41+
annotation=web.Request,
42+
)
43+
}, f"Expected {handler.__name__} with request as signature, got {handler.__annotations__}"
44+
45+
@functools.wraps(handler)
46+
async def _wrapper(request: web.Request):
47+
"""
48+
Raises:
49+
HTTPUnauthorized: if unauthorized user
50+
HTTPForbidden: if user not allowed in product
51+
"""
52+
# WARNING: note that check_authorized is patched in some tests.
53+
# Careful when changing the function signature
54+
user_id = await security_web.check_user_authorized(request)
55+
product_name = products_web.get_product_name(request)
56+
57+
await security_web.check_user_permission(
58+
request,
59+
security_web.PERMISSION_PRODUCT_LOGIN_KEY,
60+
context=security_web.AuthContextDict(
61+
product_name=product_name,
62+
authorized_uid=user_id,
63+
),
64+
)
65+
66+
request[RQT_USERID_KEY] = user_id
67+
return await handler(request)
68+
69+
return _wrapper
70+
71+
72+
def get_user_id(request: web.Request) -> UserID:
73+
return cast(UserID, request[RQT_USERID_KEY])

0 commit comments

Comments
 (0)