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.

545 lines
21 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  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),
  8. # Thanks to Laurent Lauden for his code adaptation
  9. # Active directory Donor: M. Benadiba (Informatique Assistances.fr)
  10. # Contribution : Joel Grand-Guillaume
  11. #
  12. # WARNING: This program as such is intended to be used by professional
  13. # programmers who take the whole responsibility of assessing all potential
  14. # consequences resulting from its eventual inadequacies and bugs
  15. # End users who are looking for a ready-to-use solution with commercial
  16. # guarantees and support are strongly advised to contract a Free Software
  17. # Service Company
  18. #
  19. # This program is Free Software; you can redistribute it and/or
  20. # modify it under the terms of the GNU General Public License
  21. # as published by the Free Software Foundation; either version 2
  22. # of the License, or (at your option) any later version.
  23. #
  24. # This program is distributed in the hope that it will be useful,
  25. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  26. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  27. # GNU General Public License for more details.
  28. #
  29. # You should have received a copy of the GNU General Public License
  30. # along with this program; if not, write to the Free Software
  31. # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  32. #
  33. ##############################################################################
  34. # TODO Find why company parameter are cached
  35. import re
  36. import unicodedata
  37. from openerp import netsvc
  38. try:
  39. import ldap
  40. import ldap.modlist
  41. except:
  42. print('python ldap not installed please install it in order to use this '
  43. 'module')
  44. from openerp.osv import orm, fields
  45. from openerp.tools.translate import _
  46. logger = netsvc.Logger()
  47. class LdapConnMApper(object):
  48. """LdapConnMApper: push specific fields from the Terp
  49. Partner_contacts to the LDAP schema inetOrgPerson. Ldap bind options
  50. are stored in company.
  51. """
  52. def __init__(self, cursor, uid, osv_obj, context=None):
  53. """Initialize connexion to ldap by using parameter set in the
  54. current user compagny
  55. """
  56. logger.notifyChannel("MY TOPIC", netsvc.LOG_DEBUG,
  57. _('Initalize LDAP CONN'))
  58. self.USER_DN = ''
  59. self.CONTACT_DN = ''
  60. self.LDAP_SERVER = ''
  61. self.PASS = ''
  62. self.OU = ''
  63. self.connexion = ''
  64. self.ACTIVDIR = False
  65. # Reading ldap pref
  66. user = osv_obj.pool.get('res.users').browse(
  67. cursor, uid, uid, context=context
  68. )
  69. company = user.company_id
  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
  77. mand = (
  78. self.USER_DN,
  79. self.CONTACT_DN,
  80. self.LDAP_SERVER,
  81. self.PASS, self.OU
  82. )
  83. if company.ldap_active:
  84. for param in mand:
  85. if not param:
  86. raise orm.except_orm(
  87. _('Warning !'),
  88. _('An LDAP parameter is missing for company %s') %
  89. (company.name, )
  90. )
  91. def get_connexion(self):
  92. """create a new ldap connexion"""
  93. logger.notifyChannel(
  94. "LDAP Address",
  95. netsvc.LOG_DEBUG,
  96. _('connecting to server ldap %s') % (self.LDAP_SERVER, )
  97. )
  98. if self.PORT:
  99. self.connexion = ldap.open(self.LDAP_SERVER, self.PORT)
  100. else:
  101. self.connexion = ldap.open(self.LDAP_SERVER)
  102. self.connexion.simple_bind_s(self.USER_DN, self.PASS)
  103. return self.connexion
  104. class LDAPAddress(orm.Model):
  105. """Override the CRUD of the objet in order to dynamically bind to ldap"""
  106. _inherit = 'res.partner.address'
  107. ldapMapper = None
  108. def init(self, cr):
  109. logger = netsvc.Logger()
  110. try:
  111. logger.notifyChannel(
  112. _('LDAP address init'),
  113. netsvc.LOG_INFO,
  114. _('try to ALTER TABLE res_partner_address RENAME '
  115. 'column name to lastname ;'))
  116. cr.execute(
  117. 'ALTER TABLE res_partner_address'
  118. 'RENAME column name to lastname ;'
  119. )
  120. except Exception:
  121. cr.rollback()
  122. logger.notifyChannel(
  123. _('LDAP address init'),
  124. netsvc.LOG_INFO,
  125. _('Warning ! impossible to rename column name into lastname, '
  126. 'this is probably already done or does not exist')
  127. )
  128. def _compute_name(self, firstname, lastname):
  129. firstname = firstname or u''
  130. lastname = lastname or u''
  131. firstname = (u' ' + firstname).rstrip()
  132. return u"%s%s" % (lastname, firstname)
  133. def _name_get_fnc(self, cursor, uid, ids, name, arg, context=None):
  134. """Get the name (lastname + firstname), otherwise ''"""
  135. if not ids:
  136. return []
  137. reads = self.read(cursor, uid, ids, ['lastname', 'firstname'])
  138. res = []
  139. for record in reads:
  140. # We want to have " firstname" or ""
  141. name = self._compute_name(record['firstname'], record['lastname'])
  142. res.append((record['id'], name))
  143. return dict(res)
  144. # TODO get the new version of name search not vulnerable to sql injections
  145. # def name_search(
  146. # self, cursor, user, name, args=None, operator='ilike',
  147. # context=None, limit=100):
  148. # if not context: context = {}
  149. # prep_name = '.*%s.*' %(name)
  150. # cursor.execute(("""
  151. # select id from res_partner_address where
  152. # (to_ascii(convert( lastname, 'UTF8', 'LATIN1'),'LATIN-1') ~* '%s'
  153. # or to_ascii(convert( firstname, 'UTF8', 'LATIN1'),'LATIN-1') ~* '%s')
  154. # limit %s""") % (prep_name, prep_name, limit))
  155. # res = cursor.fetchall()
  156. # if res:
  157. # res = [x[0] for x in res]
  158. # else:
  159. # res = []
  160. # search in partner name to know if we are searching partner...
  161. # partner_obj=self.pool.get('res.partner')
  162. # part_len = len(res)-limit
  163. # if part_len > 0:
  164. # partner_res = partner_obj.search(
  165. # cursor, user, [('name', 'ilike', name)],
  166. # limit=part_len, context=context
  167. # )
  168. # for p in partner_res:
  169. # addresses = partner_obj.browse(cursor, user, p).address
  170. # Take each contact and add it to
  171. # for add in addresses:
  172. # res.append(add.id)
  173. # return self.name_get(cursor, user, res, context)
  174. _columns = {
  175. 'firstname': fields.char('First name', size=256),
  176. 'lastname': fields.char('Last name', size=256),
  177. 'name': fields.function(
  178. _name_get_fnc,
  179. method=True,
  180. type="char",
  181. size=512,
  182. store=True,
  183. string='Contact Name',
  184. help='Name generated from the first name and last name',
  185. nodrop=True
  186. ),
  187. 'private_phone': fields.char('Private phone', size=128),
  188. }
  189. def create(self, cursor, uid, vals, context=None):
  190. if context is None:
  191. context = {}
  192. self.getconn(cursor, uid, {})
  193. ids = None
  194. self.validate_entries(vals, cursor, uid, ids)
  195. tmp_id = super(LDAPAddress, self).create(cursor, uid,
  196. vals, context)
  197. if self.ldaplinkactive(cursor, uid, context):
  198. self.saveLdapContact(tmp_id, vals, cursor, uid, context)
  199. return tmp_id
  200. def write(self, cursor, uid, ids, vals, context=None):
  201. context = context or {}
  202. self.getconn(cursor, uid, {})
  203. if not isinstance(ids, list):
  204. ids = [ids]
  205. if ids:
  206. self.validate_entries(vals, cursor, uid, ids)
  207. if context.get('init_mode'):
  208. success = True
  209. else:
  210. success = super(LDAPAddress, self).write(cursor, uid, ids,
  211. vals, context)
  212. if self.ldaplinkactive(cursor, uid, context):
  213. for address_id in ids:
  214. self.updateLdapContact(address_id, vals, cursor, uid, context)
  215. return success
  216. def unlink(self, cursor, uid, ids, context=None):
  217. if context is None:
  218. context = {}
  219. if ids:
  220. self.getconn(cursor, uid, {})
  221. if not isinstance(ids, list):
  222. ids = [ids]
  223. if self.ldaplinkactive(cursor, uid, context):
  224. for id in ids:
  225. self.removeLdapContact(id, cursor, uid)
  226. return super(LDAPAddress, self).unlink(cursor, uid, ids)
  227. def validate_entries(self, vals, cursor, uid, ids):
  228. """Validate data of an address based on the inetOrgPerson schema"""
  229. for val in vals:
  230. try:
  231. if isinstance(vals[val], basestring):
  232. vals[val] = unicode(vals[val].decode('utf8'))
  233. except UnicodeError:
  234. logger.notifyChannel('LDAP encode', netsvc.LOG_DEBUG,
  235. 'cannot unicode ' + vals[val])
  236. if ids is not None:
  237. if isinstance(ids, (int, long)):
  238. ids = [ids]
  239. if len(ids) == 1:
  240. self.addNeededFields(ids[0], vals, cursor, uid)
  241. email = vals.get('email', False)
  242. phone = vals.get('phone', False)
  243. fax = vals.get('fax', False)
  244. mobile = vals.get('mobile', False)
  245. private_phone = vals.get('private_phone', False)
  246. if email:
  247. if re.match("^.+\\@(\\[?)[a-zA-Z0-9\\-\\.]+\\."
  248. "([a-zA-Z]{2,3}|[0-9]{1,3})(\\]?)$",
  249. email) is None:
  250. raise orm.except_orm(_('Warning !'),
  251. _('Please enter a valid e-mail'))
  252. phones = (('phone', phone), ('fax', fax), ('mobile', mobile),
  253. ('private_phone', private_phone))
  254. for phone_tuple in phones:
  255. phone_number = phone_tuple[1]
  256. if phone_number:
  257. if not phone_number.startswith('+'):
  258. raise orm.except_orm(
  259. _('Warning !'),
  260. _('Please enter a valid phone number in %s'
  261. ' international format (i.e. leading +)')
  262. % phone_tuple[0])
  263. def getVals(self, att_name, key, vals, dico, uid, ids, cr, context=None):
  264. """map to values to dict"""
  265. if not context:
  266. context = {}
  267. # We explicitly test False value
  268. if vals.get(key, False) is not False:
  269. dico[att_name] = vals[key]
  270. else:
  271. if context.get('init_mode'):
  272. return False
  273. tmp = self.read(cr, uid, ids, [key], context={})
  274. if tmp.get(key, False):
  275. dico[att_name] = tmp[key]
  276. def _un_unicodize_buf(self, in_buf):
  277. if isinstance(in_buf, unicode):
  278. try:
  279. return in_buf.encode()
  280. except Exception:
  281. return unicodedata.normalize("NFKD", in_buf).encode(
  282. 'ascii', 'ignore'
  283. )
  284. return in_buf
  285. def unUnicodize(self, indict):
  286. """remove unicode data of modlist as unicode is not supported
  287. by python-ldap library till version 2.7
  288. """
  289. for key in indict:
  290. if not isinstance(indict[key], list):
  291. indict[key] = self._un_unicodize_buf(indict[key])
  292. else:
  293. nonutfArray = []
  294. for val in indict[key]:
  295. nonutfArray.append(self._un_unicodize_buf(val))
  296. indict[key] = nonutfArray
  297. def addNeededFields(self, id, vals, cursor, uid):
  298. previousvalue = self.browse(cursor, uid, [id])[0]
  299. if not vals.get('partner_id'):
  300. vals['partner_id'] = previousvalue.partner_id.id
  301. values_to_check = ('email', 'phone', 'fax', 'mobile', 'firstname',
  302. 'lastname', 'private_phone', 'street', 'street2')
  303. for val in values_to_check:
  304. if not vals.get(val):
  305. vals[val] = previousvalue[val]
  306. def mappLdapObject(self, id, vals, cr, uid, context):
  307. """Mapp ResPArtner adress to moddlist"""
  308. self.addNeededFields(id, vals, cr, uid)
  309. conn = self.getconn(cr, uid, {})
  310. partner_obj = self.pool.get('res.partner')
  311. part_name = partner_obj.browse(cr, uid, vals['partner_id']).name
  312. vals['partner'] = part_name
  313. name = self._compute_name(vals.get('firstname'), vals.get('lastname'))
  314. if name:
  315. cn = name
  316. else:
  317. cn = part_name
  318. if not vals.get('lastname'):
  319. vals['lastname'] = part_name
  320. contact_obj = {'objectclass': ['inetOrgPerson'],
  321. 'uid': ['terp_' + str(id)],
  322. 'ou': [conn.OU],
  323. 'cn': [cn],
  324. 'sn': [vals['lastname']]}
  325. if not vals.get('street'):
  326. vals['street'] = u''
  327. if not vals.get('street2'):
  328. vals['street2'] = u''
  329. street_key = 'street'
  330. if self.getconn(cr, uid, {}).ACTIVDIR:
  331. # ENTERING THE M$ Realm and it is weird
  332. # We manage the address
  333. street_key = 'streetAddress'
  334. contact_obj[street_key] = vals['street'] + "\r\n" + vals['street2']
  335. # we modifiy the class
  336. contact_obj['objectclass'] = [
  337. 'top',
  338. 'person',
  339. 'organizationalPerson',
  340. 'inetOrgPerson',
  341. 'user',
  342. ]
  343. # we handle the country
  344. if vals.get('country_id'):
  345. country = self.browse(cr, uid, id, context=context).country_id
  346. if country:
  347. vals['country_id'] = country.name
  348. vals['c'] = country.code
  349. else:
  350. vals['country_id'] = False
  351. vals['c'] = False
  352. if vals.get('country_id', False):
  353. self.getVals(
  354. 'co', 'country_id', vals, contact_obj, uid, id, cr,
  355. context
  356. )
  357. self.getVals('c', 'c', vals, contact_obj, uid, id, cr, context)
  358. # we compute the display name
  359. vals['display'] = '%s %s' % (vals['partner'], contact_obj['cn'][0])
  360. # we get the title
  361. if self.browse(cr, uid, id).function:
  362. contact_obj['description'] = self.browse(
  363. cr, uid, id).function.name
  364. # we replace carriage return
  365. if vals.get('comment', False):
  366. vals['comment'] = vals['comment'].replace("\n", "\r\n")
  367. # Active directory specific fields
  368. self.getVals(
  369. 'company', 'partner', vals, contact_obj, uid, id, cr, context
  370. )
  371. self.getVals(
  372. 'info', 'comment', vals, contact_obj, uid, id, cr, context
  373. )
  374. self.getVals(
  375. 'displayName', 'partner', vals, contact_obj, uid, id, cr,
  376. context
  377. )
  378. # Web site management
  379. if self.browse(cr, uid, id).partner_id.website:
  380. vals['website'] = self.browse(cr, uid, id).partner_id.website
  381. self.getVals(
  382. 'wWWHomePage', 'website', vals, contact_obj, uid, id, cr,
  383. context
  384. )
  385. del(vals['website'])
  386. self.getVals(
  387. 'title', 'title', vals, contact_obj, uid, id, cr, context
  388. )
  389. else:
  390. contact_obj[street_key] = vals['street'] + u"\n" + vals['street2']
  391. self.getVals(
  392. 'o', 'partner', vals, contact_obj, uid, id, cr, context
  393. )
  394. # Common attributes
  395. self.getVals(
  396. 'givenName', 'firstname', vals, contact_obj, uid, id, cr, context
  397. )
  398. self.getVals('mail', 'email', vals, contact_obj, uid, id, cr, context)
  399. self.getVals(
  400. 'telephoneNumber', 'phone', vals, contact_obj, uid, id, cr, context
  401. )
  402. self.getVals('l', 'city', vals, contact_obj, uid, id, cr, context)
  403. self.getVals(
  404. 'facsimileTelephoneNumber', 'fax', vals, contact_obj, uid, id, cr,
  405. context
  406. )
  407. self.getVals(
  408. 'mobile', 'mobile', vals, contact_obj, uid, id, cr, context
  409. )
  410. self.getVals(
  411. 'homePhone', 'private_phone', vals, contact_obj, uid, id, cr,
  412. context
  413. )
  414. self.getVals(
  415. 'postalCode', 'zip', vals, contact_obj, uid, id, cr, context
  416. )
  417. self.unUnicodize(contact_obj)
  418. return contact_obj
  419. def saveLdapContact(self, id, vals, cursor, uid, context=None):
  420. """save openerp adress to ldap"""
  421. contact_obj = self.mappLdapObject(id, vals, cursor, uid, context)
  422. conn = self.connectToLdap(cursor, uid, context=context)
  423. try:
  424. if self.getconn(cursor, uid, context).ACTIVDIR:
  425. conn.connexion.add_s(
  426. "CN=%s,OU=%s,%s" % (contact_obj['cn'][0],
  427. conn.OU,
  428. conn.CONTACT_DN),
  429. ldap.modlist.addModlist(contact_obj)
  430. )
  431. else:
  432. conn.connexion.add_s(
  433. "uid=terp_%s,OU=%s,%s" % (str(id),
  434. conn.OU,
  435. conn.CONTACT_DN),
  436. ldap.modlist.addModlist(contact_obj))
  437. except Exception:
  438. raise
  439. conn.connexion.unbind_s()
  440. def updateLdapContact(self, id, vals, cursor, uid, context):
  441. """update an existing contact with the data of OpenERP"""
  442. conn = self.connectToLdap(cursor, uid, context={})
  443. try:
  444. old_contatc_obj = self.getLdapContact(conn, id)
  445. except ldap.NO_SUCH_OBJECT:
  446. self.saveLdapContact(id, vals, cursor, uid, context)
  447. return
  448. contact_obj = self.mappLdapObject(id, vals, cursor, uid, context)
  449. if conn.ACTIVDIR:
  450. modlist = []
  451. for key, val in contact_obj.items():
  452. if key in ('cn', 'uid', 'objectclass'):
  453. continue
  454. if isinstance(val, list):
  455. val = val[0]
  456. modlist.append((ldap.MOD_REPLACE, key, val))
  457. else:
  458. modlist = ldap.modlist.modifyModlist(
  459. old_contatc_obj[1],
  460. contact_obj
  461. )
  462. try:
  463. conn.connexion.modify_s(old_contatc_obj[0], modlist)
  464. conn.connexion.unbind_s()
  465. except Exception:
  466. raise
  467. def removeLdapContact(self, id, cursor, uid):
  468. """Remove a contact from ldap"""
  469. conn = self.connectToLdap(cursor, uid, context={})
  470. to_delete = None
  471. try:
  472. to_delete = self.getLdapContact(conn, id)
  473. except ldap.NO_SUCH_OBJECT:
  474. logger.notifyChannel("Warning", netsvc.LOG_INFO,
  475. _("'no object to delete in ldap' %s") % (id))
  476. except Exception:
  477. raise
  478. try:
  479. if to_delete:
  480. conn.connexion.delete_s(to_delete[0])
  481. conn.connexion.unbind_s()
  482. except Exception:
  483. raise
  484. def getLdapContact(self, conn, id):
  485. result = conn.connexion.search_ext_s(
  486. "ou=%s, %s" % (conn.OU, conn.CONTACT_DN),
  487. ldap.SCOPE_SUBTREE,
  488. "(&(objectclass=*)(uid=terp_" + str(id) + "))"
  489. )
  490. if not result:
  491. raise ldap.NO_SUCH_OBJECT
  492. return result[0]
  493. def ldaplinkactive(self, cursor, uid, context=None):
  494. """Check if ldap is activated for this company"""
  495. users_pool = self.pool['res.users']
  496. user = users_pool.browse(cursor, uid, uid, context=context)
  497. return user.company_id.ldap_active
  498. def getconn(self, cursor, uid, context=None):
  499. """LdapConnMApper"""
  500. if not self.ldapMapper:
  501. self.ldapMapper = LdapConnMApper(cursor, uid, self)
  502. return self.ldapMapper
  503. def connectToLdap(self, cursor, uid, context=None):
  504. """Reinitialize ldap connection"""
  505. # getting ldap pref
  506. if not self.ldapMapper:
  507. self.getconn(cursor, uid, context)
  508. self.ldapMapper.get_connexion()
  509. return self.ldapMapper