Show More
@@ -1,20 +1,7 b'' | |||
|
1 | """Tornado handlers logging into the notebook. | |
|
1 | """Tornado handlers for logging into the notebook.""" | |
|
2 | 2 | |
|
3 | Authors: | |
|
4 | ||
|
5 | * Brian Granger | |
|
6 | """ | |
|
7 | ||
|
8 | #----------------------------------------------------------------------------- | |
|
9 | # Copyright (C) 2011 The IPython Development Team | |
|
10 | # | |
|
11 | # Distributed under the terms of the BSD License. The full license is in | |
|
12 | # the file COPYING, distributed as part of this software. | |
|
13 | #----------------------------------------------------------------------------- | |
|
14 | ||
|
15 | #----------------------------------------------------------------------------- | |
|
16 | # Imports | |
|
17 | #----------------------------------------------------------------------------- | |
|
3 | # Copyright (c) IPython Development Team. | |
|
4 | # Distributed under the terms of the Modified BSD License. | |
|
18 | 5 | |
|
19 | 6 | import uuid |
|
20 | 7 | |
@@ -24,12 +11,12 b' from IPython.lib.security import passwd_check' | |||
|
24 | 11 | |
|
25 | 12 | from ..base.handlers import IPythonHandler |
|
26 | 13 | |
|
27 | #----------------------------------------------------------------------------- | |
|
28 | # Handler | |
|
29 | #----------------------------------------------------------------------------- | |
|
30 | 14 | |
|
31 | 15 | class LoginHandler(IPythonHandler): |
|
32 | ||
|
16 | """The basic tornado login handler | |
|
17 | ||
|
18 | authenticates with a hashed password from the configuration. | |
|
19 | """ | |
|
33 | 20 | def _render(self, message=None): |
|
34 | 21 | self.write(self.render_template('login.html', |
|
35 | 22 | next=url_escape(self.get_argument('next', default=self.base_url)), |
@@ -41,22 +28,68 b' class LoginHandler(IPythonHandler):' | |||
|
41 | 28 | self.redirect(self.get_argument('next', default=self.base_url)) |
|
42 | 29 | else: |
|
43 | 30 | self._render() |
|
31 | ||
|
32 | @property | |
|
33 | def hashed_password(self): | |
|
34 | return self.password_from_settings(self.settings) | |
|
44 | 35 | |
|
45 | 36 | def post(self): |
|
46 | pwd = self.get_argument('password', default=u'') | |
|
47 | if self.login_available: | |
|
48 | if passwd_check(self.password, pwd): | |
|
37 | typed_password = self.get_argument('password', default=u'') | |
|
38 | if self.login_available(self.settings): | |
|
39 | if passwd_check(self.hashed_password, typed_password): | |
|
49 | 40 | self.set_secure_cookie(self.cookie_name, str(uuid.uuid4())) |
|
50 | 41 | else: |
|
51 | 42 | self._render(message={'error': 'Invalid password'}) |
|
52 | 43 | return |
|
53 | 44 | |
|
54 | 45 | self.redirect(self.get_argument('next', default=self.base_url)) |
|
46 | ||
|
47 | @classmethod | |
|
48 | def get_user(cls, handler): | |
|
49 | """Called by handlers.get_current_user for identifying the current user. | |
|
50 | ||
|
51 | See tornado.web.RequestHandler.get_current_user for details. | |
|
52 | """ | |
|
53 | # Can't call this get_current_user because it will collide when | |
|
54 | # called on LoginHandler itself. | |
|
55 | ||
|
56 | user_id = handler.get_secure_cookie(handler.cookie_name) | |
|
57 | # For now the user_id should not return empty, but it could, eventually. | |
|
58 | if user_id == '': | |
|
59 | user_id = 'anonymous' | |
|
60 | if user_id is None: | |
|
61 | # prevent extra Invalid cookie sig warnings: | |
|
62 | handler.clear_login_cookie() | |
|
63 | if not handler.login_available: | |
|
64 | user_id = 'anonymous' | |
|
65 | return user_id | |
|
66 | ||
|
67 | ||
|
68 | @classmethod | |
|
69 | def validate_security(cls, app, ssl_options=None): | |
|
70 | """Check the notebook application's security. | |
|
71 | ||
|
72 | Show messages, or abort if necessary, based on the security configuration. | |
|
73 | """ | |
|
74 | if not app.ip: | |
|
75 | warning = "WARNING: The notebook server is listening on all IP addresses" | |
|
76 | if ssl_options is None: | |
|
77 | app.log.critical(warning + " and not using encryption. This " | |
|
78 | "is not recommended.") | |
|
79 | if not app.password: | |
|
80 | app.log.critical(warning + " and not using authentication. " | |
|
81 | "This is highly insecure and not recommended.") | |
|
82 | ||
|
83 | @classmethod | |
|
84 | def password_from_settings(cls, settings): | |
|
85 | """Return the hashed password from the tornado settings. | |
|
86 | ||
|
87 | If there is no configured password, an empty string will be returned. | |
|
88 | """ | |
|
89 | return settings.get('password', u'') | |
|
90 | ||
|
91 | @classmethod | |
|
92 | def login_available(cls, settings): | |
|
93 | """Whether this LoginHandler is needed - and therefore whether the login page should be displayed.""" | |
|
94 | return bool(cls.password_from_settings(settings)) | |
|
55 | 95 | |
|
56 | ||
|
57 | #----------------------------------------------------------------------------- | |
|
58 | # URL to handler mappings | |
|
59 | #----------------------------------------------------------------------------- | |
|
60 | ||
|
61 | ||
|
62 | default_handlers = [(r"/login", LoginHandler)] |
@@ -68,16 +68,9 b' class AuthenticatedHandler(web.RequestHandler):' | |||
|
68 | 68 | self.clear_cookie(self.cookie_name) |
|
69 | 69 | |
|
70 | 70 | def get_current_user(self): |
|
71 | user_id = self.get_secure_cookie(self.cookie_name) | |
|
72 | # For now the user_id should not return empty, but it could eventually | |
|
73 | if user_id == '': | |
|
74 | user_id = 'anonymous' | |
|
75 | if user_id is None: | |
|
76 | # prevent extra Invalid cookie sig warnings: | |
|
77 | self.clear_login_cookie() | |
|
78 | if not self.login_available: | |
|
79 | user_id = 'anonymous' | |
|
80 | return user_id | |
|
71 | if self.login_handler is None: | |
|
72 | return 'anonymous' | |
|
73 | return self.login_handler.get_user(self) | |
|
81 | 74 | |
|
82 | 75 | @property |
|
83 | 76 | def cookie_name(self): |
@@ -87,19 +80,17 b' class AuthenticatedHandler(web.RequestHandler):' | |||
|
87 | 80 | return self.settings.get('cookie_name', default_cookie_name) |
|
88 | 81 | |
|
89 | 82 | @property |
|
90 | def password(self): | |
|
91 | """our password""" | |
|
92 | return self.settings.get('password', '') | |
|
93 | ||
|
94 | @property | |
|
95 | 83 | def logged_in(self): |
|
96 | """Is a user currently logged in? | |
|
97 | ||
|
98 | """ | |
|
84 | """Is a user currently logged in?""" | |
|
99 | 85 | user = self.get_current_user() |
|
100 | 86 | return (user and not user == 'anonymous') |
|
101 | 87 | |
|
102 | 88 | @property |
|
89 | def login_handler(self): | |
|
90 | """Return the login handler for this application, if any.""" | |
|
91 | return self.settings.get('login_handler_class', None) | |
|
92 | ||
|
93 | @property | |
|
103 | 94 | def login_available(self): |
|
104 | 95 | """May a user proceed to log in? |
|
105 | 96 | |
@@ -107,7 +98,9 b' class AuthenticatedHandler(web.RequestHandler):' | |||
|
107 | 98 | whether the user is already logged in or not. |
|
108 | 99 | |
|
109 | 100 | """ |
|
110 | return bool(self.settings.get('password', '')) | |
|
101 | if self.login_handler is None: | |
|
102 | return False | |
|
103 | return bool(self.login_handler.login_available(self.settings)) | |
|
111 | 104 | |
|
112 | 105 | |
|
113 | 106 | class IPythonHandler(AuthenticatedHandler): |
@@ -182,8 +182,10 b' class NotebookWebApplication(web.Application):' | |||
|
182 | 182 | # authentication |
|
183 | 183 | cookie_secret=ipython_app.cookie_secret, |
|
184 | 184 | login_url=url_path_join(base_url,'/login'), |
|
185 | login_handler_class=ipython_app.login_handler_class, | |
|
186 | logout_handler_class=ipython_app.logout_handler_class, | |
|
185 | 187 | password=ipython_app.password, |
|
186 | ||
|
188 | ||
|
187 | 189 | # managers |
|
188 | 190 | kernel_manager=kernel_manager, |
|
189 | 191 | contents_manager=contents_manager, |
@@ -193,7 +195,7 b' class NotebookWebApplication(web.Application):' | |||
|
193 | 195 | config_manager=config_manager, |
|
194 | 196 | |
|
195 | 197 | # IPython stuff |
|
196 |
nbextensions_path |
|
|
198 | nbextensions_path=ipython_app.nbextensions_path, | |
|
197 | 199 | websocket_url=ipython_app.websocket_url, |
|
198 | 200 | mathjax_url=ipython_app.mathjax_url, |
|
199 | 201 | config=ipython_app.config, |
@@ -211,8 +213,8 b' class NotebookWebApplication(web.Application):' | |||
|
211 | 213 | # Order matters. The first handler to match the URL will handle the request. |
|
212 | 214 | handlers = [] |
|
213 | 215 | handlers.extend(load_handlers('tree.handlers')) |
|
214 |
handlers.extend( |
|
|
215 |
handlers.extend( |
|
|
216 | handlers.extend([(r"/login", settings['login_handler_class'])]) | |
|
217 | handlers.extend([(r"/logout", settings['logout_handler_class'])]) | |
|
216 | 218 | handlers.extend(load_handlers('files.handlers')) |
|
217 | 219 | handlers.extend(load_handlers('notebook.handlers')) |
|
218 | 220 | handlers.extend(load_handlers('nbconvert.handlers')) |
@@ -501,7 +503,6 b' class NotebookApp(BaseIPythonApplication):' | |||
|
501 | 503 | |
|
502 | 504 | jinja_environment_options = Dict(config=True, |
|
503 | 505 | help="Supply extra arguments that will be passed to Jinja environment.") |
|
504 | ||
|
505 | 506 | |
|
506 | 507 | enable_mathjax = Bool(True, config=True, |
|
507 | 508 | help="""Whether to enable MathJax for typesetting math/TeX |
@@ -639,7 +640,6 b' class NotebookApp(BaseIPythonApplication):' | |||
|
639 | 640 | def _kernel_spec_manager_default(self): |
|
640 | 641 | return KernelSpecManager(ipython_dir=self.ipython_dir) |
|
641 | 642 | |
|
642 | ||
|
643 | 643 | kernel_spec_manager_class = DottedObjectName('IPython.kernel.kernelspec.KernelSpecManager', |
|
644 | 644 | config=True, |
|
645 | 645 | help=""" |
@@ -650,6 +650,14 b' class NotebookApp(BaseIPythonApplication):' | |||
|
650 | 650 | without warning between this version of IPython and the next stable one. |
|
651 | 651 | """) |
|
652 | 652 | |
|
653 | login_handler = DottedObjectName('IPython.html.auth.login.LoginHandler', | |
|
654 | config=True, | |
|
655 | help='The login handler class to use.') | |
|
656 | ||
|
657 | logout_handler = DottedObjectName('IPython.html.auth.logout.LogoutHandler', | |
|
658 | config=True, | |
|
659 | help='The logout handler class to use.') | |
|
660 | ||
|
653 | 661 | trust_xheaders = Bool(False, config=True, |
|
654 | 662 | help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers" |
|
655 | 663 | "sent by the upstream reverse proxy. Necessary if the proxy handles SSL") |
@@ -700,7 +708,6 b' class NotebookApp(BaseIPythonApplication):' | |||
|
700 | 708 | # setting App.notebook_dir implies setting notebook and kernel dirs as well |
|
701 | 709 | self.config.FileContentsManager.root_dir = new |
|
702 | 710 | self.config.MappingKernelManager.root_dir = new |
|
703 | ||
|
704 | 711 | |
|
705 | 712 | def parse_command_line(self, argv=None): |
|
706 | 713 | super(NotebookApp, self).parse_command_line(argv) |
@@ -748,6 +755,8 b' class NotebookApp(BaseIPythonApplication):' | |||
|
748 | 755 | kls = import_item(self.cluster_manager_class) |
|
749 | 756 | self.cluster_manager = kls(parent=self, log=self.log) |
|
750 | 757 | self.cluster_manager.update_profiles() |
|
758 | self.login_handler_class = import_item(self.login_handler) | |
|
759 | self.logout_handler_class = import_item(self.logout_handler) | |
|
751 | 760 | |
|
752 | 761 | kls = import_item(self.config_manager_class) |
|
753 | 762 | self.config_manager = kls(parent=self, log=self.log, |
@@ -788,17 +797,10 b' class NotebookApp(BaseIPythonApplication):' | |||
|
788 | 797 | ssl_options['keyfile'] = self.keyfile |
|
789 | 798 | else: |
|
790 | 799 | ssl_options = None |
|
791 | self.web_app.password = self.password | |
|
800 | self.login_handler_class.validate_security(self, ssl_options=ssl_options) | |
|
792 | 801 | self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options, |
|
793 | 802 | xheaders=self.trust_xheaders) |
|
794 | if not self.ip: | |
|
795 | warning = "WARNING: The notebook server is listening on all IP addresses" | |
|
796 | if ssl_options is None: | |
|
797 | self.log.critical(warning + " and not using encryption. This " | |
|
798 | "is not recommended.") | |
|
799 | if not self.password: | |
|
800 | self.log.critical(warning + " and not using authentication. " | |
|
801 | "This is highly insecure and not recommended.") | |
|
803 | ||
|
802 | 804 | success = None |
|
803 | 805 | for port in random_ports(self.port, self.port_retries+1): |
|
804 | 806 | try: |
General Comments 0
You need to be logged in to leave comments.
Login now