Development of an internal social media platform with personalised dashboards for students
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.

cursor.py 43KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904
  1. """
  2. """
  3. # Created on 2014.01.06
  4. #
  5. # Author: Giovanni Cannata
  6. #
  7. # Copyright 2014 - 2018 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. from collections import namedtuple
  25. from copy import deepcopy
  26. from datetime import datetime
  27. from os import linesep
  28. from time import sleep
  29. from . import STATUS_VIRTUAL, STATUS_READ, STATUS_WRITABLE
  30. from .. import SUBTREE, LEVEL, DEREF_ALWAYS, DEREF_NEVER, BASE, SEQUENCE_TYPES, STRING_TYPES, get_config_parameter
  31. from ..abstract import STATUS_PENDING_CHANGES
  32. from .attribute import Attribute, OperationalAttribute, WritableAttribute
  33. from .attrDef import AttrDef
  34. from .objectDef import ObjectDef
  35. from .entry import Entry, WritableEntry
  36. from ..core.exceptions import LDAPCursorError, LDAPObjectDereferenceError
  37. from ..core.results import RESULT_SUCCESS
  38. from ..utils.ciDict import CaseInsensitiveWithAliasDict
  39. from ..utils.dn import safe_dn, safe_rdn
  40. from ..utils.conv import to_raw
  41. from ..utils.config import get_config_parameter
  42. from ..utils.log import log, log_enabled, ERROR, BASIC, PROTOCOL, EXTENDED
  43. from ..protocol.oid import ATTRIBUTE_DIRECTORY_OPERATION, ATTRIBUTE_DISTRIBUTED_OPERATION, ATTRIBUTE_DSA_OPERATION, CLASS_AUXILIARY
  44. Operation = namedtuple('Operation', ('request', 'result', 'response'))
  45. def _ret_search_value(value):
  46. return value[0] + '=' + value[1:] if value[0] in '<>~' and value[1] != '=' else value
  47. def _create_query_dict(query_text):
  48. """
  49. Create a dictionary with query key:value definitions
  50. query_text is a comma delimited key:value sequence
  51. """
  52. query_dict = dict()
  53. if query_text:
  54. for arg_value_str in query_text.split(','):
  55. if ':' in arg_value_str:
  56. arg_value_list = arg_value_str.split(':')
  57. query_dict[arg_value_list[0].strip()] = arg_value_list[1].strip()
  58. return query_dict
  59. class Cursor(object):
  60. # entry_class and attribute_class define the type of entry and attribute used by the cursor
  61. # entry_initial_status defines the initial status of a entry
  62. # entry_class = Entry, must be defined in subclasses
  63. # attribute_class = Attribute, must be defined in subclasses
  64. # entry_initial_status = STATUS, must be defined in subclasses
  65. def __init__(self, connection, object_def, get_operational_attributes=False, attributes=None, controls=None, auxiliary_class=None):
  66. conf_attributes_excluded_from_object_def = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_OBJECT_DEF')]
  67. self.connection = connection
  68. self.get_operational_attributes = get_operational_attributes
  69. if connection._deferred_bind or connection._deferred_open: # probably a lazy connection, tries to bind
  70. connection._fire_deferred()
  71. if isinstance(object_def, (STRING_TYPES, SEQUENCE_TYPES)):
  72. object_def = ObjectDef(object_def, connection.server.schema, auxiliary_class=auxiliary_class)
  73. self.definition = object_def
  74. if attributes: # checks if requested attributes are defined in ObjectDef
  75. not_defined_attributes = []
  76. if isinstance(attributes, STRING_TYPES):
  77. attributes = [attributes]
  78. for attribute in attributes:
  79. if attribute not in self.definition._attributes and attribute.lower() not in conf_attributes_excluded_from_object_def:
  80. not_defined_attributes.append(attribute)
  81. if not_defined_attributes:
  82. error_message = 'Attributes \'%s\' non in definition' % ', '.join(not_defined_attributes)
  83. if log_enabled(ERROR):
  84. log(ERROR, '%s for <%s>', error_message, self)
  85. raise LDAPCursorError(error_message)
  86. self.attributes = set(attributes) if attributes else set([attr.name for attr in self.definition])
  87. self.controls = controls
  88. self.execution_time = None
  89. self.entries = []
  90. self.schema = self.connection.server.schema
  91. self._do_not_reset = False # used for refreshing entry in entry_refresh() without removing all entries from the Cursor
  92. self._operation_history = list() # a list storing all the requests, results and responses for the last cursor operation
  93. def __repr__(self):
  94. r = 'CURSOR : ' + self.__class__.__name__ + linesep
  95. r += 'CONN : ' + str(self.connection) + linesep
  96. r += 'DEFS : ' + ', '.join(self.definition._object_class)
  97. if self.definition._auxiliary_class:
  98. r += ' [AUX: ' + ', '.join(self.definition._auxiliary_class) + ']'
  99. r += linesep
  100. # for attr_def in sorted(self.definition):
  101. # r += (attr_def.key if attr_def.key == attr_def.name else (attr_def.key + ' <' + attr_def.name + '>')) + ', '
  102. # if r[-2] == ',':
  103. # r = r[:-2]
  104. # r += ']' + linesep
  105. if hasattr(self, 'attributes'):
  106. r += 'ATTRS : ' + repr(sorted(self.attributes)) + (' [OPERATIONAL]' if self.get_operational_attributes else '') + linesep
  107. if isinstance(self, Reader):
  108. if hasattr(self, 'base'):
  109. r += 'BASE : ' + repr(self.base) + (' [SUB]' if self.sub_tree else ' [LEVEL]') + linesep
  110. if hasattr(self, '_query') and self._query:
  111. r += 'QUERY : ' + repr(self._query) + ('' if '(' in self._query else (' [AND]' if self.components_in_and else ' [OR]')) + linesep
  112. if hasattr(self, 'validated_query') and self.validated_query:
  113. r += 'PARSED : ' + repr(self.validated_query) + ('' if '(' in self._query else (' [AND]' if self.components_in_and else ' [OR]')) + linesep
  114. if hasattr(self, 'query_filter') and self.query_filter:
  115. r += 'FILTER : ' + repr(self.query_filter) + linesep
  116. if hasattr(self, 'execution_time') and self.execution_time:
  117. r += 'ENTRIES: ' + str(len(self.entries))
  118. r += ' [executed at: ' + str(self.execution_time.isoformat()) + ']' + linesep
  119. if self.failed:
  120. r += 'LAST OPERATION FAILED [' + str(len(self.errors)) + ' failure' + ('s' if len(self.errors) > 1 else '') + ' at operation' + ('s ' if len(self.errors) > 1 else ' ') + ', '.join([str(i) for i, error in enumerate(self.operations) if error.result['result'] != RESULT_SUCCESS]) + ']'
  121. return r
  122. def __str__(self):
  123. return self.__repr__()
  124. def __iter__(self):
  125. return self.entries.__iter__()
  126. def __getitem__(self, item):
  127. """Return indexed item, if index is not found then try to sequentially search in DN of entries.
  128. If only one entry is found return it else raise a KeyError exception. The exception message
  129. includes the number of entries that matches, if less than 10 entries match then show the DNs
  130. in the exception message.
  131. """
  132. try:
  133. return self.entries[item]
  134. except TypeError:
  135. pass
  136. if isinstance(item, STRING_TYPES):
  137. found = self.match_dn(item)
  138. if len(found) == 1:
  139. return found[0]
  140. elif len(found) > 1:
  141. error_message = 'Multiple entries found: %d entries match the text in dn' % len(found) + ('' if len(found) > 10 else (' [' + '; '.join([e.entry_dn for e in found]) + ']'))
  142. if log_enabled(ERROR):
  143. log(ERROR, '%s for <%s>', error_message, self)
  144. raise KeyError(error_message)
  145. error_message = 'no entry found'
  146. if log_enabled(ERROR):
  147. log(ERROR, '%s for <%s>', error_message, self)
  148. raise KeyError(error_message)
  149. def __len__(self):
  150. return len(self.entries)
  151. if str is not bytes: # Python 3
  152. def __bool__(self): # needed to make the cursor appears as existing in "if cursor:" even if there are no entries
  153. return True
  154. else: # Python 2
  155. def __nonzero__(self):
  156. return True
  157. def _get_attributes(self, response, attr_defs, entry):
  158. """Assign the result of the LDAP query to the Entry object dictionary.
  159. If the optional 'post_query' callable is present in the AttrDef it is called with each value of the attribute and the callable result is stored in the attribute.
  160. Returns the default value for missing attributes.
  161. If the 'dereference_dn' in AttrDef is a ObjectDef then the attribute values are treated as distinguished name and the relevant entry is retrieved and stored in the attribute value.
  162. """
  163. conf_operational_attribute_prefix = get_config_parameter('ABSTRACTION_OPERATIONAL_ATTRIBUTE_PREFIX')
  164. conf_attributes_excluded_from_object_def = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_OBJECT_DEF')]
  165. attributes = CaseInsensitiveWithAliasDict()
  166. used_attribute_names = set()
  167. for attr in attr_defs:
  168. attr_def = attr_defs[attr]
  169. attribute_name = None
  170. for attr_name in response['attributes']:
  171. if attr_def.name.lower() == attr_name.lower():
  172. attribute_name = attr_name
  173. break
  174. if attribute_name or attr_def.default is not NotImplemented: # attribute value found in result or default value present - NotImplemented allows use of None as default
  175. attribute = self.attribute_class(attr_def, entry, self)
  176. attribute.response = response
  177. attribute.raw_values = response['raw_attributes'][attribute_name] if attribute_name else None
  178. if attr_def.post_query and attr_def.name in response['attributes'] and response['raw_attributes'] != list():
  179. attribute.values = attr_def.post_query(attr_def.key, response['attributes'][attribute_name])
  180. else:
  181. if attr_def.default is NotImplemented or (attribute_name and response['raw_attributes'][attribute_name] != list()):
  182. attribute.values = response['attributes'][attribute_name]
  183. else:
  184. attribute.values = attr_def.default if isinstance(attr_def.default, SEQUENCE_TYPES) else [attr_def.default]
  185. if not isinstance(attribute.values, list): # force attribute values to list (if attribute is single-valued)
  186. attribute.values = [attribute.values]
  187. if attr_def.dereference_dn: # try to get object referenced in value
  188. if attribute.values:
  189. temp_reader = Reader(self.connection, attr_def.dereference_dn, base='', get_operational_attributes=self.get_operational_attributes, controls=self.controls)
  190. temp_values = []
  191. for element in attribute.values:
  192. if entry.entry_dn != element:
  193. temp_values.append(temp_reader.search_object(element))
  194. else:
  195. error_message = 'object %s is referencing itself in the \'%s\' attribute' % (entry.entry_dn, attribute.definition.name)
  196. if log_enabled(ERROR):
  197. log(ERROR, '%s for <%s>', error_message, self)
  198. raise LDAPObjectDereferenceError(error_message)
  199. del temp_reader # remove the temporary Reader
  200. attribute.values = temp_values
  201. attributes[attribute.key] = attribute
  202. if attribute.other_names:
  203. attributes.set_alias(attribute.key, attribute.other_names)
  204. if attr_def.other_names:
  205. attributes.set_alias(attribute.key, attr_def.other_names)
  206. used_attribute_names.add(attribute_name)
  207. if self.attributes:
  208. used_attribute_names.update(self.attributes)
  209. for attribute_name in response['attributes']:
  210. if attribute_name not in used_attribute_names:
  211. operational_attribute = False
  212. # check if the type is an operational attribute
  213. if attribute_name in self.schema.attribute_types:
  214. if self.schema.attribute_types[attribute_name].no_user_modification or self.schema.attribute_types[attribute_name].usage in [ATTRIBUTE_DIRECTORY_OPERATION, ATTRIBUTE_DISTRIBUTED_OPERATION, ATTRIBUTE_DSA_OPERATION]:
  215. operational_attribute = True
  216. else:
  217. operational_attribute = True
  218. if not operational_attribute and attribute_name not in attr_defs and attribute_name.lower() not in conf_attributes_excluded_from_object_def:
  219. error_message = 'attribute \'%s\' not in object class \'%s\' for entry %s' % (attribute_name, ', '.join(entry.entry_definition._object_class), entry.entry_dn)
  220. if log_enabled(ERROR):
  221. log(ERROR, '%s for <%s>', error_message, self)
  222. raise LDAPCursorError(error_message)
  223. attribute = OperationalAttribute(AttrDef(conf_operational_attribute_prefix + attribute_name), entry, self)
  224. attribute.raw_values = response['raw_attributes'][attribute_name]
  225. attribute.values = response['attributes'][attribute_name] if isinstance(response['attributes'][attribute_name], SEQUENCE_TYPES) else [response['attributes'][attribute_name]]
  226. if (conf_operational_attribute_prefix + attribute_name) not in attributes:
  227. attributes[conf_operational_attribute_prefix + attribute_name] = attribute
  228. return attributes
  229. def match_dn(self, dn):
  230. """Return entries with text in DN"""
  231. matched = []
  232. for entry in self.entries:
  233. if dn.lower() in entry.entry_dn.lower():
  234. matched.append(entry)
  235. return matched
  236. def match(self, attributes, value):
  237. """Return entries with text in one of the specified attributes"""
  238. matched = []
  239. if not isinstance(attributes, SEQUENCE_TYPES):
  240. attributes = [attributes]
  241. for entry in self.entries:
  242. found = False
  243. for attribute in attributes:
  244. if attribute in entry:
  245. for attr_value in entry[attribute].values:
  246. if hasattr(attr_value, 'lower') and hasattr(value, 'lower') and value.lower() in attr_value.lower():
  247. found = True
  248. elif value == attr_value:
  249. found = True
  250. if found:
  251. matched.append(entry)
  252. break
  253. if found:
  254. break
  255. # checks raw values, tries to convert value to byte
  256. raw_value = to_raw(value)
  257. if isinstance(raw_value, (bytes, bytearray)):
  258. for attr_value in entry[attribute].raw_values:
  259. if hasattr(attr_value, 'lower') and hasattr(raw_value, 'lower') and raw_value.lower() in attr_value.lower():
  260. found = True
  261. elif raw_value == attr_value:
  262. found = True
  263. if found:
  264. matched.append(entry)
  265. break
  266. if found:
  267. break
  268. return matched
  269. def _create_entry(self, response):
  270. if not response['type'] == 'searchResEntry':
  271. return None
  272. entry = self.entry_class(response['dn'], self) # define an Entry (writable or readonly), as specified in the cursor definition
  273. entry._state.attributes = self._get_attributes(response, self.definition._attributes, entry)
  274. entry._state.entry_raw_attributes = deepcopy(response['raw_attributes'])
  275. entry._state.response = response
  276. entry._state.read_time = datetime.now()
  277. entry._state.set_status(self.entry_initial_status)
  278. for attr in entry: # returns the whole attribute object
  279. entry.__dict__[attr.key] = attr
  280. return entry
  281. def _execute_query(self, query_scope, attributes):
  282. if not self.connection:
  283. error_message = 'no connection established'
  284. if log_enabled(ERROR):
  285. log(ERROR, '%s for <%s>', error_message, self)
  286. raise LDAPCursorError(error_message)
  287. old_query_filter = None
  288. if query_scope == BASE: # requesting a single object so an always-valid filter is set
  289. if hasattr(self, 'query_filter'): # only Reader has a query filter
  290. old_query_filter = self.query_filter
  291. self.query_filter = '(objectclass=*)'
  292. else:
  293. self._create_query_filter()
  294. if log_enabled(PROTOCOL):
  295. log(PROTOCOL, 'executing query - base: %s - filter: %s - scope: %s for <%s>', self.base, self.query_filter, query_scope, self)
  296. with self.connection:
  297. result = self.connection.search(search_base=self.base,
  298. search_filter=self.query_filter,
  299. search_scope=query_scope,
  300. dereference_aliases=self.dereference_aliases,
  301. attributes=attributes if attributes else list(self.attributes),
  302. get_operational_attributes=self.get_operational_attributes,
  303. controls=self.controls)
  304. if not self.connection.strategy.sync:
  305. response, result, request = self.connection.get_response(result, get_request=True)
  306. else:
  307. response = self.connection.response
  308. result = self.connection.result
  309. request = self.connection.request
  310. self._store_operation_in_history(request, result, response)
  311. if self._do_not_reset: # trick to not remove entries when using _refresh()
  312. return self._create_entry(response[0])
  313. self.entries = []
  314. for r in response:
  315. entry = self._create_entry(r)
  316. if entry is not None:
  317. self.entries.append(entry)
  318. if 'objectClass' in entry:
  319. for object_class in entry.objectClass:
  320. if self.schema.object_classes[object_class].kind == CLASS_AUXILIARY and object_class not in self.definition._auxiliary_class:
  321. # add auxiliary class to object definition
  322. self.definition._auxiliary_class.append(object_class)
  323. self.definition._populate_attr_defs(object_class)
  324. self.execution_time = datetime.now()
  325. if old_query_filter: # requesting a single object so an always-valid filter is set
  326. self.query_filter = old_query_filter
  327. def remove(self, entry):
  328. if log_enabled(PROTOCOL):
  329. log(PROTOCOL, 'removing entry <%s> in <%s>', entry, self)
  330. self.entries.remove(entry)
  331. def _reset_history(self):
  332. self._operation_history = list()
  333. def _store_operation_in_history(self, request, result, response):
  334. self._operation_history.append(Operation(request, result, response))
  335. @property
  336. def operations(self):
  337. return self._operation_history
  338. @property
  339. def errors(self):
  340. return [error for error in self._operation_history if error.result['result'] != RESULT_SUCCESS]
  341. @property
  342. def failed(self):
  343. if hasattr(self, '_operation_history'):
  344. return any([error.result['result'] != RESULT_SUCCESS for error in self._operation_history])
  345. class Reader(Cursor):
  346. """Reader object to perform searches:
  347. :param connection: the LDAP connection object to use
  348. :type connection: LDAPConnection
  349. :param object_def: the ObjectDef of the LDAP object returned
  350. :type object_def: ObjectDef
  351. :param query: the simplified query (will be transformed in an LDAP filter)
  352. :type query: str
  353. :param base: starting base of the search
  354. :type base: str
  355. :param components_in_and: specify if assertions in the query must all be satisfied or not (AND/OR)
  356. :type components_in_and: bool
  357. :param sub_tree: specify if the search must be performed ad Single Level (False) or Whole SubTree (True)
  358. :type sub_tree: bool
  359. :param get_operational_attributes: specify if operational attributes are returned or not
  360. :type get_operational_attributes: bool
  361. :param controls: controls to be used in search
  362. :type controls: tuple
  363. """
  364. entry_class = Entry # entries are read_only
  365. attribute_class = Attribute # attributes are read_only
  366. entry_initial_status = STATUS_READ
  367. def __init__(self, connection, object_def, base, query='', components_in_and=True, sub_tree=True, get_operational_attributes=False, attributes=None, controls=None, auxiliary_class=None):
  368. Cursor.__init__(self, connection, object_def, get_operational_attributes, attributes, controls, auxiliary_class)
  369. self._components_in_and = components_in_and
  370. self.sub_tree = sub_tree
  371. self._query = query
  372. self.base = base
  373. self.dereference_aliases = DEREF_ALWAYS
  374. self.validated_query = None
  375. self._query_dict = dict()
  376. self._validated_query_dict = dict()
  377. self.query_filter = None
  378. self.reset()
  379. if log_enabled(BASIC):
  380. log(BASIC, 'instantiated Reader Cursor: <%r>', self)
  381. @property
  382. def query(self):
  383. return self._query
  384. @query.setter
  385. def query(self, value):
  386. self._query = value
  387. self.reset()
  388. @property
  389. def components_in_and(self):
  390. return self._components_in_and
  391. @components_in_and.setter
  392. def components_in_and(self, value):
  393. self._components_in_and = value
  394. self.reset()
  395. def clear(self):
  396. """Clear the Reader search parameters
  397. """
  398. self.dereference_aliases = DEREF_ALWAYS
  399. self._reset_history()
  400. def reset(self):
  401. """Clear all the Reader parameters
  402. """
  403. self.clear()
  404. self.validated_query = None
  405. self._query_dict = dict()
  406. self._validated_query_dict = dict()
  407. self.execution_time = None
  408. self.query_filter = None
  409. self.entries = []
  410. self._create_query_filter()
  411. def _validate_query(self):
  412. """Processes the text query and verifies that the requested friendly names are in the Reader dictionary
  413. If the AttrDef has a 'validate' property the callable is executed and if it returns False an Exception is raised
  414. """
  415. if not self._query_dict:
  416. self._query_dict = _create_query_dict(self._query)
  417. query = ''
  418. for d in sorted(self._query_dict):
  419. attr = d[1:] if d[0] in '&|' else d
  420. for attr_def in self.definition:
  421. if ''.join(attr.split()).lower() == attr_def.key.lower():
  422. attr = attr_def.key
  423. break
  424. if attr in self.definition:
  425. vals = sorted(self._query_dict[d].split(';'))
  426. query += (d[0] + attr if d[0] in '&|' else attr) + ': '
  427. for val in vals:
  428. val = val.strip()
  429. val_not = True if val[0] == '!' else False
  430. val_search_operator = '=' # default
  431. if val_not:
  432. if val[1:].lstrip()[0] not in '=<>~':
  433. value = val[1:].lstrip()
  434. else:
  435. val_search_operator = val[1:].lstrip()[0]
  436. value = val[1:].lstrip()[1:]
  437. else:
  438. if val[0] not in '=<>~':
  439. value = val.lstrip()
  440. else:
  441. val_search_operator = val[0]
  442. value = val[1:].lstrip()
  443. if self.definition[attr].validate:
  444. validated = self.definition[attr].validate(value) # returns True, False or a value to substitute to the actual values
  445. if validated is False:
  446. error_message = 'validation failed for attribute %s and value %s' % (d, val)
  447. if log_enabled(ERROR):
  448. log(ERROR, '%s for <%s>', error_message, self)
  449. raise LDAPCursorError(error_message)
  450. elif validated is not True: # a valid LDAP value equivalent to the actual values
  451. value = validated
  452. if val_not:
  453. query += '!' + val_search_operator + str(value)
  454. else:
  455. query += val_search_operator + str(value)
  456. query += ';'
  457. query = query[:-1] + ', '
  458. else:
  459. error_message = 'attribute \'%s\' not in definition' % attr
  460. if log_enabled(ERROR):
  461. log(ERROR, '%s for <%s>', error_message, self)
  462. raise LDAPCursorError(error_message)
  463. self.validated_query = query[:-2]
  464. self._validated_query_dict = _create_query_dict(self.validated_query)
  465. def _create_query_filter(self):
  466. """Converts the query dictionary to the filter text"""
  467. self.query_filter = ''
  468. if self.definition._object_class:
  469. self.query_filter += '(&'
  470. if isinstance(self.definition._object_class, SEQUENCE_TYPES) and len(self.definition._object_class) == 1:
  471. self.query_filter += '(objectClass=' + self.definition._object_class[0] + ')'
  472. elif isinstance(self.definition._object_class, SEQUENCE_TYPES):
  473. self.query_filter += '(&'
  474. for object_class in self.definition._object_class:
  475. self.query_filter += '(objectClass=' + object_class + ')'
  476. self.query_filter += ')'
  477. else:
  478. error_message = 'object class must be a string or a list'
  479. if log_enabled(ERROR):
  480. log(ERROR, '%s for <%s>', error_message, self)
  481. raise LDAPCursorError(error_message)
  482. if self._query and self._query.startswith('(') and self._query.endswith(')'): # query is already an LDAP filter
  483. if 'objectclass' not in self._query.lower():
  484. self.query_filter += self._query + ')' # if objectclass not in filter adds from definition
  485. else:
  486. self.query_filter = self._query
  487. return
  488. elif self._query: # if a simplified filter is present
  489. if not self.components_in_and:
  490. self.query_filter += '(|'
  491. elif not self.definition._object_class:
  492. self.query_filter += '(&'
  493. self._validate_query()
  494. attr_counter = 0
  495. for attr in sorted(self._validated_query_dict):
  496. attr_counter += 1
  497. multi = True if ';' in self._validated_query_dict[attr] else False
  498. vals = sorted(self._validated_query_dict[attr].split(';'))
  499. attr_def = self.definition[attr[1:]] if attr[0] in '&|' else self.definition[attr]
  500. if attr_def.pre_query:
  501. modvals = []
  502. for val in vals:
  503. modvals.append(val[0] + attr_def.pre_query(attr_def.key, val[1:]))
  504. vals = modvals
  505. if multi:
  506. if attr[0] in '&|':
  507. self.query_filter += '(' + attr[0]
  508. else:
  509. self.query_filter += '(|'
  510. for val in vals:
  511. if val[0] == '!':
  512. self.query_filter += '(!(' + attr_def.name + _ret_search_value(val[1:]) + '))'
  513. else:
  514. self.query_filter += '(' + attr_def.name + _ret_search_value(val) + ')'
  515. if multi:
  516. self.query_filter += ')'
  517. if not self.components_in_and:
  518. self.query_filter += '))'
  519. else:
  520. self.query_filter += ')'
  521. if not self.definition._object_class and attr_counter == 1: # removes unneeded starting filter
  522. self.query_filter = self.query_filter[2: -1]
  523. if self.query_filter == '(|)' or self.query_filter == '(&)': # removes empty filter
  524. self.query_filter = ''
  525. else: # no query, remove unneeded leading (&
  526. self.query_filter = self.query_filter[2:]
  527. def search(self, attributes=None):
  528. """Perform the LDAP search
  529. :return: Entries found in search
  530. """
  531. self.clear()
  532. query_scope = SUBTREE if self.sub_tree else LEVEL
  533. if log_enabled(PROTOCOL):
  534. log(PROTOCOL, 'performing search in <%s>', self)
  535. self._execute_query(query_scope, attributes)
  536. return self.entries
  537. def search_object(self, entry_dn=None, attributes=None): # base must be a single dn
  538. """Perform the LDAP search operation SINGLE_OBJECT scope
  539. :return: Entry found in search
  540. """
  541. if log_enabled(PROTOCOL):
  542. log(PROTOCOL, 'performing object search in <%s>', self)
  543. self.clear()
  544. if entry_dn:
  545. old_base = self.base
  546. self.base = entry_dn
  547. self._execute_query(BASE, attributes)
  548. self.base = old_base
  549. else:
  550. self._execute_query(BASE, attributes)
  551. return self.entries[0] if len(self.entries) > 0 else None
  552. def search_level(self, attributes=None):
  553. """Perform the LDAP search operation with SINGLE_LEVEL scope
  554. :return: Entries found in search
  555. """
  556. if log_enabled(PROTOCOL):
  557. log(PROTOCOL, 'performing single level search in <%s>', self)
  558. self.clear()
  559. self._execute_query(LEVEL, attributes)
  560. return self.entries
  561. def search_subtree(self, attributes=None):
  562. """Perform the LDAP search operation WHOLE_SUBTREE scope
  563. :return: Entries found in search
  564. """
  565. if log_enabled(PROTOCOL):
  566. log(PROTOCOL, 'performing whole subtree search in <%s>', self)
  567. self.clear()
  568. self._execute_query(SUBTREE, attributes)
  569. return self.entries
  570. def _entries_generator(self, responses):
  571. for response in responses:
  572. yield self._create_entry(response)
  573. def search_paged(self, paged_size, paged_criticality=True, generator=True, attributes=None):
  574. """Perform a paged search, can be called as an Iterator
  575. :param attributes: optional attributes to search
  576. :param paged_size: number of entries returned in each search
  577. :type paged_size: int
  578. :param paged_criticality: specify if server must not execute the search if it is not capable of paging searches
  579. :type paged_criticality: bool
  580. :param generator: if True the paged searches are executed while generating the entries,
  581. if False all the paged searches are execute before returning the generator
  582. :type generator: bool
  583. :return: Entries found in search
  584. """
  585. if log_enabled(PROTOCOL):
  586. log(PROTOCOL, 'performing paged search in <%s> with paged size %s', self, str(paged_size))
  587. if not self.connection:
  588. error_message = 'no connection established'
  589. if log_enabled(ERROR):
  590. log(ERROR, '%s for <%s>', error_message, self)
  591. raise LDAPCursorError(error_message)
  592. self.clear()
  593. self._create_query_filter()
  594. self.entries = []
  595. self.execution_time = datetime.now()
  596. response = self.connection.extend.standard.paged_search(search_base=self.base,
  597. search_filter=self.query_filter,
  598. search_scope=SUBTREE if self.sub_tree else LEVEL,
  599. dereference_aliases=self.dereference_aliases,
  600. attributes=attributes if attributes else self.attributes,
  601. get_operational_attributes=self.get_operational_attributes,
  602. controls=self.controls,
  603. paged_size=paged_size,
  604. paged_criticality=paged_criticality,
  605. generator=generator)
  606. if generator:
  607. return self._entries_generator(response)
  608. else:
  609. return list(self._entries_generator(response))
  610. class Writer(Cursor):
  611. entry_class = WritableEntry
  612. attribute_class = WritableAttribute
  613. entry_initial_status = STATUS_WRITABLE
  614. @staticmethod
  615. def from_cursor(cursor, connection=None, object_def=None, custom_validator=None):
  616. if connection is None:
  617. connection = cursor.connection
  618. if object_def is None:
  619. object_def = cursor.definition
  620. writer = Writer(connection, object_def, attributes=cursor.attributes)
  621. for entry in cursor.entries:
  622. if isinstance(cursor, Reader):
  623. entry.entry_writable(object_def, writer, custom_validator=custom_validator)
  624. elif isinstance(cursor, Writer):
  625. pass
  626. else:
  627. error_message = 'unknown cursor type %s' % str(type(cursor))
  628. if log_enabled(ERROR):
  629. log(ERROR, '%s', error_message)
  630. raise LDAPCursorError(error_message)
  631. writer.execution_time = cursor.execution_time
  632. if log_enabled(BASIC):
  633. log(BASIC, 'instantiated Writer Cursor <%r> from cursor <%r>', writer, cursor)
  634. return writer
  635. @staticmethod
  636. def from_response(connection, object_def, response=None):
  637. if response is None:
  638. if not connection.strategy.sync:
  639. error_message = 'with asynchronous strategies response must be specified'
  640. if log_enabled(ERROR):
  641. log(ERROR, '%s', error_message)
  642. raise LDAPCursorError(error_message)
  643. elif connection.response:
  644. response = connection.response
  645. else:
  646. error_message = 'response not present'
  647. if log_enabled(ERROR):
  648. log(ERROR, '%s', error_message)
  649. raise LDAPCursorError(error_message)
  650. writer = Writer(connection, object_def)
  651. for resp in response:
  652. if resp['type'] == 'searchResEntry':
  653. entry = writer._create_entry(resp)
  654. writer.entries.append(entry)
  655. if log_enabled(BASIC):
  656. log(BASIC, 'instantiated Writer Cursor <%r> from response', writer)
  657. return writer
  658. def __init__(self, connection, object_def, get_operational_attributes=False, attributes=None, controls=None, auxiliary_class=None):
  659. Cursor.__init__(self, connection, object_def, get_operational_attributes, attributes, controls, auxiliary_class)
  660. self.dereference_aliases = DEREF_NEVER
  661. if log_enabled(BASIC):
  662. log(BASIC, 'instantiated Writer Cursor: <%r>', self)
  663. def commit(self, refresh=True):
  664. if log_enabled(PROTOCOL):
  665. log(PROTOCOL, 'committed changes for <%s>', self)
  666. self._reset_history()
  667. successful = True
  668. for entry in self.entries:
  669. if not entry.entry_commit_changes(refresh=refresh, controls=self.controls, clear_history=False):
  670. successful = False
  671. self.execution_time = datetime.now()
  672. return successful
  673. def discard(self):
  674. if log_enabled(PROTOCOL):
  675. log(PROTOCOL, 'discarded changes for <%s>', self)
  676. for entry in self.entries:
  677. entry.entry_discard_changes()
  678. def _refresh_object(self, entry_dn, attributes=None, tries=4, seconds=2, controls=None): # base must be a single dn
  679. """Performs the LDAP search operation SINGLE_OBJECT scope
  680. :return: Entry found in search
  681. """
  682. if log_enabled(PROTOCOL):
  683. log(PROTOCOL, 'refreshing object <%s> for <%s>', entry_dn, self)
  684. if not self.connection:
  685. error_message = 'no connection established'
  686. if log_enabled(ERROR):
  687. log(ERROR, '%s for <%s>', error_message, self)
  688. raise LDAPCursorError(error_message)
  689. response = []
  690. with self.connection:
  691. counter = 0
  692. while counter < tries:
  693. result = self.connection.search(search_base=entry_dn,
  694. search_filter='(objectclass=*)',
  695. search_scope=BASE,
  696. dereference_aliases=DEREF_NEVER,
  697. attributes=attributes if attributes else self.attributes,
  698. get_operational_attributes=self.get_operational_attributes,
  699. controls=controls)
  700. if not self.connection.strategy.sync:
  701. response, result, request = self.connection.get_response(result, get_request=True)
  702. else:
  703. response = self.connection.response
  704. result = self.connection.result
  705. request = self.connection.request
  706. if result['result'] in [RESULT_SUCCESS]:
  707. break
  708. sleep(seconds)
  709. counter += 1
  710. self._store_operation_in_history(request, result, response)
  711. if len(response) == 1:
  712. return self._create_entry(response[0])
  713. elif len(response) == 0:
  714. return None
  715. error_message = 'more than 1 entry returned for a single object search'
  716. if log_enabled(ERROR):
  717. log(ERROR, '%s for <%s>', error_message, self)
  718. raise LDAPCursorError(error_message)
  719. def new(self, dn):
  720. if log_enabled(BASIC):
  721. log(BASIC, 'creating new entry <%s> for <%s>', dn, self)
  722. dn = safe_dn(dn)
  723. for entry in self.entries: # checks if dn is already used in an cursor entry
  724. if entry.entry_dn == dn:
  725. error_message = 'dn already present in cursor'
  726. if log_enabled(ERROR):
  727. log(ERROR, '%s for <%s>', error_message, self)
  728. raise LDAPCursorError(error_message)
  729. rdns = safe_rdn(dn, decompose=True)
  730. entry = self.entry_class(dn, self) # defines a new empty Entry
  731. for attr in entry.entry_mandatory_attributes: # defines all mandatory attributes as virtual
  732. entry._state.attributes[attr] = self.attribute_class(entry._state.definition[attr], entry, self)
  733. entry.__dict__[attr] = entry._state.attributes[attr]
  734. entry.objectclass.set(self.definition._object_class)
  735. for rdn in rdns: # adds virtual attributes from rdns in entry name (should be more than one with + syntax)
  736. if rdn[0] in entry._state.definition._attributes:
  737. rdn_name = entry._state.definition._attributes[rdn[0]].name # normalize case folding
  738. if rdn_name not in entry._state.attributes:
  739. entry._state.attributes[rdn_name] = self.attribute_class(entry._state.definition[rdn_name], entry, self)
  740. entry.__dict__[rdn_name] = entry._state.attributes[rdn_name]
  741. entry.__dict__[rdn_name].set(rdn[1])
  742. else:
  743. error_message = 'rdn type \'%s\' not in object class definition' % rdn[0]
  744. if log_enabled(ERROR):
  745. log(ERROR, '%s for <%s>', error_message, self)
  746. raise LDAPCursorError(error_message)
  747. entry._state.set_status(STATUS_VIRTUAL) # set intial status
  748. entry._state.set_status(STATUS_PENDING_CHANGES) # tries to change status to PENDING_CHANGES. If mandatory attributes are missing status is reverted to MANDATORY_MISSING
  749. self.entries.append(entry)
  750. return entry
  751. def refresh_entry(self, entry, tries=4, seconds=2):
  752. conf_operational_attribute_prefix = get_config_parameter('ABSTRACTION_OPERATIONAL_ATTRIBUTE_PREFIX')
  753. self._do_not_reset = True
  754. attr_list = []
  755. if log_enabled(PROTOCOL):
  756. log(PROTOCOL, 'refreshing entry <%s> for <%s>', entry, self)
  757. for attr in entry._state.attributes: # check friendly attribute name in AttrDef, do not check operational attributes
  758. if attr.lower().startswith(conf_operational_attribute_prefix.lower()):
  759. continue
  760. if entry._state.definition[attr].name:
  761. attr_list.append(entry._state.definition[attr].name)
  762. else:
  763. attr_list.append(entry._state.definition[attr].key)
  764. temp_entry = self._refresh_object(entry.entry_dn, attr_list, tries, seconds=seconds) # if any attributes is added adds only to the entry not to the definition
  765. self._do_not_reset = False
  766. if temp_entry:
  767. temp_entry._state.origin = entry._state.origin
  768. entry.__dict__.clear()
  769. entry.__dict__['_state'] = temp_entry._state
  770. for attr in entry._state.attributes: # returns the attribute key
  771. entry.__dict__[attr] = entry._state.attributes[attr]
  772. for attr in entry.entry_attributes: # if any attribute of the class was deleted makes it virtual
  773. if attr not in entry._state.attributes and attr in entry.entry_definition._attributes:
  774. entry._state.attributes[attr] = WritableAttribute(entry.entry_definition[attr], entry, self)
  775. entry.__dict__[attr] = entry._state.attributes[attr]
  776. entry._state.set_status(entry._state._initial_status)
  777. return True
  778. return False