|
5 | 5 |
|
6 | 6 | import aiohttp
|
7 | 7 | from aiohttp import web
|
8 |
| -from traitlets import Instance, Integer, Unicode, default |
| 8 | +from traitlets import Bool, Instance, Integer, Unicode, default |
9 | 9 | from traitlets.config import LoggingConfigurable
|
10 | 10 |
|
11 | 11 | from .models import User
|
@@ -315,6 +315,49 @@ def _default_jupyterhub_api_url(self):
|
315 | 315 | raise ValueError("JUPYTERHUB_API_URL must be set")
|
316 | 316 | return out
|
317 | 317 |
|
| 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 | + |
318 | 361 | tls_key = Unicode(
|
319 | 362 | "",
|
320 | 363 | help="""
|
@@ -386,9 +429,33 @@ async def authenticate(self, request):
|
386 | 429 |
|
387 | 430 | if resp.status < 400:
|
388 | 431 | 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 | + |
389 | 456 | # "groups" attribute doesn't exists in case of a service
|
390 | 457 | return User(
|
391 |
| - data["name"], |
| 458 | + username, |
392 | 459 | groups=data.get("groups", []),
|
393 | 460 | admin=data.get("admin", False),
|
394 | 461 | )
|
|
0 commit comments