[MERGE] lp881356
[odoo/odoo.git] / addons / users_ldap / users_ldap.py
1 ##############################################################################
2 #    
3 #    OpenERP, Open Source Management Solution
4 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
5 #
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.
10 #
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.
15 #
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/>.     
18 #
19 ##############################################################################
20
21 import ldap
22 import logging
23 from ldap.filter import filter_format
24
25 import openerp.exceptions
26 import pooler
27 import tools
28 from osv import fields, osv
29 from openerp import SUPERUSER_ID
30
31 class CompanyLDAP(osv.osv):
32     _name = 'res.company.ldap'
33     _order = 'sequence'
34     _rec_name = 'ldap_server'
35
36     def get_ldap_dicts(self, cr, ids=None):
37         """ 
38         Retrieve res_company_ldap resources from the database in dictionary
39         format.
40
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
45         """
46
47         if ids:
48             id_clause = 'AND id IN (%s)'
49             args = [tuple(ids)]
50         else:
51             id_clause = ''
52             args = []
53         cr.execute("""
54             SELECT id, company, ldap_server, ldap_server_port, ldap_binddn,
55                    ldap_password, ldap_filter, ldap_base, "user", create_user,
56                    ldap_tls
57             FROM res_company_ldap
58             WHERE ldap_server != '' """ + id_clause + """ ORDER BY sequence
59         """, args)
60         return cr.dictfetchall()
61
62     def connect(self, conf):
63         """ 
64         Connect to an LDAP server specified by an ldap
65         configuration dictionary.
66
67         :param dict conf: LDAP configuration
68         :return: an LDAP object
69         """
70
71         uri = 'ldap://%s:%d' % (conf['ldap_server'],
72                                 conf['ldap_server_port'])
73
74         connection = ldap.initialize(uri)
75         if conf['ldap_tls']:
76             connection.start_tls_s()
77         return connection
78
79     def authenticate(self, conf, login, password):
80         """
81         Authenticate a user against the specified LDAP server.
82
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`)
86         
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
92         """
93
94         if not password:
95             return False
96
97         entry = False
98         filter = filter_format(conf['ldap_filter'], (login,))
99         try:
100             results = self.query(conf, filter)
101             if results and len(results) == 1:
102                 dn = results[0][0]
103                 conn = self.connect(conf)
104                 conn.simple_bind_s(dn, password)
105                 conn.unbind()
106                 entry = results[0]
107         except ldap.INVALID_CREDENTIALS:
108             return False
109         except ldap.LDAPError, e:
110             logger = logging.getLogger('orm.ldap')
111             logger.error('An LDAP exception occurred: %s', e)
112         return entry
113         
114     def query(self, conf, filter, retrieve_attributes=None):
115         """ 
116         Query an LDAP server with the filter argument and scope subtree.
117
118         Allow for all authentication methods of the simple authentication
119         method:
120
121         - authenticated bind (non-empty binddn + valid password)
122         - anonymous bind (empty binddn + empty password)
123         - unauthenticated authentication (non-empty binddn + empty password)
124
125         .. seealso::
126            :rfc:`4513#section-5.1` - LDAP: Simple Authentication Method.
127
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)
134
135         """
136
137         results = []
138         logger = logging.getLogger('orm.ldap')
139         try:
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)
145             conn.unbind()
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)
150         return results
151
152     def map_ldap_attributes(self, cr, uid, conf, login, ldap_entry):
153         """
154         Compose values for a new resource of model res_users,
155         based upon the retrieved ldap entry and the LDAP settings.
156         
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
161         :rtype: dict
162         """
163
164         values = { 'name': ldap_entry[1]['cn'][0],
165                    'login': login,
166                    'company_id': conf['company']
167                    }
168         return values
169     
170     def get_or_create_user(self, cr, uid, conf, login, ldap_entry,
171                            context=None):
172         """
173         Retrieve an active resource of model res_users with the specified
174         login. Create the user if it is not initially found.
175
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
180         :rtype: int
181         """
182         
183         user_id = False
184         login = tools.ustr(login)
185         cr.execute("SELECT id, active FROM res_users WHERE login=%s", (login,))
186         res = cr.fetchone()
187         if res:
188             if res[1]:
189                 user_id = res[0]
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)
195             if conf['user']:
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)
199             else:
200                 user_id = user_obj.create(cr, SUPERUSER_ID, values)
201         return user_id
202
203     _columns = {
204         'sequence': fields.integer('Sequence'),
205         'company': fields.many2one('res.company', 'Company', required=True,
206             ondelete='cascade'),
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."),
225     }
226     _defaults = {
227         'ldap_server': '127.0.0.1',
228         'ldap_server_port': 389,
229         'sequence': 10,
230         'create_user': True,
231     }
232
233 CompanyLDAP()
234
235
236 class res_company(osv.osv):
237     _inherit = "res.company"
238     _columns = {
239         'ldaps': fields.one2many(
240             'res.company.ldap', 'company', 'LDAP Parameters'),
241     }
242 res_company()
243
244
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)
249         if user_id:
250             return user_id
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)
255             if entry:
256                 user_id = ldap_obj.get_or_create_user(
257                     cr, SUPERUSER_ID, conf, login, entry)
258                 if user_id:
259                     cr.execute('UPDATE res_users SET date=now() WHERE '
260                                'login=%s', (tools.ustr(login),))
261                     cr.commit()
262                     break
263         cr.close()
264         return user_id
265
266     def check(self, db, uid, passwd):
267         try:
268             return super(users,self).check(db, uid, passwd)
269         except openerp.exceptions.AccessDenied:
270             pass
271
272         cr = pooler.get_db(db).cursor()
273         cr.execute('SELECT login FROM res_users WHERE id=%s AND active=TRUE',
274                    (int(uid),))
275         res = cr.fetchone()
276         if res:
277             ldap_obj = pooler.get_pool(db).get('res.company.ldap')
278             for conf in ldap_obj.get_ldap_dicts(cr):
279                 if ldap_obj.authenticate(conf, res[0], passwd):
280                     self._uid_cache.setdefault(db, {})[uid] = passwd
281                     cr.close()
282                     return True
283         cr.close()
284         raise openerp.exceptions.AccessDenied()
285         
286 users()
287 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: