-
Notifications
You must be signed in to change notification settings - Fork 310
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP: add /api/me to get identity model
includes fields: - username: str - name: Optional[str] - display_name: Optional[str] - initials: Optional[str] - avatar_url: Optional[str] - color: 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. Defines new jupyter_server.auth.IdentityProvider API for implementing authorization - IdP.get_user(Handler) returns opaque truthy user for authenticated requests or None - IdP.user_model adapts opaque User to standard identity model
- Loading branch information
Showing
6 changed files
with
278 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
from .authorizer import * # noqa | ||
from .decorator import authorized # noqa | ||
from .identity import * # noqa | ||
from .security import passwd # noqa |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
"""Identity Provider interface | ||
This defines the _authentication_ layer of Jupyter Server, | ||
to be used in combination with Authorizer for _authorization_. | ||
.. versionadded:: 2.0 | ||
""" | ||
import sys | ||
from typing import Any | ||
from typing import Dict | ||
from typing import List | ||
from typing import Optional | ||
|
||
from tornado.web import RequestHandler | ||
from traitlets.config import LoggingConfigurable | ||
|
||
if sys.version_info >= (3, 8): | ||
from typing import TypedDict | ||
else: | ||
try: | ||
from typing_extensions import TypedDict | ||
except ImportError: | ||
TypedDict = Dict | ||
|
||
|
||
class IdentityModel(TypedDict): | ||
# see the JupyterLab IUser model for definitions | ||
|
||
username: str # the only truly required field | ||
|
||
# these fields are derived from username if not specified | ||
name: str | ||
display_name: str | ||
|
||
# these fields are left as None if undefined | ||
initials: Optional[str] | ||
avatar_url: Optional[str] | ||
color: Optional[str] | ||
|
||
# Jupyter Server permissions | ||
# as a dict of permitted {"resource": ["actions"]} | ||
permissions: Dict[str, List[str]] | ||
|
||
|
||
class IdentityProvider(LoggingConfigurable): | ||
""" | ||
Interface for providing identity | ||
Two principle methods: | ||
- :meth:`~.IdentityProvider.get_user` returns a user object. | ||
For successful authentication, | ||
this may return anything truthy. | ||
- :meth:`~.IdentityProvider.user_model` returns a standard identity model dictionary, | ||
for use in the /me API. | ||
This should accept whatever is returned from get_user() | ||
and return a dictionary matching the structure of | ||
:class:`~.IdentityModel`. | ||
.. versionadded:: 2.0 | ||
""" | ||
|
||
def get_user(self, handler: RequestHandler) -> Any: | ||
"""Get the authenticated user for a request | ||
User may be anything truthy, but must be understood by user_model method. | ||
Return None if the request is not authenticated. | ||
When in doubt, use a standard identity model. | ||
""" | ||
|
||
if handler.login_handler is None: | ||
return { | ||
"username": "anonymous", | ||
} | ||
|
||
# The default: call LoginHandler.get_user for backward-compatibility | ||
# TODO: move default implementation to this class, | ||
# deprecate `LoginHandler.get_user` | ||
user = handler.login_handler.get_user(handler) | ||
return user | ||
|
||
def user_model(self, user: Any) -> IdentityModel: | ||
"""Construct standardized user model for the identity API | ||
Casts objects returned by `.get_user` (generally str username or dict with 'username' or 'name') | ||
To a complete IdentityModel dict. | ||
`username` is required. | ||
Any other missing fields will be filled out with defaults. | ||
""" | ||
user_model = {} | ||
if isinstance(user, str): | ||
user_model["username"] = user | ||
return { | ||
"username": user, | ||
"name": None, | ||
} | ||
elif isinstance(user, dict): | ||
user_model = {} | ||
# username may be in 'username' field or 'name' (e.g. JupyterHub) | ||
for username_key in ("username", "name"): | ||
if username_key in user: | ||
user_model["username"] = user[username_key] | ||
break | ||
for key, value in user.items(): | ||
if key in IdentityModel: | ||
user_model[key] = user | ||
|
||
# handle other types, e.g. custom objects. Subclasses must define this method | ||
# in order to handler these. | ||
if "username" not in user_model: | ||
clsname = self.__class__.__name__ | ||
self.log.warning( | ||
f"Unable to find username in current_user. {clsname}.user_model() must accept user objects as returned by {clsname}.get_user()." | ||
) | ||
self.log.debug("Unable to find username in current_user: %s", user) | ||
user_model["username"] = "unknown" | ||
user_model.setdefault("given_name", None) | ||
# fill defaults | ||
return self.fill_defaults(user_model) | ||
|
||
def _get_user_model(self, user: Any) -> IdentityModel: | ||
""" | ||
Private method to always return a filled user model | ||
This is how the user model should be accessed, in general. | ||
""" | ||
return self.fill_defaults(self.user_model(user)) | ||
|
||
def fill_defaults(self, identity: IdentityModel) -> IdentityModel: | ||
"""Fill out default fields in the identity model | ||
- Ensures all values are defined | ||
- Fills out derivative values for name fields fields | ||
- Fills out null values for optional fields | ||
""" | ||
|
||
# username is the only truly required field | ||
if not identity.get("username"): | ||
raise ValueError(f"identity.username must not be empty: {identity}") | ||
|
||
# derive name fields from username -> name -> display name | ||
if not identity.get("name"): | ||
identity["name"] = identity["username"] | ||
if not identity.get("display_name"): | ||
identity["display_name"] = identity["name"] | ||
|
||
# fields that should be defined, but use null if no information is provided | ||
for key in ("avatar_url", "color", "initials"): | ||
identity.setdefault(key, None) | ||
return identity |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.