Skip to content

Commit

Permalink
WIP: add /api/me to get identity model
Browse files Browse the repository at this point in the history
includes fields:

- username: str
- given_name: Optional[str]
- permissions in the form {"resource": ["action", ],}

where permissions are only populated _by request_,
because the server cannot know what all resource/action combinations are available.
  • Loading branch information
minrk committed Jan 20, 2022
1 parent 4a7da00 commit 535b043
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 0 deletions.
32 changes: 32 additions & 0 deletions jupyter_server/services/auth/authorizer.py
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions 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),
]
34 changes: 34 additions & 0 deletions jupyter_server/tests/services/auth/test_authorizer.py
Expand Up @@ -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

0 comments on commit 535b043

Please sign in to comment.