[MERGE] forward port of branch 7.0 up to f5f7609
[odoo/odoo.git] / addons / auth_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 from openerp import tools
27 from openerp.osv import fields, osv
28 from openerp import SUPERUSER_ID
29 from openerp.modules.registry import RegistryManager
30 _logger = logging.getLogger(__name__)
31
32 class CompanyLDAP(osv.osv):
33     _name = 'res.company.ldap'
34     _order = 'sequence'
35     _rec_name = 'ldap_server'
36
37     def get_ldap_dicts(self, cr, ids=None):
38         """ 
39         Retrieve res_company_ldap resources from the database in dictionary
40         format.
41
42         :param list ids: Valid ids of model res_company_ldap. If not \
43         specified, process all resources (unlike other ORM methods).
44         :return: ldap configurations
45         :rtype: list of dictionaries
46         """
47
48         if ids:
49             id_clause = 'AND id IN (%s)'
50             args = [tuple(ids)]
51         else:
52             id_clause = ''
53             args = []
54         cr.execute("""
55             SELECT id, company, ldap_server, ldap_server_port, ldap_binddn,
56                    ldap_password, ldap_filter, ldap_base, "user", create_user,
57                    ldap_tls
58             FROM res_company_ldap
59             WHERE ldap_server != '' """ + id_clause + """ ORDER BY sequence
60         """, args)
61         return cr.dictfetchall()
62
63     def connect(self, conf):
64         """ 
65         Connect to an LDAP server specified by an ldap
66         configuration dictionary.
67
68         :param dict conf: LDAP configuration
69         :return: an LDAP object
70         """
71
72         uri = 'ldap://%s:%d' % (conf['ldap_server'],
73                                 conf['ldap_server_port'])
74
75         connection = ldap.initialize(uri)
76         if conf['ldap_tls']:
77             connection.start_tls_s()
78         return connection
79
80     def authenticate(self, conf, login, password):
81         """
82         Authenticate a user against the specified LDAP server.
83
84         In order to prevent an unintended 'unauthenticated authentication',
85         which is an anonymous bind with a valid dn and a blank password,
86         check for empty passwords explicitely (:rfc:`4513#section-6.3.1`)
87         
88         :param dict conf: LDAP configuration
89         :param login: username
90         :param password: Password for the LDAP user
91         :return: LDAP entry of authenticated user or False
92         :rtype: dictionary of attributes
93         """
94
95         if not password:
96             return False
97
98         entry = False
99         filter = filter_format(conf['ldap_filter'], (login,))
100         try:
101             results = self.query(conf, filter)
102
103             # Get rid of (None, attrs) for searchResultReference replies
104             results = [i for i in results if i[0]]
105             if results and len(results) == 1:
106                 dn = results[0][0]
107                 conn = self.connect(conf)
108                 conn.simple_bind_s(dn, password)
109                 conn.unbind()
110                 entry = results[0]
111         except ldap.INVALID_CREDENTIALS:
112             return False
113         except ldap.LDAPError, e:
114             _logger.error('An LDAP exception occurred: %s', e)
115         return entry
116         
117     def query(self, conf, filter, retrieve_attributes=None):
118         """ 
119         Query an LDAP server with the filter argument and scope subtree.
120
121         Allow for all authentication methods of the simple authentication
122         method:
123
124         - authenticated bind (non-empty binddn + valid password)
125         - anonymous bind (empty binddn + empty password)
126         - unauthenticated authentication (non-empty binddn + empty password)
127
128         .. seealso::
129            :rfc:`4513#section-5.1` - LDAP: Simple Authentication Method.
130
131         :param dict conf: LDAP configuration
132         :param filter: valid LDAP filter
133         :param list retrieve_attributes: LDAP attributes to be retrieved. \
134         If not specified, return all attributes.
135         :return: ldap entries
136         :rtype: list of tuples (dn, attrs)
137
138         """
139
140         results = []
141         try:
142             conn = self.connect(conf)
143             conn.simple_bind_s(conf['ldap_binddn'] or '',
144                                conf['ldap_password'] or '')
145             results = conn.search_st(conf['ldap_base'], ldap.SCOPE_SUBTREE,
146                                      filter, retrieve_attributes, timeout=60)
147             conn.unbind()
148         except ldap.INVALID_CREDENTIALS:
149             _logger.error('LDAP bind failed.')
150         except ldap.LDAPError, e:
151             _logger.error('An LDAP exception occurred: %s', e)
152         return results
153
154     def map_ldap_attributes(self, cr, uid, conf, login, ldap_entry):
155         """
156         Compose values for a new resource of model res_users,
157         based upon the retrieved ldap entry and the LDAP settings.
158         
159         :param dict conf: LDAP configuration
160         :param login: the new user's login
161         :param tuple ldap_entry: single LDAP result (dn, attrs)
162         :return: parameters for a new resource of model res_users
163         :rtype: dict
164         """
165
166         values = { 'name': ldap_entry[1]['cn'][0],
167                    'login': login,
168                    'company_id': conf['company']
169                    }
170         return values
171     
172     def get_or_create_user(self, cr, uid, conf, login, ldap_entry,
173                            context=None):
174         """
175         Retrieve an active resource of model res_users with the specified
176         login. Create the user if it is not initially found.
177
178         :param dict conf: LDAP configuration
179         :param login: the user's login
180         :param tuple ldap_entry: single LDAP result (dn, attrs)
181         :return: res_users id
182         :rtype: int
183         """
184         
185         user_id = False
186         login = tools.ustr(login.lower())
187         cr.execute("SELECT id, active FROM res_users WHERE lower(login)=%s", (login,))
188         res = cr.fetchone()
189         if res:
190             if res[1]:
191                 user_id = res[0]
192         elif conf['create_user']:
193             _logger.debug("Creating new OpenERP user \"%s\" from LDAP" % login)
194             user_obj = self.pool['res.users']
195             values = self.map_ldap_attributes(cr, uid, conf, login, ldap_entry)
196             if conf['user']:
197                 values['active'] = True
198                 user_id = user_obj.copy(cr, SUPERUSER_ID, conf['user'],
199                                         default=values)
200             else:
201                 user_id = user_obj.create(cr, SUPERUSER_ID, values)
202         return user_id
203
204     _columns = {
205         'sequence': fields.integer('Sequence'),
206         'company': fields.many2one('res.company', 'Company', required=True,
207             ondelete='cascade'),
208         'ldap_server': fields.char('LDAP Server address', size=64, required=True),
209         'ldap_server_port': fields.integer('LDAP Server port', required=True),
210         'ldap_binddn': fields.char('LDAP binddn', size=64,
211             help=("The user account on the LDAP server that is used to query "
212                   "the directory. Leave empty to connect anonymously.")),
213         'ldap_password': fields.char('LDAP password', size=64,
214             help=("The password of the user account on the LDAP server that is "
215                   "used to query the directory.")),
216         'ldap_filter': fields.char('LDAP filter', size=256, required=True),
217         'ldap_base': fields.char('LDAP base', size=64, required=True),
218         'user': fields.many2one('res.users', 'Template User',
219             help="User to copy when creating new users"),
220         'create_user': fields.boolean('Create user',
221             help="Automatically create local user accounts for new users authenticating via LDAP"),
222         'ldap_tls': fields.boolean('Use TLS',
223             help="Request secure TLS/SSL encryption when connecting to the LDAP server. "
224                  "This option requires a server with STARTTLS enabled, "
225                  "otherwise all authentication attempts will fail."),
226     }
227     _defaults = {
228         'ldap_server': '127.0.0.1',
229         'ldap_server_port': 389,
230         'sequence': 10,
231         'create_user': True,
232     }
233
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', groups="base.group_system"),
241     }
242
243
244 class users(osv.osv):
245     _inherit = "res.users"
246     def login(self, db, login, password):
247         user_id = super(users, self).login(db, login, password)
248         if user_id:
249             return user_id
250         registry = RegistryManager.get(db)
251         with registry.cursor() as cr:
252             cr.execute("SELECT id, active FROM res_users WHERE lower(login)=%s", (login,))
253             res = cr.fetchone()
254             if res:
255                 return False
256             ldap_obj = registry.get('res.company.ldap')
257             for conf in ldap_obj.get_ldap_dicts(cr):
258                 entry = ldap_obj.authenticate(conf, login, password)
259                 if entry:
260                     user_id = ldap_obj.get_or_create_user(
261                         cr, SUPERUSER_ID, conf, login, entry)
262                     if user_id:
263                         break
264             return user_id
265
266     def check_credentials(self, cr, uid, password):
267         try:
268             super(users, self).check_credentials(cr, uid, password)
269         except openerp.exceptions.AccessDenied:
270
271             cr.execute('SELECT login FROM res_users WHERE id=%s AND active=TRUE',
272                        (int(uid),))
273             res = cr.fetchone()
274             if res:
275                 ldap_obj = self.pool['res.company.ldap']
276                 for conf in ldap_obj.get_ldap_dicts(cr):
277                     if ldap_obj.authenticate(conf, res[0], password):
278                         return
279             raise
280         
281 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: