Skip to content

Loading…

Add logout button. #1012

Merged
merged 9 commits into from

3 participants

@stefanv

Written with Mateusz Paprocki.

Based on PR #1011.

@fperez
IPython member

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
IPython member

@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
IPython member

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
IPython member

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
IPython member

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
IPython member

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
IPython member

Looks great, thanks! Merging now...

@fperez fperez merged commit a97429f into ipython:master
@fperez
IPython member

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