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)