RTC and custom authenticator with authenticator-managed roles

Hi,

I’m currently trying to implement real-time collaboration without impersonation, but using a custom authenticator that supports authenticator-managed roles. My strategy is as follows:

  • Each collaboration gets it’s own “collaboration account”
  • The authenticator assigns each real user a “collaboration role”
  • The collaboration role grants server access to the collaboration account

Unlike in the RTC example linked above, no “collaboration groups” are used.

I’ve implemented my own authenticator as follows (irrelevant code removed):

class MyOAuthenticator(OAuthenticator):
  manage_roles = True
  reset_managed_roles_on_startup = True
  ...

  async def update_auth_model(self, auth_model):
    auth_model = await super().update_auth_model(auth_model)
    user_collabs = self._get_collabs(username=auth_model["name"])
    # self._create_collabs(user_collabs)  # TODO create collaboration accounts
    auth_model["roles"] = self._get_collab_roles(user_collabs)
    return auth_model

  async def load_managed_roles(self):
    collabs = self._get_collabs()
    return self._get_collab_roles(collabs)

  def _get_collabs(self, username=None):
    ...  # return all/user-specific collaboration accounts

  def _get_collab_roles(self, collabs):
    return [
      {
        "name": collab,
        "scopes": [
          "admin-ui",
          f"list:users!user={collab}",
          f"admin:servers!user={collab}",
          f"access:servers!user={collab}",
        ],
      }
      for collab in collabs
    ]

If I understand correctly, the authenticator doesn’t automatically create non-existing collaboration accounts that are specified in "scopes", and I will therefore have to explicitly create these accounts during authentication. How could I best implement _create_collabs(self, collabs: list[str]) -> None to do this (authenticator methods, REST API calls, ORM functionality, …)?

For reference, I’m deploying the JupyterHub Helm chart 4.1.0 (JupyterHub 5.2.1) on a K3s cluster behind a Traefik reverse proxy.

Thanks in advance!

Mostly solved, largely based on Programmatically adding roles for collaboration groups for RTC in a custom Authenticator - #4 by mr_z_ro. Briefly, here is what I changed:

  • update_auth_model: Similar to the “official” RTC instructions, users are now added to “collaborative groups” (same name as the “collaborative users”), primarily so that the list of collaborations is available to the post_auth_hook. The role assignments remained the same, but are now also applied to the corresponding “collaborative group”.
  • post_auth_hook (new): For each group in auth_model["groups"], a “collaborative user” (same name as the “collaborative group”) is created using handler.auth_to_user. The user is assigned to a collaborative group, primarily to distinguish “collaborative users” from “real users” in the downstream pre_spawn_hook.
  • Both the “real users” and the “collaborative users” are also explicitly assigned a user role containing the self scope, because authenticator-managed roles override the default role assignments.

Huge thanks to @mr_z_ro for documenting his approach - without this, it would have taken me forever to stumble upon BaseHandler.auth_to_user and realize that the user role gets overridden! In general, I think the instructions on Real-time collaboration without impersonation — JupyterHub documentation could benefit from a revamp for authenticator-managed groups/roles following the release of JH5.

Side note: I’ve initially attempted to create the “collaborative users” using the REST API (i.e., created a service with an access token + added a corresponding service role to the authenticator). This didn’t work because of session state etc, but it also took me a while to realize that there was a deadlock when synchronously querying the REST API from within the authenticator (Deadlock when using REST API from Authenticator.post_auth_hook · Issue #3654 · jupyterhub/jupyterhub · GitHub).

Closing.