add read-only view for notebooks

When using a password, read-only mode allows unauthenticated users
read-only access to notebooks.  Editing, execution, etc. are not
allowed in read-only mode, but save/print functions are available.

No kernels are started until an authenticated user opens a notebook.
This commit is contained in:
MinRK 2011-10-25 22:14:09 -07:00
parent 9a7fda926d
commit a6de5947de
12 changed files with 114 additions and 22 deletions

View File

@ -46,6 +46,22 @@ def not_if_readonly(f, self, *args, **kwargs):
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.ipython_app.read_only:
return f(self, *args, **kwargs)
else:
return auth_f(self, *args, **kwargs)
#-----------------------------------------------------------------------------
# Top-level handlers
#-----------------------------------------------------------------------------
@ -68,7 +84,7 @@ class AuthenticatedHandler(web.RequestHandler):
class ProjectDashboardHandler(AuthenticatedHandler):
@web.authenticated
@authenticate_unless_readonly
def get(self):
nbm = self.application.notebook_manager
project = nbm.notebook_dir
@ -81,7 +97,7 @@ class ProjectDashboardHandler(AuthenticatedHandler):
class LoginHandler(AuthenticatedHandler):
def get(self):
self.render('login.html', next='/')
self.render('login.html', next=self.get_argument('next', default='/'))
def post(self):
pwd = self.get_argument('password', default=u'')
@ -93,7 +109,6 @@ class LoginHandler(AuthenticatedHandler):
class NewHandler(AuthenticatedHandler):
@not_if_readonly
@web.authenticated
def get(self):
nbm = self.application.notebook_manager
@ -109,7 +124,7 @@ class NewHandler(AuthenticatedHandler):
class NamedNotebookHandler(AuthenticatedHandler):
@web.authenticated
@authenticate_unless_readonly
def get(self, notebook_id):
nbm = self.application.notebook_manager
project = nbm.notebook_dir
@ -130,13 +145,11 @@ class NamedNotebookHandler(AuthenticatedHandler):
class MainKernelHandler(AuthenticatedHandler):
@not_if_readonly
@web.authenticated
def get(self):
km = self.application.kernel_manager
self.finish(jsonapi.dumps(km.kernel_ids))
@not_if_readonly
@web.authenticated
def post(self):
km = self.application.kernel_manager
@ -152,7 +165,6 @@ class KernelHandler(AuthenticatedHandler):
SUPPORTED_METHODS = ('DELETE')
@not_if_readonly
@web.authenticated
def delete(self, kernel_id):
km = self.application.kernel_manager
@ -163,7 +175,6 @@ class KernelHandler(AuthenticatedHandler):
class KernelActionHandler(AuthenticatedHandler):
@not_if_readonly
@web.authenticated
def post(self, kernel_id, action):
km = self.application.kernel_manager
@ -242,7 +253,6 @@ class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
except:
logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
@not_if_readonly
def on_first_message(self, msg):
self._inject_cookie_message(msg)
if self.get_current_user() is None:
@ -380,13 +390,19 @@ class ShellHandler(AuthenticatedZMQStreamHandler):
class NotebookRootHandler(AuthenticatedHandler):
@web.authenticated
@authenticate_unless_readonly
def get(self):
# communicate read-only via Allow header
if self.application.ipython_app.read_only and not self.get_current_user():
self.set_header('Allow', 'GET')
else:
self.set_header('Allow', ', '.join(self.SUPPORTED_METHODS))
nbm = self.application.notebook_manager
files = nbm.list_notebooks()
self.finish(jsonapi.dumps(files))
@not_if_readonly
@web.authenticated
def post(self):
nbm = self.application.notebook_manager
@ -405,11 +421,18 @@ 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)
# communicate read-only via Allow header
if self.application.ipython_app.read_only and not self.get_current_user():
self.set_header('Allow', 'GET')
else:
self.set_header('Allow', ', '.join(self.SUPPORTED_METHODS))
if format == u'json':
self.set_header('Content-Type', 'application/json')
self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
@ -419,7 +442,6 @@ class NotebookHandler(AuthenticatedHandler):
self.set_header('Last-Modified', last_mod)
self.finish(data)
@not_if_readonly
@web.authenticated
def put(self, notebook_id):
nbm = self.application.notebook_manager
@ -429,7 +451,6 @@ class NotebookHandler(AuthenticatedHandler):
self.set_status(204)
self.finish()
@not_if_readonly
@web.authenticated
def delete(self, notebook_id):
nbm = self.application.notebook_manager

View File

@ -118,13 +118,22 @@ flags['no-browser']=(
)
flags['read-only'] = (
{'NotebookApp' : {'read_only' : True}},
"Launch the Notebook server in read-only mode, not allowing execution or editing"
"""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.
This flag only makes sense in conjunction with setting a password,
via the ``NotebookApp.password`` configurable.
"""
)
# 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)

View File

@ -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;
}

View File

@ -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();

View File

@ -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);

View File

@ -14,6 +14,7 @@ var IPython = (function (IPython) {
var utils = IPython.utils;
var Notebook = function (selector) {
this.read_only = false;
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,26 @@ var IPython = (function (IPython) {
Notebook.prototype.notebook_loaded = function (data, status, xhr) {
var allowed = xhr.getResponseHeader('Allow');
if (allowed && allowed.indexOf('PUT') == -1){
this.read_only = true;
// unhide login button if it's relevant
$('span#login_widget').removeClass('hidden');
}else{
this.read_only = false;
}
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.handle_read_only();
}else{
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 () {
@ -992,6 +1007,15 @@ var IPython = (function (IPython) {
};
Notebook.prototype.handle_read_only = function(){
IPython.left_panel.collapse();
IPython.save_widget.element.find('button#save_notebook').addClass('hidden');
$('button#new_notebook').addClass('hidden');
$('div#cell_section').addClass('hidden');
$('div#kernel_section').addClass('hidden');
}
IPython.Notebook = Notebook;

View File

@ -73,6 +73,15 @@ var IPython = (function (IPython) {
NotebookList.prototype.list_loaded = function (data, status, xhr) {
var allowed = xhr.getResponseHeader('Allow');
if (allowed && allowed.indexOf('PUT') == -1){
this.read_only = true;
$('#new_notebook').addClass('hidden');
// unhide login button if it's relevant
$('span#login_widget').removeClass('hidden');
}else{
this.read_only = false;
}
var len = data.length;
// Todo: remove old children
for (var i=0; i<len; i++) {
@ -80,7 +89,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 (!this.read_only){
// hide delete buttons when readonly
this.add_delete_button(item);
}
};
};

View File

@ -33,6 +33,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');

View File

@ -28,6 +28,7 @@ $(document).ready(function () {
$('div#right_panel').addClass('box-flex');
IPython.notebook_list = new IPython.NotebookList('div#notebook_list');
IPython.login_widget = new IPython.LoginWidget('span#login_widget');
IPython.notebook_list.load_list();
// These have display: none in the css file and are made visible here to prevent FLOUC.

View File

@ -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");

View File

@ -57,7 +57,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 +281,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>

View File

@ -19,6 +19,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 +57,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>