mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-13 13:03:01 +00:00
4
.coveragerc
Normal file
4
.coveragerc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[run]
|
||||||
|
omit =
|
||||||
|
jupyterhub/tests/*
|
||||||
|
jupyterhub/singleuser.py
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -14,3 +14,6 @@ share/jupyter/hub/static/css/style.min.css
|
|||||||
share/jupyter/hub/static/css/style.min.css.map
|
share/jupyter/hub/static/css/style.min.css.map
|
||||||
*.egg-info
|
*.egg-info
|
||||||
MANIFEST
|
MANIFEST
|
||||||
|
.coverage
|
||||||
|
htmlcov
|
||||||
|
|
||||||
|
@@ -12,4 +12,6 @@ install:
|
|||||||
- pip install -f travis-wheels/wheelhouse -r dev-requirements.txt .
|
- pip install -f travis-wheels/wheelhouse -r dev-requirements.txt .
|
||||||
- pip install -f travis-wheels/wheelhouse ipython[notebook]
|
- pip install -f travis-wheels/wheelhouse ipython[notebook]
|
||||||
script:
|
script:
|
||||||
- py.test jupyterhub
|
- py.test --cov jupyterhub jupyterhub/tests
|
||||||
|
after_success:
|
||||||
|
- coveralls
|
||||||
|
@@ -1,2 +1,4 @@
|
|||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
coveralls
|
||||||
|
pytest-cov
|
||||||
pytest
|
pytest
|
||||||
|
@@ -1003,6 +1003,9 @@ class JupyterHub(Application):
|
|||||||
|
|
||||||
# register cleanup on both TERM and INT
|
# register cleanup on both TERM and INT
|
||||||
atexit.register(self.atexit)
|
atexit.register(self.atexit)
|
||||||
|
self.init_signal()
|
||||||
|
|
||||||
|
def init_signal(self):
|
||||||
signal.signal(signal.SIGTERM, self.sigterm)
|
signal.signal(signal.SIGTERM, self.sigterm)
|
||||||
|
|
||||||
def sigterm(self, signum, frame):
|
def sigterm(self, signum, frame):
|
||||||
@@ -1027,7 +1030,10 @@ class JupyterHub(Application):
|
|||||||
if not self.io_loop:
|
if not self.io_loop:
|
||||||
return
|
return
|
||||||
if self.http_server:
|
if self.http_server:
|
||||||
self.io_loop.add_callback(self.http_server.stop)
|
if self.io_loop._running:
|
||||||
|
self.io_loop.add_callback(self.http_server.stop)
|
||||||
|
else:
|
||||||
|
self.http_server.stop()
|
||||||
self.io_loop.add_callback(self.io_loop.stop)
|
self.io_loop.add_callback(self.io_loop.stop)
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
|
@@ -126,11 +126,11 @@ class LocalAuthenticator(Authenticator):
|
|||||||
def check_group_whitelist(self, username):
|
def check_group_whitelist(self, username):
|
||||||
if not self.group_whitelist:
|
if not self.group_whitelist:
|
||||||
return False
|
return False
|
||||||
for group in self.group_whitelist:
|
for grnam in self.group_whitelist:
|
||||||
try:
|
try:
|
||||||
group = getgrnam(self.group_whitelist)
|
group = getgrnam(grnam)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.log.error('No such group: [%s]' % self.group_whitelist)
|
self.log.error('No such group: [%s]' % grnam)
|
||||||
continue
|
continue
|
||||||
if username in group.gr_mem:
|
if username in group.gr_mem:
|
||||||
return True
|
return True
|
||||||
|
@@ -7,6 +7,8 @@ import threading
|
|||||||
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
from tornado import gen
|
from tornado import gen
|
||||||
from tornado.concurrent import Future
|
from tornado.concurrent import Future
|
||||||
from tornado.ioloop import IOLoop
|
from tornado.ioloop import IOLoop
|
||||||
@@ -95,12 +97,18 @@ class MockHub(JupyterHub):
|
|||||||
def _admin_users_default(self):
|
def _admin_users_default(self):
|
||||||
return {'admin'}
|
return {'admin'}
|
||||||
|
|
||||||
|
def init_signal(self):
|
||||||
|
pass
|
||||||
|
|
||||||
def start(self, argv=None):
|
def start(self, argv=None):
|
||||||
self.db_file = NamedTemporaryFile()
|
self.db_file = NamedTemporaryFile()
|
||||||
self.db_url = 'sqlite:///' + self.db_file.name
|
self.db_url = 'sqlite:///' + self.db_file.name
|
||||||
|
|
||||||
evt = threading.Event()
|
evt = threading.Event()
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def _start_co():
|
def _start_co():
|
||||||
|
assert self.io_loop._running
|
||||||
# put initialize in start for SQLAlchemy threading reasons
|
# put initialize in start for SQLAlchemy threading reasons
|
||||||
yield super(MockHub, self).initialize(argv=argv)
|
yield super(MockHub, self).initialize(argv=argv)
|
||||||
# add an initial user
|
# add an initial user
|
||||||
@@ -108,16 +116,19 @@ class MockHub(JupyterHub):
|
|||||||
self.db.add(user)
|
self.db.add(user)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
yield super(MockHub, self).start()
|
yield super(MockHub, self).start()
|
||||||
|
yield self.hub.server.wait_up(http=True)
|
||||||
self.io_loop.add_callback(evt.set)
|
self.io_loop.add_callback(evt.set)
|
||||||
|
|
||||||
def _start():
|
def _start():
|
||||||
self.io_loop = IOLoop.current()
|
self.io_loop = IOLoop()
|
||||||
|
self.io_loop.make_current()
|
||||||
self.io_loop.add_callback(_start_co)
|
self.io_loop.add_callback(_start_co)
|
||||||
self.io_loop.start()
|
self.io_loop.start()
|
||||||
|
|
||||||
self._thread = threading.Thread(target=_start)
|
self._thread = threading.Thread(target=_start)
|
||||||
self._thread.start()
|
self._thread.start()
|
||||||
evt.wait(timeout=5)
|
ready = evt.wait(timeout=10)
|
||||||
|
assert ready
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
super().stop()
|
super().stop()
|
||||||
@@ -126,3 +137,15 @@ class MockHub(JupyterHub):
|
|||||||
# ignore the call that will fire in atexit
|
# ignore the call that will fire in atexit
|
||||||
self.cleanup = lambda : None
|
self.cleanup = lambda : None
|
||||||
self.db_file.close()
|
self.db_file.close()
|
||||||
|
|
||||||
|
def login_user(self, name):
|
||||||
|
r = requests.post(self.proxy.public_server.url + 'hub/login',
|
||||||
|
data={
|
||||||
|
'username': name,
|
||||||
|
'password': name,
|
||||||
|
},
|
||||||
|
allow_redirects=False,
|
||||||
|
)
|
||||||
|
assert r.cookies
|
||||||
|
return r.cookies
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
"""Tests for the REST API"""
|
"""Tests for the REST API"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@@ -284,3 +285,18 @@ def test_get_proxy(app, io_loop):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
reply = r.json()
|
reply = r.json()
|
||||||
assert list(reply.keys()) == ['/']
|
assert list(reply.keys()) == ['/']
|
||||||
|
|
||||||
|
|
||||||
|
def test_shutdown(app):
|
||||||
|
r = api_request(app, 'shutdown', method='post', data=json.dumps({
|
||||||
|
'servers': True,
|
||||||
|
'proxy': True,
|
||||||
|
}))
|
||||||
|
r.raise_for_status()
|
||||||
|
reply = r.json()
|
||||||
|
for i in range(100):
|
||||||
|
if app.io_loop._running:
|
||||||
|
time.sleep(0.1)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
assert not app.io_loop._running
|
||||||
|
@@ -3,8 +3,13 @@
|
|||||||
# Copyright (c) Jupyter Development Team.
|
# Copyright (c) Jupyter Development Team.
|
||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
|
from subprocess import CalledProcessError
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
from .mocking import MockPAMAuthenticator
|
from .mocking import MockPAMAuthenticator
|
||||||
|
|
||||||
|
from jupyterhub import auth, orm
|
||||||
|
|
||||||
def test_pam_auth(io_loop):
|
def test_pam_auth(io_loop):
|
||||||
authenticator = MockPAMAuthenticator()
|
authenticator = MockPAMAuthenticator()
|
||||||
@@ -39,3 +44,106 @@ def test_pam_auth_whitelist(io_loop):
|
|||||||
'password': 'mal',
|
'password': 'mal',
|
||||||
}))
|
}))
|
||||||
assert authorized is None
|
assert authorized is None
|
||||||
|
|
||||||
|
|
||||||
|
class MockGroup:
|
||||||
|
def __init__(self, *names):
|
||||||
|
self.gr_mem = names
|
||||||
|
|
||||||
|
|
||||||
|
def test_pam_auth_group_whitelist(io_loop):
|
||||||
|
g = MockGroup('kaylee')
|
||||||
|
def getgrnam(name):
|
||||||
|
return g
|
||||||
|
|
||||||
|
authenticator = MockPAMAuthenticator(group_whitelist={'group'})
|
||||||
|
|
||||||
|
with mock.patch.object(auth, 'getgrnam', getgrnam):
|
||||||
|
authorized = io_loop.run_sync(lambda : authenticator.authenticate(None, {
|
||||||
|
'username': 'kaylee',
|
||||||
|
'password': 'kaylee',
|
||||||
|
}))
|
||||||
|
assert authorized == 'kaylee'
|
||||||
|
|
||||||
|
with mock.patch.object(auth, 'getgrnam', getgrnam):
|
||||||
|
authorized = io_loop.run_sync(lambda : authenticator.authenticate(None, {
|
||||||
|
'username': 'mal',
|
||||||
|
'password': 'mal',
|
||||||
|
}))
|
||||||
|
assert authorized is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_pam_auth_no_such_group(io_loop):
|
||||||
|
authenticator = MockPAMAuthenticator(group_whitelist={'nosuchcrazygroup'})
|
||||||
|
authorized = io_loop.run_sync(lambda : authenticator.authenticate(None, {
|
||||||
|
'username': 'kaylee',
|
||||||
|
'password': 'kaylee',
|
||||||
|
}))
|
||||||
|
assert authorized is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_wont_add_system_user(io_loop):
|
||||||
|
user = orm.User(name='lioness4321')
|
||||||
|
authenticator = auth.PAMAuthenticator(whitelist={'mal'})
|
||||||
|
authenticator.create_system_users = False
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
io_loop.run_sync(lambda : authenticator.add_user(user))
|
||||||
|
|
||||||
|
|
||||||
|
def test_cant_add_system_user(io_loop):
|
||||||
|
user = orm.User(name='lioness4321')
|
||||||
|
authenticator = auth.PAMAuthenticator(whitelist={'mal'})
|
||||||
|
authenticator.create_system_users = True
|
||||||
|
|
||||||
|
def check_output(cmd, *a, **kw):
|
||||||
|
raise CalledProcessError(1, cmd)
|
||||||
|
|
||||||
|
with mock.patch.object(auth, 'check_output', check_output):
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
io_loop.run_sync(lambda : authenticator.add_user(user))
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_system_user(io_loop):
|
||||||
|
user = orm.User(name='lioness4321')
|
||||||
|
authenticator = auth.PAMAuthenticator(whitelist={'mal'})
|
||||||
|
authenticator.create_system_users = True
|
||||||
|
|
||||||
|
def check_output(*a, **kw):
|
||||||
|
return
|
||||||
|
|
||||||
|
record = {}
|
||||||
|
def check_call(cmd, *a, **kw):
|
||||||
|
record['cmd'] = cmd
|
||||||
|
|
||||||
|
with mock.patch.object(auth, 'check_output', check_output), \
|
||||||
|
mock.patch.object(auth, 'check_call', check_call):
|
||||||
|
io_loop.run_sync(lambda : authenticator.add_user(user))
|
||||||
|
|
||||||
|
assert user.name in record['cmd']
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_user(io_loop):
|
||||||
|
user = orm.User(name='zoe')
|
||||||
|
a = MockPAMAuthenticator(whitelist={'mal'})
|
||||||
|
|
||||||
|
assert 'zoe' not in a.whitelist
|
||||||
|
a.add_user(user)
|
||||||
|
assert 'zoe' in a.whitelist
|
||||||
|
a.delete_user(user)
|
||||||
|
assert 'zoe' not in a.whitelist
|
||||||
|
|
||||||
|
|
||||||
|
def test_urls():
|
||||||
|
a = auth.PAMAuthenticator()
|
||||||
|
logout = a.logout_url('/base/url/')
|
||||||
|
login = a.login_url('/base/url')
|
||||||
|
assert logout == '/base/url/logout'
|
||||||
|
assert login == '/base/url/login'
|
||||||
|
|
||||||
|
|
||||||
|
def test_handlers(app):
|
||||||
|
a = auth.PAMAuthenticator()
|
||||||
|
handlers = a.get_handlers(app)
|
||||||
|
assert handlers[0][0] == '/login'
|
||||||
|
|
||||||
|
|
||||||
|
58
jupyterhub/tests/test_pages.py
Normal file
58
jupyterhub/tests/test_pages.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
"""Tests for HTML pages"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from ..utils import url_path_join as ujoin
|
||||||
|
from .. import orm
|
||||||
|
|
||||||
|
|
||||||
|
def get_page(path, app, **kw):
|
||||||
|
base_url = ujoin(app.proxy.public_server.host, app.hub.server.base_url)
|
||||||
|
print(base_url)
|
||||||
|
return requests.get(ujoin(base_url, path), **kw)
|
||||||
|
|
||||||
|
def test_root_no_auth(app, io_loop):
|
||||||
|
print(app.hub.server.is_up())
|
||||||
|
routes = io_loop.run_sync(app.proxy.get_routes)
|
||||||
|
print(routes)
|
||||||
|
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)
|
||||||
|
|
||||||
|
def test_root_auth(app):
|
||||||
|
cookies = app.login_user('river')
|
||||||
|
r = requests.get(app.proxy.public_server.host, cookies=cookies)
|
||||||
|
r.raise_for_status()
|
||||||
|
assert r.url == ujoin(app.proxy.public_server.host, '/user/river')
|
||||||
|
|
||||||
|
def test_home_no_auth(app):
|
||||||
|
r = get_page('home', app, allow_redirects=False)
|
||||||
|
r.raise_for_status()
|
||||||
|
assert r.status_code == 302
|
||||||
|
assert '/hub/login' in r.headers['Location']
|
||||||
|
|
||||||
|
def test_home_auth(app):
|
||||||
|
cookies = app.login_user('river')
|
||||||
|
r = get_page('home', app, cookies=cookies)
|
||||||
|
r.raise_for_status()
|
||||||
|
assert r.url.endswith('home')
|
||||||
|
|
||||||
|
def test_admin_no_auth(app):
|
||||||
|
r = get_page('admin', app)
|
||||||
|
assert r.status_code == 403
|
||||||
|
|
||||||
|
def test_admin_not_admin(app):
|
||||||
|
cookies = app.login_user('wash')
|
||||||
|
r = get_page('admin', app, cookies=cookies)
|
||||||
|
assert r.status_code == 403
|
||||||
|
|
||||||
|
def test_admin(app):
|
||||||
|
cookies = app.login_user('river')
|
||||||
|
u = orm.User.find(app.db, 'river')
|
||||||
|
u.admin = True
|
||||||
|
app.db.commit()
|
||||||
|
r = get_page('admin', app, cookies=cookies)
|
||||||
|
r.raise_for_status()
|
||||||
|
assert r.url.endswith('/admin')
|
||||||
|
|
Reference in New Issue
Block a user