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
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

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
(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_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

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_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_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.
Optional environment variables, depending on configuration:

View File

@@ -7,7 +7,7 @@ c.JupyterHub.services = [
'name': 'grades',
'url': 'http://127.0.0.1:10101',
'command': [sys.executable, './grades.py'],
'oauth_allowed_scopes': [
'oauth_client_allowed_scopes': [
'custom:grades:write',
'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
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,
which will provide the username and current scopes.

View File

@@ -14,11 +14,11 @@ c.JupyterHub.services = [
# only requesting access to the service,
# and identification by name,
# 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,
# or the ability to take actions on users' behalf, as required.
# 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:
allowed_scopes = set()
if service.oauth_allowed_scopes:
allowed_scopes.update(service.oauth_allowed_scopes)
if service.oauth_client_allowed_scopes:
allowed_scopes.update(service.oauth_client_allowed_scopes)
if service.oauth_roles:
if not allowed_scopes:
# DEPRECATED? It's still convenient and valid,
@@ -2388,7 +2388,7 @@ class JupyterHub(Application):
else:
self.log.warning(
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(
client_id=service.oauth_client_id,

View File

@@ -333,21 +333,30 @@ class HubAuth(SingletonConfigurable):
def _default_cache(self):
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(),
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)
@default('oauth_scopes')
@default('access_scopes')
def _default_scopes(self):
env_scopes = os.getenv('JUPYTERHUB_OAUTH_ACCESS_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:
return set(json.loads(env_scopes))
# scopes not specified, use service name if defined
service_name = os.getenv("JUPYTERHUB_SERVICE_NAME")
if service_name:
return {f'access:services!service={service_name}'}
@@ -865,7 +874,7 @@ class HubAuthenticated:
- .hub_auth: A HubAuth instance
- .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
Default values include:
- 'access:services', 'access:services!service={service_name}' for services
@@ -905,8 +914,8 @@ class HubAuthenticated:
@property
def hub_scopes(self):
"""Set of allowed scopes (use hub_auth.oauth_scopes by default)"""
return self.hub_auth.oauth_scopes or None
"""Set of allowed scopes (use hub_auth.access_scopes by default)"""
return self.hub_auth.access_scopes or None
@property
def allow_all(self):

View File

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

View File

@@ -306,7 +306,7 @@ class Spawner(LoggingConfigurable):
[Callable(), List()],
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
assigned to oauth tokens issued by a single-user server's
@@ -318,9 +318,9 @@ class Spawner(LoggingConfigurable):
""",
).tag(config=True)
oauth_allowed_scopes = Union(
oauth_client_allowed_scopes = Union(
[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
assigned to oauth tokens issued by a single-user server's
@@ -332,12 +332,12 @@ class Spawner(LoggingConfigurable):
""",
).tag(config=True)
def _get_oauth_allowed_scopes(self):
def _get_oauth_client_allowed_scopes(self):
"""Private method: get oauth allowed scopes
Handle:
- oauth_allowed_scopes
- oauth_client_allowed_scopes
- callable config
- deprecated oauth_roles config
- access_scopes
@@ -347,8 +347,8 @@ class Spawner(LoggingConfigurable):
# 2. only roles
# 3. both! (conflict, favor scopes)
scopes = []
if self.oauth_allowed_scopes:
allowed_scopes = self.oauth_allowed_scopes
if self.oauth_client_allowed_scopes:
allowed_scopes = self.oauth_client_allowed_scopes
if callable(allowed_scopes):
allowed_scopes = allowed_scopes(self)
scopes.extend(allowed_scopes)
@@ -357,7 +357,7 @@ class Spawner(LoggingConfigurable):
if scopes:
# both defined! 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:
role_names = self.oauth_roles
@@ -960,7 +960,9 @@ class Spawner(LoggingConfigurable):
env['JUPYTERHUB_OAUTH_ACCESS_SCOPES'] = json.dumps(self.oauth_access_scopes)
# 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
env['JUPYTERHUB_USER'] = self.user.name

View File

@@ -894,18 +894,18 @@ async def test_server_role_api_calls(
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']
service = {
'name': 'oas1',
'api_token': 'some-token',
'oauth_allowed_scopes': allowed_scopes,
'oauth_client_allowed_scopes': allowed_scopes,
}
app.services.append(service)
app.init_services()
app_service = app.services[0]
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):

View File

@@ -439,7 +439,7 @@ async def test_hub_connect_url(db):
async def test_spawner_oauth_scopes(app, user):
allowed_scopes = ["read:users"]
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
await spawner.user.spawn()
oauth_client = spawner.orm_spawner.oauth_client

View File

@@ -670,7 +670,7 @@ class User:
client_id,
api_token,
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"
% (url_path_join(self.base_url, server_name) + '/'),
)