mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-10 19:43:01 +00:00

Removes the explicit extension of the target if it's in the same document. This allows these links to work in GitHub or in Read the Docs.
360 lines
13 KiB
Markdown
360 lines
13 KiB
Markdown
# Services
|
|
|
|
With version 0.7, JupyterHub adds support for **Services**.
|
|
|
|
This section provides the following information about Services:
|
|
|
|
- [Definition of a Service](#definition-of-a-service)
|
|
- [Properties of a Service](#properties-of-a-service)
|
|
- [Hub-Managed Services](#hub-managed-services)
|
|
- [Launching a Hub-Managed Service](#launching-a-hub-managed-service)
|
|
- [Externally-Managed Services](#externally-managed-services)
|
|
- [Writing your own Services](#writing-your-own-services)
|
|
- [Hub Authentication and Services](#hub-authentication-and-services)
|
|
|
|
## Definition of a Service
|
|
|
|
When working with JupyterHub, a **Service** is defined as a process that interacts
|
|
with the Hub's REST API. A Service may perform a specific or
|
|
action or task. For example, the following tasks can each be a unique Service:
|
|
|
|
- shutting down individuals' single user notebook servers that have been idle
|
|
for some time
|
|
- registering additional web servers which should use the Hub's authentication
|
|
and be served behind the Hub's proxy.
|
|
|
|
Two key features help define a Service:
|
|
|
|
- Is the Service **managed** by JupyterHub?
|
|
- Does the Service have a web server that should be added to the proxy's
|
|
table?
|
|
|
|
Currently, these characteristics distinguish two types of Services:
|
|
|
|
- A **Hub-Managed Service** which is managed by JupyterHub
|
|
- An **Externally-Managed Service** which runs its own web server and
|
|
communicates operation instructions via the Hub's API.
|
|
|
|
## Properties of a Service
|
|
|
|
A Service may have the following properties:
|
|
|
|
- `name: str` - the name of the service
|
|
- `admin: bool (default - false)` - whether the service should have
|
|
administrative privileges
|
|
- `url: str (default - None)` - The URL where the service is/should be. If a
|
|
url is specified for where the Service runs its own web server,
|
|
the service will be added to the proxy at `/services/:name`
|
|
- `api_token: str (default - None)` - For Externally-Managed Services you need to specify
|
|
an API token to perform API requests to the Hub
|
|
|
|
If a service is also to be managed by the Hub, it has a few extra options:
|
|
|
|
- `command: (str/Popen list`) - Command for JupyterHub to spawn the service.
|
|
- Only use this if the service should be a subprocess.
|
|
- If command is not specified, the Service is assumed to be managed
|
|
externally.
|
|
- If a command is specified for launching the Service, the Service will
|
|
be started and managed by the Hub.
|
|
- `environment: dict` - additional environment variables for the Service.
|
|
- `user: str` - the name of a system user to manage the Service. If
|
|
unspecified, run as the same user as the Hub.
|
|
|
|
## Hub-Managed Services
|
|
|
|
A **Hub-Managed Service** is started by the Hub, and the Hub is responsible
|
|
for the Service's actions. A Hub-Managed Service can only be a local
|
|
subprocess of the Hub. The Hub will take care of starting the process and
|
|
restarts it if it stops.
|
|
|
|
While Hub-Managed Services share some similarities with notebook Spawners,
|
|
there are no plans for Hub-Managed Services to support the same spawning
|
|
abstractions as a notebook Spawner.
|
|
|
|
If you wish to run a Service in a Docker container or other deployment
|
|
environments, the Service can be registered as an
|
|
**Externally-Managed Service**, as described below.
|
|
|
|
## Launching a Hub-Managed Service
|
|
|
|
A Hub-Managed Service is characterized by its specified `command` for launching
|
|
the Service. For example, a 'cull idle' notebook server task configured as a
|
|
Hub-Managed Service would include:
|
|
|
|
- the Service name,
|
|
- admin permissions, and
|
|
- the `command` to launch the Service which will cull idle servers after a
|
|
timeout interval
|
|
|
|
This example would be configured as follows in `jupyterhub_config.py`:
|
|
|
|
```python
|
|
c.JupyterHub.services = [
|
|
{
|
|
'name': 'cull-idle',
|
|
'admin': True,
|
|
'command': ['python', '/path/to/cull-idle.py', '--timeout']
|
|
}
|
|
]
|
|
```
|
|
|
|
A Hub-Managed Service may also be configured with additional optional
|
|
parameters, which describe the environment needed to start the Service process:
|
|
|
|
- `environment: dict` - additional environment variables for the Service.
|
|
- `user: str` - name of the user to run the server if different from the Hub.
|
|
Requires Hub to be root.
|
|
- `cwd: path` directory in which to run the Service, if different from the
|
|
Hub directory.
|
|
|
|
The Hub will pass the following environment variables to launch the Service:
|
|
|
|
```bash
|
|
JUPYTERHUB_SERVICE_NAME: The name of the service
|
|
JUPYTERHUB_API_TOKEN: API token assigned to the service
|
|
JUPYTERHUB_API_URL: URL for the JupyterHub API (default, http://127.0.0.1:8080/hub/api)
|
|
JUPYTERHUB_BASE_URL: Base URL of the Hub (https://mydomain[:port]/)
|
|
JUPYTERHUB_SERVICE_PREFIX: URL path prefix of this service (/services/:service-name/)
|
|
JUPYTERHUB_SERVICE_URL: Local URL where the service is expected to be listening.
|
|
Only for proxied web services.
|
|
```
|
|
|
|
For the previous 'cull idle' Service example, these environment variables
|
|
would be passed to the Service when the Hub starts the 'cull idle' Service:
|
|
|
|
```bash
|
|
JUPYTERHUB_SERVICE_NAME: 'cull-idle'
|
|
JUPYTERHUB_API_TOKEN: API token assigned to the service
|
|
JUPYTERHUB_API_URL: http://127.0.0.1:8080/hub/api
|
|
JUPYTERHUB_BASE_URL: https://mydomain[:port]
|
|
JUPYTERHUB_SERVICE_PREFIX: /services/cull-idle/
|
|
```
|
|
|
|
See the JupyterHub GitHub repo for additional information about the
|
|
[`cull-idle` example](https://github.com/jupyterhub/jupyterhub/tree/master/examples/cull-idle).
|
|
|
|
## Externally-Managed Services
|
|
|
|
You may prefer to use your own service management tools, such as Docker or
|
|
systemd, to manage a JupyterHub Service. These **Externally-Managed
|
|
Services**, unlike Hub-Managed Services, are not subprocesses of the Hub. You
|
|
must tell JupyterHub which API token the Externally-Managed Service is using
|
|
to perform its API requests. Each Externally-Managed Service will need a
|
|
unique API token, because the Hub authenticates each API request and the API
|
|
token is used to identify the originating Service or user.
|
|
|
|
A configuration example of an Externally-Managed Service with admin access and
|
|
running its own web server is:
|
|
|
|
```python
|
|
c.JupyterHub.services = [
|
|
{
|
|
'name': 'my-web-service',
|
|
'url': 'https://10.0.1.1:1984',
|
|
'api_token': 'super-secret',
|
|
}
|
|
]
|
|
```
|
|
|
|
In this case, the `url` field will be passed along to the Service as
|
|
`JUPYTERHUB_SERVICE_URL`.
|
|
|
|
## Writing your own Services
|
|
|
|
When writing your own services, you have a few decisions to make (in addition
|
|
to what your service does!):
|
|
|
|
1. Does my service need a public URL?
|
|
2. Do I want JupyterHub to start/stop the service?
|
|
3. Does my service need to authenticate users?
|
|
|
|
When a Service is managed by JupyterHub, the Hub will pass the necessary
|
|
information to the Service via the environment variables described above. A
|
|
flexible Service, whether managed by the Hub or not, can make use of these
|
|
same environment variables.
|
|
|
|
When you run a service that has a url, it will be accessible under a
|
|
`/services/` prefix, such as `https://myhub.horse/services/my-service/`. For
|
|
your service to route proxied requests properly, it must take
|
|
`JUPYTERHUB_SERVICE_PREFIX` into account when routing requests. For example, a
|
|
web service would normally service its root handler at `'/'`, but the proxied
|
|
service would need to serve `JUPYTERHUB_SERVICE_PREFIX + '/'`.
|
|
|
|
## Hub Authentication and Services
|
|
|
|
JupyterHub 0.7 introduces some utilities for using the Hub's authentication
|
|
mechanism to govern access to your service. When a user logs into JupyterHub,
|
|
the Hub sets a **cookie (`jupyterhub-services`)**. The service can use this
|
|
cookie to authenticate requests.
|
|
|
|
JupyterHub ships with a reference implementation of Hub authentication that
|
|
can be used by services. You may go beyond this reference implementation and
|
|
create custom hub-authenticating clients and services. We describe the process
|
|
below.
|
|
|
|
The reference, or base, implementation is the [`HubAuth`][HubAuth] class,
|
|
which implements the requests to the Hub.
|
|
|
|
To use HubAuth, you must set the `.api_token`, either programmatically when constructing the class,
|
|
or via the `JUPYTERHUB_API_TOKEN` environment variable.
|
|
|
|
Most of the logic for authentication implementation is found in the
|
|
[`HubAuth.user_for_cookie`](services.auth.html#jupyterhub.services.auth.HubAuth.user_for_cookie)
|
|
method, which makes a request of the Hub, and returns:
|
|
|
|
- None, if no user could be identified, or
|
|
- a dict of the following form:
|
|
|
|
```python
|
|
{
|
|
"name": "username",
|
|
"groups": ["list", "of", "groups"],
|
|
"admin": False, # or True
|
|
}
|
|
```
|
|
|
|
You are then free to use the returned user information to take appropriate
|
|
action.
|
|
|
|
HubAuth also caches the Hub's response for a number of seconds,
|
|
configurable by the `cookie_cache_max_age` setting (default: five minutes).
|
|
|
|
### Flask Example
|
|
|
|
For example, you have a Flask service that returns information about a user.
|
|
JupyterHub's HubAuth class can be used to authenticate requests to the Flask
|
|
service. See the `service-whoami-flask` example in the
|
|
[JupyterHub GitHub repo](https://github.com/jupyterhub/jupyterhub/tree/master/examples/service-whoami-flask)
|
|
for more details.
|
|
|
|
```python
|
|
from functools import wraps
|
|
import json
|
|
import os
|
|
from urllib.parse import quote
|
|
|
|
from flask import Flask, redirect, request, Response
|
|
|
|
from jupyterhub.services.auth import HubAuth
|
|
|
|
prefix = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '/')
|
|
|
|
auth = HubAuth(
|
|
api_token=os.environ['JUPYTERHUB_API_TOKEN'],
|
|
cookie_cache_max_age=60,
|
|
)
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
|
def authenticated(f):
|
|
"""Decorator for authenticating with the Hub"""
|
|
@wraps(f)
|
|
def decorated(*args, **kwargs):
|
|
cookie = request.cookies.get(auth.cookie_name)
|
|
if cookie:
|
|
user = auth.user_for_cookie(cookie)
|
|
else:
|
|
user = None
|
|
if user:
|
|
return f(user, *args, **kwargs)
|
|
else:
|
|
# redirect to login url on failed auth
|
|
return redirect(auth.login_url + '?next=%s' % quote(request.path))
|
|
return decorated
|
|
|
|
|
|
@app.route(prefix + '/')
|
|
@authenticated
|
|
def whoami(user):
|
|
return Response(
|
|
json.dumps(user, indent=1, sort_keys=True),
|
|
mimetype='application/json',
|
|
)
|
|
```
|
|
|
|
|
|
### Authenticating tornado services with JupyterHub
|
|
|
|
Since most Jupyter services are written with tornado,
|
|
we include a mixin class, [`HubAuthenticated`][HubAuthenticated],
|
|
for quickly authenticating your own tornado services with JupyterHub.
|
|
|
|
Tornado's `@web.authenticated` method calls a Handler's `.get_current_user`
|
|
method to identify the user. Mixing in `HubAuthenticated` defines
|
|
`get_current_user` to use HubAuth. If you want to configure the HubAuth
|
|
instance beyond the default, you'll want to define an `initialize` method,
|
|
such as:
|
|
|
|
```python
|
|
class MyHandler(HubAuthenticated, web.RequestHandler):
|
|
hub_users = {'inara', 'mal'}
|
|
|
|
def initialize(self, hub_auth):
|
|
self.hub_auth = hub_auth
|
|
|
|
@web.authenticated
|
|
def get(self):
|
|
...
|
|
```
|
|
|
|
|
|
The HubAuth will automatically load the desired configuration from the Service
|
|
environment variables.
|
|
|
|
If you want to limit user access, you can whitelist users through either the
|
|
`.hub_users` attribute or `.hub_groups`. These are sets that check against the
|
|
username and user group list, respectively. If a user matches neither the user
|
|
list nor the group list, they will not be allowed access. If both are left
|
|
undefined, then any user will be allowed.
|
|
|
|
|
|
### Implementing your own Authentication with JupyterHub
|
|
|
|
If you don't want to use the reference implementation
|
|
(e.g. you find the implementation a poor fit for your Flask app),
|
|
you can implement authentication via the Hub yourself.
|
|
We recommend looking at the [`HubAuth`][HubAuth] class implementation for reference,
|
|
and taking note of the following process:
|
|
|
|
1. retrieve the cookie `jupyterhub-services` from the request.
|
|
2. Make an API request `GET /hub/api/authorizations/cookie/jupyterhub-services/cookie-value`,
|
|
where cookie-value is the url-encoded value of the `jupyterhub-services` cookie.
|
|
This request must be authenticated with a Hub API token in the `Authorization` header.
|
|
For example, with [requests][]:
|
|
|
|
```python
|
|
r = requests.get(
|
|
'/'.join((["http://127.0.0.1:8081/hub/api",
|
|
"authorizations/cookie/jupyterhub-services",
|
|
quote(encrypted_cookie, safe=''),
|
|
]),
|
|
headers = {
|
|
'Authorization' : 'token %s' % api_token,
|
|
},
|
|
)
|
|
r.raise_for_status()
|
|
user = r.json()
|
|
```
|
|
|
|
3. On success, the reply will be a JSON model describing the user:
|
|
|
|
```json
|
|
{
|
|
"name": "inara",
|
|
"groups": ["serenity", "guild"],
|
|
|
|
}
|
|
```
|
|
|
|
An example of using an Externally-Managed Service and authentication is
|
|
[nbviewer](https://github.com/jupyter/nbviewer#securing-the-notebook-viewer),
|
|
and an example of its configuration is found [here](https://github.com/jupyter/nbviewer/blob/master/nbviewer/providers/base.py#L94).
|
|
nbviewer can also be run as a Hub-Managed Service as described [here](https://github.com/jupyter/nbviewer#securing-the-notebook-viewer).
|
|
|
|
|
|
[requests]: http://docs.python-requests.org/en/master/
|
|
[services_auth]: api/services.auth.html
|
|
[HubAuth]: api/services.auth.html#jupyterhub.services.auth.HubAuth
|
|
[HubAuthenticated]: api/services.auth.html#jupyterhub.services.auth.HubAuthenticated
|