Compare commits

..

37 Commits
0.2.0 ... 0.3.0

Author SHA1 Message Date
Min RK
4931684a2c release 0.3 2015-11-04 17:10:36 +01:00
Min RK
62d3cc53ef changelog for 0.3 2015-11-04 17:09:34 +01:00
Min RK
bd002e5340 Merge pull request #325 from minrk/authenticator-hooks
add pre/post-spawn hooks for Authenticators
2015-11-04 16:07:01 +00:00
Min RK
6f2aefb990 add pre/post-spawn hooks for Authenticators
allows setup/cleanup to be performed by the authenticator

use this to open PAM sessions at spawn
and close them at stop,
rather than open at login and never close.
2015-10-16 12:02:44 +02:00
Min RK
bd3c878c67 Merge pull request #320 from minrk/authenticator-username
get username from authenticator
2015-10-06 15:43:22 +02:00
Min RK
c1de376b6a Merge pull request #310 from minrk/singleuser-notebook
single-user imports notebook package directly
2015-10-06 14:08:35 +02:00
Min RK
4cc74d287e get username from authenticator 2015-10-06 13:36:34 +02:00
Min RK
411a7a0bd8 single-user imports notebook package directly
instead of relying on IPython.html shims

when should we drop support for IPython 3?
2015-09-24 16:13:28 +02:00
Min RK
498c062ee0 Merge pull request #309 from minrk/gen.sleep
use gen.sleep
2015-09-24 16:09:55 +02:00
Min RK
d1edbddb77 use gen.sleep
instead of elaborate `gen.Task(add_timeout...)`

requires tornado 4.1
2015-09-23 17:04:01 +02:00
Min RK
0c9214ffb7 Merge pull request #307 from minrk/test-3.5
test on 3.5
2015-09-22 14:17:30 +02:00
Min RK
db0aaf1027 test on 3.5
requires pytest >= 2.8
2015-09-22 14:09:23 +02:00
Min RK
42681f8512 Merge pull request #306 from minrk/test-token-username
update token app test
2015-09-22 14:08:41 +02:00
Min RK
e5c1414b6a update token app test
now that admin user isn't added by default
2015-09-22 10:14:11 +02:00
Min RK
d857c20de0 Merge pull request #304 from minrk/rm-default-admin
Remove implicit admin of launching user
2015-09-22 08:59:28 +02:00
Min RK
a267174a03 Remove implicit admin of launching user
instead, warn about missing admins and point to config.
2015-09-21 10:52:19 +02:00
Min RK
768eeee470 Merge pull request #298 from minrk/spawner-authenticator
give Spawners a handle on the Authenticator
2015-09-11 14:24:38 +02:00
Min RK
a451f11cd3 give Spawners a handle on the Authenticator
band-aid for spawner-authenticator pairs
2015-09-11 11:57:41 +02:00
Min RK
63a476f9a6 remove some unused cruft from spawner 2015-09-11 11:23:00 +02:00
Min RK
100b17819d Merge pull request #296 from minrk/pamela
use pamela instead of simplepam
2015-09-11 11:02:14 +02:00
Min RK
024d8d7378 update mocking for pamela 2015-09-09 14:24:53 +02:00
Min RK
15e50529ff use pamela instead of simplepam
and open PAM sessions after successful auth
2015-09-09 13:55:02 +02:00
Min RK
a1a10be747 Merge pull request #290 from jhamrick/clear-login-cookies
Unset all login cookies
2015-08-22 18:55:30 -07:00
Jessica B. Hamrick
a91ee67e74 Reset other_user_cookies after clearing them 2015-08-22 13:14:05 -07:00
Jessica B. Hamrick
ea5bfa9999 Unset all login cookies 2015-08-21 19:24:44 -07:00
Min RK
bea58ee622 Merge pull request #288 from minrk/dont-auto-redirect-root
redirect unauthenticated root to *regular* login page
2015-08-19 21:44:00 -07:00
Min RK
b698d4d226 redirect root to *regular* login page
shows "Login with..." button for external services
instead of auto-redirecting to login service
(no good for oauth)
2015-08-19 12:43:32 -07:00
Min RK
139c7ecacb always render login page at /login 2015-08-19 12:30:10 -07:00
Min RK
eefa8fcad7 Merge pull request #284 from minrk/double-base-url
remove double base_url in login redirect
2015-08-06 21:48:49 -07:00
Min RK
acaedcd898 remove double base_url in login redirect
user.server.base_url is already correct,
and shouldn't be joined with the hub url
2015-08-06 21:37:06 -07:00
Min RK
a075661bfb Merge pull request #276 from Crunch-io/redirect-to-login
Redirect unauthenticated root to login
2015-07-23 13:00:16 -07:00
Joseph Tate
f2246df5bb Fix logging and comments 2015-07-23 15:08:53 -04:00
Joseph Tate
1a3c062512 Fix broken test 2015-07-23 15:06:20 -04:00
Joseph Tate
05e4ab41fe Redirect to the loginurl when not logged in, fix the user.running redirect to fix a redirect loop 2015-07-23 15:06:03 -04:00
Min RK
6f3ccb2d3d Merge pull request #275 from jhamrick/installation-instructions
Update installation instructions
2015-07-14 22:06:52 -07:00
Jessica B. Hamrick
6e5ce236c1 Update installation instructions 2015-07-14 15:36:35 -07:00
Min RK
58437057a1 back to dev 2015-07-12 15:30:47 -05:00
20 changed files with 154 additions and 77 deletions

View File

@@ -2,6 +2,7 @@
language: python
sudo: false
python:
- 3.5
- 3.4
- 3.3
before_install:
@@ -10,7 +11,7 @@ before_install:
- git clone --quiet --depth 1 https://github.com/minrk/travis-wheels travis-wheels
install:
- pip install -f travis-wheels/wheelhouse -r dev-requirements.txt .
- pip install -f travis-wheels/wheelhouse ipython[notebook]
- pip install -f travis-wheels/wheelhouse notebook
script:
- py.test --cov jupyterhub jupyterhub/tests -v
after_success:

View File

@@ -48,16 +48,10 @@ Then install javascript dependencies:
## Installation
As usual start with cloning the code:
JupyterHub can be installed with pip:
git clone https://github.com/jupyter/jupyterhub.git
cd jupyterhub
pip3 install jupyterhub
Then you can install the Python package by doing:
pip3 install -r requirements.txt
pip3 install .
If the `pip3 install .` command fails and complains about `lessc` being unavailable, you may need to explicitly install some additional javascript dependencies:
npm install
@@ -66,7 +60,6 @@ If you plan to run notebook servers locally, you may also need to install the IP
pip3 install "ipython[notebook]"
This will fetch client-side javascript dependencies and compile CSS,
and install these files to `sys.prefix`/share/jupyter, as well as
install any Python dependencies.
@@ -74,8 +67,10 @@ install any Python dependencies.
### Development install
For a development install:
For a development install, clone the repository and then install from source:
git clone https://github.com/jupyter/jupyterhub
cd jupyterhub
pip3 install -r dev-requirements.txt -e .
In which case you may need to manually update javascript and css after some updates, with:

View File

@@ -1,4 +1,4 @@
-r requirements.txt
coveralls
pytest-cov
pytest
pytest>=2.8

22
docs/changelog.md Normal file
View File

@@ -0,0 +1,22 @@
# Summary of changes in JupyterHub
See `git log` for a more detailed summary.
## 0.3.0
- No longer make the user starting the Hub an admin
- start PAM sessions on login
- hooks for Authenticators to fire before spawners start and after they stop,
allowing deeper interaction between Spawner/Authenticator pairs.
- login redirect fixes
## 0.2.0
- Based on standalone traitlets instead of IPython.utils.traitlets
- multiple users in admin panel
- Fixes for usernames that require escaping
## 0.1.0
First preview release

View File

@@ -196,6 +196,7 @@ class UserAdminAccessAPIHandler(APIHandler):
if not user.running:
raise web.HTTPError(400, "%s's server is not running" % name)
self.set_server_cookie(user)
current.other_user_cookies.add(name)
default_handlers = [

View File

@@ -628,10 +628,8 @@ class JupyterHub(Application):
admin_users = self.authenticator.admin_users
if not admin_users:
# add current user as admin if there aren't any others
admins = db.query(orm.User).filter(orm.User.admin==True)
if admins.first() is None:
admin_users.add(getuser())
self.log.warning("No admin users, admin interface will be unavailable.")
self.log.warning("Add any administrative users to `c.Authenticator.admin_users` in config.")
new_users = []
@@ -711,6 +709,7 @@ class JupyterHub(Application):
self.log.debug("Loading state for %s from db", user.name)
user.spawner = spawner = self.spawner_class(
user=user, hub=self.hub, config=self.config, db=self.db,
authenticator=self.authenticator,
)
status = yield spawner.poll()
if status is None:

View File

@@ -8,7 +8,7 @@ import pwd
from subprocess import check_call, check_output, CalledProcessError
from tornado import gen
import simplepam
import pamela
from traitlets.config import LoggingConfigurable
from traitlets import Bool, Set, Unicode, Any
@@ -58,6 +58,18 @@ class Authenticator(LoggingConfigurable):
and return None on failed authentication.
"""
def pre_spawn_start(self, user, spawner):
"""Hook called before spawning a user's server.
Can be used to do auth-related startup, e.g. opening PAM sessions.
"""
def post_spawn_stop(self, user, spawner):
"""Hook called after stopping a user container.
Can be used to do auth-related cleanup, e.g. closing PAM sessions.
"""
def check_whitelist(self, user):
"""
Return True if the whitelist is empty or user is in the whitelist.
@@ -208,10 +220,24 @@ class PAMAuthenticator(LocalAuthenticator):
username = data['username']
if not self.check_whitelist(username):
return
# simplepam wants bytes, not unicode
# see simplepam#3
busername = username.encode(self.encoding)
bpassword = data['password'].encode(self.encoding)
if simplepam.authenticate(busername, bpassword, service=self.service):
try:
pamela.authenticate(username, data['password'], service=self.service)
except pamela.PAMError as e:
self.log.warn("PAM Authentication failed: %s", e)
else:
return username
def pre_spawn_start(self, user, spawner):
"""Open PAM session for user"""
try:
pamela.open_session(user.name, service=self.service)
except pamela.PAMError as e:
self.log.warn("Failed to open PAM session for %s: %s", user.name, e)
def post_spawn_stop(self, user, spawner):
"""Close PAM session for user"""
try:
pamela.close_session(user.name, service=self.service)
except pamela.PAMError as e:
self.log.warn("Failed to close PAM session for %s: %s", user.name, e)

View File

@@ -179,8 +179,11 @@ class BaseHandler(RequestHandler):
self.db.commit()
return user
def clear_login_cookie(self):
user = self.get_current_user()
def clear_login_cookie(self, name=None):
if name is None:
user = self.get_current_user()
else:
user = self.find_user(name)
if user and user.server:
self.clear_cookie(user.server.cookie_name, path=user.server.base_url)
self.clear_cookie(self.hub.server.cookie_name, path=self.hub.server.base_url)
@@ -262,6 +265,7 @@ class BaseHandler(RequestHandler):
base_url=self.base_url,
hub=self.hub,
config=self.config,
authenticator=self.authenticator,
)
@gen.coroutine
def finish_user_spawn(f=None):

View File

@@ -16,6 +16,9 @@ class LogoutHandler(BaseHandler):
if user:
self.log.info("User logged out: %s", user.name)
self.clear_login_cookie()
for name in user.other_user_cookies:
self.clear_login_cookie(name)
user.other_user_cookies = set([])
self.redirect(self.hub.server.base_url, permanent=False)
@@ -28,6 +31,7 @@ class LoginHandler(BaseHandler):
username=username,
login_error=login_error,
custom_login_form=self.authenticator.custom_html,
login_url=self.settings['login_url'],
)
def get(self):
@@ -57,9 +61,8 @@ class LoginHandler(BaseHandler):
for arg in self.request.arguments:
data[arg] = self.get_argument(arg)
username = data['username']
authorized = yield self.authenticate(data)
if authorized:
username = yield self.authenticate(data)
if username:
user = self.user_from_username(username)
already_running = False
if user.spawner:
@@ -85,5 +88,6 @@ class LoginHandler(BaseHandler):
# Only logout is a default handler.
default_handlers = [
(r"/login", LoginHandler),
(r"/logout", LogoutHandler),
]

View File

@@ -8,6 +8,7 @@ from tornado import web
from .. import orm
from ..utils import admin_only, url_path_join
from .base import BaseHandler
from .login import LoginHandler
class RootHandler(BaseHandler):
@@ -25,15 +26,15 @@ class RootHandler(BaseHandler):
if user:
if user.running:
url = user.server.base_url
self.log.debug("User is running: %s", url)
else:
url = url_path_join(self.hub.server.base_url, 'home')
self.redirect(url, permanent=False)
self.log.debug("User is not running: %s", url)
self.redirect(url)
return
html = self.render_template('login.html',
login_url=self.settings['login_url'],
custom_html=self.authenticator.custom_html,
)
self.finish(html)
url = url_path_join(self.hub.server.base_url, 'login')
self.redirect(url)
class HomeHandler(BaseHandler):
"""Render the user's home page."""

View File

@@ -285,6 +285,8 @@ class User(Base):
spawner = None
spawn_pending = False
stop_pending = False
other_user_cookies = set([])
def __repr__(self):
if self.server:
@@ -334,7 +336,7 @@ class User(Base):
return db.query(cls).filter(cls.name==name).first()
@gen.coroutine
def spawn(self, spawner_class, base_url='/', hub=None, config=None):
def spawn(self, spawner_class, base_url='/', hub=None, authenticator=None, config=None):
"""Start the user's spawner"""
db = inspect(self).session
if hub is None:
@@ -355,11 +357,15 @@ class User(Base):
user=self,
hub=hub,
db=db,
authenticator=authenticator,
)
# we are starting a new server, make sure it doesn't restore state
spawner.clear_state()
spawner.api_token = api_token
# trigger pre-spawn hook on authenticator
if (authenticator):
yield gen.maybe_future(authenticator.pre_spawn_start(self, spawner))
self.spawn_pending = True
# wait for spawner.start to return
try:
@@ -426,21 +432,27 @@ class User(Base):
and cleanup after it.
"""
self.spawn_pending = False
if self.spawner is None:
spawner = self.spawner
if spawner is None:
return
self.spawner.stop_polling()
spawner.stop_polling()
self.stop_pending = True
try:
status = yield self.spawner.poll()
status = yield spawner.poll()
if status is None:
yield self.spawner.stop()
self.spawner.clear_state()
self.state = self.spawner.get_state()
spawner.clear_state()
self.state = spawner.get_state()
self.server = None
inspect(self).session.commit()
finally:
self.stop_pending = False
# trigger post-spawner hook on authenticator
auth = spawner.authenticator
if auth:
yield gen.maybe_future(
auth.post_spawn_stop(self, spawner)
)
class APIToken(Base):
"""An API token"""

View File

@@ -24,18 +24,28 @@ from IPython.utils.traitlets import (
CUnicode,
)
from IPython.html.notebookapp import NotebookApp, aliases as notebook_aliases
from IPython.html.auth.login import LoginHandler
from IPython.html.auth.logout import LogoutHandler
try:
import notebook
# 4.x
except ImportError:
from IPython.html.notebookapp import NotebookApp, aliases as notebook_aliases
from IPython.html.auth.login import LoginHandler
from IPython.html.auth.logout import LogoutHandler
from IPython.html.utils import url_path_join
from IPython.html.utils import url_path_join
from distutils.version import LooseVersion as V
from distutils.version import LooseVersion as V
import IPython
if V(IPython.__version__) < V('3.0'):
raise ImportError("JupyterHub Requires IPython >= 3.0, found %s" % IPython.__version__)
else:
from notebook.notebookapp import NotebookApp, aliases as notebook_aliases
from notebook.auth.login import LoginHandler
from notebook.auth.logout import LogoutHandler
from notebook.utils import url_path_join
import IPython
if V(IPython.__version__) < V('3.0'):
raise ImportError("JupyterHub Requires IPython >= 3.0, found %s" % IPython.__version__)
# Define two methods to attach to AuthenticatedHandler,
# which authenticate via the central auth server.

View File

@@ -7,11 +7,10 @@ import errno
import os
import pipes
import pwd
import re
import signal
import sys
import grp
from subprocess import Popen, check_output, PIPE, CalledProcessError
from subprocess import Popen
from tempfile import TemporaryDirectory
from tornado import gen
@@ -19,14 +18,12 @@ from tornado.ioloop import IOLoop, PeriodicCallback
from traitlets.config import LoggingConfigurable
from traitlets import (
Any, Bool, Dict, Enum, Instance, Integer, Float, List, Unicode,
Any, Bool, Dict, Instance, Integer, Float, List, Unicode,
)
from .traitlets import Command
from .utils import random_port
NUM_PAT = re.compile(r'\d+')
class Spawner(LoggingConfigurable):
"""Base class for spawning single-user notebook servers.
@@ -42,6 +39,7 @@ class Spawner(LoggingConfigurable):
db = Any()
user = Any()
hub = Any()
authenticator = Any()
api_token = Unicode()
ip = Unicode('localhost', config=True,
help="The IP address (or hostname) the single-user server should listen on"
@@ -254,7 +252,7 @@ class Spawner(LoggingConfigurable):
if status is not None:
break
else:
yield gen.Task(loop.add_timeout, loop.time() + self.death_interval)
yield gen.sleep(self.death_interval)
def _try_setcwd(path):
"""Try to set CWD, walking up and ultimately falling back to a temp dir"""

View File

@@ -18,16 +18,18 @@ from ..app import JupyterHub
from ..auth import PAMAuthenticator
from .. import orm
from pamela import PAMError
def mock_authenticate(username, password, service='login'):
# mimic simplepam's failure to handle unicode
if isinstance(username, str):
return False
if isinstance(password, str):
return False
# just use equality for testing
if password == username:
return True
else:
raise PAMError("Fake")
def mock_open_session(username, service):
pass
class MockSpawner(LocalProcessSpawner):
@@ -51,12 +53,12 @@ class SlowSpawner(MockSpawner):
@gen.coroutine
def start(self):
yield gen.Task(IOLoop.current().add_timeout, timedelta(seconds=2))
yield gen.sleep(2)
yield super().start()
@gen.coroutine
def stop(self):
yield gen.Task(IOLoop.current().add_timeout, timedelta(seconds=2))
yield gen.sleep(2)
yield super().stop()
@@ -80,7 +82,9 @@ class MockPAMAuthenticator(PAMAuthenticator):
return not user.name.startswith('dne')
def authenticate(self, *args, **kwargs):
with mock.patch('simplepam.authenticate', mock_authenticate):
with mock.patch.multiple('pamela',
authenticate=mock_authenticate,
open_session=mock_open_session):
return super(MockPAMAuthenticator, self).authenticate(*args, **kwargs)
class MockHub(JupyterHub):

View File

@@ -363,11 +363,10 @@ def test_slow_spawn(app, io_loop):
assert app_user.spawn_pending
assert not app_user.stop_pending
dt = timedelta(seconds=0.1)
@gen.coroutine
def wait_spawn():
while app_user.spawn_pending:
yield gen.Task(io_loop.add_timeout, dt)
yield gen.sleep(0.1)
io_loop.run_sync(wait_spawn)
assert not app_user.spawn_pending
@@ -377,7 +376,7 @@ def test_slow_spawn(app, io_loop):
@gen.coroutine
def wait_stop():
while app_user.stop_pending:
yield gen.Task(io_loop.add_timeout, dt)
yield gen.sleep(0.1)
r = api_request(app, 'users', name, 'server', method='delete')
r.raise_for_status()
@@ -410,11 +409,10 @@ def test_never_spawn(app, io_loop):
assert app_user.spawner is not None
assert app_user.spawn_pending
dt = timedelta(seconds=0.1)
@gen.coroutine
def wait_pending():
while app_user.spawn_pending:
yield gen.Task(io_loop.add_timeout, dt)
yield gen.sleep(0.1)
io_loop.run_sync(wait_pending)
assert not app_user.spawn_pending

View File

@@ -3,7 +3,6 @@
import os
import re
import sys
from getpass import getuser
from subprocess import check_output
from tempfile import NamedTemporaryFile, TemporaryDirectory
@@ -16,7 +15,9 @@ def test_token_app():
cmd = [sys.executable, '-m', 'jupyterhub', 'token']
out = check_output(cmd + ['--help-all']).decode('utf8', 'replace')
with TemporaryDirectory() as td:
out = check_output(cmd + [getuser()], cwd=td).decode('utf8', 'replace').strip()
with open(os.path.join(td, 'jupyterhub_config.py'), 'w') as f:
f.write("c.Authenticator.admin_users={'user'}")
out = check_output(cmd + ['user'], cwd=td).decode('utf8', 'replace').strip()
assert re.match(r'^[a-z0-9]+$', out)
def test_generate_config():

View File

@@ -18,7 +18,7 @@ def test_root_no_auth(app, io_loop):
print(app.hub.server)
r = requests.get(app.proxy.public_server.host)
r.raise_for_status()
assert r.url == ujoin(app.proxy.public_server.host, app.hub.server.base_url)
assert r.url == ujoin(app.proxy.public_server.host, app.hub.server.base_url, 'login')
def test_root_auth(app):
cookies = app.login_user('river')

View File

@@ -41,7 +41,7 @@ def wait_for_server(ip, port, timeout=10):
app_log.error("Unexpected error waiting for %s:%i %s",
ip, port, e
)
yield gen.Task(loop.add_timeout, loop.time() + 0.1)
yield gen.sleep(0.1)
else:
return
raise TimeoutError("Server at {ip}:{port} didn't respond in {timeout} seconds".format(
@@ -67,14 +67,14 @@ def wait_for_http_server(url, timeout=10):
# we expect 599 for no connection,
# but 502 or other proxy error is conceivable
app_log.warn("Server at %s responded with error: %s", url, e.code)
yield gen.Task(loop.add_timeout, loop.time() + 0.25)
yield gen.sleep(0.1)
else:
app_log.debug("Server at %s responded with %s", url, e.code)
return
except (OSError, socket.error) as e:
if e.errno not in {errno.ECONNABORTED, errno.ECONNREFUSED, errno.ECONNRESET}:
app_log.warn("Failed to connect to %s (%s)", url, e)
yield gen.Task(loop.add_timeout, loop.time() + 0.25)
yield gen.sleep(0.1)
else:
return

View File

@@ -5,8 +5,9 @@
version_info = (
0,
2,
3,
0,
# 'dev',
)
__version__ = '.'.join(map(str, version_info))

View File

@@ -1,6 +1,6 @@
traitlets>=4
tornado>=4
tornado>=4.1
jinja2
simplepam
pamela
sqlalchemy
requests