From 41ac76a7c8e789c26ea24e65b4086c522047cb9c Mon Sep 17 00:00:00 2001 From: Tyler Potts <49161327+tylerpotts@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:43:18 -0500 Subject: [PATCH 1/2] feat(sharing): default user-role share scopes and Keycloak group sync jhub-apps builds the 'Individuals and group access' dropdown from (other hub users + hub groups) filtered by the requesting user's token scopes. Stock deployments of this chart shipped neither half: JupyterHub's default user role is self-only (so even admins resolve an empty visible-users list), and nothing mirrors Keycloak groups into JupyterHub, so the group list is always empty. Net effect: the sharing feature was dead on every deployment. - hub.loadRoles.user: extend the user role with read:users:name, read:groups:name, shares!user - the scopes jhub-apps' reference jupyterhub_config.py prescribes. Deployers opt out by overriding the role to scopes: [self] (read:users:name implies username enumeration, inherent to sharing by name; documented in the values comment). - hub.config.KeyCloakOAuthenticator.manage_groups: true - standard oauthenticator trait, inert under the dummy authenticator; the pack's KeyCloakOAuthenticator.update_auth_model defers to super() so the groups claim flows through untouched. Fixes #188 --- values.yaml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/values.yaml b/values.yaml index 2ba3062..96b7ba5 100644 --- a/values.yaml +++ b/values.yaml @@ -531,6 +531,16 @@ jupyterhub: # For production, use GenericOAuthenticator with Keycloak (see below) authenticator_class: dummy + # Mirror each user's Keycloak groups into JupyterHub groups at login so + # groups can be jhub-apps share targets (#188) - without this the hub + # has no groups at all and the "Individuals and group access" dropdown + # can never list one. Standard oauthenticator trait (the pack's + # KeyCloakOAuthenticator subclasses GenericOAuthenticator); inert under + # the dummy authenticator. A group appears once one of its members + # logs in. Opt out by overriding to false. + KeyCloakOAuthenticator: + manage_groups: true + # ============================================================ # Optional: Keycloak/OAuth Configuration # ============================================================ @@ -555,6 +565,27 @@ jupyterhub: # - email # ============================================================ + # jhub-apps "Individuals and group access": the share dropdown is built + # from (other hub users + hub groups) filtered by the requesting user's + # TOKEN scopes. JupyterHub's default `user` role is self-only, so without + # this extension the dropdown is empty for every user - admins included + # (#188). These are the scopes jhub-apps' reference jupyterhub_config.py + # prescribes for sharing. + # + # NOTE: read:users:name lets any authenticated user enumerate all + # usernames - inherent to sharing by name. Opt out by overriding this + # role to scopes: [self]. + # + # `self` is repeated deliberately: naming a role in load_roles REPLACES + # its default definition rather than extending it. + loadRoles: + user: + scopes: + - self + - read:users:name + - read:groups:name + - shares!user + # Mount custom config files from ConfigMap + the operator-provisioned # Keycloak OIDC client Secret. 00-gateway-auth.py reads client-id / # client-secret / issuer-url from /etc/oauth/ when KC OAuth is wired. From 2c8459d61cbadaf0bf7144fda4b5488cc63bf634 Mon Sep 17 00:00:00 2001 From: Tyler Potts <49161327+tylerpotts@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:47:35 -0500 Subject: [PATCH 2/2] fix(sharing): extend the user role in place, not via hub.loadRoles install_jhub_apps appends its own 'user' role to load_roles, and z2jh's hub.loadRoles loop runs before config.d - the values route produces two roles named user and JupyterHub aborts startup with 'Role user multiply defined' (caught on a live deployment). Extend the existing role's scopes right after install_jhub_apps instead, the pattern jhub-apps' reference config uses, gated on custom.sharing-scopes-enabled (default true). --- config/jupyterhub/02-jhub-apps.py | 22 ++++++++++++++++++++++ values.yaml | 21 --------------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/config/jupyterhub/02-jhub-apps.py b/config/jupyterhub/02-jhub-apps.py index 5a8c0c8..31acc0f 100644 --- a/config/jupyterhub/02-jhub-apps.py +++ b/config/jupyterhub/02-jhub-apps.py @@ -73,6 +73,28 @@ # Install jhub-apps (sets up service, roles, etc.) c = install_jhub_apps(c, spawner_to_subclass=KubeSpawner) +# Extend the `user` role with the scopes the jhub-apps sharing dropdown +# ("Individuals and group access") needs: the dropdown is (other hub users + +# hub groups) filtered by the requesting user's token scopes, and without +# read:users:name / read:groups:name it is empty for every user - admins +# included (#188). This MUST extend, in place, the `user` role that +# install_jhub_apps just appended to load_roles: defining a second role via +# z2jh's hub.loadRoles (which runs before this file) makes JupyterHub abort +# startup with "Role user multiply defined". Mirrors jhub-apps' reference +# jupyterhub_config.py. +# +# NOTE: read:users:name lets any authenticated user enumerate all usernames, +# inherent to sharing by name. Opt out via +# ``jupyterhub.custom.sharing-scopes-enabled: false``. +if get_config("custom.sharing-scopes-enabled", True): + for _role in c.JupyterHub.load_roles: + if _role.get("name") == "user": + _role["scopes"] = sorted( + set(_role["scopes"]) + | {"read:users:name", "read:groups:name", "shares!user"} + ) + break + # Forward JUPYTERHUB_OIDC_CLIENT_SECRET to the jhub-apps subprocess so that # 03-nebi-envs.py (which is re-evaluated inside the subprocess via # get_jupyterhub_config()) can read it for Keycloak token exchange. diff --git a/values.yaml b/values.yaml index 96b7ba5..43300cc 100644 --- a/values.yaml +++ b/values.yaml @@ -565,27 +565,6 @@ jupyterhub: # - email # ============================================================ - # jhub-apps "Individuals and group access": the share dropdown is built - # from (other hub users + hub groups) filtered by the requesting user's - # TOKEN scopes. JupyterHub's default `user` role is self-only, so without - # this extension the dropdown is empty for every user - admins included - # (#188). These are the scopes jhub-apps' reference jupyterhub_config.py - # prescribes for sharing. - # - # NOTE: read:users:name lets any authenticated user enumerate all - # usernames - inherent to sharing by name. Opt out by overriding this - # role to scopes: [self]. - # - # `self` is repeated deliberately: naming a role in load_roles REPLACES - # its default definition rather than extending it. - loadRoles: - user: - scopes: - - self - - read:users:name - - read:groups:name - - shares!user - # Mount custom config files from ConfigMap + the operator-provisioned # Keycloak OIDC client Secret. 00-gateway-auth.py reads client-id / # client-secret / issuer-url from /etc/oauth/ when KC OAuth is wired.