mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-08 02:24:08 +00:00
Compare commits
25 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 |
@@ -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,
|
||||
|
@@ -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
|
||||
|
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',
|
||||
],
|
||||
}
|
||||
]
|
@@ -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)
|
||||
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
67
jupyterhub/singleuser.py
Normal file → Executable file
67
jupyterhub/singleuser.py
Normal file → Executable 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,6 +49,11 @@ 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):
|
||||
@@ -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,12 +140,48 @@ 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
|
||||
|
||||
@@ -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
|
||||
|
@@ -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
|
||||
@@ -415,6 +415,8 @@ class Spawner(LoggingConfigurable):
|
||||
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.
|
||||
|
@@ -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")
|
||||
@@ -194,6 +195,25 @@ def test_hub_authenticated(request):
|
||||
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):
|
||||
cookies = app.login_user('badger')
|
||||
|
@@ -6,7 +6,7 @@
|
||||
version_info = (
|
||||
0,
|
||||
7,
|
||||
1,
|
||||
2,
|
||||
# 'dev',
|
||||
)
|
||||
|
||||
|
Reference in New Issue
Block a user