Merge lp:~openerp-community/openobject-addons/stefan-therp_lp794584 into lp:openobject-addons
- stefan-therp_lp794584
- Merge into trunk
Status: | Merged | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Approved by: | Olivier Dony (Odoo) | ||||||||||||
Approved revision: | no longer in the source branch. | ||||||||||||
Merged at revision: | 5520 | ||||||||||||
Proposed branch: | lp:~openerp-community/openobject-addons/stefan-therp_lp794584 | ||||||||||||
Merge into: | lp:openobject-addons | ||||||||||||
Diff against target: |
373 lines (+198/-116) 1 file modified
users_ldap/users_ldap.py (+198/-116) |
||||||||||||
To merge this branch: | bzr merge lp:~openerp-community/openobject-addons/stefan-therp_lp794584 | ||||||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Olivier Dony (Odoo) | Approve | ||
Stefan Rijnhart (Opener) (community) | Needs Resubmitting | ||
Review via email: mp+63872@code.launchpad.net |
Commit message
Description of the change
This branch breaks down LDAP authentication into a number of atomic functions so that additional modules can more easily override specific functionality.
Olivier Dony (Odoo) (odo-openerp) wrote : | # |
Stefan Rijnhart (Opener) (stefan-opener) wrote : | # |
On 17-06-11 15:51, Olivier Dony (OpenERP) wrote:
> Review: Needs Information
> Hello Stefan,
>
> I agree with the idea of the bug report "wishlist", I wonder, isn't this use case sufficiently specific that it could be done in a separate module (e.g. 'ldap_import')?
> Using the "template user" system alone is perhaps sufficient for the normal case?
>
> Now, regardless of the above remark, some initial comments:
>
> - the bind() call should now support anonymous login as provided by your other merge proposal ;-)
> - DRY question: shouldn't the code of getting an ldap session, and the code of deciding/creating a user be refactored into separate functions? If we're starting to see it 2 or 3 times, it's time to think about it. Splitting these into separate functions would also allow them to be reused/extended into other module, such as perhaps an "ldap_import" one.
>
> What do you think?
Hi Olivier,
you hit the chicken right on the egg! If the basic functionality in the
module were sufficiently broken down, I would prefer to create a new
module for this particular use case. As it is however, I would have to
duplicate a large part of the basic functionality in the new module (I
had no problem doing so in the original module as this merely
demonstrates the need for refacturing).
I will therefore reuse this branch to suggest such a breakdown in
separate functions, and leave the added functionality for a separate
module of our own make.
Cheers,
Stefan.
--
Therp - Maatwerk in open ontwikkeling
Stefan Rijnhart - Ontwerp en implementatie
mail: <email address hidden>
tel: +31 (0) 614478606
web: http://
Olivier Dony (Odoo) (odo-openerp) wrote : | # |
On 06/20/2011 10:21 AM, Stefan Rijnhart (Therp) wrote:
> I will therefore reuse this branch to suggest such a breakdown in
> separate functions, and leave the added functionality for a separate
> module of our own make.
Sounds great, thanks! :-)
Stefan Rijnhart (Opener) (stefan-opener) wrote : | # |
> On 06/20/2011 10:21 AM, Stefan Rijnhart (Therp) wrote:
> > I will therefore reuse this branch to suggest such a breakdown in
> > separate functions, and leave the added functionality for a separate
> > module of our own make.
>
> Sounds great, thanks! :-)
Hi Olivier,
this branch now features the refactured version of users_ldap. Here is an account of the changes I made:
[REM] reverted the populate functionality, moving to extra addon
As discussed, my team will publish a separate module with the populate functionality.
[REF] split up LDAP authentication in atomic methods
As discussed, res.company.ldap now has simple methods for retrieving the ldap configurations, connecting to the ldap server, query the subtree with the user accounts, authenticate dn/password combinations and compose a set of field values for creating res.users.
[IMP] leave it to res.users' defaults to select menu item for new users
Selecting the correct ir_action_action was broken in the module. After having installed several modules, a dashboard from one of these modules would be picked. I noticed that res.users assigned the correct default anyway and removed the corresponding code from users_ldap. As for the user's action_id, users_ldap used to set the menu action here as well which caused a continuous refreshing in the web client when such a user logs on. As res.users leaves this value empty by default, I removed this code as well.
[REF] remove unnecessary lambdas from defaults
[REF] improve pep8 compatibility
Respect 79 columns almost everywhere. Load system libraries first.
[REF] remove unnecessary conditionals when an exception is raised anyway
The old code checks the result of the user authentication against the ldap server. However, an unsuccesful authentication raises an exception which was handled already, rendering the conditional meaningless.
[IMP] do not log redundant exception ldap.INVALID_
An invalid login is already logged by WEB-SERVICE.
[IMP] replaced generic exceptions by ldap subclass
[FIX] update timestamp when LDAP user logs in
[FIX] prevent inactive LDAP users from logging in
Looking for guidance in res.users, I encounted these aspects of logins and I remembered coming across lp:784501. It seemed artificial to make an extra effort to preserve the original behaviour, but admittedly this is the only area in which the behaviour of the module has actually changed.
[FIX] use ldap.search_st() which actually does take the timeout argument
The old code uses ldap.search() with a timeout argument, but this function takes no such argument. In fact, ldap.search() is an asynchronous function which the old code does not anticipate so it seems to me that ldap.search_st() was the intended function. Also, the new code replaces ldap.open(), which is deprecated, by ldap.initialize().
Cheers,
Stefan.
Olivier Dony (Odoo) (odo-openerp) wrote : | # |
Hello Stefan,
Wow, very impressive work, thanks!!
Here are a few comments (line numbers are within current diff):
- we're trying to unify out docstring formats (at least for new ones, as indeed the old ones are still a mess), and trying to use RST docstrings, so that they will be readily rendered within our online doc, with the help of sphinx's autodoc module. The format is not very different, you can have a look at examples [1][2] for the format, as well as the sphinx reference [3].
- to improve the signal-to-noise ratio of the documentation, we are trying to remove all redundant documentation of the 'cr', 'uid' and 'ids' parameters. Existing docstrings have a lot of these, but we're trying to stop/remove that. 'context' can be documented when a specific key/value in it could impact the behavior of the function, otherwise it's useless too.
- any particular reason you dropped the comments about the anonymous authentication ?
- l.94, l.278, l.339: not sure if this was done on purpose, but when logging from within an except block, you can simply use logger.
- l.183: the explicit test on empty passwords at the start of login() is really needed, because login() does not behave like check() and does not raise in case of failure. Therefore we can't distinguish super().login() returning False because of an empty password or because of a wrong password => we need to test this case explicitly, otherwise we'll perform an anonymous auth of the _user_, something that must be prevented by applications, as discussed previously! ;-)
- l.264: 'name' seems unused now?
- l.257-226 and 324-333 seem mostly identical, how about abstracting them out also in their own overridable function, e.g. auth_user() or similar, with only the code handling the result/exception being different?
Everything else looks quite excellent, well done and thanks a lot!
[1] Simple (outdated) example: http://
[2] Complicated example, showing many options http://
[3] Sphinx reference: http://
[4] http://
Stefan Rijnhart (Opener) (stefan-opener) wrote : | # |
Hi Olivier,
glad you like our work! Thank you for your valuable comments. I have tried to honour all of them in my latest commit. Additional comments:
> - l.94, l.278, l.339: not sure if this was done on purpose, but when logging
> from within an except block, you can simply use logger.
> automatically log the full exception info and trace
Did not think that the trace is very interesting in these cases, as the exceptions most likely occur due to a configuration error or an error on the side of the LDAP server. I did change the code so as to leave the formatting to the logger though.
> - l.183: the explicit test on empty passwords at the start of login() is
> really needed
Ouch, that hurts :-( Thank you for saving me there! I put the check right in the heart of the authorization method of the ldap class, so there is no escaping it now.
Regards,
Stefan.
Olivier Dony (Odoo) (odo-openerp) wrote : | # |
Excellent work, Stefan, looks good for merging.
Nice touch with the RFC reference in the docstrings. I didn't know the :rfc: RST role yet :-)
Thanks a lot for another great contribution!
Olivier Dony (Odoo) (odo-openerp) wrote : | # |
Just merged after re-testing and replacing UID 1 with the new constant meant for that.
Thanks for your great work on this!
I will proceed with Ian's dependent branch next.
Preview Diff
1 | === modified file 'users_ldap/users_ldap.py' |
2 | --- users_ldap/users_ldap.py 2011-06-16 16:02:19 +0000 |
3 | +++ users_ldap/users_ldap.py 2011-07-27 22:12:47 +0000 |
4 | @@ -18,19 +18,181 @@ |
5 | # |
6 | ############################################################################## |
7 | |
8 | +import ldap |
9 | +from ldap.filter import filter_format |
10 | from osv import fields, osv |
11 | import pooler |
12 | import tools |
13 | import logging |
14 | from service import security |
15 | -import ldap |
16 | -from ldap.filter import filter_format |
17 | - |
18 | |
19 | class CompanyLDAP(osv.osv): |
20 | _name = 'res.company.ldap' |
21 | _order = 'sequence' |
22 | _rec_name = 'ldap_server' |
23 | + |
24 | + def get_ldap_dicts(self, cr, ids=None): |
25 | + """ |
26 | + Retrieve res_company_ldap resources from the database in dictionary |
27 | + format. |
28 | + |
29 | + :param list ids: Valid ids of model res_company_ldap. If not \ |
30 | + specified, process all resources (unlike other ORM methods). |
31 | + :return: ldap configurations |
32 | + :rtype: list of dictionaries |
33 | + """ |
34 | + |
35 | + if ids: |
36 | + id_clause = 'AND id IN (%s)' |
37 | + args = [tuple(ids)] |
38 | + else: |
39 | + id_clause = '' |
40 | + args = [] |
41 | + cr.execute(""" |
42 | + SELECT id, company, ldap_server, ldap_server_port, ldap_binddn, |
43 | + ldap_password, ldap_filter, ldap_base, "user", create_user |
44 | + FROM res_company_ldap |
45 | + WHERE ldap_server != '' """ + id_clause + """ ORDER BY sequence |
46 | + """, args) |
47 | + return cr.dictfetchall() |
48 | + |
49 | + def connect(self, conf): |
50 | + """ |
51 | + Connect to an LDAP server specified by an ldap |
52 | + configuration dictionary. |
53 | + |
54 | + :param dict conf: LDAP configuration |
55 | + :return: an LDAP object |
56 | + """ |
57 | + |
58 | + uri = 'ldap://%s:%d' % (conf['ldap_server'], |
59 | + conf['ldap_server_port']) |
60 | + return ldap.initialize(uri) |
61 | + |
62 | + def authenticate(self, conf, login, password): |
63 | + """ |
64 | + Authenticate a user against the specified LDAP server. |
65 | + |
66 | + In order to prevent an unintended 'unauthenticated authentication', |
67 | + which is an anonymous bind with a valid dn and a blank password, |
68 | + check for empty passwords explicitely (:rfc:`4513#section-6.3.1`) |
69 | + |
70 | + :param dict conf: LDAP configuration |
71 | + :param login: username |
72 | + :param password: Password for the LDAP user |
73 | + :return: LDAP entry of authenticated user or False |
74 | + :rtype: dictionary of attributes |
75 | + """ |
76 | + |
77 | + if not password: |
78 | + return False |
79 | + |
80 | + entry = False |
81 | + filter = filter_format(conf['ldap_filter'], (login,)) |
82 | + try: |
83 | + results = self.query(conf, filter) |
84 | + if results and len(results) == 1: |
85 | + dn = results[0][0] |
86 | + conn = self.connect(conf) |
87 | + conn.simple_bind_s(dn, password) |
88 | + conn.unbind() |
89 | + entry = results[0] |
90 | + except ldap.INVALID_CREDENTIALS: |
91 | + return False |
92 | + except ldap.LDAPError, e: |
93 | + logger = logging.getLogger('orm.ldap') |
94 | + logger.error('An LDAP exception occurred: %s', e) |
95 | + return entry |
96 | + |
97 | + def query(self, conf, filter, retrieve_attributes=None): |
98 | + """ |
99 | + Query an LDAP server with the filter argument and scope subtree. |
100 | + |
101 | + Allow for all authentication methods of the simple authentication |
102 | + method: |
103 | + |
104 | + - authenticated bind (non-empty binddn + valid password) |
105 | + - anonymous bind (empty binddn + empty password) |
106 | + - unauthenticated authentication (non-empty binddn + empty password) |
107 | + |
108 | + .. seealso:: |
109 | + :rfc:`4513#section-5.1` - LDAP: Simple Authentication Method. |
110 | + |
111 | + :param dict conf: LDAP configuration |
112 | + :param filter: valid LDAP filter |
113 | + :param list retrieve_attributes: LDAP attributes to be retrieved. \ |
114 | + If not specified, return all attributes. |
115 | + :return: ldap entries |
116 | + :rtype: list of tuples (dn, attrs) |
117 | + |
118 | + """ |
119 | + |
120 | + results = [] |
121 | + logger = logging.getLogger('orm.ldap') |
122 | + try: |
123 | + conn = self.connect(conf) |
124 | + conn.simple_bind_s(conf['ldap_binddn'] or '', |
125 | + conf['ldap_password'] or '') |
126 | + results = conn.search_st(conf['ldap_base'], ldap.SCOPE_SUBTREE, |
127 | + filter, retrieve_attributes, timeout=60) |
128 | + conn.unbind() |
129 | + except ldap.INVALID_CREDENTIALS: |
130 | + logger.error('LDAP bind failed.') |
131 | + except ldap.LDAPError, e: |
132 | + logger.error('An LDAP exception occurred: %s', e) |
133 | + return results |
134 | + |
135 | + def map_ldap_attributes(self, cr, uid, conf, login, ldap_entry): |
136 | + """ |
137 | + Compose values for a new resource of model res_users, |
138 | + based upon the retrieved ldap entry and the LDAP settings. |
139 | + |
140 | + :param dict conf: LDAP configuration |
141 | + :param login: the new user's login |
142 | + :param tuple ldap_entry: single LDAP result (dn, attrs) |
143 | + :return: parameters for a new resource of model res_users |
144 | + :rtype: dict |
145 | + """ |
146 | + |
147 | + values = { 'name': ldap_entry[1]['cn'][0], |
148 | + 'login': login, |
149 | + 'company_id': conf['company'] |
150 | + } |
151 | + return values |
152 | + |
153 | + def get_or_create_user(self, cr, uid, conf, login, ldap_entry, |
154 | + context=None): |
155 | + """ |
156 | + Retrieve an active resource of model res_users with the specified |
157 | + login. Create the user if it is not initially found. |
158 | + |
159 | + :param dict conf: LDAP configuration |
160 | + :param login: the user's login |
161 | + :param tuple ldap_entry: single LDAP result (dn, attrs) |
162 | + :return: res_users id |
163 | + :rtype: int |
164 | + """ |
165 | + |
166 | + user_id = False |
167 | + login = tools.ustr(login) |
168 | + cr.execute("SELECT id, active FROM res_users WHERE login=%s", (login,)) |
169 | + res = cr.fetchone() |
170 | + if res: |
171 | + if res[1]: |
172 | + user_id = res[0] |
173 | + elif conf['create_user']: |
174 | + logger = logging.getLogger('orm.ldap') |
175 | + logger.debug("Creating new OpenERP user \"%s\" from LDAP" % login) |
176 | + user_obj = self.pool.get('res.users') |
177 | + values = self.map_ldap_attributes(cr, uid, conf, login, ldap_entry) |
178 | + if conf['user']: |
179 | + user_id = user_obj.copy(cr, 1, conf['user'], |
180 | + default={'active': True}) |
181 | + user_obj.write(cr, 1, user_id, values) |
182 | + else: |
183 | + user_id = user_obj.create(cr, 1, values) |
184 | + return user_id |
185 | + |
186 | _columns = { |
187 | 'sequence': fields.integer('Sequence'), |
188 | 'company': fields.many2one('res.company', 'Company', required=True, |
189 | @@ -43,7 +205,7 @@ |
190 | 'ldap_password': fields.char('LDAP password', size=64, |
191 | help=("The password of the user account on the LDAP server that is " |
192 | "used to query the directory.")), |
193 | - 'ldap_filter': fields.char('LDAP filter', size=64, required=True), |
194 | + 'ldap_filter': fields.char('LDAP filter', size=256, required=True), |
195 | 'ldap_base': fields.char('LDAP base', size=64, required=True), |
196 | 'user': fields.many2one('res.users', 'Model User', |
197 | help="Model used for user creation"), |
198 | @@ -51,10 +213,10 @@ |
199 | help="Create the user if not in database"), |
200 | } |
201 | _defaults = { |
202 | - 'ldap_server': lambda *a: '127.0.0.1', |
203 | - 'ldap_server_port': lambda *a: 389, |
204 | - 'sequence': lambda *a: 10, |
205 | - 'create_user': lambda *a: True, |
206 | + 'ldap_server': '127.0.0.1', |
207 | + 'ldap_server_port': 389, |
208 | + 'sequence': 10, |
209 | + 'create_user': True, |
210 | } |
211 | |
212 | CompanyLDAP() |
213 | @@ -63,87 +225,32 @@ |
214 | class res_company(osv.osv): |
215 | _inherit = "res.company" |
216 | _columns = { |
217 | - 'ldaps': fields.one2many('res.company.ldap', 'company', 'LDAP Parameters'), |
218 | + 'ldaps': fields.one2many( |
219 | + 'res.company.ldap', 'company', 'LDAP Parameters'), |
220 | } |
221 | res_company() |
222 | |
223 | + |
224 | class users(osv.osv): |
225 | _inherit = "res.users" |
226 | def login(self, db, login, password): |
227 | - |
228 | - if not password: |
229 | - # empty passwords are disallowed for obvious security reasons |
230 | - return False |
231 | - |
232 | - ret = super(users,self).login(db, login, password) |
233 | - if ret: |
234 | - return ret |
235 | - logger = logging.getLogger('orm.ldap') |
236 | - pool = pooler.get_pool(db) |
237 | + user_id = super(users, self).login(db, login, password) |
238 | + if user_id: |
239 | + return user_id |
240 | cr = pooler.get_db(db).cursor() |
241 | - action_obj = pool.get('ir.actions.actions') |
242 | - cr.execute(""" |
243 | - SELECT id, company, ldap_server, ldap_server_port, ldap_binddn, ldap_password, |
244 | - ldap_filter, ldap_base, "user", create_user |
245 | - FROM res_company_ldap |
246 | - WHERE ldap_server != '' ORDER BY sequence""") |
247 | - for res_company_ldap in cr.dictfetchall(): |
248 | - logger.debug(res_company_ldap) |
249 | - try: |
250 | - l = ldap.open(res_company_ldap['ldap_server'], res_company_ldap['ldap_server_port']) |
251 | - # An empty binddn means anonymous auth, so it should be replaced w/ an empty string |
252 | - # See LDAP RFC 4513, Section 5.1.1 |
253 | - if l.simple_bind_s(res_company_ldap['ldap_binddn'] or '', |
254 | - res_company_ldap['ldap_password'] or ''): |
255 | - base = res_company_ldap['ldap_base'] |
256 | - scope = ldap.SCOPE_SUBTREE |
257 | - filter = filter_format(res_company_ldap['ldap_filter'], (login,)) |
258 | - retrieve_attributes = None |
259 | - result_id = l.search(base, scope, filter, retrieve_attributes) |
260 | - timeout = 60 |
261 | - result_type, result_data = l.result(result_id, timeout) |
262 | - if not result_data: |
263 | - continue |
264 | - if result_type == ldap.RES_SEARCH_RESULT and len(result_data) == 1: |
265 | - dn = result_data[0][0] |
266 | - logger.debug(dn) |
267 | - name = result_data[0][1]['cn'][0] |
268 | - if l.bind_s(dn, password): |
269 | - l.unbind() |
270 | - cr.execute("SELECT id FROM res_users WHERE login=%s",(tools.ustr(login),)) |
271 | - res = cr.fetchone() |
272 | - logger.debug(res) |
273 | - if res: |
274 | - cr.close() |
275 | - return res[0] |
276 | - if not res_company_ldap['create_user']: |
277 | - continue |
278 | - action_id = action_obj.search(cr, 1, [('usage', '=', 'menu')])[0] |
279 | - if res_company_ldap['user']: |
280 | - res = self.copy(cr, 1, res_company_ldap['user'], |
281 | - default={'active': True}) |
282 | - self.write(cr, 1, res, { |
283 | - 'name': name, |
284 | - 'login': login.encode('utf-8'), |
285 | - 'company_id': res_company_ldap['company'], |
286 | - }) |
287 | - else: |
288 | - res = self.create(cr, 1, { |
289 | - 'name': name, |
290 | - 'login': login.encode('utf-8'), |
291 | - 'company_id': res_company_ldap['company'], |
292 | - 'action_id': action_id, |
293 | - 'menu_id': action_id, |
294 | - }) |
295 | - cr.commit() |
296 | - cr.close() |
297 | - return res |
298 | - l.unbind() |
299 | - except Exception: |
300 | - logger.warning("Cannot auth", exc_info=True) |
301 | - continue |
302 | + ldap_obj = pooler.get_pool(db).get('res.company.ldap') |
303 | + for conf in ldap_obj.get_ldap_dicts(cr): |
304 | + entry = ldap_obj.authenticate(conf, login, password) |
305 | + if entry: |
306 | + user_id = ldap_obj.get_or_create_user( |
307 | + cr, 1, conf, login, entry) |
308 | + if user_id: |
309 | + cr.execute('UPDATE res_users SET date=now() WHERE ' |
310 | + 'login=%s', (tools.ustr(login),)) |
311 | + cr.commit() |
312 | + break |
313 | cr.close() |
314 | - return False |
315 | + return user_id |
316 | |
317 | def check(self, db, uid, passwd): |
318 | try: |
319 | @@ -151,44 +258,19 @@ |
320 | except security.ExceptionNoTb: # AccessDenied |
321 | pass |
322 | |
323 | - if not passwd: |
324 | - # empty passwords disallowed for obvious security reasons |
325 | - raise security.ExceptionNoTb('AccessDenied') |
326 | - |
327 | cr = pooler.get_db(db).cursor() |
328 | - user = self.browse(cr, 1, uid) |
329 | - logger = logging.getLogger('orm.ldap') |
330 | - if user and user.company_id.ldaps: |
331 | - for res_company_ldap in user.company_id.ldaps: |
332 | - try: |
333 | - l = ldap.open(res_company_ldap.ldap_server, res_company_ldap.ldap_server_port) |
334 | - # An empty binddn means anonymous auth, so it should be replaced w/ an empty string |
335 | - # See LDAP RFC 4513, Section 5.1.1 |
336 | - if l.simple_bind_s(res_company_ldap.ldap_binddn or '', |
337 | - res_company_ldap.ldap_password or ''): |
338 | - base = res_company_ldap.ldap_base |
339 | - scope = ldap.SCOPE_SUBTREE |
340 | - filter = filter_format(res_company_ldap.ldap_filter, (user.login,)) |
341 | - retrieve_attributes = None |
342 | - result_id = l.search(base, scope, filter, retrieve_attributes) |
343 | - timeout = 60 |
344 | - result_type, result_data = l.result(result_id, timeout) |
345 | - if result_data and result_type == ldap.RES_SEARCH_RESULT and len(result_data) == 1: |
346 | - dn = result_data[0][0] |
347 | - # some LDAP servers allow anonymous binding with blank passwords, |
348 | - # but these have been rejected above, so we're safe to use bind() |
349 | - if l.bind_s(dn, passwd): |
350 | - l.unbind() |
351 | - self._uid_cache.setdefault(db, {})[uid] = passwd |
352 | - cr.close() |
353 | - return True |
354 | - l.unbind() |
355 | - except Exception: |
356 | - logger.warning('cannot check', exc_info=True) |
357 | - pass |
358 | + cr.execute('SELECT login FROM res_users WHERE id=%s AND active=TRUE', |
359 | + (int(uid),)) |
360 | + res = cr.fetchone() |
361 | + if res: |
362 | + ldap_obj = pooler.get_pool(db).get('res.company.ldap') |
363 | + for conf in ldap_obj.get_ldap_dicts(cr): |
364 | + if ldap_obj.authenticate(conf, res[0], passwd): |
365 | + self._uid_cache.setdefault(db, {})[uid] = passwd |
366 | + cr.close() |
367 | + return True |
368 | cr.close() |
369 | raise security.ExceptionNoTb('AccessDenied') |
370 | |
371 | users() |
372 | # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
373 | - |
Hello Stefan,
I agree with the idea of the bug report "wishlist", I wonder, isn't this use case sufficiently specific that it could be done in a separate module (e.g. 'ldap_import')?
Using the "template user" system alone is perhaps sufficient for the normal case?
Now, regardless of the above remark, some initial comments:
- the bind() call should now support anonymous login as provided by your other merge proposal ;-)
- DRY question: shouldn't the code of getting an ldap session, and the code of deciding/creating a user be refactored into separate functions? If we're starting to see it 2 or 3 times, it's time to think about it. Splitting these into separate functions would also allow them to be reused/extended into other module, such as perhaps an "ldap_import" one.
What do you think?