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.

546 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
  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. pass
  237. if ids is not None:
  238. if isinstance(ids, (int, long)):
  239. ids = [ids]
  240. if len(ids) == 1:
  241. self.addNeededFields(ids[0], vals, cursor, uid)
  242. email = vals.get('email', False)
  243. phone = vals.get('phone', False)
  244. fax = vals.get('fax', False)
  245. mobile = vals.get('mobile', False)
  246. private_phone = vals.get('private_phone', False)
  247. if email:
  248. if re.match("^.+\\@(\\[?)[a-zA-Z0-9\\-\\.]+\\."
  249. "([a-zA-Z]{2,3}|[0-9]{1,3})(\\]?)$",
  250. email) is None:
  251. raise orm.except_orm(_('Warning !'),
  252. _('Please enter a valid e-mail'))
  253. phones = (('phone', phone), ('fax', fax), ('mobile', mobile),
  254. ('private_phone', private_phone))
  255. for phone_tuple in phones:
  256. phone_number = phone_tuple[1]
  257. if phone_number:
  258. if not phone_number.startswith('+'):
  259. raise orm.except_orm(
  260. _('Warning !'),
  261. _('Please enter a valid phone number in %s'
  262. ' international format (i.e. leading +)')
  263. % phone_tuple[0])
  264. def getVals(self, att_name, key, vals, dico, uid, ids, cr, context=None):
  265. """map to values to dict"""
  266. if not context:
  267. context = {}
  268. # We explicitly test False value
  269. if vals.get(key, False) is not False:
  270. dico[att_name] = vals[key]
  271. else:
  272. if context.get('init_mode'):
  273. return False
  274. tmp = self.read(cr, uid, ids, [key], context={})
  275. if tmp.get(key, False):
  276. dico[att_name] = tmp[key]
  277. def _un_unicodize_buf(self, in_buf):
  278. if isinstance(in_buf, unicode):
  279. try:
  280. return in_buf.encode()
  281. except Exception:
  282. return unicodedata.normalize("NFKD", in_buf).encode(
  283. 'ascii', 'ignore'
  284. )
  285. return in_buf
  286. def unUnicodize(self, indict):
  287. """remove unicode data of modlist as unicode is not supported
  288. by python-ldap library till version 2.7
  289. """
  290. for key in indict:
  291. if not isinstance(indict[key], list):
  292. indict[key] = self._un_unicodize_buf(indict[key])
  293. else:
  294. nonutfArray = []
  295. for val in indict[key]:
  296. nonutfArray.append(self._un_unicodize_buf(val))
  297. indict[key] = nonutfArray
  298. def addNeededFields(self, id, vals, cursor, uid):
  299. previousvalue = self.browse(cursor, uid, [id])[0]
  300. if not vals.get('partner_id'):
  301. vals['partner_id'] = previousvalue.partner_id.id
  302. values_to_check = ('email', 'phone', 'fax', 'mobile', 'firstname',
  303. 'lastname', 'private_phone', 'street', 'street2')
  304. for val in values_to_check:
  305. if not vals.get(val):
  306. vals[val] = previousvalue[val]
  307. def mappLdapObject(self, id, vals, cr, uid, context):
  308. """Mapp ResPArtner adress to moddlist"""
  309. self.addNeededFields(id, vals, cr, uid)
  310. conn = self.getconn(cr, uid, {})
  311. partner_obj = self.pool.get('res.partner')
  312. part_name = partner_obj.browse(cr, uid, vals['partner_id']).name
  313. vals['partner'] = part_name
  314. name = self._compute_name(vals.get('firstname'), vals.get('lastname'))
  315. if name:
  316. cn = name
  317. else:
  318. cn = part_name
  319. if not vals.get('lastname'):
  320. vals['lastname'] = part_name
  321. contact_obj = {'objectclass': ['inetOrgPerson'],
  322. 'uid': ['terp_' + str(id)],
  323. 'ou': [conn.OU],
  324. 'cn': [cn],
  325. 'sn': [vals['lastname']]}
  326. if not vals.get('street'):
  327. vals['street'] = u''
  328. if not vals.get('street2'):
  329. vals['street2'] = u''
  330. street_key = 'street'
  331. if self.getconn(cr, uid, {}).ACTIVDIR:
  332. # ENTERING THE M$ Realm and it is weird
  333. # We manage the address
  334. street_key = 'streetAddress'
  335. contact_obj[street_key] = vals['street'] + "\r\n" + vals['street2']
  336. # we modifiy the class
  337. contact_obj['objectclass'] = [
  338. 'top',
  339. 'person',
  340. 'organizationalPerson',
  341. 'inetOrgPerson',
  342. 'user',
  343. ]
  344. # we handle the country
  345. if vals.get('country_id'):
  346. country = self.browse(cr, uid, id, context=context).country_id
  347. if country:
  348. vals['country_id'] = country.name
  349. vals['c'] = country.code
  350. else:
  351. vals['country_id'] = False
  352. vals['c'] = False
  353. if vals.get('country_id', False):
  354. self.getVals(
  355. 'co', 'country_id', vals, contact_obj, uid, id, cr,
  356. context
  357. )
  358. self.getVals('c', 'c', vals, contact_obj, uid, id, cr, context)
  359. # we compute the display name
  360. vals['display'] = '%s %s' % (vals['partner'], contact_obj['cn'][0])
  361. # we get the title
  362. if self.browse(cr, uid, id).function:
  363. contact_obj['description'] = self.browse(
  364. cr, uid, id).function.name
  365. # we replace carriage return
  366. if vals.get('comment', False):
  367. vals['comment'] = vals['comment'].replace("\n", "\r\n")
  368. # Active directory specific fields
  369. self.getVals(
  370. 'company', 'partner', vals, contact_obj, uid, id, cr, context
  371. )
  372. self.getVals(
  373. 'info', 'comment', vals, contact_obj, uid, id, cr, context
  374. )
  375. self.getVals(
  376. 'displayName', 'partner', vals, contact_obj, uid, id, cr,
  377. context
  378. )
  379. # Web site management
  380. if self.browse(cr, uid, id).partner_id.website:
  381. vals['website'] = self.browse(cr, uid, id).partner_id.website
  382. self.getVals(
  383. 'wWWHomePage', 'website', vals, contact_obj, uid, id, cr,
  384. context
  385. )
  386. del(vals['website'])
  387. self.getVals(
  388. 'title', 'title', vals, contact_obj, uid, id, cr, context
  389. )
  390. else:
  391. contact_obj[street_key] = vals['street'] + u"\n" + vals['street2']
  392. self.getVals(
  393. 'o', 'partner', vals, contact_obj, uid, id, cr, context
  394. )
  395. # Common attributes
  396. self.getVals(
  397. 'givenName', 'firstname', vals, contact_obj, uid, id, cr, context
  398. )
  399. self.getVals('mail', 'email', vals, contact_obj, uid, id, cr, context)
  400. self.getVals(
  401. 'telephoneNumber', 'phone', vals, contact_obj, uid, id, cr, context
  402. )
  403. self.getVals('l', 'city', vals, contact_obj, uid, id, cr, context)
  404. self.getVals(
  405. 'facsimileTelephoneNumber', 'fax', vals, contact_obj, uid, id, cr,
  406. context
  407. )
  408. self.getVals(
  409. 'mobile', 'mobile', vals, contact_obj, uid, id, cr, context
  410. )
  411. self.getVals(
  412. 'homePhone', 'private_phone', vals, contact_obj, uid, id, cr,
  413. context
  414. )
  415. self.getVals(
  416. 'postalCode', 'zip', vals, contact_obj, uid, id, cr, context
  417. )
  418. self.unUnicodize(contact_obj)
  419. return contact_obj
  420. def saveLdapContact(self, id, vals, cursor, uid, context=None):
  421. """save openerp adress to ldap"""
  422. contact_obj = self.mappLdapObject(id, vals, cursor, uid, context)
  423. conn = self.connectToLdap(cursor, uid, context=context)
  424. try:
  425. if self.getconn(cursor, uid, context).ACTIVDIR:
  426. conn.connexion.add_s(
  427. "CN=%s,OU=%s,%s" % (contact_obj['cn'][0],
  428. conn.OU,
  429. conn.CONTACT_DN),
  430. ldap.modlist.addModlist(contact_obj)
  431. )
  432. else:
  433. conn.connexion.add_s(
  434. "uid=terp_%s,OU=%s,%s" % (str(id),
  435. conn.OU,
  436. conn.CONTACT_DN),
  437. ldap.modlist.addModlist(contact_obj))
  438. except Exception:
  439. raise
  440. conn.connexion.unbind_s()
  441. def updateLdapContact(self, id, vals, cursor, uid, context):
  442. """update an existing contact with the data of OpenERP"""
  443. conn = self.connectToLdap(cursor, uid, context={})
  444. try:
  445. old_contatc_obj = self.getLdapContact(conn, id)
  446. except ldap.NO_SUCH_OBJECT:
  447. self.saveLdapContact(id, vals, cursor, uid, context)
  448. return
  449. contact_obj = self.mappLdapObject(id, vals, cursor, uid, context)
  450. if conn.ACTIVDIR:
  451. modlist = []
  452. for key, val in contact_obj.items():
  453. if key in ('cn', 'uid', 'objectclass'):
  454. continue
  455. if isinstance(val, list):
  456. val = val[0]
  457. modlist.append((ldap.MOD_REPLACE, key, val))
  458. else:
  459. modlist = ldap.modlist.modifyModlist(
  460. old_contatc_obj[1],
  461. contact_obj
  462. )
  463. try:
  464. conn.connexion.modify_s(old_contatc_obj[0], modlist)
  465. conn.connexion.unbind_s()
  466. except Exception:
  467. raise
  468. def removeLdapContact(self, id, cursor, uid):
  469. """Remove a contact from ldap"""
  470. conn = self.connectToLdap(cursor, uid, context={})
  471. to_delete = None
  472. try:
  473. to_delete = self.getLdapContact(conn, id)
  474. except ldap.NO_SUCH_OBJECT:
  475. logger.notifyChannel("Warning", netsvc.LOG_INFO,
  476. _("'no object to delete in ldap' %s") % (id))
  477. except Exception:
  478. raise
  479. try:
  480. if to_delete:
  481. conn.connexion.delete_s(to_delete[0])
  482. conn.connexion.unbind_s()
  483. except Exception:
  484. raise
  485. def getLdapContact(self, conn, id):
  486. result = conn.connexion.search_ext_s(
  487. "ou=%s, %s" % (conn.OU, conn.CONTACT_DN),
  488. ldap.SCOPE_SUBTREE,
  489. "(&(objectclass=*)(uid=terp_" + str(id) + "))"
  490. )
  491. if not result:
  492. raise ldap.NO_SUCH_OBJECT
  493. return result[0]
  494. def ldaplinkactive(self, cursor, uid, context=None):
  495. """Check if ldap is activated for this company"""
  496. users_pool = self.pool['res.users']
  497. user = users_pool.browse(cursor, uid, uid, context=context)
  498. return user.company_id.ldap_active
  499. def getconn(self, cursor, uid, context=None):
  500. """LdapConnMApper"""
  501. if not self.ldapMapper:
  502. self.ldapMapper = LdapConnMApper(cursor, uid, self)
  503. return self.ldapMapper
  504. def connectToLdap(self, cursor, uid, context=None):
  505. """Reinitialize ldap connection"""
  506. # getting ldap pref
  507. if not self.ldapMapper:
  508. self.getconn(cursor, uid, context)
  509. self.ldapMapper.get_connexion()
  510. return self.ldapMapper