Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Add logout button. #1012

Merged
merged 9 commits into from

3 participants

@stefanv

Written with Mateusz Paprocki.

Based on PR #1011.

@fperez
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!

@fperez
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...

@takluyver
Owner

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.

@fperez
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.

@takluyver
Owner

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.

@fperez
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.

@fperez
Owner

Looks great, thanks! Merging now...

@fperez fperez merged commit a97429f into from
@fperez
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.
View
35 IPython/frontend/html/notebook/handlers.py
@@ -28,6 +28,7 @@
from IPython.external.decorator import decorator
from IPython.zmq.session import Session
+from IPython.lib.security import passwd_check
try:
from docutils.core import publish_string
@@ -115,7 +116,14 @@ def auth_f(self, *args, **kwargs):
# Top-level handlers
#-----------------------------------------------------------------------------
-class AuthenticatedHandler(web.RequestHandler):
+class RequestHandler(web.RequestHandler):
+ """RequestHandler with default variable setting."""
+
+ def render(*args, **kwargs):
+ kwargs.setdefault('message', '')
+ return web.RequestHandler.render(*args, **kwargs)
+
+class AuthenticatedHandler(RequestHandler):
"""A RequestHandler with an authenticated user."""
def get_current_user(self):
@@ -166,19 +174,38 @@ def get(self):
class LoginHandler(AuthenticatedHandler):
- def get(self):
+ def _render(self, message=None):
self.render('login.html',
next=self.get_argument('next', default='/'),
read_only=self.read_only,
+ message=message
)
+ def get(self):
+ if self.current_user:
+ self.redirect(self.get_argument('next', default='/'))
+ else:
+ self._render()
+
def post(self):
pwd = self.get_argument('password', default=u'')
- if self.application.password and pwd == self.application.password:
- self.set_secure_cookie('username', str(uuid.uuid4()))
+ if self.application.password:
+ if passwd_check(self.application.password, pwd):
+ self.set_secure_cookie('username', str(uuid.uuid4()))
+ else:
+ self._render(message={'error': 'Invalid password'})
+ return
+
self.redirect(self.get_argument('next', default='/'))
+class LogoutHandler(AuthenticatedHandler):
+
+ def get(self):
+ self.clear_cookie('username')
+ self.render('logout.html', message={'info': 'Successfully logged out.'})
+
+
class NewHandler(AuthenticatedHandler):
@web.authenticated
View
14 IPython/frontend/html/notebook/notebookapp.py
@@ -40,7 +40,7 @@
# Our own libraries
from .kernelmanager import MappingKernelManager
-from .handlers import (LoginHandler,
+from .handlers import (LoginHandler, LogoutHandler,
ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
ShellHandler, NotebookRootHandler, NotebookHandler, RSTHandler
@@ -87,6 +87,7 @@ def __init__(self, ipython_app, kernel_manager, notebook_manager, log):
handlers = [
(r"/", ProjectDashboardHandler),
(r"/login", LoginHandler),
+ (r"/logout", LogoutHandler),
(r"/new", NewHandler),
(r"/%s" % _notebook_id_regex, NamedNotebookHandler),
(r"/kernels", MainKernelHandler),
@@ -208,7 +209,16 @@ def _ip_changed(self, name, old, new):
)
password = Unicode(u'', config=True,
- help="""Password to use for web authentication"""
+ help="""Hashed password to use for web authentication.
+
+ To generate, do:
+
+ from IPython.lib import passwd
+
+ passwd('mypassphrase')
+
+ The string should be of the form type:salt:hashed-password.
+ """
)
open_browser = Bool(True, config=True,
View
27 IPython/frontend/html/notebook/static/css/layout.css
@@ -101,3 +101,30 @@
-moz-box-pack: center;
box-pack: center;
}
+
+.message {
+ border-width: 1px;
+ border-style: solid;
+ text-align: center;
+ padding: 0.5em;
+ margin: 0.5em 0;
+}
+
+.message.error {
+ background-color: #FFD3D1;
+ border-color: red;
+}
+
+.message.warning {
+ background-color: #FFD09E;
+ border-color: orange;
+}
+
+.message.info {
+ background-color: #CBFFBA;
+ border-color: green;
+}
+
+#content_panel {
+ margin: 0.5em;
+}
View
2  IPython/frontend/html/notebook/static/css/projectdashboard.css
@@ -31,7 +31,7 @@ body {
}
#content_toolbar {
- padding: 10px 5px 5px 5px;
+ padding: 5px;
height: 25px;
line-height: 25px;
}
View
6 IPython/frontend/html/notebook/static/js/loginwidget.js
@@ -21,12 +21,12 @@ var IPython = (function (IPython) {
};
LoginWidget.prototype.style = function () {
- this.element.find('button#login').button();
+ this.element.find('button#logout').button();
};
LoginWidget.prototype.bind_events = function () {
var that = this;
- this.element.find("button#login").click(function () {
- window.location = "/login?next="+location.pathname;
+ this.element.find("button#logout").click(function () {
+ window.location = "/logout";
});
};
View
77 IPython/frontend/html/notebook/templates/layout.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+
+ <title>{% block title %}IPython Notebook{% end %}</title>
+
+ <link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
+ <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
+ <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
+ <link rel="stylesheet" href="static/css/base.css" type="text/css"/>
+ {% block stylesheet %}
+ {% end %}
+
+ {% block meta %}
+ {% end %}
+
+</head>
+
+<body {% block params %}{% end %}>
+
+<div id="header">
+ <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
+ <span id="login_widget">
+ {% if current_user and current_user != 'anonymous' %}
+ <button id="logout">Logout</button>
+ {% end %}
+ </span>
+ {% block header %}
+ {% end %}
+</div>
+
+<div id="header_border"></div>
+
+<div id="main_app">
+
+ <div id="app_hbox">
+
+ <div id="left_panel">
+ {% block left_panel %}
+ {% end %}
+ </div>
+
+ <div id="content_panel">
+ {% if message %}
+
+ {% for key in message %}
+ <div class="message {{key}}">
+ {{message[key]}}
+ </div>
+ {% end %}
+ {% end %}
+
+ {% block content_panel %}
+ {% end %}
+ </div>
+ <div id="right_panel">
+ {% block right_panel %}
+ {% end %}
+ </div>
+
+ </div>
+
+</div>
+
+<script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
+<script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
+<script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
+<script src="static/js/loginmain.js" type="text/javascript" charset="utf-8"></script>
+<script src="static/js/loginwidget.js" type="text/javascript" charset="utf-8"></script>
+{% block script %}
+{% end %}
+
+</body>
+
+</html>
View
63 IPython/frontend/html/notebook/templates/login.html
@@ -1,55 +1,8 @@
-<!DOCTYPE HTML>
-<html>
-
-<head>
- <meta charset="utf-8">
-
- <title>IPython Notebook</title>
-
- <link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
- <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
- <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
- <link rel="stylesheet" href="static/css/base.css" type="text/css" />
-
- <meta name="read_only" content="{{read_only}}"/>
-
-</head>
-
-<body>
-
-<div id="header">
- <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
-</div>
-
-<div id="header_border"></div>
-
-<div id="main_app">
-
- <div id="app_hbox">
-
- <div id="left_panel">
- </div>
-
- <div id="content_panel">
- <form action="/login?next={{url_escape(next)}}" method="post">
- Password: <input type="password" name="password">
- <input type="submit" value="Sign in" id="signin">
- </form>
- </div>
- <div id="right_panel">
- </div>
-
- </div>
-
-</div>
-
-<script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
-<script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
-<script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
-<script src="static/js/loginmain.js" type="text/javascript" charset="utf-8"></script>
-
-</body>
-
-</html>
-
-
+{% extends layout.html %}
+
+{% block content_panel %}
+ <form action="/login?next={{url_escape(next)}}" method="post">
+ Password: <input type="password" name="password">
+ <input type="submit" value="Sign in" id="signin">
+ </form>
+{% end %}
View
5 IPython/frontend/html/notebook/templates/logout.html
@@ -0,0 +1,5 @@
+{% extends layout.html %}
+
+{% block content_panel %}
+Proceed to the <a href="/login">login page</a>.
+{% end %}
View
10 IPython/frontend/html/notebook/templates/notebook.html
@@ -59,9 +59,15 @@
<span id="quick_help_area">
<button id="quick_help">Quick<u>H</u>elp</button>
</span>
- <span id="login_widget" class="hidden">
- <button id="login">Login</button>
+
+ <span id="login_widget">
+ {% comment This is a temporary workaround to hide the logout button %}
+ {% comment when appropriate until notebook.html is templated %}
+ {% if current_user and current_user != 'anonymous' %}
+ <button id="logout">Logout</button>
+ {% end %}
</span>
+
<span id="kernel_status">Idle</span>
</div>
View
89 IPython/frontend/html/notebook/templates/projectdashboard.html
@@ -1,69 +1,36 @@
-<!DOCTYPE HTML>
-<html>
+{% extends layout.html %}
-<head>
- <meta charset="utf-8">
+{% block title %}
+IPython Dashboard
+{% end %}
- <title>IPython Dashboard</title>
-
- <link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
- <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
- <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
- <link rel="stylesheet" href="static/css/base.css" type="text/css" />
+{% block stylesheet %}
<link rel="stylesheet" href="static/css/projectdashboard.css" type="text/css" />
+{% end %}
+{% block meta %}
<meta name="read_only" content="{{read_only}}"/>
-
-</head>
-
-<body data-project={{project}} data-base-project-url={{base_project_url}}
- data-base-kernel-url={{base_kernel_url}}>
-
-<div id="header">
- <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
- <span id="login_widget" class="hidden">
- <button id="login">Login</button>
- </span>
-</div>
-
-<div id="header_border"></div>
-
-<div id="main_app">
-
- <div id="app_hbox">
-
- <div id="left_panel">
- </div>
-
- <div id="content_panel">
- <div id="content_toolbar">
- <span id="drag_info">Drag files onto the list to import notebooks.</span>
- <span id="notebooks_buttons">
- <button id="new_notebook">New Notebook</button>
- </span>
- </div>
- <div id="notebook_list">
- <div id="project_name"><h2>{{project}}</h2></div>
- </div>
-
+{% end %}
+
+{% block params %}
+data-project={{project}}
+data-base-project-url={{base_project_url}}
+data-base-kernel-url={{base_kernel_url}}
+{% end %}
+
+{% block content_panel %}
+ <div id="content_toolbar">
+ <span id="drag_info">Drag files onto the list to import notebooks.</span>
+ <span id="notebooks_buttons">
+ <button id="new_notebook">New Notebook</button>
+ </span>
</div>
-
- <div id="right_panel">
+ <div id="notebook_list">
+ <div id="project_name"><h2>{{project}}</h2></div>
</div>
-
- </div>
-
-</div>
-
-<script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
-<script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
-<script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
-<script src="static/js/notebooklist.js" type="text/javascript" charset="utf-8"></script>
-<script src="static/js/loginwidget.js" type="text/javascript" charset="utf-8"></script>
-<script src="static/js/projectdashboardmain.js" type="text/javascript" charset="utf-8"></script>
-
-</body>
-
-</html>
-
+{% end %}
+{% block script %}
+ <script src="static/js/notebooklist.js" type="text/javascript" charset="utf-8"></script>
+ <script src="static/js/projectdashboardmain.js" type="text/javascript" charset="utf-8"></script>
+{% end %}
View
2  IPython/lib/__init__.py
@@ -25,6 +25,8 @@
current_gui
)
+from IPython.lib.security import passwd
+
#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------
View
82 IPython/lib/security.py
@@ -0,0 +1,82 @@
+"""
+Password generation for the IPython notebook.
+"""
+
+import hashlib
+import random
+
+def passwd(passphrase):
+ """Generate hashed password and salt for use in notebook configuration.
+
+ In the notebook configuration, set `c.NotebookApp.password` to
+ the generated string.
+
+ Parameters
+ ----------
+ passphrase : str
+ Password to hash.
+
+ Returns
+ -------
+ hashed_passphrase : str
+ Hashed password, in the format 'hash_algorithm:salt:passphrase_hash'.
+
+ Examples
+ --------
+ In [1]: passwd('mypassword')
+ Out[1]: 'sha1:7cf3:b7d6da294ea9592a9480c8f52e63cd42cfb9dd12'
+
+ """
+ algorithm = 'sha1'
+
+ h = hashlib.new(algorithm)
+ salt = hex(int(random.getrandbits(16)))[2:]
+ h.update(passphrase + salt)
+
+ return ':'.join((algorithm, salt, h.hexdigest()))
+
+def passwd_check(hashed_passphrase, passphrase):
+ """Verify that a given passphrase matches its hashed version.
+
+ Parameters
+ ----------
+ hashed_passphrase : str
+ Hashed password, in the format returned by `passwd`.
+ passphrase : str
+ Passphrase to validate.
+
+ Returns
+ -------
+ valid : bool
+ True if the passphrase matches the hash.
+
+ Examples
+ --------
+ In [1]: from IPython.lib.security import passwd_check
+
+ In [2]: passwd_check('sha1:7cf3:b7d6da294ea9592a9480c8f52e63cd42cfb9dd12',
+ ...: 'mypassword')
+ Out[2]: True
+
+ In [3]: passwd_check('sha1:7cf3:b7d6da294ea9592a9480c8f52e63cd42cfb9dd12',
+ ...: 'anotherpassword')
+ Out[3]: False
+
+ """
+ # Algorithm and hash length
+ supported_algorithms = {'sha1': 40}
+
+ try:
+ algorithm, salt, pw_digest = hashed_passphrase.split(':', 2)
+ except (ValueError, TypeError):
+ return False
+
+ if not (algorithm in supported_algorithms and \
+ len(pw_digest) == supported_algorithms[algorithm] and \
+ len(salt) == 4):
+ return False
+
+ h = hashlib.new(algorithm)
+ h.update(passphrase + salt)
+
+ return h.hexdigest() == pw_digest
View
21 IPython/lib/tests/test_security.py
@@ -0,0 +1,21 @@
+from IPython.lib import passwd
+from IPython.lib.security import passwd_check
+import nose.tools as nt
+
+def test_passwd_structure():
+ p = passwd('passphrase')
+ algorithm, salt, hashed = p.split(':')
+ nt.assert_equals(algorithm, 'sha1')
+ nt.assert_equals(len(salt), 4)
+ nt.assert_equals(len(hashed), 40)
+
+def test_roundtrip():
+ p = passwd('passphrase')
+ nt.assert_equals(passwd_check(p, 'passphrase'), True)
+
+def test_bad():
+ p = passwd('passphrase')
+ nt.assert_equals(passwd_check(p, p), False)
+ nt.assert_equals(passwd_check(p, 'a:b:c:d'), False)
+ nt.assert_equals(passwd_check(p, 'a:b'), False)
+
Something went wrong with that request. Please try again.