[sagenb] 01/179: reapplied LDAP code to current upstream

felix salfelder felix-guest at moszumanska.debian.org
Tue May 6 12:05:04 UTC 2014


This is an automated email from the git hooks/post-receive script.

felix-guest pushed a commit to branch master
in repository sagenb.

commit 3f47c7af260a611506c32ff768c6b61d744df742
Author: Robin Martinjak <rob at rmartinjak.de>
Date:   Wed Jun 20 16:35:00 2012 +0200

    reapplied LDAP code to current upstream
---
 flask_version/worksheet.py                         |   6 +-
 .../data/sage/html/notebook/worksheet_share.html   |  33 +++++-
 .../data/sage/html/settings/account_settings.html  |   2 +-
 sagenb/notebook/auth.py                            | 131 +++++++++++++++++++++
 sagenb/notebook/notebook.py                        |  13 +-
 sagenb/notebook/server_conf.py                     |  73 ++++++++++++
 sagenb/notebook/user.py                            |   6 +
 sagenb/notebook/user_manager.py                    |  94 +++++++++++++--
 8 files changed, 346 insertions(+), 12 deletions(-)

diff --git a/flask_version/worksheet.py b/flask_version/worksheet.py
index f293418..9acb194 100644
--- a/flask_version/worksheet.py
+++ b/flask_version/worksheet.py
@@ -564,6 +564,10 @@ def worksheet_edit_published_page(worksheet):
 def worksheet_share(worksheet):
     return g.notebook.html_share(worksheet, g.username)
 
+ at worksheet_command('search_collab')
+def worksheet_search_collab(worksheet):
+    return g.notebook.html_share(worksheet, g.username, request.values.get('lookup'))
+
 @worksheet_command('invite_collab')
 def worksheet_invite_collab(worksheet):
     owner = worksheet.owner()
@@ -573,7 +577,6 @@ def worksheet_invite_collab(worksheet):
     if len(collaborators-old_collaborators)>500:
         # to prevent abuse, you can't add more than 500 collaborators at a time
         return current_app.message(_("Error: can't add more than 500 collaborators at a time"), cont=url_for_worksheet(worksheet))
-    worksheet.set_collaborators(collaborators)
     user_manager = g.notebook.user_manager()
     # add worksheet to new collaborators
     for u in collaborators-old_collaborators:
@@ -590,6 +593,7 @@ def worksheet_invite_collab(worksheet):
             # user doesn't exist
             pass
 
+    worksheet.set_collaborators(collaborators)
     return redirect(url_for_worksheet(worksheet))
     
 ########################################################
diff --git a/sagenb/data/sage/html/notebook/worksheet_share.html b/sagenb/data/sage/html/notebook/worksheet_share.html
index 42ff38c..731d957 100644
--- a/sagenb/data/sage/html/notebook/worksheet_share.html
+++ b/sagenb/data/sage/html/notebook/worksheet_share.html
@@ -3,7 +3,8 @@
 INPUT:
     - worksheet - an instance of Worksheet
     - username - a string containing a username
-    - other_users - a list of strings containing other users names
+    - lookup - (optional) string used for user lookup
+    - lookup_result - (optional) list of users returned by user lookup
 #}
 
 {% block sharebar_title %}
@@ -13,6 +14,16 @@ INPUT:
 {% set select = "share" %}
 
 {% block after_sharebar %}
+<script type="text/javascript">
+function add_collab(u) {
+    var col = document.getElementById('collaborators');
+    if (col.value != "") {
+        col.value+= ", ";
+    }
+    col.value+=u;
+}
+</script>
+
 {% 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 <a href="copy">make your own copy</a>.') }}
 {% else %}
@@ -23,5 +34,25 @@ INPUT:
     <textarea name="collaborators" rows=5 cols=70 class="edit" id="collaborators" style="display:block; margin-bottom:1em;">{{ worksheet.collaborators()|join(', ') }}</textarea>
     <input type="submit" title="{{ gettext('Give access to your worksheet to the above collaborators') }}" value="{{ gettext('Invite Collaborators') }}" />
 </form>
+
+    <hr class="usercontrol" />
+    {% if lookup %}
+        <div>
+        <p>{{ gettext ('Search results:') }}{% if lookup_result %}
+            {% for u in lookup_result %}
+                <span class="users">
+                    <a href="javascript:add_collab('{{ u }}');" class="users">{{ u }}</a>
+                </span>
+            {% endfor %}
+         {% else %} {{ gettext('No match found') }}
+         {% endif %}</p>
+         </div>
+    {% else %}
+        {{ gettext('Search Users') }}
+    {% endif %}
+    <form width=70% method="post" action="search_collab" style="margin-bottom:1em">
+        <input type="text" class="edit" id="lookup" name="lookup" value="{{ lookup if lookup else '' }}" />
+        <input type="submit" value="{{ gettext('Search') }}" />
+    </form>
 {% endif %}
 {% endblock %}
diff --git a/sagenb/data/sage/html/settings/account_settings.html b/sagenb/data/sage/html/settings/account_settings.html
index b0c46a1..5bc6158 100644
--- a/sagenb/data/sage/html/settings/account_settings.html
+++ b/sagenb/data/sage/html/settings/account_settings.html
@@ -20,6 +20,7 @@
             </select>
         </div>
     </div>
+	{% if not external %}
     <div class="section">
         <h2>{{ gettext('Change Password') }}</h2>
         <div id="passwd">
@@ -41,7 +42,6 @@
         </div>
     </div>
 
-    {% if true %}
     <div class="section">
         <h2>{{ gettext('Change E-mail Address') }}</h2>
         
diff --git a/sagenb/notebook/auth.py b/sagenb/notebook/auth.py
new file mode 100644
index 0000000..f23650f
--- /dev/null
+++ b/sagenb/notebook/auth.py
@@ -0,0 +1,131 @@
+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 user_lookup(self, search):
+        raise NotImplementedError
+
+    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:
+    1a. bind to LDAP with either
+            - generic configured DN and password (simple bind)
+            - GSSAPI (e.g. Kerberos)
+    1b. find the ldap object matching username.
+        (return None if more than 1 object is found)
+    2. if 1 succeeds, try simple bind with the supplied user DN and password
+
+    User lookup:
+    wildcard-search 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
+        from ldap.sasl import gssapi
+        conn = ldap.initialize(self._conf['ldap_uri'])
+        try: 
+            if self._conf['ldap_gssapi']:
+                token = gssapi()
+                conn.sasl_interactive_bind_s("", token)
+            else:
+                conn.simple_bind_s(self._conf['ldap_binddn'], self._conf['ldap_bindpw'])
+
+            result = conn.search_ext_s(self._conf['ldap_basedn'],
+                                         ldap.SCOPE_SUBTREE,
+                                         filterstr=query,
+                                         attrlist=attrlist,
+                                         timeout=self._conf['ldap_timeout'],
+                                         sizelimit=self._conf['ldap_sizelimit'])
+        except ldap.INVALID_CREDENTIALS:
+            raise ValueError, "invalid LDAP credentials"
+        except ldap.LDAPError, e:
+            raise ValueError, e
+        finally:
+            conn.unbind_s()
+
+        return result
+
+    def _get_ldapuser(self, username, attrlist=None):
+        from ldap.filter import filter_format
+        try:
+            result = self._ldap_search(filter_format("(%s=%s)", [self._conf['ldap_username_attrib'], username]), attrlist)
+        except ValueError, 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 ldap OR query
+        q = "(|%s)" % ''.join([filter_format("(%s=*%s*)", [a, search]) for a in self._conf['ldap_lookup_attribs']])
+
+        try:
+            r = self._ldap_search(q, attrlist=[str(self._conf['ldap_username_attrib'])])
+        except ValueError, e:
+            print(e)
+            return []
+        except:
+            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):
+        # LDAP is NOT case sensitive while sage is, so only lowercase names are allowed
+        if username != username.lower():
+            return False
+        return self._get_ldapuser(username) 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
+        conn = ldap.initialize(uri=self._conf['ldap_uri'])
+        try: 
+            conn.simple_bind_s(userdn, password)
+            return True
+        except ldap.INVALID_CREDENTIALS: 
+            return False
+        finally:
+            conn.unbind_s()
+
+    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/notebook/notebook.py b/sagenb/notebook/notebook.py
index bdca928..8605f06 100644
--- a/sagenb/notebook/notebook.py
+++ b/sagenb/notebook/notebook.py
@@ -1368,7 +1368,7 @@ class Notebook(object):
                         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.
 
@@ -1389,10 +1389,19 @@ class Notebook(object):
             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/server_conf.py b/sagenb/notebook/server_conf.py
index eafd913..580efd5 100644
--- a/sagenb/notebook/server_conf.py
+++ b/sagenb/notebook/server_conf.py
@@ -44,11 +44,23 @@ defaults = {'word_wrap_cols':72,
             '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_gssapi': False,
+            'ldap_username_attrib': 'cn',
+            'ldap_lookup_attribs': ['cn', 'sn', 'givenName', 'mail'],
+            'ldap_timeout': 5,
+            'ldap_sizelimit': 30,
             }
 
 G_APPEARANCE = _('Appearance')
 G_AUTH = _('Authentication')
 G_SERVER = _('Server')
+G_LDAP = _('LDAP')
 
 defaults_descriptions = {
 
@@ -183,6 +195,67 @@ defaults_descriptions = {
         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_gssapi': {
+        POS : 5,
+        DESC : _('Use GSSAPI instead of Bind DN/Password'),
+        GROUP : G_LDAP,
+        TYPE : T_BOOL,
+        },
+    'ldap_basedn': {
+        POS : 6,
+        DESC : _('Base DN'),
+        GROUP : G_LDAP,
+        TYPE : T_STRING,
+        },
+    'ldap_username_attrib': {
+        POS : 7,
+        DESC: _('Username Attribute (i.e. cn, uid or userPrincipalName)'),
+        GROUP : G_LDAP,
+        TYPE : T_STRING,
+        },
+    'ldap_lookup_attribs': {
+        POS : 8,
+        DESC: _('Attributes for user lookup'),
+        GROUP : G_LDAP,
+        TYPE : T_LIST,
+        },
+    'ldap_timeout': {
+        POS : 9,
+        DESC: _('Query timeout (seconds)'),
+        GROUP : G_LDAP,
+        TYPE : T_INTEGER,
+        },
+    'ldap_sizelimit': {
+        POS : 9,
+        DESC: _('Max. number of search results'),
+        GROUP : G_LDAP,
+        TYPE : T_INTEGER,
+        },
 }
 
 
diff --git a/sagenb/notebook/user.py b/sagenb/notebook/user.py
index 7eda777..d2fc6b1 100644
--- a/sagenb/notebook/user.py
+++ b/sagenb/notebook/user.py
@@ -289,6 +289,12 @@ class User(object):
             False
         """
         return self._account_type == 'guest'
+
+    def is_external(self):
+        return self._external_auth is not None
+
+    def external_auth(self):
+        return self._external_auth
         
     def is_suspended(self):
         """
diff --git a/sagenb/notebook/user_manager.py b/sagenb/notebook/user_manager.py
index 2b7730f..e612418 100644
--- a/sagenb/notebook/user_manager.py
+++ b/sagenb/notebook/user_manager.py
@@ -112,7 +112,15 @@ class UserManager(object):
             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):
         """
         Return a list of users that can log in.
@@ -270,7 +278,7 @@ class UserManager(object):
         return self._accounts
 
 
-    def add_user(self, username, password, email, account_type="user", force=False):
+    def add_user(self, username, password, email, account_type="user", external_auth=None, force=False):
         """
         Adds a new user to the user dictionary.
 
@@ -296,7 +304,7 @@ class UserManager(object):
         us = self.users()
         if us.has_key(username):
             print "WARNING: User '%s' already exists -- and is now being replaced."%username
-        U = user.User(username, password, email, account_type)
+        U = user.User(username, password, email, account_type, external_auth)
         us[username] = U
         self.set_password(username, password)
 
@@ -457,7 +465,7 @@ class SimpleUserManager(UserManager):
         if username == "pub" or password == '':
             return False
         user_password = self.password(username)
-        if user_password is None:
+        if user_password is None and not self.user(username).is_external():
             print "User %s has None password"%username
             return False
         if user_password.find('$') == -1:
@@ -468,7 +476,12 @@ class SimpleUserManager(UserManager):
                 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
@@ -481,8 +494,68 @@ class SimpleUserManager(UserManager):
         self._accounts = value
         self._conf['accounts'] = value
 
-class OpenIDUserManager(SimpleUserManager):
+
+
+class ExtAuthUserManager(SimpleUserManager):
     def __init__(self, accounts=None, conf=None):
+        SimpleUserManager.__init__(self, accounts=accounts, conf=conf)
+
+        from auth import LdapAuth
+
+        # keys 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(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='user', external_auth=a, force=True)
+                    return self.users()[username]
+
+        raise KeyError, "no user '%s'"%username
+
+    def _check_password(self, username, password):
+        """
+        Find auth method for user 'username' and
+        use that auth method to check username/password combination.
+        """
+        u = self.users()[username]
+        if u.is_external():
+            a = u.external_auth()
+        else:
+            return False
+
+        if self._conf[a]:
+            return self._auth_methods[a].check_password(username, password)
+
+        return False
+
+    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]:
+                # avoid duplicates
+                r += [u for u in self._auth_methods[a].user_lookup(search) if u not in r]
+        return r
+
+class OpenIDUserManager(ExtAuthUserManager):
+    def __init__(self, accounts=True, conf=None):
         """
         Creates an user_manager that supports OpenID identities
         EXAMPLES:
@@ -492,7 +565,7 @@ class OpenIDUserManager(SimpleUserManager):
             sage: UM.check_password('admin','passpass')
             True
         """
-        SimpleUserManager.__init__(self, accounts=accounts, conf=conf)
+        ExtAuthUserManager.__init__(self, accounts=accounts, conf=conf)
         self._openid = {} 
 
     def load(self, datastore):
@@ -518,6 +591,9 @@ class OpenIDUserManager(SimpleUserManager):
             sage: UM.get_username_from_openid('https://www.google.com/accounts/o8/id?id=AItdaWgzjV1HJTa552549o1csTDdfeH6_bPxF14')
             'thedude' 
         """
+        if not self._conf['openid']:
+            raise RuntimeError
+
         try:
             return self._openid[identity_url]
         except KeyError:
@@ -534,10 +610,14 @@ class OpenIDUserManager(SimpleUserManager):
             sage: UM.get_username_from_openid('https://www.google.com/accounts/o8/id?id=AItdaWgzjV1HJTa552549o1csTDdfeH6_bPxF14')
             'thedude'
         """
+        if not self._conf['openid']:
+            raise RuntimeError
         self._openid[identity_url] = username
 
     def get_user_from_openid(self, identity_url):
         """
         Return the user object corresponding ot a given identity_url
         """
+        if not self._conf['openid']:
+            raise RuntimeError
         return self.user(self.get_username_from_openid(identity_url)) 

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-science/packages/sagenb.git



More information about the debian-science-commits mailing list