Skip to content

Commit

Permalink
Merge pull request ipython#931 from minrk/readonly
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
fperez committed Oct 28, 2011
2 parents 6fb30ae + 81edd9f commit 80e60eb
Show file tree
Hide file tree
Showing 15 changed files with 197 additions and 23 deletions.
68 changes: 57 additions & 11 deletions IPython/frontend/html/notebook/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from zmq.eventloop import ioloop
from zmq.utils import jsonapi

from IPython.external.decorator import decorator
from IPython.zmq.session import Session

try:
Expand All @@ -34,6 +35,32 @@
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
Expand All @@ -50,34 +77,48 @@ def get_current_user(self):
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):
Expand All @@ -91,23 +132,26 @@ def get(self):
'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,
)


Expand Down Expand Up @@ -363,8 +407,9 @@ def on_close(self):

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))
Expand All @@ -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)
Expand Down
21 changes: 19 additions & 2 deletions IPython/frontend/html/notebook/notebookapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def __init__(self, ipython_app, kernel_manager, notebook_manager, log):
self.log = log
self.notebook_manager = notebook_manager
self.ipython_app = ipython_app
self.read_only = self.ipython_app.read_only


#-----------------------------------------------------------------------------
Expand All @@ -121,11 +122,23 @@ def __init__(self, ipython_app, kernel_manager, notebook_manager, log):
{'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)

Expand Down Expand Up @@ -208,6 +221,10 @@ def _ip_changed(self, name, old, new):

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."""
Expand Down Expand Up @@ -288,7 +305,7 @@ def initialize(self, argv=None):
# 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:
Expand Down
9 changes: 9 additions & 0 deletions IPython/frontend/html/notebook/static/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
4 changes: 4 additions & 0 deletions IPython/frontend/html/notebook/static/js/cell.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions IPython/frontend/html/notebook/static/js/codecell.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 4 additions & 2 deletions IPython/frontend/html/notebook/static/js/leftpanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,10 @@ var IPython = (function (IPython) {

LeftPanel.prototype.create_children = function () {
this.notebook_section = new IPython.NotebookSection('div#notebook_section');
this.cell_section = new IPython.CellSection('div#cell_section');
this.kernel_section = new IPython.KernelSection('div#kernel_section');
if (! IPython.read_only){
this.cell_section = new IPython.CellSection('div#cell_section');
this.kernel_section = new IPython.KernelSection('div#kernel_section');
}
this.help_section = new IPython.HelpSection('div#help_section');
}

Expand Down
38 changes: 38 additions & 0 deletions IPython/frontend/html/notebook/static/js/loginwidget.js
Original file line number Diff line number Diff line change
@@ -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));
13 changes: 9 additions & 4 deletions IPython/frontend/html/notebook/static/js/notebook.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
});
Expand All @@ -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()) {
Expand Down Expand Up @@ -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.";
};
});
Expand Down Expand Up @@ -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 () {
Expand All @@ -991,7 +997,6 @@ var IPython = (function (IPython) {
}, 50);
};


IPython.Notebook = Notebook;


Expand Down
5 changes: 4 additions & 1 deletion IPython/frontend/html/notebook/static/js/notebooklist.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};
};

Expand Down
Loading

0 comments on commit 80e60eb

Please sign in to comment.