mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 10:04:07 +00:00
384 lines
12 KiB
Markdown
384 lines
12 KiB
Markdown
(using-jupyterhub-rest-api)=
|
|
|
|
# Using JupyterHub's REST API
|
|
|
|
This section will give you information on:
|
|
|
|
- What you can do with the API
|
|
- How to create an API token
|
|
- Assigning permissions to a token
|
|
- Updating to admin services
|
|
- Making an API request programmatically using the requests library
|
|
- Paginating API requests
|
|
- Enabling users to spawn multiple named-servers via the API
|
|
- Learn more about JupyterHub's API
|
|
|
|
Before we discuss about JupyterHub's REST API, you can learn about [REST APIs here](https://en.wikipedia.org/wiki/Representational_state_transfer). A REST
|
|
API provides a standard way for users to get and send information to the
|
|
Hub.
|
|
|
|
## What you can do with the API
|
|
|
|
Using the [JupyterHub REST API](jupyterhub-rest-API), you can perform actions on the Hub,
|
|
such as:
|
|
|
|
- Checking which users are active
|
|
- Adding or removing users
|
|
- Adding or removing services
|
|
- Stopping or starting single user notebook servers
|
|
- Authenticating services
|
|
- Communicating with an individual Jupyter server's REST API
|
|
|
|
## Create an API token
|
|
|
|
To send requests using the JupyterHub API, you must pass an API token with
|
|
the request.
|
|
|
|
While JupyterHub is running, any JupyterHub user can request a token via the `token` page.
|
|
This is accessible via a `token` link in the top nav bar from the JupyterHub home page,
|
|
or at the URL `/hub/token`.
|
|
|
|
:::{figure-md}
|
|
|
|

|
|
|
|
JupyterHub's API token page
|
|
:::
|
|
|
|
:::{figure-md}
|
|

|
|
|
|
JupyterHub's token page after successfully requesting a token.
|
|
|
|
:::
|
|
|
|
### Register API tokens via configuration
|
|
|
|
Sometimes, you'll want to pre-generate a token for access to JupyterHub,
|
|
typically for use by external services,
|
|
so that both JupyterHub and the service have access to the same value.
|
|
|
|
First, you need to generate a good random secret.
|
|
A good way of generating an API token is by running:
|
|
|
|
```bash
|
|
openssl rand -hex 32
|
|
```
|
|
|
|
This `openssl` command generates a random token that can be added to the JupyterHub configuration in `jupyterhub_config.py`.
|
|
|
|
For external services, this would be registered with JupyterHub via configuration:
|
|
|
|
```python
|
|
c.JupyterHub.services = [
|
|
{
|
|
"name": "my-service",
|
|
"api_token": the_secret_value,
|
|
},
|
|
]
|
|
```
|
|
|
|
At this point, requests authenticated with the token will be associated with The service `my-service`.
|
|
|
|
```{note}
|
|
You can also load additional tokens for users via the `JupyterHub.api_tokens` configuration.
|
|
|
|
However, this option has been deprecated since the introduction of services.
|
|
```
|
|
|
|
## Assigning permissions to a token
|
|
|
|
Prior to JupyterHub 2.0, there were two levels of permissions:
|
|
|
|
1. user, and
|
|
2. admin
|
|
|
|
where a token would always have full permissions to do whatever its owner could do.
|
|
|
|
In JupyterHub 2.0,
|
|
specific permissions are now defined as '**scopes**',
|
|
and can be assigned both at the user/service level,
|
|
and at the individual token level.
|
|
The previous behavior is represented by the scope `inherit`,
|
|
and is still the default behavior for requesting a token if limited permissions are not specified.
|
|
|
|
This allows e.g. a user with full admin permissions to request a token with limited permissions.
|
|
|
|
In JupyterHub 5.0, you can specify scopes for a token when requesting it via the `/hub/tokens` page as a space-separated list.
|
|
In JupyterHub 3.0 and later, you can also request tokens with limited scopes via the JupyterHub API (provided you already have a token!):
|
|
|
|
```python
|
|
import json
|
|
from urllib.parse import quote
|
|
|
|
import requests
|
|
|
|
def request_token(
|
|
username, *, api_token, scopes=None, expires_in=0, hub_url="http://127.0.0.1:8081"
|
|
):
|
|
"""Request a new token for a user"""
|
|
request_body = {}
|
|
if expires_in:
|
|
request_body["expires_in"] = expires_in
|
|
if scopes:
|
|
request_body["scopes"] = scopes
|
|
url = hub_url.rstrip("/") + f"/hub/api/users/{quote(username)}/tokens"
|
|
r = requests.post(
|
|
url,
|
|
data=json.dumps(request_body),
|
|
headers={"Authorization": f"token {api_token}"},
|
|
)
|
|
if r.status_code >= 400:
|
|
# extract error message for nicer error messages
|
|
r.reason = r.json().get("message", r.text)
|
|
r.raise_for_status()
|
|
# response is a dict and will include the token itself in the 'token' field,
|
|
# as well as other fields about the token
|
|
return r.json()
|
|
|
|
request_token("myusername", scopes=["list:users"], api_token="abc123")
|
|
```
|
|
|
|
## Updating to admin services
|
|
|
|
```{note}
|
|
The `api_tokens` configuration has been softly deprecated since the introduction of services.
|
|
We have no plans to remove it,
|
|
but deployments are encouraged to use service configuration instead.
|
|
```
|
|
|
|
If you have been using `api_tokens` to create an admin user
|
|
and the token for that user to perform some automations, then
|
|
the services' mechanism may be a better fit if you have the following configuration:
|
|
|
|
```python
|
|
c.JupyterHub.admin_users = {"service-admin"}
|
|
c.JupyterHub.api_tokens = {
|
|
"secret-token": "service-admin",
|
|
}
|
|
```
|
|
|
|
This can be updated to create a service, with the following configuration:
|
|
|
|
```python
|
|
c.JupyterHub.services = [
|
|
{
|
|
# give the token a name
|
|
"name": "service-admin",
|
|
"api_token": "secret-token",
|
|
# "admin": True, # if using JupyterHub 1.x
|
|
},
|
|
]
|
|
|
|
# roles were introduced in JupyterHub 2.0
|
|
# prior to 2.0, only "admin": True or False was available
|
|
|
|
c.JupyterHub.load_roles = [
|
|
{
|
|
"name": "service-role",
|
|
"scopes": [
|
|
# specify the permissions the token should have
|
|
"admin:users",
|
|
],
|
|
"services": [
|
|
# assign the service the above permissions
|
|
"service-admin",
|
|
],
|
|
}
|
|
]
|
|
```
|
|
|
|
The token will have the permissions listed in the role
|
|
(see [scopes][] for a list of available permissions),
|
|
but there will no longer be a user account created to house it.
|
|
The main noticeable difference between a user and a service is that there will be no notebook server associated with the account
|
|
and the service will not show up in the various user list pages and APIs.
|
|
|
|
## Make an API request
|
|
|
|
To authenticate your requests, pass the API token in the request's
|
|
Authorization header.
|
|
|
|
### Use requests
|
|
|
|
Using the popular Python [requests](https://docs.python-requests.org)
|
|
library, an API GET request is made to [/users](rest-api-get-users), and the request sends an API token for
|
|
authorization. The response contains information about the users, here's example code to make an API request for the users of a JupyterHub deployment
|
|
|
|
```python
|
|
import requests
|
|
|
|
api_url = 'http://127.0.0.1:8081/hub/api'
|
|
|
|
r = requests.get(api_url + '/users',
|
|
headers={
|
|
'Authorization': f'token {token}',
|
|
}
|
|
)
|
|
|
|
r.raise_for_status()
|
|
users = r.json()
|
|
```
|
|
|
|
This example provides a slightly more complicated request (to [/groups/formgrade-data301/users](rest-api-post-group-users)), yet the
|
|
process is very similar:
|
|
|
|
```python
|
|
import requests
|
|
|
|
api_url = 'http://127.0.0.1:8081/hub/api'
|
|
|
|
data = {'name': 'mygroup', 'users': ['user1', 'user2']}
|
|
|
|
r = requests.post(api_url + '/groups/formgrade-data301/users',
|
|
headers={
|
|
'Authorization': f'token {token}',
|
|
},
|
|
json=data,
|
|
)
|
|
r.raise_for_status()
|
|
r.json()
|
|
```
|
|
|
|
The same API token can also authorize access to the [Jupyter Notebook REST API][]
|
|
|
|
provided by notebook servers managed by JupyterHub if it has the necessary `access:servers` scope.
|
|
|
|
(api-pagination)=
|
|
|
|
## Paginating API requests
|
|
|
|
```{versionadded} 2.0
|
|
|
|
```
|
|
|
|
Pagination is available through the `offset` and `limit` query parameters on
|
|
list endpoints, which can be used to return ideally sized windows of results.
|
|
Here's example code demonstrating pagination on the [`GET /users`](rest-api-get-users)
|
|
endpoint to fetch the first 20 records.
|
|
|
|
```python
|
|
import os
|
|
import requests
|
|
|
|
api_url = 'http://127.0.0.1:8081/hub/api'
|
|
|
|
r = requests.get(
|
|
api_url + '/users?offset=0&limit=20',
|
|
headers={
|
|
"Accept": "application/jupyterhub-pagination+json",
|
|
"Authorization": f"token {token}",
|
|
},
|
|
)
|
|
r.raise_for_status()
|
|
r.json()
|
|
```
|
|
|
|
For backward-compatibility, the default structure of list responses is unchanged.
|
|
However, this lacks pagination information (e.g. is there a next page),
|
|
so if you have enough users that they won't fit in the first response,
|
|
it is a good idea to opt-in to the new paginated list format.
|
|
There is a new schema for list responses which include pagination information.
|
|
You can request this by including the header:
|
|
|
|
```
|
|
Accept: application/jupyterhub-pagination+json
|
|
```
|
|
|
|
with your request, in which case a response will look like:
|
|
|
|
```python
|
|
{
|
|
"items": [
|
|
{
|
|
"name": "username",
|
|
"kind": "user",
|
|
...
|
|
},
|
|
],
|
|
"_pagination": {
|
|
"offset": 0,
|
|
"limit": 20,
|
|
"total": 50,
|
|
"next": {
|
|
"offset": 20,
|
|
"limit": 20,
|
|
"url": "http://127.0.0.1:8081/hub/api/users?limit=20&offset=20"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
where the list results (same as pre-2.0) will be in `items`,
|
|
and pagination info will be in `_pagination`.
|
|
The `next` field will include the `offset`, `limit`, and `url` for requesting the next page.
|
|
`next` will be `null` if there is no next page.
|
|
|
|
Pagination is governed by two configuration options:
|
|
|
|
- `JupyterHub.api_page_default_limit` - the page size, if `limit` is unspecified in the request
|
|
and the new pagination API is requested
|
|
(default: 50)
|
|
- `JupyterHub.api_page_max_limit` - the maximum page size a request can ask for (default: 200)
|
|
|
|
Pagination is enabled on the `GET /users`, `GET /groups`, and `GET /proxy` REST endpoints.
|
|
|
|
## Enabling users to spawn multiple named-servers via the API
|
|
|
|
Support for multiple servers per user was introduced in JupyterHub [version 0.8.](changelog)
|
|
Prior to that, each user could only launch a single default server via the API
|
|
like this:
|
|
|
|
```bash
|
|
curl -X POST -H "Authorization: token <token>" "http://127.0.0.1:8081/hub/api/users/<user>/server"
|
|
```
|
|
|
|
With the named-server functionality, it's now possible to launch more than one
|
|
specifically named servers against a given user. This could be used, for instance,
|
|
to launch each server based on a different image.
|
|
|
|
First you must enable named-servers by including the following setting in the `jupyterhub_config.py` file.
|
|
|
|
`c.JupyterHub.allow_named_servers = True`
|
|
|
|
If you are using the [zero-to-jupyterhub-k8s](https://github.com/jupyterhub/zero-to-jupyterhub-k8s) set-up to run JupyterHub,
|
|
then instead of editing the `jupyterhub_config.py` file directly, you could pass
|
|
the following as part of the `config.yaml` file, as per the [tutorial](https://z2jh.jupyter.org/en/latest/):
|
|
|
|
```bash
|
|
hub:
|
|
extraConfig: |
|
|
c.JupyterHub.allow_named_servers = True
|
|
```
|
|
|
|
With that setting in place, a new named-server is activated like this:
|
|
|
|
```{parsed-literal}
|
|
[POST /api/users/:username/servers/:servername](rest-api-post-user-server-name)
|
|
```
|
|
|
|
e.g.
|
|
|
|
```bash
|
|
curl -X POST -H "Authorization: token <token>" "http://127.0.0.1:8081/hub/api/users/<user>/servers/<serverA>"
|
|
curl -X POST -H "Authorization: token <token>" "http://127.0.0.1:8081/hub/api/users/<user>/servers/<serverB>"
|
|
```
|
|
|
|
The same servers can be [stopped](rest-api-delete-user-server-name) by substituting `DELETE` for `POST` above.
|
|
|
|
### Some caveats for using named-servers
|
|
|
|
For named-servers via the API to work, the spawner used to spawn these servers
|
|
will need to be able to handle the case of multiple servers per user and ensure
|
|
uniqueness of names, particularly if servers are spawned via docker containers
|
|
or kubernetes pods.
|
|
|
|
## Learn more about the API
|
|
|
|
You can see the full [JupyterHub REST API](jupyterhub-rest-api) for more details.
|
|
|
|
[openapi initiative]: https://www.openapis.org/
|
|
[jupyterhub rest api]: ./rest-api
|
|
[scopes]: ../rbac/scopes.md
|
|
[jupyter notebook rest api]: https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/HEAD/notebook/services/api/api.yaml
|