diff --git a/addons/users_ldap/users_ldap.py b/addons/users_ldap/users_ldap.py index c62e41f4a9c..518dfe44147 100644 --- a/addons/users_ldap/users_ldap.py +++ b/addons/users_ldap/users_ldap.py @@ -18,82 +18,149 @@ # ############################################################################## +import ldap +from ldap.filter import filter_format from osv import fields, osv import pooler import tools import logging from service import security -import ldap -from ldap.filter import filter_format -import re class CompanyLDAP(osv.osv): _name = 'res.company.ldap' _order = 'sequence' _rec_name = 'ldap_server' - def populate(self, cr, uid, ids, context=None): + def get_ldap_dicts(self, cr, ids=None): """ - Populate OpenERP user base from LDAP. - Call from the button on the form or from the task scheduler. + Return res_company_ldap resources in the database as a list of + dictionaries. + + @param cr : database cursor + @param ids : optional list of res_company_ldap ids """ + + if ids: + id_clause = 'AND id IN (%s)' + args = [tuple(ids)] + else: + id_clause = '' + args = [] + cr.execute(""" + SELECT id, company, ldap_server, ldap_server_port, ldap_binddn, + ldap_password, ldap_filter, ldap_base, "user", create_user + FROM res_company_ldap + WHERE ldap_server != '' """ + id_clause + """ ORDER BY sequence + """, args) + return cr.dictfetchall() + + def connect(self, conf): + """ + Compose an LDAP uri from an ldap configuration dictionary + and return a connection to it. + + @param conf : LDAP configuration dictionary + """ + + uri = 'ldap://%s:%d' % (conf['ldap_server'], + conf['ldap_server_port']) + return ldap.initialize(uri) + + def authenticate(self, conf, dn, password): + """ + Perform an atomic LDAP authentication. + + @param conf : LDAP configuration dictionary + @param dn : LDAP dn + @param password : Password for the LDAP user + """ + + conn = self.connect(conf) + conn.simple_bind_s(dn, password) + conn.unbind() + + def query(self, conf, filter, retrieve_attributes=None): + """ + Query an LDAP server with the filter argument and scope subtree. + Return the results in native python-ldap format, ie. a list of + tuples (dn, attrs). + + @param conf : LDAP configuration dictionary + @param filter : valid LDAP filter + @param retrieve_attributes : optional list of LDAP attributes to be + retrieved. If not specified, return all attributes. + """ + + results = [] logger = logging.getLogger('orm.ldap') - action_obj = self.pool.get('ir.actions.actions') - action_id = action_obj.search(cr, 1, [('usage', '=', 'menu')])[0] - user_obj = self.pool.get('res.users') - for res_company_ldap in self.browse(cr, uid, ids, context): - if not res_company_ldap['create_user']: - continue - try: - l = ldap.open(res_company_ldap.ldap_server, res_company_ldap.ldap_server_port) - if l.simple_bind_s(res_company_ldap.ldap_binddn, res_company_ldap.ldap_password): - base = res_company_ldap.ldap_base - scope = ldap.SCOPE_SUBTREE - attr_match = re.search('([a-zA-Z_]+)=\%s', res_company_ldap['ldap_filter']) - if attr_match: - login_attr = str(attr_match.group(1)) - else: - logger.debug("Could not extract attribute found in ldap_filter %s." % res_company_ldap['ldap_filter']) - continue - filter = res_company_ldap.ldap_filter % '*' - retrieve_attributes = ['cn', login_attr] - result_id = l.search(base, scope, filter, retrieve_attributes) - timeout = 60 - result_type, result_data = l.result(result_id, timeout) - if not result_data: - continue - if result_type == ldap.RES_SEARCH_RESULT: - for entry in result_data: - dn = entry[0] - name = entry[1]['cn'][0] - login = entry[1][login_attr][0] - cr.execute("SELECT id FROM res_users WHERE login=%s",(login,)) - res = cr.fetchone() - if res: - continue - logger.debug("Creating new OpenERP user \"%s\" from LDAP" % login) - if res_company_ldap['user']: - res = user_obj.copy(cr, 1, res_company_ldap['user'].id, - default={'active': True}) - user_obj.write(cr, 1, res, { - 'name': name, - 'login': login.encode('utf-8'), - 'company_id': res_company_ldap['company'].id, - }) - else: - res = user_obj.create(cr, 1, { - 'name': name, - 'login': login.encode('utf-8'), - 'company_id': res_company_ldap['company'].id, - 'action_id': action_id, - 'menu_id': action_id, - }) - l.unbind() - except Exception: - logger.warning('cannot check', exc_info=True) - pass - return True + try: + conn = self.connect(conf) + conn.simple_bind_s(conf['ldap_binddn'] or '', + conf['ldap_password'] or '') + results = conn.search_st(conf['ldap_base'], ldap.SCOPE_SUBTREE, + filter, retrieve_attributes, timeout=60) + conn.unbind() + except ldap.LDAPError, e: + logger.warning('An LDAP exception occurred: %s' % e) + pass + return results + + def map_ldap_attributes(self, cr, uid, login, conf, ldap_entry): + """ + Compose a list of field values for creating a new resource of model + res_users, based upon the retrieved ldap entry and the LDAP settings. + + @param cr : the database cursor + @param uid : the OpenERP user id + @param login : the new user's login + @param conf : LDAP configuration dictionary + @param ldap_entry : single LDAP result in (dn, attrs) format + """ + + values = { 'name': ldap_entry[1]['cn'][0], + 'login': login, + 'company_id': conf['company'] + } + if not conf['user']: + action_obj = self.pool.get('ir.actions.actions') + values['action_id'] = values['menu_id'] = action_obj.search( + cr, uid, [('usage', '=', 'menu')], order='id')[0] + return values + def get_or_create_user(self, cr, uid, login, conf, ldap_entry, + context=None): + """ + Return the id of an active res_users resource with the specified + login. Create the user if it is not initially found. + + @param cr : the database cursor + @param uid : the OpenERP user id + @param login : the user's login + @param conf : LDAP configuration dictionary + @param ldap_entry : single LDAP result in (dn, attrs) format + @param context : the OpenERP context + """ + + user_id = False + login = tools.ustr(login) + cr.execute("SELECT id, active FROM res_users WHERE login=%s", (login,)) + res = cr.fetchone() + if res: + if res[1]: + user_id = res[0] + elif conf['create_user']: + logger = logging.getLogger('orm.ldap') + logger.debug("Creating new OpenERP user \"%s\" from LDAP" % login) + user_obj = self.pool.get('res.users') + values = self.map_ldap_attributes(cr, uid, login, conf, ldap_entry) + if conf['user']: + user_id = user_obj.copy(cr, 1, conf['user'], + default={'active': True}) + user_obj.write(cr, 1, user_id, values) + else: + user_id = user_obj.create(cr, 1, values) + return user_id + _columns = { 'sequence': fields.integer('Sequence'), 'company': fields.many2one('res.company', 'Company', required=True, @@ -114,10 +181,10 @@ class CompanyLDAP(osv.osv): help="Create the user if not in database"), } _defaults = { - 'ldap_server': lambda *a: '127.0.0.1', - 'ldap_server_port': lambda *a: 389, - 'sequence': lambda *a: 10, - 'create_user': lambda *a: True, + 'ldap_server': '127.0.0.1', + 'ldap_server_port': 389, + 'sequence': 10, + 'create_user': True, } CompanyLDAP() @@ -126,87 +193,44 @@ CompanyLDAP() class res_company(osv.osv): _inherit = "res.company" _columns = { - 'ldaps': fields.one2many('res.company.ldap', 'company', 'LDAP Parameters'), + 'ldaps': fields.one2many( + 'res.company.ldap', 'company', 'LDAP Parameters'), } res_company() + class users(osv.osv): _inherit = "res.users" def login(self, db, login, password): - - if not password: - # empty passwords are disallowed for obvious security reasons - return False - - ret = super(users,self).login(db, login, password) - if ret: - return ret - logger = logging.getLogger('orm.ldap') - pool = pooler.get_pool(db) + user_id = super(users, self).login(db, login, password) + if user_id: + return user_id cr = pooler.get_db(db).cursor() - action_obj = pool.get('ir.actions.actions') - cr.execute(""" - SELECT id, company, ldap_server, ldap_server_port, ldap_binddn, ldap_password, - ldap_filter, ldap_base, "user", create_user - FROM res_company_ldap - WHERE ldap_server != '' ORDER BY sequence""") - for res_company_ldap in cr.dictfetchall(): - logger.debug(res_company_ldap) - try: - l = ldap.open(res_company_ldap['ldap_server'], res_company_ldap['ldap_server_port']) - # An empty binddn means anonymous auth, so it should be replaced w/ an empty string - # See LDAP RFC 4513, Section 5.1.1 - if l.simple_bind_s(res_company_ldap['ldap_binddn'] or '', - res_company_ldap['ldap_password'] or ''): - base = res_company_ldap['ldap_base'] - scope = ldap.SCOPE_SUBTREE - filter = filter_format(res_company_ldap['ldap_filter'], (login,)) - retrieve_attributes = None - result_id = l.search(base, scope, filter, retrieve_attributes) - timeout = 60 - result_type, result_data = l.result(result_id, timeout) - if not result_data: - continue - if result_type == ldap.RES_SEARCH_RESULT and len(result_data) == 1: - dn = result_data[0][0] - logger.debug(dn) - name = result_data[0][1]['cn'][0] - if l.bind_s(dn, password): - l.unbind() - cr.execute("SELECT id FROM res_users WHERE login=%s",(tools.ustr(login),)) - res = cr.fetchone() - logger.debug(res) - if res: - cr.close() - return res[0] - if not res_company_ldap['create_user']: - continue - action_id = action_obj.search(cr, 1, [('usage', '=', 'menu')])[0] - if res_company_ldap['user']: - res = self.copy(cr, 1, res_company_ldap['user'], - default={'active': True}) - self.write(cr, 1, res, { - 'name': name, - 'login': login.encode('utf-8'), - 'company_id': res_company_ldap['company'], - }) - else: - res = self.create(cr, 1, { - 'name': name, - 'login': login.encode('utf-8'), - 'company_id': res_company_ldap['company'], - 'action_id': action_id, - 'menu_id': action_id, - }) - cr.commit() - cr.close() - return res - l.unbind() - except Exception: - logger.warning("Cannot auth", exc_info=True) - continue + ldap_obj = pooler.get_pool(db).get('res.company.ldap') + for conf in ldap_obj.get_ldap_dicts(cr): + filter = filter_format(conf['ldap_filter'], (login,)) + results = ldap_obj.query(conf, filter) + if results and len(results) == 1: + entry = results[0] + dn = entry[0] + name = entry[1]['cn'][0] + try: + ldap_obj.authenticate(conf, dn, password) + user_id = ldap_obj.get_or_create_user( + cr, 1, login, conf, entry) + if user_id: + cr.execute('UPDATE res_users SET date=now() WHERE ' + 'login=%s', (tools.ustr(login),)) + cr.commit() + break + except ldap.INVALID_CREDENTIALS: + pass + except ldap.LDAPError, e: + logger = logging.getLogger('orm.ldap') + logger.warning('An LDAP exception occurred: %s' % e) + pass cr.close() - return False + return user_id def check(self, db, uid, passwd): try: @@ -219,39 +243,29 @@ class users(osv.osv): raise security.ExceptionNoTb('AccessDenied') cr = pooler.get_db(db).cursor() - user = self.browse(cr, 1, uid) - logger = logging.getLogger('orm.ldap') - if user and user.company_id.ldaps: - for res_company_ldap in user.company_id.ldaps: - try: - l = ldap.open(res_company_ldap.ldap_server, res_company_ldap.ldap_server_port) - # An empty binddn means anonymous auth, so it should be replaced w/ an empty string - # See LDAP RFC 4513, Section 5.1.1 - if l.simple_bind_s(res_company_ldap.ldap_binddn or '', - res_company_ldap.ldap_password or ''): - base = res_company_ldap.ldap_base - scope = ldap.SCOPE_SUBTREE - filter = filter_format(res_company_ldap.ldap_filter, (user.login,)) - retrieve_attributes = None - result_id = l.search(base, scope, filter, retrieve_attributes) - timeout = 60 - result_type, result_data = l.result(result_id, timeout) - if result_data and result_type == ldap.RES_SEARCH_RESULT and len(result_data) == 1: - dn = result_data[0][0] - # some LDAP servers allow anonymous binding with blank passwords, - # but these have been rejected above, so we're safe to use bind() - if l.bind_s(dn, passwd): - l.unbind() - self._uid_cache.setdefault(db, {})[uid] = passwd - cr.close() - return True - l.unbind() - except Exception: - logger.warning('cannot check', exc_info=True) - pass + cr.execute('SELECT login FROM res_users WHERE id=%s AND active=TRUE', + (int(uid),)) + res = cr.fetchone() + if res: + ldap_obj = pooler.get_pool(db).get('res.company.ldap') + for conf in ldap_obj.get_ldap_dicts(cr): + filter = filter_format(conf['ldap_filter'], (res[0],)) + results = ldap_obj.query(conf, filter) + if results and len(results) == 1: + dn = results[0][0] + # some LDAP servers allow anonymous binding with blank passwords, + # but these have been rejected above, so we're safe to use bind() + try: + ldap_obj.authenticate(conf, dn, passwd) + self._uid_cache.setdefault(db, {})[uid] = passwd + cr.close() + return True + except ldap.LDAPError, e: + logger = logging.getLogger('orm.ldap') + logger.warning('An LDAP exception occurred: %s' % e) + pass cr.close() raise security.ExceptionNoTb('AccessDenied') users() # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/addons/users_ldap/users_ldap_view.xml b/addons/users_ldap/users_ldap_view.xml index 569d5b554aa..b055ef2938d 100644 --- a/addons/users_ldap/users_ldap_view.xml +++ b/addons/users_ldap/users_ldap_view.xml @@ -20,7 +20,6 @@ -