mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-10 19:43:01 +00:00
Compare commits
49 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
aa132cade7 | ||
![]() |
dd35ffbe86 | ||
![]() |
8edcf8be81 | ||
![]() |
29b02b7bcb | ||
![]() |
0383bc27b2 | ||
![]() |
65d5102b49 | ||
![]() |
8a226e6f46 | ||
![]() |
0bd34e0a10 | ||
![]() |
186107d959 | ||
![]() |
91b07b7ea4 | ||
![]() |
f5b30fd2b4 | ||
![]() |
0234396c2c | ||
![]() |
a43d677ae4 | ||
![]() |
dcfe71e7f0 | ||
![]() |
5d41376c2e | ||
![]() |
dd083359ec | ||
![]() |
e6d54960ba | ||
![]() |
a9295bc5c2 | ||
![]() |
2015c701fa | ||
![]() |
3e9c18f50a | ||
![]() |
7cac874afc | ||
![]() |
a7b6bd8d32 | ||
![]() |
1649a98656 | ||
![]() |
ecbe51f60f | ||
![]() |
fed14abed3 | ||
![]() |
94978ea9e0 | ||
![]() |
bf6999e439 | ||
![]() |
020ee7378f | ||
![]() |
e4a0569961 | ||
![]() |
4ff525d5bd | ||
![]() |
37a31b01b2 | ||
![]() |
1604cb1b0b | ||
![]() |
45702ac18c | ||
![]() |
c81e9d60e4 | ||
![]() |
224865b894 | ||
![]() |
3b3bc8224b | ||
![]() |
c56dc2ea6f | ||
![]() |
62202bbb74 | ||
![]() |
7ba28c0207 | ||
![]() |
9392a29dad | ||
![]() |
72ab8f99ec | ||
![]() |
fcf32c7e50 | ||
![]() |
da451d6552 | ||
![]() |
662b1a4d4a | ||
![]() |
732adea997 | ||
![]() |
7e1dbf3515 | ||
![]() |
65b92ec246 | ||
![]() |
dc42ee4779 | ||
![]() |
c04441c1b2 |
@@ -2,6 +2,7 @@
|
|||||||
language: python
|
language: python
|
||||||
sudo: false
|
sudo: false
|
||||||
python:
|
python:
|
||||||
|
- 3.6-dev
|
||||||
- 3.5
|
- 3.5
|
||||||
- 3.4
|
- 3.4
|
||||||
- 3.3
|
- 3.3
|
||||||
|
@@ -131,11 +131,11 @@ Some examples, meant as illustration and testing of this concept:
|
|||||||
----
|
----
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
A ready to go [docker image for JupyterHub](https://hub.docker.com/r/jupyterhub/jupyterhub/) gives a straightforward deployment of JupyterHub.
|
A starter [docker image for JupyterHub](https://hub.docker.com/r/jupyterhub/jupyterhub/) gives a baseline deployment of JupyterHub.
|
||||||
|
|
||||||
*Note: This `jupyterhub/jupyterhub` docker image is only an image for running the Hub service itself.
|
**Important:** This `jupyterhub/jupyterhub` image contains only the Hub itself, with no configuration. In general, one needs
|
||||||
It does not require the other Jupyter components, such as Notebook installation, which are needed by the single-user servers.
|
to make a derivative image, with at least a `jupyterhub_config.py` setting up an Authenticator and/or a Spawner. To run the
|
||||||
To run the single-user servers, which may be on the same system as the Hub or not, Jupyter Notebook version 4 or greater must be installed.*
|
single-user servers, which may be on the same system as the Hub or not, Jupyter Notebook version 4 or greater must be installed.
|
||||||
|
|
||||||
#### Starting JupyterHub with docker
|
#### Starting JupyterHub with docker
|
||||||
The JupyterHub docker image can be started with the following command:
|
The JupyterHub docker image can be started with the following command:
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bootprint": "^0.8.5",
|
"bootprint": "^0.10.0",
|
||||||
"bootprint-openapi": "^0.17.0"
|
"bootprint-openapi": "^0.17.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,33 @@ command line for details.
|
|||||||
|
|
||||||
## 0.7
|
## 0.7
|
||||||
|
|
||||||
|
### [0.7.2] - 2017-01-09
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
- Support service environment variables and defaults in `jupyterhub-singleuser`
|
||||||
|
for easier deployment of notebook servers as a Service.
|
||||||
|
- Add `--group` parameter for deploying `jupyterhub-singleuser` as a Service with group authentication.
|
||||||
|
- Include URL parameters when redirecting through `/user-redirect/`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix group authentication for HubAuthenticated services
|
||||||
|
|
||||||
|
### [0.7.1] - 2017-01-02
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
- `Spawner.will_resume` for signaling that a single-user server is paused instead of stopped.
|
||||||
|
This is needed for cases like `DockerSpawner.remove_containers = False`,
|
||||||
|
where the first API token is re-used for subsequent spawns.
|
||||||
|
- Warning on startup about single-character usernames,
|
||||||
|
caused by common `set('string')` typo in config.
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
- Removed spurious warning about empty `next_url`, which is AOK.
|
||||||
|
|
||||||
### [0.7.0] - 2016-12-2
|
### [0.7.0] - 2016-12-2
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
@@ -118,8 +145,9 @@ Fix removal of `/login` page in 0.4.0, breaking some OAuth providers.
|
|||||||
First preview release
|
First preview release
|
||||||
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/0.7.0...HEAD
|
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/0.7.1...HEAD
|
||||||
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/0.6.1...0.7.0
|
[0.7.1]: https://github.com/jupyterhub/jupyterhub/compare/0.7.0...0.7.1
|
||||||
|
[0.7.0]: https://github.com/jupyterhub/jupyterhub/compare/0.6.1...0.7.0
|
||||||
[0.6.1]: https://github.com/jupyterhub/jupyterhub/compare/0.6.0...0.6.1
|
[0.6.1]: https://github.com/jupyterhub/jupyterhub/compare/0.6.0...0.6.1
|
||||||
[0.6.0]: https://github.com/jupyterhub/jupyterhub/compare/0.5.0...0.6.0
|
[0.6.0]: https://github.com/jupyterhub/jupyterhub/compare/0.5.0...0.6.0
|
||||||
[0.5]: https://github.com/jupyterhub/jupyterhub/compare/0.4.1...0.5.0
|
[0.5]: https://github.com/jupyterhub/jupyterhub/compare/0.4.1...0.5.0
|
||||||
|
@@ -405,6 +405,9 @@ You can restrict which users are allowed to login with `Authenticator.whitelist`
|
|||||||
c.Authenticator.whitelist = {'mal', 'zoe', 'inara', 'kaylee'}
|
c.Authenticator.whitelist = {'mal', 'zoe', 'inara', 'kaylee'}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Users listed in the whitelist are added to the Hub database when the Hub is
|
||||||
|
started.
|
||||||
|
|
||||||
### Managing Hub administrators
|
### Managing Hub administrators
|
||||||
|
|
||||||
Admin users of JupyterHub have the ability to take actions on users' behalf,
|
Admin users of JupyterHub have the ability to take actions on users' behalf,
|
||||||
@@ -427,12 +430,17 @@ Note: additional configuration examples are provided in this guide's
|
|||||||
|
|
||||||
### Add or remove users from the Hub
|
### Add or remove users from the Hub
|
||||||
|
|
||||||
Users can be added and removed to the Hub via the admin panel or REST API. These users will be
|
Users can be added to and removed from the Hub via either the admin panel or
|
||||||
added to the whitelist and database. Restarting the Hub will not require manually updating the
|
REST API.
|
||||||
whitelist in your config file, as the users will be loaded from the database. This means that
|
|
||||||
after starting the Hub once, it is not sufficient to remove users from the whitelist in your
|
If a user is **added**, the user will be automatically added to the whitelist
|
||||||
config file. You must also remove them from the database, either by discarding the database file,
|
and database. Restarting the Hub will not require manually updating the
|
||||||
or via the admin UI.
|
whitelist in your config file, as the users will be loaded from the database.
|
||||||
|
|
||||||
|
After starting the Hub once, it is not sufficient to **remove** a user from
|
||||||
|
the whitelist in your config file. You must also remove the user from the Hub's
|
||||||
|
database, either by deleting the user from the admin page, or you can clear
|
||||||
|
the `jupyterhub.sqlite` database and start fresh.
|
||||||
|
|
||||||
The default `PAMAuthenticator` is one case of a special kind of authenticator, called a
|
The default `PAMAuthenticator` is one case of a special kind of authenticator, called a
|
||||||
`LocalAuthenticator`, indicating that it manages users on the local system. When you add a user to
|
`LocalAuthenticator`, indicating that it manages users on the local system. When you add a user to
|
||||||
|
@@ -67,4 +67,4 @@ Note: The Swagger specification is being renamed the [OpenAPI Initiative][].
|
|||||||
|
|
||||||
[on swagger's petstore]: http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/master/docs/rest-api.yml#!/default
|
[on swagger's petstore]: http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/master/docs/rest-api.yml#!/default
|
||||||
[OpenAPI Initiative]: https://www.openapis.org/
|
[OpenAPI Initiative]: https://www.openapis.org/
|
||||||
[JupyterHub REST API]: ./api/index.html
|
[JupyterHub REST API]: ./_static/rest-api/index.html
|
||||||
|
@@ -54,7 +54,7 @@ If a service is also to be managed by the Hub, it has a few extra options:
|
|||||||
externally.
|
externally.
|
||||||
- If a command is specified for launching the Service, the Service will
|
- If a command is specified for launching the Service, the Service will
|
||||||
be started and managed by the Hub.
|
be started and managed by the Hub.
|
||||||
- `env: dict` - environment variables to add to the current env
|
- `environment: dict` - additional environment variables for the Service.
|
||||||
- `user: str` - the name of a system user to manage the Service. If
|
- `user: str` - the name of a system user to manage the Service. If
|
||||||
unspecified, run as the same user as the Hub.
|
unspecified, run as the same user as the Hub.
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ c.JupyterHub.services = [
|
|||||||
A Hub-Managed Service may also be configured with additional optional
|
A Hub-Managed Service may also be configured with additional optional
|
||||||
parameters, which describe the environment needed to start the Service process:
|
parameters, which describe the environment needed to start the Service process:
|
||||||
|
|
||||||
- `env: dict` - additional environment variables for the Service.
|
- `environment: dict` - additional environment variables for the Service.
|
||||||
- `user: str` - name of the user to run the server if different from the Hub.
|
- `user: str` - name of the user to run the server if different from the Hub.
|
||||||
Requires Hub to be root.
|
Requires Hub to be root.
|
||||||
- `cwd: path` directory in which to run the Service, if different from the
|
- `cwd: path` directory in which to run the Service, if different from the
|
||||||
|
@@ -249,3 +249,26 @@ jupyter kernelspec list
|
|||||||
```bash
|
```bash
|
||||||
jupyterhub --debug
|
jupyterhub --debug
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Toree integration with HDFS rack awareness script
|
||||||
|
|
||||||
|
The Apache Toree kernel will an issue, when running with JupyterHub, if the standard HDFS
|
||||||
|
rack awareness script is used. This will materialize in the logs as a repeated WARN:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
16/11/29 16:24:20 WARN ScriptBasedMapping: Exception running /etc/hadoop/conf/topology_script.py some.ip.address
|
||||||
|
ExitCodeException exitCode=1: File "/etc/hadoop/conf/topology_script.py", line 63
|
||||||
|
print rack
|
||||||
|
^
|
||||||
|
SyntaxError: Missing parentheses in call to 'print'
|
||||||
|
|
||||||
|
at `org.apache.hadoop.util.Shell.runCommand(Shell.java:576)`
|
||||||
|
```
|
||||||
|
|
||||||
|
In order to resolve this issue, there are two potential options.
|
||||||
|
|
||||||
|
1. Update HDFS core-site.xml, so the parameter "net.topology.script.file.name" points to a custom
|
||||||
|
script (e.g. /etc/hadoop/conf/custom_topology_script.py). Copy the original script and change the first line point
|
||||||
|
to a python two installation (e.g. /usr/bin/python).
|
||||||
|
2. In spark-env.sh add a Python 2 installation to your path (e.g. export PATH=/opt/anaconda2/bin:$PATH).
|
||||||
|
|
||||||
|
25
examples/service-notebook/README.md
Normal file
25
examples/service-notebook/README.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Running a shared notebook as a service
|
||||||
|
|
||||||
|
This directory contains two examples of running a shared notebook server as a service,
|
||||||
|
one as a 'managed' service, and one as an external service with supervisor.
|
||||||
|
|
||||||
|
These examples require jupyterhub >= 0.7.2.
|
||||||
|
|
||||||
|
A single-user notebook server is run as a service,
|
||||||
|
and uses groups to authenticate a collection of users with the Hub.
|
||||||
|
|
||||||
|
In these examples, a JupyterHub group `'shared'` is created,
|
||||||
|
and a notebook server is spawned at `/services/shared-notebook`.
|
||||||
|
Any user in the `'shared'` group will be able to access the notebook server at `/services/shared-notebook/`.
|
||||||
|
|
||||||
|
In both examples, you will want to select the name of the group,
|
||||||
|
and the name of the shared-notebook service.
|
||||||
|
|
||||||
|
In the external example, some extra steps are required to set up supervisor:
|
||||||
|
|
||||||
|
1. select a system user to run the service. This is a user on the system, and does not need to be a Hub user. Add this to the user field in `shared-notebook.conf`, replacing `someuser`.
|
||||||
|
2. generate a secret token for authentication, and replace the `super-secret` fields in `shared-notebook-service` and `jupyterhub_config.py`
|
||||||
|
3. install `shared-notebook-service` somewhere on your system, and update `/path/to/shared-notebook-service` to the absolute path of this destination
|
||||||
|
3. copy `shared-notebook.conf` to `/etc/supervisor/conf.d/`
|
||||||
|
4. `supervisorctl reload`
|
||||||
|
|
24
examples/service-notebook/external/jupyterhub_config.py
vendored
Normal file
24
examples/service-notebook/external/jupyterhub_config.py
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# our user list
|
||||||
|
c.Authenticator.whitelist = [
|
||||||
|
'minrk',
|
||||||
|
'ellisonbg',
|
||||||
|
'willingc',
|
||||||
|
]
|
||||||
|
|
||||||
|
# ellisonbg and willingc have access to a shared server:
|
||||||
|
|
||||||
|
c.JupyterHub.load_groups = {
|
||||||
|
'shared': [
|
||||||
|
'ellisonbg',
|
||||||
|
'willingc',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# start the notebook server as a service
|
||||||
|
c.JupyterHub.services = [
|
||||||
|
{
|
||||||
|
'name': 'shared-notebook',
|
||||||
|
'url': 'http://127.0.0.1:9999',
|
||||||
|
'api_token': 'super-secret',
|
||||||
|
}
|
||||||
|
]
|
9
examples/service-notebook/external/shared-notebook-service
vendored
Executable file
9
examples/service-notebook/external/shared-notebook-service
vendored
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash -l
|
||||||
|
set -e
|
||||||
|
|
||||||
|
export JUPYTERHUB_API_TOKEN=super-secret
|
||||||
|
export JUPYTERHUB_SERVICE_URL=http://127.0.0.1:9999
|
||||||
|
export JUPYTERHUB_SERVICE_NAME=shared-notebook
|
||||||
|
|
||||||
|
jupyterhub-singleuser \
|
||||||
|
--group='shared'
|
14
examples/service-notebook/external/shared-notebook.conf
vendored
Normal file
14
examples/service-notebook/external/shared-notebook.conf
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[program:jupyterhub-shared-notebook]
|
||||||
|
user=someuser
|
||||||
|
command=bash -l /path/to/shared-notebook-service
|
||||||
|
directory=/home/someuser
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
startretries=1
|
||||||
|
exitcodes=0,2
|
||||||
|
stopsignal=TERM
|
||||||
|
redirect_stderr=true
|
||||||
|
stdout_logfile=/var/log/jupyterhub-service-shared-notebook.log
|
||||||
|
stdout_logfile_maxbytes=1MB
|
||||||
|
stdout_logfile_backups=10
|
||||||
|
stdout_capture_maxbytes=1MB
|
32
examples/service-notebook/managed/jupyterhub_config.py
Normal file
32
examples/service-notebook/managed/jupyterhub_config.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# our user list
|
||||||
|
c.Authenticator.whitelist = [
|
||||||
|
'minrk',
|
||||||
|
'ellisonbg',
|
||||||
|
'willingc',
|
||||||
|
]
|
||||||
|
|
||||||
|
# ellisonbg and willingc have access to a shared server:
|
||||||
|
|
||||||
|
c.JupyterHub.load_groups = {
|
||||||
|
'shared': [
|
||||||
|
'ellisonbg',
|
||||||
|
'willingc',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
service_name = 'shared-notebook'
|
||||||
|
service_port = 9999
|
||||||
|
group_name = 'shared'
|
||||||
|
|
||||||
|
# start the notebook server as a service
|
||||||
|
c.JupyterHub.services = [
|
||||||
|
{
|
||||||
|
'name': service_name,
|
||||||
|
'url': 'http://127.0.0.1:{}'.format(service_port),
|
||||||
|
'command': [
|
||||||
|
'jupyterhub-singleuser',
|
||||||
|
'--group=shared',
|
||||||
|
'--debug',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
@@ -146,18 +146,18 @@ class NewToken(Application):
|
|||||||
|
|
||||||
class UpgradeDB(Application):
|
class UpgradeDB(Application):
|
||||||
"""Upgrade the JupyterHub database schema."""
|
"""Upgrade the JupyterHub database schema."""
|
||||||
|
|
||||||
name = 'jupyterhub-upgrade-db'
|
name = 'jupyterhub-upgrade-db'
|
||||||
version = jupyterhub.__version__
|
version = jupyterhub.__version__
|
||||||
description = """Upgrade the JupyterHub database to the current schema.
|
description = """Upgrade the JupyterHub database to the current schema.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
jupyterhub upgrade-db
|
jupyterhub upgrade-db
|
||||||
"""
|
"""
|
||||||
aliases = common_aliases
|
aliases = common_aliases
|
||||||
classes = []
|
classes = []
|
||||||
|
|
||||||
def _backup_db_file(self, db_file):
|
def _backup_db_file(self, db_file):
|
||||||
"""Backup a database file"""
|
"""Backup a database file"""
|
||||||
if not os.path.exists(db_file):
|
if not os.path.exists(db_file):
|
||||||
@@ -171,7 +171,7 @@ class UpgradeDB(Application):
|
|||||||
backup_db_file = '{}.{}.{}'.format(db_file, timestamp, i)
|
backup_db_file = '{}.{}.{}'.format(db_file, timestamp, i)
|
||||||
if os.path.exists(backup_db_file):
|
if os.path.exists(backup_db_file):
|
||||||
self.exit("backup db file already exists: %s" % backup_db_file)
|
self.exit("backup db file already exists: %s" % backup_db_file)
|
||||||
|
|
||||||
self.log.info("Backing up %s => %s", db_file, backup_db_file)
|
self.log.info("Backing up %s => %s", db_file, backup_db_file)
|
||||||
shutil.copy(db_file, backup_db_file)
|
shutil.copy(db_file, backup_db_file)
|
||||||
|
|
||||||
@@ -222,12 +222,12 @@ class JupyterHub(Application):
|
|||||||
Authenticator,
|
Authenticator,
|
||||||
PAMAuthenticator,
|
PAMAuthenticator,
|
||||||
])
|
])
|
||||||
|
|
||||||
load_groups = Dict(List(Unicode()),
|
load_groups = Dict(List(Unicode()),
|
||||||
help="""Dict of 'group': ['usernames'] to load at startup.
|
help="""Dict of 'group': ['usernames'] to load at startup.
|
||||||
|
|
||||||
This strictly *adds* groups and users to groups.
|
This strictly *adds* groups and users to groups.
|
||||||
|
|
||||||
Loading one set of groups, then starting JupyterHub again with a different
|
Loading one set of groups, then starting JupyterHub again with a different
|
||||||
set will not remove users or groups from previous launches.
|
set will not remove users or groups from previous launches.
|
||||||
That must be done through the API.
|
That must be done through the API.
|
||||||
@@ -414,7 +414,7 @@ class JupyterHub(Application):
|
|||||||
|
|
||||||
api_tokens = Dict(Unicode(),
|
api_tokens = Dict(Unicode(),
|
||||||
help="""PENDING DEPRECATION: consider using service_tokens
|
help="""PENDING DEPRECATION: consider using service_tokens
|
||||||
|
|
||||||
Dict of token:username to be loaded into the database.
|
Dict of token:username to be loaded into the database.
|
||||||
|
|
||||||
Allows ahead-of-time generation of API tokens for use by externally managed services,
|
Allows ahead-of-time generation of API tokens for use by externally managed services,
|
||||||
@@ -437,14 +437,14 @@ class JupyterHub(Application):
|
|||||||
Allows ahead-of-time generation of API tokens for use by externally managed services.
|
Allows ahead-of-time generation of API tokens for use by externally managed services.
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
services = List(Dict(),
|
services = List(Dict(),
|
||||||
help="""List of service specification dictionaries.
|
help="""List of service specification dictionaries.
|
||||||
|
|
||||||
A service
|
A service
|
||||||
|
|
||||||
For instance::
|
For instance::
|
||||||
|
|
||||||
services = [
|
services = [
|
||||||
{
|
{
|
||||||
'name': 'cull_idle',
|
'name': 'cull_idle',
|
||||||
@@ -454,7 +454,7 @@ class JupyterHub(Application):
|
|||||||
'name': 'formgrader',
|
'name': 'formgrader',
|
||||||
'url': 'http://127.0.0.1:1234',
|
'url': 'http://127.0.0.1:1234',
|
||||||
'token': 'super-secret',
|
'token': 'super-secret',
|
||||||
'env':
|
'environment':
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
@@ -608,7 +608,7 @@ class JupyterHub(Application):
|
|||||||
Instance(logging.Handler),
|
Instance(logging.Handler),
|
||||||
help="Extra log handlers to set on JupyterHub logger",
|
help="Extra log handlers to set on JupyterHub logger",
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
statsd = Any(allow_none=False, help="The statsd client, if any. A mock will be used if we aren't using statsd")
|
statsd = Any(allow_none=False, help="The statsd client, if any. A mock will be used if we aren't using statsd")
|
||||||
@default('statsd')
|
@default('statsd')
|
||||||
def _statsd(self):
|
def _statsd(self):
|
||||||
@@ -919,7 +919,7 @@ class JupyterHub(Application):
|
|||||||
# The whitelist set and the users in the db are now the same.
|
# The whitelist set and the users in the db are now the same.
|
||||||
# From this point on, any user changes should be done simultaneously
|
# From this point on, any user changes should be done simultaneously
|
||||||
# to the whitelist set and user db, unless the whitelist is empty (all users allowed).
|
# to the whitelist set and user db, unless the whitelist is empty (all users allowed).
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def init_groups(self):
|
def init_groups(self):
|
||||||
"""Load predefined groups into the database"""
|
"""Load predefined groups into the database"""
|
||||||
@@ -941,7 +941,7 @@ class JupyterHub(Application):
|
|||||||
db.add(user)
|
db.add(user)
|
||||||
group.users.append(user)
|
group.users.append(user)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def _add_tokens(self, token_dict, kind):
|
def _add_tokens(self, token_dict, kind):
|
||||||
"""Add tokens for users or services to the database"""
|
"""Add tokens for users or services to the database"""
|
||||||
@@ -982,13 +982,13 @@ class JupyterHub(Application):
|
|||||||
else:
|
else:
|
||||||
self.log.debug("Not duplicating token %s", orm_token)
|
self.log.debug("Not duplicating token %s", orm_token)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def init_api_tokens(self):
|
def init_api_tokens(self):
|
||||||
"""Load predefined API tokens (for services) into database"""
|
"""Load predefined API tokens (for services) into database"""
|
||||||
yield self._add_tokens(self.service_tokens, kind='service')
|
yield self._add_tokens(self.service_tokens, kind='service')
|
||||||
yield self._add_tokens(self.api_tokens, kind='user')
|
yield self._add_tokens(self.api_tokens, kind='user')
|
||||||
|
|
||||||
def init_services(self):
|
def init_services(self):
|
||||||
self._service_map.clear()
|
self._service_map.clear()
|
||||||
if self.domain:
|
if self.domain:
|
||||||
@@ -1458,7 +1458,7 @@ class JupyterHub(Application):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.critical("Failed to start proxy", exc_info=True)
|
self.log.critical("Failed to start proxy", exc_info=True)
|
||||||
self.exit(1)
|
self.exit(1)
|
||||||
|
|
||||||
for service_name, service in self._service_map.items():
|
for service_name, service in self._service_map.items():
|
||||||
if not service.managed:
|
if not service.managed:
|
||||||
continue
|
continue
|
||||||
|
@@ -56,6 +56,17 @@ class Authenticator(LoggingConfigurable):
|
|||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
|
@observe('whitelist')
|
||||||
|
def _check_whitelist(self, change):
|
||||||
|
short_names = [name for name in change['new'] if len(name) <= 1]
|
||||||
|
if short_names:
|
||||||
|
sorted_names = sorted(short_names)
|
||||||
|
single = ''.join(sorted_names)
|
||||||
|
string_set_typo = "set('%s')" % single
|
||||||
|
self.log.warning("whitelist contains single-character names: %s; did you mean set([%r]) instead of %s?",
|
||||||
|
sorted_names[:8], single, string_set_typo,
|
||||||
|
)
|
||||||
|
|
||||||
custom_html = Unicode(
|
custom_html = Unicode(
|
||||||
help="""
|
help="""
|
||||||
HTML form to be overridden by authenticators if they want a custom authentication form.
|
HTML form to be overridden by authenticators if they want a custom authentication form.
|
||||||
|
@@ -571,6 +571,9 @@ class UserRedirectHandler(BaseHandler):
|
|||||||
def get(self, path):
|
def get(self, path):
|
||||||
user = self.get_current_user()
|
user = self.get_current_user()
|
||||||
url = url_path_join(user.url, path)
|
url = url_path_join(user.url, path)
|
||||||
|
if self.request.query:
|
||||||
|
# FIXME: use urlunparse instead?
|
||||||
|
url += '?' + self.request.query
|
||||||
self.redirect(url)
|
self.redirect(url)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -28,7 +28,7 @@ class RootHandler(BaseHandler):
|
|||||||
"""
|
"""
|
||||||
def get(self):
|
def get(self):
|
||||||
next_url = self.get_argument('next', '')
|
next_url = self.get_argument('next', '')
|
||||||
if not next_url.startswith('/'):
|
if next_url and not next_url.startswith('/'):
|
||||||
self.log.warning("Disallowing redirect outside JupyterHub: %r", next_url)
|
self.log.warning("Disallowing redirect outside JupyterHub: %r", next_url)
|
||||||
next_url = ''
|
next_url = ''
|
||||||
if next_url and next_url.startswith(url_path_join(self.base_url, 'user/')):
|
if next_url and next_url.startswith(url_path_join(self.base_url, 'user/')):
|
||||||
|
@@ -174,6 +174,7 @@ class HubAuth(Configurable):
|
|||||||
raise HTTPError(500, msg)
|
raise HTTPError(500, msg)
|
||||||
|
|
||||||
if r.status_code == 404:
|
if r.status_code == 404:
|
||||||
|
app_log.warning("No Hub user identified for request")
|
||||||
data = None
|
data = None
|
||||||
elif r.status_code == 403:
|
elif r.status_code == 403:
|
||||||
app_log.error("I don't have permission to verify cookies, my auth token may have expired: [%i] %s", r.status_code, r.reason)
|
app_log.error("I don't have permission to verify cookies, my auth token may have expired: [%i] %s", r.status_code, r.reason)
|
||||||
@@ -186,6 +187,7 @@ class HubAuth(Configurable):
|
|||||||
raise HTTPError(500, "Failed to check authorization")
|
raise HTTPError(500, "Failed to check authorization")
|
||||||
else:
|
else:
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
app_log.debug("Received request from Hub user %s", data)
|
||||||
self.cookie_cache[encrypted_cookie] = data
|
self.cookie_cache[encrypted_cookie] = data
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -274,14 +276,18 @@ class HubAuthenticated(object):
|
|||||||
Returns:
|
Returns:
|
||||||
user_model (dict): The user model if the user should be allowed, None otherwise.
|
user_model (dict): The user model if the user should be allowed, None otherwise.
|
||||||
"""
|
"""
|
||||||
|
name = user_model['name']
|
||||||
if self.hub_users is None and self.hub_groups is None:
|
if self.hub_users is None and self.hub_groups is None:
|
||||||
# no whitelist specified, allow any authenticated Hub user
|
# no whitelist specified, allow any authenticated Hub user
|
||||||
|
app_log.debug("Allowing Hub user %s (all Hub users allowed)", name)
|
||||||
return user_model
|
return user_model
|
||||||
name = user_model['name']
|
|
||||||
if self.hub_users and name in self.hub_users:
|
if self.hub_users and name in self.hub_users:
|
||||||
# user in whitelist
|
# user in whitelist
|
||||||
|
app_log.debug("Allowing whitelisted Hub user %s", name)
|
||||||
return user_model
|
return user_model
|
||||||
elif self.hub_groups and set(user_model['groups']).union(self.hub_groups):
|
elif self.hub_groups and set(user_model['groups']).intersection(self.hub_groups):
|
||||||
|
allowed_groups = set(user_model['groups']).intersection(self.hub_groups)
|
||||||
|
app_log.debug("Allowing Hub user %s in group(s) %s", name, ','.join(sorted(allowed_groups)))
|
||||||
# group in whitelist
|
# group in whitelist
|
||||||
return user_model
|
return user_model
|
||||||
else:
|
else:
|
||||||
@@ -294,8 +300,12 @@ class HubAuthenticated(object):
|
|||||||
Returns:
|
Returns:
|
||||||
user_model (dict): The user model, if a user is identified, None if authentication fails.
|
user_model (dict): The user model, if a user is identified, None if authentication fails.
|
||||||
"""
|
"""
|
||||||
|
if hasattr(self, '_hub_auth_user_cache'):
|
||||||
|
return self._hub_auth_user_cache
|
||||||
user_model = self.hub_auth.get_user(self)
|
user_model = self.hub_auth.get_user(self)
|
||||||
if not user_model:
|
if not user_model:
|
||||||
|
self._hub_auth_user_cache = None
|
||||||
return
|
return
|
||||||
return self.check_hub_user(user_model)
|
self._hub_auth_user_cache = self.check_hub_user(user_model)
|
||||||
|
return self._hub_auth_user_cache
|
||||||
|
|
||||||
|
@@ -70,12 +70,12 @@ class _MockUser(HasTraits):
|
|||||||
|
|
||||||
class _ServiceSpawner(LocalProcessSpawner):
|
class _ServiceSpawner(LocalProcessSpawner):
|
||||||
"""Subclass of LocalProcessSpawner
|
"""Subclass of LocalProcessSpawner
|
||||||
|
|
||||||
Removes notebook-specific-ness from LocalProcessSpawner.
|
Removes notebook-specific-ness from LocalProcessSpawner.
|
||||||
"""
|
"""
|
||||||
cwd = Unicode()
|
cwd = Unicode()
|
||||||
cmd = Command(minlen=0)
|
cmd = Command(minlen=0)
|
||||||
|
|
||||||
def make_preexec_fn(self, name):
|
def make_preexec_fn(self, name):
|
||||||
if not name or name == getuser():
|
if not name or name == getuser():
|
||||||
# no setuid if no name
|
# no setuid if no name
|
||||||
@@ -116,25 +116,25 @@ class Service(LoggingConfigurable):
|
|||||||
- url: str (None)
|
- url: str (None)
|
||||||
The URL where the service is/should be.
|
The URL where the service is/should be.
|
||||||
If specified, the service will be added to the proxy at /services/:name
|
If specified, the service will be added to the proxy at /services/:name
|
||||||
|
|
||||||
If a service is to be managed by the Hub, it has a few extra options:
|
If a service is to be managed by the Hub, it has a few extra options:
|
||||||
|
|
||||||
- command: (str/Popen list)
|
- command: (str/Popen list)
|
||||||
Command for JupyterHub to spawn the service.
|
Command for JupyterHub to spawn the service.
|
||||||
Only use this if the service should be a subprocess.
|
Only use this if the service should be a subprocess.
|
||||||
If command is not specified, it is assumed to be managed
|
If command is not specified, it is assumed to be managed
|
||||||
by a
|
by a
|
||||||
- env: dict
|
- environment: dict
|
||||||
environment variables to add to the current env
|
Additional environment variables for the service.
|
||||||
- user: str
|
- user: str
|
||||||
The name of a system user to become.
|
The name of a system user to become.
|
||||||
If unspecified, run as the same user as the Hub.
|
If unspecified, run as the same user as the Hub.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# inputs:
|
# inputs:
|
||||||
name = Unicode(
|
name = Unicode(
|
||||||
help="""The name of the service.
|
help="""The name of the service.
|
||||||
|
|
||||||
If the service has an http endpoint, it
|
If the service has an http endpoint, it
|
||||||
"""
|
"""
|
||||||
).tag(input=True)
|
).tag(input=True)
|
||||||
@@ -143,14 +143,14 @@ class Service(LoggingConfigurable):
|
|||||||
).tag(input=True)
|
).tag(input=True)
|
||||||
url = Unicode(
|
url = Unicode(
|
||||||
help="""URL of the service.
|
help="""URL of the service.
|
||||||
|
|
||||||
Only specify if the service runs an HTTP(s) endpoint that.
|
Only specify if the service runs an HTTP(s) endpoint that.
|
||||||
If managed, will be passed as JUPYTERHUB_SERVICE_URL env.
|
If managed, will be passed as JUPYTERHUB_SERVICE_URL env.
|
||||||
"""
|
"""
|
||||||
).tag(input=True)
|
).tag(input=True)
|
||||||
api_token = Unicode(
|
api_token = Unicode(
|
||||||
help="""The API token to use for the service.
|
help="""The API token to use for the service.
|
||||||
|
|
||||||
If unspecified, an API token will be generated for managed services.
|
If unspecified, an API token will be generated for managed services.
|
||||||
"""
|
"""
|
||||||
).tag(input=True)
|
).tag(input=True)
|
||||||
|
71
jupyterhub/singleuser.py
Normal file → Executable file
71
jupyterhub/singleuser.py
Normal file → Executable file
@@ -5,6 +5,7 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from jinja2 import ChoiceLoader, FunctionLoader
|
from jinja2 import ChoiceLoader, FunctionLoader
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ from traitlets import (
|
|||||||
Unicode,
|
Unicode,
|
||||||
CUnicode,
|
CUnicode,
|
||||||
default,
|
default,
|
||||||
|
observe,
|
||||||
validate,
|
validate,
|
||||||
TraitError,
|
TraitError,
|
||||||
)
|
)
|
||||||
@@ -47,7 +49,12 @@ class HubAuthenticatedHandler(HubAuthenticated):
|
|||||||
@property
|
@property
|
||||||
def hub_users(self):
|
def hub_users(self):
|
||||||
return { self.settings['user'] }
|
return { self.settings['user'] }
|
||||||
|
@property
|
||||||
|
def hub_groups(self):
|
||||||
|
if self.settings['group']:
|
||||||
|
return { self.settings['group'] }
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
class JupyterHubLoginHandler(LoginHandler):
|
class JupyterHubLoginHandler(LoginHandler):
|
||||||
"""LoginHandler that hooks up Hub authentication"""
|
"""LoginHandler that hooks up Hub authentication"""
|
||||||
@@ -76,6 +83,7 @@ class JupyterHubLogoutHandler(LogoutHandler):
|
|||||||
aliases = dict(notebook_aliases)
|
aliases = dict(notebook_aliases)
|
||||||
aliases.update({
|
aliases.update({
|
||||||
'user' : 'SingleUserNotebookApp.user',
|
'user' : 'SingleUserNotebookApp.user',
|
||||||
|
'group': 'SingleUserNotebookApp.group',
|
||||||
'cookie-name': 'HubAuth.cookie_name',
|
'cookie-name': 'HubAuth.cookie_name',
|
||||||
'hub-prefix': 'SingleUserNotebookApp.hub_prefix',
|
'hub-prefix': 'SingleUserNotebookApp.hub_prefix',
|
||||||
'hub-host': 'SingleUserNotebookApp.hub_host',
|
'hub-host': 'SingleUserNotebookApp.hub_host',
|
||||||
@@ -118,6 +126,7 @@ def _exclude_home(path_list):
|
|||||||
if not p.startswith(home):
|
if not p.startswith(home):
|
||||||
yield p
|
yield p
|
||||||
|
|
||||||
|
|
||||||
class SingleUserNotebookApp(NotebookApp):
|
class SingleUserNotebookApp(NotebookApp):
|
||||||
"""A Subclass of the regular NotebookApp that is aware of the parent multiuser context."""
|
"""A Subclass of the regular NotebookApp that is aware of the parent multiuser context."""
|
||||||
description = dedent("""
|
description = dedent("""
|
||||||
@@ -131,15 +140,51 @@ class SingleUserNotebookApp(NotebookApp):
|
|||||||
version = __version__
|
version = __version__
|
||||||
classes = NotebookApp.classes + [HubAuth]
|
classes = NotebookApp.classes + [HubAuth]
|
||||||
|
|
||||||
user = CUnicode(config=True)
|
user = CUnicode().tag(config=True)
|
||||||
def _user_changed(self, name, old, new):
|
group = CUnicode().tag(config=True)
|
||||||
self.log.name = new
|
@observe('user')
|
||||||
hub_prefix = Unicode().tag(config=True)
|
def _user_changed(self, change):
|
||||||
|
self.log.name = change.new
|
||||||
|
|
||||||
hub_host = Unicode().tag(config=True)
|
hub_host = Unicode().tag(config=True)
|
||||||
|
|
||||||
|
hub_prefix = Unicode('/hub/').tag(config=True)
|
||||||
|
@default('hub_prefix')
|
||||||
|
def _hub_prefix_default(self):
|
||||||
|
base_url = os.environ.get('JUPYTERHUB_BASE_URL') or '/'
|
||||||
|
return base_url + 'hub/'
|
||||||
|
|
||||||
hub_api_url = Unicode().tag(config=True)
|
hub_api_url = Unicode().tag(config=True)
|
||||||
|
@default('hub_api_url')
|
||||||
|
def _hub_api_url_default(self):
|
||||||
|
return os.environ.get('JUPYTERHUB_API_URL') or 'http://127.0.0.1:8081/hub/api'
|
||||||
|
|
||||||
|
# defaults for some configurables that may come from service env variables:
|
||||||
|
@default('base_url')
|
||||||
|
def _base_url_default(self):
|
||||||
|
return os.environ.get('JUPYTERHUB_SERVICE_PREFIX') or '/'
|
||||||
|
|
||||||
|
@default('cookie_name')
|
||||||
|
def _cookie_name_default(self):
|
||||||
|
if os.environ.get('JUPYTERHUB_SERVICE_NAME'):
|
||||||
|
# if I'm a service, use the services cookie name
|
||||||
|
return 'jupyterhub-services'
|
||||||
|
|
||||||
|
@default('port')
|
||||||
|
def _port_default(self):
|
||||||
|
if os.environ.get('JUPYTERHUB_SERVICE_URL'):
|
||||||
|
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
|
||||||
|
return url.port
|
||||||
|
|
||||||
|
@default('ip')
|
||||||
|
def _ip_default(self):
|
||||||
|
if os.environ.get('JUPYTERHUB_SERVICE_URL'):
|
||||||
|
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
|
||||||
|
return url.hostname
|
||||||
|
|
||||||
aliases = aliases
|
aliases = aliases
|
||||||
flags = flags
|
flags = flags
|
||||||
|
|
||||||
# disble some single-user configurables
|
# disble some single-user configurables
|
||||||
token = ''
|
token = ''
|
||||||
open_browser = False
|
open_browser = False
|
||||||
@@ -221,11 +266,18 @@ class SingleUserNotebookApp(NotebookApp):
|
|||||||
super(SingleUserNotebookApp, self).start()
|
super(SingleUserNotebookApp, self).start()
|
||||||
|
|
||||||
def init_hub_auth(self):
|
def init_hub_auth(self):
|
||||||
if not os.environ.get('JPY_API_TOKEN'):
|
api_token = None
|
||||||
self.exit("JPY_API_TOKEN env is required to run jupyterhub-singleuser. Did you launch it manually?")
|
if os.getenv('JPY_API_TOKEN'):
|
||||||
|
# Deprecated env variable (as of 0.7.2)
|
||||||
|
api_token = os.environ.pop('JPY_API_TOKEN')
|
||||||
|
if os.getenv('JUPYTERHUB_API_TOKEN'):
|
||||||
|
api_token = os.environ.pop('JUPYTERHUB_API_TOKEN')
|
||||||
|
|
||||||
|
if not api_token:
|
||||||
|
self.exit("JUPYTERHUB_API_TOKEN env is required to run jupyterhub-singleuser. Did you launch it manually?")
|
||||||
self.hub_auth = HubAuth(
|
self.hub_auth = HubAuth(
|
||||||
parent=self,
|
parent=self,
|
||||||
api_token=os.environ.pop('JPY_API_TOKEN'),
|
api_token=api_token,
|
||||||
api_url=self.hub_api_url,
|
api_url=self.hub_api_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -234,6 +286,7 @@ class SingleUserNotebookApp(NotebookApp):
|
|||||||
self.init_hub_auth()
|
self.init_hub_auth()
|
||||||
s = self.tornado_settings
|
s = self.tornado_settings
|
||||||
s['user'] = self.user
|
s['user'] = self.user
|
||||||
|
s['group'] = self.group
|
||||||
s['hub_prefix'] = self.hub_prefix
|
s['hub_prefix'] = self.hub_prefix
|
||||||
s['hub_host'] = self.hub_host
|
s['hub_host'] = self.hub_host
|
||||||
s['hub_auth'] = self.hub_auth
|
s['hub_auth'] = self.hub_auth
|
||||||
|
@@ -52,6 +52,17 @@ class Spawner(LoggingConfigurable):
|
|||||||
authenticator = Any()
|
authenticator = Any()
|
||||||
api_token = Unicode()
|
api_token = Unicode()
|
||||||
|
|
||||||
|
will_resume = Bool(False,
|
||||||
|
help="""Whether the Spawner will resume on next start
|
||||||
|
|
||||||
|
|
||||||
|
Default is False where each launch of the Spawner will be a new instance.
|
||||||
|
If True, an existing Spawner will resume instead of starting anew
|
||||||
|
(e.g. resuming a Docker container),
|
||||||
|
and API tokens in use when the Spawner stops will not be deleted.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
ip = Unicode('127.0.0.1',
|
ip = Unicode('127.0.0.1',
|
||||||
help="""
|
help="""
|
||||||
The IP address (or hostname) the single-user server should listen on.
|
The IP address (or hostname) the single-user server should listen on.
|
||||||
@@ -179,7 +190,7 @@ class Spawner(LoggingConfigurable):
|
|||||||
Environment variables that end up in the single-user server's process come from 3 sources:
|
Environment variables that end up in the single-user server's process come from 3 sources:
|
||||||
- This `environment` configurable
|
- This `environment` configurable
|
||||||
- The JupyterHub process' environment variables that are whitelisted in `env_keep`
|
- The JupyterHub process' environment variables that are whitelisted in `env_keep`
|
||||||
- Variables to establish contact between the single-user notebook and the hub (such as JPY_API_TOKEN)
|
- Variables to establish contact between the single-user notebook and the hub (such as JUPYTERHUB_API_TOKEN)
|
||||||
|
|
||||||
The `enviornment` configurable should be set by JupyterHub administrators to add
|
The `enviornment` configurable should be set by JupyterHub administrators to add
|
||||||
installation specific environment variables. It is a dict where the key is the name of the environment
|
installation specific environment variables. It is a dict where the key is the name of the environment
|
||||||
@@ -403,7 +414,9 @@ class Spawner(LoggingConfigurable):
|
|||||||
env[key] = value(self)
|
env[key] = value(self)
|
||||||
else:
|
else:
|
||||||
env[key] = value
|
env[key] = value
|
||||||
|
|
||||||
|
env['JUPYTERHUB_API_TOKEN'] = self.api_token
|
||||||
|
# deprecated (as of 0.7.2), for old versions of singleuser
|
||||||
env['JPY_API_TOKEN'] = self.api_token
|
env['JPY_API_TOKEN'] = self.api_token
|
||||||
|
|
||||||
# Put in limit and guarantee info if they exist.
|
# Put in limit and guarantee info if they exist.
|
||||||
|
@@ -101,7 +101,8 @@ def test_hub_auth():
|
|||||||
def test_hub_authenticated(request):
|
def test_hub_authenticated(request):
|
||||||
auth = HubAuth(cookie_name='jubal')
|
auth = HubAuth(cookie_name='jubal')
|
||||||
mock_model = {
|
mock_model = {
|
||||||
'name': 'jubalearly'
|
'name': 'jubalearly',
|
||||||
|
'groups': ['lions'],
|
||||||
}
|
}
|
||||||
cookie_url = url_path_join(auth.api_url, "authorizations/cookie", auth.cookie_name)
|
cookie_url = url_path_join(auth.api_url, "authorizations/cookie", auth.cookie_name)
|
||||||
good_url = url_path_join(cookie_url, "early")
|
good_url = url_path_join(cookie_url, "early")
|
||||||
@@ -193,6 +194,25 @@ def test_hub_authenticated(request):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
assert r.status_code == 302
|
assert r.status_code == 302
|
||||||
assert auth.login_url in r.headers['Location']
|
assert auth.login_url in r.headers['Location']
|
||||||
|
|
||||||
|
# pass group whitelist
|
||||||
|
TestHandler.hub_groups = {'lions'}
|
||||||
|
r = requests.get('http://127.0.0.1:%i' % port,
|
||||||
|
cookies={'jubal': 'early'},
|
||||||
|
allow_redirects=False,
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
assert r.status_code == 200
|
||||||
|
|
||||||
|
# no pass group whitelist
|
||||||
|
TestHandler.hub_groups = {'tigers'}
|
||||||
|
r = requests.get('http://127.0.0.1:%i' % port,
|
||||||
|
cookies={'jubal': 'early'},
|
||||||
|
allow_redirects=False,
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
assert r.status_code == 302
|
||||||
|
assert auth.login_url in r.headers['Location']
|
||||||
|
|
||||||
|
|
||||||
def test_service_cookie_auth(app, mockservice_url):
|
def test_service_cookie_auth(app, mockservice_url):
|
||||||
|
@@ -234,6 +234,12 @@ class User(HasTraits):
|
|||||||
# prior to 0.7, spawners had to store this info in user.server themselves.
|
# prior to 0.7, spawners had to store this info in user.server themselves.
|
||||||
# Handle < 0.7 behavior with a warning, assuming info was stored in db by the Spawner.
|
# Handle < 0.7 behavior with a warning, assuming info was stored in db by the Spawner.
|
||||||
self.log.warning("DEPRECATION: Spawner.start should return (ip, port) in JupyterHub >= 0.7")
|
self.log.warning("DEPRECATION: Spawner.start should return (ip, port) in JupyterHub >= 0.7")
|
||||||
|
if spawner.api_token != api_token:
|
||||||
|
# Spawner re-used an API token, discard the unused api_token
|
||||||
|
orm_token = orm.APIToken.find(self.db, api_token)
|
||||||
|
if orm_token is not None:
|
||||||
|
self.db.delete(orm_token)
|
||||||
|
self.db.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if isinstance(e, gen.TimeoutError):
|
if isinstance(e, gen.TimeoutError):
|
||||||
self.log.warning("{user}'s server failed to start in {s} seconds, giving up".format(
|
self.log.warning("{user}'s server failed to start in {s} seconds, giving up".format(
|
||||||
@@ -313,10 +319,13 @@ class User(HasTraits):
|
|||||||
if self.server:
|
if self.server:
|
||||||
# cleanup server entry from db
|
# cleanup server entry from db
|
||||||
self.db.delete(self.server)
|
self.db.delete(self.server)
|
||||||
orm_token = orm.APIToken.find(self.db, api_token)
|
|
||||||
if orm_token:
|
|
||||||
self.db.delete(orm_token)
|
|
||||||
self.server = None
|
self.server = None
|
||||||
|
if not spawner.will_resume:
|
||||||
|
# find and remove the API token if the spawner isn't
|
||||||
|
# going to re-use it next time
|
||||||
|
orm_token = orm.APIToken.find(self.db, api_token)
|
||||||
|
if orm_token:
|
||||||
|
self.db.delete(orm_token)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
finally:
|
finally:
|
||||||
self.stop_pending = False
|
self.stop_pending = False
|
||||||
|
@@ -6,7 +6,8 @@
|
|||||||
version_info = (
|
version_info = (
|
||||||
0,
|
0,
|
||||||
7,
|
7,
|
||||||
0,
|
2,
|
||||||
|
# 'dev',
|
||||||
)
|
)
|
||||||
|
|
||||||
__version__ = '.'.join(map(str, version_info))
|
__version__ = '.'.join(map(str, version_info))
|
||||||
|
Reference in New Issue
Block a user