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.

457 lines
20 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
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), 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 responsibility 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. # guarantees and support are strongly advised 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. from openerp 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 openerp.osv import orm, fields
  43. from openerp.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 orm.except_orm(_('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(orm.Model):
  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:
  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.get('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 context is None:
  185. context = {}
  186. if ids:
  187. self.getconn(cursor, uid, {})
  188. if not isinstance(ids, list):
  189. ids = [ids]
  190. if self.ldaplinkactive(cursor, uid, context):
  191. for id in ids:
  192. self.removeLdapContact(id, cursor, uid)
  193. return super(LDAPAddress, self).unlink(cursor, uid, ids)
  194. def validate_entries(self, vals, cursor, uid, ids):
  195. """Validate data of an address based on the inetOrgPerson schema"""
  196. for val in vals:
  197. try:
  198. if isinstance(vals[val], basestring):
  199. vals[val] = unicode(vals[val].decode('utf8'))
  200. except UnicodeError:
  201. logger.notifyChannel('LDAP encode', netsvc.LOG_DEBUG,
  202. 'cannot unicode ' + vals[val])
  203. pass
  204. if ids is not None:
  205. if isinstance(ids, (int, long)):
  206. ids = [ids]
  207. if len(ids) == 1:
  208. self.addNeededFields(ids[0], vals, cursor, uid)
  209. email = vals.get('email', False)
  210. phone = vals.get('phone', False)
  211. fax = vals.get('fax', False)
  212. mobile = vals.get('mobile', 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 orm.except_orm(_('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 orm.except_orm(_('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:
  231. context = {}
  232. # We explicitly test False value
  233. if vals.get(key, False) is not False:
  234. dico[att_name] = vals[key]
  235. else:
  236. if context.get('init_mode'):
  237. return False
  238. tmp = self.read(cursor, uid, ids, [key], context={})
  239. if tmp.get(key, False):
  240. dico[att_name] = tmp[key]
  241. def _un_unicodize_buf(self, in_buf):
  242. if isinstance(in_buf, unicode):
  243. try:
  244. return in_buf.encode()
  245. except Exception:
  246. return unicodedata.normalize("NFKD", in_buf).encode('ascii', 'ignore')
  247. return in_buf
  248. def unUnicodize(self, indict):
  249. """remove unicode data of modlist as unicode is not supported
  250. by python-ldap library till version 2.7
  251. """
  252. for key in indict:
  253. if not isinstance(indict[key], list):
  254. indict[key] = self._un_unicodize_buf(indict[key])
  255. else:
  256. nonutfArray = []
  257. for val in indict[key]:
  258. nonutfArray.append(self._un_unicodize_buf(val))
  259. indict[key] = nonutfArray
  260. def addNeededFields(self, id, vals, cursor, uid):
  261. previousvalue = self.browse(cursor, uid, [id])[0]
  262. if not vals.get('partner_id'):
  263. vals['partner_id'] = previousvalue.partner_id.id
  264. values_to_check = ('email', 'phone', 'fax', 'mobile', 'firstname',
  265. 'lastname', 'private_phone', 'street', 'street2')
  266. for val in values_to_check:
  267. if not vals.get(val):
  268. vals[val] = previousvalue[val]
  269. def mappLdapObject(self, id, vals, cursor, uid, context):
  270. """Mapp ResPArtner adress to moddlist"""
  271. self.addNeededFields(id, vals, cursor, uid)
  272. conn = self.getconn(cursor, uid, {})
  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, context=context).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:
  356. raise
  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:
  381. raise
  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:
  392. raise
  393. try:
  394. if to_delete:
  395. conn.connexion.delete_s(to_delete[0])
  396. conn.connexion.unbind_s()
  397. except Exception:
  398. raise
  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