Compare commits

..

25 Commits
0.7.1 ... 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
13 changed files with 237 additions and 24 deletions

View File

@@ -9,11 +9,24 @@ command line for details.
## 0.7
### [0.7.1] - 2016-01-02
### [0.7.2] - 2017-01-09
#### Added
- `Spawner.will_resume` for signalling that a single-user server is paused instead of stopped.
- 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,

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

@@ -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

@@ -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

@@ -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

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

@@ -190,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
@@ -414,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

@@ -6,7 +6,7 @@
version_info = (
0,
7,
1,
2,
# 'dev',
)