diff --git a/IPython/html/auth/login.py b/IPython/html/auth/login.py
index 1ad4673..a412082 100644
--- a/IPython/html/auth/login.py
+++ b/IPython/html/auth/login.py
@@ -1,20 +1,7 @@
-"""Tornado handlers logging into the notebook.
+"""Tornado handlers for logging into the notebook."""
-Authors:
-
-* Brian Granger
-"""
-
-#-----------------------------------------------------------------------------
-# Copyright (C) 2011 The IPython Development Team
-#
-# Distributed under the terms of the BSD License. The full license is in
-# the file COPYING, distributed as part of this software.
-#-----------------------------------------------------------------------------
-
-#-----------------------------------------------------------------------------
-# Imports
-#-----------------------------------------------------------------------------
+# Copyright (c) IPython Development Team.
+# Distributed under the terms of the Modified BSD License.
import uuid
@@ -24,12 +11,12 @@ from IPython.lib.security import passwd_check
from ..base.handlers import IPythonHandler
-#-----------------------------------------------------------------------------
-# Handler
-#-----------------------------------------------------------------------------
class LoginHandler(IPythonHandler):
-
+ """The basic tornado login handler
+
+ authenticates with a hashed password from the configuration.
+ """
def _render(self, message=None):
self.write(self.render_template('login.html',
next=url_escape(self.get_argument('next', default=self.base_url)),
@@ -41,22 +28,68 @@ class LoginHandler(IPythonHandler):
self.redirect(self.get_argument('next', default=self.base_url))
else:
self._render()
+
+ @property
+ def hashed_password(self):
+ return self.password_from_settings(self.settings)
def post(self):
- pwd = self.get_argument('password', default=u'')
- if self.login_available:
- if passwd_check(self.password, pwd):
+ typed_password = self.get_argument('password', default=u'')
+ if self.login_available(self.settings):
+ if passwd_check(self.hashed_password, typed_password):
self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
else:
self._render(message={'error': 'Invalid password'})
return
self.redirect(self.get_argument('next', default=self.base_url))
+
+ @classmethod
+ def get_user(cls, handler):
+ """Called by handlers.get_current_user for identifying the current user.
+
+ See tornado.web.RequestHandler.get_current_user for details.
+ """
+ # Can't call this get_current_user because it will collide when
+ # called on LoginHandler itself.
+
+ user_id = handler.get_secure_cookie(handler.cookie_name)
+ # For now the user_id should not return empty, but it could, eventually.
+ if user_id == '':
+ user_id = 'anonymous'
+ if user_id is None:
+ # prevent extra Invalid cookie sig warnings:
+ handler.clear_login_cookie()
+ if not handler.login_available:
+ user_id = 'anonymous'
+ return user_id
+
+
+ @classmethod
+ def validate_security(cls, app, ssl_options=None):
+ """Check the notebook application's security.
+
+ Show messages, or abort if necessary, based on the security configuration.
+ """
+ if not app.ip:
+ warning = "WARNING: The notebook server is listening on all IP addresses"
+ if ssl_options is None:
+ app.log.critical(warning + " and not using encryption. This "
+ "is not recommended.")
+ if not app.password:
+ app.log.critical(warning + " and not using authentication. "
+ "This is highly insecure and not recommended.")
+
+ @classmethod
+ def password_from_settings(cls, settings):
+ """Return the hashed password from the tornado settings.
+
+ If there is no configured password, an empty string will be returned.
+ """
+ return settings.get('password', u'')
+
+ @classmethod
+ def login_available(cls, settings):
+ """Whether this LoginHandler is needed - and therefore whether the login page should be displayed."""
+ return bool(cls.password_from_settings(settings))
-
-#-----------------------------------------------------------------------------
-# URL to handler mappings
-#-----------------------------------------------------------------------------
-
-
-default_handlers = [(r"/login", LoginHandler)]
diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py
index fa58c2b..70a1b13 100644
--- a/IPython/html/base/handlers.py
+++ b/IPython/html/base/handlers.py
@@ -68,16 +68,9 @@ class AuthenticatedHandler(web.RequestHandler):
self.clear_cookie(self.cookie_name)
def get_current_user(self):
- user_id = self.get_secure_cookie(self.cookie_name)
- # For now the user_id should not return empty, but it could eventually
- if user_id == '':
- user_id = 'anonymous'
- if user_id is None:
- # prevent extra Invalid cookie sig warnings:
- self.clear_login_cookie()
- if not self.login_available:
- user_id = 'anonymous'
- return user_id
+ if self.login_handler is None:
+ return 'anonymous'
+ return self.login_handler.get_user(self)
@property
def cookie_name(self):
@@ -87,19 +80,17 @@ class AuthenticatedHandler(web.RequestHandler):
return self.settings.get('cookie_name', default_cookie_name)
@property
- def password(self):
- """our password"""
- return self.settings.get('password', '')
-
- @property
def logged_in(self):
- """Is a user currently logged in?
-
- """
+ """Is a user currently logged in?"""
user = self.get_current_user()
return (user and not user == 'anonymous')
@property
+ def login_handler(self):
+ """Return the login handler for this application, if any."""
+ return self.settings.get('login_handler_class', None)
+
+ @property
def login_available(self):
"""May a user proceed to log in?
@@ -107,7 +98,9 @@ class AuthenticatedHandler(web.RequestHandler):
whether the user is already logged in or not.
"""
- return bool(self.settings.get('password', ''))
+ if self.login_handler is None:
+ return False
+ return bool(self.login_handler.login_available(self.settings))
class IPythonHandler(AuthenticatedHandler):
diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py
index d69b274..6b798c8 100644
--- a/IPython/html/notebookapp.py
+++ b/IPython/html/notebookapp.py
@@ -182,8 +182,10 @@ class NotebookWebApplication(web.Application):
# authentication
cookie_secret=ipython_app.cookie_secret,
login_url=url_path_join(base_url,'/login'),
+ login_handler_class=ipython_app.login_handler_class,
+ logout_handler_class=ipython_app.logout_handler_class,
password=ipython_app.password,
-
+
# managers
kernel_manager=kernel_manager,
contents_manager=contents_manager,
@@ -193,7 +195,7 @@ class NotebookWebApplication(web.Application):
config_manager=config_manager,
# IPython stuff
- nbextensions_path = ipython_app.nbextensions_path,
+ nbextensions_path=ipython_app.nbextensions_path,
websocket_url=ipython_app.websocket_url,
mathjax_url=ipython_app.mathjax_url,
config=ipython_app.config,
@@ -211,8 +213,8 @@ class NotebookWebApplication(web.Application):
# Order matters. The first handler to match the URL will handle the request.
handlers = []
handlers.extend(load_handlers('tree.handlers'))
- handlers.extend(load_handlers('auth.login'))
- handlers.extend(load_handlers('auth.logout'))
+ handlers.extend([(r"/login", settings['login_handler_class'])])
+ handlers.extend([(r"/logout", settings['logout_handler_class'])])
handlers.extend(load_handlers('files.handlers'))
handlers.extend(load_handlers('notebook.handlers'))
handlers.extend(load_handlers('nbconvert.handlers'))
@@ -501,7 +503,6 @@ class NotebookApp(BaseIPythonApplication):
jinja_environment_options = Dict(config=True,
help="Supply extra arguments that will be passed to Jinja environment.")
-
enable_mathjax = Bool(True, config=True,
help="""Whether to enable MathJax for typesetting math/TeX
@@ -639,7 +640,6 @@ class NotebookApp(BaseIPythonApplication):
def _kernel_spec_manager_default(self):
return KernelSpecManager(ipython_dir=self.ipython_dir)
-
kernel_spec_manager_class = DottedObjectName('IPython.kernel.kernelspec.KernelSpecManager',
config=True,
help="""
@@ -650,6 +650,14 @@ class NotebookApp(BaseIPythonApplication):
without warning between this version of IPython and the next stable one.
""")
+ login_handler = DottedObjectName('IPython.html.auth.login.LoginHandler',
+ config=True,
+ help='The login handler class to use.')
+
+ logout_handler = DottedObjectName('IPython.html.auth.logout.LogoutHandler',
+ config=True,
+ help='The logout handler class to use.')
+
trust_xheaders = Bool(False, config=True,
help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
"sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
@@ -700,7 +708,6 @@ class NotebookApp(BaseIPythonApplication):
# setting App.notebook_dir implies setting notebook and kernel dirs as well
self.config.FileContentsManager.root_dir = new
self.config.MappingKernelManager.root_dir = new
-
def parse_command_line(self, argv=None):
super(NotebookApp, self).parse_command_line(argv)
@@ -748,6 +755,8 @@ class NotebookApp(BaseIPythonApplication):
kls = import_item(self.cluster_manager_class)
self.cluster_manager = kls(parent=self, log=self.log)
self.cluster_manager.update_profiles()
+ self.login_handler_class = import_item(self.login_handler)
+ self.logout_handler_class = import_item(self.logout_handler)
kls = import_item(self.config_manager_class)
self.config_manager = kls(parent=self, log=self.log,
@@ -788,17 +797,10 @@ class NotebookApp(BaseIPythonApplication):
ssl_options['keyfile'] = self.keyfile
else:
ssl_options = None
- self.web_app.password = self.password
+ self.login_handler_class.validate_security(self, ssl_options=ssl_options)
self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
xheaders=self.trust_xheaders)
- if not self.ip:
- warning = "WARNING: The notebook server is listening on all IP addresses"
- if ssl_options is None:
- self.log.critical(warning + " and not using encryption. This "
- "is not recommended.")
- if not self.password:
- self.log.critical(warning + " and not using authentication. "
- "This is highly insecure and not recommended.")
+
success = None
for port in random_ports(self.port, self.port_retries+1):
try: