Skip to content
This repository

Add logout button. #1012

Merged
merged 9 commits into from over 2 years ago

3 participants

Stefan van der Walt Fernando Perez Thomas Kluyver
Stefan van der Walt

Written with Mateusz Paprocki.

Based on PR #1011.

Fernando Perez
Owner

Had a first look and tests, and it all looks great! But I'm a bit tired and I'm going to crash now. I'll pick it up tomorrow to finish the review/merge. Thanks!

Fernando Perez
Owner

@stefanv, the logout button should not appear in the top bar for non-authenticated notebooks. Otherwise this looks great to me! I'll wait for this fix and for @takluyver to have a chance to check things on py3...

Thomas Kluyver
Collaborator

I'm confused as to why IPython/lib/security.py is showing up as a new file here, when it was already added by PR #1011.

Fernando Perez
Owner

Just ignore commits 551f71e through 7ab92dd, they were already merged in #1011 and github simply isn't showing the branch correctly. The first new commit in this branch is 233e9c0.

Thomas Kluyver
Collaborator

While we're on this topic - have we left an easy way to replace our own authentication method with something a site is already using. I'm thinking for example of PythonAnywhere.

Fernando Perez
Owner

I don't know how well spearated the code is for that yet, honestly. But I don't want to over-engineer up-front; we should obviously keep those use cases in mind, but we shouldn't try to fake a multiuser authentication mechanism here. The current notebooks is firmly and squarely tied to a true unix user, this is just a mechanism to authorize sharing. But you are giving someone effectively your system shell...

What we're building is orthogonal to a separate multiuser system that can be implemented outside of the notebook to manage users, accounts, etc, and then will use the single-user notebook for each user. This is just a way for an individual user to effectively give access to his shell to friends/colleagues without having to give up his real unix password or provide a full ssh login. But for the duration of the session, the other users have full system access, so it's not something to be granted lightly.

Fernando Perez
Owner

Looks great, thanks! Merging now...

Fernando Perez fperez merged commit a97429f into from November 20, 2011
Fernando Perez fperez closed this November 20, 2011
Fernando Perez
Owner

Mmh, auto-close isn't working. Closing manually. Merged in 80e7338.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
35  IPython/frontend/html/notebook/handlers.py
@@ -28,6 +28,7 @@
28 28
 
29 29
 from IPython.external.decorator import decorator
30 30
 from IPython.zmq.session import Session
  31
+from IPython.lib.security import passwd_check
31 32
 
32 33
 try:
33 34
     from docutils.core import publish_string
@@ -115,7 +116,14 @@ def auth_f(self, *args, **kwargs):
115 116
 # Top-level handlers
116 117
 #-----------------------------------------------------------------------------
117 118
 
118  
-class AuthenticatedHandler(web.RequestHandler):
  119
+class RequestHandler(web.RequestHandler):
  120
+    """RequestHandler with default variable setting."""
  121
+
  122
+    def render(*args, **kwargs):
  123
+        kwargs.setdefault('message', '')
  124
+        return web.RequestHandler.render(*args, **kwargs)
  125
+
  126
+class AuthenticatedHandler(RequestHandler):
119 127
     """A RequestHandler with an authenticated user."""
120 128
 
121 129
     def get_current_user(self):
@@ -166,19 +174,38 @@ def get(self):
166 174
 
167 175
 class LoginHandler(AuthenticatedHandler):
168 176
 
169  
-    def get(self):
  177
+    def _render(self, message=None):
170 178
         self.render('login.html',
171 179
                 next=self.get_argument('next', default='/'),
172 180
                 read_only=self.read_only,
  181
+                message=message
173 182
         )
174 183
 
  184
+    def get(self):
  185
+        if self.current_user:
  186
+            self.redirect(self.get_argument('next', default='/'))
  187
+        else:
  188
+            self._render()
  189
+
175 190
     def post(self):
176 191
         pwd = self.get_argument('password', default=u'')
177  
-        if self.application.password and pwd == self.application.password:
178  
-            self.set_secure_cookie('username', str(uuid.uuid4()))
  192
+        if self.application.password:
  193
+            if passwd_check(self.application.password, pwd):
  194
+                self.set_secure_cookie('username', str(uuid.uuid4()))
  195
+            else:
  196
+                self._render(message={'error': 'Invalid password'})
  197
+                return
  198
+
179 199
         self.redirect(self.get_argument('next', default='/'))
180 200
 
181 201
 
  202
+class LogoutHandler(AuthenticatedHandler):
  203
+
  204
+    def get(self):
  205
+        self.clear_cookie('username')
  206
+        self.render('logout.html', message={'info': 'Successfully logged out.'})
  207
+
  208
+
182 209
 class NewHandler(AuthenticatedHandler):
183 210
 
184 211
     @web.authenticated
14  IPython/frontend/html/notebook/notebookapp.py
@@ -40,7 +40,7 @@
40 40
 
41 41
 # Our own libraries
42 42
 from .kernelmanager import MappingKernelManager
43  
-from .handlers import (LoginHandler,
  43
+from .handlers import (LoginHandler, LogoutHandler,
44 44
     ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
45 45
     MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
46 46
     ShellHandler, NotebookRootHandler, NotebookHandler, RSTHandler
@@ -87,6 +87,7 @@ def __init__(self, ipython_app, kernel_manager, notebook_manager, log):
87 87
         handlers = [
88 88
             (r"/", ProjectDashboardHandler),
89 89
             (r"/login", LoginHandler),
  90
+            (r"/logout", LogoutHandler),
90 91
             (r"/new", NewHandler),
91 92
             (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
92 93
             (r"/kernels", MainKernelHandler),
@@ -208,7 +209,16 @@ def _ip_changed(self, name, old, new):
208 209
     )
209 210
 
210 211
     password = Unicode(u'', config=True,
211  
-                      help="""Password to use for web authentication"""
  212
+                      help="""Hashed password to use for web authentication.
  213
+
  214
+                      To generate, do:
  215
+
  216
+                        from IPython.lib import passwd
  217
+
  218
+                        passwd('mypassphrase')
  219
+
  220
+                      The string should be of the form type:salt:hashed-password.
  221
+                      """
212 222
     )
213 223
     
214 224
     open_browser = Bool(True, config=True,
27  IPython/frontend/html/notebook/static/css/layout.css
@@ -101,3 +101,30 @@
101 101
 	-moz-box-pack: center;
102 102
 	box-pack: center;
103 103
 }
  104
+
  105
+.message {
  106
+    border-width: 1px;
  107
+    border-style: solid;
  108
+    text-align: center;
  109
+    padding: 0.5em;
  110
+    margin: 0.5em 0;
  111
+}
  112
+
  113
+.message.error {
  114
+    background-color: #FFD3D1;
  115
+    border-color: red;
  116
+}
  117
+
  118
+.message.warning {
  119
+    background-color: #FFD09E;
  120
+    border-color: orange;
  121
+}
  122
+
  123
+.message.info {
  124
+    background-color: #CBFFBA;
  125
+    border-color: green;
  126
+}
  127
+
  128
+#content_panel {
  129
+    margin: 0.5em;
  130
+}
2  IPython/frontend/html/notebook/static/css/projectdashboard.css
@@ -31,7 +31,7 @@ body {
31 31
 }
32 32
 
33 33
 #content_toolbar {
34  
-    padding: 10px 5px 5px 5px;
  34
+    padding: 5px;
35 35
     height: 25px;
36 36
     line-height: 25px;
37 37
 }
6  IPython/frontend/html/notebook/static/js/loginwidget.js
@@ -21,12 +21,12 @@ var IPython = (function (IPython) {
21 21
     };
22 22
 
23 23
     LoginWidget.prototype.style = function () {
24  
-        this.element.find('button#login').button();
  24
+        this.element.find('button#logout').button();
25 25
     };
26 26
     LoginWidget.prototype.bind_events = function () {
27 27
         var that = this;
28  
-        this.element.find("button#login").click(function () {
29  
-            window.location = "/login?next="+location.pathname;
  28
+        this.element.find("button#logout").click(function () {
  29
+            window.location = "/logout";
30 30
         });
31 31
     };
32 32
 
77  IPython/frontend/html/notebook/templates/layout.html
... ...
@@ -0,0 +1,77 @@
  1
+<!DOCTYPE HTML>
  2
+<html>
  3
+
  4
+<head>
  5
+    <meta charset="utf-8">
  6
+
  7
+    <title>{% block title %}IPython Notebook{% end %}</title>
  8
+
  9
+    <link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
  10
+    <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
  11
+    <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
  12
+    <link rel="stylesheet" href="static/css/base.css" type="text/css"/>
  13
+    {% block stylesheet %}
  14
+    {% end %}
  15
+
  16
+    {% block meta %}
  17
+    {% end %}
  18
+
  19
+</head>
  20
+
  21
+<body {% block params %}{% end %}>
  22
+
  23
+<div id="header">
  24
+    <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
  25
+    <span id="login_widget">
  26
+      {% if current_user and current_user != 'anonymous' %}
  27
+        <button id="logout">Logout</button>
  28
+      {% end %}
  29
+    </span>
  30
+    {% block header %}
  31
+    {% end %}
  32
+</div>
  33
+
  34
+<div id="header_border"></div>
  35
+
  36
+<div id="main_app">
  37
+
  38
+    <div id="app_hbox">
  39
+
  40
+    <div id="left_panel">
  41
+    {% block left_panel %}
  42
+    {% end %}
  43
+    </div>
  44
+
  45
+    <div id="content_panel">
  46
+        {% if message %}
  47
+
  48
+          {% for key in message %}
  49
+            <div class="message {{key}}">
  50
+               {{message[key]}}
  51
+            </div>
  52
+          {% end %}
  53
+        {% end %}
  54
+
  55
+        {% block content_panel %}
  56
+        {% end %}
  57
+    </div>
  58
+    <div id="right_panel">
  59
+    {% block right_panel %}
  60
+    {% end %}
  61
+    </div>
  62
+
  63
+    </div>
  64
+
  65
+</div>
  66
+
  67
+<script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
  68
+<script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
  69
+<script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
  70
+<script src="static/js/loginmain.js" type="text/javascript" charset="utf-8"></script>
  71
+<script src="static/js/loginwidget.js" type="text/javascript" charset="utf-8"></script>
  72
+{% block script %}
  73
+{% end %}
  74
+
  75
+</body>
  76
+
  77
+</html>
63  IPython/frontend/html/notebook/templates/login.html
... ...
@@ -1,55 +1,8 @@
1  
-<!DOCTYPE HTML>
2  
-<html>
3  
-
4  
-<head>
5  
-    <meta charset="utf-8">
6  
-
7  
-    <title>IPython Notebook</title>
8  
-
9  
-    <link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
10  
-    <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
11  
-    <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
12  
-    <link rel="stylesheet" href="static/css/base.css" type="text/css" />
13  
-
14  
-    <meta name="read_only" content="{{read_only}}"/>
15  
-
16  
-</head>
17  
-
18  
-<body>
19  
-
20  
-<div id="header">
21  
-    <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
22  
-</div>
23  
-
24  
-<div id="header_border"></div>
25  
-
26  
-<div id="main_app">
27  
-
28  
-    <div id="app_hbox">
29  
-
30  
-    <div id="left_panel">
31  
-    </div>
32  
-    
33  
-    <div id="content_panel">
34  
-        <form action="/login?next={{url_escape(next)}}" method="post">
35  
-            Password: <input type="password" name="password">
36  
-            <input type="submit" value="Sign in" id="signin">
37  
-        </form>
38  
-    </div>
39  
-    <div id="right_panel">
40  
-    </div>
41  
-    
42  
-    </div>
43  
-
44  
-</div>
45  
-
46  
-<script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
47  
-<script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
48  
-<script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
49  
-<script src="static/js/loginmain.js" type="text/javascript" charset="utf-8"></script>
50  
-
51  
-</body>
52  
-
53  
-</html>
54  
-
55  
-
  1
+{% extends layout.html %}
  2
+
  3
+{% block content_panel %}
  4
+    <form action="/login?next={{url_escape(next)}}" method="post">
  5
+        Password: <input type="password" name="password">
  6
+        <input type="submit" value="Sign in" id="signin">
  7
+    </form>
  8
+{% end %}
5  IPython/frontend/html/notebook/templates/logout.html
... ...
@@ -0,0 +1,5 @@
  1
+{% extends layout.html %}
  2
+
  3
+{% block content_panel %}
  4
+Proceed to the <a href="/login">login page</a>.
  5
+{% end %}
10  IPython/frontend/html/notebook/templates/notebook.html
@@ -59,9 +59,15 @@
59 59
     <span id="quick_help_area">
60 60
       <button id="quick_help">Quick<u>H</u>elp</button>
61 61
     </span>
62  
-    <span id="login_widget" class="hidden">
63  
-      <button id="login">Login</button>
  62
+
  63
+    <span id="login_widget">
  64
+      {% comment  This is a temporary workaround to hide the logout button %}
  65
+      {% comment  when appropriate until notebook.html is templated %}
  66
+      {% if current_user and current_user != 'anonymous' %}
  67
+        <button id="logout">Logout</button>
  68
+      {% end %}
64 69
     </span>
  70
+
65 71
     <span id="kernel_status">Idle</span>
66 72
 </div>
67 73
 
89  IPython/frontend/html/notebook/templates/projectdashboard.html
... ...
@@ -1,69 +1,36 @@
1  
-<!DOCTYPE HTML>
2  
-<html>
  1
+{% extends layout.html %}
3 2
 
4  
-<head>
5  
-    <meta charset="utf-8">
  3
+{% block title %}
  4
+IPython Dashboard
  5
+{% end %}
6 6
 
7  
-    <title>IPython Dashboard</title>
8  
-
9  
-    <link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
10  
-    <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
11  
-    <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
12  
-    <link rel="stylesheet" href="static/css/base.css" type="text/css" />
  7
+{% block stylesheet %}
13 8
     <link rel="stylesheet" href="static/css/projectdashboard.css" type="text/css" />
  9
+{% end %}
14 10
 
  11
+{% block meta %}
15 12
     <meta name="read_only" content="{{read_only}}"/>
16  
-
17  
-</head>
18  
-
19  
-<body data-project={{project}} data-base-project-url={{base_project_url}}
20  
-      data-base-kernel-url={{base_kernel_url}}>
21  
-
22  
-<div id="header">
23  
-    <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
24  
-    <span id="login_widget" class="hidden">
25  
-      <button id="login">Login</button>
26  
-    </span>
27  
-</div>
28  
-
29  
-<div id="header_border"></div>
30  
-
31  
-<div id="main_app">
32  
-
33  
-    <div id="app_hbox">
34  
-
35  
-    <div id="left_panel">
36  
-    </div>
37  
-
38  
-    <div id="content_panel">
39  
-        <div id="content_toolbar">
40  
-            <span id="drag_info">Drag files onto the list to import notebooks.</span>
41  
-            <span id="notebooks_buttons">
42  
-                <button id="new_notebook">New Notebook</button>
43  
-            </span>
44  
-        </div>
45  
-        <div id="notebook_list">
46  
-            <div id="project_name"><h2>{{project}}</h2></div>
47  
-        </div>
48  
-
  13
+{% end %}
  14
+
  15
+{% block params %}
  16
+data-project={{project}}
  17
+data-base-project-url={{base_project_url}}
  18
+data-base-kernel-url={{base_kernel_url}}
  19
+{% end %}
  20
+
  21
+{% block content_panel %}
  22
+    <div id="content_toolbar">
  23
+        <span id="drag_info">Drag files onto the list to import notebooks.</span>
  24
+        <span id="notebooks_buttons">
  25
+            <button id="new_notebook">New Notebook</button>
  26
+        </span>
49 27
     </div>
50  
-
51  
-    <div id="right_panel">
  28
+    <div id="notebook_list">
  29
+        <div id="project_name"><h2>{{project}}</h2></div>
52 30
     </div>
53  
-    
54  
-    </div>
55  
-
56  
-</div>
57  
-
58  
-<script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
59  
-<script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
60  
-<script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
61  
-<script src="static/js/notebooklist.js" type="text/javascript" charset="utf-8"></script>
62  
-<script src="static/js/loginwidget.js" type="text/javascript" charset="utf-8"></script>
63  
-<script src="static/js/projectdashboardmain.js" type="text/javascript" charset="utf-8"></script>
64  
-
65  
-</body>
66  
-
67  
-</html>
68  
-
  31
+{% end %}
69 32
 
  33
+{% block script %}
  34
+    <script src="static/js/notebooklist.js" type="text/javascript" charset="utf-8"></script>
  35
+    <script src="static/js/projectdashboardmain.js" type="text/javascript" charset="utf-8"></script>
  36
+{% end %}
2  IPython/lib/__init__.py
@@ -25,6 +25,8 @@
25 25
     current_gui
26 26
 )
27 27
 
  28
+from IPython.lib.security import passwd
  29
+
28 30
 #-----------------------------------------------------------------------------
29 31
 # Code
30 32
 #-----------------------------------------------------------------------------
82  IPython/lib/security.py
... ...
@@ -0,0 +1,82 @@
  1
+"""
  2
+Password generation for the IPython notebook.
  3
+"""
  4
+
  5
+import hashlib
  6
+import random
  7
+
  8
+def passwd(passphrase):
  9
+    """Generate hashed password and salt for use in notebook configuration.
  10
+
  11
+    In the notebook configuration, set `c.NotebookApp.password` to
  12
+    the generated string.
  13
+
  14
+    Parameters
  15
+    ----------
  16
+    passphrase : str
  17
+        Password to hash.
  18
+
  19
+    Returns
  20
+    -------
  21
+    hashed_passphrase : str
  22
+        Hashed password, in the format 'hash_algorithm:salt:passphrase_hash'.
  23
+
  24
+    Examples
  25
+    --------
  26
+    In [1]: passwd('mypassword')
  27
+    Out[1]: 'sha1:7cf3:b7d6da294ea9592a9480c8f52e63cd42cfb9dd12'
  28
+
  29
+    """
  30
+    algorithm = 'sha1'
  31
+
  32
+    h = hashlib.new(algorithm)
  33
+    salt = hex(int(random.getrandbits(16)))[2:]
  34
+    h.update(passphrase + salt)
  35
+
  36
+    return ':'.join((algorithm, salt, h.hexdigest()))
  37
+
  38
+def passwd_check(hashed_passphrase, passphrase):
  39
+    """Verify that a given passphrase matches its hashed version.
  40
+
  41
+    Parameters
  42
+    ----------
  43
+    hashed_passphrase : str
  44
+        Hashed password, in the format returned by `passwd`.
  45
+    passphrase : str
  46
+        Passphrase to validate.
  47
+
  48
+    Returns
  49
+    -------
  50
+    valid : bool
  51
+        True if the passphrase matches the hash.
  52
+
  53
+    Examples
  54
+    --------
  55
+    In [1]: from IPython.lib.security import passwd_check
  56
+
  57
+    In [2]: passwd_check('sha1:7cf3:b7d6da294ea9592a9480c8f52e63cd42cfb9dd12',
  58
+       ...:              'mypassword')
  59
+    Out[2]: True
  60
+
  61
+    In [3]: passwd_check('sha1:7cf3:b7d6da294ea9592a9480c8f52e63cd42cfb9dd12',
  62
+       ...:              'anotherpassword')
  63
+    Out[3]: False
  64
+
  65
+    """
  66
+    # Algorithm and hash length
  67
+    supported_algorithms = {'sha1': 40}
  68
+
  69
+    try:
  70
+        algorithm, salt, pw_digest = hashed_passphrase.split(':', 2)
  71
+    except (ValueError, TypeError):
  72
+        return False
  73
+
  74
+    if not (algorithm in supported_algorithms and \
  75
+            len(pw_digest) == supported_algorithms[algorithm] and \
  76
+            len(salt) == 4):
  77
+        return False
  78
+
  79
+    h = hashlib.new(algorithm)
  80
+    h.update(passphrase + salt)
  81
+
  82
+    return h.hexdigest() == pw_digest
21  IPython/lib/tests/test_security.py
... ...
@@ -0,0 +1,21 @@
  1
+from IPython.lib import passwd
  2
+from IPython.lib.security import passwd_check
  3
+import nose.tools as nt
  4
+
  5
+def test_passwd_structure():
  6
+    p = passwd('passphrase')
  7
+    algorithm, salt, hashed = p.split(':')
  8
+    nt.assert_equals(algorithm, 'sha1')
  9
+    nt.assert_equals(len(salt), 4)
  10
+    nt.assert_equals(len(hashed), 40)
  11
+
  12
+def test_roundtrip():
  13
+    p = passwd('passphrase')
  14
+    nt.assert_equals(passwd_check(p, 'passphrase'), True)
  15
+
  16
+def test_bad():
  17
+    p = passwd('passphrase')
  18
+    nt.assert_equals(passwd_check(p, p), False)
  19
+    nt.assert_equals(passwd_check(p, 'a:b:c:d'), False)
  20
+    nt.assert_equals(passwd_check(p, 'a:b'), False)
  21
+
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.