diff --git a/README.rst b/README.rst index 2f2a59498..c4fa69218 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,5 @@ +.. nodoctest + .. This README does not explain how to handle installation into versions of Sage which do not yet ship the flask notebook, as the packaging of the notebook's dependencies is still in flux. Please see diff --git a/flask_version/authentication.py b/flask_version/authentication.py index 2b7b6e205..0b4b153ca 100644 --- a/flask_version/authentication.py +++ b/flask_version/authentication.py @@ -21,7 +21,6 @@ def lookup_current_user(): def login(template_dict={}): from sagenb.misc.misc import SAGE_VERSION template_dict.update({'accounts': g.notebook.user_manager().get_accounts(), - 'default_user': g.notebook.user_manager().default_user(), 'recovery': g.notebook.conf()['email'], 'next': request.values.get('next', ''), 'sage_version': SAGE_VERSION, @@ -94,6 +93,8 @@ def logout(): @authentication.route('/register', methods = ['GET','POST']) @with_lock def register(): + if not g.notebook.user_manager().get_accounts(): + return redirect(url_for('base.index')) from sagenb.notebook.misc import is_valid_username, is_valid_password, \ is_valid_email, do_passwords_match from sagenb.notebook.challenge import challenge @@ -243,7 +244,6 @@ def register(): # Go to the login page. from sagenb.misc.misc import SAGE_VERSION template_dict = {'accounts': g.notebook.user_manager().get_accounts(), - 'default_user': g.notebook.user_manager().default_user(), 'welcome_user': username, 'recovery': g.notebook.conf()['email'], 'sage_version': SAGE_VERSION} diff --git a/flask_version/base.py b/flask_version/base.py index 0920e7677..fe0400b33 100755 --- a/flask_version/base.py +++ b/flask_version/base.py @@ -22,23 +22,23 @@ def __init__(self, *args, **kwds): self.root_path = SAGENB_ROOT - self.add_static_path('/css', os.path.join(DATA, "sage", "css")) + self.add_static_path('/css', os.path.join(DATA, "sage", "css")) self.add_static_path('/images', os.path.join(DATA, "sage", "images")) self.add_static_path('/javascript', DATA) self.add_static_path('/static', DATA) self.add_static_path('/java', DATA) self.add_static_path('/java/jmol', os.path.join(os.environ["SAGE_ROOT"],"local","share","jmol")) - import mimetypes + import mimetypes mimetypes.add_type('text/plain','.jmol') - + ####### # Doc # ####### #These "should" be in doc.py DOC = os.path.join(SAGE_DOC, 'output', 'html', 'en') self.add_static_path('/pdf', os.path.join(SAGE_DOC, 'output', 'pdf')) - self.add_static_path('/doc/static', DOC) + self.add_static_path('/doc/static', DOC) #self.add_static_path('/doc/static/reference', os.path.join(SAGE_DOC, 'reference')) def create_jinja_environment(self): @@ -108,14 +108,14 @@ def index(): response.set_cookie('nb_session_%s'%g.notebook.port) response.set_cookie('cookie_test_%s'%g.notebook.port, expires=1) return response - + from authentication import login - + if current_app.startup_token is not None and 'startup_token' in request.args: if request.args['startup_token'] == current_app.startup_token: - g.username = session['username'] = 'admin' + g.username = session['username'] = 'admin' session.modified = True - current_app.startup_token = -1 + current_app.startup_token = None return index() return login() @@ -192,7 +192,7 @@ def keyboard_js(browser_os): ############### @base.route('/css/main.css') def main_css(): - from sagenb.notebook.css import css + from sagenb.notebook.css import css data,datahash = css() if request.environ.get('HTTP_IF_NONE_MATCH', None) == datahash: response = make_response('',304) @@ -260,7 +260,7 @@ def create_or_login(resp): session['username'] = g.username = username session.modified = True except KeyError: - session['openid_response'] = resp + session['openid_response'] = resp session.modified = True return redirect(url_for('set_profiles')) return redirect(request.values.get('next', url_for('base.index'))) @@ -272,7 +272,7 @@ def set_profiles(): from sagenb.notebook.challenge import challenge - + show_challenge=g.notebook.conf()['challenge'] if show_challenge: chal = challenge(g.notebook.conf(), @@ -326,14 +326,14 @@ def set_profiles(): if not is_valid_username(username): parse_dict['username_invalid'] = True - raise ValueError + raise ValueError if g.notebook.user_manager().user_exists(username): parse_dict['username_taken'] = True - raise ValueError + raise ValueError if not is_valid_email(request.form.get('email')): parse_dict['email_invalid'] = True raise ValueError - try: + try: new_user = User(username, '', email = resp.email, account_type='user') g.notebook.user_manager().add_user_object(new_user) except ValueError: @@ -357,7 +357,7 @@ def set_profiles(): def init_updates(): global save_interval, idle_interval, last_save_time, last_idle_time from sagenb.misc.misc import walltime - + save_interval = notebook.conf()['save_interval'] idle_interval = notebook.conf()['idle_check_interval'] last_save_time = walltime() @@ -410,7 +410,7 @@ def create_app(path_to_notebook, *args, **kwds): """ global notebook startup_token = kwds.pop('startup_token', None) - + ############# # OLD STUFF # ############# @@ -436,7 +436,7 @@ def set_notebook_object(): #################################### babel = Babel(app, default_locale=notebook.conf()['default_language'], default_timezone='UTC', - date_formats=None, configure_jinja=True) + date_formats=None, configure_jinja=True) ######################## # Register the modules # @@ -444,7 +444,7 @@ def set_notebook_object(): app.register_blueprint(base) from worksheet_listing import worksheet_listing - app.register_blueprint(worksheet_listing) + app.register_blueprint(worksheet_listing) from admin import admin app.register_blueprint(admin) @@ -473,10 +473,10 @@ def autoindex(path='.'): if os.path.isfile(filename): from cgi import escape src = escape(open(filename).read().decode('utf-8','ignore')) - if (os.path.splitext(filename)[1] in + if (os.path.splitext(filename)[1] in ['.py','.c','.cc','.h','.hh','.pyx','.pxd']): return render_template(os.path.join('html', 'source_code.html'), - src_filename=path, + src_filename=path, src=src, username = g.username) return src return idx.render_autoindex(path) diff --git a/flask_version/decorators.py b/flask_version/decorators.py index 00a4b3282..7ef65799d 100644 --- a/flask_version/decorators.py +++ b/flask_version/decorators.py @@ -10,18 +10,13 @@ def login_required(f): @wraps(f) def wrapper(*args, **kwds): if 'username' not in session: - if not g.notebook.conf()['require_login']: - g.username = session['username'] = 'admin' - session.modified = True + #XXX: Do we have to specify this for the publised + #worksheets here? + if request.path.startswith('/home/_sage_/'): + g.username = 'guest' return f(*args, **kwds) else: - #XXX: Do we have to specify this for the publised - #worksheets here? - if request.path.startswith('/home/_sage_/'): - g.username = 'guest' - return f(*args, **kwds) - else: - return redirect(url_for('base.index', next=request.url)) + return redirect(url_for('base.index', next=request.url)) else: g.username = session['username'] return f(*args, **kwds) diff --git a/flask_version/settings.py b/flask_version/settings.py index 00057d66e..65b2a6c25 100644 --- a/flask_version/settings.py +++ b/flask_version/settings.py @@ -71,6 +71,7 @@ def settings_page(): td['email_confirmed'] = 'Not confirmed' td['admin'] = nu.is_admin() + td['form_email'] = nu.may_change_email() return render_template(os.path.join('html', 'settings', 'account_settings.html'), **td) diff --git a/flask_version/worksheet_listing.py b/flask_version/worksheet_listing.py index f9493bf4b..32c68e7c5 100644 --- a/flask_version/worksheet_listing.py +++ b/flask_version/worksheet_listing.py @@ -31,18 +31,11 @@ def render_worksheet_list(args, pub, username): from sagenb.notebook.notebook import sort_worksheet_list from sagenb.misc.misc import unicode_str, SAGE_VERSION - from sagenb.notebook.pagination import Paginator - + typ = args['typ'] if 'typ' in args else 'active' search = unicode_str(args['search']) if 'search' in args else None sort = args['sort'] if 'sort' in args else 'last_edited' reverse = (args['reverse'] == 'True') if 'reverse' in args else False - page = 1 - if 'page' in args: - try: - page = int(args['page']) - except ValueError as E: - print "Error improper page input", E try: if not pub: @@ -56,19 +49,6 @@ def render_worksheet_list(args, pub, username): print "Error displaying worksheet listing: ", E return current_app.message(_("Error displaying worksheet listing.")) - paginator = Paginator(worksheets, 25) #25 is number of items per page, change this value to change the number of objects per page. - - try: - pages = paginator.page(page) - except PageNotAnInteger: - # If page is not an integer, deliver first page. - pages = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - pages = paginator.page(paginator.num_pages) - - worksheets = pages.object_list - worksheet_filenames = [x.filename() for x in worksheets] if pub and (not username or username == tuple([])): diff --git a/sagenb/data/sage/html/login.html b/sagenb/data/sage/html/login.html index e090ee9a4..c16a1b371 100644 --- a/sagenb/data/sage/html/login.html +++ b/sagenb/data/sage/html/login.html @@ -64,7 +64,7 @@

Sign into the Sage Notebook v{{ sage_version }}

- + {% if username_error %} {{ gettext('Error') }}: {{ gettext('Username is not in the system') }} {% endif %} diff --git a/sagenb/data/sage/html/notebook/worksheet_share.html b/sagenb/data/sage/html/notebook/worksheet_share.html index 42ff38c6b..36c120f3d 100644 --- a/sagenb/data/sage/html/notebook/worksheet_share.html +++ b/sagenb/data/sage/html/notebook/worksheet_share.html @@ -13,6 +13,16 @@ {% set select = "share" %} {% block after_sharebar %} + + {% if not (notebook.user_manager().user_is_admin(username) or username == worksheet.owner()) %} {{ gettext('Only the owner of a worksheet is allowed to share it. You can do whatever you want if you make your own copy.') }} {% else %} @@ -23,5 +33,38 @@ + +
+{% if lookup %} +
+

{{ gettext('Search results:') }} {% if lookup_result %} + {% for u in lookup_result %} + + {{ u }} + + {% endfor %} + {% else %} {{ gettext('sorry, no match found') }} + {% endif %}

+
+{% else %} + {{ gettext('Search Users') }} +{% endif %} +
+ + +
+ +{% if other_users %} +
+

+ {{ gettext('Known Sage Users:') }} + {% for u in other_users %} + + {{ u }} + + {% endfor %} +

+{% endif %} + {% endif %} {% endblock %} diff --git a/sagenb/data/sage/html/settings/account_settings.html b/sagenb/data/sage/html/settings/account_settings.html index b0c46a123..82f30637b 100644 --- a/sagenb/data/sage/html/settings/account_settings.html +++ b/sagenb/data/sage/html/settings/account_settings.html @@ -20,6 +20,7 @@

{{ gettext('Change Auto-Save Interval') }}

+ {% if form_password %}

{{ gettext('Change Password') }}

@@ -40,11 +41,12 @@

{{ gettext('Change Password') }}

+ {% endif %} - {% if true %} + {% if form_email %}

{{ gettext('Change E-mail Address') }}

- +
@@ -56,6 +58,12 @@

{{ gettext('Change E-mail Address') }}

+ {% else %} +
+
+ {{ gettext('Your Email') }}: {{ email_address }} +
+
{% endif %} {%- if auto_table %} diff --git a/sagenb/data/sage/html/worksheet_listing.html b/sagenb/data/sage/html/worksheet_listing.html index 89b4a03e3..6a5ac9a68 100644 --- a/sagenb/data/sage/html/worksheet_listing.html +++ b/sagenb/data/sage/html/worksheet_listing.html @@ -25,60 +25,6 @@ {% block javascript %} {% if not pub %} - @@ -279,33 +225,4 @@ {% endif %} - {% endblock %} diff --git a/sagenb/data/sage/js/notebook_lib.js b/sagenb/data/sage/js/notebook_lib.js index 23590a4b2..a4cc2901f 100644 --- a/sagenb/data/sage/js/notebook_lib.js +++ b/sagenb/data/sage/js/notebook_lib.js @@ -3002,9 +3002,6 @@ function evaluate_cell(id, newcell) { return; } - queue_id_list.push(id); - cell_set_running(id); - // Request a new cell to insert after this one? newcell = (newcell || (id === extreme_compute_cell(-1))) ? 1 : 0; if (evaluating_all) { diff --git a/sagenb/misc/sageinspect.py b/sagenb/misc/sageinspect.py index 9a8403cd8..416febbfd 100644 --- a/sagenb/misc/sageinspect.py +++ b/sagenb/misc/sageinspect.py @@ -648,11 +648,15 @@ def sage_getargspec(obj): EXAMPLES:: sage: from sagenb.misc.sageinspect import sage_getargspec + sage: def f(x, y, z=1, t=2, *args, **keywords): + ... pass + sage: sage_getargspec(f) + (['x', 'y', 'z', 't'], 'args', 'keywords', (1, 2)) + + We now run sage_getargspec on some functions from the Sage library:: + sage: sage_getargspec(identity_matrix) (['ring', 'n', 'sparse'], None, None, (0, False)) - sage: sage_getargspec(Poset) - (['data', 'element_labels', 'cover_relations', 'category', 'facade', - 'key'], None, None, (None, None, False, None, None, None)) sage: sage_getargspec(factor) (['n', 'proof', 'int_', 'algorithm', 'verbose'], None, diff --git a/sagenb/notebook/notebook.py b/sagenb/notebook/notebook.py index 17d816592..ffde8285b 100644 --- a/sagenb/notebook/notebook.py +++ b/sagenb/notebook/notebook.py @@ -142,8 +142,8 @@ def __init__(self, dir, user_manager = None): # Worksheet has never been saved before, so the server conf doesn't exist. self.__worksheets = WorksheetDict(self) - from user_manager import SimpleUserManager, OpenIDUserManager - self._user_manager = OpenIDUserManager(conf=self.conf()) if user_manager is None else user_manager + from user_manager import ExtAuthUserManager + self._user_manager = ExtAuthUserManager(conf=self.conf()) if user_manager is None else user_manager # Set the list of users try: @@ -278,7 +278,7 @@ def user(self, username): """ return self.user_manager().user(username) - def valid_login_names(self): + def known_users(self): """ Return a list of users that can log in. @@ -290,15 +290,15 @@ def valid_login_names(self): sage: nb = sagenb.notebook.notebook.Notebook(tmp_dir()+'.sagenb') sage: nb.create_default_users('password') - sage: nb.valid_login_names() + sage: nb.known_users() ['admin'] sage: nb.user_manager().add_user('Mark', 'password', '', force=True) sage: nb.user_manager().add_user('Sarah', 'password', '', force=True) sage: nb.user_manager().add_user('David', 'password', '', force=True) - sage: sorted(nb.valid_login_names()) + sage: sorted(nb.known_users()) ['David', 'Mark', 'Sarah', 'admin'] """ - return self.user_manager().valid_login_names() + return self.user_manager().known_users() ########################################################## # Publishing worksheets @@ -682,7 +682,7 @@ def set_color(self, color): def user_history(self, username): if not hasattr(self, '_user_history'): self._user_history = {} - if 'username' in self._user_history: + if username in self._user_history: return self._user_history[username] history = [] for hunk in self.__storage.load_user_history(username): @@ -1363,7 +1363,7 @@ def html_specific_revision(self, username, ws, rev): username = username, rev = rev, prev_rev = prev_rev, next_rev = next_rev, time_ago = time_ago) - def html_share(self, worksheet, username): + def html_share(self, worksheet, username, lookup=None): r""" Return the HTML for the "share" page of a worksheet. @@ -1384,10 +1384,19 @@ def html_share(self, worksheet, username): sage: nb.html_share(W, 'admin') u'...currently shared...add or remove collaborators...' """ + + lookup_result = self.user_manager().user_lookup(lookup) if lookup else None + if lookup_result is not None: + lookup_result.sort(lambda x,y: cmp(x.lower(), y.lower())) + if username in lookup_result: + lookup_result.remove(username) + return template(os.path.join("html", "notebook", "worksheet_share.html"), worksheet = worksheet, notebook = self, - username = username) + username = username, + lookup = lookup, + lookup_result = lookup_result) def html_download_or_delete_datafile(self, ws, username, filename): r""" diff --git a/sagenb/notebook/notebook_object.py b/sagenb/notebook/notebook_object.py index 8ec1797d9..1c81fade8 100644 --- a/sagenb/notebook/notebook_object.py +++ b/sagenb/notebook/notebook_object.py @@ -35,9 +35,7 @@ class NotebookObject: - ``interface`` -- string (default: ``'localhost'``), address of network interface to listen on; give ``''`` to listen on - all interfaces. You may use ``address`` here for backwards - compatibility, but this is deprecated and will be removed in - the future. + all interfaces. - ``port_tries`` -- integer (default: ``0``), number of additional ports to try if the first one doesn't work (*not* @@ -50,10 +48,6 @@ class NotebookObject: Sage yourself, have the OpenSSL development libraries installed. *Highly recommended!* - - ``require_login`` -- boolean (default: ``True``) if True - login is required else web user is automatically logged in - as user admin. - - ``reset`` -- boolean (default: ``False``) if True allows you to set the admin password. Use this if you forget your admin password. @@ -74,10 +68,10 @@ class NotebookObject: user_manager.add_user("username", "password", "email@place", "user") nb.save() - - ``open_viewer`` -- boolean (default: True) whether to pop up - a web browser. You can override the default browser by - setting the ``SAGE_BROWSER`` environment variable, e.g., by - putting + - ``automatic_login`` -- boolean (default: True) whether to pop up + a web browser and automatically log into the server as admin. You can + override the default browser by setting the ``SAGE_BROWSER`` environment + variable, e.g., by putting :: @@ -85,6 +79,13 @@ class NotebookObject: in the file .bashrc in your home directory. + .. warning:: + + If you are running a server for others to log into, set `automatic_login=False`. + Otherwise, all of the worksheets on the entire server will be loaded when the server + automatically logs into the admin account. + + - ``timeout`` -- integer (default: 0) seconds until idle worksheet sessions automatically timeout, i.e., the corresponding Sage session terminates. 0 means "never @@ -101,6 +102,11 @@ class NotebookObject: If you have problems with the server certificate hostname not matching, do ``notebook.setup()``. + + .. note:: + + The ``require_login`` option has been removed. Use `automatic_login` to control + automatic logins instead---`automatic_login=False` corresponds to `require_login=True`. EXAMPLES: @@ -117,24 +123,14 @@ class NotebookObject: administrator password. Use this to login. NOTE: You may have to run ``notebook.setup()`` again and change the hostname. - 3. I just want to run the server locally on my laptop and do not - want to be bothered with having to log in:: - - notebook(require_login=False) - - Use this ONLY if you are *absolutely certain* that you are the - only user logged into the laptop and do not have to worry about - somebody else using the Sage notebook on localhost and deleting - your files. - - 4. I want to create a Sage notebook server that is open to anybody + 3. I want to create a Sage notebook server that is open to anybody in the world to create new accounts. To run the Sage notebook publicly (1) at a minimum run it from a chroot jail or inside a virtual machine (see `this Sage wiki page`_) and (2) use a command like:: notebook(interface='', server_pool=['sage1@localhost'], - ulimit='-v 500000', accounts=True) + ulimit='-v 500000', accounts=True, automatic_login=False) The server_pool option specifies that worksheet processes run as a separate user. The ulimit option restricts the memory @@ -168,6 +164,14 @@ class NotebookObject: now, so if the machines are separate the server machine must NSF export ``/tmp``. + - ``server`` -- string ("twistd" (default) or "flask"). The server + to use to server content. + + - ``profile`` -- True, False, or file prefix (default: False - no profiling), + If True, profiling is saved to a randomly-named file like `sagenb-*-profile*.stats` + in the $DOT_SAGE directory. If a string, that string is used as a + prefix for the pstats data file. + - ``ulimit`` -- string (initial default: None -- leave as is), if given and ``server_pool`` is also given, the worksheet processes are run with these constraints. See the ulimit @@ -205,7 +209,7 @@ class NotebookObject: def __call__(self, *args, **kwds): return self.notebook(*args, **kwds) - notebook = run_notebook.notebook_twisted + notebook = run_notebook.notebook_run setup = run_notebook.notebook_setup notebook = NotebookObject() @@ -254,7 +258,7 @@ def test_notebook(admin_passwd, secure=False, directory=None, port=8050, nb.save() p = notebook(directory=directory, accounts=True, secure=secure, port=port, - interface=interface, open_viewer=False, fork=True, quiet=True) + interface=interface, automatic_login=False, fork=True, quiet=True) p.expect("Starting factory") def dispose(): try: diff --git a/sagenb/notebook/pagination.py b/sagenb/notebook/pagination.py deleted file mode 100644 index bcb884357..000000000 --- a/sagenb/notebook/pagination.py +++ /dev/null @@ -1,125 +0,0 @@ -from math import ceil -class InvalidPage(Exception): - pass - -class PageNotAnInteger(InvalidPage): - pass - -class EmptyPage(InvalidPage): - pass - -class Paginator(object): - def __init__(self, object_list, per_page, orphans=0, allow_empty_first_page=True): - self.object_list = object_list - self.per_page = per_page - self.orphans = orphans - self.allow_empty_first_page = allow_empty_first_page - self._num_pages = self._count = None - - def validate_number(self, number): - "Validates the given 1-based page number." - try: - number = int(number) - except ValueError: - raise PageNotAnInteger('That page number is not an integer') - if number < 1: - raise EmptyPage('That page number is less than 1') - if number > self.num_pages: - if number == 1 and self.allow_empty_first_page: - pass - else: - raise EmptyPage('That page contains no results') - return number - - def page(self, number): - "Returns a Page object for the given 1-based page number." - number = self.validate_number(number) - bottom = (number - 1) * self.per_page - top = bottom + self.per_page - if top + self.orphans >= self.count: - top = self.count - return Page(self.object_list[bottom:top], number, self) - - def _get_count(self): - "Returns the total number of objects, across all pages." - if self._count is None: - try: - self._count = self.object_list.count() - except (AttributeError, TypeError): - # AttributeError if object_list has no count() method. - # TypeError if object_list.count() requires arguments - # (i.e. is of type list). - self._count = len(self.object_list) - return self._count - count = property(_get_count) - - def _get_num_pages(self): - "Returns the total number of pages." - if self._num_pages is None: - if self.count == 0 and not self.allow_empty_first_page: - self._num_pages = 0 - else: - hits = max(1, self.count - self.orphans) - self._num_pages = int(ceil(hits / float(self.per_page))) - return self._num_pages - num_pages = property(_get_num_pages) - - def page_range(self, page=1): - """ - Returns a 1-based range of pages for iterating through within - a template for loop. - """ - bottom = 1 - top = self.num_pages+1 - if(page <= self.num_pages and page > 0): - if(page+5 < self.num_pages+1): - top = page+5 - if(page-5 > 0): - bottom = page-5 - return range(bottom, top) - -QuerySetPaginator = Paginator # For backwards-compatibility. - -class Page(object): - def __init__(self, object_list, number, paginator): - self.object_list = object_list - self.number = number - self.paginator = paginator - - def __repr__(self): - return '' % (self.number, self.paginator.num_pages) - - def has_next(self): - return self.number < self.paginator.num_pages - - def has_previous(self): - return self.number > 1 - - def has_other_pages(self): - return self.has_previous() or self.has_next() - - def next_page_number(self): - return self.number + 1 - - def previous_page_number(self): - return self.number - 1 - - def start_index(self): - """ - Returns the 1-based index of the first object on this page, - relative to total objects in the paginator. - """ - # Special case, return zero if no items. - if self.paginator.count == 0: - return 0 - return (self.paginator.per_page * (self.number - 1)) + 1 - - def end_index(self): - """ - Returns the 1-based index of the last object on this page, - relative to total objects found (hits). - """ - # Special case for the last page because there can be orphans. - if self.number == self.paginator.num_pages: - return self.paginator.count - return self.number * self.paginator.per_page \ No newline at end of file diff --git a/sagenb/notebook/run_notebook.py b/sagenb/notebook/run_notebook.py index da1a13664..e7e7aede4 100644 --- a/sagenb/notebook/run_notebook.py +++ b/sagenb/notebook/run_notebook.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -* """nodoctest -Server the Sage Notebook. +Serve the Sage Notebook. """ ############################################################################# @@ -10,9 +10,6 @@ # http://www.gnu.org/licenses/ ############################################################################# -# From 5.0 forward, no longer supporting GnuTLS, so only use SSL protocol from OpenSSL -protocol = 'ssl' - # System libraries import getpass import os @@ -35,57 +32,189 @@ public_pem = os.path.join(conf_path, 'public.pem') template_file = os.path.join(conf_path, 'cert.cfg') -FLASK_NOTEBOOK_CONFIG = """ -#################################################################### + +class NotebookRun(object): + config_stub=""" +#################################################################### # WARNING -- Do not edit this file! It is autogenerated each time # the notebook(...) command is executed. -# See http://twistedmatrix.com/documents/current/web/howto/using-twistedweb.html -# (Serving WSGI Applications) for the basic ideas of the below code #################################################################### -from twisted.internet import reactor - -# Now set things up and start the notebook -import sagenb.notebook.notebook -sagenb.notebook.notebook.JSMATH=True -import sagenb.notebook.notebook as notebook -import sagenb.notebook.worksheet as worksheet - -import sagenb.notebook.misc as misc +import sagenb.notebook.misc +sagenb.notebook.misc.DIR = %(cwd)r #We should really get rid of this! -misc.DIR = %(cwd)r #We should really get rid of this! +######### +# Flask # +######### +import os, sys, random +flask_dir = os.path.join(os.environ['SAGE_ROOT'], 'devel', 'sagenb', 'flask_version') +sys.path.append(flask_dir) +import base as flask_base +opts={} +startup_token = '{0:x}'.format(random.randint(0, 2**128)) +if %(automatic_login)s: + opts['startup_token'] = startup_token +flask_app = flask_base.create_app(%(notebook_opts)s, **opts) +sys.path.remove(flask_dir) -import signal, sys, random def save_notebook(notebook): - from twisted.internet.error import ReactorNotRunning print "Quitting all running worksheets..." notebook.quit() print "Saving notebook..." notebook.save() print "Notebook cleanly saved." - + +""" + def prepare_kwds(self, kw): + import os + kw['absdirectory']=os.path.abspath(kw['directory']) + kw['notebook_opts'] = '"%(absdirectory)s",interface="%(interface)s",port=%(port)s,secure=%(secure)s'%kw + kw['hostname'] = kw['interface'] if kw['interface'] else 'localhost' + if kw['automatic_login']: + kw['start_path'] = "'/?startup_token=%s' % startup_token" + kw['open_page'] = "from sagenb.misc.misc import open_page; open_page('%(hostname)s', %(port)s, %(secure)s, %(start_path)s)" % kw + else: + kw['open_page'] = '' + + + def profile_file(self, profile): + import random + _id=random.random() + if isinstance(profile, basestring): + profilefile = profile+'%s.stats'%_id + else: + profilefile = 'sagenb-%s-profile-%s.stats'%(self.name,_id) + return profilefile + + def run_command(self, kw): + raise NotImplementedError + +class NotebookRunuWSGI(NotebookRun): + name="uWSGI" + uWSGI_NOTEBOOK_CONFIG = """ +import atexit +from functools import partial +atexit.register(partial(save_notebook,flask_base.notebook)) +%(open_page)s +""" + def run_command(self, kw): + """Run a uWSGI webserver.""" + # TODO: Check to see if server is running already (PID file?) + self.prepare_kwds(kw) + run_file = os.path.join(kw['directory'], 'run_uwsgi') + + with open(run_file, 'w') as script: + script.write((self.config_stub+self.uWSGI_NOTEBOOK_CONFIG)%kw) + + port=kw['port'] + pidfile=kw['pidfile'] + cmd = 'uwsgi --single-interpreter --socket-timeout 30 --http-timeout 30 --listen 300 --http-socket :%s --file %s --callable flask_app --pidfile %s' % (port, run_file, pidfile) + # Comment out the line below to turn on request logging + cmd += ' --disable-logging' + cmd += ' --threads 4 --enable-threads' + return cmd + +class NotebookRunFlask(NotebookRun): + name="flask" + FLASK_NOTEBOOK_CONFIG = """ +import os +with open(%(pidfile)r, 'w') as pidfile: + pidfile.write(str(os.getpid())) + +if %(secure)s: + from OpenSSL import SSL + ssl_context = SSL.Context(SSL.SSLv23_METHOD) + ssl_context.use_privatekey_file(%(private_pem)r) + ssl_context.use_certificate_file(%(public_pem)r) +else: + ssl_context = None + +import logging +logger=logging.getLogger('werkzeug') +logger.setLevel(logging.WARNING) +#logger.setLevel(logging.INFO) # to see page requests +#logger.setLevel(logging.DEBUG) +logger.addHandler(logging.StreamHandler()) + +if %(secure)s: + # Monkey-patch werkzeug so that it works with pyOpenSSL and Python 2.7 + # otherwise, we constantly get TypeError: shutdown() takes exactly 0 arguments (1 given) + + # Monkey patching idiom: http://mail.python.org/pipermail/python-dev/2008-January/076194.html + def monkeypatch_method(cls): + def decorator(func): + setattr(cls, func.__name__, func) + return func + return decorator + from werkzeug import serving + + @monkeypatch_method(serving.BaseWSGIServer) + def shutdown_request(self, request): + request.shutdown() + +%(open_page)s +try: + flask_app.run(host=%(interface)r, port=%(port)s, threaded=True, + ssl_context=ssl_context, debug=False) +finally: + save_notebook(flask_base.notebook) + os.unlink(%(pidfile)r) +""" + + def run_command(self, kw): + """Run a flask (werkzeug) webserver.""" + # TODO: Check to see if server is running already (PID file?) + self.prepare_kwds(kw) + run_file = os.path.join(kw['directory'], 'run_flask') + + with open(run_file, 'w') as script: + script.write((self.config_stub+self.FLASK_NOTEBOOK_CONFIG)%kw) + + if kw['profile']: + profilecmd = '-m cProfile -o %s'%self.profile_file(kw['profile']) + else: + profilecmd='' + cmd = 'python %s %s' % (profilecmd, run_file) + return cmd + +class NotebookRunTwisted(NotebookRun): + name="twistd" + TWISTD_NOTEBOOK_CONFIG = """ +######################################################################## +# See http://twistedmatrix.com/documents/current/web/howto/using-twistedweb.html +# (Serving WSGI Applications) for the basic ideas of the below code +#################################################################### + +##### START EPOLL +# Take this out when Twisted 12.1 is released, since epoll will then +# be the default reactor when needed. See http://twistedmatrix.com/trac/ticket/5478 +import sys, platform +if (platform.system()=='Linux' + and (platform.release().startswith('2.6') + or platform.release().startswith('3'))): + try: + from twisted.internet import epollreactor + epollreactor.install() + except: + pass +#### END EPOLL + + +def save_notebook2(notebook): + from twisted.internet.error import ReactorNotRunning + save_notebook(notebook) + +import signal +from twisted.internet import reactor def my_sigint(x, n): try: reactor.stop() except ReactorNotRunning: pass signal.signal(signal.SIGINT, signal.SIG_DFL) - - + signal.signal(signal.SIGINT, my_sigint) from twisted.web import server - -######### -# Flask # -######### -import os -flask_dir = os.path.join(os.environ['SAGE_ROOT'], 'devel', 'sagenb', 'flask_version') -sys.path.append(flask_dir) -import base as flask_base -startup_token = '{0:x}'.format(random.randint(0, 2**128)) -flask_app = flask_base.create_app(%(notebook_opts)s, startup_token=startup_token) -sys.path.remove(flask_dir) - from twisted.web.wsgi import WSGIResource resource = WSGIResource(reactor, reactor.getThreadPool(), flask_app) @@ -108,33 +237,81 @@ def log(*args, **kwargs): #This has to be done after flask_base.create_app is run from functools import partial -reactor.addSystemEventTrigger('before', 'shutdown', partial(save_notebook, flask_base.notebook)) +reactor.addSystemEventTrigger('before', 'shutdown', partial(save_notebook2, flask_base.notebook)) """ + def run_command(self, kw): + """Run a twistd webserver.""" + # Is a server already running? Check if a Twistd PID exists in + # the given directory. + self.prepare_kwds(kw) + conf = os.path.join(kw['directory'], 'twistedconf.tac') + if platformType != 'win32': + from twisted.scripts._twistd_unix import checkPID + try: + checkPID(kw['pidfile']) + except SystemExit as e: + pid = int(open(kw['pidfile']).read()) + + if str(e).startswith('Another twistd server is running,'): + print 'Another Sage Notebook server is running, PID %d.' % pid + + old_interface, old_port, old_secure = self.get_old_settings(conf) + if kw['automatic_login'] and old_port: + old_interface = old_interface or 'localhost' + + print 'Opening web browser at http%s://%s:%s/ ...' % ( + 's' if old_secure else '', old_interface, old_port) + + from sagenb.misc.misc import open_page as browse_to + browse_to(old_interface, old_port, old_secure, '/') + return None + print '\nPlease either stop the old server or run the new server in a different directory.' + return None + + ## Create the config file + if kw['secure']: + kw['strport'] = 'ssl:%(port)s:interface=%(interface)s:privateKey=%(private_pem)s:certKey=%(public_pem)s'%kw + else: + kw['strport'] = 'tcp:%(port)s:interface=%(interface)s'%kw + + + with open(conf, 'w') as config: + config.write((self.config_stub+self.TWISTD_NOTEBOOK_CONFIG)%kw) + + if kw['profile']: + profilecmd = '--profile=%s --profiler=cprofile --savestats'%self.profile_file(kw['profile']) + else: + profilecmd='' + cmd = 'twistd %s --pidfile="%s" -ny "%s"' % (profilecmd, kw['pidfile'], conf) + return cmd + + def get_old_settings(self, conf): + """ + Returns three settings from the Twisted configuration file conf: + the interface, port number, and whether the server is secure. If + there are any errors, this returns (None, None, None). + """ + import re + # This should match the format written to twistedconf.tac below. + p = re.compile(r'interface="(.*)",port=(\d*),secure=(True|False)') + try: + interface, port, secure = p.search(open(conf, 'r').read()).groups() + if secure == 'True': + secure = True + else: + secure = False + return interface, port, secure + except IOError, AttributeError: + return None, None, None + + def cmd_exists(cmd): """ Return True if the given cmd exists. """ return os.system('which %s 2>/dev/null >/dev/null' % cmd) == 0 -def get_old_settings(conf): - """ - Returns three settings from the Twisted configuration file conf: - the interface, port number, and whether the server is secure. If - there are any errors, this returns (None, None, None). - """ - import re - # This should match the format written to twistedconf.tac below. - p = re.compile(r'interface="(.*)",port=(\d*),secure=(True|False)') - try: - interface, port, secure = p.search(open(conf, 'r').read()).groups() - if secure == 'True': - secure = True - else: - secure = False - return interface, port, secure - except IOError, AttributeError: - return None, None, None def notebook_setup(self=None): if not os.path.exists(conf_path): @@ -206,61 +383,73 @@ def notebook_setup(self=None): '--outfile %s' % (template_file, private_pem, public_pem)] print cmd[0] subprocess.call(cmd, shell=True) - + # Set permissions on private cert os.chmod(private_pem, 0600) print "Successfully configured notebook." -def notebook_twisted(self, +command={'flask': NotebookRunFlask, 'twistd': NotebookRunTwisted, 'uwsgi': NotebookRunuWSGI} +def notebook_run(self, directory = None, port = 8080, - interface = 'localhost', - address = None, + interface = 'localhost', port_tries = 50, secure = False, reset = False, - require_login = True, accounts = None, openid = None, - + + auth_ldap = False, + ldap_uri = 'ldap://example.net:389/', + ldap_basedn = 'ou=users,dc=example,dc=net', + ldap_binddn = 'cn=manager,dc=example,dc=net', + ldap_bindpw = 'secret', + ldap_username_attrib ='cn', + ldap_lookup_attribs = ('cn', 'sn', 'givenName', 'mail'), + server_pool = None, ulimit = '', timeout = 0, - open_viewer = True, + automatic_login = True, - sagetex_path = "", start_path = "", fork = False, quiet = False, - subnets = None): + + server = "twistd", + profile = False, + + subnets = None, + require_login = None, + open_viewer = None, + address = None, + ): if subnets is not None: raise ValueError("""The subnets parameter is no longer supported. Please use a firewall to block subnets, or even better, volunteer to write the code to implement subnets again.""") + if require_login is not None or open_viewer is not None: + raise ValueError("The require_login and open_viewer parameters are no longer supported. " + "Please use automatic_login=True to automatically log in as admin, " + "or use automatic_login=False to not automatically log in.") + if address is not None: + raise ValueError("Use 'interface' instead of 'address' when calling notebook(...).") cwd = os.getcwd() - # For backwards compatible, we still allow the address to be set - # instead of the interface argument - if address is not None: - from warnings import warn - message = "Use 'interface' instead of 'address' when calling notebook(...)." - warn(message, DeprecationWarning, stacklevel=3) - interface = address if directory is None: - directory = '%s/sage_notebook' % DOT_SAGENB + directory = '%s/sage_notebook.sagenb' % DOT_SAGENB else: - if (isinstance(directory, basestring) and len(directory) > 0 and - directory[-1] == "/"): - directory = directory[:-1] + directory = directory.rstrip('/') # First change to the directory that contains the notebook directory wd = os.path.split(directory) if wd[0]: os.chdir(wd[0]) directory = wd[1] + pidfile = os.path.join(directory, 'sagenb.pid') port = int(port) @@ -275,26 +464,37 @@ def notebook_twisted(self, # if none use defaults nb = notebook.load_notebook(directory) - + directory = nb._dir - conf = os.path.join(directory, 'twistedconf.tac') - + if not quiet: print "The notebook files are stored in:", nb._dir nb.conf()['idle_timeout'] = int(timeout) - nb.conf()['require_login'] = require_login if openid is not None: - nb.conf()['openid'] = openid + nb.conf()['openid'] = openid elif not nb.conf()['openid']: + # What is the purpose behind this elif? It seems rather pointless. + # all it appears to do is set the config to False if bool(config) is False nb.conf()['openid'] = False if accounts is not None: nb.user_manager().set_accounts(accounts) - elif not nb.conf()['accounts']: - nb.user_manager().set_accounts(True) - + else: + nb.user_manager().set_accounts(nb.conf()['accounts']) + + if auth_ldap: + nb.conf()['auth_ldap'] = True + nb.conf()['ldap_uri'] = ldap_uri + nb.conf()['ldap_basedn'] = ldap_basedn + nb.conf()['ldap_binddn'] = ldap_binddn + nb.conf()['ldap_bindpw'] = ldap_bindpw + nb.conf()['ldap_username_attrib'] = ldap_username_attrib + nb.conf()['ldap_lookup_attribs'] = ldap_lookup_attribs + elif not nb.conf()['auth_ldap']: + nb.conf()['auth_ldap'] = False + if nb.user_manager().user_exists('root') and not nb.user_manager().user_exists('admin'): # This is here only for backward compatibility with one # version of the notebook. @@ -303,7 +503,7 @@ def notebook_twisted(self, if not nb.user_manager().user_exists('admin'): reset = True - + if reset: passwd = get_admin_passwd() if reset: @@ -330,97 +530,9 @@ def notebook_twisted(self, nb.upgrade_model() - nb.save() del nb - def run(port): - # Is a server already running? Check if a Twistd PID exists in - # the given directory. - pidfile = os.path.join(directory, 'twistd.pid') - if platformType != 'win32': - from twisted.scripts._twistd_unix import checkPID - try: - checkPID(pidfile) - except SystemExit as e: - pid = int(open(pidfile).read()) - - if str(e).startswith('Another twistd server is running,'): - print 'Another Sage Notebook server is running, PID %d.' % pid - old_interface, old_port, old_secure = get_old_settings(conf) - if open_viewer and old_port: - old_interface = old_interface or 'localhost' - - print 'Opening web browser at http%s://%s:%s/ ...' % ( - 's' if old_secure else '', old_interface, old_port) - - from sagenb.misc.misc import open_page as browse_to - browse_to(old_interface, old_port, old_secure, '/') - return - print '\nPlease either stop the old server or run the new server in a different directory.' - return - - ## Create the config file - if secure: - if (not os.path.exists(private_pem) or - not os.path.exists(public_pem)): - print "In order to use an SECURE encrypted notebook, you must first run notebook.setup()." - print "Now running notebook.setup()" - notebook_setup() - if (not os.path.exists(private_pem) or - not os.path.exists(public_pem)): - print "Failed to setup notebook. Please try notebook.setup() again manually." - strport = '%s:%s:interface=%s:privateKey=%s:certKey=%s'%( - protocol, port, interface, private_pem, public_pem) - else: - strport = 'tcp:%s:interface=%s' % (port, interface) - - notebook_opts = '"%s",interface="%s",port=%s,secure=%s' % ( - os.path.abspath(directory), interface, port, secure) - - if open_viewer: - start_path = "'/?startup_token=%s' % startup_token" - if interface: - hostname = interface - else: - hostname = 'localhost' - open_page = "from sagenb.misc.misc import open_page; open_page('%s', %s, %s, %s)" % (hostname, port, secure, start_path) - else: - open_page = '' - - config = open(conf, 'w') - - config.write(FLASK_NOTEBOOK_CONFIG%{'notebook_opts': notebook_opts, 'sagetex_path': sagetex_path, - 'do_not_require_login': not require_login, - 'dir': os.path.abspath(directory), 'cwd':cwd, - 'strport': strport, - 'open_page': open_page}) - - - config.close() - - ## Start up twisted - cmd = 'twistd --pidfile="%s" -ny "%s"' % (pidfile, conf) - if not quiet: - print_open_msg('localhost' if not interface else interface, - port, secure=secure) - if secure and not quiet: - print "There is an admin account. If you do not remember the password," - print "quit the notebook and type notebook(reset=True)." - - if fork: - import pexpect - return pexpect.spawn(cmd) - else: - e = os.system(cmd) - - os.chdir(cwd) - if e == 256: - raise socket.error - - return True - # end of inner function run - if interface != 'localhost' and not secure: print "*" * 70 print "WARNING: Insecure notebook server listening on external interface." @@ -429,9 +541,40 @@ def run(port): print "*" * 70 port = find_next_available_port(interface, port, port_tries) - if open_viewer: - "Open viewer automatically isn't fully implemented. You have to manually open your web browser to the above URL." - return run(port) + if automatic_login: + "Automatic login isn't fully implemented. You have to manually open your web browser to the above URL." + if secure: + if (not os.path.exists(private_pem) or + not os.path.exists(public_pem)): + print "In order to use an SECURE encrypted notebook, you must first run notebook.setup()." + print "Now running notebook.setup()" + notebook_setup() + if (not os.path.exists(private_pem) or + not os.path.exists(public_pem)): + print "Failed to setup notebook. Please try notebook.setup() again manually." + + kw = dict(port=port, automatic_login=automatic_login, secure=secure, private_pem=private_pem, public_pem=public_pem, + interface=interface, directory=directory, pidfile=pidfile, cwd=cwd, profile=profile) + cmd = command[server]().run_command(kw) + if cmd is None: + return + + if not quiet: + print_open_msg('localhost' if not interface else interface, + port, secure=secure) + if secure and not quiet: + print "There is an admin account. If you do not remember the password," + print "quit the notebook and type notebook(reset=True)." + print "Executing", cmd + if fork: + import pexpect + return pexpect.spawn(cmd) + else: + e = os.system(cmd) + + os.chdir(cwd) + if e == 256: + raise socket.error def get_admin_passwd(): print "\n" * 2 diff --git a/sagenb/notebook/server_conf.py b/sagenb/notebook/server_conf.py index 69447ae06..41e99ded1 100644 --- a/sagenb/notebook/server_conf.py +++ b/sagenb/notebook/server_conf.py @@ -9,6 +9,7 @@ T_CHOICE, T_REAL, T_COLOR, T_STRING, T_LIST, T_INFO) from sagenb.misc.misc import get_languages from flaskext.babel import gettext, lazy_gettext +_ = lazy_gettext defaults = {'word_wrap_cols':72, 'max_history_length':250, @@ -43,11 +44,20 @@ 'recaptcha_private_key':'', 'default_language': 'en_US', 'model_version': 0, + + 'auth_ldap':False, + 'ldap_uri':'ldap://example.net:389/', + 'ldap_basedn':'ou=users,dc=example,dc=net', + 'ldap_binddn':'cn=manager,dc=example,dc=net', + 'ldap_bindpw': 'secret', + 'ldap_username_attrib': 'cn', + 'ldap_lookup_attribs':['cn', 'sn', 'givenName', 'mail'], } G_APPEARANCE = lazy_gettext('Appearance') G_AUTH = lazy_gettext('Authentication') G_SERVER = lazy_gettext('Server') +G_LDAP = _('LDAP') defaults_descriptions = { @@ -181,7 +191,49 @@ DESC : lazy_gettext('Model Version'), GROUP : G_SERVER, TYPE : T_INFO, - } + }, + 'auth_ldap': { + POS : 1, + DESC : _('Enable LDAP Authentication'), + GROUP : G_LDAP, + TYPE : T_BOOL, + }, + 'ldap_uri': { + POS : 2, + DESC : 'LDAP URI', + GROUP : G_LDAP, + TYPE : T_STRING, + }, + 'ldap_binddn': { + POS : 3, + DESC : _('Bind DN'), + GROUP : G_LDAP, + TYPE : T_STRING, + }, + 'ldap_bindpw': { + POS : 4, + DESC : _('Bind Password'), + GROUP : G_LDAP, + TYPE : T_STRING, + }, + 'ldap_basedn': { + POS : 5, + DESC : _('Base DN'), + GROUP : G_LDAP, + TYPE : T_STRING, + }, + 'ldap_username_attrib': { + POS : 6, + DESC: _('Username Attribute (i.e. cn, uid or userPrincipalName)'), + GROUP : G_LDAP, + TYPE : T_STRING, + }, + 'ldap_lookup_attribs': { + POS : 7, + DESC: _('Attributes for user lookup'), + GROUP : G_LDAP, + TYPE : T_LIST, + }, } diff --git a/sagenb/notebook/tutorial.py b/sagenb/notebook/tutorial.py index 1bb3e0db9..e976b31c9 100644 --- a/sagenb/notebook/tutorial.py +++ b/sagenb/notebook/tutorial.py @@ -25,7 +25,7 @@ edit and evaluate, which contain scalable typeset mathematics and beautiful antialised images. To try it out immediately, do this: - sage: notebook(open_viewer=True) # not tested + sage: notebook() # not tested the sage notebook starts... \subsection{Supported Browsers} diff --git a/sagenb/notebook/user.py b/sagenb/notebook/user.py index 7eda77702..341aa0470 100644 --- a/sagenb/notebook/user.py +++ b/sagenb/notebook/user.py @@ -32,8 +32,8 @@ def __init__(self, username, password='', email='', account_type='admin'): self.set_password(password) self._email = email self._email_confirmed = False - if not account_type in ['admin', 'user', 'guest']: - raise ValueError("account type must be one of admin, user, or guest") + if not account_type in ['admin', 'user', 'guest', 'external' ]: + raise ValueError, "account type must be one of admin, user, guest or external" self._account_type = account_type self._conf = user_conf.UserConfiguration() self._temporary_password = '' @@ -289,7 +289,13 @@ def is_guest(self): False """ return self._account_type == 'guest' + + def may_change_email(self): + return self._account_type in ( 'user', 'admin' ) + def may_change_password(self): + return self._account_type in ( 'user', 'admin' ) + def is_suspended(self): """ EXAMPLES:: diff --git a/sagenb/notebook/user_manager.py b/sagenb/notebook/user_manager.py index cd9dd982e..5f7c802f2 100644 --- a/sagenb/notebook/user_manager.py +++ b/sagenb/notebook/user_manager.py @@ -112,8 +112,16 @@ def user(self, username): pass raise KeyError, "no user '%s'"%username + + def user_lookup(self, search): + r = [x for x in self.users().keys() if search in x] + try: + r += [u for u in self._user_lookup(search) if u not in r] + except AttributeError: + pass + return r - def valid_login_names(self): + def known_users(self): """ Return a list of users that can log in. """ @@ -203,34 +211,6 @@ def create_default_users(self, passwd, verbose=False): self.add_user('guest', '', '', account_type='guest', force=True) self.add_user('admin', passwd, '', account_type='admin', force=True) - def default_user(self): - """ - Return a default login name that the user will see when confronted with the - Sage notebook login page. - - Currently this returns 'admin' if that is the *only* user. Otherwise it - returns the string ''. - - OUTPUT: - string - - EXAMPLES: - sage: from sagenb.notebook.user_manager import SimpleUserManager - sage: U = SimpleUserManager(accounts=True) - sage: U.create_default_users('password') - sage: U.default_user() - 'admin' - sage: U.add_user('william', '', '') - sage: U.default_user() - '' - - """ - usernames = [x for x in self.usernames() if not x in ['guest', '_sage_', 'pub']] - if usernames == ['admin']: - return 'admin' - else: - return '' - def delete_user(self, username): """ Deletes the user username from the users dictionary. @@ -492,11 +472,14 @@ def check_password(self, username, password): if user_password == crypt.crypt(password, user.SALT): self.set_password(username, password) return True - else: - return False else: salt, user_password = user_password.split('$')[1:] - return hashlib.sha256(salt + password).hexdigest() == user_password + if hashlib.sha256(salt + password).hexdigest() == user_password: + return True + try: + return self._check_password(username, password) + except AttributeError: + return False; def get_accounts(self): # need to use notebook's conf because those are already serialized @@ -569,3 +552,197 @@ def get_user_from_openid(self, identity_url): Return the user object corresponding ot a given identity_url """ return self.user(self.get_username_from_openid(identity_url)) + + +class ExtAuthUserManager(OpenIDUserManager): + def __init__(self, conf=None, accounts=None): + OpenIDUserManager.__init__(self, accounts=accounts, conf=conf) + self._conf = conf + # currently only 'auth_ldap' here. the key must match to a T_BOOL option in server_config.py + # so we can turn this auth method on/off + self._auth_methods = { + 'auth_ldap': LdapAuth(conf=self._conf), + } + + def _user(self, username): + # check all auth methods that are enabled in the notebook's config + # if a valid username is found, a new User object will be created. + for a in self._auth_methods: + if self._conf[a]: + u = self._auth_methods[a].check_user(username) + if u: + try: + email = self._auth_methods[a].get_attrib(username, 'email') + except KeyError: + email = None + + self.add_user(username, password='', email=email, account_type='external', force=True) + return self.users()[username] + + return OpenIDUserManager._user(self, username) + + def _check_password(self, username, password): + for a in self._auth_methods: + if self._conf[a]: + # users should be unique among auth methods + u = self._auth_methods[a].check_user(username) + if u: + return self._auth_methods[a].check_password(username, password) + return OpenIDUserManager.check_password(self, username, password) + + def _user_lookup(self, search): + """ + Returns a list of usernames that are found when calling user_lookup on all enabled auth methods + """ + r = [] + for a in self._auth_methods: + if self._conf[a]: + r += [u for u in self._auth_methods[a].user_lookup(search) if u not in r] + return r + + # the openid methods should not be callable if openid is disabled + def get_username_from_openid(self, identity_url): + if self._conf['openid']: + return OpenIDUserManager.get_username_from_openid(self, identity_url) + else: + raise RuntimeError + + def create_new_openid(self, identity_url, username): + if self._conf['openid']: + OpenIDUserManager.create_new_openid(self, identity_url, username) + else: + raise RuntimeError + + def get_user_from_openid(self, identity_url): + if self._conf['openid']: + return OpenIDUserManager.get_user_from_openid(self, identity_url) + else: + raise RuntimeError + + +class AuthMethod(): + """ + Abstract class for authmethods that are used by ExtAuthUserManager + All auth methods must implement the following methods + """ + + def __init__(self, conf): + self._conf = conf + + def check_user(self, username): + raise NotImplementedError + + def check_password(self, username, password): + raise NotImplementedError + + def get_attrib(self, username, attrib): + raise NotImplementedError + + +class LdapAuth(AuthMethod): + """ + Authentication via LDAP + + User authentication works like this: + 1a. bind to LDAP with a generic (configured) DN and password + 1b. find the ldap object matching to username. return None when more than 1 object is found + 2. if 1 succeeds, bind with the user DN and the supplied password + + User lookup: + wildcard-match all configured "user lookup attributes" for + the given search string + """ + def __init__(self, conf): + AuthMethod.__init__(self, conf) + + def _ldap_search(self, query, attrlist=None): + """ + runs any ldap query passed as arg + """ + import ldap + conn = ldap.initialize(self._conf['ldap_uri']) + try: + conn.simple_bind_s(self._conf['ldap_binddn'], self._conf['ldap_bindpw']) + except ldap.LDAPError, e: + print e + raise + except ldap.INVALID_CREDENTIALS, e: + print e + raise ValueError, "invalid LDAP credentials" + + result = conn.search_ext_s(self._conf['ldap_basedn'], + ldap.SCOPE_SUBTREE, + filterstr=query, + attrlist=attrlist, + # TODO: + #timeout=self._conf['ldap_timeout'], + #sizelimit=self._conf['ldap_sizelimit'] + ) + conn.unbind_s() + return result + + def _get_ldapuser(self, username, attrlist=None): + from ldap import LDAPError + from ldap.filter import filter_format + # escape + try: + result = self._ldap_search(filter_format("(%s=%s)", [self._conf['ldap_username_attrib'], username]), attrlist) + except LDAPError, e: + print e + return None + # return None if more than 1 object found + return result[0] if len(result) == 1 else None + + def user_lookup(self, search): + from ldap.filter import filter_format + from ldap import LDAPError + # build a ldap OR query + q = u''.join(( + filter_format("(%s=*%s*)", [a, search]) + for a in self._conf['ldap_lookup_attribs'] + )) + q = q.join(('(|', ')')) + + try: + r = self._ldap_search(q, attrlist=[str(self._conf['ldap_username_attrib'])]) + except LDAPError, e: + print(e) + return [] + except Exception, e: + print e + return [] + # return a list of usernames + return [ x[1][self._conf['ldap_username_attrib']][0].lower() + for x in r if x[1].has_key(self._conf['ldap_username_attrib']) ] + + def check_user(self, username): + u = self._get_ldapuser(username) + return u is not None + + def check_password(self, username, password): + import ldap + # retrieve username's DN + try: + u = self._get_ldapuser(username) + #u[0] is DN, u[1] is a dict with all other attributes + userdn = u[0] + except ValueError: + return False + + # try to bind with that DN + try: + conn = ldap.initialize(uri=self._conf['ldap_uri']) + conn.simple_bind_s(userdn, password) + conn.unbind_s() + return True + except ldap.INVALID_CREDENTIALS: + return False + + def get_attrib(self, username, attrib): + # translate some common attribute names to their ldap equivalents, i.e. "email" is "mail + attrib = 'mail' if attrib == 'email' else attrib + + u = self._get_ldapuser(username) + if u is not None: + a = u[1][attrib][0] #if u[1].has_key(attrib) else '' + return a diff --git a/sagenb/testing/notebook_test_case.py b/sagenb/testing/notebook_test_case.py index 4d2dd154f..c554ae223 100644 --- a/sagenb/testing/notebook_test_case.py +++ b/sagenb/testing/notebook_test_case.py @@ -23,7 +23,7 @@ 'address': 'localhost', # Should *not* be left empty. 'directory': '', # Set automatically if empty. Must end # in '.sagenb'. - 'open_viewer': False, + 'automatic_login': False, 'port': 8080, 'secure': False } diff --git a/setup.py b/setup.py index d1bbd06fc..255971c8d 100644 --- a/setup.py +++ b/setup.py @@ -34,30 +34,31 @@ def all_files(dir, lstrip): author = 'William Stein et al.', author_email= 'http://groups.google.com/group/sage-notebook', url = 'http://code.google.com/p/sagenb', - install_requires = ['twisted>=11.0.0', - 'flask', - 'flask-openid', - 'flask-autoindex', - 'babel', - 'flask-babel', - 'hg-git', - 'pyOpenSSL'], + install_requires = [ 'twisted>=11.0.0' + , 'flask' + , 'flask-openid' + , 'flask-autoindex' + , 'babel' + , 'flask-babel' + , 'hg-git' + , 'pyOpenSSL<=0.12' + ], test_suite = 'sagenb.testing.run_tests.all_tests', - packages = ['sagenb', - 'sagenb.interfaces', - 'sagenb.misc', - 'sagenb.notebook', - 'sagenb.notebook.compress', - 'sagenb.simple', - 'sagenb.storage', - 'sagenb.testing', - 'sagenb.testing.tests', - 'sagenb.testing.selenium' - ], + packages = [ 'sagenb' + , 'sagenb.interfaces' + , 'sagenb.misc' + , 'sagenb.notebook' + , 'sagenb.notebook.compress' + , 'sagenb.simple' + , 'sagenb.storage' + , 'sagenb.testing' + , 'sagenb.testing.tests' + , 'sagenb.testing.selenium' + ], scripts = [ 'sagenb/data/sage3d/sage3d', ], package_data = {'sagenb': all_files('sagenb/data', 'sagenb/') + all_files('sagenb/translations', 'sagenb/') - }, + }, ) diff --git a/spkg-dist b/spkg-dist index 7cefb227c..68957d7b6 100755 --- a/spkg-dist +++ b/spkg-dist @@ -88,20 +88,21 @@ def fetch_packages(): tmp_dir = mkdtemp() # in order of dependencies - required_packages = ('twisted>=11.0.0', - 'pytz>=2011n', - 'Babel>=0.9.6', - 'Werkzeug>=0.8.2', - 'speaklater>=1.2', - 'python-openid>=2.2.5', - 'Flask>=0.8', - 'Flask-Silk>=0.1.1', - 'Flask-AutoIndex>=0.4.0', - 'Flask-Babel>=0.8', - 'Flask-OpenID>=1.0.1', - 'dulwich>=0.8.0', - 'hg-git>=0.3.1', - 'pyOpenSSL') + required_packages = ( 'twisted>=11.0.0' + , 'pytz>=2011n' + , 'Babel>=0.9.6' + , 'Werkzeug>=0.8.2' + , 'speaklater>=1.2' + , 'python-openid>=2.2.5' + , 'Flask>=0.8' + , 'Flask-Silk>=0.1.1' + , 'Flask-AutoIndex>=0.4.0' + , 'Flask-Babel>=0.8' + , 'Flask-OpenID>=1.0.1' + , 'dulwich>=0.8.0' + , 'hg-git>=0.3.1' + , 'pyOpenSSL<=0.12' + ) pkg_locations = [] for pkg in required_packages: @@ -144,6 +145,10 @@ if [ -z "$SAGE_LOCAL" ]; then fi cd src +[ -z "$CPATH" ] || CPATH="$CPATH": +[ -z "$LIBRARY_PATH" ] || LIBRARY_PATH="$LIBRARY_PATH": +export CPATH="$CPATH""$SAGE_LOCAL"/include +export LIBRARY_PATH="$LIBRARY_PATH""$SAGE_LOCAL"/lib %(install_dependencies)s