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: |
|
3 | # Copyright (c) IPython Development Team. | |
4 |
|
4 | # Distributed under the terms of the Modified BSD License. | ||
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 | #----------------------------------------------------------------------------- |
|
|||
18 |
|
5 | |||
19 | import uuid |
|
6 | import uuid | |
20 |
|
7 | |||
@@ -24,12 +11,12 b' from IPython.lib.security import passwd_check' | |||||
24 |
|
11 | |||
25 | from ..base.handlers import IPythonHandler |
|
12 | from ..base.handlers import IPythonHandler | |
26 |
|
13 | |||
27 | #----------------------------------------------------------------------------- |
|
|||
28 | # Handler |
|
|||
29 | #----------------------------------------------------------------------------- |
|
|||
30 |
|
14 | |||
31 | class LoginHandler(IPythonHandler): |
|
15 | class LoginHandler(IPythonHandler): | |
32 |
|
16 | """The basic tornado login handler | ||
|
17 | ||||
|
18 | authenticates with a hashed password from the configuration. | |||
|
19 | """ | |||
33 | def _render(self, message=None): |
|
20 | def _render(self, message=None): | |
34 | self.write(self.render_template('login.html', |
|
21 | self.write(self.render_template('login.html', | |
35 | next=url_escape(self.get_argument('next', default=self.base_url)), |
|
22 | next=url_escape(self.get_argument('next', default=self.base_url)), | |
@@ -41,22 +28,68 b' class LoginHandler(IPythonHandler):' | |||||
41 | self.redirect(self.get_argument('next', default=self.base_url)) |
|
28 | self.redirect(self.get_argument('next', default=self.base_url)) | |
42 | else: |
|
29 | else: | |
43 | self._render() |
|
30 | self._render() | |
|
31 | ||||
|
32 | @property | |||
|
33 | def hashed_password(self): | |||
|
34 | return self.password_from_settings(self.settings) | |||
44 |
|
35 | |||
45 | def post(self): |
|
36 | def post(self): | |
46 | pwd = self.get_argument('password', default=u'') |
|
37 | typed_password = self.get_argument('password', default=u'') | |
47 | if self.login_available: |
|
38 | if self.login_available(self.settings): | |
48 | if passwd_check(self.password, pwd): |
|
39 | if passwd_check(self.hashed_password, typed_password): | |
49 | self.set_secure_cookie(self.cookie_name, str(uuid.uuid4())) |
|
40 | self.set_secure_cookie(self.cookie_name, str(uuid.uuid4())) | |
50 | else: |
|
41 | else: | |
51 | self._render(message={'error': 'Invalid password'}) |
|
42 | self._render(message={'error': 'Invalid password'}) | |
52 | return |
|
43 | return | |
53 |
|
44 | |||
54 | self.redirect(self.get_argument('next', default=self.base_url)) |
|
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 | self.clear_cookie(self.cookie_name) |
|
68 | self.clear_cookie(self.cookie_name) | |
69 |
|
69 | |||
70 | def get_current_user(self): |
|
70 | def get_current_user(self): | |
71 | user_id = self.get_secure_cookie(self.cookie_name) |
|
71 | if self.login_handler is None: | |
72 | # For now the user_id should not return empty, but it could eventually |
|
72 | return 'anonymous' | |
73 | if user_id == '': |
|
73 | return self.login_handler.get_user(self) | |
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 |
|
|||
81 |
|
74 | |||
82 | @property |
|
75 | @property | |
83 | def cookie_name(self): |
|
76 | def cookie_name(self): | |
@@ -87,19 +80,17 b' class AuthenticatedHandler(web.RequestHandler):' | |||||
87 | return self.settings.get('cookie_name', default_cookie_name) |
|
80 | return self.settings.get('cookie_name', default_cookie_name) | |
88 |
|
81 | |||
89 | @property |
|
82 | @property | |
90 | def password(self): |
|
|||
91 | """our password""" |
|
|||
92 | return self.settings.get('password', '') |
|
|||
93 |
|
||||
94 | @property |
|
|||
95 | def logged_in(self): |
|
83 | def logged_in(self): | |
96 | """Is a user currently logged in? |
|
84 | """Is a user currently logged in?""" | |
97 |
|
||||
98 | """ |
|
|||
99 | user = self.get_current_user() |
|
85 | user = self.get_current_user() | |
100 | return (user and not user == 'anonymous') |
|
86 | return (user and not user == 'anonymous') | |
101 |
|
87 | |||
102 | @property |
|
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 | def login_available(self): |
|
94 | def login_available(self): | |
104 | """May a user proceed to log in? |
|
95 | """May a user proceed to log in? | |
105 |
|
96 | |||
@@ -107,7 +98,9 b' class AuthenticatedHandler(web.RequestHandler):' | |||||
107 | whether the user is already logged in or not. |
|
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 | class IPythonHandler(AuthenticatedHandler): |
|
106 | class IPythonHandler(AuthenticatedHandler): |
@@ -182,8 +182,10 b' class NotebookWebApplication(web.Application):' | |||||
182 | # authentication |
|
182 | # authentication | |
183 | cookie_secret=ipython_app.cookie_secret, |
|
183 | cookie_secret=ipython_app.cookie_secret, | |
184 | login_url=url_path_join(base_url,'/login'), |
|
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 | password=ipython_app.password, |
|
187 | password=ipython_app.password, | |
186 |
|
188 | |||
187 | # managers |
|
189 | # managers | |
188 | kernel_manager=kernel_manager, |
|
190 | kernel_manager=kernel_manager, | |
189 | contents_manager=contents_manager, |
|
191 | contents_manager=contents_manager, | |
@@ -193,7 +195,7 b' class NotebookWebApplication(web.Application):' | |||||
193 | config_manager=config_manager, |
|
195 | config_manager=config_manager, | |
194 |
|
196 | |||
195 | # IPython stuff |
|
197 | # IPython stuff | |
196 |
nbextensions_path |
|
198 | nbextensions_path=ipython_app.nbextensions_path, | |
197 | websocket_url=ipython_app.websocket_url, |
|
199 | websocket_url=ipython_app.websocket_url, | |
198 | mathjax_url=ipython_app.mathjax_url, |
|
200 | mathjax_url=ipython_app.mathjax_url, | |
199 | config=ipython_app.config, |
|
201 | config=ipython_app.config, | |
@@ -211,8 +213,8 b' class NotebookWebApplication(web.Application):' | |||||
211 | # Order matters. The first handler to match the URL will handle the request. |
|
213 | # Order matters. The first handler to match the URL will handle the request. | |
212 | handlers = [] |
|
214 | handlers = [] | |
213 | handlers.extend(load_handlers('tree.handlers')) |
|
215 | handlers.extend(load_handlers('tree.handlers')) | |
214 |
handlers.extend( |
|
216 | handlers.extend([(r"/login", settings['login_handler_class'])]) | |
215 |
handlers.extend( |
|
217 | handlers.extend([(r"/logout", settings['logout_handler_class'])]) | |
216 | handlers.extend(load_handlers('files.handlers')) |
|
218 | handlers.extend(load_handlers('files.handlers')) | |
217 | handlers.extend(load_handlers('notebook.handlers')) |
|
219 | handlers.extend(load_handlers('notebook.handlers')) | |
218 | handlers.extend(load_handlers('nbconvert.handlers')) |
|
220 | handlers.extend(load_handlers('nbconvert.handlers')) | |
@@ -501,7 +503,6 b' class NotebookApp(BaseIPythonApplication):' | |||||
501 |
|
503 | |||
502 | jinja_environment_options = Dict(config=True, |
|
504 | jinja_environment_options = Dict(config=True, | |
503 | help="Supply extra arguments that will be passed to Jinja environment.") |
|
505 | help="Supply extra arguments that will be passed to Jinja environment.") | |
504 |
|
||||
505 |
|
506 | |||
506 | enable_mathjax = Bool(True, config=True, |
|
507 | enable_mathjax = Bool(True, config=True, | |
507 | help="""Whether to enable MathJax for typesetting math/TeX |
|
508 | help="""Whether to enable MathJax for typesetting math/TeX | |
@@ -639,7 +640,6 b' class NotebookApp(BaseIPythonApplication):' | |||||
639 | def _kernel_spec_manager_default(self): |
|
640 | def _kernel_spec_manager_default(self): | |
640 | return KernelSpecManager(ipython_dir=self.ipython_dir) |
|
641 | return KernelSpecManager(ipython_dir=self.ipython_dir) | |
641 |
|
642 | |||
642 |
|
||||
643 | kernel_spec_manager_class = DottedObjectName('IPython.kernel.kernelspec.KernelSpecManager', |
|
643 | kernel_spec_manager_class = DottedObjectName('IPython.kernel.kernelspec.KernelSpecManager', | |
644 | config=True, |
|
644 | config=True, | |
645 | help=""" |
|
645 | help=""" | |
@@ -650,6 +650,14 b' class NotebookApp(BaseIPythonApplication):' | |||||
650 | without warning between this version of IPython and the next stable one. |
|
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 | trust_xheaders = Bool(False, config=True, |
|
661 | trust_xheaders = Bool(False, config=True, | |
654 | help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers" |
|
662 | help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers" | |
655 | "sent by the upstream reverse proxy. Necessary if the proxy handles SSL") |
|
663 | "sent by the upstream reverse proxy. Necessary if the proxy handles SSL") | |
@@ -700,7 +708,6 b' class NotebookApp(BaseIPythonApplication):' | |||||
700 | # setting App.notebook_dir implies setting notebook and kernel dirs as well |
|
708 | # setting App.notebook_dir implies setting notebook and kernel dirs as well | |
701 | self.config.FileContentsManager.root_dir = new |
|
709 | self.config.FileContentsManager.root_dir = new | |
702 | self.config.MappingKernelManager.root_dir = new |
|
710 | self.config.MappingKernelManager.root_dir = new | |
703 |
|
||||
704 |
|
711 | |||
705 | def parse_command_line(self, argv=None): |
|
712 | def parse_command_line(self, argv=None): | |
706 | super(NotebookApp, self).parse_command_line(argv) |
|
713 | super(NotebookApp, self).parse_command_line(argv) | |
@@ -748,6 +755,8 b' class NotebookApp(BaseIPythonApplication):' | |||||
748 | kls = import_item(self.cluster_manager_class) |
|
755 | kls = import_item(self.cluster_manager_class) | |
749 | self.cluster_manager = kls(parent=self, log=self.log) |
|
756 | self.cluster_manager = kls(parent=self, log=self.log) | |
750 | self.cluster_manager.update_profiles() |
|
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 | kls = import_item(self.config_manager_class) |
|
761 | kls = import_item(self.config_manager_class) | |
753 | self.config_manager = kls(parent=self, log=self.log, |
|
762 | self.config_manager = kls(parent=self, log=self.log, | |
@@ -788,17 +797,10 b' class NotebookApp(BaseIPythonApplication):' | |||||
788 | ssl_options['keyfile'] = self.keyfile |
|
797 | ssl_options['keyfile'] = self.keyfile | |
789 | else: |
|
798 | else: | |
790 | ssl_options = None |
|
799 | ssl_options = None | |
791 | self.web_app.password = self.password |
|
800 | self.login_handler_class.validate_security(self, ssl_options=ssl_options) | |
792 | self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options, |
|
801 | self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options, | |
793 | xheaders=self.trust_xheaders) |
|
802 | xheaders=self.trust_xheaders) | |
794 | if not self.ip: |
|
803 | ||
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.") |
|
|||
802 | success = None |
|
804 | success = None | |
803 | for port in random_ports(self.port, self.port_retries+1): |
|
805 | for port in random_ports(self.port, self.port_retries+1): | |
804 | try: |
|
806 | try: |
General Comments 0
You need to be logged in to leave comments.
Login now