diff --git a/jupyter_server/services/auth/authorizer.py b/jupyter_server/services/auth/authorizer.py index 1b16629856..85738e4088 100644 --- a/jupyter_server/services/auth/authorizer.py +++ b/jupyter_server/services/auth/authorizer.py @@ -6,6 +6,9 @@ """ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. +from typing import Any +from typing import Dict + from traitlets.config import LoggingConfigurable from jupyter_server.base.handlers import JupyterHandler @@ -49,6 +52,35 @@ def is_authorized(self, handler: JupyterHandler, user: str, action: str, resourc """ raise NotImplementedError() + def user_model(self, user: Any) -> Dict: + """Construct standardized user model for the identity API + + Casts accepted `.current_user` structure (generally str username or dict with 'username' or 'name') + """ + user_model = {} + if isinstance(user, str): + user_model["username"] = user + return { + "username": user, + "given_name": None, + } + elif isinstance(user, dict): + user_model = {} + # username may be in 'username' field or 'name' (e.g. JupyterHub) + for key in ("username", "name"): + if key in user: + user_model["username"] = user[key] + break + if "given_name" in user: + user_model["given_name"] = user["given_name"] + # handle other types, e.g. `.user`? Subclasses can handle these. + if "username" not in user_model: + self.log.warning("Unable to find username in current_user") + self.log.debug("Unknown to find username in current_user: %s", user) + user_model["username"] = "unknown" + user_model.setdefault("given_name", None) + return user_model + class AllowAllAuthorizer(Authorizer): """A no-op implementation of the Authorizer diff --git a/jupyter_server/services/auth/handlers.py b/jupyter_server/services/auth/handlers.py new file mode 100644 index 0000000000..e3a03fef9a --- /dev/null +++ b/jupyter_server/services/auth/handlers.py @@ -0,0 +1,56 @@ +"""Handlers related to authorization + +""" +import json +import sys +from typing import Dict +from typing import List +from typing import Optional + +if sys.version_info >= (3, 8): + from typing import TypedDict +else: + try: + from typing_extensions import TypedDict + except ImportError: + TypedDict = Dict + +from tornado import web + +from ...base.handlers import APIHandler + + +class IdentityModel(TypedDict): + username: str + given_name: Optional[str] + permissions: Dict[str, List[str]] + + +class IdentityHandler(APIHandler): + """Get the current user's identity model""" + + @web.authenticated + def get(self): + resources: List[str] = self.get_argument("resources") or [] + actions: List[str] = self.get_argument("actions") or [ + "read", + "write", + "execute", + ] + permissions: Dict[str, List[str]] = {} + user = self.current_user + for resource in resources: + allowed = permissions[resource] = [] + for action in actions: + if self.authorizer.is_authorized(self, user=user, resource=resource, action=action): + allowed.append(action) + user_model: IdentityModel = dict( + permissions=permissions, + **self.authorizer.user_model(user), + ) + self.write(json.dumps(user_model)) + + +default_handlers = [ + (r"/api/me", IdentityHandler), +] diff --git a/jupyter_server/tests/services/auth/test_authorizer.py b/jupyter_server/tests/services/auth/test_authorizer.py index e2a1ec8e63..612a02d444 100644 --- a/jupyter_server/tests/services/auth/test_authorizer.py +++ b/jupyter_server/tests/services/auth/test_authorizer.py @@ -275,3 +275,37 @@ async def test_authorized_requests( code = await send_request(url, body=body, method=method) assert code in expected_codes + + +class CustomUser: + def __init__(self, name): + self.name = name + + +@pytest.mark.parametrize( + "user, expected", + [ + ("str-name", {"username": "str-name", "given_name": None}), + ({"name": "user.name"}, {"username": "user.name", "given_name": None}), + ( + {"username": "user.username"}, + {"username": "user.username", "given_name": None}, + ), + ( + {"name": "user.name", "username": "user.username"}, + {"username": "user.username", "given_name": None}, + ), + ( + {"username": "user.username", "given_name": "given"}, + {"username": "user.username", "given_name": "given"}, + ), + ( + {"username": "user.username", "given_name": "given"}, + {"username": "user.username", "given_name": "given"}, + ), + (CustomUser("custom_name"), {"username": "unknown", "given_name": None}), + ], +) +def test_user_model(user, expected): + authorizer = AuthorizerforTesting() + assert authorizer.user_model(user) == expected