-
Notifications
You must be signed in to change notification settings - Fork 308
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add /api/me to get identity and permissions model
identity includes fields: - username: str - name: Optional[str] - display_name: Optional[str] - initials: Optional[str] - avatar_url: Optional[str] - color: Optional[str] permissions is sibling to identity, with 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 User dataclass - IdP.identity_model renders dataclass to standard JSON dict model - IdP.get_handlers returns possible custom RequestHandlers (e.g. OAuth callback) - get user_id from cookie, even when token-authenticated, for stable random user ids in the default implementation, even for token-authenticated requests. Default get_user implementation still resides in LoginHandler.get_user, to be moved and deprecated in subsequent PR. Ensures authorizer, identity_provider are defined in case of custom Application, load default Authorizer/IdentityProvider if none is available in settings For CI: Fix numpy-style docstring format
- Loading branch information
Showing
17 changed files
with
823 additions
and
51 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
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,139 @@ | ||
"""Identity Provider interface | ||
This defines the _authentication_ layer of Jupyter Server, | ||
to be used in combination with Authorizer for _authorization_. | ||
.. versionadded:: 2.0 | ||
""" | ||
from dataclasses import asdict, dataclass | ||
from typing import Any, Optional | ||
|
||
from tornado.web import RequestHandler | ||
from traitlets.config import LoggingConfigurable | ||
|
||
# from dataclasses import field | ||
|
||
|
||
@dataclass | ||
class User: | ||
"""Object representing a User | ||
This or a subclass should be returned from IdentityProvider.get_user | ||
""" | ||
|
||
username: str # the only truly required field | ||
|
||
# these fields are filled from username if not specified | ||
# name is the 'real' name of the user | ||
name: str = "" | ||
# display_name is a shorter name for us in UI, | ||
# if different from name. e.g. a nickname | ||
display_name: str = "" | ||
|
||
# these fields are left as None if undefined | ||
initials: Optional[str] = None | ||
avatar_url: Optional[str] = None | ||
color: Optional[str] = None | ||
|
||
# TODO: extension fields? | ||
# ext: Dict[str, Dict[str, Any]] = field(default_factory=dict) | ||
|
||
def __post_init__(self): | ||
self.fill_defaults() | ||
|
||
def fill_defaults(self): | ||
"""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 self.username: | ||
raise ValueError(f"user.username must not be empty: {self}") | ||
|
||
# derive name fields from username -> name -> display name | ||
if not self.name: | ||
self.name = self.username | ||
if not self.display_name: | ||
self.display_name = self.name | ||
|
||
def to_dict(self): | ||
pass | ||
|
||
|
||
def _backward_compat_user(got_user: Any) -> User: | ||
"""Backward-compatibility for LoginHandler.get_user | ||
Prior to 2.0, LoginHandler.get_user could return anything truthy. | ||
Typically, this was either a simple string username, | ||
or a simple dict. | ||
Make some effort to allow common patterns to keep working. | ||
""" | ||
if isinstance(got_user, str): | ||
return User(username=got_user) | ||
elif isinstance(got_user, dict): | ||
kwargs = {} | ||
if "username" not in got_user: | ||
if "name" in got_user: | ||
kwargs["username"] = got_user["name"] | ||
for field in User.__dataclass_fields__: | ||
if field in got_user: | ||
kwargs[field] = got_user[field] | ||
try: | ||
return User(**kwargs) | ||
except TypeError: | ||
raise ValueError(f"Unrecognized user: {got_user}") | ||
else: | ||
raise ValueError(f"Unrecognized user: {got_user}") | ||
|
||
|
||
class IdentityProvider(LoggingConfigurable): | ||
""" | ||
Interface for providing identity | ||
Two principle methods: | ||
- :meth:`~.IdentityProvider.get_user` returns a :class:`~.User` object | ||
for successful authentication, or None for no-identity-found. | ||
- :meth:`~.IdentityProvider.identity_model` turns a :class:`~.User` into a JSONable dict. | ||
The default is to use :py:meth:`dataclasses.asdict`, | ||
and usually shouldn't need override. | ||
.. versionadded:: 2.0 | ||
""" | ||
|
||
def get_user(self, handler: RequestHandler) -> User: | ||
"""Get the authenticated user for a request | ||
Must return a :class:`.jupyter_server.auth.User`, | ||
though it may be a subclass. | ||
Return None if the request is not authenticated. | ||
""" | ||
|
||
if handler.login_handler is None: | ||
return User("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) | ||
if user and not isinstance(user, User): | ||
return _backward_compat_user(user) | ||
return user | ||
|
||
def identity_model(self, user: User) -> dict: | ||
"""Return a User as an Identity model""" | ||
# TODO: validate? | ||
return asdict(user) | ||
|
||
def get_handlers(self) -> list: | ||
"""Return list of additional handlers for this identity provider | ||
For example, an OAuth callback handler. | ||
""" | ||
return [] |
Oops, something went wrong.