Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 

907 wiersze
42 KiB

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