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.
See also
Documentation on roles and scopes for more details on how permissions work in JupyterHub, and in particular access scopes.
In JupyterHub, shares:
are ‘granted’ to a user or group
grant only limited permissions (e.g. only ‘access’ or access and start/stop)
may be revoked by anyone with the
shares
permissionsmay 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:
access other users’ servers
grant access to your servers
see servers shared with you
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
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
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
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
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
or
GET /api/groups/:groupname/shared
These are paginated endpoints.
Access permission for a single user on a single server#
GET /api/users/:username/shared/:ownername/:servername
or
GET /api/groups/:groupname/shared/:ownername/:servername
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:
they don’t associate with specific users
they can be used multiple times, by more than one user (i.e. send one invite email to several recipients)
they expire (default: 1 day)
they can only be accepted by individual users, not groups
Creating share codes#
To create a share code:
POST /api/share-codes/:username/:servername
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",
"full_accept_url": "https://hub.example.org/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
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
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.