call client-allowed scopes JUPYTERHUB_OAUTH_CLIENT_ALLOWED_SCOPES

This commit is contained in:
Min RK
2022-07-08 10:16:57 -07:00
parent 62b38934e5
commit 225ace636a
13 changed files with 44 additions and 33 deletions

View File

@@ -235,7 +235,7 @@ against the `.hub_scopes` attribute of each Handler
:::{versionchanged} 2.4 :::{versionchanged} 2.4
The JUPYTERHUB_OAUTH_SCOPES environment variable is deprecated and renamed to JUPYTERHUB_OAUTH_ACCESS_SCOPES, The JUPYTERHUB_OAUTH_SCOPES environment variable is deprecated and renamed to JUPYTERHUB_OAUTH_ACCESS_SCOPES,
to avoid ambiguity with JUPYTERHUB_OAUTH_ALLOWED_SCOPES to avoid ambiguity with JUPYTERHUB_OAUTH_CLIENT_ALLOWED_SCOPES
::: :::
```python ```python

View File

@@ -119,7 +119,7 @@ JUPYTERHUB_SERVICE_URL: Local URL where the service is expected to be listeni
JUPYTERHUB_OAUTH_SCOPES: JSON-serialized list of scopes to use for allowing access to the service JUPYTERHUB_OAUTH_SCOPES: JSON-serialized list of scopes to use for allowing access to the service
(deprecated in 2.4, use JUPYTERHUB_OAUTH_ACCESS_SCOPES). (deprecated in 2.4, use JUPYTERHUB_OAUTH_ACCESS_SCOPES).
JUPYTERHUB_OAUTH_ACCESS_SCOPES: JSON-serialized list of scopes to use for allowing access to the service (new in 2.4). JUPYTERHUB_OAUTH_ACCESS_SCOPES: JSON-serialized list of scopes to use for allowing access to the service (new in 2.4).
JUPYTERHUB_OAUTH_ALLOWED_SCOPES: JSON-serialized list of scopes that can be requested on behalf of users (new in 2.4). JUPYTERHUB_OAUTH_CLIENT_ALLOWED_SCOPES: JSON-serialized list of scopes that can be requested by the oauth client on behalf of users (new in 2.4).
``` ```
For the previous 'cull idle' Service example, these environment variables For the previous 'cull idle' Service example, these environment variables

View File

@@ -309,7 +309,7 @@ The process environment is returned by `Spawner.get_env`, which specifies the fo
- JUPYTERHUB_CLIENT_ID - the OAuth client ID for authenticating visitors. - JUPYTERHUB_CLIENT_ID - the OAuth client ID for authenticating visitors.
- JUPYTERHUB_OAUTH_CALLBACK_URL - the callback URL to use in oauth, typically `/user/:name/oauth_callback` - JUPYTERHUB_OAUTH_CALLBACK_URL - the callback URL to use in oauth, typically `/user/:name/oauth_callback`
- JUPYTERHUB_OAUTH_ACCESS_SCOPES - the scopes required to access the server (called JUPYTERHUB_OAUTH_SCOPES prior to 2.4) - JUPYTERHUB_OAUTH_ACCESS_SCOPES - the scopes required to access the server (called JUPYTERHUB_OAUTH_SCOPES prior to 2.4)
- JUPYTERHUB_OAUTH_ALLOWED_SCOPES - the scopes the service is allowed to request. - JUPYTERHUB_OAUTH_CLIENT_ALLOWED_SCOPES - the scopes the service is allowed to request.
If no scopes are requested explicitly, these scopes will be requested. If no scopes are requested explicitly, these scopes will be requested.
Optional environment variables, depending on configuration: Optional environment variables, depending on configuration:

View File

@@ -7,7 +7,7 @@ c.JupyterHub.services = [
'name': 'grades', 'name': 'grades',
'url': 'http://127.0.0.1:10101', 'url': 'http://127.0.0.1:10101',
'command': [sys.executable, './grades.py'], 'command': [sys.executable, './grades.py'],
'oauth_allowed_scopes': [ 'oauth_client_allowed_scopes': [
'custom:grades:write', 'custom:grades:write',
'custom:grades:read', 'custom:grades:read',
], ],

View File

@@ -26,7 +26,7 @@ After logging in with any username and password, you should see a JSON dump of y
``` ```
What is contained in the model will depend on the permissions What is contained in the model will depend on the permissions
requested in the `oauth_allowed_scopes` configuration of the service `whoami-oauth` service. requested in the `oauth_client_allowed_scopes` configuration of the service `whoami-oauth` service.
The default is the minimum required for identification and access to the service, The default is the minimum required for identification and access to the service,
which will provide the username and current scopes. which will provide the username and current scopes.

View File

@@ -14,11 +14,11 @@ c.JupyterHub.services = [
# only requesting access to the service, # only requesting access to the service,
# and identification by name, # and identification by name,
# nothing more. # nothing more.
# Specifying 'oauth_allowed_scopes' as a list of scopes # Specifying 'oauth_client_allowed_scopes' as a list of scopes
# allows requesting more information about users, # allows requesting more information about users,
# or the ability to take actions on users' behalf, as required. # or the ability to take actions on users' behalf, as required.
# the 'inherit' scope means the full permissions of the owner # the 'inherit' scope means the full permissions of the owner
# 'oauth_allowed_scopes': ['inherit'], # 'oauth_client_allowed_scopes': ['inherit'],
}, },
] ]

View File

@@ -2373,8 +2373,8 @@ class JupyterHub(Application):
if service.oauth_available: if service.oauth_available:
allowed_scopes = set() allowed_scopes = set()
if service.oauth_allowed_scopes: if service.oauth_client_allowed_scopes:
allowed_scopes.update(service.oauth_allowed_scopes) allowed_scopes.update(service.oauth_client_allowed_scopes)
if service.oauth_roles: if service.oauth_roles:
if not allowed_scopes: if not allowed_scopes:
# DEPRECATED? It's still convenient and valid, # DEPRECATED? It's still convenient and valid,
@@ -2388,7 +2388,7 @@ class JupyterHub(Application):
else: else:
self.log.warning( self.log.warning(
f"Ignoring oauth_roles for {service.name}: {service.oauth_roles}," f"Ignoring oauth_roles for {service.name}: {service.oauth_roles},"
f" using oauth_allowed_scopes={allowed_scopes}." f" using oauth_client_allowed_scopes={allowed_scopes}."
) )
oauth_client = self.oauth_provider.add_client( oauth_client = self.oauth_provider.add_client(
client_id=service.oauth_client_id, client_id=service.oauth_client_id,

View File

@@ -333,21 +333,30 @@ class HubAuth(SingletonConfigurable):
def _default_cache(self): def _default_cache(self):
return _ExpiringDict(self.cache_max_age) return _ExpiringDict(self.cache_max_age)
oauth_scopes = Set( @property
def oauth_scopes(self):
warnings.warn(
"HubAuth.oauth_scopes is deprecated in JupyterHub 2.4. Use .access_scopes"
)
return self.access_scopes
access_scopes = Set(
Unicode(), Unicode(),
help="""OAuth scopes to use for allowing access. help="""OAuth scopes to use for allowing access.
Get from $JUPYTERHUB_OAUTH_SCOPES by default. Get from $JUPYTERHUB_OAUTH_ACCESS_SCOPES by default.
""", """,
).tag(config=True) ).tag(config=True)
@default('oauth_scopes') @default('access_scopes')
def _default_scopes(self): def _default_scopes(self):
env_scopes = os.getenv('JUPYTERHUB_OAUTH_ACCESS_SCOPES') env_scopes = os.getenv('JUPYTERHUB_OAUTH_ACCESS_SCOPES')
if not env_scopes: if not env_scopes:
env_scopes = os.getenv('JUPYTERHUB_OAUTH_ACCESS_SCOPES') # deprecated name (since 2.4)
env_scopes = os.getenv('JUPYTERHUB_OAUTH_SCOPES')
if env_scopes: if env_scopes:
return set(json.loads(env_scopes)) return set(json.loads(env_scopes))
# scopes not specified, use service name if defined
service_name = os.getenv("JUPYTERHUB_SERVICE_NAME") service_name = os.getenv("JUPYTERHUB_SERVICE_NAME")
if service_name: if service_name:
return {f'access:services!service={service_name}'} return {f'access:services!service={service_name}'}
@@ -865,7 +874,7 @@ class HubAuthenticated:
- .hub_auth: A HubAuth instance - .hub_auth: A HubAuth instance
- .hub_scopes: A set of JupyterHub 2.0 OAuth scopes to allow. - .hub_scopes: A set of JupyterHub 2.0 OAuth scopes to allow.
Default comes from .hub_auth.oauth_scopes, Default comes from .hub_auth.oauth_access_scopes,
which in turn is set by $JUPYTERHUB_OAUTH_ACCESS_SCOPES which in turn is set by $JUPYTERHUB_OAUTH_ACCESS_SCOPES
Default values include: Default values include:
- 'access:services', 'access:services!service={service_name}' for services - 'access:services', 'access:services!service={service_name}' for services
@@ -905,8 +914,8 @@ class HubAuthenticated:
@property @property
def hub_scopes(self): def hub_scopes(self):
"""Set of allowed scopes (use hub_auth.oauth_scopes by default)""" """Set of allowed scopes (use hub_auth.access_scopes by default)"""
return self.hub_auth.oauth_scopes or None return self.hub_auth.access_scopes or None
@property @property
def allow_all(self): def allow_all(self):

View File

@@ -203,11 +203,11 @@ class Service(LoggingConfigurable):
oauth_roles = List( oauth_roles = List(
help="""OAuth allowed roles. help="""OAuth allowed roles.
DEPRECATED in 2.4: use oauth_allowed_scopes DEPRECATED in 2.4: use oauth_client_allowed_scopes
""" """
).tag(input=True) ).tag(input=True)
oauth_allowed_scopes = List( oauth_client_allowed_scopes = List(
help="""OAuth allowed scopes. help="""OAuth allowed scopes.
This sets the maximum and default scopes This sets the maximum and default scopes

View File

@@ -306,7 +306,7 @@ class Spawner(LoggingConfigurable):
[Callable(), List()], [Callable(), List()],
help="""Allowed roles for oauth tokens. help="""Allowed roles for oauth tokens.
Deprecated in 2.4: use oauth_allowed_scopes Deprecated in 2.4: use oauth_client_allowed_scopes
This sets the maximum and default roles This sets the maximum and default roles
assigned to oauth tokens issued by a single-user server's assigned to oauth tokens issued by a single-user server's
@@ -318,9 +318,9 @@ class Spawner(LoggingConfigurable):
""", """,
).tag(config=True) ).tag(config=True)
oauth_allowed_scopes = Union( oauth_client_allowed_scopes = Union(
[Callable(), List()], [Callable(), List()],
help="""Allowed scopes for oauth tokens. help="""Allowed scopes for oauth tokens issued by this server's oauth client.
This sets the maximum and default scopes This sets the maximum and default scopes
assigned to oauth tokens issued by a single-user server's assigned to oauth tokens issued by a single-user server's
@@ -332,12 +332,12 @@ class Spawner(LoggingConfigurable):
""", """,
).tag(config=True) ).tag(config=True)
def _get_oauth_allowed_scopes(self): def _get_oauth_client_allowed_scopes(self):
"""Private method: get oauth allowed scopes """Private method: get oauth allowed scopes
Handle: Handle:
- oauth_allowed_scopes - oauth_client_allowed_scopes
- callable config - callable config
- deprecated oauth_roles config - deprecated oauth_roles config
- access_scopes - access_scopes
@@ -347,8 +347,8 @@ class Spawner(LoggingConfigurable):
# 2. only roles # 2. only roles
# 3. both! (conflict, favor scopes) # 3. both! (conflict, favor scopes)
scopes = [] scopes = []
if self.oauth_allowed_scopes: if self.oauth_client_allowed_scopes:
allowed_scopes = self.oauth_allowed_scopes allowed_scopes = self.oauth_client_allowed_scopes
if callable(allowed_scopes): if callable(allowed_scopes):
allowed_scopes = allowed_scopes(self) allowed_scopes = allowed_scopes(self)
scopes.extend(allowed_scopes) scopes.extend(allowed_scopes)
@@ -357,7 +357,7 @@ class Spawner(LoggingConfigurable):
if scopes: if scopes:
# both defined! Warn # both defined! Warn
warnings.warn( warnings.warn(
f"Ignoring deprecated Spawner.oauth_roles={self.oauth_roles} in favor of Spawner.oauth_allowed_scopes.", f"Ignoring deprecated Spawner.oauth_roles={self.oauth_roles} in favor of Spawner.oauth_client_allowed_scopes.",
) )
else: else:
role_names = self.oauth_roles role_names = self.oauth_roles
@@ -960,7 +960,9 @@ class Spawner(LoggingConfigurable):
env['JUPYTERHUB_OAUTH_ACCESS_SCOPES'] = json.dumps(self.oauth_access_scopes) env['JUPYTERHUB_OAUTH_ACCESS_SCOPES'] = json.dumps(self.oauth_access_scopes)
# added in 2.4 # added in 2.4
env['JUPYTERHUB_OAUTH_ALLOWED_SCOPES'] = json.dumps(self.oauth_allowed_scopes) env['JUPYTERHUB_OAUTH_CLIENT_ALLOWED_SCOPES'] = json.dumps(
self.oauth_client_allowed_scopes
)
# Info previously passed on args # Info previously passed on args
env['JUPYTERHUB_USER'] = self.user.name env['JUPYTERHUB_USER'] = self.user.name

View File

@@ -894,18 +894,18 @@ async def test_server_role_api_calls(
assert r.status_code == response assert r.status_code == response
async def test_oauth_allowed_scopes(app): async def test_oauth_client_allowed_scopes(app):
allowed_scopes = ['read:users', 'read:groups'] allowed_scopes = ['read:users', 'read:groups']
service = { service = {
'name': 'oas1', 'name': 'oas1',
'api_token': 'some-token', 'api_token': 'some-token',
'oauth_allowed_scopes': allowed_scopes, 'oauth_client_allowed_scopes': allowed_scopes,
} }
app.services.append(service) app.services.append(service)
app.init_services() app.init_services()
app_service = app.services[0] app_service = app.services[0]
assert app_service['name'] == 'oas1' assert app_service['name'] == 'oas1'
assert set(app_service['oauth_allowed_scopes']) == set(allowed_scopes) assert set(app_service['oauth_client_allowed_scopes']) == set(allowed_scopes)
async def test_user_group_roles(app, create_temp_role): async def test_user_group_roles(app, create_temp_role):

View File

@@ -439,7 +439,7 @@ async def test_hub_connect_url(db):
async def test_spawner_oauth_scopes(app, user): async def test_spawner_oauth_scopes(app, user):
allowed_scopes = ["read:users"] allowed_scopes = ["read:users"]
spawner = user.spawners[''] spawner = user.spawners['']
spawner.oauth_allowed_scopes = allowed_scopes spawner.oauth_client_allowed_scopes = allowed_scopes
# exercise start/stop which assign roles to oauth client # exercise start/stop which assign roles to oauth client
await spawner.user.spawn() await spawner.user.spawn()
oauth_client = spawner.orm_spawner.oauth_client oauth_client = spawner.orm_spawner.oauth_client

View File

@@ -670,7 +670,7 @@ class User:
client_id, client_id,
api_token, api_token,
url_path_join(self.url, url_escape_path(server_name), 'oauth_callback'), url_path_join(self.url, url_escape_path(server_name), 'oauth_callback'),
allowed_scopes=spawner._get_oauth_allowed_scopes(), allowed_scopes=spawner._get_oauth_client_allowed_scopes(),
description="Server at %s" description="Server at %s"
% (url_path_join(self.base_url, server_name) + '/'), % (url_path_join(self.base_url, server_name) + '/'),
) )