1
# -*- coding: utf-8 -*-
2
##############################################################################
4
# Copyright (c) 2010-2011 Camptocamp SA (http://www.camptocamp.com)
7
# Author : Nicolas Bessi (Camptocamp), Thanks to Laurent Lauden for his code adaptation
8
# Active directory Donor: M. Benadiba (Informatique Assistances.fr)
9
# Contribution : Joel Grand-Guillaume
11
# WARNING: This program as such is intended to be used by professional
12
# programmers who take the whole responsability of assessing all potential
13
# consequences resulting from its eventual inadequacies and bugs
14
# End users who are looking for a ready-to-use solution with commercial
15
# garantees and support are strongly adviced to contract a Free Software
18
# This program is Free Software; you can redistribute it and/or
19
# modify it under the terms of the GNU General Public License
20
# as published by the Free Software Foundation; either version 2
21
# of the License, or (at your option) any later version.
23
# This program is distributed in the hope that it will be useful,
24
# but WITHOUT ANY WARRANTY; without even the implied warranty of
25
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
# GNU General Public License for more details.
28
# You should have received a copy of the GNU General Public License
29
# along with this program; if not, write to the Free Software
30
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
32
##############################################################################
33
#TODO FInd why company parameter are cached
41
print 'python ldap not installed please install it in order to use this module'
44
from osv import osv, fields
45
from tools.translate import _
47
logger = netsvc.Logger()
49
class LdapConnMApper(object):
50
"""LdapConnMApper: push specific fields from the Terp Partner_contacts to the
51
LDAP schema inetOrgPerson. Ldap bind options are stored in company.r"""
52
def __init__(self, cursor, uid, osv_obj, context=None):
53
"""Initialize connexion to ldap by using parameter set in the current user compagny"""
54
logger.notifyChannel("MY TOPIC", netsvc.LOG_DEBUG,
55
_('Initalize LDAP CONN'))
65
user = osv_obj.pool.get('res.users').browse(cursor, uid, uid, context=context)
66
company = osv_obj.pool.get('res.company').browse(cursor,
70
self.USER_DN = company.base_dn
71
self.CONTACT_DN = company.contact_dn
72
self.LDAP_SERVER = company.ldap_server
73
self.PASS = company.passwd
74
self.PORT = company.ldap_port
75
self.OU = company.ounit
76
self.ACTIVDIR = company.is_activedir
78
mand = (self.USER_DN, self.CONTACT_DN, self.LDAP_SERVER , self.PASS, self.OU)
79
if company.ldap_active:
82
raise osv.except_osv(_('Warning !'),
83
_('An LDAP parameter is missing for company %s') % (company.name,))
85
def get_connexion(self):
86
"""create a new ldap connexion"""
87
logger.notifyChannel("LDAP Address", netsvc.LOG_DEBUG,
88
_('connecting to server ldap %s') % (self.LDAP_SERVER,))
90
self.connexion = ldap.open(self.LDAP_SERVER, self.PORT)
92
self.connexion = ldap.open(self.LDAP_SERVER)
93
self.connexion.simple_bind_s(self.USER_DN, self.PASS)
97
class LDAPAddress(osv.osv):
98
"""Override the CRUD of the objet in order to dynamically bind to ldap"""
99
_inherit = 'res.partner.address'
103
logger = netsvc.Logger()
105
logger.notifyChannel(_('LDAP address init'),
107
_('try to ALTER TABLE res_partner_address RENAME '
108
'column name to lastname ;'))
109
cr.execute('ALTER TABLE res_partner_address RENAME column name to lastname ;')
113
logger.notifyChannel(_('LDAP address init'),
115
_('Warning ! impossible to rename column name'
116
' into lastname, this is probabely aleready'
117
' done or does not exist'))
119
def _compute_name(self, firstname, lastname):
120
firstname = firstname or u''
121
lastname = lastname or u''
122
firstname = (u' '+ firstname).rstrip()
123
return u"%s%s" % (lastname, firstname)
125
def _name_get_fnc(self, cursor, uid, ids, name, arg, context=None):
126
"""Get the name (lastname + firstname), otherwise ''"""
129
reads = self.read(cursor, uid, ids, ['lastname', 'firstname'])
132
## We want to have " firstname" or ""
133
name = self._compute_name(record['firstname'], record['lastname'])
134
res.append((record['id'], name))
137
# TODO get the new version of name search not vulnerable to sql injections
138
# def name_search(self, cursor, user, name, args=None, operator='ilike', context=None, limit=100):
139
# if not context: context = {}
140
# prep_name = '.*%s.*' %(name)
141
# cursor.execute(("select id from res_partner_address where"
142
# " (to_ascii(convert( lastname, 'UTF8', 'LATIN1'),'LATIN-1') ~* '%s'"
143
# " or to_ascii(convert( firstname, 'UTF8', 'LATIN1'),'LATIN-1') ~* '%s')"
144
# " limit %s") % (prep_name, prep_name, limit))
145
# res = cursor.fetchall()
147
# res = [x[0] for x in res]
150
# # search in partner name to know if we are searching partner...
151
# partner_obj=self.pool.get('res.partner')
152
# part_len = len(res)-limit
154
# partner_res = partner_obj.search(cursor, user, [('name', 'ilike', name)],
155
# limit=part_len, context=context)
156
# for p in partner_res:
157
# addresses = partner_obj.browse(cursor, user, p).address
158
# # Take each contact and add it to
159
# for add in addresses:
161
# return self.name_get(cursor, user, res, context)
165
'firstname': fields.char('First name', size=256),
166
'lastname': fields.char('Last name', size=256),
167
'name': fields.function(_name_get_fnc, method=True,
168
type="char", size=512,
169
store=True, string='Contact Name',
170
help='Name generated from the first name and last name',
172
'private_phone':fields.char('Private phone', size=128),
175
def create(self, cursor, uid, vals, context={}):
176
self.getconn(cursor, uid, {})
178
self.validate_entries(vals, cursor, uid, ids)
179
tmp_id = super(LDAPAddress, self).create(cursor, uid,
181
if self.ldaplinkactive(cursor, uid, context):
182
self.saveLdapContact(tmp_id, vals, cursor, uid, context)
185
def write(self, cursor, uid, ids, vals, context=None):
186
context = context or {}
187
self.getconn(cursor, uid, {})
188
if not isinstance(ids, list):
191
self.validate_entries(vals, cursor, uid, ids)
192
if context.has_key('init_mode') and context['init_mode'] :
195
success = super(LDAPAddress, self).write(cursor, uid, ids,
197
if self.ldaplinkactive(cursor, uid, context):
198
for address_id in ids:
199
self.updateLdapContact(address_id, vals, cursor, uid, context)
202
def unlink(self, cursor, uid, ids, context=None):
203
if not context: context = {}
205
self.getconn(cursor, uid, {})
206
if not isinstance(ids, list):
208
if self.ldaplinkactive(cursor, uid, context):
210
self.removeLdapContact(id, cursor, uid)
211
return super(LDAPAddress, self).unlink(cursor, uid, ids)
213
def validate_entries(self, vals, cursor, uid, ids):
214
"""Validate data of an address based on the inetOrgPerson schema"""
217
if isinstance(vals[val], basestring):
218
vals[val] = unicode(vals[val].decode('utf8'))
220
logger.notifyChannel('LDAP encode', netsvc.LOG_DEBUG,
221
'cannot unicode '+ vals[val])
225
if isinstance(ids, (int, long)):
228
self.addNeededFields(ids[0],vals,cursor,uid)
229
email = vals.get('email', False)
230
phone = vals.get('phone', False)
231
fax = vals.get('fax', False)
232
mobile = vals.get('mobile', False)
233
lastname = vals.get('lastname', False)
234
private_phone = vals.get('private_phone', False)
236
if re.match("^.+\\@(\\[?)[a-zA-Z0-9\\-\\.]+\\.([a-zA-Z]{2,3}|[0-9]{1,3})(\\]?)$",
238
raise osv.except_osv(_('Warning !'),
239
_('Please enter a valid e-mail'))
240
phones = (('phone', phone), ('fax', fax), ('mobile', mobile),
241
('private_phone', private_phone))
242
for phone_tuple in phones:
243
phone_number = phone_tuple[1]
245
if not phone_number.startswith('+'):
246
raise osv.except_osv(_('Warning !'),
247
_('Please enter a valid phone number in %s'
248
' international format (i.e. leading +)') % phone_tuple[0])
250
def getVals(self, att_name, key, vals, dico, uid, ids, cursor, context=None):
251
"""map to values to dict"""
252
if not context: context = {}
253
## We explicitely test False value
254
if vals.get(key, False) != False:
255
dico[att_name] = vals[key]
257
if context.get('init_mode'):
259
tmp = self.read(cursor, uid, ids, [key], context={})
260
if tmp.get(key, False) :
261
dico[att_name] = tmp[key]
264
def _un_unicodize_buf(self, in_buf):
265
if isinstance(in_buf, unicode) :
267
return in_buf.encode()
269
return unicodedata.normalize("NFKD", in_buf).encode('ascii','ignore')
272
def unUnicodize(self, indict) :
273
"""remove unicode data of modlist as unicode is not supported
274
by python-ldap librairy till version 2.7"""
276
if not isinstance(indict[key], list):
277
indict[key] = self._un_unicodize_buf(indict[key])
280
for val in indict[key] :
281
nonutfArray.append(self._un_unicodize_buf(val))
282
indict[key] = nonutfArray
284
def addNeededFields(self, id, vals, cursor, uid):
286
previousvalue = self.browse(cursor, uid, [id])[0]
287
if not vals.get('partner_id'):
288
vals['partner_id'] = previousvalue.partner_id.id
289
values_to_check = ('email', 'phone', 'fax', 'mobile', 'firstname',
290
'lastname', 'private_phone', 'street', 'street2')
291
for val in values_to_check:
292
if not vals.get(val):
293
vals[val] = previousvalue[val]
295
def mappLdapObject(self, id, vals, cursor, uid, context):
296
"""Mapp ResPArtner adress to moddlist"""
297
self.addNeededFields(id, vals, cursor, uid)
298
conn = self.getconn(cursor, uid, {})
300
partner_obj=self.pool.get('res.partner')
301
part_name = partner_obj.browse(cursor, uid, vals['partner_id']).name
302
vals['partner'] = part_name
303
name = self._compute_name(vals.get('firstname'), vals.get('lastname'))
308
if not vals.get('lastname') :
309
vals['lastname'] = part_name
310
contact_obj = {'objectclass' : ['inetOrgPerson'],
311
'uid': ['terp_'+str(id)],
314
'sn':[vals['lastname']]}
315
if not vals.get('street'):
317
if not vals.get('street2'):
318
vals['street2'] = u''
319
street_key = 'street'
320
if self.getconn(cursor, uid, {}).ACTIVDIR :
321
# ENTERING THE M$ Realm and it is weird
322
# We manage the address
323
street_key = 'streetAddress'
324
contact_obj[street_key] = vals['street'] + "\r\n" + vals['street2']
325
#we modifiy the class
326
contact_obj['objectclass'] = ['top','person','organizationalPerson','inetOrgPerson','user']
327
#we handle the country
328
if vals.get('country_id') :
329
country = self.browse(cursor, uid, id).country_id
331
vals['country_id'] = country.name
332
vals['c'] = country.code
334
vals['country_id'] = False
336
if vals.get('country_id', False) :
337
self.getVals('co', 'country_id', vals, contact_obj, uid, id, cursor, context)
338
self.getVals('c', 'c', vals, contact_obj, uid, id, cursor, context)
339
# we compute the display name
340
vals['display'] = '%s %s'%(vals['partner'], contact_obj['cn'][0])
342
if self.browse(cursor, uid, id).function:
343
contact_obj['description'] = self.browse(cursor, uid, id).function.name
344
# we replace carriage return
345
if vals.get('comment', False):
346
vals['comment'] = vals['comment'].replace("\n","\r\n")
347
# Active directory specific fields
348
self.getVals('company', 'partner' ,vals, contact_obj, uid, id, cursor, context)
349
self.getVals('info', 'comment' ,vals, contact_obj, uid, id, cursor, context)
350
self.getVals('displayName', 'partner' ,vals, contact_obj, uid, id, cursor, context)
351
## Web site management
352
if self.browse(cursor, uid, id).partner_id.website:
353
vals['website'] = self.browse(cursor, uid, id).partner_id.website
354
self.getVals('wWWHomePage', 'website', vals, contact_obj, uid, id, cursor, context)
356
self.getVals('title', 'title', vals, contact_obj, uid, id, cursor, context)
358
contact_obj[street_key] = vals['street'] + u"\n" + vals['street2']
359
self.getVals('o','partner' ,vals, contact_obj, uid, id, cursor, context)
362
self.getVals('givenName', 'firstname',vals, contact_obj, uid, id, cursor, context)
363
self.getVals('mail', 'email',vals, contact_obj, uid, id, cursor, context)
364
self.getVals('telephoneNumber', 'phone',vals, contact_obj, uid, id, cursor, context)
365
self.getVals('l', 'city',vals, contact_obj, uid, id, cursor, context)
366
self.getVals('facsimileTelephoneNumber', 'fax',vals, contact_obj, uid, id, cursor, context)
367
self.getVals('mobile', 'mobile',vals, contact_obj, uid, id, cursor, context)
368
self.getVals('homePhone', 'private_phone',vals, contact_obj, uid, id, cursor, context)
369
self.getVals('postalCode', 'zip',vals, contact_obj, uid, id, cursor, context)
370
self.unUnicodize(contact_obj)
373
def saveLdapContact(self, id, vals, cursor, uid, context=None):
374
"""save openerp adress to ldap"""
375
contact_obj = self.mappLdapObject(id,vals,cursor,uid,context)
376
conn = self.connectToLdap(cursor, uid, context=context)
378
if self.getconn(cursor, uid, context).ACTIVDIR:
379
conn.connexion.add_s("CN=%s,OU=%s,%s"%(contact_obj['cn'][0], conn.OU, conn.CONTACT_DN),
380
ldap.modlist.addModlist(contact_obj))
382
conn.connexion.add_s("uid=terp_%s,OU=%s,%s"%(str(id), conn.OU, conn.CONTACT_DN),
383
ldap.modlist.addModlist(contact_obj))
386
conn.connexion.unbind_s()
388
def updateLdapContact(self, id, vals, cursor, uid, context):
389
"""update an existing contact with the data of OpenERP"""
390
conn = self.connectToLdap(cursor,uid,context={})
392
old_contatc_obj = self.getLdapContact(conn,id)
393
except ldap.NO_SUCH_OBJECT:
394
self.saveLdapContact(id,vals,cursor,uid,context)
396
contact_obj = self.mappLdapObject(id,vals,cursor,uid,context)
399
for key, val in contact_obj.items() :
400
if key in ('cn', 'uid', 'objectclass'):
402
if isinstance(val, list):
404
modlist.append((ldap.MOD_REPLACE, key, val))
406
modlist = ldap.modlist.modifyModlist(old_contatc_obj[1], contact_obj)
408
conn.connexion.modify_s(old_contatc_obj[0], modlist)
409
conn.connexion.unbind_s()
413
def removeLdapContact(self, id, cursor, uid):
414
"""Remove a contact from ldap"""
415
conn = self.connectToLdap(cursor,uid,context={})
418
to_delete = self.getLdapContact(conn,id)
419
except ldap.NO_SUCH_OBJECT:
420
logger.notifyChannel("Warning", netsvc.LOG_INFO,
421
_("'no object to delete in ldap' %s") %(id))
422
except Exception, e :
426
conn.connexion.delete_s(to_delete[0])
427
conn.connexion.unbind_s()
431
def getLdapContact(self, conn, id):
432
result = conn.connexion.search_ext_s("ou=%s,%s"%(conn.OU,conn.CONTACT_DN),
434
"(&(objectclass=*)(uid=terp_"+str(id)+"))")
436
raise ldap.NO_SUCH_OBJECT
439
def ldaplinkactive(self, cursor, uid, context=None):
440
"""Check if ldap is activated for this company"""
441
user = self.pool.get('res.users').browse(cursor, uid, uid, context=context)
442
company = self.pool.get('res.company').browse(cursor, uid,user.company_id.id, context=context)
443
return company.ldap_active
445
def getconn(self, cursor, uid, context=None):
447
if not self.ldapMapper :
448
self.ldapMapper = LdapConnMApper(cursor, uid, self)
449
return self.ldapMapper
451
def connectToLdap(self, cursor, uid, context=None):
452
"""Reinitialize ldap connection"""
454
if not self.ldapMapper :
455
self.getconn(cursor, uid, context)
456
self.ldapMapper.get_connexion()
457
return self.ldapMapper