mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-08 10:34:10 +00:00
Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
95649a3ece | ||
![]() |
08288f5b0f | ||
![]() |
01b1ce3995 | ||
![]() |
cbe93810be | ||
![]() |
75309d9dc4 | ||
![]() |
249b4af59f | ||
![]() |
db3b2d8961 | ||
![]() |
7d44a0ffc8 | ||
![]() |
202b2590e9 | ||
![]() |
c98ef547a8 | ||
![]() |
8a866a9102 | ||
![]() |
b186bdbce3 | ||
![]() |
36fe6c6f66 | ||
![]() |
8bf559db52 | ||
![]() |
750085f627 | ||
![]() |
2dc2c99b4a | ||
![]() |
e703555888 | ||
![]() |
7e102f0511 | ||
![]() |
facde96425 | ||
![]() |
608c746a59 | ||
![]() |
a8c834410f | ||
![]() |
bda14b487a | ||
![]() |
fd5cf8c360 | ||
![]() |
03758e5b46 | ||
![]() |
e540d143bb | ||
![]() |
b2c5ad40c5 | ||
![]() |
edfdf672d8 | ||
![]() |
39f19aef49 | ||
![]() |
8813bb63d4 | ||
![]() |
7c18d6fe14 | ||
![]() |
d1fe17d3cb | ||
![]() |
b8965c2017 | ||
![]() |
733d7bc158 | ||
![]() |
88f31c29bb |
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
@@ -190,3 +190,23 @@ jobs:
|
|||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ join(fromJson(steps.demotags.outputs.tags)) }}
|
tags: ${{ join(fromJson(steps.demotags.outputs.tags)) }}
|
||||||
|
|
||||||
|
# jupyterhub/singleuser
|
||||||
|
- name: Get list of jupyterhub/singleuser tags
|
||||||
|
id: singleusertags
|
||||||
|
uses: jupyterhub/action-major-minor-tag-calculator@v2
|
||||||
|
with:
|
||||||
|
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
prefix: "${{ env.REGISTRY }}jupyterhub/singleuser:"
|
||||||
|
defaultTag: "${{ env.REGISTRY }}jupyterhub/singleuser:noref"
|
||||||
|
branchRegex: ^\w[\w-.]*$
|
||||||
|
|
||||||
|
- name: Build and push jupyterhub/singleuser
|
||||||
|
uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f # associated tag: v2.4.0
|
||||||
|
with:
|
||||||
|
build-args: |
|
||||||
|
JUPYTERHUB_VERSION=${{ github.ref_type == 'tag' && github.ref_name || format('git:{0}', github.sha) }}
|
||||||
|
context: singleuser
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ join(fromJson(steps.singleusertags.outputs.tags)) }}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v2.29.0
|
rev: v2.29.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args:
|
args:
|
||||||
@@ -10,11 +10,11 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: reorder-python-imports
|
- id: reorder-python-imports
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 21.10b0
|
rev: 21.11b1
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
rev: v2.4.1
|
rev: v2.5.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
- repo: https://github.com/PyCQA/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,3 @@
|
|||||||
.. _admin/upgrading:
|
|
||||||
|
|
||||||
====================
|
====================
|
||||||
Upgrading JupyterHub
|
Upgrading JupyterHub
|
||||||
====================
|
====================
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -205,7 +205,10 @@ epub_exclude_files = ['search.html']
|
|||||||
|
|
||||||
# -- Intersphinx ----------------------------------------------------------
|
# -- Intersphinx ----------------------------------------------------------
|
||||||
|
|
||||||
intersphinx_mapping = {'https://docs.python.org/3/': None}
|
intersphinx_mapping = {
|
||||||
|
'python': ('https://docs.python.org/3/', None),
|
||||||
|
'tornado': ('https://www.tornadoweb.org/en/stable/', None),
|
||||||
|
}
|
||||||
|
|
||||||
# -- Read The Docs --------------------------------------------------------
|
# -- Read The Docs --------------------------------------------------------
|
||||||
|
|
||||||
|
BIN
docs/source/images/binder-404.png
Normal file
BIN
docs/source/images/binder-404.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 160 KiB |
BIN
docs/source/images/binderhub-form.png
Normal file
BIN
docs/source/images/binderhub-form.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 138 KiB |
BIN
docs/source/images/chp-404.png
Normal file
BIN
docs/source/images/chp-404.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
BIN
docs/source/images/server-not-running.png
Normal file
BIN
docs/source/images/server-not-running.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
@@ -114,7 +114,9 @@ class ScopeTableGenerator:
|
|||||||
if doc_description:
|
if doc_description:
|
||||||
description = doc_description
|
description = doc_description
|
||||||
scope_dict[scope] = description
|
scope_dict[scope] = description
|
||||||
content['securityDefinitions']['oauth2']['scopes'] = scope_dict
|
content['components']['securitySchemes']['oauth2']['flows'][
|
||||||
|
'authorizationCode'
|
||||||
|
]['scopes'] = scope_dict
|
||||||
|
|
||||||
with open(filename, 'w') as f:
|
with open(filename, 'w') as f:
|
||||||
yaml.dump(content, f)
|
yaml.dump(content, f)
|
||||||
|
128
docs/source/reference/api-only.md
Normal file
128
docs/source/reference/api-only.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
(api-only)=
|
||||||
|
|
||||||
|
# Deploying JupyterHub in "API only mode"
|
||||||
|
|
||||||
|
As a service for deploying and managing Jupyter servers for users, JupyterHub
|
||||||
|
exposes this functionality _primarily_ via a [REST API](rest).
|
||||||
|
For convenience, JupyterHub also ships with a _basic_ web UI built using that REST API.
|
||||||
|
The basic web UI enables users to click a button to quickly start and stop their servers,
|
||||||
|
and it lets admins perform some basic user and server management tasks.
|
||||||
|
|
||||||
|
The REST API has always provided additional functionality beyond what is available in the basic web UI.
|
||||||
|
Similarly, we avoid implementing UI functionality that is also not available via the API.
|
||||||
|
With JupyterHub 2.0, the basic web UI will **always** be composed using the REST API.
|
||||||
|
In other words, no UI pages should rely on information not available via the REST API.
|
||||||
|
Previously, some admin UI functionality could only be achieved via admin pages,
|
||||||
|
such as paginated requests.
|
||||||
|
|
||||||
|
## Limited UI customization via templates
|
||||||
|
|
||||||
|
The JupyterHub UI is customizable via extensible HTML [templates](templates),
|
||||||
|
but this has some limited scope to what can be customized.
|
||||||
|
Adding some content and messages to existing pages is well supported,
|
||||||
|
but changing the page flow and what pages are available are beyond the scope of what is customizable.
|
||||||
|
|
||||||
|
## Rich UI customization with REST API based apps
|
||||||
|
|
||||||
|
Increasingly, JupyterHub is used purely as an API for managing Jupyter servers
|
||||||
|
for other Jupyter-based applications that might want to present a different user experience.
|
||||||
|
If you want a fully customized user experience,
|
||||||
|
you can now disable the Hub UI and use your own pages together with the JupyterHub REST API
|
||||||
|
to build your own web application to serve your users,
|
||||||
|
relying on the Hub only as an API for managing users and servers.
|
||||||
|
|
||||||
|
One example of such an application is [BinderHub][], which powers https://mybinder.org,
|
||||||
|
and motivates many of these changes.
|
||||||
|
|
||||||
|
BinderHub is distinct from a traditional JupyterHub deployment
|
||||||
|
because it uses temporary users created for each launch.
|
||||||
|
Instead of presenting a login page,
|
||||||
|
users are presented with a form to specify what environment they would like to launch:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
When a launch is requested:
|
||||||
|
|
||||||
|
1. an image is built, if necessary
|
||||||
|
2. a temporary user is created,
|
||||||
|
3. a server is launched for that user, and
|
||||||
|
4. when running, users are redirected to an already running server with an auth token in the URL
|
||||||
|
5. after the session is over, the user is deleted
|
||||||
|
|
||||||
|
This means that a lot of JupyterHub's UI flow doesn't make sense:
|
||||||
|
|
||||||
|
- there is no way for users to login
|
||||||
|
- the human user doesn't map onto a JupyterHub `User` in a meaningful way
|
||||||
|
- when a server isn't running, there isn't a 'restart your server' action available because the user has been deleted
|
||||||
|
- users do not have any access to any Hub functionality, so presenting pages for those features would be confusing
|
||||||
|
|
||||||
|
BinderHub is one of the motivating use cases for JupyterHub supporting being used _only_ via its API.
|
||||||
|
We'll use BinderHub here as an example of various configuration options.
|
||||||
|
|
||||||
|
[binderhub]: https://binderhub.readthedocs.io
|
||||||
|
|
||||||
|
## Disabling Hub UI
|
||||||
|
|
||||||
|
`c.JupyterHub.hub_routespec` is a configuration option to specify which URL prefix should be routed to the Hub.
|
||||||
|
The default is `/` which means that the Hub will receive all requests not already specified to be routed somewhere else.
|
||||||
|
|
||||||
|
There are three values that are most logical for `hub_routespec`:
|
||||||
|
|
||||||
|
- `/` - this is the default, and used in most deployments.
|
||||||
|
It is also the only option prior to JupyterHub 1.4.
|
||||||
|
- `/hub/` - this serves only Hub pages, both UI and API
|
||||||
|
- `/hub/api` - this serves _only the Hub API_, so all Hub UI is disabled,
|
||||||
|
aside from the OAuth confirmation page, if used.
|
||||||
|
|
||||||
|
If you choose a hub routespec other than `/`,
|
||||||
|
the main JupyterHub feature you will lose is the automatic handling of requests for `/user/:username`
|
||||||
|
when the requested server is not running.
|
||||||
|
|
||||||
|
JupyterHub's handling of this request shows this page,
|
||||||
|
telling you that the server is not running,
|
||||||
|
with a button to launch it again:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
If you set `hub_routespec` to something other than `/`,
|
||||||
|
it is likely that you also want to register another destination for `/` to handle requests to not-running servers.
|
||||||
|
If you don't, you will see a default 404 page from the proxy:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
For mybinder.org, the default "start my server" page doesn't make sense,
|
||||||
|
because when a server is gone, there is no restart action.
|
||||||
|
Instead, we provide hints about how to get back to a link to start a _new_ server:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
To achieve this, mybinder.org registers a route for `/` that goes to a custom endpoint
|
||||||
|
that runs nginx and only serves this static HTML error page.
|
||||||
|
This is set with
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.Proxy.extra_routes = {
|
||||||
|
"/": "http://custom-404-entpoint/",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You may want to use an alternate behavior, such as redirecting to a landing page,
|
||||||
|
or taking some other action based on the requested page.
|
||||||
|
|
||||||
|
If you use `c.JupyterHub.hub_routespec = "/hub/"`,
|
||||||
|
then all the Hub pages will be available,
|
||||||
|
and only this default-page-404 issue will come up.
|
||||||
|
|
||||||
|
If you use `c.JupyterHub.hub_routespec = "/hub/api/"`,
|
||||||
|
then only the Hub _API_ will be available,
|
||||||
|
and all UI will be up to you.
|
||||||
|
mybinder.org takes this last option,
|
||||||
|
because none of the Hub UI pages really make sense.
|
||||||
|
Binder users don't have any reason to know or care that JupyterHub happens
|
||||||
|
to be an implementation detail of how their environment is managed.
|
||||||
|
Seeing Hub error pages and messages in that situation is more likely to be confusing than helpful.
|
||||||
|
|
||||||
|
:::{versionadded} 1.4
|
||||||
|
|
||||||
|
`c.JupyterHub.hub_routespec` and `c.Proxy.extra_routes` are new in JupyterHub 1.4.
|
||||||
|
:::
|
@@ -21,6 +21,7 @@ what happens under-the-hood when you deploy and configure your JupyterHub.
|
|||||||
monitoring
|
monitoring
|
||||||
database
|
database
|
||||||
templates
|
templates
|
||||||
|
api-only
|
||||||
../events/index
|
../events/index
|
||||||
config-user-env
|
config-user-env
|
||||||
config-examples
|
config-examples
|
||||||
|
@@ -5,7 +5,7 @@ Below is an interactive view of JupyterHub's OpenAPI specification.
|
|||||||
<!-- client-rendered openapi UI copied from FastAPI -->
|
<!-- client-rendered openapi UI copied from FastAPI -->
|
||||||
|
|
||||||
<link type="text/css" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css">
|
<link type="text/css" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@4.1/swagger-ui-bundle.js"></script>
|
||||||
<!-- `SwaggerUIBundle` is now available on the page -->
|
<!-- `SwaggerUIBundle` is now available on the page -->
|
||||||
|
|
||||||
<!-- render the ui here -->
|
<!-- render the ui here -->
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
(rest-api)=
|
||||||
|
|
||||||
# Using JupyterHub's REST API
|
# Using JupyterHub's REST API
|
||||||
|
|
||||||
This section will give you information on:
|
This section will give you information on:
|
||||||
|
@@ -1,17 +1,5 @@
|
|||||||
# Services
|
# 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
|
## Definition of a Service
|
||||||
|
|
||||||
When working with JupyterHub, a **Service** is defined as a process that interacts
|
When working with JupyterHub, a **Service** is defined as a process that interacts
|
||||||
@@ -115,6 +103,8 @@ parameters, which describe the environment needed to start the Service process:
|
|||||||
|
|
||||||
The Hub will pass the following environment variables to launch the Service:
|
The Hub will pass the following environment variables to launch the Service:
|
||||||
|
|
||||||
|
(service-env)=
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
JUPYTERHUB_SERVICE_NAME: The name of the service
|
JUPYTERHUB_SERVICE_NAME: The name of the service
|
||||||
JUPYTERHUB_API_TOKEN: API token assigned to the service
|
JUPYTERHUB_API_TOKEN: API token assigned to the service
|
||||||
@@ -196,18 +186,38 @@ extra slash you might get unexpected behavior. For example if your service has a
|
|||||||
|
|
||||||
## Hub Authentication and Services
|
## Hub Authentication and Services
|
||||||
|
|
||||||
JupyterHub 0.7 introduces some utilities for using the Hub's authentication
|
JupyterHub provides some utilities for using the Hub's authentication
|
||||||
mechanism to govern access to your service. When a user logs into JupyterHub,
|
mechanism to govern access to your service.
|
||||||
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
|
Requests to all JupyterHub services are made with OAuth tokens.
|
||||||
|
These can either be requests with a token in the `Authorization` header,
|
||||||
|
or url parameter `?token=...`,
|
||||||
|
or browser requests which must complete the OAuth authorization code flow,
|
||||||
|
which results in a token that should be persisted for future requests
|
||||||
|
(persistence is up to the service,
|
||||||
|
but an encrypted cookie confined to the service path is appropriate,
|
||||||
|
and provided by default).
|
||||||
|
|
||||||
|
:::{versionchanged} 2.0
|
||||||
|
The shared `jupyterhub-services` cookie is removed.
|
||||||
|
OAuth must be used to authenticate browser requests with services.
|
||||||
|
:::
|
||||||
|
|
||||||
|
JupyterHub includes a reference implementation of Hub authentication that
|
||||||
can be used by services. You may go beyond this reference implementation and
|
can be used by services. You may go beyond this reference implementation and
|
||||||
create custom hub-authenticating clients and services. We describe the process
|
create custom hub-authenticating clients and services. We describe the process
|
||||||
below.
|
below.
|
||||||
|
|
||||||
The reference, or base, implementation is the [`HubAuth`][hubauth] class,
|
The reference, or base, implementation is the [`HubAuth`][hubauth] class,
|
||||||
which implements the requests to the Hub.
|
which implements the API requests to the Hub that resolve a token to a User model.
|
||||||
|
|
||||||
|
There are two levels of authentication with the Hub:
|
||||||
|
|
||||||
|
- [`HubAuth`][hubauth] - the most basic authentication,
|
||||||
|
for services that should only accept API requests authorized with a token.
|
||||||
|
|
||||||
|
- [`HubOAuth`][huboauth] - For services that should use oauth to authenticate with the Hub.
|
||||||
|
This should be used for any service that serves pages that should be visited with a browser.
|
||||||
|
|
||||||
To use HubAuth, you must set the `.api_token`, either programmatically when constructing the class,
|
To use HubAuth, you must set the `.api_token`, either programmatically when constructing the class,
|
||||||
or via the `JUPYTERHUB_API_TOKEN` environment variable.
|
or via the `JUPYTERHUB_API_TOKEN` environment variable.
|
||||||
@@ -250,18 +260,17 @@ for more details.
|
|||||||
### Authenticating tornado services with JupyterHub
|
### Authenticating tornado services with JupyterHub
|
||||||
|
|
||||||
Since most Jupyter services are written with tornado,
|
Since most Jupyter services are written with tornado,
|
||||||
we include a mixin class, [`HubAuthenticated`][hubauthenticated],
|
we include a mixin class, [`HubOAuthenticated`][huboauthenticated],
|
||||||
for quickly authenticating your own tornado services with JupyterHub.
|
for quickly authenticating your own tornado services with JupyterHub.
|
||||||
|
|
||||||
Tornado's `@web.authenticated` method calls a Handler's `.get_current_user`
|
Tornado's {py:func}`~.tornado.web.authenticated` decorator calls a Handler's {py:meth}`~.tornado.web.RequestHandler.get_current_user`
|
||||||
method to identify the user. Mixing in `HubAuthenticated` defines
|
method to identify the user. Mixing in {class}`.HubAuthenticated` defines
|
||||||
`get_current_user` to use HubAuth. If you want to configure the HubAuth
|
{meth}`~.HubAuthenticated.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,
|
instance beyond the default, you'll want to define an {py:meth}`~.tornado.web.RequestHandler.initialize` method,
|
||||||
such as:
|
such as:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class MyHandler(HubAuthenticated, web.RequestHandler):
|
class MyHandler(HubOAuthenticated, web.RequestHandler):
|
||||||
hub_users = {'inara', 'mal'}
|
|
||||||
|
|
||||||
def initialize(self, hub_auth):
|
def initialize(self, hub_auth):
|
||||||
self.hub_auth = hub_auth
|
self.hub_auth = hub_auth
|
||||||
@@ -271,14 +280,21 @@ class MyHandler(HubAuthenticated, web.RequestHandler):
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
The HubAuth will automatically load the desired configuration from the Service
|
The HubAuth class will automatically load the desired configuration from the Service
|
||||||
environment variables.
|
[environment variables](service-env).
|
||||||
|
|
||||||
If you want to limit user access, you can specify allowed users through either the
|
:::{versionchanged} 2.0
|
||||||
`.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
|
Access scopes are used to govern access to services.
|
||||||
list nor the group list, they will not be allowed access. If both are left
|
Prior to 2.0,
|
||||||
undefined, then any user will be allowed.
|
sets of users and groups could be used to grant access
|
||||||
|
by defining `.hub_groups` or `.hub_users` on the authenticated handler.
|
||||||
|
These are ignored if the 2.0 `.hub_scopes` is defined.
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::{seealso}
|
||||||
|
{meth}`.HubAuth.check_scopes`
|
||||||
|
:::
|
||||||
|
|
||||||
### Implementing your own Authentication with JupyterHub
|
### Implementing your own Authentication with JupyterHub
|
||||||
|
|
||||||
@@ -354,9 +370,11 @@ section on securing the notebook viewer.
|
|||||||
|
|
||||||
[requests]: http://docs.python-requests.org/en/master/
|
[requests]: http://docs.python-requests.org/en/master/
|
||||||
[services_auth]: ../api/services.auth.html
|
[services_auth]: ../api/services.auth.html
|
||||||
|
[hubauth]: ../api/services.auth.html#jupyterhub.services.auth.HubAuth
|
||||||
[huboauth]: ../api/services.auth.html#jupyterhub.services.auth.HubOAuth
|
[huboauth]: ../api/services.auth.html#jupyterhub.services.auth.HubOAuth
|
||||||
[hubauth.user_for_token]: ../api/services.auth.html#jupyterhub.services.auth.HubAuth.user_for_token
|
[hubauth.user_for_token]: ../api/services.auth.html#jupyterhub.services.auth.HubAuth.user_for_token
|
||||||
[hubauthenticated]: ../api/services.auth.html#jupyterhub.services.auth.HubAuthenticated
|
[hubauthenticated]: ../api/services.auth.html#jupyterhub.services.auth.HubAuthenticated
|
||||||
|
[huboauthenticated]: ../api/services.auth.html#jupyterhub.services.auth.HubOAuthenticated
|
||||||
[nbviewer example]: https://github.com/jupyter/nbviewer#securing-the-notebook-viewer
|
[nbviewer example]: https://github.com/jupyter/nbviewer#securing-the-notebook-viewer
|
||||||
[fastapi example]: https://github.com/jupyterhub/jupyterhub/tree/HEAD/examples/service-fastapi
|
[fastapi example]: https://github.com/jupyterhub/jupyterhub/tree/HEAD/examples/service-fastapi
|
||||||
[fastapi]: https://fastapi.tiangolo.com
|
[fastapi]: https://fastapi.tiangolo.com
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
# Copyright (c) Jupyter Development Team.
|
# Copyright (c) Jupyter Development Team.
|
||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
# version_info updated by running `tbump`
|
# version_info updated by running `tbump`
|
||||||
version_info = (2, 0, 0, "rc3", "")
|
version_info = (2, 0, 0, "", "")
|
||||||
|
|
||||||
# pep 440 version: no dot before beta/rc, but before .dev
|
# pep 440 version: no dot before beta/rc, but before .dev
|
||||||
# 0.1.0rc1
|
# 0.1.0rc1
|
||||||
|
@@ -31,6 +31,9 @@ class APIHandler(BaseHandler):
|
|||||||
- methods for REST API models
|
- methods for REST API models
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# accept token-based authentication for API requests
|
||||||
|
_accept_token_auth = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def content_security_policy(self):
|
def content_security_policy(self):
|
||||||
return '; '.join([super().content_security_policy, "default-src 'none'"])
|
return '; '.join([super().content_security_policy, "default-src 'none'"])
|
||||||
@@ -210,6 +213,7 @@ class APIHandler(BaseHandler):
|
|||||||
'last_activity': isoformat(token.last_activity),
|
'last_activity': isoformat(token.last_activity),
|
||||||
'expires_at': isoformat(token.expires_at),
|
'expires_at': isoformat(token.expires_at),
|
||||||
'note': token.note,
|
'note': token.note,
|
||||||
|
'session_id': token.session_id,
|
||||||
'oauth_client': token.oauth_client.description
|
'oauth_client': token.oauth_client.description
|
||||||
or token.oauth_client.identifier,
|
or token.oauth_client.identifier,
|
||||||
}
|
}
|
||||||
|
@@ -58,6 +58,14 @@ class SelfAPIHandler(APIHandler):
|
|||||||
|
|
||||||
model = get_model(user)
|
model = get_model(user)
|
||||||
|
|
||||||
|
# add session_id associated with token
|
||||||
|
# added in 2.0
|
||||||
|
token = self.get_token()
|
||||||
|
if token:
|
||||||
|
model["session_id"] = token.session_id
|
||||||
|
else:
|
||||||
|
model["session_id"] = None
|
||||||
|
|
||||||
# add scopes to identify model,
|
# add scopes to identify model,
|
||||||
# but not the scopes we added to ensure we could read our own model
|
# but not the scopes we added to ensure we could read our own model
|
||||||
model["scopes"] = sorted(self.expanded_scopes.difference(_added_scopes))
|
model["scopes"] = sorted(self.expanded_scopes.difference(_added_scopes))
|
||||||
|
@@ -71,6 +71,12 @@ SESSION_COOKIE_NAME = 'jupyterhub-session-id'
|
|||||||
class BaseHandler(RequestHandler):
|
class BaseHandler(RequestHandler):
|
||||||
"""Base Handler class with access to common methods and properties."""
|
"""Base Handler class with access to common methods and properties."""
|
||||||
|
|
||||||
|
# by default, only accept cookie-based authentication
|
||||||
|
# The APIHandler base class enables token auth
|
||||||
|
# versionadded: 2.0
|
||||||
|
_accept_cookie_auth = True
|
||||||
|
_accept_token_auth = False
|
||||||
|
|
||||||
async def prepare(self):
|
async def prepare(self):
|
||||||
"""Identify the user during the prepare stage of each request
|
"""Identify the user during the prepare stage of each request
|
||||||
|
|
||||||
@@ -340,6 +346,7 @@ class BaseHandler(RequestHandler):
|
|||||||
auth_info['auth_state'] = await user.get_auth_state()
|
auth_info['auth_state'] = await user.get_auth_state()
|
||||||
return await self.auth_to_user(auth_info, user)
|
return await self.auth_to_user(auth_info, user)
|
||||||
|
|
||||||
|
@functools.lru_cache()
|
||||||
def get_token(self):
|
def get_token(self):
|
||||||
"""get token from authorization header"""
|
"""get token from authorization header"""
|
||||||
token = self.get_auth_token()
|
token = self.get_auth_token()
|
||||||
@@ -410,9 +417,11 @@ class BaseHandler(RequestHandler):
|
|||||||
async def get_current_user(self):
|
async def get_current_user(self):
|
||||||
"""get current username"""
|
"""get current username"""
|
||||||
if not hasattr(self, '_jupyterhub_user'):
|
if not hasattr(self, '_jupyterhub_user'):
|
||||||
|
user = None
|
||||||
try:
|
try:
|
||||||
|
if self._accept_token_auth:
|
||||||
user = self.get_current_user_token()
|
user = self.get_current_user_token()
|
||||||
if user is None:
|
if user is None and self._accept_cookie_auth:
|
||||||
user = self.get_current_user_cookie()
|
user = self.get_current_user_cookie()
|
||||||
if user and isinstance(user, User):
|
if user and isinstance(user, User):
|
||||||
user = await self.refresh_auth(user)
|
user = await self.refresh_auth(user)
|
||||||
|
@@ -295,7 +295,7 @@ def get_scopes_for(orm_object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(orm_object, orm.APIToken):
|
if isinstance(orm_object, orm.APIToken):
|
||||||
app_log.warning(f"Authenticated with token {orm_object}")
|
app_log.debug(f"Authenticated with token {orm_object}")
|
||||||
owner = orm_object.user or orm_object.service
|
owner = orm_object.user or orm_object.service
|
||||||
token_scopes = roles.expand_roles_to_scopes(orm_object)
|
token_scopes = roles.expand_roles_to_scopes(orm_object)
|
||||||
if orm_object.client_id != "jupyterhub":
|
if orm_object.client_id != "jupyterhub":
|
||||||
|
@@ -3,10 +3,24 @@
|
|||||||
Tokens are sent to the Hub for verification.
|
Tokens are sent to the Hub for verification.
|
||||||
The Hub replies with a JSON model describing the authenticated user.
|
The Hub replies with a JSON model describing the authenticated user.
|
||||||
|
|
||||||
``HubAuth`` can be used in any application, even outside tornado.
|
This contains two levels of authentication:
|
||||||
|
|
||||||
``HubAuthenticated`` is a mixin class for tornado handlers that should
|
- :class:`HubOAuth` - Use OAuth 2 to authenticate browsers with the Hub.
|
||||||
authenticate with the Hub.
|
This should be used for any service that should respond to browser requests
|
||||||
|
(i.e. most services).
|
||||||
|
|
||||||
|
- :class:`HubAuth` - token-only authentication, for a service that only need to handle token-authenticated API requests
|
||||||
|
|
||||||
|
The ``Auth`` classes (:class:`HubAuth`, :class:`HubOAuth`)
|
||||||
|
can be used in any application, even outside tornado.
|
||||||
|
They contain reference implementations of talking to the Hub API
|
||||||
|
to resolve a token to a user.
|
||||||
|
|
||||||
|
The ``Authenticated`` classes (:class:`HubAuthenticated`, :class:`HubOAuthenticated`)
|
||||||
|
are mixins for tornado handlers that should authenticate with the Hub.
|
||||||
|
|
||||||
|
If you are using OAuth, you will also need to register an oauth callback handler to complete the oauth process.
|
||||||
|
A tornado implementation is provided in :class:`HubOAuthCallbackHandler`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import base64
|
import base64
|
||||||
@@ -212,6 +226,7 @@ class HubAuth(SingletonConfigurable):
|
|||||||
help="""The base API URL of the Hub.
|
help="""The base API URL of the Hub.
|
||||||
|
|
||||||
Typically `http://hub-ip:hub-port/hub/api`
|
Typically `http://hub-ip:hub-port/hub/api`
|
||||||
|
Default: $JUPYTERHUB_API_URL
|
||||||
""",
|
""",
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
@@ -227,7 +242,10 @@ class HubAuth(SingletonConfigurable):
|
|||||||
os.getenv('JUPYTERHUB_API_TOKEN', ''),
|
os.getenv('JUPYTERHUB_API_TOKEN', ''),
|
||||||
help="""API key for accessing Hub API.
|
help="""API key for accessing Hub API.
|
||||||
|
|
||||||
Generate with `jupyterhub token [username]` or add to JupyterHub.services config.
|
Default: $JUPYTERHUB_API_TOKEN
|
||||||
|
|
||||||
|
Loaded from services configuration in jupyterhub_config.
|
||||||
|
Will be auto-generated for hub-managed services.
|
||||||
""",
|
""",
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
@@ -236,6 +254,7 @@ class HubAuth(SingletonConfigurable):
|
|||||||
help="""The URL prefix for the Hub itself.
|
help="""The URL prefix for the Hub itself.
|
||||||
|
|
||||||
Typically /hub/
|
Typically /hub/
|
||||||
|
Default: $JUPYTERHUB_BASE_URL
|
||||||
""",
|
""",
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
@@ -854,8 +873,6 @@ class HubAuthenticated:
|
|||||||
Examples::
|
Examples::
|
||||||
|
|
||||||
class MyHandler(HubAuthenticated, web.RequestHandler):
|
class MyHandler(HubAuthenticated, web.RequestHandler):
|
||||||
hub_users = {'inara', 'mal'}
|
|
||||||
|
|
||||||
def initialize(self, hub_auth):
|
def initialize(self, hub_auth):
|
||||||
self.hub_auth = hub_auth
|
self.hub_auth = hub_auth
|
||||||
|
|
||||||
@@ -865,6 +882,7 @@ class HubAuthenticated:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# deprecated, pre-2.0 allow sets
|
||||||
hub_services = None # set of allowed services
|
hub_services = None # set of allowed services
|
||||||
hub_users = None # set of allowed users
|
hub_users = None # set of allowed users
|
||||||
hub_groups = None # set of allowed groups
|
hub_groups = None # set of allowed groups
|
||||||
@@ -960,6 +978,10 @@ class HubAuthenticated:
|
|||||||
raise UserNotAllowed(model)
|
raise UserNotAllowed(model)
|
||||||
|
|
||||||
# proceed with the pre-2.0 way if hub_scopes is not set
|
# proceed with the pre-2.0 way if hub_scopes is not set
|
||||||
|
warnings.warn(
|
||||||
|
"hub_scopes ($JUPYTERHUB not set, proceeding with pre-2.0 authentication",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
|
||||||
if self.allow_admin and model.get('admin', False):
|
if self.allow_admin and model.get('admin', False):
|
||||||
app_log.debug("Allowing Hub admin %s", name)
|
app_log.debug("Allowing Hub admin %s", name)
|
||||||
|
@@ -18,6 +18,7 @@ import sys
|
|||||||
import warnings
|
import warnings
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
|
from importlib import import_module
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@@ -606,10 +607,34 @@ class SingleUserNotebookAppMixin(Configurable):
|
|||||||
t = self.hub_activity_interval * (1 + 0.2 * (random.random() - 0.5))
|
t = self.hub_activity_interval * (1 + 0.2 * (random.random() - 0.5))
|
||||||
await asyncio.sleep(t)
|
await asyncio.sleep(t)
|
||||||
|
|
||||||
|
def _log_app_versions(self):
|
||||||
|
"""Log application versions at startup
|
||||||
|
|
||||||
|
Logs versions of jupyterhub and singleuser-server base versions (jupyterlab, jupyter_server, notebook)
|
||||||
|
"""
|
||||||
|
self.log.info(f"Starting jupyterhub single-user server version {__version__}")
|
||||||
|
|
||||||
|
# don't log these package versions
|
||||||
|
seen = {"jupyterhub", "traitlets", "jupyter_core", "builtins"}
|
||||||
|
|
||||||
|
for cls in self.__class__.mro():
|
||||||
|
module_name = cls.__module__.partition(".")[0]
|
||||||
|
if module_name not in seen:
|
||||||
|
seen.add(module_name)
|
||||||
|
try:
|
||||||
|
mod = import_module(module_name)
|
||||||
|
mod_version = getattr(mod, "__version__")
|
||||||
|
except Exception:
|
||||||
|
mod_version = ""
|
||||||
|
self.log.info(
|
||||||
|
f"Extending {cls.__module__}.{cls.__name__} from {module_name} {mod_version}"
|
||||||
|
)
|
||||||
|
|
||||||
def initialize(self, argv=None):
|
def initialize(self, argv=None):
|
||||||
# disable trash by default
|
# disable trash by default
|
||||||
# this can be re-enabled by config
|
# this can be re-enabled by config
|
||||||
self.config.FileContentsManager.delete_to_trash = False
|
self.config.FileContentsManager.delete_to_trash = False
|
||||||
|
self._log_app_versions()
|
||||||
return super().initialize(argv)
|
return super().initialize(argv)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
@@ -578,6 +578,41 @@ async def test_login_page(app, url, params, redirected_url, form_action):
|
|||||||
assert action.endswith(form_action)
|
assert action.endswith(form_action)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"url, token_in",
|
||||||
|
[
|
||||||
|
("/home", "url"),
|
||||||
|
("/home", "header"),
|
||||||
|
("/login", "url"),
|
||||||
|
("/login", "header"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_page_with_token(app, user, url, token_in):
|
||||||
|
cookies = await app.login_user(user.name)
|
||||||
|
token = user.new_api_token()
|
||||||
|
if token_in == "url":
|
||||||
|
url = url_concat(url, {"token": token})
|
||||||
|
headers = None
|
||||||
|
elif token_in == "header":
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"token {token}",
|
||||||
|
}
|
||||||
|
|
||||||
|
# request a page with ?token= in URL shouldn't be allowed
|
||||||
|
r = await get_page(
|
||||||
|
url,
|
||||||
|
app,
|
||||||
|
headers=headers,
|
||||||
|
allow_redirects=False,
|
||||||
|
)
|
||||||
|
if "/hub/login" in r.url:
|
||||||
|
assert r.status_code == 200
|
||||||
|
else:
|
||||||
|
assert r.status_code == 302
|
||||||
|
assert r.headers["location"].partition("?")[0].endswith("/hub/login")
|
||||||
|
assert not r.cookies
|
||||||
|
|
||||||
|
|
||||||
async def test_login_fail(app):
|
async def test_login_fail(app):
|
||||||
name = 'wash'
|
name = 'wash'
|
||||||
base_url = public_url(app)
|
base_url = public_url(app)
|
||||||
|
@@ -11,7 +11,7 @@ target_version = [
|
|||||||
github_url = "https://github.com/jupyterhub/jupyterhub"
|
github_url = "https://github.com/jupyterhub/jupyterhub"
|
||||||
|
|
||||||
[tool.tbump.version]
|
[tool.tbump.version]
|
||||||
current = "2.0.0rc3"
|
current = "2.0.0"
|
||||||
|
|
||||||
# Example of a semver regexp.
|
# Example of a semver regexp.
|
||||||
# Make sure this matches current_version before
|
# Make sure this matches current_version before
|
||||||
|
5
setup.py
5
setup.py
@@ -46,10 +46,9 @@ def get_data_files():
|
|||||||
"""Get data files in share/jupyter"""
|
"""Get data files in share/jupyter"""
|
||||||
|
|
||||||
data_files = []
|
data_files = []
|
||||||
ntrim = len(here + os.path.sep)
|
|
||||||
|
|
||||||
for (d, dirs, filenames) in os.walk(share_jupyterhub):
|
for (d, dirs, filenames) in os.walk(share_jupyterhub):
|
||||||
data_files.append((d[ntrim:], [pjoin(d, f) for f in filenames]))
|
rel_d = os.path.relpath(d, here)
|
||||||
|
data_files.append((rel_d, [os.path.join(rel_d, f) for f in filenames]))
|
||||||
return data_files
|
return data_files
|
||||||
|
|
||||||
|
|
||||||
|
@@ -6,7 +6,6 @@ FROM $BASE_IMAGE
|
|||||||
MAINTAINER Project Jupyter <jupyter@googlegroups.com>
|
MAINTAINER Project Jupyter <jupyter@googlegroups.com>
|
||||||
|
|
||||||
ADD install_jupyterhub /tmp/install_jupyterhub
|
ADD install_jupyterhub /tmp/install_jupyterhub
|
||||||
ARG JUPYTERHUB_VERSION=main
|
ARG JUPYTERHUB_VERSION=git:HEAD
|
||||||
# install pinned jupyterhub and ensure jupyterlab is installed
|
# install pinned jupyterhub
|
||||||
RUN python3 /tmp/install_jupyterhub && \
|
RUN python3 /tmp/install_jupyterhub
|
||||||
python3 -m pip install jupyterlab
|
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -ex
|
|
||||||
|
|
||||||
docker build --build-arg JUPYTERHUB_VERSION=$DOCKER_TAG -t $DOCKER_REPO:$DOCKER_TAG .
|
|
@@ -1,21 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -ex
|
|
||||||
|
|
||||||
function get_hub_version() {
|
|
||||||
rm -f hub_version
|
|
||||||
V=$1
|
|
||||||
docker run --rm -v $PWD:/version -u $(id -u) -i $DOCKER_REPO:$DOCKER_TAG sh -c 'jupyterhub --version > /version/hub_version'
|
|
||||||
hub_xyz=$(cat hub_version)
|
|
||||||
split=( ${hub_xyz//./ } )
|
|
||||||
hub_xy="${split[0]}.${split[1]}"
|
|
||||||
# add .dev on hub_xy so it's 1.0.dev
|
|
||||||
if [[ ! -z "${split[3]:-}" ]]; then
|
|
||||||
hub_xy="${hub_xy}.${split[3]}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
# tag e.g. 0.9 with main
|
|
||||||
get_hub_version
|
|
||||||
docker tag $DOCKER_REPO:$DOCKER_TAG $DOCKER_REPO:$hub_xy
|
|
||||||
docker push $DOCKER_REPO:$hub_xy
|
|
||||||
docker tag $DOCKER_REPO:$DOCKER_TAG $DOCKER_REPO:$hub_xyz
|
|
||||||
docker push $DOCKER_REPO:$hub_xyz
|
|
@@ -3,19 +3,22 @@ import os
|
|||||||
from subprocess import check_call
|
from subprocess import check_call
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
V = os.environ['JUPYTERHUB_VERSION']
|
version = os.environ['JUPYTERHUB_VERSION']
|
||||||
|
|
||||||
pip_install = [
|
pip_install = [
|
||||||
sys.executable, '-m', 'pip', 'install', '--no-cache', '--upgrade',
|
sys.executable,
|
||||||
'--upgrade-strategy', 'only-if-needed',
|
'-m',
|
||||||
|
'pip',
|
||||||
|
'install',
|
||||||
|
'--no-cache',
|
||||||
|
'--upgrade',
|
||||||
|
'--upgrade-strategy',
|
||||||
|
'only-if-needed',
|
||||||
]
|
]
|
||||||
if V in {'main', 'HEAD'}:
|
if version.startswith("git:"):
|
||||||
req = 'https://github.com/jupyterhub/jupyterhub/archive/HEAD.tar.gz'
|
ref = version.partition(":")[-1]
|
||||||
|
req = f"https://github.com/jupyterhub/jupyterhub/archive/{ref}.tar.gz"
|
||||||
else:
|
else:
|
||||||
version_info = [ int(part) for part in V.split('.') ]
|
req = f"jupyterhub=={version}"
|
||||||
version_info[-1] += 1
|
|
||||||
upper_bound = '.'.join(map(str, version_info))
|
|
||||||
vs = '>=%s,<%s' % (V, upper_bound)
|
|
||||||
req = 'jupyterhub%s' % vs
|
|
||||||
|
|
||||||
check_call(pip_install + [req])
|
check_call(pip_install + [req])
|
||||||
|
Reference in New Issue
Block a user