Skip to content

Commit 9235074

Browse files
committed
support using JupyterHub service access scopes in JupyterHubAuth
- disabled by default for backward-compatibility - opt-in by setting jupyterhub_service_name - prefix service usernames so they don't collide with users
1 parent 05f05c4 commit 9235074

File tree

1 file changed

+69
-2
lines changed
  • dask-gateway-server/dask_gateway_server

1 file changed

+69
-2
lines changed

dask-gateway-server/dask_gateway_server/auth.py

+69-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import aiohttp
77
from aiohttp import web
8-
from traitlets import Instance, Integer, Unicode, default
8+
from traitlets import Bool, Instance, Integer, Unicode, default
99
from traitlets.config import LoggingConfigurable
1010

1111
from .models import User
@@ -315,6 +315,49 @@ def _default_jupyterhub_api_url(self):
315315
raise ValueError("JUPYTERHUB_API_URL must be set")
316316
return out
317317

318+
jupyterhub_service_name = Unicode(
319+
# should this be "dask-gateway"?
320+
# that would enable service scope enforcement by default
321+
"",
322+
help="""
323+
The name of dask-gateway as a jupyterhub service.
324+
325+
By default this is determined from the ``JUPYTERHUB_SERVICE_NAME``
326+
environment variable.
327+
""",
328+
config=True,
329+
)
330+
331+
@default("jupyterhub_service_name")
332+
def _default_jupyterhub_service_name(self):
333+
return os.environ.get("JUPYTERHUB_SERVICE_NAME", "")
334+
335+
use_service_access_scopes = Bool(
336+
help="""
337+
Require tokens to have `access:services!service={jupyterhub_service_name}` permissions
338+
in order to access the gateway.
339+
340+
Allows JupyterHub RBAC to controll access to dask-gateway.
341+
342+
Disabled by default for backward-compatibility, but strongly encouraged.
343+
Enabled by default if `jupyterhub_service_name` is set.
344+
""",
345+
config=True,
346+
)
347+
348+
@default("use_service_access_scopes")
349+
def _default_use_service_access_scopes(self):
350+
if self.jupyterhub_service_name:
351+
return True
352+
else:
353+
self.log.warning(
354+
"jupyterhub_service_name not set, "
355+
"any jupyterhub token may be used to create clusters. "
356+
"Set JupyterHubAuth.jupyterhub_service_name "
357+
"to use jupyterhub scopes to control access to dask-gateway."
358+
)
359+
return False
360+
318361
tls_key = Unicode(
319362
"",
320363
help="""
@@ -386,9 +429,33 @@ async def authenticate(self, request):
386429

387430
if resp.status < 400:
388431
data = await resp.json()
432+
# avoid collisions between user names and service names
433+
# 'kind' may be 'user' or 'service'
434+
username = data["name"]
435+
if data["kind"] != "user" or username.startswith(("user:", "service:")):
436+
# avoid collision without changing the name for users
437+
username = f"{data['kind']}:{username}"
438+
439+
scopes = data.get("scopes", [])
440+
if self.use_service_access_scopes:
441+
# check scopes for access permissions
442+
access_scopes = {
443+
"access:services",
444+
f"access:services!service={self.jupyterhub_service_name}",
445+
}
446+
have_scopes = set(scopes)
447+
if not access_scopes.intersection(have_scopes):
448+
self.log.debug(
449+
"Token for %r does not have access to service %r; has scopes: %s",
450+
username,
451+
self.jupyterhub_service_name,
452+
scopes,
453+
)
454+
raise unauthorized("jupyterhub")
455+
389456
# "groups" attribute doesn't exists in case of a service
390457
return User(
391-
data["name"],
458+
username,
392459
groups=data.get("groups", []),
393460
admin=data.get("admin", False),
394461
)

0 commit comments

Comments
 (0)