diff --git a/IPython/html/auth/login.py b/IPython/html/auth/login.py index 1ad467337..81fff81ef 100644 --- a/IPython/html/auth/login.py +++ b/IPython/html/auth/login.py @@ -3,10 +3,11 @@ Authors: * Brian Granger +* Phil Elson """ #----------------------------------------------------------------------------- -# Copyright (C) 2011 The IPython Development Team +# Copyright (C) 2014 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. @@ -19,7 +20,9 @@ Authors: import uuid from tornado.escape import url_escape +from tornado import web +from IPython.config.configurable import Configurable from IPython.lib.security import passwd_check from ..base.handlers import IPythonHandler @@ -29,7 +32,10 @@ from ..base.handlers import IPythonHandler #----------------------------------------------------------------------------- class LoginHandler(IPythonHandler): - + """ The basic IPythonWebApplication login handler which 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)), @@ -43,20 +49,38 @@ class LoginHandler(IPythonHandler): self._render() def post(self): - pwd = self.get_argument('password', default=u'') - if self.login_available: - if passwd_check(self.password, pwd): + hashed_password = self.password_from_configuration(self.application) + typed_password = self.get_argument('password', default=u'') + if self.login_available(self.application): + if passwd_check(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 validate_notebook_app_security(cls, notebook_app, ssl_options=None): + if not notebook_app.ip: + warning = "WARNING: The notebook server is listening on all IP addresses" + if ssl_options is None: + notebook_app.log.critical(warning + " and not using encryption. This " + "is not recommended.") + if not self.password_from_configuration(notebook_app): + notebook_app.log.critical(warning + " and not using authentication. " + "This is highly insecure and not recommended.") + @staticmethod + def password_from_configuration(webapp): + """ Return the hashed password from the given NotebookWebApplication's configuration. + + If there is no configured password, None will be returned. + + """ + return webapp.settings['config']['NotebookApp'].get('password', None) -#----------------------------------------------------------------------------- -# URL to handler mappings -#----------------------------------------------------------------------------- - - -default_handlers = [(r"/login", LoginHandler)] + @classmethod + def login_available(cls, webapp): + """Whether this LoginHandler is needed - and therefore whether the login page should be displayed.""" + return bool(cls.password_from_configuration(webapp)) diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py index fa58c2b25..19e53984c 100644 --- a/IPython/html/base/handlers.py +++ b/IPython/html/base/handlers.py @@ -86,11 +86,6 @@ 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? @@ -99,6 +94,11 @@ class AuthenticatedHandler(web.RequestHandler): user = self.get_current_user() return (user and not user == 'anonymous') + @property + def _login_handler(self): + """Return the login handler for this application.""" + return self.settings['login_handler_class'] + @property def login_available(self): """May a user proceed to log in? @@ -107,7 +107,7 @@ class AuthenticatedHandler(web.RequestHandler): whether the user is already logged in or not. """ - return bool(self.settings.get('password', '')) + return bool(self._login_handler.login_available(self.application)) class IPythonHandler(AuthenticatedHandler): diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index d69b27411..e320819eb 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -182,8 +182,9 @@ 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, password=ipython_app.password, - + # managers kernel_manager=kernel_manager, contents_manager=contents_manager, @@ -193,7 +194,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,7 +212,7 @@ 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([(r"/login", settings['login_handler_class'])]) handlers.extend(load_handlers('auth.logout')) handlers.extend(load_handlers('files.handlers')) handlers.extend(load_handlers('notebook.handlers')) @@ -501,7 +502,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 +639,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 +649,10 @@ 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.') + 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 +703,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 +750,7 @@ 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) kls = import_item(self.config_manager_class) self.config_manager = kls(parent=self, log=self.log, @@ -788,17 +791,10 @@ class NotebookApp(BaseIPythonApplication): ssl_options['keyfile'] = self.keyfile else: ssl_options = None - self.web_app.password = self.password + self.login_handler_class.validate_notebook_app_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: