mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-14 13:33:00 +00:00
add configurable authentication
and coroutine auth and spawning
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
|
mock
|
||||||
pytest
|
pytest
|
||||||
|
@@ -9,7 +9,7 @@ import tornado.httpserver
|
|||||||
import tornado.ioloop
|
import tornado.ioloop
|
||||||
import tornado.options
|
import tornado.options
|
||||||
from tornado.log import LogFormatter
|
from tornado.log import LogFormatter
|
||||||
from tornado import web
|
from tornado import gen, web
|
||||||
|
|
||||||
from IPython.utils.traitlets import (
|
from IPython.utils.traitlets import (
|
||||||
Unicode, Integer, Dict, TraitError, List, Instance, Bool, Bytes, Any,
|
Unicode, Integer, Dict, TraitError, List, Instance, Bool, Bytes, Any,
|
||||||
@@ -87,8 +87,27 @@ class JupyterHubApp(Application):
|
|||||||
def _cookie_secret_default(self):
|
def _cookie_secret_default(self):
|
||||||
return b'secret!'
|
return b'secret!'
|
||||||
|
|
||||||
|
authenticator = DottedObjectName("jupyterhub.auth.PAMAuthenticator", config=True,
|
||||||
|
help="""Class for authenticating users.
|
||||||
|
|
||||||
|
This should be a class with the following form:
|
||||||
|
|
||||||
|
- constructor takes one kwarg: `config`, the IPython config object.
|
||||||
|
|
||||||
|
- is a tornado.gen.coroutine
|
||||||
|
- returns username on success, None on failure
|
||||||
|
- takes two arguments: (handler, data),
|
||||||
|
where `handler` is the calling web.RequestHandler,
|
||||||
|
and `data` is the POST form data from the login page.
|
||||||
|
"""
|
||||||
|
)
|
||||||
# class for spawning single-user servers
|
# class for spawning single-user servers
|
||||||
spawner_class = DottedObjectName("jupyterhub.spawner.LocalProcessSpawner")
|
spawner_class = DottedObjectName("jupyterhub.spawner.LocalProcessSpawner", config=True,
|
||||||
|
help="""The class to use for spawning single-user servers.
|
||||||
|
|
||||||
|
Should be a subclass of Spawner.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
db_url = Unicode('sqlite:///:memory:', config=True)
|
db_url = Unicode('sqlite:///:memory:', config=True)
|
||||||
debug_db = Bool(False)
|
debug_db = Bool(False)
|
||||||
@@ -201,6 +220,7 @@ class JupyterHubApp(Application):
|
|||||||
log=self.log,
|
log=self.log,
|
||||||
db=self.db,
|
db=self.db,
|
||||||
hub=self.hub,
|
hub=self.hub,
|
||||||
|
authenticator=import_item(self.authenticator)(config=self.config),
|
||||||
spawner_class=import_item(self.spawner_class),
|
spawner_class=import_item(self.spawner_class),
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
cookie_secret=self.cookie_secret,
|
cookie_secret=self.cookie_secret,
|
||||||
@@ -225,15 +245,25 @@ class JupyterHubApp(Application):
|
|||||||
self.init_tornado_settings()
|
self.init_tornado_settings()
|
||||||
self.init_tornado_application()
|
self.init_tornado_application()
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
self.log.info("Cleaning up proxy...")
|
self.log.info("Cleaning up proxy...")
|
||||||
self.proxy.terminate()
|
self.proxy.terminate()
|
||||||
self.log.info("Cleaning up single-user servers...")
|
self.log.info("Cleaning up single-user servers...")
|
||||||
Spawner = import_item(self.spawner_class)
|
|
||||||
|
# request (async) process termination
|
||||||
|
futures = []
|
||||||
for user in self.db.query(db.User):
|
for user in self.db.query(db.User):
|
||||||
if user.spawner is not None:
|
if user.spawner is not None:
|
||||||
user.spawner.stop()
|
futures.append(user.spawner.stop())
|
||||||
|
|
||||||
|
# wait for the requests to stop finish:
|
||||||
|
for f in futures:
|
||||||
|
yield f
|
||||||
|
|
||||||
|
# finally stop the loop once we are all cleaned up
|
||||||
self.log.info("...done")
|
self.log.info("...done")
|
||||||
|
tornado.ioloop.IOLoop.instance().stop()
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
"""Start the whole thing"""
|
"""Start the whole thing"""
|
||||||
@@ -242,12 +272,17 @@ class JupyterHubApp(Application):
|
|||||||
# start the webserver
|
# start the webserver
|
||||||
http_server = tornado.httpserver.HTTPServer(self.tornado_application)
|
http_server = tornado.httpserver.HTTPServer(self.tornado_application)
|
||||||
http_server.listen(self.hub_port)
|
http_server.listen(self.hub_port)
|
||||||
|
|
||||||
|
loop = tornado.ioloop.IOLoop.instance()
|
||||||
try:
|
try:
|
||||||
tornado.ioloop.IOLoop.instance().start()
|
loop.start()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nInterrupted")
|
print("\nInterrupted")
|
||||||
finally:
|
finally:
|
||||||
self.cleanup()
|
# have to start the loop one more time,
|
||||||
|
# to allow for async cleanup code.
|
||||||
|
loop.add_callback(self.cleanup)
|
||||||
|
tornado.ioloop.IOLoop.instance().start()
|
||||||
|
|
||||||
main = JupyterHubApp.launch_instance
|
main = JupyterHubApp.launch_instance
|
||||||
|
|
||||||
|
45
jupyterhub/auth.py
Normal file
45
jupyterhub/auth.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"""Simple PAM authenticator"""
|
||||||
|
|
||||||
|
from tornado import gen
|
||||||
|
import simplepam
|
||||||
|
|
||||||
|
from IPython.config import LoggingConfigurable
|
||||||
|
from IPython.utils.traitlets import Unicode
|
||||||
|
|
||||||
|
class Authenticator(LoggingConfigurable):
|
||||||
|
"""A class for authentication.
|
||||||
|
|
||||||
|
The API is one method, `authenticate`, a tornado gen.coroutine.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
|
def authenticate(self, handler, data):
|
||||||
|
"""Authenticate a user with login form data.
|
||||||
|
|
||||||
|
This must be a tornado gen.coroutine.
|
||||||
|
It must return the username on successful authentication,
|
||||||
|
and return None on failed authentication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class PAMAuthenticator(Authenticator):
|
||||||
|
encoding = Unicode('utf8', config=True,
|
||||||
|
help="""The encoding to use for PAM """
|
||||||
|
)
|
||||||
|
service = Unicode('login', config=True,
|
||||||
|
help="""The PAM service to use for authentication."""
|
||||||
|
)
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
|
def authenticate(self, handler, data):
|
||||||
|
"""Authenticate with PAM, and return the username if login is successful.
|
||||||
|
|
||||||
|
Return None otherwise.
|
||||||
|
"""
|
||||||
|
username = data['username']
|
||||||
|
# simplepam wants bytes, not unicode
|
||||||
|
# see
|
||||||
|
busername = username.encode(self.encoding)
|
||||||
|
bpassword = data['password'].encode(self.encoding)
|
||||||
|
if simplepam.authenticate(busername, bpassword, service=self.service):
|
||||||
|
raise gen.Return(username)
|
||||||
|
|
@@ -12,11 +12,11 @@ from tornado.log import app_log
|
|||||||
from tornado.escape import url_escape
|
from tornado.escape import url_escape
|
||||||
from tornado.httputil import url_concat
|
from tornado.httputil import url_concat
|
||||||
from tornado.web import RequestHandler
|
from tornado.web import RequestHandler
|
||||||
from tornado import web
|
from tornado import gen, web
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
from .spawner import LocalProcessSpawner
|
from .spawner import LocalProcessSpawner
|
||||||
from .utils import random_port, wait_for_server, url_path_join
|
from .utils import wait_for_server, url_path_join
|
||||||
|
|
||||||
|
|
||||||
class BaseHandler(RequestHandler):
|
class BaseHandler(RequestHandler):
|
||||||
@@ -59,7 +59,7 @@ class BaseHandler(RequestHandler):
|
|||||||
def clear_login_cookie(self):
|
def clear_login_cookie(self):
|
||||||
username = self.get_current_user()
|
username = self.get_current_user()
|
||||||
if username is not None:
|
if username is not None:
|
||||||
user = self.db.query(User).filter(name=username).first()
|
user = self.db.query(db.User).filter(name=username).first()
|
||||||
if user is not None:
|
if user is not None:
|
||||||
self.clear_cookie(user.server.cookie_name, path=user.server.base_url)
|
self.clear_cookie(user.server.cookie_name, path=user.server.base_url)
|
||||||
self.clear_cookie(self.cookie_name, path=self.hub.base_url)
|
self.clear_cookie(self.cookie_name, path=self.hub.base_url)
|
||||||
@@ -103,10 +103,10 @@ class LogoutHandler(BaseHandler):
|
|||||||
class LoginHandler(BaseHandler):
|
class LoginHandler(BaseHandler):
|
||||||
"""Render the login page."""
|
"""Render the login page."""
|
||||||
|
|
||||||
def _render(self, message=None, user=None):
|
def _render(self, message=None, username=None):
|
||||||
self.render('login.html',
|
self.render('login.html',
|
||||||
next=url_escape(self.get_argument('next', default='')),
|
next=url_escape(self.get_argument('next', default='')),
|
||||||
user=user,
|
username=username,
|
||||||
message=message,
|
message=message,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -114,9 +114,10 @@ class LoginHandler(BaseHandler):
|
|||||||
if False and self.get_current_user():
|
if False and self.get_current_user():
|
||||||
self.redirect(self.get_argument('next', default='/'))
|
self.redirect(self.get_argument('next', default='/'))
|
||||||
else:
|
else:
|
||||||
user = self.get_argument('user', default='')
|
username = self.get_argument('username', default='')
|
||||||
self._render(user=user)
|
self._render(username=username)
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
def notify_proxy(self, user):
|
def notify_proxy(self, user):
|
||||||
proxy = self.db.query(db.Proxy).first()
|
proxy = self.db.query(db.Proxy).first()
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
@@ -130,9 +131,10 @@ class LoginHandler(BaseHandler):
|
|||||||
)),
|
)),
|
||||||
headers={'Authorization': "token %s" % proxy.auth_token},
|
headers={'Authorization': "token %s" % proxy.auth_token},
|
||||||
)
|
)
|
||||||
wait_for_server(user.server.ip, user.server.port)
|
yield wait_for_server(user.server.ip, user.server.port)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
def spawn_single_user(self, name):
|
def spawn_single_user(self, name):
|
||||||
user = db.User(name=name,
|
user = db.User(name=name,
|
||||||
server=db.Server(
|
server=db.Server(
|
||||||
@@ -154,51 +156,69 @@ class LoginHandler(BaseHandler):
|
|||||||
hub=self.hub,
|
hub=self.hub,
|
||||||
api_token=api_token.token,
|
api_token=api_token.token,
|
||||||
)
|
)
|
||||||
spawner.start()
|
yield spawner.start()
|
||||||
|
|
||||||
# store state
|
# store state
|
||||||
user.state = spawner.get_state()
|
user.state = spawner.get_state()
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
self.notify_proxy(user)
|
self.notify_proxy(user)
|
||||||
return user
|
raise gen.Return(user)
|
||||||
|
|
||||||
def post(self):
|
def set_login_cookies(self, user):
|
||||||
name = self.get_argument('user', default='')
|
"""Set login cookies for the Hub and single-user server."""
|
||||||
pwd = self.get_argument('password', default=u'')
|
# create and set a new cookie token for the single-user server
|
||||||
next_url = self.get_argument('next', default='') or '/user/%s/' % name
|
cookie_token = user.new_cookie_token()
|
||||||
if name and pwd == 'password':
|
self.db.add(cookie_token)
|
||||||
user = self.db.query(db.User).filter(db.User.name == name).first()
|
self.db.commit()
|
||||||
if user is None:
|
|
||||||
user = self.spawn_single_user(name)
|
self.set_cookie(
|
||||||
|
user.server.cookie_name,
|
||||||
# create and set a new cookie token for the single-user server
|
cookie_token.token,
|
||||||
cookie_token = user.new_cookie_token()
|
path=user.server.base_url,
|
||||||
self.db.add(cookie_token)
|
)
|
||||||
self.db.commit()
|
|
||||||
|
# create and set a new cookie token for the hub
|
||||||
self.set_cookie(
|
cookie_token = user.new_cookie_token()
|
||||||
user.server.cookie_name,
|
self.db.add(cookie_token)
|
||||||
cookie_token.token,
|
self.db.commit()
|
||||||
path=user.server.base_url,
|
self.set_cookie(
|
||||||
)
|
self.hub.server.cookie_name,
|
||||||
|
cookie_token.token,
|
||||||
# create and set a new cookie token for the hub
|
path=self.hub.server.base_url)
|
||||||
cookie_token = user.new_cookie_token()
|
|
||||||
self.db.add(cookie_token)
|
@gen.coroutine
|
||||||
self.db.commit()
|
def authenticate(self, data):
|
||||||
self.set_cookie(
|
auth = self.settings.get('authenticator', None)
|
||||||
self.hub.server.cookie_name,
|
if auth is not None:
|
||||||
cookie_token.token,
|
result = yield auth.authenticate(self, data)
|
||||||
path=self.hub.server.base_url)
|
raise gen.Return(result)
|
||||||
else:
|
else:
|
||||||
|
self.log.error("No authentication function, login is impossible!")
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
|
def post(self):
|
||||||
|
# parse the arguments dict
|
||||||
|
data = {}
|
||||||
|
for arg in self.request.arguments:
|
||||||
|
data[arg] = self.get_argument(arg)
|
||||||
|
|
||||||
|
username = data['username']
|
||||||
|
authorized = yield self.authenticate(data)
|
||||||
|
if authorized:
|
||||||
|
user = self.db.query(db.User).filter(db.User.name == username).first()
|
||||||
|
if user is None:
|
||||||
|
user = yield self.spawn_single_user(username)
|
||||||
|
self.set_login_cookies(user)
|
||||||
|
next_url = self.get_argument('next', default='') or '/user/%s/' % username
|
||||||
|
self.redirect(next_url)
|
||||||
|
else:
|
||||||
|
self.log.debug("Failed login for %s", username)
|
||||||
self._render(
|
self._render(
|
||||||
message={'error': 'Invalid username or password'},
|
message={'error': 'Invalid username or password'},
|
||||||
user=user,
|
username=username,
|
||||||
)
|
)
|
||||||
return
|
|
||||||
|
|
||||||
self.redirect(next_url)
|
|
||||||
|
|
||||||
#------------------------------------------------------------------------------
|
#------------------------------------------------------------------------------
|
||||||
# API Handlers
|
# API Handlers
|
||||||
@@ -216,7 +236,6 @@ def token_authorized(method):
|
|||||||
raise web.HTTPError(403)
|
raise web.HTTPError(403)
|
||||||
token = match.group(1)
|
token = match.group(1)
|
||||||
db_token = self.db.query(db.APIToken).filter(db.APIToken.token == token).first()
|
db_token = self.db.query(db.APIToken).filter(db.APIToken.token == token).first()
|
||||||
self.log.info("Token: %s: %s", token, db_token)
|
|
||||||
if db_token is None:
|
if db_token is None:
|
||||||
raise web.HTTPError(403)
|
raise web.HTTPError(403)
|
||||||
return method(self, *args, **kwargs)
|
return method(self, *args, **kwargs)
|
||||||
|
@@ -11,6 +11,7 @@ import time
|
|||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
|
|
||||||
from tornado import gen
|
from tornado import gen
|
||||||
|
from tornado.ioloop import IOLoop
|
||||||
|
|
||||||
from IPython.config import LoggingConfigurable
|
from IPython.config import LoggingConfigurable
|
||||||
from IPython.utils.traitlets import (
|
from IPython.utils.traitlets import (
|
||||||
@@ -18,7 +19,7 @@ from IPython.utils.traitlets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
from .utils import random_port, wait_for_server
|
from .utils import random_port
|
||||||
|
|
||||||
|
|
||||||
class Spawner(LoggingConfigurable):
|
class Spawner(LoggingConfigurable):
|
||||||
@@ -103,15 +104,18 @@ class Spawner(LoggingConfigurable):
|
|||||||
'--hub-prefix=%s' % self.hub.server.base_url,
|
'--hub-prefix=%s' % self.hub.server.base_url,
|
||||||
'--hub-api-url=%s' % self.hub.api_url,
|
'--hub-api-url=%s' % self.hub.api_url,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
def start(self):
|
def start(self):
|
||||||
raise NotImplementedError("Override in subclass")
|
raise NotImplementedError("Override in subclass. Must be a Tornado gen.coroutine.")
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
def stop(self):
|
def stop(self):
|
||||||
raise NotImplementedError("Override in subclass")
|
raise NotImplementedError("Override in subclass. Must be a Tornado gen.coroutine.")
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
def poll(self):
|
def poll(self):
|
||||||
raise NotImplementedError("Override in subclass")
|
raise NotImplementedError("Override in subclass. Must be a Tornado gen.coroutine.")
|
||||||
|
|
||||||
|
|
||||||
class LocalProcessSpawner(Spawner):
|
class LocalProcessSpawner(Spawner):
|
||||||
@@ -125,6 +129,7 @@ class LocalProcessSpawner(Spawner):
|
|||||||
def get_state(self):
|
def get_state(self):
|
||||||
return dict(pid=self.pid)
|
return dict(pid=self.pid)
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
def start(self):
|
def start(self):
|
||||||
self.user.server.port = random_port()
|
self.user.server.port = random_port()
|
||||||
cmd = self.cmd + self.get_args()
|
cmd = self.cmd + self.get_args()
|
||||||
@@ -136,10 +141,11 @@ class LocalProcessSpawner(Spawner):
|
|||||||
)
|
)
|
||||||
self.pid = self.proc.pid
|
self.pid = self.proc.pid
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
def poll(self):
|
def poll(self):
|
||||||
# if we started the process, poll with Popen
|
# if we started the process, poll with Popen
|
||||||
if self.proc is not None:
|
if self.proc is not None:
|
||||||
return self.proc.poll()
|
raise gen.Return(self.proc.poll())
|
||||||
|
|
||||||
# if we resumed from stored state,
|
# if we resumed from stored state,
|
||||||
# we don't have the Popen handle anymore
|
# we don't have the Popen handle anymore
|
||||||
@@ -150,38 +156,62 @@ class LocalProcessSpawner(Spawner):
|
|||||||
except OSError as e:
|
except OSError as e:
|
||||||
if e.errno == errno.ESRCH:
|
if e.errno == errno.ESRCH:
|
||||||
# no such process, return exitcode == 0, since we don't know the exit status
|
# no such process, return exitcode == 0, since we don't know the exit status
|
||||||
return 0
|
raise gen.Return(0)
|
||||||
else:
|
else:
|
||||||
# None indicates the process is running
|
# None indicates the process is running
|
||||||
return None
|
raise gen.Return(None)
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
def _wait_for_death(self, timeout=10):
|
def _wait_for_death(self, timeout=10):
|
||||||
"""wait for the process to die, up to timeout seconds"""
|
"""wait for the process to die, up to timeout seconds"""
|
||||||
for i in range(int(timeout * 10)):
|
for i in range(int(timeout * 10)):
|
||||||
if self.poll() is not None:
|
status = yield self.poll()
|
||||||
|
if status is not None:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
time.sleep(0.1)
|
yield gen.Task(IOLoop.instance().add_timeout, time.time() + 0.1)
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
def stop(self, now=False):
|
def stop(self, now=False):
|
||||||
"""stop the subprocess"""
|
"""stop the subprocess
|
||||||
|
|
||||||
|
if `now`, skip waiting for clean shutdown
|
||||||
|
"""
|
||||||
if not now:
|
if not now:
|
||||||
# double-sigint to request clean shutdown
|
# SIGINT to request clean shutdown
|
||||||
os.kill(self.pid, signal.SIGINT)
|
self.log.debug("Interrupting %i", self.pid)
|
||||||
os.kill(self.pid, signal.SIGINT)
|
try:
|
||||||
self._wait_for_death(10)
|
os.kill(self.pid, signal.SIGINT)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno == errno.ESRCH:
|
||||||
|
return
|
||||||
|
|
||||||
|
yield self._wait_for_death(10)
|
||||||
|
|
||||||
# clean shutdown failed, use TERM
|
# clean shutdown failed, use TERM
|
||||||
if self.poll() is None:
|
status = yield self.poll()
|
||||||
os.kill(self.pid, signal.SIGTERM)
|
if status is None:
|
||||||
self._wait_for_death(5)
|
self.log.debug("Terminating %i", self.pid)
|
||||||
|
try:
|
||||||
|
os.kill(self.pid, signal.SIGTERM)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno == errno.ESRCH:
|
||||||
|
return
|
||||||
|
yield self._wait_for_death(5)
|
||||||
|
|
||||||
# TERM failed, use KILL
|
# TERM failed, use KILL
|
||||||
if self.poll() is None:
|
status = yield self.poll()
|
||||||
os.kill(self.pid, signal.SIGKILL)
|
if status is None:
|
||||||
self._wait_for_death(5)
|
self.log.debug("Killing %i", self.pid)
|
||||||
|
try:
|
||||||
|
os.kill(self.pid, signal.SIGKILL)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno == errno.ESRCH:
|
||||||
|
return
|
||||||
|
yield self._wait_for_death(5)
|
||||||
|
|
||||||
if self.poll() is None:
|
status = yield self.poll()
|
||||||
|
if status is None:
|
||||||
# it all failed, zombie process
|
# it all failed, zombie process
|
||||||
self.log.warn("Process %i never died", self.pid)
|
self.log.warn("Process %i never died", self.pid)
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<div id="message">{{message}}</div>
|
<div id="message">{{message}}</div>
|
||||||
{% end if %}
|
{% end if %}
|
||||||
<form action="?next={{next}}" method="post">
|
<form action="?next={{next}}" method="post">
|
||||||
<input type="text" name="user" id="user_input" value="{{user}}">
|
<input type="text" name="username" id="user_input" value="{{username}}">
|
||||||
<input type="password" name="password" id="password_input">
|
<input type="password" name="password" id="password_input">
|
||||||
<button type="submit" id="login_submit">Log in</button>
|
<button type="submit" id="login_submit">Log in</button>
|
||||||
</form>
|
</form>
|
||||||
|
40
jupyterhub/tests/test_auth.py
Normal file
40
jupyterhub/tests/test_auth.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"""Tests for PAM authentication"""
|
||||||
|
|
||||||
|
import mock
|
||||||
|
import simplepam
|
||||||
|
from tornado.testing import AsyncTestCase, gen_test
|
||||||
|
from IPython.utils.py3compat import unicode_type
|
||||||
|
|
||||||
|
from ..auth import PAMAuthenticator
|
||||||
|
|
||||||
|
|
||||||
|
def fake_auth(username, password, service='login'):
|
||||||
|
# mimic simplepam's failure to handle unicode
|
||||||
|
if isinstance(username, unicode_type):
|
||||||
|
return False
|
||||||
|
if isinstance(password, unicode_type):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# just use equality
|
||||||
|
if password == username:
|
||||||
|
return True
|
||||||
|
|
||||||
|
class TestPAM(AsyncTestCase):
|
||||||
|
|
||||||
|
@gen_test
|
||||||
|
def test_pam_auth(self):
|
||||||
|
authenticator = PAMAuthenticator()
|
||||||
|
with mock.patch('simplepam.authenticate', fake_auth):
|
||||||
|
authorized = yield authenticator.authenticate(None, {
|
||||||
|
u'username': u'match',
|
||||||
|
u'password': u'match',
|
||||||
|
})
|
||||||
|
self.assertEqual(authorized, u'match')
|
||||||
|
|
||||||
|
with mock.patch('simplepam.authenticate', fake_auth):
|
||||||
|
authorized = yield authenticator.authenticate(None, {
|
||||||
|
u'username': u'match',
|
||||||
|
u'password': u'nomatch',
|
||||||
|
})
|
||||||
|
self.assertEqual(authorized, None)
|
||||||
|
|
@@ -1,3 +1,4 @@
|
|||||||
|
ipython[notebook]
|
||||||
|
simplepam
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
requests
|
requests
|
||||||
ipython[notebook]
|
|
||||||
|
Reference in New Issue
Block a user