Merge pull request #931 from minrk/readonly

The notebook now supports a `--read-only` flag, which allows users to view all notebooks being served but not to edit them or execute any code.  These actions are not allowed and the buttons, shortcuts, etc. are removed, but the requests will raise authentication errors if they manage to send the events anyway.  Save/print functions remain available.

This flag can be used in two modes:

1. When running an unauthenticated server, one can run a *second* read-only server in the same directory on a public IP address.  This will let users connect to the read-only view without having to worry about configuring passwords and certificates for the execution server.

2. When running a server configured with authentication (and hopefully an SSL certificate), starting it with `--read-only` allows unauthenticated users read-only access to notebooks. This means that the same server on a single port can be both used by authenticated users for execution and by the public for viewing the available notebooks.
pull/37/head
Fernando Perez 14 years ago
commit 83ddbf5987

@ -26,6 +26,7 @@ from tornado import websocket
from zmq.eventloop import ioloop
from zmq.utils import jsonapi
from IPython.external.decorator import decorator
from IPython.zmq.session import Session
try:
@ -34,6 +35,32 @@ except ImportError:
publish_string = None
#-----------------------------------------------------------------------------
# Decorator for disabling read-only handlers
#-----------------------------------------------------------------------------
@decorator
def not_if_readonly(f, self, *args, **kwargs):
if self.application.read_only:
raise web.HTTPError(403, "Notebook server is read-only")
else:
return f(self, *args, **kwargs)
@decorator
def authenticate_unless_readonly(f, self, *args, **kwargs):
"""authenticate this page *unless* readonly view is active.
In read-only mode, the notebook list and print view should
be accessible without authentication.
"""
@web.authenticated
def auth_f(self, *args, **kwargs):
return f(self, *args, **kwargs)
if self.application.read_only:
return f(self, *args, **kwargs)
else:
return auth_f(self, *args, **kwargs)
#-----------------------------------------------------------------------------
# Top-level handlers
@ -50,34 +77,48 @@ class AuthenticatedHandler(web.RequestHandler):
if user_id is None:
# prevent extra Invalid cookie sig warnings:
self.clear_cookie('username')
if not self.application.password:
if not self.application.password and not self.application.read_only:
user_id = 'anonymous'
return user_id
@property
def read_only(self):
if self.application.read_only:
if self.application.password:
return self.get_current_user() is None
else:
return True
else:
return False
class ProjectDashboardHandler(AuthenticatedHandler):
@web.authenticated
@authenticate_unless_readonly
def get(self):
nbm = self.application.notebook_manager
project = nbm.notebook_dir
self.render(
'projectdashboard.html', project=project,
base_project_url=u'/', base_kernel_url=u'/'
base_project_url=u'/', base_kernel_url=u'/',
read_only=self.read_only,
)
class LoginHandler(AuthenticatedHandler):
def get(self):
self.render('login.html', next='/')
self.render('login.html',
next=self.get_argument('next', default='/'),
read_only=self.read_only,
)
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()))
url = self.get_argument('next', default='/')
self.redirect(url)
self.redirect(self.get_argument('next', default='/'))
class NewHandler(AuthenticatedHandler):
@ -91,23 +132,26 @@ class NewHandler(AuthenticatedHandler):
'notebook.html', project=project,
notebook_id=notebook_id,
base_project_url=u'/', base_kernel_url=u'/',
kill_kernel=False
kill_kernel=False,
read_only=False,
)
class NamedNotebookHandler(AuthenticatedHandler):
@web.authenticated
@authenticate_unless_readonly
def get(self, notebook_id):
nbm = self.application.notebook_manager
project = nbm.notebook_dir
if not nbm.notebook_exists(notebook_id):
raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
self.render(
'notebook.html', project=project,
notebook_id=notebook_id,
base_project_url=u'/', base_kernel_url=u'/',
kill_kernel=False
kill_kernel=False,
read_only=self.read_only,
)
@ -363,8 +407,9 @@ class ShellHandler(AuthenticatedZMQStreamHandler):
class NotebookRootHandler(AuthenticatedHandler):
@web.authenticated
@authenticate_unless_readonly
def get(self):
nbm = self.application.notebook_manager
files = nbm.list_notebooks()
self.finish(jsonapi.dumps(files))
@ -387,11 +432,12 @@ class NotebookHandler(AuthenticatedHandler):
SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
@web.authenticated
@authenticate_unless_readonly
def get(self, notebook_id):
nbm = self.application.notebook_manager
format = self.get_argument('format', default='json')
last_mod, name, data = nbm.get_notebook(notebook_id, format)
if format == u'json':
self.set_header('Content-Type', 'application/json')
self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)

@ -110,6 +110,7 @@ class NotebookWebApplication(web.Application):
self.log = log
self.notebook_manager = notebook_manager
self.ipython_app = ipython_app
self.read_only = self.ipython_app.read_only
#-----------------------------------------------------------------------------
@ -121,11 +122,23 @@ flags['no-browser']=(
{'NotebookApp' : {'open_browser' : False}},
"Don't open the notebook in a browser after startup."
)
flags['read-only'] = (
{'NotebookApp' : {'read_only' : True}},
"""Allow read-only access to notebooks.
When using a password to protect the notebook server, this flag
allows unauthenticated clients to view the notebook list, and
individual notebooks, but not edit them, start kernels, or run
code.
If no password is set, the server will be entirely read-only.
"""
)
# the flags that are specific to the frontend
# these must be scrubbed before being passed to the kernel,
# or it will raise an error on unrecognized flags
notebook_flags = ['no-browser']
notebook_flags = ['no-browser', 'read-only']
aliases = dict(ipkernel_aliases)
@ -208,6 +221,10 @@ class NotebookApp(BaseIPythonApplication):
open_browser = Bool(True, config=True,
help="Whether to open in a browser after starting.")
read_only = Bool(False, config=True,
help="Whether to prevent editing/execution of notebooks."
)
def get_ws_url(self):
"""Return the WebSocket URL for this server."""
@ -288,7 +305,7 @@ class NotebookApp(BaseIPythonApplication):
# Try random ports centered around the default.
from random import randint
n = 50 # Max number of attempts, keep reasonably large.
for port in [self.port] + [self.port + randint(-2*n, 2*n) for i in range(n)]:
for port in range(self.port, self.port+5) + [self.port + randint(-2*n, 2*n) for i in range(n-5)]:
try:
self.http_server.listen(port, self.ip)
except socket.error, e:

@ -51,3 +51,12 @@ div#main_app {
padding: 0.2em 0.8em;
font-size: 77%;
}
span#login_widget {
float: right;
}
/* generic class for hidden objects */
.hidden {
display: none;
}

@ -15,6 +15,10 @@ var IPython = (function (IPython) {
var Cell = function (notebook) {
this.notebook = notebook;
this.read_only = false;
if (notebook){
this.read_only = notebook.read_only;
}
this.selected = false;
this.element = null;
this.create_element();

@ -37,6 +37,7 @@ var IPython = (function (IPython) {
indentUnit : 4,
mode: 'python',
theme: 'ipython',
readOnly: this.read_only,
onKeyEvent: $.proxy(this.handle_codemirror_keyevent,this)
});
input.append(input_area);

@ -0,0 +1,38 @@
//----------------------------------------------------------------------------
// Copyright (C) 2008-2011 The IPython Development Team
//
// Distributed under the terms of the BSD License. The full license is in
// the file COPYING, distributed as part of this software.
//----------------------------------------------------------------------------
//============================================================================
// Login button
//============================================================================
var IPython = (function (IPython) {
var LoginWidget = function (selector) {
this.selector = selector;
if (this.selector !== undefined) {
this.element = $(selector);
this.style();
this.bind_events();
}
};
LoginWidget.prototype.style = function () {
this.element.find('button#login').button();
};
LoginWidget.prototype.bind_events = function () {
var that = this;
this.element.find("button#login").click(function () {
window.location = "/login?next="+location.pathname;
});
};
// Set module variables
IPython.LoginWidget = LoginWidget;
return IPython;
}(IPython));

@ -14,6 +14,7 @@ var IPython = (function (IPython) {
var utils = IPython.utils;
var Notebook = function (selector) {
this.read_only = IPython.read_only;
this.element = $(selector);
this.element.scroll();
this.element.data("notebook", this);
@ -42,6 +43,7 @@ var IPython = (function (IPython) {
var that = this;
var end_space = $('<div class="end_space"></div>').height(150);
end_space.dblclick(function (e) {
if (that.read_only) return;
var ncells = that.ncells();
that.insert_code_cell_below(ncells-1);
});
@ -54,6 +56,7 @@ var IPython = (function (IPython) {
var that = this;
$(document).keydown(function (event) {
// console.log(event);
if (that.read_only) return;
if (event.which === 38) {
var cell = that.selected_cell();
if (cell.at_top()) {
@ -185,11 +188,11 @@ var IPython = (function (IPython) {
});
$(window).bind('beforeunload', function () {
var kill_kernel = $('#kill_kernel').prop('checked');
var kill_kernel = $('#kill_kernel').prop('checked');
if (kill_kernel) {
that.kernel.kill();
}
if (that.dirty) {
if (that.dirty && ! that.read_only) {
return "You have unsaved changes that will be lost if you leave this page.";
};
});
@ -975,14 +978,17 @@ var IPython = (function (IPython) {
Notebook.prototype.notebook_loaded = function (data, status, xhr) {
var allowed = xhr.getResponseHeader('Allow');
this.fromJSON(data);
if (this.ncells() === 0) {
this.insert_code_cell_below();
};
IPython.save_widget.status_save();
IPython.save_widget.set_notebook_name(data.metadata.name);
this.start_kernel();
this.dirty = false;
if (! this.read_only) {
this.start_kernel();
}
// fromJSON always selects the last cell inserted. We need to wait
// until that is done before scrolling to the top.
setTimeout(function () {
@ -991,7 +997,6 @@ var IPython = (function (IPython) {
}, 50);
};
IPython.Notebook = Notebook;

@ -80,7 +80,10 @@ var IPython = (function (IPython) {
var nbname = data[i].name;
var item = this.new_notebook_item(i);
this.add_link(notebook_id, nbname, item);
this.add_delete_button(item);
if (!IPython.read_only){
// hide delete buttons when readonly
this.add_delete_button(item);
}
};
};

@ -23,6 +23,7 @@ $(document).ready(function () {
}
});
IPython.markdown_converter = new Markdown.Converter();
IPython.read_only = $('meta[name=read_only]').attr("content") == 'True';
$('div#header').addClass('border-box-sizing');
$('div#main_app').addClass('border-box-sizing ui-widget ui-widget-content');
@ -33,6 +34,7 @@ $(document).ready(function () {
IPython.left_panel = new IPython.LeftPanel('div#left_panel', 'div#left_panel_splitter');
IPython.save_widget = new IPython.SaveWidget('span#save_widget');
IPython.quick_help = new IPython.QuickHelp('span#quick_help_area');
IPython.login_widget = new IPython.LoginWidget('span#login_widget');
IPython.print_widget = new IPython.PrintWidget('span#print_widget');
IPython.notebook = new IPython.Notebook('div#notebook');
IPython.kernel_status_widget = new IPython.KernelStatusWidget('#kernel_status');
@ -42,6 +44,21 @@ $(document).ready(function () {
// These have display: none in the css file and are made visible here to prevent FLOUC.
$('div#header').css('display','block');
if(IPython.read_only){
// hide various elements from read-only view
IPython.save_widget.element.find('button#save_notebook').addClass('hidden');
IPython.quick_help.element.addClass('hidden'); // shortcuts are disabled in read_only
$('button#new_notebook').addClass('hidden');
$('div#cell_section').addClass('hidden');
$('div#kernel_section').addClass('hidden');
$('span#login_widget').removeClass('hidden');
// left panel starts collapsed, but the collapse must happen after
// elements start drawing. Don't draw contents of the panel until
// after they are collapsed
IPython.left_panel.left_panel_element.css('visibility', 'hidden');
}
$('div#main_app').css('display','block');
// Perform these actions after the notebook has been loaded.
@ -52,6 +69,14 @@ $(document).ready(function () {
IPython.save_widget.update_url();
IPython.layout_manager.do_resize();
IPython.pager.collapse();
if(IPython.read_only){
// collapse the left panel on read-only
IPython.left_panel.collapse();
// and finally unhide the panel contents after collapse
setTimeout(function(){
IPython.left_panel.left_panel_element.css('visibility', 'visible');
}, 200)
}
},100);
});

@ -27,7 +27,16 @@ $(document).ready(function () {
$('div#left_panel').addClass('box-flex');
$('div#right_panel').addClass('box-flex');
IPython.read_only = $('meta[name=read_only]').attr("content") == 'True';
IPython.notebook_list = new IPython.NotebookList('div#notebook_list');
IPython.login_widget = new IPython.LoginWidget('span#login_widget');
if (IPython.read_only){
$('#new_notebook').addClass('hidden');
// unhide login button if it's relevant
$('span#login_widget').removeClass('hidden');
}
IPython.notebook_list.load_list();
// These have display: none in the css file and are made visible here to prevent FLOUC.

@ -33,7 +33,8 @@ var IPython = (function (IPython) {
indentUnit : 4,
mode: this.code_mirror_mode,
theme: 'default',
value: this.placeholder
value: this.placeholder,
readOnly: this.read_only,
});
// The tabindex=-1 makes this div focusable.
var render_area = $('<div/>').addClass('text_cell_render').
@ -65,6 +66,7 @@ var IPython = (function (IPython) {
TextCell.prototype.edit = function () {
if ( this.read_only ) return;
if (this.rendered === true) {
var text_cell = this.element;
var output = text_cell.find("div.text_cell_render");

@ -11,6 +11,8 @@
<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>

@ -40,7 +40,8 @@
<link rel="stylesheet" href="static/css/base.css" type="text/css" />
<link rel="stylesheet" href="static/css/notebook.css" type="text/css" />
<link rel="stylesheet" href="static/css/renderedhtml.css" type="text/css" />
<meta name="read_only" content="{{read_only}}"/>
</head>
@ -57,7 +58,10 @@
</span>
<span id="quick_help_area">
<button id="quick_help">Quick<u>H</u>elp</button>
</span>
</span>
<span id="login_widget" class="hidden">
<button id="login">Login</button>
</span>
<span id="kernel_status">Idle</span>
</div>
@ -278,6 +282,7 @@
<script src="static/js/layout.js" type="text/javascript" charset="utf-8"></script>
<script src="static/js/savewidget.js" type="text/javascript" charset="utf-8"></script>
<script src="static/js/quickhelp.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/pager.js" type="text/javascript" charset="utf-8"></script>
<script src="static/js/panelsection.js" type="text/javascript" charset="utf-8"></script>
<script src="static/js/printwidget.js" type="text/javascript" charset="utf-8"></script>

@ -12,6 +12,8 @@
<link rel="stylesheet" href="static/css/base.css" type="text/css" />
<link rel="stylesheet" href="static/css/projectdashboard.css" type="text/css" />
<meta name="read_only" content="{{read_only}}"/>
</head>
<body data-project={{project}} data-base-project-url={{base_project_url}}
@ -19,6 +21,9 @@
<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>
@ -54,6 +59,7 @@
<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>

Loading…
Cancel
Save