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.
 
 
 
 

713 lines
35 KiB

  1. """
  2. """
  3. # Created on 2016.08.19
  4. #
  5. # Author: Giovanni Cannata
  6. #
  7. # Copyright 2016 - 2020 Giovanni Cannata
  8. #
  9. # This file is part of ldap3.
  10. #
  11. # ldap3 is free software: you can redistribute it and/or modify
  12. # it under the terms of the GNU Lesser General Public License as published
  13. # by the Free Software Foundation, either version 3 of the License, or
  14. # (at your option) any later version.
  15. #
  16. # ldap3 is distributed in the hope that it will be useful,
  17. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. # GNU Lesser General Public License for more details.
  20. #
  21. # You should have received a copy of the GNU Lesser General Public License
  22. # along with ldap3 in the COPYING and COPYING.LESSER files.
  23. # If not, see <http://www.gnu.org/licenses/>.
  24. import json
  25. try:
  26. from collections import OrderedDict
  27. except ImportError:
  28. from ..utils.ordDict import OrderedDict # for Python 2.6
  29. from os import linesep
  30. from copy import deepcopy
  31. from .. import STRING_TYPES, SEQUENCE_TYPES, MODIFY_ADD, MODIFY_REPLACE
  32. from .attribute import WritableAttribute
  33. from .objectDef import ObjectDef
  34. from .attrDef import AttrDef
  35. from ..core.exceptions import LDAPKeyError, LDAPCursorError, LDAPCursorAttributeError
  36. from ..utils.conv import check_json_dict, format_json, prepare_for_stream
  37. from ..protocol.rfc2849 import operation_to_ldif, add_ldif_header
  38. from ..utils.dn import safe_dn, safe_rdn, to_dn
  39. from ..utils.repr import to_stdout_encoding
  40. from ..utils.ciDict import CaseInsensitiveWithAliasDict
  41. from ..utils.config import get_config_parameter
  42. from . import STATUS_VIRTUAL, STATUS_WRITABLE, STATUS_PENDING_CHANGES, STATUS_COMMITTED, STATUS_DELETED,\
  43. STATUS_INIT, STATUS_READY_FOR_DELETION, STATUS_READY_FOR_MOVING, STATUS_READY_FOR_RENAMING, STATUS_MANDATORY_MISSING, STATUSES, INITIAL_STATUSES
  44. from ..core.results import RESULT_SUCCESS
  45. from ..utils.log import log, log_enabled, ERROR, BASIC, PROTOCOL, EXTENDED
  46. class EntryState(object):
  47. """Contains data on the status of the entry. Does not pollute the Entry __dict__.
  48. """
  49. def __init__(self, dn, cursor):
  50. self.dn = dn
  51. self._initial_status = None
  52. self._to = None # used for move and rename
  53. self.status = STATUS_INIT
  54. self.attributes = CaseInsensitiveWithAliasDict()
  55. self.raw_attributes = CaseInsensitiveWithAliasDict()
  56. self.response = None
  57. self.cursor = cursor
  58. self.origin = None # reference to the original read-only entry (set when made writable). Needed to update attributes in read-only when modified (only if both refer the same server)
  59. self.read_time = None
  60. self.changes = OrderedDict() # includes changes to commit in a writable entry
  61. if cursor.definition:
  62. self.definition = cursor.definition
  63. else:
  64. self.definition = None
  65. def __repr__(self):
  66. if self.__dict__ and self.dn is not None:
  67. r = 'DN: ' + to_stdout_encoding(self.dn) + ' - STATUS: ' + ((self._initial_status + ', ') if self._initial_status != self.status else '') + self.status + ' - READ TIME: ' + (self.read_time.isoformat() if self.read_time else '<never>') + linesep
  68. r += 'attributes: ' + ', '.join(sorted(self.attributes.keys())) + linesep
  69. r += 'object def: ' + (', '.join(sorted(self.definition._object_class)) if self.definition._object_class else '<None>') + linesep
  70. r += 'attr defs: ' + ', '.join(sorted(self.definition._attributes.keys())) + linesep
  71. r += 'response: ' + ('present' if self.response else '<None>') + linesep
  72. r += 'cursor: ' + (self.cursor.__class__.__name__ if self.cursor else '<None>') + linesep
  73. return r
  74. else:
  75. return object.__repr__(self)
  76. def __str__(self):
  77. return self.__repr__()
  78. def __getstate__(self):
  79. cpy = dict(self.__dict__)
  80. cpy['cursor'] = None
  81. return cpy
  82. def set_status(self, status):
  83. conf_ignored_mandatory_attributes_in_object_def = [v.lower() for v in get_config_parameter('IGNORED_MANDATORY_ATTRIBUTES_IN_OBJECT_DEF')]
  84. if status not in STATUSES:
  85. error_message = 'invalid entry status ' + str(status)
  86. if log_enabled(ERROR):
  87. log(ERROR, '%s for <%s>', error_message, self)
  88. raise LDAPCursorError(error_message)
  89. if status in INITIAL_STATUSES:
  90. self._initial_status = status
  91. self.status = status
  92. if status == STATUS_DELETED:
  93. self._initial_status = STATUS_VIRTUAL
  94. if status == STATUS_COMMITTED:
  95. self._initial_status = STATUS_WRITABLE
  96. if self.status == STATUS_VIRTUAL or (self.status == STATUS_PENDING_CHANGES and self._initial_status == STATUS_VIRTUAL): # checks if all mandatory attributes are present in new entries
  97. for attr in self.definition._attributes:
  98. if self.definition._attributes[attr].mandatory and attr.lower() not in conf_ignored_mandatory_attributes_in_object_def:
  99. if (attr not in self.attributes or self.attributes[attr].virtual) and attr not in self.changes:
  100. self.status = STATUS_MANDATORY_MISSING
  101. break
  102. @property
  103. def entry_raw_attributes(self):
  104. return self.raw_attributes
  105. class EntryBase(object):
  106. """The Entry object contains a single LDAP entry.
  107. Attributes can be accessed either by sequence, by assignment
  108. or as dictionary keys. Keys are not case sensitive.
  109. The Entry object is read only
  110. - The DN is retrieved by entry_dn
  111. - The cursor reference is in _cursor
  112. - Raw attributes values are retrieved with _raw_attributes and the _raw_attribute() methods
  113. """
  114. def __init__(self, dn, cursor):
  115. self._state = EntryState(dn, cursor)
  116. def __repr__(self):
  117. if self.__dict__ and self.entry_dn is not None:
  118. r = 'DN: ' + to_stdout_encoding(self.entry_dn) + ' - STATUS: ' + ((self._state._initial_status + ', ') if self._state._initial_status != self.entry_status else '') + self.entry_status + ' - READ TIME: ' + (self.entry_read_time.isoformat() if self.entry_read_time else '<never>') + linesep
  119. if self._state.attributes:
  120. for attr in sorted(self._state.attributes):
  121. if self._state.attributes[attr] or (hasattr(self._state.attributes[attr], 'changes') and self._state.attributes[attr].changes):
  122. r += ' ' + repr(self._state.attributes[attr]) + linesep
  123. return r
  124. else:
  125. return object.__repr__(self)
  126. def __str__(self):
  127. return self.__repr__()
  128. def __iter__(self):
  129. for attribute in self._state.attributes:
  130. yield self._state.attributes[attribute]
  131. # raise StopIteration # deprecated in PEP 479
  132. return
  133. def __contains__(self, item):
  134. try:
  135. self.__getitem__(item)
  136. return True
  137. except LDAPKeyError:
  138. return False
  139. def __getattr__(self, item):
  140. if isinstance(item, STRING_TYPES):
  141. if item == '_state':
  142. return object.__getattr__(self, item)
  143. item = ''.join(item.split()).lower()
  144. attr_found = None
  145. for attr in self._state.attributes.keys():
  146. if item == attr.lower():
  147. attr_found = attr
  148. break
  149. if not attr_found:
  150. for attr in self._state.attributes.aliases():
  151. if item == attr.lower():
  152. attr_found = attr
  153. break
  154. if not attr_found:
  155. for attr in self._state.attributes.keys():
  156. if item + ';binary' == attr.lower():
  157. attr_found = attr
  158. break
  159. if not attr_found:
  160. for attr in self._state.attributes.aliases():
  161. if item + ';binary' == attr.lower():
  162. attr_found = attr
  163. break
  164. if not attr_found:
  165. for attr in self._state.attributes.keys():
  166. if item + ';range' in attr.lower():
  167. attr_found = attr
  168. break
  169. if not attr_found:
  170. for attr in self._state.attributes.aliases():
  171. if item + ';range' in attr.lower():
  172. attr_found = attr
  173. break
  174. if not attr_found:
  175. error_message = 'attribute \'%s\' not found' % item
  176. if log_enabled(ERROR):
  177. log(ERROR, '%s for <%s>', error_message, self)
  178. raise LDAPCursorAttributeError(error_message)
  179. return self._state.attributes[attr]
  180. error_message = 'attribute name must be a string'
  181. if log_enabled(ERROR):
  182. log(ERROR, '%s for <%s>', error_message, self)
  183. raise LDAPCursorAttributeError(error_message)
  184. def __setattr__(self, item, value):
  185. if item == '_state':
  186. object.__setattr__(self, item, value)
  187. elif item in self._state.attributes:
  188. error_message = 'attribute \'%s\' is read only' % item
  189. if log_enabled(ERROR):
  190. log(ERROR, '%s for <%s>', error_message, self)
  191. raise LDAPCursorAttributeError(error_message)
  192. else:
  193. error_message = 'entry is read only, cannot add \'%s\'' % item
  194. if log_enabled(ERROR):
  195. log(ERROR, '%s for <%s>', error_message, self)
  196. raise LDAPCursorAttributeError(error_message)
  197. def __getitem__(self, item):
  198. if isinstance(item, STRING_TYPES):
  199. item = ''.join(item.split()).lower()
  200. attr_found = None
  201. for attr in self._state.attributes.keys():
  202. if item == attr.lower():
  203. attr_found = attr
  204. break
  205. if not attr_found:
  206. for attr in self._state.attributes.aliases():
  207. if item == attr.lower():
  208. attr_found = attr
  209. break
  210. if not attr_found:
  211. for attr in self._state.attributes.keys():
  212. if item + ';binary' == attr.lower():
  213. attr_found = attr
  214. break
  215. if not attr_found:
  216. for attr in self._state.attributes.aliases():
  217. if item + ';binary' == attr.lower():
  218. attr_found = attr
  219. break
  220. if not attr_found:
  221. error_message = 'key \'%s\' not found' % item
  222. if log_enabled(ERROR):
  223. log(ERROR, '%s for <%s>', error_message, self)
  224. raise LDAPKeyError(error_message)
  225. return self._state.attributes[attr]
  226. error_message = 'key must be a string'
  227. if log_enabled(ERROR):
  228. log(ERROR, '%s for <%s>', error_message, self)
  229. raise LDAPKeyError(error_message)
  230. def __eq__(self, other):
  231. if isinstance(other, EntryBase):
  232. return self.entry_dn == other.entry_dn
  233. return False
  234. def __lt__(self, other):
  235. if isinstance(other, EntryBase):
  236. return self.entry_dn <= other.entry_dn
  237. return False
  238. @property
  239. def entry_dn(self):
  240. return self._state.dn
  241. @property
  242. def entry_cursor(self):
  243. return self._state.cursor
  244. @property
  245. def entry_status(self):
  246. return self._state.status
  247. @property
  248. def entry_definition(self):
  249. return self._state.definition
  250. @property
  251. def entry_raw_attributes(self):
  252. return self._state.raw_attributes
  253. def entry_raw_attribute(self, name):
  254. """
  255. :param name: name of the attribute
  256. :return: raw (unencoded) value of the attribute, None if attribute is not found
  257. """
  258. return self._state.raw_attributes[name] if name in self._state.raw_attributes else None
  259. @property
  260. def entry_mandatory_attributes(self):
  261. return [attribute for attribute in self.entry_definition._attributes if self.entry_definition._attributes[attribute].mandatory]
  262. @property
  263. def entry_attributes(self):
  264. return list(self._state.attributes.keys())
  265. @property
  266. def entry_attributes_as_dict(self):
  267. return dict((attribute_key, deepcopy(attribute_value.values)) for (attribute_key, attribute_value) in self._state.attributes.items())
  268. @property
  269. def entry_read_time(self):
  270. return self._state.read_time
  271. @property
  272. def _changes(self):
  273. return self._state.changes
  274. def entry_to_json(self, raw=False, indent=4, sort=True, stream=None, checked_attributes=True, include_empty=True):
  275. json_entry = dict()
  276. json_entry['dn'] = self.entry_dn
  277. if checked_attributes:
  278. if not include_empty:
  279. # needed for python 2.6 compatibility
  280. json_entry['attributes'] = dict((key, self.entry_attributes_as_dict[key]) for key in self.entry_attributes_as_dict if self.entry_attributes_as_dict[key])
  281. else:
  282. json_entry['attributes'] = self.entry_attributes_as_dict
  283. if raw:
  284. if not include_empty:
  285. # needed for python 2.6 compatibility
  286. json_entry['raw'] = dict((key, self.entry_raw_attributes[key]) for key in self.entry_raw_attributes if self.entry_raw_attributes[key])
  287. else:
  288. json_entry['raw'] = dict(self.entry_raw_attributes)
  289. if str is bytes: # Python 2
  290. check_json_dict(json_entry)
  291. json_output = json.dumps(json_entry,
  292. ensure_ascii=True,
  293. sort_keys=sort,
  294. indent=indent,
  295. check_circular=True,
  296. default=format_json,
  297. separators=(',', ': '))
  298. if stream:
  299. stream.write(json_output)
  300. return json_output
  301. def entry_to_ldif(self, all_base64=False, line_separator=None, sort_order=None, stream=None):
  302. ldif_lines = operation_to_ldif('searchResponse', [self._state.response], all_base64, sort_order=sort_order)
  303. ldif_lines = add_ldif_header(ldif_lines)
  304. line_separator = line_separator or linesep
  305. ldif_output = line_separator.join(ldif_lines)
  306. if stream:
  307. if stream.tell() == 0:
  308. header = add_ldif_header(['-'])[0]
  309. stream.write(prepare_for_stream(header + line_separator + line_separator))
  310. stream.write(prepare_for_stream(ldif_output + line_separator + line_separator))
  311. return ldif_output
  312. class Entry(EntryBase):
  313. """The Entry object contains a single LDAP entry.
  314. Attributes can be accessed either by sequence, by assignment
  315. or as dictionary keys. Keys are not case sensitive.
  316. The Entry object is read only
  317. - The DN is retrieved by entry_dn
  318. - The Reader reference is in _cursor()
  319. - Raw attributes values are retrieved by the _ra_attributes and
  320. _raw_attribute() methods
  321. """
  322. def entry_writable(self, object_def=None, writer_cursor=None, attributes=None, custom_validator=None, auxiliary_class=None):
  323. conf_operational_attribute_prefix = get_config_parameter('ABSTRACTION_OPERATIONAL_ATTRIBUTE_PREFIX')
  324. if not self.entry_cursor.schema:
  325. error_message = 'schema must be available to make an entry writable'
  326. if log_enabled(ERROR):
  327. log(ERROR, '%s for <%s>', error_message, self)
  328. raise LDAPCursorError(error_message)
  329. # returns a new WritableEntry and its Writer cursor
  330. if object_def is None:
  331. if self.entry_cursor.definition._object_class:
  332. object_def = self.entry_definition._object_class
  333. auxiliary_class = self.entry_definition._auxiliary_class + (auxiliary_class if isinstance(auxiliary_class, SEQUENCE_TYPES) else [])
  334. elif 'objectclass' in self:
  335. object_def = self.objectclass.values
  336. if not object_def:
  337. error_message = 'object class must be specified to make an entry writable'
  338. if log_enabled(ERROR):
  339. log(ERROR, '%s for <%s>', error_message, self)
  340. raise LDAPCursorError(error_message)
  341. if not isinstance(object_def, ObjectDef):
  342. object_def = ObjectDef(object_def, self.entry_cursor.schema, custom_validator, auxiliary_class)
  343. if attributes:
  344. if isinstance(attributes, STRING_TYPES):
  345. attributes = [attributes]
  346. if isinstance(attributes, SEQUENCE_TYPES):
  347. for attribute in attributes:
  348. if attribute not in object_def._attributes:
  349. error_message = 'attribute \'%s\' not in schema for \'%s\'' % (attribute, object_def)
  350. if log_enabled(ERROR):
  351. log(ERROR, '%s for <%s>', error_message, self)
  352. raise LDAPCursorError(error_message)
  353. else:
  354. attributes = []
  355. if not writer_cursor:
  356. from .cursor import Writer # local import to avoid circular reference in import at startup
  357. writable_cursor = Writer(self.entry_cursor.connection, object_def)
  358. else:
  359. writable_cursor = writer_cursor
  360. if attributes: # force reading of attributes
  361. writable_entry = writable_cursor._refresh_object(self.entry_dn, list(attributes) + self.entry_attributes)
  362. else:
  363. writable_entry = writable_cursor._create_entry(self._state.response)
  364. writable_cursor.entries.append(writable_entry)
  365. writable_entry._state.read_time = self.entry_read_time
  366. writable_entry._state.origin = self # reference to the original read-only entry
  367. # checks original entry for custom definitions in AttrDefs
  368. attr_to_add = []
  369. attr_to_remove = []
  370. object_def_to_add = []
  371. object_def_to_remove = []
  372. for attr in writable_entry._state.origin.entry_definition._attributes:
  373. original_attr = writable_entry._state.origin.entry_definition._attributes[attr]
  374. if attr != original_attr.name and (attr not in writable_entry._state.attributes or conf_operational_attribute_prefix + original_attr.name not in writable_entry._state.attributes):
  375. old_attr_def = writable_entry.entry_definition._attributes[original_attr.name]
  376. new_attr_def = AttrDef(original_attr.name,
  377. key=attr,
  378. validate=original_attr.validate,
  379. pre_query=original_attr.pre_query,
  380. post_query=original_attr.post_query,
  381. default=original_attr.default,
  382. dereference_dn=original_attr.dereference_dn,
  383. description=original_attr.description,
  384. mandatory=old_attr_def.mandatory, # keeps value read from schema
  385. single_value=old_attr_def.single_value, # keeps value read from schema
  386. alias=original_attr.other_names)
  387. od = writable_entry.entry_definition
  388. object_def_to_remove.append(old_attr_def)
  389. object_def_to_add.append(new_attr_def)
  390. # updates attribute name in entry attributes
  391. new_attr = WritableAttribute(new_attr_def, writable_entry, writable_cursor)
  392. if original_attr.name in writable_entry._state.attributes:
  393. new_attr.other_names = writable_entry._state.attributes[original_attr.name].other_names
  394. new_attr.raw_values = writable_entry._state.attributes[original_attr.name].raw_values
  395. new_attr.values = writable_entry._state.attributes[original_attr.name].values
  396. new_attr.response = writable_entry._state.attributes[original_attr.name].response
  397. attr_to_add.append((attr, new_attr))
  398. attr_to_remove.append(original_attr.name)
  399. # writable_entry._state.attributes[attr] = new_attr
  400. ## writable_entry._state.attributes.set_alias(attr, new_attr.other_names)
  401. # del writable_entry._state.attributes[original_attr.name]
  402. for attr, new_attr in attr_to_add:
  403. writable_entry._state.attributes[attr] = new_attr
  404. for attr in attr_to_remove:
  405. del writable_entry._state.attributes[attr]
  406. for object_def in object_def_to_remove:
  407. o = writable_entry.entry_definition
  408. o -= object_def
  409. for object_def in object_def_to_add:
  410. o = writable_entry.entry_definition
  411. o += object_def
  412. writable_entry._state.set_status(STATUS_WRITABLE)
  413. return writable_entry
  414. class WritableEntry(EntryBase):
  415. def __setitem__(self, key, value):
  416. if value is not Ellipsis: # hack for using implicit operators in writable attributes
  417. self.__setattr__(key, value)
  418. def __setattr__(self, item, value):
  419. conf_attributes_excluded_from_object_def = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_OBJECT_DEF')]
  420. if item == '_state' and isinstance(value, EntryState):
  421. self.__dict__['_state'] = value
  422. return
  423. if value is not Ellipsis: # hack for using implicit operators in writable attributes
  424. # checks if using an alias
  425. if item in self.entry_cursor.definition._attributes or item.lower() in conf_attributes_excluded_from_object_def:
  426. if item not in self._state.attributes: # setting value to an attribute still without values
  427. new_attribute = WritableAttribute(self.entry_cursor.definition._attributes[item], self, cursor=self.entry_cursor)
  428. self._state.attributes[str(item)] = new_attribute # force item to a string for key in attributes dict
  429. self._state.attributes[item].set(value) # try to add to new_values
  430. else:
  431. error_message = 'attribute \'%s\' not defined' % item
  432. if log_enabled(ERROR):
  433. log(ERROR, '%s for <%s>', error_message, self)
  434. raise LDAPCursorAttributeError(error_message)
  435. def __getattr__(self, item):
  436. if isinstance(item, STRING_TYPES):
  437. if item == '_state':
  438. return self.__dict__['_state']
  439. item = ''.join(item.split()).lower()
  440. for attr in self._state.attributes.keys():
  441. if item == attr.lower():
  442. return self._state.attributes[attr]
  443. for attr in self._state.attributes.aliases():
  444. if item == attr.lower():
  445. return self._state.attributes[attr]
  446. if item in self.entry_definition._attributes: # item is a new attribute to commit, creates the AttrDef and add to the attributes to retrive
  447. self._state.attributes[item] = WritableAttribute(self.entry_definition._attributes[item], self, self.entry_cursor)
  448. self.entry_cursor.attributes.add(item)
  449. return self._state.attributes[item]
  450. error_message = 'attribute \'%s\' not defined' % item
  451. if log_enabled(ERROR):
  452. log(ERROR, '%s for <%s>', error_message, self)
  453. raise LDAPCursorAttributeError(error_message)
  454. else:
  455. error_message = 'attribute name must be a string'
  456. if log_enabled(ERROR):
  457. log(ERROR, '%s for <%s>', error_message, self)
  458. raise LDAPCursorAttributeError(error_message)
  459. @property
  460. def entry_virtual_attributes(self):
  461. return [attr for attr in self.entry_attributes if self[attr].virtual]
  462. def entry_commit_changes(self, refresh=True, controls=None, clear_history=True):
  463. if clear_history:
  464. self.entry_cursor._reset_history()
  465. if self.entry_status == STATUS_READY_FOR_DELETION:
  466. result = self.entry_cursor.connection.delete(self.entry_dn, controls)
  467. if not self.entry_cursor.connection.strategy.sync:
  468. response, result, request = self.entry_cursor.connection.get_response(result, get_request=True)
  469. else:
  470. if self.entry_cursor.connection.strategy.thread_safe:
  471. _, result, response, request = result
  472. else:
  473. response = self.entry_cursor.connection.response
  474. result = self.entry_cursor.connection.result
  475. request = self.entry_cursor.connection.request
  476. self.entry_cursor._store_operation_in_history(request, result, response)
  477. if result['result'] == RESULT_SUCCESS:
  478. dn = self.entry_dn
  479. if self._state.origin and self.entry_cursor.connection.server == self._state.origin.entry_cursor.connection.server: # deletes original read-only Entry
  480. cursor = self._state.origin.entry_cursor
  481. self._state.origin.__dict__.clear()
  482. self._state.origin.__dict__['_state'] = EntryState(dn, cursor)
  483. self._state.origin._state.set_status(STATUS_DELETED)
  484. cursor = self.entry_cursor
  485. self.__dict__.clear()
  486. self._state = EntryState(dn, cursor)
  487. self._state.set_status(STATUS_DELETED)
  488. return True
  489. return False
  490. elif self.entry_status == STATUS_READY_FOR_MOVING:
  491. result = self.entry_cursor.connection.modify_dn(self.entry_dn, '+'.join(safe_rdn(self.entry_dn)), new_superior=self._state._to)
  492. if not self.entry_cursor.connection.strategy.sync:
  493. response, result, request = self.entry_cursor.connection.get_response(result, get_request=True)
  494. else:
  495. if self.entry_cursor.connection.strategy.thread_safe:
  496. _, result, response, request = result
  497. else:
  498. response = self.entry_cursor.connection.response
  499. result = self.entry_cursor.connection.result
  500. request = self.entry_cursor.connection.request
  501. self.entry_cursor._store_operation_in_history(request, result, response)
  502. if result['result'] == RESULT_SUCCESS:
  503. self._state.dn = safe_dn('+'.join(safe_rdn(self.entry_dn)) + ',' + self._state._to)
  504. if refresh:
  505. if self.entry_refresh():
  506. if self._state.origin and self.entry_cursor.connection.server == self._state.origin.entry_cursor.connection.server: # refresh dn of origin
  507. self._state.origin._state.dn = self.entry_dn
  508. self._state.set_status(STATUS_COMMITTED)
  509. self._state._to = None
  510. return True
  511. return False
  512. elif self.entry_status == STATUS_READY_FOR_RENAMING:
  513. rdn = '+'.join(safe_rdn(self._state._to))
  514. result = self.entry_cursor.connection.modify_dn(self.entry_dn, rdn)
  515. if not self.entry_cursor.connection.strategy.sync:
  516. response, result, request = self.entry_cursor.connection.get_response(result, get_request=True)
  517. else:
  518. if self.entry_cursor.connection.strategy.thread_safe:
  519. _, result, response, request = result
  520. else:
  521. response = self.entry_cursor.connection.response
  522. result = self.entry_cursor.connection.result
  523. request = self.entry_cursor.connection.request
  524. self.entry_cursor._store_operation_in_history(request, result, response)
  525. if result['result'] == RESULT_SUCCESS:
  526. self._state.dn = rdn + ',' + ','.join(to_dn(self.entry_dn)[1:])
  527. if refresh:
  528. if self.entry_refresh():
  529. if self._state.origin and self.entry_cursor.connection.server == self._state.origin.entry_cursor.connection.server: # refresh dn of origin
  530. self._state.origin._state.dn = self.entry_dn
  531. self._state.set_status(STATUS_COMMITTED)
  532. self._state._to = None
  533. return True
  534. return False
  535. elif self.entry_status in [STATUS_VIRTUAL, STATUS_MANDATORY_MISSING]:
  536. missing_attributes = []
  537. for attr in self.entry_mandatory_attributes:
  538. if (attr not in self._state.attributes or self._state.attributes[attr].virtual) and attr not in self._changes:
  539. missing_attributes.append('\'' + attr + '\'')
  540. error_message = 'mandatory attributes %s missing in entry %s' % (', '.join(missing_attributes), self.entry_dn)
  541. if log_enabled(ERROR):
  542. log(ERROR, '%s for <%s>', error_message, self)
  543. raise LDAPCursorError(error_message)
  544. elif self.entry_status == STATUS_PENDING_CHANGES:
  545. if self._changes:
  546. if self.entry_definition._auxiliary_class: # checks if an attribute is from an auxiliary class and adds it to the objectClass attribute if not present
  547. for attr in self._changes:
  548. # checks schema to see if attribute is defined in one of the already present object classes
  549. attr_classes = self.entry_cursor.schema.attribute_types[attr].mandatory_in + self.entry_cursor.schema.attribute_types[attr].optional_in
  550. for object_class in self.objectclass:
  551. if object_class in attr_classes:
  552. break
  553. else: # executed only if the attribute class is not present in the objectClass attribute
  554. # checks if attribute is defined in one of the possible auxiliary classes
  555. for aux_class in self.entry_definition._auxiliary_class:
  556. if aux_class in attr_classes:
  557. if self._state._initial_status == STATUS_VIRTUAL: # entry is new, there must be a pending objectClass MODIFY_REPLACE
  558. self._changes['objectClass'][0][1].append(aux_class)
  559. else:
  560. self.objectclass += aux_class
  561. if self._state._initial_status == STATUS_VIRTUAL:
  562. new_attributes = dict()
  563. for attr in self._changes:
  564. new_attributes[attr] = self._changes[attr][0][1]
  565. result = self.entry_cursor.connection.add(self.entry_dn, None, new_attributes, controls)
  566. else:
  567. result = self.entry_cursor.connection.modify(self.entry_dn, self._changes, controls)
  568. if not self.entry_cursor.connection.strategy.sync: # asynchronous request
  569. response, result, request = self.entry_cursor.connection.get_response(result, get_request=True)
  570. else:
  571. if self.entry_cursor.connection.strategy.thread_safe:
  572. _, result, response, request = result
  573. else:
  574. response = self.entry_cursor.connection.response
  575. result = self.entry_cursor.connection.result
  576. request = self.entry_cursor.connection.request
  577. self.entry_cursor._store_operation_in_history(request, result, response)
  578. if result['result'] == RESULT_SUCCESS:
  579. if refresh:
  580. if self.entry_refresh():
  581. if self._state.origin and self.entry_cursor.connection.server == self._state.origin.entry_cursor.connection.server: # updates original read-only entry if present
  582. for attr in self: # adds AttrDefs from writable entry to origin entry definition if some is missing
  583. if attr.key in self.entry_definition._attributes and attr.key not in self._state.origin.entry_definition._attributes:
  584. self._state.origin.entry_cursor.definition.add_attribute(self.entry_cursor.definition._attributes[attr.key]) # adds AttrDef from writable entry to original entry if missing
  585. temp_entry = self._state.origin.entry_cursor._create_entry(self._state.response)
  586. self._state.origin.__dict__.clear()
  587. self._state.origin.__dict__['_state'] = temp_entry._state
  588. for attr in self: # returns the whole attribute object
  589. if not hasattr(attr,'virtual'):
  590. self._state.origin.__dict__[attr.key] = self._state.origin._state.attributes[attr.key]
  591. self._state.origin._state.read_time = self.entry_read_time
  592. else:
  593. self.entry_discard_changes() # if not refreshed remove committed changes
  594. self._state.set_status(STATUS_COMMITTED)
  595. return True
  596. return False
  597. def entry_discard_changes(self):
  598. self._changes.clear()
  599. self._state.set_status(self._state._initial_status)
  600. def entry_delete(self):
  601. if self.entry_status not in [STATUS_WRITABLE, STATUS_COMMITTED, STATUS_READY_FOR_DELETION]:
  602. error_message = 'cannot delete entry, invalid status: ' + self.entry_status
  603. if log_enabled(ERROR):
  604. log(ERROR, '%s for <%s>', error_message, self)
  605. raise LDAPCursorError(error_message)
  606. self._state.set_status(STATUS_READY_FOR_DELETION)
  607. def entry_refresh(self, tries=4, seconds=2):
  608. """
  609. Refreshes the entry from the LDAP Server
  610. """
  611. if self.entry_cursor.connection:
  612. if self.entry_cursor.refresh_entry(self, tries, seconds):
  613. return True
  614. return False
  615. def entry_move(self, destination_dn):
  616. if self.entry_status not in [STATUS_WRITABLE, STATUS_COMMITTED, STATUS_READY_FOR_MOVING]:
  617. error_message = 'cannot move entry, invalid status: ' + self.entry_status
  618. if log_enabled(ERROR):
  619. log(ERROR, '%s for <%s>', error_message, self)
  620. raise LDAPCursorError(error_message)
  621. self._state._to = safe_dn(destination_dn)
  622. self._state.set_status(STATUS_READY_FOR_MOVING)
  623. def entry_rename(self, new_name):
  624. if self.entry_status not in [STATUS_WRITABLE, STATUS_COMMITTED, STATUS_READY_FOR_RENAMING]:
  625. error_message = 'cannot rename entry, invalid status: ' + self.entry_status
  626. if log_enabled(ERROR):
  627. log(ERROR, '%s for <%s>', error_message, self)
  628. raise LDAPCursorError(error_message)
  629. self._state._to = new_name
  630. self._state.set_status(STATUS_READY_FOR_RENAMING)
  631. @property
  632. def entry_changes(self):
  633. return self._changes