You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

459 lines
20 KiB

  1. # -*- coding: utf-8 -*-
  2. ##############################################################################
  3. #
  4. # Copyright (c) 2010-2011 Camptocamp SA (http://www.camptocamp.com)
  5. # All Right Reserved
  6. #
  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
  10. #
  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
  16. # Service Company
  17. #
  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.
  22. #
  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.
  27. #
  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.
  31. #
  32. ##############################################################################
  33. #TODO FInd why company parameter are cached
  34. import re
  35. import unicodedata
  36. import netsvc
  37. try:
  38. import ldap
  39. import ldap.modlist
  40. except :
  41. print 'python ldap not installed please install it in order to use this module'
  42. from osv import osv, fields
  43. from tools.translate import _
  44. logger = netsvc.Logger()
  45. class LdapConnMApper(object):
  46. """LdapConnMApper: push specific fields from the Terp Partner_contacts to the
  47. LDAP schema inetOrgPerson. Ldap bind options are stored in company.r"""
  48. def __init__(self, cursor, uid, osv_obj, context=None):
  49. """Initialize connexion to ldap by using parameter set in the current user compagny"""
  50. logger.notifyChannel("MY TOPIC", netsvc.LOG_DEBUG,
  51. _('Initalize LDAP CONN'))
  52. self.USER_DN = ''
  53. self.CONTACT_DN = ''
  54. self.LDAP_SERVER = ''
  55. self.PASS = ''
  56. self.OU = ''
  57. self.connexion = ''
  58. self.ACTIVDIR = False
  59. #Reading ldap pref
  60. user = osv_obj.pool.get('res.users').browse(cursor, uid, uid, context=context)
  61. company = osv_obj.pool.get('res.company').browse(cursor,
  62. uid,
  63. user.company_id.id,
  64. context=context)
  65. self.USER_DN = company.base_dn
  66. self.CONTACT_DN = company.contact_dn
  67. self.LDAP_SERVER = company.ldap_server
  68. self.PASS = company.passwd
  69. self.PORT = company.ldap_port
  70. self.OU = company.ounit
  71. self.ACTIVDIR = company.is_activedir
  72. mand = (self.USER_DN, self.CONTACT_DN, self.LDAP_SERVER , self.PASS, self.OU)
  73. if company.ldap_active:
  74. for param in mand:
  75. if not param:
  76. raise osv.except_osv(_('Warning !'),
  77. _('An LDAP parameter is missing for company %s') % (company.name,))
  78. def get_connexion(self):
  79. """create a new ldap connexion"""
  80. logger.notifyChannel("LDAP Address", netsvc.LOG_DEBUG,
  81. _('connecting to server ldap %s') % (self.LDAP_SERVER,))
  82. if self.PORT :
  83. self.connexion = ldap.open(self.LDAP_SERVER, self.PORT)
  84. else :
  85. self.connexion = ldap.open(self.LDAP_SERVER)
  86. self.connexion.simple_bind_s(self.USER_DN, self.PASS)
  87. return self.connexion
  88. class LDAPAddress(osv.osv):
  89. """Override the CRUD of the objet in order to dynamically bind to ldap"""
  90. _inherit = 'res.partner.address'
  91. ldapMapper = None
  92. def init(self, cr):
  93. logger = netsvc.Logger()
  94. try:
  95. logger.notifyChannel(_('LDAP address init'),
  96. netsvc.LOG_INFO,
  97. _('try to ALTER TABLE res_partner_address RENAME '
  98. 'column name to lastname ;'))
  99. cr.execute('ALTER TABLE res_partner_address RENAME column name to lastname ;')
  100. except Exception, e:
  101. cr.rollback()
  102. logger.notifyChannel(_('LDAP address init'),
  103. netsvc.LOG_INFO,
  104. _('Warning ! impossible to rename column name'
  105. ' into lastname, this is probabely aleready'
  106. ' done or does not exist'))
  107. def _compute_name(self, firstname, lastname):
  108. firstname = firstname or u''
  109. lastname = lastname or u''
  110. firstname = (u' '+ firstname).rstrip()
  111. return u"%s%s" % (lastname, firstname)
  112. def _name_get_fnc(self, cursor, uid, ids, name, arg, context=None):
  113. """Get the name (lastname + firstname), otherwise ''"""
  114. if not ids:
  115. return []
  116. reads = self.read(cursor, uid, ids, ['lastname', 'firstname'])
  117. res = []
  118. for record in reads:
  119. ## We want to have " firstname" or ""
  120. name = self._compute_name(record['firstname'], record['lastname'])
  121. res.append((record['id'], name))
  122. return dict(res)
  123. # TODO get the new version of name search not vulnerable to sql injections
  124. # def name_search(self, cursor, user, name, args=None, operator='ilike', context=None, limit=100):
  125. # if not context: context = {}
  126. # prep_name = '.*%s.*' %(name)
  127. # cursor.execute(("select id from res_partner_address where"
  128. # " (to_ascii(convert( lastname, 'UTF8', 'LATIN1'),'LATIN-1') ~* '%s'"
  129. # " or to_ascii(convert( firstname, 'UTF8', 'LATIN1'),'LATIN-1') ~* '%s')"
  130. # " limit %s") % (prep_name, prep_name, limit))
  131. # res = cursor.fetchall()
  132. # if res:
  133. # res = [x[0] for x in res]
  134. # else:
  135. # res = []
  136. # # search in partner name to know if we are searching partner...
  137. # partner_obj=self.pool.get('res.partner')
  138. # part_len = len(res)-limit
  139. # if part_len > 0:
  140. # partner_res = partner_obj.search(cursor, user, [('name', 'ilike', name)],
  141. # limit=part_len, context=context)
  142. # for p in partner_res:
  143. # addresses = partner_obj.browse(cursor, user, p).address
  144. # # Take each contact and add it to
  145. # for add in addresses:
  146. # res.append(add.id)
  147. # return self.name_get(cursor, user, res, context)
  148. _columns = {
  149. 'firstname': fields.char('First name', size=256),
  150. 'lastname': fields.char('Last name', size=256),
  151. 'name': fields.function(_name_get_fnc, method=True,
  152. type="char", size=512,
  153. store=True, string='Contact Name',
  154. help='Name generated from the first name and last name',
  155. nodrop=True),
  156. 'private_phone':fields.char('Private phone', size=128),
  157. }
  158. def create(self, cursor, uid, vals, context={}):
  159. self.getconn(cursor, uid, {})
  160. ids = None
  161. self.validate_entries(vals, cursor, uid, ids)
  162. tmp_id = super(LDAPAddress, self).create(cursor, uid,
  163. vals, context)
  164. if self.ldaplinkactive(cursor, uid, context):
  165. self.saveLdapContact(tmp_id, vals, cursor, uid, context)
  166. return tmp_id
  167. def write(self, cursor, uid, ids, vals, context=None):
  168. context = context or {}
  169. self.getconn(cursor, uid, {})
  170. if not isinstance(ids, list):
  171. ids = [ids]
  172. if ids:
  173. self.validate_entries(vals, cursor, uid, ids)
  174. if context.has_key('init_mode') and context['init_mode'] :
  175. success = True
  176. else :
  177. success = super(LDAPAddress, self).write(cursor, uid, ids,
  178. vals, context)
  179. if self.ldaplinkactive(cursor, uid, context):
  180. for address_id in ids:
  181. self.updateLdapContact(address_id, vals, cursor, uid, context)
  182. return success
  183. def unlink(self, cursor, uid, ids, context=None):
  184. if not context: context = {}
  185. if ids:
  186. self.getconn(cursor, uid, {})
  187. if not isinstance(ids, list):
  188. ids = [ids]
  189. if self.ldaplinkactive(cursor, uid, context):
  190. for id in ids:
  191. self.removeLdapContact(id, cursor, uid)
  192. return super(LDAPAddress, self).unlink(cursor, uid, ids)
  193. def validate_entries(self, vals, cursor, uid, ids):
  194. """Validate data of an address based on the inetOrgPerson schema"""
  195. for val in vals :
  196. try :
  197. if isinstance(vals[val], basestring):
  198. vals[val] = unicode(vals[val].decode('utf8'))
  199. except UnicodeError:
  200. logger.notifyChannel('LDAP encode', netsvc.LOG_DEBUG,
  201. 'cannot unicode '+ vals[val])
  202. pass
  203. if ids is not None:
  204. if isinstance(ids, (int, long)):
  205. ids = [ids]
  206. if len(ids) == 1:
  207. self.addNeededFields(ids[0],vals,cursor,uid)
  208. email = vals.get('email', False)
  209. phone = vals.get('phone', False)
  210. fax = vals.get('fax', False)
  211. mobile = vals.get('mobile', False)
  212. lastname = vals.get('lastname', False)
  213. private_phone = vals.get('private_phone', False)
  214. if email :
  215. if re.match("^.+\\@(\\[?)[a-zA-Z0-9\\-\\.]+\\.([a-zA-Z]{2,3}|[0-9]{1,3})(\\]?)$",
  216. email) is None:
  217. raise osv.except_osv(_('Warning !'),
  218. _('Please enter a valid e-mail'))
  219. phones = (('phone', phone), ('fax', fax), ('mobile', mobile),
  220. ('private_phone', private_phone))
  221. for phone_tuple in phones:
  222. phone_number = phone_tuple[1]
  223. if phone_number :
  224. if not phone_number.startswith('+'):
  225. raise osv.except_osv(_('Warning !'),
  226. _('Please enter a valid phone number in %s'
  227. ' international format (i.e. leading +)') % phone_tuple[0])
  228. def getVals(self, att_name, key, vals, dico, uid, ids, cursor, context=None):
  229. """map to values to dict"""
  230. if not context: context = {}
  231. ## We explicitely test False value
  232. if vals.get(key, False) != False:
  233. dico[att_name] = vals[key]
  234. else :
  235. if context.get('init_mode'):
  236. return False
  237. tmp = self.read(cursor, uid, ids, [key], context={})
  238. if tmp.get(key, False) :
  239. dico[att_name] = tmp[key]
  240. def _un_unicodize_buf(self, in_buf):
  241. if isinstance(in_buf, unicode) :
  242. try:
  243. return in_buf.encode()
  244. except Exception, e:
  245. return unicodedata.normalize("NFKD", in_buf).encode('ascii','ignore')
  246. return in_buf
  247. def unUnicodize(self, indict) :
  248. """remove unicode data of modlist as unicode is not supported
  249. by python-ldap librairy till version 2.7"""
  250. for key in indict :
  251. if not isinstance(indict[key], list):
  252. indict[key] = self._un_unicodize_buf(indict[key])
  253. else:
  254. nonutfArray = []
  255. for val in indict[key] :
  256. nonutfArray.append(self._un_unicodize_buf(val))
  257. indict[key] = nonutfArray
  258. def addNeededFields(self, id, vals, cursor, uid):
  259. keys = vals.keys()
  260. previousvalue = self.browse(cursor, uid, [id])[0]
  261. if not vals.get('partner_id'):
  262. vals['partner_id'] = previousvalue.partner_id.id
  263. values_to_check = ('email', 'phone', 'fax', 'mobile', 'firstname',
  264. 'lastname', 'private_phone', 'street', 'street2')
  265. for val in values_to_check:
  266. if not vals.get(val):
  267. vals[val] = previousvalue[val]
  268. def mappLdapObject(self, id, vals, cursor, uid, context):
  269. """Mapp ResPArtner adress to moddlist"""
  270. self.addNeededFields(id, vals, cursor, uid)
  271. conn = self.getconn(cursor, uid, {})
  272. keys = vals.keys()
  273. partner_obj=self.pool.get('res.partner')
  274. part_name = partner_obj.browse(cursor, uid, vals['partner_id']).name
  275. vals['partner'] = part_name
  276. name = self._compute_name(vals.get('firstname'), vals.get('lastname'))
  277. if name :
  278. cn = name
  279. else:
  280. cn = part_name
  281. if not vals.get('lastname') :
  282. vals['lastname'] = part_name
  283. contact_obj = {'objectclass' : ['inetOrgPerson'],
  284. 'uid': ['terp_'+str(id)],
  285. 'ou':[conn.OU],
  286. 'cn':[cn],
  287. 'sn':[vals['lastname']]}
  288. if not vals.get('street'):
  289. vals['street'] = u''
  290. if not vals.get('street2'):
  291. vals['street2'] = u''
  292. street_key = 'street'
  293. if self.getconn(cursor, uid, {}).ACTIVDIR :
  294. # ENTERING THE M$ Realm and it is weird
  295. # We manage the address
  296. street_key = 'streetAddress'
  297. contact_obj[street_key] = vals['street'] + "\r\n" + vals['street2']
  298. #we modifiy the class
  299. contact_obj['objectclass'] = ['top','person','organizationalPerson','inetOrgPerson','user']
  300. #we handle the country
  301. if vals.get('country_id') :
  302. country = self.browse(cursor, uid, id).country_id
  303. if country :
  304. vals['country_id'] = country.name
  305. vals['c'] = country.code
  306. else :
  307. vals['country_id'] = False
  308. vals['c'] = False
  309. if vals.get('country_id', False) :
  310. self.getVals('co', 'country_id', vals, contact_obj, uid, id, cursor, context)
  311. self.getVals('c', 'c', vals, contact_obj, uid, id, cursor, context)
  312. # we compute the display name
  313. vals['display'] = '%s %s'%(vals['partner'], contact_obj['cn'][0])
  314. # we get the title
  315. if self.browse(cursor, uid, id).function:
  316. contact_obj['description'] = self.browse(cursor, uid, id).function.name
  317. # we replace carriage return
  318. if vals.get('comment', False):
  319. vals['comment'] = vals['comment'].replace("\n","\r\n")
  320. # Active directory specific fields
  321. self.getVals('company', 'partner' ,vals, contact_obj, uid, id, cursor, context)
  322. self.getVals('info', 'comment' ,vals, contact_obj, uid, id, cursor, context)
  323. self.getVals('displayName', 'partner' ,vals, contact_obj, uid, id, cursor, context)
  324. ## Web site management
  325. if self.browse(cursor, uid, id).partner_id.website:
  326. vals['website'] = self.browse(cursor, uid, id).partner_id.website
  327. self.getVals('wWWHomePage', 'website', vals, contact_obj, uid, id, cursor, context)
  328. del(vals['website'])
  329. self.getVals('title', 'title', vals, contact_obj, uid, id, cursor, context)
  330. else :
  331. contact_obj[street_key] = vals['street'] + u"\n" + vals['street2']
  332. self.getVals('o','partner' ,vals, contact_obj, uid, id, cursor, context)
  333. #Common attributes
  334. self.getVals('givenName', 'firstname',vals, contact_obj, uid, id, cursor, context)
  335. self.getVals('mail', 'email',vals, contact_obj, uid, id, cursor, context)
  336. self.getVals('telephoneNumber', 'phone',vals, contact_obj, uid, id, cursor, context)
  337. self.getVals('l', 'city',vals, contact_obj, uid, id, cursor, context)
  338. self.getVals('facsimileTelephoneNumber', 'fax',vals, contact_obj, uid, id, cursor, context)
  339. self.getVals('mobile', 'mobile',vals, contact_obj, uid, id, cursor, context)
  340. self.getVals('homePhone', 'private_phone',vals, contact_obj, uid, id, cursor, context)
  341. self.getVals('postalCode', 'zip',vals, contact_obj, uid, id, cursor, context)
  342. self.unUnicodize(contact_obj)
  343. return contact_obj
  344. def saveLdapContact(self, id, vals, cursor, uid, context=None):
  345. """save openerp adress to ldap"""
  346. contact_obj = self.mappLdapObject(id,vals,cursor,uid,context)
  347. conn = self.connectToLdap(cursor, uid, context=context)
  348. try:
  349. if self.getconn(cursor, uid, context).ACTIVDIR:
  350. conn.connexion.add_s("CN=%s,OU=%s,%s"%(contact_obj['cn'][0], conn.OU, conn.CONTACT_DN),
  351. ldap.modlist.addModlist(contact_obj))
  352. else:
  353. conn.connexion.add_s("uid=terp_%s,OU=%s,%s"%(str(id), conn.OU, conn.CONTACT_DN),
  354. ldap.modlist.addModlist(contact_obj))
  355. except Exception, e:
  356. raise e
  357. conn.connexion.unbind_s()
  358. def updateLdapContact(self, id, vals, cursor, uid, context):
  359. """update an existing contact with the data of OpenERP"""
  360. conn = self.connectToLdap(cursor,uid,context={})
  361. try:
  362. old_contatc_obj = self.getLdapContact(conn,id)
  363. except ldap.NO_SUCH_OBJECT:
  364. self.saveLdapContact(id,vals,cursor,uid,context)
  365. return
  366. contact_obj = self.mappLdapObject(id,vals,cursor,uid,context)
  367. if conn.ACTIVDIR:
  368. modlist = []
  369. for key, val in contact_obj.items() :
  370. if key in ('cn', 'uid', 'objectclass'):
  371. continue
  372. if isinstance(val, list):
  373. val = val[0]
  374. modlist.append((ldap.MOD_REPLACE, key, val))
  375. else :
  376. modlist = ldap.modlist.modifyModlist(old_contatc_obj[1], contact_obj)
  377. try:
  378. conn.connexion.modify_s(old_contatc_obj[0], modlist)
  379. conn.connexion.unbind_s()
  380. except Exception, e:
  381. raise e
  382. def removeLdapContact(self, id, cursor, uid):
  383. """Remove a contact from ldap"""
  384. conn = self.connectToLdap(cursor,uid,context={})
  385. to_delete = None
  386. try:
  387. to_delete = self.getLdapContact(conn,id)
  388. except ldap.NO_SUCH_OBJECT:
  389. logger.notifyChannel("Warning", netsvc.LOG_INFO,
  390. _("'no object to delete in ldap' %s") %(id))
  391. except Exception, e :
  392. raise e
  393. try:
  394. if to_delete :
  395. conn.connexion.delete_s(to_delete[0])
  396. conn.connexion.unbind_s()
  397. except Exception, e:
  398. raise e
  399. def getLdapContact(self, conn, id):
  400. result = conn.connexion.search_ext_s("ou=%s,%s"%(conn.OU,conn.CONTACT_DN),
  401. ldap.SCOPE_SUBTREE,
  402. "(&(objectclass=*)(uid=terp_"+str(id)+"))")
  403. if not result:
  404. raise ldap.NO_SUCH_OBJECT
  405. return result[0]
  406. def ldaplinkactive(self, cursor, uid, context=None):
  407. """Check if ldap is activated for this company"""
  408. user = self.pool.get('res.users').browse(cursor, uid, uid, context=context)
  409. company = self.pool.get('res.company').browse(cursor, uid,user.company_id.id, context=context)
  410. return company.ldap_active
  411. def getconn(self, cursor, uid, context=None):
  412. """LdapConnMApper"""
  413. if not self.ldapMapper :
  414. self.ldapMapper = LdapConnMApper(cursor, uid, self)
  415. return self.ldapMapper
  416. def connectToLdap(self, cursor, uid, context=None):
  417. """Reinitialize ldap connection"""
  418. #getting ldap pref
  419. if not self.ldapMapper :
  420. self.getconn(cursor, uid, context)
  421. self.ldapMapper.get_connexion()
  422. return self.ldapMapper
  423. LDAPAddress()