From f4584e17f8672038e2bf0858e134599d2b8bf4a6 2014-12-09 08:12:50 From: Matthias Bussonnier Date: 2014-12-09 08:12:50 Subject: [PATCH] Merge pull request #6977 from minrk/finish-5384 Add authentication configuration --- 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: