Compare commits

...

49 Commits
0.7.0 ... 0.7.2

Author SHA1 Message Date
Min RK
aa132cade7 release 0.7.2 2017-01-10 16:12:45 +01:00
Carol Willing
dd35ffbe86 Merge pull request #928 from minrk/0.7.2
Changelog for 0.7.2
2017-01-09 16:18:12 -08:00
Min RK
8edcf8be81 Changelog for 0.7.2 2017-01-09 12:45:16 +01:00
Min RK
29b02b7bcb Merge pull request #927 from willingc/clarify-whitelist
Add better prose for removing users
2017-01-06 20:25:01 +01:00
Carol Willing
0383bc27b2 Add better prose for removing users 2017-01-06 08:52:48 -08:00
Carol Willing
65d5102b49 Merge pull request #926 from minrk/singleuser-service-defaults
support service env vars in singleuser entrypoint
2017-01-06 08:28:48 -08:00
Min RK
8a226e6f46 simplify singleuser-service examples
now that service env vars are respected
2017-01-06 17:21:28 +01:00
Min RK
0bd34e0a10 support service env variables in singleuser
and set a few sensible defaults where they are known/likely
2017-01-06 17:21:10 +01:00
Min RK
186107d959 cache HubAuth user per request 2017-01-06 17:19:10 +01:00
Carol Willing
91b07b7ea4 Merge pull request #924 from minrk/singleuser-service-example
singleuser-server service example
2017-01-06 08:10:34 -08:00
Min RK
f5b30fd2b4 move version requirement further up 2017-01-06 16:57:13 +01:00
Min RK
0234396c2c Merge pull request #922 from yuvipanda/fix-user-redirect
Pass query params through with user-redirect
2017-01-06 13:31:23 +01:00
Min RK
a43d677ae4 add external-service shared notebook example 2017-01-06 13:30:53 +01:00
Min RK
dcfe71e7f0 add managed notebook service example 2017-01-06 13:30:53 +01:00
Min RK
5d41376c2e use JUPYTERHUB_API_TOKEN env in Spawner
to be more consistent with services

deprecate JPY_API_TOKEN, but keep it around for compatibility
2017-01-06 13:30:53 +01:00
Min RK
dd083359ec Merge pull request #925 from minrk/fix-hub-group-auth
[HubAuth] Fix group authentication for services
2017-01-06 13:30:30 +01:00
Min RK
e6d54960ba test group whitelist checking 2017-01-06 13:24:40 +01:00
Min RK
a9295bc5c2 more debug logging for Hub auth 2017-01-06 13:24:40 +01:00
Min RK
2015c701fa HubAuth services: fix group authentication checking
If group authentication checking was enabled, any user would be allowed
2017-01-06 13:24:40 +01:00
YuviPanda
3e9c18f50a Pass query params through with user-redirect 2017-01-05 17:18:36 -08:00
Min RK
7cac874afc Merge pull request #919 from ellisonbg/nbserver-group
Adding group to single user server for group based auth
2017-01-05 14:37:20 +01:00
Brian E. Granger
a7b6bd8d32 Adding group to single user server for group based auth 2017-01-04 20:12:34 -07:00
Min RK
1649a98656 2017 typo 2017-01-03 15:55:39 +01:00
Min RK
ecbe51f60f signaling typo 2017-01-02 14:50:10 +01:00
Min RK
fed14abed3 back to dev 2017-01-02 14:44:07 +01:00
Min RK
94978ea9e0 release 0.7.1 2017-01-02 13:53:43 +01:00
Min RK
bf6999e439 changelog for 0.7.1 2017-01-02 13:53:43 +01:00
Carol Willing
020ee7378f Merge pull request #916 from rachmaninovquartet/master
Added Toree troubleshooting to docs
2016-12-22 13:56:51 -08:00
Min RK
e4a0569961 Merge pull request #915 from jupyterhub/willingc-patch-1
Update README to clarify docker image contents
2016-12-22 16:43:02 +01:00
Ian Maloney
4ff525d5bd updated docs/source/troubleshooting.md per conversation with @willingc in issue 889 2016-12-21 15:21:50 -05:00
Carol Willing
37a31b01b2 Update README to clarify docker image contents
Addresses #879 and #772 re: confusion about the docker image contents
2016-12-21 10:46:30 -08:00
Carol Willing
1604cb1b0b Merge pull request #914 from minrk/update-bootprint
fix rest-api doc building
2016-12-21 10:29:08 -08:00
Min RK
45702ac18c update bootprint to 0.10
0.8 has stopped working for some reason
2016-12-21 14:51:12 +01:00
Min RK
c81e9d60e4 fix rest-api link
link to REST API, not Python API
2016-12-21 14:51:12 +01:00
Carol Willing
224865b894 Merge pull request #910 from minrk/cleanup-server-token
Avoid cleaning up API tokens for Spawners that will resume
2016-12-20 08:29:06 -08:00
Min RK
3b3bc8224b comment review 2016-12-20 16:41:26 +01:00
Carol Willing
c56dc2ea6f Merge pull request #911 from jjaraalm/master
Update Service Docs

Closes #908
2016-12-19 10:28:30 -08:00
jjaraalm
62202bbb74 Revert "Revert "Update service docs""
This reverts commit 7ba28c0207.
2016-12-19 13:00:48 -05:00
jjaraalm
7ba28c0207 Revert "Update service docs"
This reverts commit 9392a29dad.
2016-12-19 12:59:42 -05:00
jjaraalm
9392a29dad Update service docs
Fixes #908
2016-12-19 12:56:26 -05:00
Min RK
72ab8f99ec Avoid cleaning up API tokens for Spawners that will resume
in which case the previous API token should be left alone.
2016-12-19 10:50:25 +01:00
Min RK
fcf32c7e50 Merge pull request #909 from willingc/update-travis
Add 3.6 to travis
2016-12-19 09:59:47 +01:00
Carol Willing
da451d6552 Add 3.6 to travis 2016-12-18 21:26:52 -08:00
Carol Willing
662b1a4d4a Merge pull request #902 from minrk/redirect-empty-msg
Don't warn about empty next_url
2016-12-09 08:04:56 -08:00
Min RK
732adea997 Don't warn about empty next_url
empty next_url is fine
2016-12-09 15:34:32 +01:00
Carol Willing
7e1dbf3515 Merge pull request #896 from minrk/whitelist-warning
Warn about single-character names in whitelist
2016-12-05 11:16:30 -06:00
Min RK
65b92ec246 Warn about single-character names in whitelist
likely cause is `set('string')` typo instead of `set(['string'])`,
so include that in the error message:

    whitelist contains single-character names: ['i', 'k', 'm', 'n', 'r']; did you mean set(['ikmnr']) instead of set('ikmnr')?
2016-12-05 09:46:52 +01:00
Min RK
dc42ee4779 typo in changelog link 2016-12-02 18:12:28 +01:00
Min RK
c04441c1b2 back to dev 2016-12-02 18:08:03 +01:00
24 changed files with 349 additions and 65 deletions

View File

@@ -2,6 +2,7 @@
language: python
sudo: false
python:
- 3.6-dev
- 3.5
- 3.4
- 3.3

View File

@@ -131,11 +131,11 @@ Some examples, meant as illustration and testing of this concept:
----
## 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.
It does not require the other Jupyter components, such as Notebook installation, which are needed by the single-user servers.
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.*
**Important:** This `jupyterhub/jupyterhub` image contains only the Hub itself, with no configuration. In general, one needs
to make a derivative image, with at least a `jupyterhub_config.py` setting up an Authenticator and/or a Spawner. 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.
#### Starting JupyterHub with docker
The JupyterHub docker image can be started with the following command:

View File

@@ -8,7 +8,7 @@
"author": "",
"license": "BSD-3-Clause",
"devDependencies": {
"bootprint": "^0.8.5",
"bootprint": "^0.10.0",
"bootprint-openapi": "^0.17.0"
}
}

View File

@@ -9,6 +9,33 @@ command line for details.
## 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
#### Added
@@ -118,8 +145,9 @@ Fix removal of `/login` page in 0.4.0, breaking some OAuth providers.
First preview release
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/0.7.0...HEAD
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/0.6.1...0.7.0
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/0.7.1...HEAD
[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.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

View File

@@ -405,6 +405,9 @@ You can restrict which users are allowed to login with `Authenticator.whitelist`
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
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
Users can be added and removed to the Hub via the admin panel or REST API. These users will be
added to the whitelist and database. Restarting the Hub will not require manually updating the
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
config file. You must also remove them from the database, either by discarding the database file,
or via the admin UI.
Users can be added to and removed from the Hub via either the admin panel or
REST API.
If a user is **added**, the user will be automatically added to the whitelist
and database. Restarting the Hub will not require manually updating the
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
`LocalAuthenticator`, indicating that it manages users on the local system. When you add a user to

View File

@@ -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
[OpenAPI Initiative]: https://www.openapis.org/
[JupyterHub REST API]: ./api/index.html
[JupyterHub REST API]: ./_static/rest-api/index.html

View File

@@ -54,7 +54,7 @@ If a service is also to be managed by the Hub, it has a few extra options:
externally.
- If a command is specified for launching the Service, the Service will
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
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
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.
Requires Hub to be root.
- `cwd: path` directory in which to run the Service, if different from the

View File

@@ -249,3 +249,26 @@ jupyter kernelspec list
```bash
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).

View 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`

View 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',
}
]

View 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'

View 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

View 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',
],
}
]

View File

@@ -146,18 +146,18 @@ class NewToken(Application):
class UpgradeDB(Application):
"""Upgrade the JupyterHub database schema."""
name = 'jupyterhub-upgrade-db'
version = jupyterhub.__version__
description = """Upgrade the JupyterHub database to the current schema.
Usage:
jupyterhub upgrade-db
"""
aliases = common_aliases
classes = []
def _backup_db_file(self, db_file):
"""Backup a database file"""
if not os.path.exists(db_file):
@@ -171,7 +171,7 @@ class UpgradeDB(Application):
backup_db_file = '{}.{}.{}'.format(db_file, timestamp, i)
if os.path.exists(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)
shutil.copy(db_file, backup_db_file)
@@ -222,12 +222,12 @@ class JupyterHub(Application):
Authenticator,
PAMAuthenticator,
])
load_groups = Dict(List(Unicode()),
help="""Dict of 'group': ['usernames'] to load at startup.
This strictly *adds* groups and users to groups.
Loading one set of groups, then starting JupyterHub again with a different
set will not remove users or groups from previous launches.
That must be done through the API.
@@ -414,7 +414,7 @@ class JupyterHub(Application):
api_tokens = Dict(Unicode(),
help="""PENDING DEPRECATION: consider using service_tokens
Dict of token:username to be loaded into the database.
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.
"""
).tag(config=True)
services = List(Dict(),
help="""List of service specification dictionaries.
A service
For instance::
services = [
{
'name': 'cull_idle',
@@ -454,7 +454,7 @@ class JupyterHub(Application):
'name': 'formgrader',
'url': 'http://127.0.0.1:1234',
'token': 'super-secret',
'env':
'environment':
}
]
"""
@@ -608,7 +608,7 @@ class JupyterHub(Application):
Instance(logging.Handler),
help="Extra log handlers to set on JupyterHub logger",
).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")
@default('statsd')
def _statsd(self):
@@ -919,7 +919,7 @@ class JupyterHub(Application):
# The whitelist set and the users in the db are now the same.
# 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).
@gen.coroutine
def init_groups(self):
"""Load predefined groups into the database"""
@@ -941,7 +941,7 @@ class JupyterHub(Application):
db.add(user)
group.users.append(user)
db.commit()
@gen.coroutine
def _add_tokens(self, token_dict, kind):
"""Add tokens for users or services to the database"""
@@ -982,13 +982,13 @@ class JupyterHub(Application):
else:
self.log.debug("Not duplicating token %s", orm_token)
db.commit()
@gen.coroutine
def init_api_tokens(self):
"""Load predefined API tokens (for services) into database"""
yield self._add_tokens(self.service_tokens, kind='service')
yield self._add_tokens(self.api_tokens, kind='user')
def init_services(self):
self._service_map.clear()
if self.domain:
@@ -1458,7 +1458,7 @@ class JupyterHub(Application):
except Exception as e:
self.log.critical("Failed to start proxy", exc_info=True)
self.exit(1)
for service_name, service in self._service_map.items():
if not service.managed:
continue

View File

@@ -56,6 +56,17 @@ class Authenticator(LoggingConfigurable):
"""
).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(
help="""
HTML form to be overridden by authenticators if they want a custom authentication form.

View File

@@ -571,6 +571,9 @@ class UserRedirectHandler(BaseHandler):
def get(self, path):
user = self.get_current_user()
url = url_path_join(user.url, path)
if self.request.query:
# FIXME: use urlunparse instead?
url += '?' + self.request.query
self.redirect(url)

View File

@@ -28,7 +28,7 @@ class RootHandler(BaseHandler):
"""
def get(self):
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)
next_url = ''
if next_url and next_url.startswith(url_path_join(self.base_url, 'user/')):

View File

@@ -174,6 +174,7 @@ class HubAuth(Configurable):
raise HTTPError(500, msg)
if r.status_code == 404:
app_log.warning("No Hub user identified for request")
data = None
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)
@@ -186,6 +187,7 @@ class HubAuth(Configurable):
raise HTTPError(500, "Failed to check authorization")
else:
data = r.json()
app_log.debug("Received request from Hub user %s", data)
self.cookie_cache[encrypted_cookie] = data
return data
@@ -274,14 +276,18 @@ class HubAuthenticated(object):
Returns:
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:
# no whitelist specified, allow any authenticated Hub user
app_log.debug("Allowing Hub user %s (all Hub users allowed)", name)
return user_model
name = user_model['name']
if self.hub_users and name in self.hub_users:
# user in whitelist
app_log.debug("Allowing whitelisted Hub user %s", name)
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
return user_model
else:
@@ -294,8 +300,12 @@ class HubAuthenticated(object):
Returns:
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)
if not user_model:
self._hub_auth_user_cache = None
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

View File

@@ -70,12 +70,12 @@ class _MockUser(HasTraits):
class _ServiceSpawner(LocalProcessSpawner):
"""Subclass of LocalProcessSpawner
Removes notebook-specific-ness from LocalProcessSpawner.
"""
cwd = Unicode()
cmd = Command(minlen=0)
def make_preexec_fn(self, name):
if not name or name == getuser():
# no setuid if no name
@@ -116,25 +116,25 @@ class Service(LoggingConfigurable):
- url: str (None)
The URL where the service is/should be.
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:
- 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, it is assumed to be managed
by a
- 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 become.
If unspecified, run as the same user as the Hub.
"""
# inputs:
name = Unicode(
help="""The name of the service.
If the service has an http endpoint, it
"""
).tag(input=True)
@@ -143,14 +143,14 @@ class Service(LoggingConfigurable):
).tag(input=True)
url = Unicode(
help="""URL of the service.
Only specify if the service runs an HTTP(s) endpoint that.
If managed, will be passed as JUPYTERHUB_SERVICE_URL env.
"""
).tag(input=True)
api_token = Unicode(
help="""The API token to use for the service.
If unspecified, an API token will be generated for managed services.
"""
).tag(input=True)

71
jupyterhub/singleuser.py Normal file → Executable file
View File

@@ -5,6 +5,7 @@
# Distributed under the terms of the Modified BSD License.
import os
from urllib.parse import urlparse
from jinja2 import ChoiceLoader, FunctionLoader
@@ -21,6 +22,7 @@ from traitlets import (
Unicode,
CUnicode,
default,
observe,
validate,
TraitError,
)
@@ -47,7 +49,12 @@ class HubAuthenticatedHandler(HubAuthenticated):
@property
def hub_users(self):
return { self.settings['user'] }
@property
def hub_groups(self):
if self.settings['group']:
return { self.settings['group'] }
return set()
class JupyterHubLoginHandler(LoginHandler):
"""LoginHandler that hooks up Hub authentication"""
@@ -76,6 +83,7 @@ class JupyterHubLogoutHandler(LogoutHandler):
aliases = dict(notebook_aliases)
aliases.update({
'user' : 'SingleUserNotebookApp.user',
'group': 'SingleUserNotebookApp.group',
'cookie-name': 'HubAuth.cookie_name',
'hub-prefix': 'SingleUserNotebookApp.hub_prefix',
'hub-host': 'SingleUserNotebookApp.hub_host',
@@ -118,6 +126,7 @@ def _exclude_home(path_list):
if not p.startswith(home):
yield p
class SingleUserNotebookApp(NotebookApp):
"""A Subclass of the regular NotebookApp that is aware of the parent multiuser context."""
description = dedent("""
@@ -131,15 +140,51 @@ class SingleUserNotebookApp(NotebookApp):
version = __version__
classes = NotebookApp.classes + [HubAuth]
user = CUnicode(config=True)
def _user_changed(self, name, old, new):
self.log.name = new
hub_prefix = Unicode().tag(config=True)
user = CUnicode().tag(config=True)
group = CUnicode().tag(config=True)
@observe('user')
def _user_changed(self, change):
self.log.name = change.new
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)
@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
flags = flags
# disble some single-user configurables
token = ''
open_browser = False
@@ -221,11 +266,18 @@ class SingleUserNotebookApp(NotebookApp):
super(SingleUserNotebookApp, self).start()
def init_hub_auth(self):
if not os.environ.get('JPY_API_TOKEN'):
self.exit("JPY_API_TOKEN env is required to run jupyterhub-singleuser. Did you launch it manually?")
api_token = None
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(
parent=self,
api_token=os.environ.pop('JPY_API_TOKEN'),
api_token=api_token,
api_url=self.hub_api_url,
)
@@ -234,6 +286,7 @@ class SingleUserNotebookApp(NotebookApp):
self.init_hub_auth()
s = self.tornado_settings
s['user'] = self.user
s['group'] = self.group
s['hub_prefix'] = self.hub_prefix
s['hub_host'] = self.hub_host
s['hub_auth'] = self.hub_auth

View File

@@ -52,6 +52,17 @@ class Spawner(LoggingConfigurable):
authenticator = Any()
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',
help="""
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:
- This `environment` configurable
- 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
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)
else:
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
# Put in limit and guarantee info if they exist.

View File

@@ -101,7 +101,8 @@ def test_hub_auth():
def test_hub_authenticated(request):
auth = HubAuth(cookie_name='jubal')
mock_model = {
'name': 'jubalearly'
'name': 'jubalearly',
'groups': ['lions'],
}
cookie_url = url_path_join(auth.api_url, "authorizations/cookie", auth.cookie_name)
good_url = url_path_join(cookie_url, "early")
@@ -193,6 +194,25 @@ def test_hub_authenticated(request):
r.raise_for_status()
assert r.status_code == 302
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):

View File

@@ -234,6 +234,12 @@ class User(HasTraits):
# 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.
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:
if isinstance(e, gen.TimeoutError):
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:
# cleanup server entry from db
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
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()
finally:
self.stop_pending = False

View File

@@ -6,7 +6,8 @@
version_info = (
0,
7,
0,
2,
# 'dev',
)
__version__ = '.'.join(map(str, version_info))