-
Notifications
You must be signed in to change notification settings - Fork 304
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.identity_model adapts opaque User to standard identity model dict
- Loading branch information
Showing
11 changed files
with
678 additions
and
22 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
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
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
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,155 @@ | ||
"""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.identity_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 identity_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 identity_model(self, user: Any) -> IdentityModel: | ||
"""Construct standardized identity 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. | ||
""" | ||
identity = {} | ||
if isinstance(user, str): | ||
return { | ||
"username": user, | ||
} | ||
elif isinstance(user, dict): | ||
# username may be in 'username' field or 'name' (e.g. JupyterHub) | ||
# but only accept 'name' for username if 'username' not present | ||
for username_key in ("username", "name"): | ||
if username_key in user: | ||
identity["username"] = user[username_key] | ||
break | ||
|
||
for key, value in user.items(): | ||
# annotations is where fields are stored | ||
if key in IdentityModel.__annotations__: | ||
identity[key] = user[key] | ||
|
||
# handle other types, e.g. custom objects. Subclasses must define this method | ||
# in order to handler these. | ||
if "username" not in identity: | ||
clsname = self.__class__.__name__ | ||
self.log.warning( | ||
f"Unable to find username in current_user. {clsname}.identity_model() must accept user objects as returned by {clsname}.get_user()." | ||
) | ||
self.log.debug("Unable to find username in current_user: %s", user) | ||
identity["username"] = "unknown" | ||
# fill defaults | ||
return self.fill_identity(identity) | ||
|
||
def _get_identity_model(self, user: Any) -> IdentityModel: | ||
""" | ||
Private method to always return a filled identity model | ||
This is how the user model should be accessed, in general. | ||
""" | ||
return self.fill_identity(self.identity_model(user)) | ||
|
||
def fill_identity(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) | ||
|
||
identity.setdefault("permissions", {}) | ||
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
Oops, something went wrong.