Roundup Tracker

It is the version for 1.5.1 which replaces the error handling "error_message.append" by "add_error_message". It is the updated version LDAPLogin2 for roundup version 1.5.1. In case of a wrong password the older version LDAPLogin2 will throw an error in roundup version 1.5.1.

This module is another LDAP/AD login method, which is a merge of ldap_login.py from Moinmoin 1.8.2 and ldaplogin.py described in LDAPLogin.

Since I could get the LDAP/AD modules working for me, I used this one since it already work quite well with Moinmoin. I am not an LDAP expert at all, therefore I only can say it works for me.

It first tries to authenticate the user against the LDAP/AD. If this is successful then it copies all user attributes over to the local roundup user database, if these values are empty in the roundup database. The attributes are username, password, phone, address, organisation, realname. The LDAP/AD login is done via a simple bind. If no LDAP/AD login is possible a fallback to the roundup database is done. Either the username/password is in the roundup database then the login succeeds or otherwise it finally fails.

To customize for your LDAP/AD environment, use the variable CONFIG_VALS. For an AD configuration you will need to adapt the values for server_uri, bind_dn, base_dn. Furthermore, it might be that you are required to adapt some of the other values depending on your LDAP/AD configuration.

In your 'extensions' directory, create a file 'ldap_login.py' with the below content. The class LDAPLoginAction is registered with the login action.

   1 #!/usr/bin/env python
   2 # -*- coding: iso-8859-1 -*-
   3 """
   4     This is an adpated MoinMoin - LDAP / Active Directory authentication to
   5     fit the needs for roundup ldap authentification. It first tries to login
   6     via LDAP (AD). In case this fails it tries to login against the roundup
   7     database. If a user is authenticated the first time via LDAP his
   8     attributes are copied over to the roundup database, including the
   9     password.  Therefore even if later on the LDAP login does not succeed a
  10     local login would require a password for authentication. If the user exist
  11     both in LDAP and in the local database, all the non-empty attributes which
  12     are empty in the local database are copied over.
  13 
  14     python-ldap needs to be at least 2.0.0pre06 (available since mid 2002) for
  15     ldaps support - some older debian installations (woody and older?) require
  16     libldap2-tls and python2.x-ldap-tls, otherwise you get ldap.SERVER_DOWN:
  17     "Can't contact LDAP server" - more recent debian installations have tls
  18     support in libldap2 (see dependency on gnu2tls) and also in python-ldap.
  19 
  20     TODO: allow more configuration (alias name, ...) by using callables as
  21     parameters
  22 
  23     @copyright: 2006-2008 MoinMoin:ThomasWaldmann,
  24                 2006 Nick Phillips
  25                 2009 Andreas Floeter: adpatation for roundup done
  26     @license: GNU GPL, see COPYING for details.
  27 """
  28 import logging
  29 import os
  30 import sys
  31 
  32 LOG = logging.getLogger(__name__)
  33 INSTPATH = "/var/log/roundup"
  34 logging.basicConfig(level=logging.DEBUG,
  35                     format='%(asctime)s %(levelname)s %(message)s',
  36                     filename=os.path.join(INSTPATH, "ldap.log"),
  37                     filemode='a')
  38 
  39 try:
  40     import ldap
  41 except ImportError, errmsg:
  42     LOG.error("You need to have python-ldap installed (%s)." % str(errmsg))
  43     raise
  44 
  45 from roundup import password as PW
  46 from roundup.cgi import exceptions
  47 from roundup.cgi.actions import LoginAction
  48 from roundup.i18n import _
  49 
  50 LOGIN_FAILED = 0
  51 LOGIN_SUCCEDED = 1
  52 
  53 DEFAULT_VALS = {
  54     'use_local_auth' : None,
  55     # ldap / active directory server URI use ldaps://server:636 url for
  56     # ldaps, use ldap://server for ldap without tls (and set start_tls to
  57     # 0), use ldap://server for ldap with tls (and set start_tls to 1 or
  58     # 2).
  59     'server_uri' : 'ldap://localhost',
  60     # We can either use some fixed user and password for binding to LDAP.
  61     # Be careful if you need a % char in those strings - as they are used
  62     # as a format string, you have to write %% to get a single % in the
  63     # end.
  64 
  65     #'bind_dn' : 'binduser@example.org' # (AD)
  66     #'bind_dn' : 'cn=admin,dc=example,dc=org' # (OpenLDAP)
  67     #'bind_pw' : 'secret'
  68     # or we can use the username and password we got from the user:
  69     #'bind_dn' : '%(username)s@example.org'
  70     # DN we use for first bind (AD)
  71     #'bind_pw' : '%(password)s' # password we use for first bind
  72     # or we can bind anonymously (if that is supported by your directory).
  73     # In any case, bind_dn and bind_pw must be defined.
  74     'bind_dn' : '',
  75     'bind_pw' : '',
  76     # base DN we use for searching
  77     #base_dn : 'ou=SOMEUNIT,dc=example,dc=org'
  78     'base_dn' : '',
  79     # scope of the search we do (2 == ldap.SCOPE_SUBTREE)
  80     'scope' : ldap.SCOPE_SUBTREE,
  81     # LDAP REFERRALS (0 needed for AD)
  82     'referrals' : 0,
  83     # ldap filter used for searching:
  84     #search_filter : '(sAMAccountName=%(username)s)' # (AD)
  85     #search_filter : '(uid=%(username)s)' # (OpenLDAP)
  86     # you can also do more complex filtering like:
  87     # "(&(cn=%(username)s)(memberOf=CN=WikiUsers,OU=Groups,\
  88     #  DC=example,DC=org))"
  89     'search_filter' : '(uid=%(username)s)',
  90     # some attribute names we use to extract information from LDAP:
  91     # ('givenName') ldap attribute we get the first name from
  92     'givenname_attribute' : None,
  93     # ('sn') ldap attribute we get the family name from
  94     'surname_attribute' : None,
  95     # ('displayName') ldap attribute we get the aliasname from
  96     'aliasname_attribute' : None,
  97     # ('mail') ldap attribute we get the email address from
  98     'email_attribute' : None,
  99     # called to make up email address
 100     'email_callback' : None,
 101     # phone number
 102     'telephonenumber_attribute' : None,
 103     # department
 104     'department_attribute' : None,
 105     # coding used for ldap queries and result values
 106     'coding' : 'utf-8',
 107     # how long we wait for the ldap server [s]
 108     'timeout' : 10,
 109     # 0 = No, 1 = Try, 2 = Required
 110     'start_tls' : 0,
 111     'tls_cacertdir' : '',
 112     'tls_cacertfile' : '',
 113     'tls_certfile' : '',
 114     'tls_keyfile' : '',
 115     # 0 == ldap.OPT_X_TLS_NEVER (needed for self-signed certs)
 116     'tls_require_cert' : 0,
 117     # set to True to only do one bind - useful if configured to bind as
 118     # the user on the first attempt
 119     'bind_once' : False,
 120     # set to True if you want to autocreate user profiles
 121     'autocreate' : False,
 122     }
 123 
 124 CONFIG_VALS = {'referrals' : 0,
 125                'use_local_auth' : None,
 126                'server_uri' : 'ldap://ad_server.your.domain',
 127                'bind_dn' : '%(username)s@AD.DOMAIN.NAME',
 128                'bind_pw' : '%(password)s',
 129                'base_dn' : 'dc=ad,dc=domain,dc=name',
 130                'search_filter' : '(sAMAccountName=%(username)s)',
 131                'givenname_attribute' : 'givenName',
 132                'surname_attribute' : 'sn',
 133                'aliasname_attribute' : 'displayName',
 134                'email_attribute' : 'mail',
 135                'telephonenumber_attribute' : 'telephoneNumber',
 136                'department_attribute' : 'department',
 137                'autocreate' : True
 138                }
 139 
 140 class LDAPLoginAction(LoginAction):
 141     """ get authentication data from form, authenticate against LDAP (or Active
 142         Directory), fetch some user infos from LDAP and create a user object
 143         for that user. The session is kept by moin automatically.
 144     """
 145     def __init__(self, *args):
 146         self.set_values(DEFAULT_VALS)
 147         # self.use_local_auth = use_local_auth
 148 
 149         # self.server_uri = server_uri
 150         # self.bind_dn = bind_dn
 151         # self.bind_pw = bind_pw
 152         # self.base_dn = base_dn
 153         # self.scope = scope
 154         # self.referrals = referrals
 155         # self.search_filter = search_filter
 156 
 157         # self.givenname_attribute = givenname_attribute
 158         # self.surname_attribute = surname_attribute
 159         # self.aliasname_attribute = aliasname_attribute
 160         # self.email_attribute = email_attribute
 161         # self.email_callback = email_callback
 162         # self.telephonenumber_attribute = telephonenumber_attribute
 163         # self.department_attribute = department_attribute
 164 
 165         # self.coding = coding
 166         # self.timeout = timeout
 167 
 168         # self.start_tls = start_tls
 169         # self.tls_cacertdir = tls_cacertdir
 170         # self.tls_cacertfile = tls_cacertfile
 171         # self.tls_certfile = tls_certfile
 172         # self.tls_keyfile = tls_keyfile
 173         # self.tls_require_cert = tls_require_cert
 174 
 175         # self.bind_once = bind_once
 176         # self.autocreate = autocreate
 177         LoginAction.__init__(self, *args)
 178 
 179     def ldap_login(self, username='', password=''):
 180         """Perform a login against LDAP."""
 181         self.auth_method = 'ldap'
 182 
 183         # we require non-empty password as ldap bind does a anon (not password
 184         # protected) bind if the password is empty and SUCCEEDS!
 185         if not password:
 186             msg = _('Empty password for user "%s"') % self.client.user
 187             LOG.debug(msg)
 188             self.client.add_error_message(msg)
 189             return LOGIN_FAILED
 190         try:
 191             try:
 192 #                u = None
 193                 dn = None
 194                 coding = self.coding
 195                 LOG.debug("Setting misc. ldap options...")
 196                 # ldap v2 is outdated
 197                 ldap.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3)
 198                 ldap.set_option(ldap.OPT_REFERRALS, self.referrals)
 199                 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
 200 
 201                 if hasattr(ldap, 'TLS_AVAIL') and ldap.TLS_AVAIL:
 202                     for option, value in (
 203                         (ldap.OPT_X_TLS_CACERTDIR, self.tls_cacertdir),
 204                         (ldap.OPT_X_TLS_CACERTFILE, self.tls_cacertfile),
 205                         (ldap.OPT_X_TLS_CERTFILE, self.tls_certfile),
 206                         (ldap.OPT_X_TLS_KEYFILE, self.tls_keyfile),
 207                         (ldap.OPT_X_TLS_REQUIRE_CERT, self.tls_require_cert),
 208                         (ldap.OPT_X_TLS, self.start_tls),
 209                         #(ldap.OPT_X_TLS_ALLOW, 1),
 210                     ):
 211                         if value is not None:
 212                             ldap.set_option(option, value)
 213 
 214                 server = self.server_uri
 215                 LOG.debug("Trying to initialize %r." % server)
 216                 l = ldap.initialize(server)
 217                 LOG.debug("Connected to LDAP server %r." % server)
 218 
 219                 if self.start_tls and server.startswith('ldap:'):
 220                     LOG.debug("Trying to start TLS to %r." % server)
 221                     try:
 222                         l.start_tls_s()
 223                         LOG.debug("Using TLS to %r." % server)
 224                     except (ldap.SERVER_DOWN, ldap.CONNECT_ERROR), err:
 225                         LOG.warning("Couldn't establish TLS to %r (err: %s)." %\
 226                                         (server, str(err)))
 227                         return LOGIN_FAILED
 228 
 229                 # you can use %(username)s and %(password)s here to get the
 230                 # stuff entered in the form:
 231                 binddn = self.bind_dn % locals()
 232                 bindpw = self.bind_pw % locals()
 233                 l.simple_bind_s(binddn.encode(coding), bindpw.encode(coding))
 234                 LOG.debug("Bound with binddn %r" % binddn)
 235 
 236                 # you can use %(username)s here to get the stuff entered in
 237                 # the form:
 238                 filterstr = self.search_filter % locals()
 239                 LOG.debug("Searching %r" % filterstr)
 240                 attrs = [getattr(self, attr) for attr in [
 241                                          'email_attribute',
 242                                          'aliasname_attribute',
 243                                          'surname_attribute',
 244                                          'givenname_attribute',
 245                                          'telephonenumber_attribute',
 246                                          'department_attribute',
 247                                          ] if getattr(self, attr) is not None]
 248                 lusers = l.search_st(self.base_dn, self.scope,
 249                                      filterstr.encode(coding), attrlist=attrs,
 250                                      timeout=self.timeout)
 251                 # we remove entries with dn == None to get the real result list:
 252                 lusers = [(dn, ldap_dict) for dn, ldap_dict in lusers \
 253                               if dn is not None]
 254                 for dn, ldap_dict in lusers:
 255                     LOG.debug("dn:%r" % dn)
 256                     for key, val in ldap_dict.items():
 257                         LOG.debug("    %r: %r" % (key, val))
 258 
 259                 result_length = len(lusers)
 260                 if result_length != 1:
 261                     if result_length > 1:
 262                         LOG.warning("Search found more than one (%d) matches \
 263 for %r." % (result_length, filterstr))
 264                     if result_length == 0:
 265                         LOG.debug("Search found no matches for %r." % \
 266                                       (filterstr, ))
 267                     msg = _("Invalid username or password.")
 268                     LOG.debug(msg)
 269                     self.client.add_error_message(msg)
 270                     return LOGIN_FAILED
 271 
 272                 dn, ldap_dict = lusers[0]
 273                 if not self.bind_once:
 274                     LOG.debug("DN found is %r, trying to bind with pw" % dn)
 275                     l.simple_bind_s(dn, password.encode(coding))
 276                     LOG.debug("Bound with dn %r (username: %r)" % \
 277                                   (dn, username))
 278 
 279                 if self.email_callback is None:
 280                     if self.email_attribute:
 281                         email = ldap_dict.get(self.email_attribute, [''])[0].\
 282 decode(coding)
 283                     else:
 284                         email = None
 285                 else:
 286                     email = self.email_callback(ldap_dict)
 287 
 288                 aliasname = ''
 289                 try:
 290                     aliasname = ldap_dict[self.aliasname_attribute][0]
 291                 except (KeyError, IndexError):
 292                     pass
 293                 if not aliasname:
 294                     sn = ldap_dict.get(self.surname_attribute, [''])[0]
 295                     gn = ldap_dict.get(self.givenname_attribute, [''])[0]
 296                     if sn and gn:
 297                         aliasname = "%s, %s" % (sn, gn)
 298                     elif sn:
 299                         aliasname = sn
 300                 aliasname = aliasname.decode(coding)
 301                 try:
 302                     phonenumber = ldap_dict.get(self.telephonenumber_attribute,
 303                                                 [''])[0]
 304                 except (KeyError, IndexError):
 305                     phonenumber = ''
 306                 try:
 307                     department = ldap_dict.get(self.department_attribute,
 308                                                [''])[0]
 309                 except (KeyError, IndexError):
 310                     department = ''
 311 
 312                 LOG.debug("User data [%r, %r, %r, %r, %r] " % \
 313                               (username, phonenumber, email, department,
 314                                aliasname))
 315                 self.add_attr_local_user(username=username,
 316                                          password=password,
 317                                          phone=phonenumber,
 318                                          address=email,
 319                                          organisation=department,
 320                                          realname=aliasname)
 321                 msg = "Login succeded with LDAP authentication for user '%s'." \
 322 % username
 323                 LOG.debug(msg)
 324                 # Determine whether the user has permission to log in. Base
 325                 # behaviour is to check the user has "Web Access".
 326                 rights = "Web Access"
 327                 if not self.hasPermission(rights):
 328                     msg = _("You do not have permission '%s' to login" % rights)
 329                     LOG.debug("%s, %s, %s", msg, self.client.user, rights)
 330                     raise exceptions.LoginError, msg
 331                 return LOGIN_SUCCEDED
 332             except ldap.INVALID_CREDENTIALS, err:
 333                 LOG.debug("invalid credentials (wrong password?) for dn %r \
 334 (username: %r)" % (dn, username))
 335                 return LOGIN_FAILED
 336         except ldap.SERVER_DOWN, err:
 337             # looks like this LDAP server isn't working, so we just try the
 338             # next authenticator object in cfg.auth list (there could be some
 339             # second ldap authenticator that queries a backup server or any
 340             # other auth method).
 341             ## only one auth server supported for roundup, change it
 342             LOG.error("LDAP server %s failed (%s). Trying to authenticate \
 343 with next auth list entry." % (server, str(err)))
 344             msg = "LDAP server %(server)s failed." % {'server': server}
 345             LOG.debug(msg)
 346             return LOGIN_FAILED
 347         except Exception, err:
 348             LOG.error("Couldn't establish TLS to %r (err: %s)." % (server,
 349                                                                      str(err)))
 350             LOG.exception("caught an exception, traceback follows...")
 351             return LOGIN_FAILED
 352 
 353     def set_values(self, props):
 354         for kprop, value in props.items():
 355             setattr(self, kprop, value)
 356 
 357     def local_user_exists(self):
 358         """Verify if the given user exists. As a side effect set the
 359         'client.userid'."""
 360         # make sure the user exists
 361         try:
 362             self.client.userid = self.db.user.lookup(self.client.user)
 363         except KeyError:
 364             msg = _("Unknown user '%s'") % self.client.user
 365             LOG.debug("__['%s'", msg)
 366             self.client.add_error_message(
 367                         _("Unknown user  '%s'") % self.client.user)
 368             return False
 369         return True
 370 
 371     def local_login(self, password):
 372         """Try local authentication."""
 373         self.auth_method = 'localdb'
 374         if not self.local_user_exists():
 375             return LOGIN_FAILED
 376         if not self.verifyPassword(self.client.userid, password):
 377             msg = _('Invalid password')
 378             LOG.debug("%s for userid=%s", msg, self.client.userid)
 379             self.client.add_error_message(msg)
 380             return LOGIN_FAILED
 381 
 382         # Determine whether the user has permission to log in. Base behaviour
 383         # is to check the user has "Web Access".
 384         rights = "Web Access"
 385         if not self.hasPermission(rights):
 386             msg = _("You do not have permission to login")
 387             LOG.debug("%s, %s, %s", msg, self.client.user, rights)
 388             raise exceptions.LoginError, msg
 389         return LOGIN_SUCCEDED
 390 
 391     def verifyLogin(self, username, password):
 392         """Verify the login of `username` with `password`. Try first LDAP if
 393         this is specified as authentication source, and then login against
 394         local database."""
 395         LOG = self.db.get_logger()
 396         LOG.debug("username=%s password=%s", username, '*'*len(password))
 397         self.set_values(CONFIG_VALS)
 398         authenticated = False
 399         if not self.use_local_auth:
 400             LOG.debug("LDAP authentication")
 401             authenticated = self.ldap_login(username, password)
 402             if authenticated:
 403                 LOG.debug("User '%s' authenticated against LDAP.",
 404                           username)
 405         if not authenticated:
 406             LOG.debug("Local database authentication")
 407             authenticated = self.local_login(password)
 408             if authenticated:
 409                 LOG.debug("User '%s' authenticated against local database.",
 410                           username)
 411         if not authenticated:
 412             msg = _("Could not authenticate user '%s'" % username)
 413             LOG.debug(msg)
 414             raise exceptions.LoginError, msg
 415         return authenticated
 416 
 417     def add_attr_local_user(self, **props):
 418         """Add the attributes `props` for a user to the local database if
 419         those are still empty. If 'self.autocreate' is False then the user is
 420         considered a new user."""
 421         props['password'] = PW.Password(props['password'])
 422         self.db.journaltag = 'admin'
 423         try:
 424             self.client.userid = self.db.user.lookup(self.client.user)
 425             # update the empty values with LDAP values
 426             uid = self.client.userid
 427             if self.autocreate:
 428                 for pkey, prop in props.items():
 429                     try:
 430                         LOG.debug("Look key '%s' for user '%s'", pkey, uid)
 431                         value = self.db.user.get(uid, pkey)
 432                         LOG.debug("Value %r for key,user '%s','%s'", value,
 433                                   pkey, uid)
 434                         if not value:
 435                             LOG.debug("Set value %r for property %r of user \
 436 '%s'", props[pkey], pkey, self.client.user)
 437                             pair = {pkey : props[pkey]}
 438                             self.db.user.set(uid, **pair)
 439                     except Exception, err_msg:
 440                         LOG.exception("caught an exception, traceback follows.\
 441 ..")
 442         except KeyError:
 443             # add new user to local database
 444             props['roles'] = self.db.config.NEW_WEB_USER_ROLES
 445             self.userid = self.db.user.create(**props)
 446             self.db.commit()
 447             ## ?? why do we re-read the userid ??
 448             # self.client.userid = self.db.user.lookup(self.client.user)
 449             msg = u"New account created for user '%s'" % props['username']
 450             LOG.debug(msg)
 451             self.client.ok_message.append(msg)
 452 
 453 def init(instance):
 454     """Register the roundup action 'login'."""
 455     instance.registerAction('login', LDAPLoginAction)


CategoryActions CategoryAuthentication