Files
jupyterhub/docs/source/reference/sharing.md

12 KiB

(sharing-reference)=

Sharing access to user servers

In order to make use of features like JupyterLab's real-time collaboration (RTC), multiple users must have access to a single server. There are a few ways to do this, but ultimately both users must have the appropriate access:servers scope. Prior to JupyterHub 5.0, this could only be granted via static role assignments in JupyterHub configuration. JupyterHub 5.0 adds the concept of a 'share', allowing users to grant each other limited access to their servers.

:::{seealso} Documentation on roles and scopes for more details on how permissions work in JupyterHub, and in particular access scopes. :::

In JupyterHub, shares:

  1. are 'granted' to a user or group
  2. grant only limited permissions (e.g. only 'access' or access and start/stop)
  3. may be revoked by anyone with the shares permissions
  4. may always be revoked by the shared-with user or group

Additionally a "share code" is a random string, which has all the same properties as a Share aside from the user or group. The code can be exchanged for actual sharing permission, to enable the pattern of sharing permissions without needing to know the username(s) of who you'd like to share with (e.g. email a link).

There is not yet UI to create shares, but they can be managed via JupyterHub's REST API.

In general, with shares you can:

  1. access other users' servers
  2. grant access to your servers
  3. see servers shared with you
  4. review and revoke permissions for servers you manage

Enable sharing

For safety, users do not have permission to share access to their servers by default. To grant this permission, a user must have the shares scope for their servers. To grant all users permission to share access to their servers:

c.JupyterHub.load_roles = [
    {
        "name": "user",
        "scopes": ["self", "shares!user"],
    },
]

With this, only the sharing via invitation code described below will be available.

Additionally, to share access with a specific user or group (more below), a user must have permission to read that user or group's name. To enable the full sharing API for all users:

c.JupyterHub.load_roles = [
    {
        "name": "user",
        "scopes": ["self", "shares!user", "read:users:name", "read:groups:name"],
    },
]

Note that this exposes the ability for all users to discover existing user and group names, which is part of why we have the share-by-code pattern, so users don't need this ability to share with each other.

Share or revoke access to a server

To modify who has access to a server, you need the permission shares with the appropriate server filter, and access to read the name of the target user or group (read:users:name or read:groups:name). You can only modify access to one server at a time.

Granting access to a server

To grant access to a particular user, in addition to shares, the granter must have at least read:user:name permission for the target user (or read:group:name if it's a group).

Send a POST request to /api/shares/:username/:servername to grant permissions.

[POST /api/shares/:username/:servername](rest-api-post-shares-server)

The JSON body should specify what permissions to grant and whom to grant them to:

{
    "scopes": [],
    "user": "username", # or:
    "group": "groupname",
}

It should have exactly one of "user" or "group" defined (not both). The specified user or group will be granted access to the target server.

If scopes is specified, all requested scopes must have the !server=:username/:servername filter applied. The default value for scopes is ["access:servers!server=:username/:servername"] (i.e. the 'access scope' for the server).

Revoke access

To revoke permissions, you need the permission shares with the appropriate server filter, and read:users:name (or read:groups:name) for the user or group to modify. You can only modify access to one server at a time.

Send a PATCH request to /api/shares/:username/:servername to revoke permissions.

[PATCH /api/shares/:username/:servername](rest-api-patch-shares-server)

The JSON body should specify the scopes to revoke

POST /api/shares/:username/:servername
{
    "scopes": [],
    "user": "username", # or:
    "group": "groupname",
}

If scopes is empty or unspecified, all scopes are revoked from the target user or group.

Revoke all permissions

A DELETE request will revoke all shared access permissions for the given server.

[DELETE /api/shares/:username/:servername](rest-api-delete-shares-server)

View shares for a server

To view shares for a given server, you need the permission read:shares with the appropriate server filter.

[GET /api/shares/:username/:servername](rest-api-get-shares-server)

This is a paginated endpoint, so responses has items as a list of Share models, and _pagination for information about retrieving all shares if there are many:

{
  "items": [
    {
      "server": {...},
      "scopes": ["access:servers!server=sharer/"],
      "user": {
        "name": "shared-with",
      },
      "group": None, # or {"name": "groupname"},
      ...
    },
    ...
  ],
  "_pagination": {
    "total": 5,
    "limit": 50,
    "offset": 0,
    "next": None,
  },
}

see the rest-api for full details of the response models.

View servers shared with user or group

To review servers shared with a given user or group, you need the permission read:users:shares or read:groups:shares with the appropriate user or group filter.

[GET /api/users/:username/shared](rest-api-get-user-shared)

or

[GET /api/groups/:groupname/shared](rest-api-get-group-shared)

These are paginated endpoints.

Access permission for a single user on a single server

[GET /api/users/:username/shared/:ownername/:servername](rest-api-get-user-shared-server)

or

[GET /api/groups/:groupname/shared/:ownername/:servername](rest-api-get-group-shared-server)

will return the single Share info for the given user or group for the server specified by ownername/servername, or 404 if no access is granted.

Revoking one's own permissions for a server

To revoke sharing permissions from the perspective of the user or group being shared with, you need the permissions users:shares or groups:shares with the appropriate user or group filter. This allows users to 'leave' shared servers, without needing permission to manage the server's sharing permissions.

[DELETE /api/users/:username/shared/:ownername/:servername](rest-api-delete-user-shared-server)

or

[DELETE /api/groups/:groupname/shared/:ownername/:servername](rest-api-delete-group-shared-server)

will revoke all permissions granted to the user or group for the specified server.

The Share model

A Share returned in the REST API has the following structure:

{
    "server": {
        "name": "servername",
        "user": {
          "name": "ownername"
        },
        "url": "/users/ownername/servername/",
        "ready": True,

    },
    "scopes": ["access:servers!server=username/servername"],
    "user": { # or None
        "name": "username",
    },
    "group": None, # or {"name": "groupname"},
    "created_at": "2023-10-02T13:27Z",
}

where exactly one of user and group is not null and the other is null.

See the rest-api for full details of the response models.

Share via invitation code

Sometimes you would like to share access to a server with one or more users, but you don't want to deal with collecting everyone's username. For this, you can create shares via share code. This is identical to sharing with a user, only it adds the step where the sharer creates the code and distributes the code to one or more users, then the users themselves exchange the code for actual sharing permissions.

Share codes are much like shares, except:

  1. they don't associate with specific users
  2. they can be used multiple times, by more than one user (i.e. send one invite email to several recipients)
  3. they expire (default: 1 day)
  4. they can only be accepted by individual users, not groups

Creating share codes

To create a share code:

[POST /api/share-code/:username/:servername](rest-api-post-share-code)

where the body should include the scopes to be granted and expiration. Share codes must expire.

{
  "scopes": ["access:servers!server=:user/:server"],
  "expires_in": 86400, # seconds, default: 1 day
}

If no scopes are specified, the access scope for the specified server will be used. If no expiration is specified, the code will expire in one day (86400 seconds).

The response contains the code itself:

{
  "code": "abc1234....",
  "accept_url": "/hub/accept-share?code=abc1234",
  "id": "sc_1234",
  "scopes": [...],
  ...
}

See the rest-api for full details of the response models.

Accepting sharing invitations

Sharing invitations can be accepted by visiting:

/hub/accept-share/?code=:share-code

where you will be able to confirm the permissions you would like to accept. After accepting permissions, you will be redirected to the running server.

If the server is not running and you have not also been granted permission to start it, you will need to contact the owner of the server to start it.

Listing existing invitations

You can see existing invitations for

[GET /hub/api/share-codes/:username/:servername](rest-api-get-share-codes-server)

which produces a paginated list of share codes (excluding the codes themselves, which are not stored by jupyterhub):

{
  "items": [
    {
      "id": "sc_1234",
      "exchange_count": 0,
      "last_exchanged_at": None,
      "scopes": ["access:servers!server=username/servername"],
      "server": {
        "name": "",
        "user": {
          "name": "username",
        },
      },
      ...
    }
  ],
  "_pagination": {
    "total": 5,
    "limit": 50,
    "offset": 0,
    "next": None,
  }
}

see the rest-api for full details of the response models.

Share code model

A Share Code returned in the REST API has most of the same fields as a Share, but lacks the association with a user or group, and adds information about exchanges of the share code, and the id that can be used for revocation:

{

  # common share fields
  "server": {
    "user": {
      "name": "sharer"
    },
    "name": "",
    "url": "/user/sharer/",
    "ready": True,
  },
  "scopes": [
    "access:servers!server=sharer/"
  ],
  # share-code-specific fields
  "id": "sc_1",
  "created_at": "2024-01-23T11:46:32.154416Z",
  "expires_at": "2024-01-24T11:46:32.153582Z",
  "exchange_count": 1,
  "last_exchanged_at": "2024-01-23T11:46:43.589701Z"
}

see the rest-api for full details of the response models.

Revoking invitations

If you've finished inviting users to a server, you can revoke all invitations with:

[DELETE /hub/api/share-codes/:username/:servername](rest-api-delete-share-code)

or revoke a single invitation code:

DELETE /hub/api/share-codes/:username/:servername?code=:thecode

You can also revoke a code by id, if you non longer have the code:

DELETE /hub/api/share-codes/:username/:servername?id=sc_123

where the id is retrieved from the share-code model, e.g. when listing current share codes.