I have a common use case (I believe) that I have been banging my head on for the last week.
I have a set of users/students lets call them A, B and C. These users work together in different projects represented by groups (in my case by ldap groups) AB, AC where each user A and C is part of group AC etc. The number of groups and the members of each group are dynamic.
Now I want to enable “teamwork” in jupyterhub so that:
If a user A is part of a group (ie AC), user A should be able to login and starts its own notebook server (as normal) BUT also be able to start a shared notebook server for each team it is part of.
All members of group AC should be able to access this shared server (but not all servers) so User A and C should be able to access servers AC but user C should not access AB etc.
I think this is a rather common use case and it even have a tutorial for static memberships ( Real-time collaboration without impersonation — JupyterHub documentation ). However, making this dynamic (I cannot restart the hub every time a group changes) and allowing all users of a group to automatically spawn and access the shared servers are not so straight forward.
I have gotten almost there but it involves doing “brain surgery” on the database and adding “group users”, roles that allow all users of that (ldap) group access to the server of the “group_user”, a group to distinguish real users from “group_users” by inserting records directly into the database from post_auth_hooks and some rather awkward filtering of users in the spawn step. It feels it should be some easier way of doing this as I believe it is a rather common use case.
Am I designing this completely wrong or am I missing something fundamental (the amount of users and groups make it impractical use jupyterhub for manual user and group mgmt and/or create “group users” on the SSO).
We actually implemented this feature in the last couple of weeks to address the same use case you described. In particular, we made the following changes to get this work.
We developed a custom hub-managed JupyterHub service, allowing users to create groups, manage invites, etc. In the end, we store the group memberships in a database table.
For each group, we dynamically create a “collaboration user”, i.e., JupyterHub user, using JupyterHub’s REST API (see POST /users/{name}).
All members of the group will be granted access to start a server in the name of the collaboration user. Thus, every user can still run their personal servers, but also work together via the “collaboration user”. Access can be granted using the JupyterHub’s share endpoint (see POST /shares/{user}). We grant the scopes access:servers!server=<collab user>/ and admin:servers!server=<collab user>/. The first scope is necessary to actually access the server. The second scope is necessary to start the collab user’s server.
Access to the collab user can be revoked using empty scopes (e.g., if a member leaves a group).
Since we are providing multiple profiles, we ensured that the collaboration user has only access to one specific profile by returning only the said profile in the profile list if the user is a collaboration user (i.e., username starts with collab-)
Similarly, we fail the spawn if a regular user tries to run the group profile
If you have any further questions or a better way to achieve the same functionality, please let me know!
I got inspired by your solution and we are now doing something very similar. However, we opted for the less dynamic approach to only update the “roles” at login (post auth hook).
With this and the handler.user_from_username() function I am happy to say that I can avoid the ORM and low level db mgmt I did previously.
I am still not sure how “correct” it is to use handler.user_from_username() to create users on the fly and await user.save_auth_state() to save the extra auth_state settings in the post_auth_hook step but it mostly work (I seam to get some random hangs on login that I have not yet identified where they are).