implement access scopes

- access:services for services
- access:users:servers for servers
- tokens automatically have access to their issuing client (if their owner does, too)
- Check access scope in HubAuth integration
This commit is contained in:
Min RK
2021-05-12 14:49:06 +02:00
parent e5198b4039
commit e2076e6c91
11 changed files with 304 additions and 36 deletions

View File

@@ -33,13 +33,50 @@ from traitlets import Dict
from traitlets import Instance
from traitlets import Integer
from traitlets import observe
from traitlets import Set
from traitlets import Unicode
from traitlets import validate
from traitlets.config import SingletonConfigurable
from ..scopes import _intersect_scopes
from ..utils import url_path_join
def check_scopes(required_scopes, scopes):
"""Check that required_scope(s) are in scopes
Returns the subset of scopes matching required_scopes,
which is truthy if any scopes match any required scopes.
Correctly resolves scope filters *except* for groups -> user,
e.g. require: access:server!user=x, have: access:server!group=y
will not grant access to user x even if user x is in group y.
Parameters
----------
required_scopes: set
The set of scopes required.
scopes: set
The set (or list) of scopes to check against required_scopes
Returns
-------
relevant_scopes: set
The set of scopes in required_scopes that are present in scopes,
which is truthy if any required scopes are present,
and falsy otherwise.
"""
if isinstance(required_scopes, str):
required_scopes = {required_scopes}
intersection = _intersect_scopes(required_scopes, scopes)
# re-intersect with required_scopes in case the intersection
# applies stricter filters than required_scopes declares
# e.g. required_scopes = {'read:users'} and intersection has only {'read:users!user=x'}
return set(required_scopes) & intersection
class _ExpiringDict(dict):
"""Dict-like cache for Hub API requests
@@ -285,6 +322,24 @@ class HubAuth(SingletonConfigurable):
def _default_cache(self):
return _ExpiringDict(self.cache_max_age)
oauth_scopes = Set(
Unicode(),
help="""OAuth scopes to use for allowing access.
Get from $JUPYTERHUB_OAUTH_SCOPES by default.
""",
).tag(config=True)
@default('oauth_scopes')
def _default_scopes(self):
env_scopes = os.getenv('JUPYTERHUB_OAUTH_SCOPES')
if env_scopes:
return set(json.loads(env_scopes))
service_name = os.getenv("JUPYTERHUB_SERVICE_NAME")
if service_name:
return {f'access:services!service={service_name}'}
return set()
def _check_hub_authorization(self, url, api_token, cache_key=None, use_cache=True):
"""Identify a user with the Hub
@@ -495,6 +550,10 @@ class HubAuth(SingletonConfigurable):
app_log.debug("No user identified")
return user_model
def check_scopes(self, required_scopes, user):
"""Check whether the user has required scope(s)"""
return check_scopes(required_scopes, set(user["scopes"]))
class HubOAuth(HubAuth):
"""HubAuth using OAuth for login instead of cookies set by the Hub.
@@ -771,6 +830,7 @@ class HubAuthenticated(object):
A handler that mixes this in must have the following attributes/properties:
- .hub_auth: A HubAuth instance
- .hub_scopes: A set of JupyterHub 2.0 OAuth scopes to allow.
- .hub_users: A set of usernames to allow.
If left unspecified or None, username will not be checked.
- .hub_groups: A set of group names to allow.
@@ -795,13 +855,19 @@ class HubAuthenticated(object):
hub_groups = None # set of allowed groups
allow_admin = False # allow any admin user access
@property
def hub_scopes(self):
"""Set of allowed scopes (use hub_auth.oauth_scopes by default)"""
return self.hub_auth.oauth_scopes or None
@property
def allow_all(self):
"""Property indicating that all successfully identified user
or service should be allowed.
"""
return (
self.hub_services is None
self.hub_scopes is None
and self.hub_services is None
and self.hub_users is None
and self.hub_groups is None
)
@@ -842,22 +908,41 @@ class HubAuthenticated(object):
Returns the input if the user should be allowed, None otherwise.
Override if you want to check anything other than the username's presence in hub_users list.
Override for custom logic in authenticating users.
Args:
model (dict): the user or service model returned from :class:`HubAuth`
user_model (dict): the user or service model returned from :class:`HubAuth`
Returns:
user_model (dict): The user model if the user should be allowed, None otherwise.
"""
name = model['name']
kind = model.setdefault('kind', 'user')
if self.allow_all:
app_log.debug(
"Allowing Hub %s %s (all Hub users and services allowed)", kind, name
)
return model
if self.hub_scopes:
scopes = self.hub_auth.check_scopes(self.hub_scopes, model)
if scopes:
app_log.debug(
f"Allowing Hub {kind} {name} based on oauth scopes {scopes}"
)
return model
else:
app_log.warning(
f"Not allowing Hub {kind} {name}: missing required scopes"
)
app_log.debug(
f"Hub {kind} {name} needs scope(s) {self.hub_scopes}, has scope(s) {model['scopes']}"
)
# if hub_scopes are used, *only* hub_scopes are used
# note: this means successful authentication, but insufficient permission
raise UserNotAllowed(model)
if self.allow_admin and model.get('admin', False):
app_log.debug("Allowing Hub admin %s", name)
return model