1 ##############################################################################
3 # OpenERP, Open Source Management Solution
4 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as
8 # published by the Free Software Foundation, either version 3 of the
9 # License, or (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Affero General Public License for more details.
16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 ##############################################################################
23 from ldap.filter import filter_format
25 import openerp.exceptions
28 from osv import fields, osv
29 from openerp import SUPERUSER_ID
31 class CompanyLDAP(osv.osv):
32 _name = 'res.company.ldap'
34 _rec_name = 'ldap_server'
36 def get_ldap_dicts(self, cr, ids=None):
38 Retrieve res_company_ldap resources from the database in dictionary
41 :param list ids: Valid ids of model res_company_ldap. If not \
42 specified, process all resources (unlike other ORM methods).
43 :return: ldap configurations
44 :rtype: list of dictionaries
48 id_clause = 'AND id IN (%s)'
54 SELECT id, company, ldap_server, ldap_server_port, ldap_binddn,
55 ldap_password, ldap_filter, ldap_base, "user", create_user,
58 WHERE ldap_server != '' """ + id_clause + """ ORDER BY sequence
60 return cr.dictfetchall()
62 def connect(self, conf):
64 Connect to an LDAP server specified by an ldap
65 configuration dictionary.
67 :param dict conf: LDAP configuration
68 :return: an LDAP object
71 uri = 'ldap://%s:%d' % (conf['ldap_server'],
72 conf['ldap_server_port'])
74 connection = ldap.initialize(uri)
76 connection.start_tls_s()
79 def authenticate(self, conf, login, password):
81 Authenticate a user against the specified LDAP server.
83 In order to prevent an unintended 'unauthenticated authentication',
84 which is an anonymous bind with a valid dn and a blank password,
85 check for empty passwords explicitely (:rfc:`4513#section-6.3.1`)
87 :param dict conf: LDAP configuration
88 :param login: username
89 :param password: Password for the LDAP user
90 :return: LDAP entry of authenticated user or False
91 :rtype: dictionary of attributes
98 filter = filter_format(conf['ldap_filter'], (login,))
100 results = self.query(conf, filter)
101 if results and len(results) == 1:
103 conn = self.connect(conf)
104 conn.simple_bind_s(dn, password)
107 except ldap.INVALID_CREDENTIALS:
109 except ldap.LDAPError, e:
110 logger = logging.getLogger('orm.ldap')
111 logger.error('An LDAP exception occurred: %s', e)
114 def query(self, conf, filter, retrieve_attributes=None):
116 Query an LDAP server with the filter argument and scope subtree.
118 Allow for all authentication methods of the simple authentication
121 - authenticated bind (non-empty binddn + valid password)
122 - anonymous bind (empty binddn + empty password)
123 - unauthenticated authentication (non-empty binddn + empty password)
126 :rfc:`4513#section-5.1` - LDAP: Simple Authentication Method.
128 :param dict conf: LDAP configuration
129 :param filter: valid LDAP filter
130 :param list retrieve_attributes: LDAP attributes to be retrieved. \
131 If not specified, return all attributes.
132 :return: ldap entries
133 :rtype: list of tuples (dn, attrs)
138 logger = logging.getLogger('orm.ldap')
140 conn = self.connect(conf)
141 conn.simple_bind_s(conf['ldap_binddn'] or '',
142 conf['ldap_password'] or '')
143 results = conn.search_st(conf['ldap_base'], ldap.SCOPE_SUBTREE,
144 filter, retrieve_attributes, timeout=60)
146 except ldap.INVALID_CREDENTIALS:
147 logger.error('LDAP bind failed.')
148 except ldap.LDAPError, e:
149 logger.error('An LDAP exception occurred: %s', e)
152 def map_ldap_attributes(self, cr, uid, conf, login, ldap_entry):
154 Compose values for a new resource of model res_users,
155 based upon the retrieved ldap entry and the LDAP settings.
157 :param dict conf: LDAP configuration
158 :param login: the new user's login
159 :param tuple ldap_entry: single LDAP result (dn, attrs)
160 :return: parameters for a new resource of model res_users
164 values = { 'name': ldap_entry[1]['cn'][0],
166 'company_id': conf['company']
170 def get_or_create_user(self, cr, uid, conf, login, ldap_entry,
173 Retrieve an active resource of model res_users with the specified
174 login. Create the user if it is not initially found.
176 :param dict conf: LDAP configuration
177 :param login: the user's login
178 :param tuple ldap_entry: single LDAP result (dn, attrs)
179 :return: res_users id
184 login = tools.ustr(login)
185 cr.execute("SELECT id, active FROM res_users WHERE login=%s", (login,))
190 elif conf['create_user']:
191 logger = logging.getLogger('orm.ldap')
192 logger.debug("Creating new OpenERP user \"%s\" from LDAP" % login)
193 user_obj = self.pool.get('res.users')
194 values = self.map_ldap_attributes(cr, uid, conf, login, ldap_entry)
196 user_id = user_obj.copy(cr, SUPERUSER_ID, conf['user'],
197 default={'active': True})
198 user_obj.write(cr, SUPERUSER_ID, user_id, values)
200 user_id = user_obj.create(cr, SUPERUSER_ID, values)
204 'sequence': fields.integer('Sequence'),
205 'company': fields.many2one('res.company', 'Company', required=True,
207 'ldap_server': fields.char('LDAP Server address', size=64, required=True),
208 'ldap_server_port': fields.integer('LDAP Server port', required=True),
209 'ldap_binddn': fields.char('LDAP binddn', size=64,
210 help=("The user account on the LDAP server that is used to query "
211 "the directory. Leave empty to connect anonymously.")),
212 'ldap_password': fields.char('LDAP password', size=64,
213 help=("The password of the user account on the LDAP server that is "
214 "used to query the directory.")),
215 'ldap_filter': fields.char('LDAP filter', size=256, required=True),
216 'ldap_base': fields.char('LDAP base', size=64, required=True),
217 'user': fields.many2one('res.users', 'Model User',
218 help="Model used for user creation"),
219 'create_user': fields.boolean('Create user',
220 help="Create the user if not in database"),
221 'ldap_tls': fields.boolean('Use TLS',
222 help="Request secure TLS/SSL encryption when connecting to the LDAP server. "
223 "This option requires a server with STARTTLS enabled, "
224 "otherwise all authentication attempts will fail."),
227 'ldap_server': '127.0.0.1',
228 'ldap_server_port': 389,
236 class res_company(osv.osv):
237 _inherit = "res.company"
239 'ldaps': fields.one2many(
240 'res.company.ldap', 'company', 'LDAP Parameters'),
245 class users(osv.osv):
246 _inherit = "res.users"
247 def login(self, db, login, password):
248 user_id = super(users, self).login(db, login, password)
251 cr = pooler.get_db(db).cursor()
252 ldap_obj = pooler.get_pool(db).get('res.company.ldap')
253 for conf in ldap_obj.get_ldap_dicts(cr):
254 entry = ldap_obj.authenticate(conf, login, password)
256 user_id = ldap_obj.get_or_create_user(
257 cr, SUPERUSER_ID, conf, login, entry)
259 cr.execute("""UPDATE res_users
260 SET date=now() AT TIME ZONE 'UTC'
262 (tools.ustr(login),))
268 def check(self, db, uid, passwd):
270 return super(users,self).check(db, uid, passwd)
271 except openerp.exceptions.AccessDenied:
274 cr = pooler.get_db(db).cursor()
275 cr.execute('SELECT login FROM res_users WHERE id=%s AND active=TRUE',
279 ldap_obj = pooler.get_pool(db).get('res.company.ldap')
280 for conf in ldap_obj.get_ldap_dicts(cr):
281 if ldap_obj.authenticate(conf, res[0], passwd):
282 self._uid_cache.setdefault(db, {})[uid] = passwd
286 raise openerp.exceptions.AccessDenied()
289 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: