more hooks for authenticators

Allow authenticators to:

- register custom handlers
- change login and logout URLs
- replace the entire login form

This appears to be enough to get oauth working.
This commit is contained in:
MinRK
2014-09-22 22:08:01 -07:00
parent ee5ad66ba7
commit 0577e10276
6 changed files with 41 additions and 7 deletions

View File

@@ -210,7 +210,8 @@ class JupyterHubApp(Application):
def _cookie_secret_default(self): def _cookie_secret_default(self):
return os.environ.get('JPY_COOKIE_SECRET', random_hex(64)) return os.environ.get('JPY_COOKIE_SECRET', random_hex(64))
authenticator_class = Type("jupyterhub.auth.PAMAuthenticator", config=True, authenticator_class = Type(PAMAuthenticator, Authenticator,
config=True,
help="""Class for authenticating users. help="""Class for authenticating users.
This should be a class with the following form: This should be a class with the following form:
@@ -224,12 +225,14 @@ class JupyterHubApp(Application):
and `data` is the POST form data from the login page. and `data` is the POST form data from the login page.
""" """
) )
authenticator = Instance(Authenticator) authenticator = Instance(Authenticator)
def _authenticator_default(self): def _authenticator_default(self):
return self.authenticator_class(config=self.config) return self.authenticator_class(config=self.config)
# class for spawning single-user servers # class for spawning single-user servers
spawner_class = Type("jupyterhub.spawner.LocalProcessSpawner", config=True, spawner_class = Type(LocalProcessSpawner, Spawner,
config=True,
help="""The class to use for spawning single-user servers. help="""The class to use for spawning single-user servers.
Should be a subclass of Spawner. Should be a subclass of Spawner.
@@ -323,6 +326,8 @@ class JupyterHubApp(Application):
h = [] h = []
h.extend(handlers.default_handlers) h.extend(handlers.default_handlers)
h.extend(apihandlers.default_handlers) h.extend(apihandlers.default_handlers)
# load handlers from the authenticator
h.extend(self.authenticator.get_handlers(self))
self.handlers = self.add_url_prefix(self.hub_prefix, h) self.handlers = self.add_url_prefix(self.hub_prefix, h)
@@ -534,13 +539,16 @@ class JupyterHubApp(Application):
def init_tornado_settings(self): def init_tornado_settings(self):
"""Set up the tornado settings dict.""" """Set up the tornado settings dict."""
base_url = self.base_url base_url = self.hub.server.base_url
template_path = os.path.join(self.data_files_path, 'templates'), template_path = os.path.join(self.data_files_path, 'templates'),
jinja_env = Environment( jinja_env = Environment(
loader=FileSystemLoader(template_path), loader=FileSystemLoader(template_path),
**self.jinja_environment_options **self.jinja_environment_options
) )
login_url = self.authenticator.login_url(base_url)
logout_url = self.authenticator.logout_url(base_url)
settings = dict( settings = dict(
config=self.config, config=self.config,
log=self.log, log=self.log,
@@ -550,9 +558,10 @@ class JupyterHubApp(Application):
admin_users=self.admin_users, admin_users=self.admin_users,
authenticator=self.authenticator, authenticator=self.authenticator,
spawner_class=self.spawner_class, spawner_class=self.spawner_class,
base_url=base_url, base_url=self.base_url,
cookie_secret=self.hub.server.cookie_secret, cookie_secret=self.hub.server.cookie_secret,
login_url=url_path_join(self.hub.server.base_url, 'login'), login_url=login_url,
logout_url=logout_url,
static_path=os.path.join(self.data_files_path, 'static'), static_path=os.path.join(self.data_files_path, 'static'),
static_url_prefix=url_path_join(self.hub.server.base_url, 'static/'), static_url_prefix=url_path_join(self.hub.server.base_url, 'static/'),
template_path=template_path, template_path=template_path,

View File

@@ -9,6 +9,8 @@ import simplepam
from IPython.config import LoggingConfigurable from IPython.config import LoggingConfigurable
from IPython.utils.traitlets import Unicode, Set from IPython.utils.traitlets import Unicode, Set
from .utils import url_path_join
class Authenticator(LoggingConfigurable): class Authenticator(LoggingConfigurable):
"""A class for authentication. """A class for authentication.
@@ -22,6 +24,7 @@ class Authenticator(LoggingConfigurable):
If empty, allow any user to attempt login. If empty, allow any user to attempt login.
""" """
) )
custom_html = Unicode('')
@gen.coroutine @gen.coroutine
def authenticate(self, handler, data): def authenticate(self, handler, data):
@@ -31,6 +34,22 @@ class Authenticator(LoggingConfigurable):
It must return the username on successful authentication, It must return the username on successful authentication,
and return None on failed authentication. and return None on failed authentication.
""" """
def login_url(self, base_url):
"""Override to register a custom login handler"""
return url_path_join(base_url, 'login')
def logout_url(self, base_url):
"""Override to register a custom logout handler"""
return url_path_join(base_url, 'logout')
def get_handlers(self, app):
"""Return any custom handlers the authenticator needs to register
(e.g. for OAuth)
"""
return []
class PAMAuthenticator(Authenticator): class PAMAuthenticator(Authenticator):
encoding = Unicode('utf8', config=True, encoding = Unicode('utf8', config=True,

View File

@@ -206,6 +206,7 @@ class BaseHandler(RequestHandler):
base_url=self.hub.server.base_url, base_url=self.hub.server.base_url,
user=user, user=user,
login_url=self.settings['login_url'], login_url=self.settings['login_url'],
logout_url=self.settings['logout_url'],
static_url=self.static_url, static_url=self.static_url,
) )

View File

@@ -25,6 +25,7 @@ class LoginHandler(BaseHandler):
next=url_escape(self.get_argument('next', default='')), next=url_escape(self.get_argument('next', default='')),
username=username, username=username,
message=message, message=message,
custom_html=self.authenticator.custom_html,
) )
def get(self): def get(self):

View File

@@ -6,6 +6,9 @@
{% block main %} {% block main %}
<div id="login-main" class="container"> <div id="login-main" class="container">
{% if custom_html %}
{{custom_html}}
{% else %}
<form action="{{login_url}}?next={{next}}" method="post" role="form"> <form action="{{login_url}}?next={{next}}" method="post" role="form">
<div class="input-group"> <div class="input-group">
<span class="input-group-addon">Username:</span> <span class="input-group-addon">Username:</span>
@@ -24,6 +27,7 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endif %}
<div/> <div/>
{% endblock %} {% endblock %}

View File

@@ -83,9 +83,9 @@
<span id="login_widget"> <span id="login_widget">
{% if user %} {% if user %}
<a id="logout" class="btn navbar-btn btn-default pull-right" href="{{base_url}}logout">Logout</a> <a id="logout" class="btn navbar-btn btn-default pull-right" href="{{logout_url}}">Logout</a>
{% else %} {% else %}
<a id="login" class="btn navbar-btn btn-default pull-right" href="{{base_url}}login">Login</a> <a id="login" class="btn navbar-btn btn-default pull-right" href="{{login_url}}">Login</a>
{% endif %} {% endif %}
</span> </span>