Ticket #11501: trac_11501_ldap_auth.patch

File trac_11501_ldap_auth.patch, 23.7 KB (added by rmartinjak, 10 years ago)
  • flask_version/admin.py

    # HG changeset patch
    # User Robin Martinjak <rob@rmartinjak.de>
    # Date 1308341963 -7200
    # Node ID 4994a363ec27317f731f60d66281cc7d432a9fb6
    # Parent  a8d3d264fcd4bd1b9faf67f4f8fae84e101c49be
    #11501: LDAP Authentication
    
    diff -r a8d3d264fcd4 -r 4994a363ec27 flask_version/admin.py
    a b  
    3232        except KeyError:
    3333            pass
    3434
    35     template_dict['number_of_users'] = len(g.notebook.user_manager().valid_login_names()) if len(g.notebook.user_manager().valid_login_names()) > 1 else None
    36     users = sorted(g.notebook.user_manager().valid_login_names())
     35    template_dict['number_of_users'] = len(g.notebook.user_manager().known_users()) if len(g.notebook.user_manager().known_users()) > 1 else None
     36    users = sorted(g.notebook.user_manager().known_users())
    3737    del users[users.index('admin')]
    3838    template_dict['users'] = [g.notebook.user_manager().user(username) for username in users]
    3939    template_dict['admin'] = g.notebook.user_manager().user(g.username).is_admin()
  • flask_version/authentication.py

    diff -r a8d3d264fcd4 -r 4994a363ec27 flask_version/authentication.py
    a b  
    2323                          'recovery': g.notebook.conf()['email'],
    2424                          'next': request.values.get('next', ''),
    2525                          'sage_version': SAGE_VERSION,
    26                           'openIDlogin': True,
     26                          'openIDlogin': g.notebook.conf()['openid'],
    2727                          'username_error': False,
    2828                          'password_error': False})
    2929   
    3030    if request.method == 'POST':
    3131        username = request.form['email']
    32         password = request.form['password']
     32        password = request.form['password'True]
    3333
    3434        if username == 'COOKIESDISABLED':
    3535            return "Please enable cookies or delete all Sage cookies and localhost cookies in your browser and try again."
  • flask_version/settings.py

    diff -r a8d3d264fcd4 -r 4994a363ec27 flask_version/settings.py
    a b  
    6262
    6363    td['autosave_intervals'] = ((i, ' selected') if nu['autosave_interval']/60 == i else (i, '') for i in range(1, 10, 2))
    6464
    65     td['email'] = g.notebook.conf()['email']
    66     if td['email']:
    67         td['email_address'] = nu.get_email() or 'None'
    68         if nu.is_email_confirmed():
    69             td['email_confirmed'] = 'Confirmed'
    70         else:
    71             td['email_confirmed'] = 'Not confirmed'
     65    td['email_address'] = nu.get_email() or 'None'
     66    if nu.is_email_confirmed():
     67        td['email_confirmed'] = 'Confirmed'
     68    else:
     69        td['email_confirmed'] = 'Not confirmed'
    7270
    7371    td['admin'] = nu.is_admin()
     72    td['form_email'] = nu.may_change_email()
     73    td['form_password'] = nu.may_change_password()
     74
    7475
    7576    return render_template(os.path.join('html', 'settings', 'account_settings.html'), **td)
    7677
  • flask_version/worksheet.py

    diff -r a8d3d264fcd4 -r 4994a363ec27 flask_version/worksheet.py
    a b  
    475475def worksheet_share(worksheet):
    476476    return g.notebook.html_share(worksheet, g.username)
    477477
     478@worksheet_command('search_collab')
     479def worksheet_user_lookup(worksheet):
     480    return g.notebook.html_share(worksheet, g.username, request.values.get('lookup'))
     481
    478482@worksheet_command('invite_collab')
    479483def worksheet_invite_collab(worksheet):
    480     collaborators = [u.strip() for u in request.values.get('collaborators', '').split(',')]
     484    collaborators = [u.strip() for u in request.values.get('collaborators', '').split(',') if u.strip()]
    481485    worksheet.set_collaborators(collaborators)
    482486    return redirect(url_for_worksheet(worksheet))
    483487   
  • sagenb/data/sage/html/notebook/worksheet_share.html

    diff -r a8d3d264fcd4 -r 4994a363ec27 sagenb/data/sage/html/notebook/worksheet_share.html
    a b  
    1313{% set select = "share" %}
    1414
    1515{% block after_sharebar %}
     16<script type="text/javascript">
     17function add_collab(u) {
     18    var col = document.getElementById('collaborators');
     19    if (col.value != "") {
     20        col.value+= ", ";
     21    }
     22    col.value+=u;
     23}
     24</script>
     25   
    1626{% if not (notebook.user_manager().user_is_admin(username) or username == worksheet.owner()) %}
    1727    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>.
    1828{% else %}
     
    2434    <input type="submit" title="Give access to your worksheet to the above collaborators" value="Invite Collaborators" />
    2535</form>
    2636
    27 <hr class="usercontrol" />
    28 <span class="username">Sage Users:</span>
    29 <span class="users">
    30     {{ other_users|join(', ') }}
    31 </span>
     37    <hr class="usercontrol" />
     38    {% if lookup %}
     39        <div>
     40        <p>Search results: {% if lookup_result %}
     41            {% for u in lookup_result %}
     42                <span class="users">
     43                    <a href="javascript:add_collab('{{ u }}');" class="users">{{ u }}</a>
     44                </span>
     45            {% endfor %}
     46         {% else %} sorry, no match found
     47         {% endif %}</p>
     48         </div>
     49    {% else %}
     50        Search Users
     51    {% endif %}
     52    <form width=70% method="post" action="search_collab" style="margin-bottom:1em">
     53        <input type="text" class="edit" id="lookup" name="lookup" value="{{ lookup if lookup else '' }}" />
     54        <input type="submit" value="Search" />
     55    </form>
     56
     57    <hr class="usercontrol" />
     58    <p>
     59    <span class="username">Known Sage Users:</span>
     60        {% for u in other_users %}
     61        <span class="users">
     62        <a href="javascript:add_collab('{{ u }}');" class="users">{{ u }}</a>
     63        </span>
     64        {% endfor %}
     65    </p>
    3266{% endif %}
    3367{% endblock %}
  • sagenb/data/sage/html/settings/account_settings.html

    diff -r a8d3d264fcd4 -r 4994a363ec27 sagenb/data/sage/html/settings/account_settings.html
    a b  
    2020            </select>
    2121        </div>
    2222    </div>
    23     <div class="section">
     23    {% if form_password %}
     24        <div class="section">
    2425        <h2>Change Password</h2>
    2526        <div id="passwd">
    2627            {% if message -%}
     
    3940                <input type="password" name="retype-pass" />
    4041            </div>
    4142        </div>
    42     </div>
     43        </div>
     44    {% endif %}
    4345
    44     {% if true %}
    45     <div class="section">
     46    {% if form_email %}
     47        <div class="section">
    4648        <h2>Change E-mail Address</h2>
    4749       
    4850        <div>
     
    5052                <label>Current e-mail</label>
    5153                {{ email_address }} {{ email_confirmed }}
    5254            </div>
     55        </div>
     56        </div>
     57    {% else %}
     58    <div class="section">
    5359            <div>
    54                 <label for="new-email">New e-mail</label>
    55                 <input type="text" name="new-email" class="c1" />
     60            <div>
     61                Your Email: {{ email_address }}   
    5662            </div>
    5763        </div>
    5864    </div>
  • sagenb/notebook/conf.py

    diff -r a8d3d264fcd4 -r 4994a363ec27 sagenb/notebook/conf.py
    a b  
    1010#                  http://www.gnu.org/licenses/
    1111#############################################################################
    1212
     13POS = 'pos'
    1314DESC = 'desc'
    1415GROUP = 'group'
    1516TYPE = 'type'
     
    151152            s += '<div class="section">\n  <h2>%s</h2>\n  <table>\n' % group
    152153
    153154            opts = G[group]
    154             opts.sort()
     155            try:
     156                opts.sort(lambda x,y: cmp(DS[x][POS], DS[y][POS]))
     157            except KeyError:
     158                opts.sort()
     159
    155160            for o in opts:
    156161                s += '    <tr>\n      <td>%s</td>\n      <td>\n' % DS[o][DESC]
    157162                input_type = 'text'
  • sagenb/notebook/notebook.py

    diff -r a8d3d264fcd4 -r 4994a363ec27 sagenb/notebook/notebook.py
    a b  
    9393            # Worksheet has never been saved before, so the server conf doesn't exist.
    9494            self.__worksheets = {}
    9595
    96         from user_manager import SimpleUserManager, OpenIDUserManager
    97         self._user_manager = OpenIDUserManager(conf=self.conf()) if user_manager is None else user_manager
     96        from user_manager import ExtAuthUserManager
     97        self._user_manager = ExtAuthUserManager(conf=self.conf()) if user_manager is None else user_manager
    9898
    9999        # Set the list of users
    100100        try:
     
    212212        """
    213213        return self.user_manager().user(username)
    214214
    215     def valid_login_names(self):
     215    def known_users(self):
    216216        """
    217217        Return a list of users that can log in.
    218218
     
    224224
    225225            sage: nb = sagenb.notebook.notebook.Notebook(tmp_dir()+'.sagenb')
    226226            sage: nb.create_default_users('password')
    227             sage: nb.valid_login_names()
     227            sage: nb.known_users()
    228228            ['admin']
    229229            sage: nb.user_manager().add_user('Mark', 'password', '', force=True)
    230230            sage: nb.user_manager().add_user('Sarah', 'password', '', force=True)
    231231            sage: nb.user_manager().add_user('David', 'password', '', force=True)
    232             sage: sorted(nb.valid_login_names())
     232            sage: sorted(nb.known_users())
    233233            ['David', 'Mark', 'Sarah', 'admin']
    234234        """
    235         return self.user_manager().valid_login_names()
     235        return self.user_manager().known_users()
    236236
    237237    ##########################################################
    238238    # Publishing worksheets
     
    10451045                        username = username, rev = rev, prev_rev = prev_rev,
    10461046                        next_rev = next_rev, time_ago = time_ago)
    10471047
    1048     def html_share(self, worksheet, username):
     1048    def html_share(self, worksheet, username, lookup=None):
    10491049        r"""
    10501050        Return the HTML for the "share" page of a worksheet.
    10511051
     
    10701070        other_users = [x for x, u in U.iteritems() if not u.is_guest() and not u.username() in [username, 'pub', '_sage_']]
    10711071        other_users.sort(lambda x,y: cmp(x.lower(), y.lower()))
    10721072
     1073        lookup_result = self.user_manager().user_lookup(lookup) if lookup else None
     1074        if lookup_result is not None:
     1075            lookup_result.sort(lambda x,y: cmp(x.lower(), y.lower()))
     1076            if username in lookup_result:
     1077                lookup_result.remove(username)
     1078
     1079
    10731080        return template(os.path.join("html", "notebook", "worksheet_share.html"),
    10741081                        worksheet = worksheet,
    10751082                        notebook = self,
    1076                         username = username, other_users = other_users)
     1083                        username = username,
     1084                        other_users = other_users,
     1085                        lookup = lookup,
     1086                        lookup_result = lookup_result)
    10771087   
    10781088    def html_download_or_delete_datafile(self, ws, username, filename):
    10791089        r"""
  • sagenb/notebook/server_conf.py

    diff -r a8d3d264fcd4 -r 4994a363ec27 sagenb/notebook/server_conf.py
    a b  
    55import copy
    66
    77import conf
    8 from conf import (DESC, GROUP, TYPE, CHOICES, T_BOOL, T_INTEGER,
     8from conf import (POS, DESC, GROUP, TYPE, CHOICES, T_BOOL, T_INTEGER,
    99                  T_CHOICE, T_REAL, T_COLOR, T_STRING, T_LIST)
    1010
    1111defaults = {'word_wrap_cols':72,
     
    3434            'challenge_type':'simple',
    3535            'recaptcha_public_key':'',
    3636            'recaptcha_private_key':'',
     37            'openid':False,
     38
     39            'auth_ldap':False,
     40            'ldap_uri':'ldap://example.net:389/',
     41            'ldap_basedn':'ou=users,dc=example,dc=net',
     42            'ldap_binddn':'cn=manager,dc=example,dc=net',
     43            'ldap_bindpw': 'secret',
     44            'ldap_username_attrib': 'cn',
     45            'ldap_lookup_attribs':['cn', 'sn', 'givenName', 'mail'],
    3746            }
    3847
    3948G_APPEARANCE = 'Appearance'
    4049G_AUTH = 'Authentication'
     50G_LDAP = 'LDAP'
    4151G_SERVER = 'Server'
    4252
    4353defaults_descriptions = {
     
    138148        GROUP : G_AUTH,
    139149        TYPE : T_STRING,
    140150        },
     151    'openid': {
     152        DESC : 'Allow OpenID login',
     153        GROUP : G_AUTH,
     154        TYPE : T_BOOL,
     155        },
     156    'auth_ldap': {
     157        POS : 1,
     158        DESC : 'Enable LDAP Authentication',
     159        GROUP : G_LDAP,
     160        TYPE : T_BOOL,
     161        },
     162    'ldap_uri': {
     163        POS : 2,
     164        DESC : 'LDAP URI',
     165        GROUP : G_LDAP,
     166        TYPE : T_STRING,
     167        },
     168    'ldap_binddn': {
     169        POS : 3,
     170        DESC : 'Bind DN',
     171        GROUP : G_LDAP,
     172        TYPE : T_STRING,
     173        },
     174    'ldap_bindpw': {
     175        POS : 4,
     176        DESC : 'Bind Password',
     177        GROUP : G_LDAP,
     178        TYPE : T_STRING,
     179        },
     180    'ldap_basedn': {
     181        POS : 5,
     182        DESC : 'Base DN',
     183        GROUP : G_LDAP,
     184        TYPE : T_STRING,
     185        },
     186    'ldap_username_attrib': {
     187        POS : 6,
     188        DESC: 'Username Attribute (i.e. cn, uid or userPrincipalName)',
     189        GROUP : G_LDAP,
     190        TYPE : T_STRING,
     191        },
     192    'ldap_lookup_attribs': {
     193        POS : 7,
     194        DESC: 'Attributes for user lookup',
     195        GROUP : G_LDAP,
     196        TYPE : T_LIST,
     197        },
    141198}
    142199
    143200
  • sagenb/notebook/user.py

    diff -r a8d3d264fcd4 -r 4994a363ec27 sagenb/notebook/user.py
    a b  
    3232        self.set_password(password)
    3333        self._email = email
    3434        self._email_confirmed = False
    35         if not account_type in ['admin', 'user', 'guest']:
    36             raise ValueError("account type must be one of admin, user, or guest")
     35        if not account_type in ['admin', 'user', 'guest', 'external' ]:
     36            raise ValueError, "account type must be one of admin, user, guest or external"
    3737        self._account_type = account_type
    3838        self._conf = user_conf.UserConfiguration()
    3939        self._temporary_password = ''
     
    289289            False
    290290        """
    291291        return self._account_type == 'guest'
     292
     293    def may_change_email(self):
     294        return self._account_type in ( 'user', 'admin' )
    292295       
     296    def may_change_password(self):
     297        return self._account_type in ( 'user', 'admin' )
     298
    293299    def is_suspended(self):
    294300        """
    295301        EXAMPLES::
  • sagenb/notebook/user_manager.py

    diff -r a8d3d264fcd4 -r 4994a363ec27 sagenb/notebook/user_manager.py
    a b  
    112112            pass
    113113
    114114        raise KeyError, "no user '%s'"%username
     115
     116    def user_lookup(self, search):
     117        r = [x for x in self.users().keys() if search in x]
     118        try:
     119            r += [u for u in self._user_lookup(search) if u not in r]
     120        except AttributeError:
     121            pass
     122        return r
    115123           
    116     def valid_login_names(self):
     124    def known_users(self):
    117125        """
    118126        Return a list of users that can log in.
    119127        """
     
    487495            if user_password == crypt.crypt(password, user.SALT):
    488496                self.set_password(username, password)
    489497                return True
    490             else:
    491                 return False
    492498        else:
    493499            salt, user_password = user_password.split('$')[1:]
    494             return hashlib.sha256(salt + password).hexdigest() == user_password
     500            if hashlib.sha256(salt + password).hexdigest() == user_password:
     501                return True
     502        try:
     503            return self._check_password(username, password)
     504        except AttributeError:
     505            return False;
    495506
    496507    def get_accounts(self):
    497508        # need to use notebook's conf because those are already serialized
     
    564575        Return the user object corresponding ot a given identity_url
    565576        """
    566577        return self.user(self.get_username_from_openid(identity_url))
     578
     579
     580class ExtAuthUserManager(OpenIDUserManager):
     581    def __init__(self, conf):
     582        OpenIDUserManager.__init__(self, conf=conf)
     583        self._conf = conf
     584        # currently only 'auth_ldap' here. the key must match to a T_BOOL option in server_config.py
     585        # so we can turn this auth method on/off
     586        self._auth_methods = {
     587                    'auth_ldap': LdapAuth(self._conf),
     588                    }
     589
     590    def _user(self, username):
     591        # check all auth methods that are enabled in the notebook's config
     592        # if a valid username is found, a new User object will be created.
     593        # (if that user was already known, we wouldn't be here)
     594        for a in self._auth_methods:
     595            if self._conf[a]:
     596                u = self._auth_methods[a].check_user(username)
     597                if u:
     598                    try:
     599                        email = self._auth_methods[a].get_attrib(username, 'email')
     600                    except KeyError:
     601                        email = None
     602
     603                    self.add_user(username, password='', email=email, account_type='external', force=True)
     604                    return self.users()[username]
     605
     606        raise KeyError, "no user '%s'"%username
     607
     608    def _check_password(self, username, password):
     609        for a in self._auth_methods:
     610            if self._conf[a]:
     611                # users should be unique among auth methods
     612                u = self._auth_methods[a].check_user(username)
     613                if u:
     614                    return self._auth_methods[a].check_password(username, password)
     615        return False
     616       
     617    def _user_lookup(self, search):
     618        """
     619        Returns a list of usernames that are found when calling user_lookup on all enabled auth methods
     620        """
     621        r = []
     622        for a in self._auth_methods:
     623            if self._conf[a]:
     624                r += [u for u in self._auth_methods[a].user_lookup(search) if u not in r]
     625        return r
     626
     627    # the openid methods should not be callable if openid is disabled
     628    def get_username_from_openid(self, identity_url):
     629        if self._conf['openid']:
     630            return OpenIDUserManager.get_username_from_openid(self, identity_url)
     631        else:
     632            raise RuntimeError
     633
     634    def create_new_openid(self, identity_url, username):
     635        if self._conf['openid']:
     636            OpenIDUserManager.create_new_openid(self, identity_url, username)
     637        else:
     638            raise RuntimeError
     639    def get_user_from_openid(self, identity_url):
     640        if self._conf['openid']:
     641            return OpenIDUserManager.get_user_from_openid(self, identity_url)
     642        else:
     643            raise RuntimeError
     644
     645
     646class AuthMethod():
     647    """
     648    Abstract class for authmethods that are used by ExtAuthUserManager
     649    All auth methods must implement the following methods
     650    """
     651
     652    def __init__(self, conf):
     653        self._conf = conf
     654
     655    def check_user(self, username):
     656        raise NotImplementedError
     657
     658    def check_password(self, username, password):
     659        raise NotImplementedError
     660
     661    def get_attrib(self, username, attrib):
     662        raise NotImplementedError
     663       
     664
     665class LdapAuth(AuthMethod):
     666    """
     667    Authentication via LDAP
     668   
     669    User authentication works like this:
     670    1a. bind to LDAP with a generic (configured) DN and password
     671    1b. find the ldap object matching to username. return None when more than 1 object is found
     672    2. if 1 succeeds, bind with the user DN and the supplied password
     673
     674    User lookup:
     675    wildcard-match all configured "user lookup attributes" for
     676    the given search string
     677    """
     678    def __init__(self, conf):
     679        AuthMethod.__init__(self, conf)
     680
     681    def _ldap_search(self, query, attrlist=None):
     682        """
     683        runs any ldap query passed as arg
     684        """
     685        import ldap
     686        conn = ldap.initialize(self._conf['ldap_uri'])
     687        try:
     688            conn.simple_bind_s(self._conf['ldap_binddn'], self._conf['ldap_bindpw'])
     689        except:
     690            raise ValueError, "invalid LDAP credentials"
     691
     692        # convert query and attrlist to ascii
     693        from unicodedata import normalize
     694        attrlist = [normalize("NFKD", unicode(x)).encode('ascii', 'ignore') for x in attrlist] if attrlist is not None else None
     695        query = normalize("NFKD", unicode(query)).encode('ascii', 'ignore')
     696
     697        result = conn.search_s(self._conf['ldap_basedn'], ldap.SCOPE_SUBTREE, query, attrlist)
     698        conn.unbind_s()
     699        return result
     700       
     701    def _get_ldapuser(self, username, attrlist=None):
     702        # only alphanumeric ascii usernames allowed
     703        try:
     704            username.decode('ascii')
     705            assert(username.isalnum())
     706        except:
     707            return None
     708
     709        result = self._ldap_search("(%s=%s)" % (self._conf['ldap_username_attrib'], username), attrlist)
     710        # there can be only one
     711        if len(result) == 1:
     712            return result[0]
     713        else:
     714            return None
     715
     716    def user_lookup(self, search):
     717        # build a ldap OR query
     718        q = "(|"
     719        for a in self._conf['ldap_lookup_attribs']:
     720            q += "(%s=*%s*)" % (a, search)
     721        q += ")"
     722
     723        r = self._ldap_search(q, attrlist=[self._conf['ldap_username_attrib']])
     724        # return a list of usernames. looks quite ugly
     725        return [x[1][self._conf['ldap_username_attrib']][0] for x in r if x[1].has_key(self._conf['ldap_username_attrib'])]
     726       
     727       
     728    def check_user(self, username):
     729        u = self._get_ldapuser(username)
     730        return u is not None
     731   
     732    def check_password(self, username, password):
     733        import ldap
     734        # retrieve username's DN
     735        try:
     736            u = self._get_ldapuser(username)
     737            #u[0] is DN, u[1] is a dict with all other attributes
     738            userdn = u[0]
     739        except ValueError:
     740            return False
     741
     742        # try to bind with that DN
     743        try:
     744            conn = ldap.initialize(uri=self._conf['ldap_uri'])
     745            conn.simple_bind_s(userdn, password)
     746            conn.unbind_s()
     747            return True
     748        except ldap.INVALID_CREDENTIALS:
     749            return False
     750
     751    def get_attrib(self, username, attrib):
     752        # translate some common attribute names to their ldap equivalents, i.e. "email" is "mail
     753        attrib = 'mail' if attrib == 'email' else attrib
     754
     755        u = self._get_ldapuser(username)
     756        if u is not None:
     757            a = u[1][attrib][0] #if u[1].has_key(attrib) else '' 
     758            return a
  • sagenb/notebook/worksheet.py

    diff -r a8d3d264fcd4 -r 4994a363ec27 sagenb/notebook/worksheet.py
    a b  
    583583            ['hilbert', 'sage']
    584584        """
    585585        n = self.notebook()
    586         U = n.user_manager().users().keys()
    587         L = [x.lower() for x in U]
    588586        owner = self.owner()
    589587        self.__collaborators = []
    590588        for x in v:
    591589            y = x.lower()
    592             try:
    593                 i = L.index(y)
    594                 z = U[i]
    595                 if z != owner and z not in self.__collaborators:
    596                     self.__collaborators.append(z)
    597             except ValueError:
     590            try :
     591                u = n.user_manager().user(y).username()
     592                if u != owner and u not in self.__collaborators:
     593                    self.__collaborators.append(u)
     594            except KeyError:  #no user found
    598595                pass
    599596        self.__collaborators.sort()
    600597