mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 10:04:07 +00:00
call client-allowed scopes JUPYTERHUB_OAUTH_CLIENT_ALLOWED_SCOPES
This commit is contained in:
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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:
|
||||||
|
@@ -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',
|
||||||
],
|
],
|
||||||
|
@@ -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.
|
||||||
|
|
||||||
|
@@ -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'],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
|
@@ -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):
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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):
|
||||||
|
@@ -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
|
||||||
|
@@ -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) + '/'),
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user