Hi,
To answer my own post if anyone is interested in the solution we finally use (soon at least ) it is available here: jupyterhub/jupyterhub_config.py · 117-next-version-rtc · csma / jupyterhub · GitLab
There is however many site specific bits in the code above, below is a simplified stub .
In overview we do :
- Enable save authstate and “manage_roles”
- create “post_auth_hook” to create “collab_users” and assign roles dynamically based on (in our case) ldap group membership and save information in auth_state.
- Override refresh_user to not check/update information for “collab_users”
The main config :
c.JupyterHub.authenticator_class = ( myGitLabOAuthenticator # We override refresh_user to differentiate bwteeen different users ) c.Authenticator.enable_auth_state = True c.Authenticator.manage_roles = True c.Authenticator.post_auth_hook = post_auth_hook
The overrides and hook to create the “collab_users” and assign roles to users
class myGitLabOAuthenticator(GitLabOAuthenticator):
async def refresh_user(self, user, handler=None):
auth_state = await user.get_auth_state() or {}
# Skip refresh check for group accounts
if auth_state["collab_user"]:
return True # No refresh needed
# Update the auth_state
auth_model = await super().refresh_user(user, handler)
if auth_model:
auth_state = auth_model["auth_state"]
gitlab_user = auth_state["gitlab_user"]
auth_state["collab_user"] = False
auth_state["ldap_info"] = await get_ldap_info(self.log, LDAP_URL, user.name, gitlab_user)
auth_state["membership_info"] = get_direct_membership_from_jwt_token(auth_state)
auth_model["auth_state"] = auth_state
return auth_model
async def post_auth_hook(authenticator, handler, authentication):
"""
Sync LDAP groups to JupyterHub roles after authentication.
Used to enable RTC Sharing and enable group based mounting
ldap_info: uid, gid and groups
gl_info: memebership check for predefined gitlab groups, (gpu, sudo and gpu_super) and ssh keys
"""
username = authentication["name"]
authenticator.log.info(f"[POST_AUTH] Enter for {username}")
auth_state = authentication.get("auth_state", {})
authenticator.log.debug(f"[POST_AUTH] {auth_state=}")
gitlab_user = auth_state["gitlab_user"]
auth_state["collab_user"] = False # Collab users never login, always true ?
auth_state["ldap_info"] = await get_ldap_info(authenticator.log, LDAP_URL, username, gitlab_user)
auth_state["membership_info"] = get_direct_membership_from_jwt_token(auth_state)
authentication["auth_state"] = auth_state
authentication["roles"] = DEFAULT_USER_ROLES
# Add collaborative role definitions defined by ldap_groups, pre-filtered to only include projects as groups
if ALLOW_RTC:
authentication["roles"] += await create_collab_users_and_roles(
authenticator.log, handler, auth_state["ldap_info"]["unixGroups"].values(), username
)
return authentication
The code to create “collab_users” and return the related roles to allow access to servers.
async def create_collab_users_and_roles(log, handler, ldap_groups, username):
extra_roles = []
for gid, group_name in ldap_groups:
collab_username = role_name = group_name
if collab_user := handler.user_from_username(collab_username):
collab_auth_state = {
"ldap_info": {"uidNumber": gid, "gidNumber": gid, "unixGroups": {}},
"membership_info": [],
"collab_user": True,
}
await collab_user.save_auth_state(collab_auth_state)
role = {
"name": role_name,
"description": f"Role to allow users in group {group_name} to fully manage the server for {collab_username}",
"scopes": [
f"admin:servers!user={collab_username}",
f"servers!user={collab_username}",
f"access:servers!user={collab_username}",
"admin-ui",
f"list:users!user={collab_username}",
f"read:users!user={collab_username}",
],
}
extra_roles.append(role)
return extra_roles
To access the “shared group server” as users first logins normally and then can access:
<jupyterhub_url>/hub/spawn/groupname
(groupname == collab_user.username == cn of ldap group )
The caveat is that while it works for us more testing and vetting need to be done to ensure it works and do not introduce bugs in other installations.
We have for instance no cleanup of “old users” as we reset the jupyterhub database on every restart of jupyerhub anyway.