codekingpro/portable-devtools
114k
1"""2"""3 4# Created on 2014.01.065#6# Author: Giovanni Cannata7#8# Copyright 2014 - 2020 Giovanni Cannata9#10# This file is part of ldap3.11#12# ldap3 is free software: you can redistribute it and/or modify13# it under the terms of the GNU Lesser General Public License as published14# by the Free Software Foundation, either version 3 of the License, or15# (at your option) any later version.16#17# ldap3 is distributed in the hope that it will be useful,18# but WITHOUT ANY WARRANTY; without even the implied warranty of19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the20# GNU Lesser General Public License for more details.21#22# You should have received a copy of the GNU Lesser General Public License23# along with ldap3 in the COPYING and COPYING.LESSER files.24# If not, see <http://www.gnu.org/licenses/>.25from collections import namedtuple26from copy import deepcopy27from datetime import datetime28from os import linesep29from time import sleep30 31from . import STATUS_VIRTUAL, STATUS_READ, STATUS_WRITABLE32from .. import SUBTREE, LEVEL, DEREF_ALWAYS, DEREF_NEVER, BASE, SEQUENCE_TYPES, STRING_TYPES, get_config_parameter33from ..abstract import STATUS_PENDING_CHANGES34from .attribute import Attribute, OperationalAttribute, WritableAttribute35from .attrDef import AttrDef36from .objectDef import ObjectDef37from .entry import Entry, WritableEntry38from ..core.exceptions import LDAPCursorError, LDAPObjectDereferenceError39from ..core.results import RESULT_SUCCESS40from ..utils.ciDict import CaseInsensitiveWithAliasDict41from ..utils.dn import safe_dn, safe_rdn42from ..utils.conv import to_raw43from ..utils.config import get_config_parameter44from ..utils.log import log, log_enabled, ERROR, BASIC, PROTOCOL, EXTENDED45from ..protocol.oid import ATTRIBUTE_DIRECTORY_OPERATION, ATTRIBUTE_DISTRIBUTED_OPERATION, ATTRIBUTE_DSA_OPERATION, CLASS_AUXILIARY46 47Operation = namedtuple('Operation', ('request', 'result', 'response'))48 49 50def _ret_search_value(value):51 return value[0] + '=' + value[1:] if value[0] in '<>~' and value[1] != '=' else value52 53 54def _create_query_dict(query_text):55 """56 Create a dictionary with query key:value definitions57 query_text is a comma delimited key:value sequence58 """59 query_dict = dict()60 if query_text:61 for arg_value_str in query_text.split(','):62 if ':' in arg_value_str:63 arg_value_list = arg_value_str.split(':')64 query_dict[arg_value_list[0].strip()] = arg_value_list[1].strip()65 66 return query_dict67 68 69class Cursor(object):70 # entry_class and attribute_class define the type of entry and attribute used by the cursor71 # entry_initial_status defines the initial status of a entry72 # entry_class = Entry, must be defined in subclasses73 # attribute_class = Attribute, must be defined in subclasses74 # entry_initial_status = STATUS, must be defined in subclasses75 76 def __init__(self, connection, object_def, get_operational_attributes=False, attributes=None, controls=None, auxiliary_class=None):77 conf_attributes_excluded_from_object_def = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_OBJECT_DEF')]78 self.connection = connection79 self.get_operational_attributes = get_operational_attributes80 if connection._deferred_bind or connection._deferred_open: # probably a lazy connection, tries to bind81 connection._fire_deferred()82 83 if isinstance(object_def, (STRING_TYPES, SEQUENCE_TYPES)):84 if connection.closed: # try to open connection if closed to read schema85 connection.bind()86 object_def = ObjectDef(object_def, connection.server.schema, auxiliary_class=auxiliary_class)87 self.definition = object_def88 if attributes: # checks if requested attributes are defined in ObjectDef89 not_defined_attributes = []90 if isinstance(attributes, STRING_TYPES):91 attributes = [attributes]92 93 for attribute in attributes:94 if attribute not in self.definition._attributes and attribute.lower() not in conf_attributes_excluded_from_object_def:95 not_defined_attributes.append(attribute)96 97 if not_defined_attributes:98 error_message = 'Attributes \'%s\' non in definition' % ', '.join(not_defined_attributes)99 if log_enabled(ERROR):100 log(ERROR, '%s for <%s>', error_message, self)101 raise LDAPCursorError(error_message)102 103 self.attributes = set(attributes) if attributes else set([attr.name for attr in self.definition])104 self.controls = controls105 self.execution_time = None106 self.entries = []107 self.schema = self.connection.server.schema108 self._do_not_reset = False # used for refreshing entry in entry_refresh() without removing all entries from the Cursor109 self._operation_history = list() # a list storing all the requests, results and responses for the last cursor operation110 111 def __repr__(self):112 r = 'CURSOR : ' + self.__class__.__name__ + linesep113 r += 'CONN : ' + str(self.connection) + linesep114 r += 'DEFS : ' + ', '.join(self.definition._object_class)115 if self.definition._auxiliary_class:116 r += ' [AUX: ' + ', '.join(self.definition._auxiliary_class) + ']'117 r += linesep118 # for attr_def in sorted(self.definition):119 # r += (attr_def.key if attr_def.key == attr_def.name else (attr_def.key + ' <' + attr_def.name + '>')) + ', '120 # if r[-2] == ',':121 # r = r[:-2]122 # r += ']' + linesep123 if hasattr(self, 'attributes'):124 r += 'ATTRS : ' + repr(sorted(self.attributes)) + (' [OPERATIONAL]' if self.get_operational_attributes else '') + linesep125 if isinstance(self, Reader):126 if hasattr(self, 'base'):127 r += 'BASE : ' + repr(self.base) + (' [SUB]' if self.sub_tree else ' [LEVEL]') + linesep128 if hasattr(self, '_query') and self._query:129 r += 'QUERY : ' + repr(self._query) + ('' if '(' in self._query else (' [AND]' if self.components_in_and else ' [OR]')) + linesep130 if hasattr(self, 'validated_query') and self.validated_query:131 r += 'PARSED : ' + repr(self.validated_query) + ('' if '(' in self._query else (' [AND]' if self.components_in_and else ' [OR]')) + linesep132 if hasattr(self, 'query_filter') and self.query_filter:133 r += 'FILTER : ' + repr(self.query_filter) + linesep134 135 if hasattr(self, 'execution_time') and self.execution_time:136 r += 'ENTRIES: ' + str(len(self.entries))137 r += ' [executed at: ' + str(self.execution_time.isoformat()) + ']' + linesep138 139 if self.failed:140 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]) + ']'141 142 return r143 144 def __str__(self):145 return self.__repr__()146 147 def __iter__(self):148 return self.entries.__iter__()149 150 def __getitem__(self, item):151 """Return indexed item, if index is not found then try to sequentially search in DN of entries.152 If only one entry is found return it else raise a KeyError exception. The exception message153 includes the number of entries that matches, if less than 10 entries match then show the DNs154 in the exception message.155 """156 try:157 return self.entries[item]158 except TypeError:159 pass160 161 if isinstance(item, STRING_TYPES):162 found = self.match_dn(item)163 164 if len(found) == 1:165 return found[0]166 elif len(found) > 1:167 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]) + ']'))168 if log_enabled(ERROR):169 log(ERROR, '%s for <%s>', error_message, self)170 raise KeyError(error_message)171 172 error_message = 'no entry found'173 if log_enabled(ERROR):174 log(ERROR, '%s for <%s>', error_message, self)175 raise KeyError(error_message)176 177 def __len__(self):178 return len(self.entries)179 180 if str is not bytes: # Python 3181 def __bool__(self): # needed to make the cursor appears as existing in "if cursor:" even if there are no entries182 return True183 else: # Python 2184 def __nonzero__(self):185 return True186 187 def _get_attributes(self, response, attr_defs, entry):188 """Assign the result of the LDAP query to the Entry object dictionary.189 190 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.191 192 Returns the default value for missing attributes.193 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.194 195 """196 conf_operational_attribute_prefix = get_config_parameter('ABSTRACTION_OPERATIONAL_ATTRIBUTE_PREFIX')197 conf_attributes_excluded_from_object_def = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_OBJECT_DEF')]198 attributes = CaseInsensitiveWithAliasDict()199 used_attribute_names = set()200 for attr in attr_defs:201 attr_def = attr_defs[attr]202 attribute_name = None203 for attr_name in response['attributes']:204 if attr_def.name.lower() == attr_name.lower():205 attribute_name = attr_name206 break207 208 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 default209 attribute = self.attribute_class(attr_def, entry, self)210 attribute.response = response211 attribute.raw_values = response['raw_attributes'][attribute_name] if attribute_name else None212 if attr_def.post_query and attr_def.name in response['attributes'] and response['raw_attributes'] != list():213 attribute.values = attr_def.post_query(attr_def.key, response['attributes'][attribute_name])214 else:215 if attr_def.default is NotImplemented or (attribute_name and response['raw_attributes'][attribute_name] != list()):216 attribute.values = response['attributes'][attribute_name]217 else:218 attribute.values = attr_def.default if isinstance(attr_def.default, SEQUENCE_TYPES) else [attr_def.default]219 if not isinstance(attribute.values, list): # force attribute values to list (if attribute is single-valued)220 attribute.values = [attribute.values]221 if attr_def.dereference_dn: # try to get object referenced in value222 if attribute.values:223 temp_reader = Reader(self.connection, attr_def.dereference_dn, base='', get_operational_attributes=self.get_operational_attributes, controls=self.controls)224 temp_values = []225 for element in attribute.values:226 if entry.entry_dn != element:227 temp_values.append(temp_reader.search_object(element))228 else:229 error_message = 'object %s is referencing itself in the \'%s\' attribute' % (entry.entry_dn, attribute.definition.name)230 if log_enabled(ERROR):231 log(ERROR, '%s for <%s>', error_message, self)232 raise LDAPObjectDereferenceError(error_message)233 del temp_reader # remove the temporary Reader234 attribute.values = temp_values235 attributes[attribute.key] = attribute236 if attribute.other_names:237 attributes.set_alias(attribute.key, attribute.other_names)238 if attr_def.other_names:239 attributes.set_alias(attribute.key, attr_def.other_names)240 used_attribute_names.add(attribute_name)241 242 if self.attributes:243 used_attribute_names.update(self.attributes)244 245 for attribute_name in response['attributes']:246 if attribute_name not in used_attribute_names:247 operational_attribute = False248 # check if the type is an operational attribute249 if attribute_name in self.schema.attribute_types:250 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]:251 operational_attribute = True252 else:253 operational_attribute = True254 if not operational_attribute and attribute_name not in attr_defs and attribute_name.lower() not in conf_attributes_excluded_from_object_def:255 error_message = 'attribute \'%s\' not in object class \'%s\' for entry %s' % (attribute_name, ', '.join(entry.entry_definition._object_class), entry.entry_dn)256 if log_enabled(ERROR):257 log(ERROR, '%s for <%s>', error_message, self)258 raise LDAPCursorError(error_message)259 attribute = OperationalAttribute(AttrDef(conf_operational_attribute_prefix + attribute_name), entry, self)260 attribute.raw_values = response['raw_attributes'][attribute_name]261 attribute.values = response['attributes'][attribute_name] if isinstance(response['attributes'][attribute_name], SEQUENCE_TYPES) else [response['attributes'][attribute_name]]262 if (conf_operational_attribute_prefix + attribute_name) not in attributes:263 attributes[conf_operational_attribute_prefix + attribute_name] = attribute264 265 return attributes266 267 def match_dn(self, dn):268 """Return entries with text in DN"""269 matched = []270 for entry in self.entries:271 if dn.lower() in entry.entry_dn.lower():272 matched.append(entry)273 return matched274 275 def match(self, attributes, value):276 """Return entries with text in one of the specified attributes"""277 matched = []278 if not isinstance(attributes, SEQUENCE_TYPES):279 attributes = [attributes]280 281 for entry in self.entries:282 found = False283 for attribute in attributes:284 if attribute in entry:285 for attr_value in entry[attribute].values:286 if hasattr(attr_value, 'lower') and hasattr(value, 'lower') and value.lower() in attr_value.lower():287 found = True288 elif value == attr_value:289 found = True290 if found:291 matched.append(entry)292 break293 if found:294 break295 # checks raw values, tries to convert value to byte296 raw_value = to_raw(value)297 if isinstance(raw_value, (bytes, bytearray)):298 for attr_value in entry[attribute].raw_values:299 if hasattr(attr_value, 'lower') and hasattr(raw_value, 'lower') and raw_value.lower() in attr_value.lower():300 found = True301 elif raw_value == attr_value:302 found = True303 if found:304 matched.append(entry)305 break306 if found:307 break308 return matched309 310 def _create_entry(self, response):311 if not response['type'] == 'searchResEntry':312 return None313 314 entry = self.entry_class(response['dn'], self) # define an Entry (writable or readonly), as specified in the cursor definition315 entry._state.attributes = self._get_attributes(response, self.definition._attributes, entry)316 entry._state.raw_attributes = deepcopy(response['raw_attributes'])317 318 entry._state.response = response319 entry._state.read_time = datetime.now()320 entry._state.set_status(self.entry_initial_status)321 for attr in entry: # returns the whole attribute object322 entry.__dict__[attr.key] = attr323 324 return entry325 326 def _execute_query(self, query_scope, attributes):327 if not self.connection:328 error_message = 'no connection established'329 if log_enabled(ERROR):330 log(ERROR, '%s for <%s>', error_message, self)331 raise LDAPCursorError(error_message)332 old_query_filter = None333 if query_scope == BASE: # requesting a single object so an always-valid filter is set334 if hasattr(self, 'query_filter'): # only Reader has a query filter335 old_query_filter = self.query_filter336 self.query_filter = '(objectclass=*)'337 else:338 self._create_query_filter()339 if log_enabled(PROTOCOL):340 log(PROTOCOL, 'executing query - base: %s - filter: %s - scope: %s for <%s>', self.base, self.query_filter, query_scope, self)341 with self.connection:342 result = self.connection.search(search_base=self.base,343 search_filter=self.query_filter,344 search_scope=query_scope,345 dereference_aliases=self.dereference_aliases,346 attributes=attributes if attributes else list(self.attributes),347 get_operational_attributes=self.get_operational_attributes,348 controls=self.controls)349 if not self.connection.strategy.sync:350 response, result, request = self.connection.get_response(result, get_request=True)351 else:352 if self.connection.strategy.thread_safe:353 _, result, response, _ = result354 else:355 response = self.connection.response356 result = self.connection.result357 request = self.connection.request358 359 self._store_operation_in_history(request, result, response)360 361 if self._do_not_reset: # trick to not remove entries when using _refresh()362 return self._create_entry(response[0])363 364 self.entries = []365 for r in response:366 entry = self._create_entry(r)367 if entry is not None:368 self.entries.append(entry)369 if 'objectClass' in entry:370 for object_class in entry.objectClass:371 if self.schema and self.schema.object_classes[object_class].kind == CLASS_AUXILIARY and object_class not in self.definition._auxiliary_class:372 # add auxiliary class to object definition373 self.definition._auxiliary_class.append(object_class)374 self.definition._populate_attr_defs(object_class)375 self.execution_time = datetime.now()376 377 if old_query_filter: # requesting a single object so an always-valid filter is set378 self.query_filter = old_query_filter379 380 def remove(self, entry):381 if log_enabled(PROTOCOL):382 log(PROTOCOL, 'removing entry <%s> in <%s>', entry, self)383 self.entries.remove(entry)384 385 def _reset_history(self):386 self._operation_history = list()387 388 def _store_operation_in_history(self, request, result, response):389 self._operation_history.append(Operation(request, result, response))390 391 @property392 def operations(self):393 return self._operation_history394 395 @property396 def errors(self):397 return [error for error in self._operation_history if error.result['result'] != RESULT_SUCCESS]398 399 @property400 def failed(self):401 if hasattr(self, '_operation_history'):402 return any([error.result['result'] != RESULT_SUCCESS for error in self._operation_history])403 404 405class Reader(Cursor):406 """Reader object to perform searches:407 408 :param connection: the LDAP connection object to use409 :type connection: LDAPConnection410 :param object_def: the ObjectDef of the LDAP object returned411 :type object_def: ObjectDef412 :param query: the simplified query (will be transformed in an LDAP filter)413 :type query: str414 :param base: starting base of the search415 :type base: str416 :param components_in_and: specify if assertions in the query must all be satisfied or not (AND/OR)417 :type components_in_and: bool418 :param sub_tree: specify if the search must be performed ad Single Level (False) or Whole SubTree (True)419 :type sub_tree: bool420 :param get_operational_attributes: specify if operational attributes are returned or not421 :type get_operational_attributes: bool422 :param controls: controls to be used in search423 :type controls: tuple424 425 """426 entry_class = Entry # entries are read_only427 attribute_class = Attribute # attributes are read_only428 entry_initial_status = STATUS_READ429 430 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):431 Cursor.__init__(self, connection, object_def, get_operational_attributes, attributes, controls, auxiliary_class)432 self._components_in_and = components_in_and433 self.sub_tree = sub_tree434 self._query = query435 self.base = base436 self.dereference_aliases = DEREF_ALWAYS437 self.validated_query = None438 self._query_dict = dict()439 self._validated_query_dict = dict()440 self.query_filter = None441 self.reset()442 443 if log_enabled(BASIC):444 log(BASIC, 'instantiated Reader Cursor: <%r>', self)445 446 @property447 def query(self):448 return self._query449 450 @query.setter451 def query(self, value):452 self._query = value453 self.reset()454 455 @property456 def components_in_and(self):457 return self._components_in_and458 459 @components_in_and.setter460 def components_in_and(self, value):461 self._components_in_and = value462 self.reset()463 464 def clear(self):465 """Clear the Reader search parameters466 467 """468 self.dereference_aliases = DEREF_ALWAYS469 self._reset_history()470 471 def reset(self):472 """Clear all the Reader parameters473 474 """475 self.clear()476 self.validated_query = None477 self._query_dict = dict()478 self._validated_query_dict = dict()479 self.execution_time = None480 self.query_filter = None481 self.entries = []482 self._create_query_filter()483 484 def _validate_query(self):485 """Processes the text query and verifies that the requested friendly names are in the Reader dictionary486 If the AttrDef has a 'validate' property the callable is executed and if it returns False an Exception is raised487 488 """489 if not self._query_dict:490 self._query_dict = _create_query_dict(self._query)491 492 query = ''493 for d in sorted(self._query_dict):494 attr = d[1:] if d[0] in '&|' else d495 for attr_def in self.definition:496 if ''.join(attr.split()).lower() == attr_def.key.lower():497 attr = attr_def.key498 break499 if attr in self.definition:500 vals = sorted(self._query_dict[d].split(';'))501 502 query += (d[0] + attr if d[0] in '&|' else attr) + ': '503 for val in vals:504 val = val.strip()505 val_not = True if val[0] == '!' else False506 val_search_operator = '=' # default507 if val_not:508 if val[1:].lstrip()[0] not in '=<>~':509 value = val[1:].lstrip()510 else:511 val_search_operator = val[1:].lstrip()[0]512 value = val[1:].lstrip()[1:]513 else:514 if val[0] not in '=<>~':515 value = val.lstrip()516 else:517 val_search_operator = val[0]518 value = val[1:].lstrip()519 520 if self.definition[attr].validate:521 validated = self.definition[attr].validate(value) # returns True, False or a value to substitute to the actual values522 if validated is False:523 error_message = 'validation failed for attribute %s and value %s' % (d, val)524 if log_enabled(ERROR):525 log(ERROR, '%s for <%s>', error_message, self)526 raise LDAPCursorError(error_message)527 elif validated is not True: # a valid LDAP value equivalent to the actual values528 value = validated529 if val_not:530 query += '!' + val_search_operator + str(value)531 else:532 query += val_search_operator + str(value)533 534 query += ';'535 query = query[:-1] + ', '536 else:537 error_message = 'attribute \'%s\' not in definition' % attr538 if log_enabled(ERROR):539 log(ERROR, '%s for <%s>', error_message, self)540 raise LDAPCursorError(error_message)541 self.validated_query = query[:-2]542 self._validated_query_dict = _create_query_dict(self.validated_query)543 544 def _create_query_filter(self):545 """Converts the query dictionary to the filter text"""546 self.query_filter = ''547 548 if self.definition._object_class:549 self.query_filter += '(&'550 if isinstance(self.definition._object_class, SEQUENCE_TYPES) and len(self.definition._object_class) == 1:551 self.query_filter += '(objectClass=' + self.definition._object_class[0] + ')'552 elif isinstance(self.definition._object_class, SEQUENCE_TYPES):553 self.query_filter += '(&'554 for object_class in self.definition._object_class:555 self.query_filter += '(objectClass=' + object_class + ')'556 self.query_filter += ')'557 else:558 error_message = 'object class must be a string or a list'559 if log_enabled(ERROR):560 log(ERROR, '%s for <%s>', error_message, self)561 raise LDAPCursorError(error_message)562 563 if self._query and self._query.startswith('(') and self._query.endswith(')'): # query is already an LDAP filter564 if 'objectclass' not in self._query.lower():565 self.query_filter += self._query + ')' # if objectclass not in filter adds from definition566 else:567 self.query_filter = self._query568 return569 elif self._query: # if a simplified filter is present570 if not self.components_in_and:571 self.query_filter += '(|'572 elif not self.definition._object_class:573 self.query_filter += '(&'574 575 self._validate_query()576 577 attr_counter = 0578 for attr in sorted(self._validated_query_dict):579 attr_counter += 1580 multi = True if ';' in self._validated_query_dict[attr] else False581 vals = sorted(self._validated_query_dict[attr].split(';'))582 attr_def = self.definition[attr[1:]] if attr[0] in '&|' else self.definition[attr]583 if attr_def.pre_query:584 modvals = []585 for val in vals:586 modvals.append(val[0] + attr_def.pre_query(attr_def.key, val[1:]))587 vals = modvals588 if multi:589 if attr[0] in '&|':590 self.query_filter += '(' + attr[0]591 else:592 self.query_filter += '(|'593 594 for val in vals:595 if val[0] == '!':596 self.query_filter += '(!(' + attr_def.name + _ret_search_value(val[1:]) + '))'597 else:598 self.query_filter += '(' + attr_def.name + _ret_search_value(val) + ')'599 if multi:600 self.query_filter += ')'601 602 if not self.components_in_and:603 self.query_filter += '))'604 else:605 self.query_filter += ')'606 607 if not self.definition._object_class and attr_counter == 1: # removes unneeded starting filter608 self.query_filter = self.query_filter[2: -1]609 610 if self.query_filter == '(|)' or self.query_filter == '(&)': # removes empty filter611 self.query_filter = ''612 else: # no query, remove unneeded leading (&613 self.query_filter = self.query_filter[2:]614 615 def search(self, attributes=None):616 """Perform the LDAP search617 618 :return: Entries found in search619 620 """621 self.clear()622 query_scope = SUBTREE if self.sub_tree else LEVEL623 if log_enabled(PROTOCOL):624 log(PROTOCOL, 'performing search in <%s>', self)625 self._execute_query(query_scope, attributes)626 627 return self.entries628 629 def search_object(self, entry_dn=None, attributes=None): # base must be a single dn630 """Perform the LDAP search operation SINGLE_OBJECT scope631 632 :return: Entry found in search633 634 """635 if log_enabled(PROTOCOL):636 log(PROTOCOL, 'performing object search in <%s>', self)637 self.clear()638 if entry_dn:639 old_base = self.base640 self.base = entry_dn641 self._execute_query(BASE, attributes)642 self.base = old_base643 else:644 self._execute_query(BASE, attributes)645 646 return self.entries[0] if len(self.entries) > 0 else None647 648 def search_level(self, attributes=None):649 """Perform the LDAP search operation with SINGLE_LEVEL scope650 651 :return: Entries found in search652 653 """654 if log_enabled(PROTOCOL):655 log(PROTOCOL, 'performing single level search in <%s>', self)656 self.clear()657 self._execute_query(LEVEL, attributes)658 659 return self.entries660 661 def search_subtree(self, attributes=None):662 """Perform the LDAP search operation WHOLE_SUBTREE scope663 664 :return: Entries found in search665 666 """667 if log_enabled(PROTOCOL):668 log(PROTOCOL, 'performing whole subtree search in <%s>', self)669 self.clear()670 self._execute_query(SUBTREE, attributes)671 672 return self.entries673 674 def _entries_generator(self, responses):675 for response in responses:676 yield self._create_entry(response)677 678 def search_paged(self, paged_size, paged_criticality=True, generator=True, attributes=None):679 """Perform a paged search, can be called as an Iterator680 681 :param attributes: optional attributes to search682 :param paged_size: number of entries returned in each search683 :type paged_size: int684 :param paged_criticality: specify if server must not execute the search if it is not capable of paging searches685 :type paged_criticality: bool686 :param generator: if True the paged searches are executed while generating the entries,687 if False all the paged searches are execute before returning the generator688 :type generator: bool689 :return: Entries found in search690 691 """692 if log_enabled(PROTOCOL):693 log(PROTOCOL, 'performing paged search in <%s> with paged size %s', self, str(paged_size))694 if not self.connection:695 error_message = 'no connection established'696 if log_enabled(ERROR):697 log(ERROR, '%s for <%s>', error_message, self)698 raise LDAPCursorError(error_message)699 700 self.clear()701 self._create_query_filter()702 self.entries = []703 self.execution_time = datetime.now()704 response = self.connection.extend.standard.paged_search(search_base=self.base,705 search_filter=self.query_filter,706 search_scope=SUBTREE if self.sub_tree else LEVEL,707 dereference_aliases=self.dereference_aliases,708 attributes=attributes if attributes else self.attributes,709 get_operational_attributes=self.get_operational_attributes,710 controls=self.controls,711 paged_size=paged_size,712 paged_criticality=paged_criticality,713 generator=generator)714 if generator:715 return self._entries_generator(response)716 else:717 return list(self._entries_generator(response))718 719 720class Writer(Cursor):721 entry_class = WritableEntry722 attribute_class = WritableAttribute723 entry_initial_status = STATUS_WRITABLE724 725 @staticmethod726 def from_cursor(cursor, connection=None, object_def=None, custom_validator=None):727 if connection is None:728 connection = cursor.connection729 if object_def is None:730 object_def = cursor.definition731 writer = Writer(connection, object_def, attributes=cursor.attributes)732 for entry in cursor.entries:733 if isinstance(cursor, Reader):734 entry.entry_writable(object_def, writer, custom_validator=custom_validator)735 elif isinstance(cursor, Writer):736 pass737 else:738 error_message = 'unknown cursor type %s' % str(type(cursor))739 if log_enabled(ERROR):740 log(ERROR, '%s', error_message)741 raise LDAPCursorError(error_message)742 writer.execution_time = cursor.execution_time743 if log_enabled(BASIC):744 log(BASIC, 'instantiated Writer Cursor <%r> from cursor <%r>', writer, cursor)745 return writer746 747 @staticmethod748 def from_response(connection, object_def, response=None):749 if response is None:750 if not connection.strategy.sync:751 error_message = 'with asynchronous strategies response must be specified'752 if log_enabled(ERROR):753 log(ERROR, '%s', error_message)754 raise LDAPCursorError(error_message)755 elif connection.response:756 response = connection.response757 else:758 error_message = 'response not present'759 if log_enabled(ERROR):760 log(ERROR, '%s', error_message)761 raise LDAPCursorError(error_message)762 writer = Writer(connection, object_def)763 764 for resp in response:765 if resp['type'] == 'searchResEntry':766 entry = writer._create_entry(resp)767 writer.entries.append(entry)768 if log_enabled(BASIC):769 log(BASIC, 'instantiated Writer Cursor <%r> from response', writer)770 return writer771 772 def __init__(self, connection, object_def, get_operational_attributes=False, attributes=None, controls=None, auxiliary_class=None):773 Cursor.__init__(self, connection, object_def, get_operational_attributes, attributes, controls, auxiliary_class)774 self.dereference_aliases = DEREF_NEVER775 776 if log_enabled(BASIC):777 log(BASIC, 'instantiated Writer Cursor: <%r>', self)778 779 def commit(self, refresh=True):780 if log_enabled(PROTOCOL):781 log(PROTOCOL, 'committed changes for <%s>', self)782 self._reset_history()783 successful = True784 for entry in self.entries:785 if not entry.entry_commit_changes(refresh=refresh, controls=self.controls, clear_history=False):786 successful = False787 788 self.execution_time = datetime.now()789 790 return successful791 792 def discard(self):793 if log_enabled(PROTOCOL):794 log(PROTOCOL, 'discarded changes for <%s>', self)795 for entry in self.entries:796 entry.entry_discard_changes()797 798 def _refresh_object(self, entry_dn, attributes=None, tries=4, seconds=2, controls=None): # base must be a single dn799 """Performs the LDAP search operation SINGLE_OBJECT scope800 801 :return: Entry found in search802 803 """804 if log_enabled(PROTOCOL):805 log(PROTOCOL, 'refreshing object <%s> for <%s>', entry_dn, self)806 if not self.connection:807 error_message = 'no connection established'808 if log_enabled(ERROR):809 log(ERROR, '%s for <%s>', error_message, self)810 raise LDAPCursorError(error_message)811 812 response = []813 with self.connection:814 counter = 0815 while counter < tries:816 result = self.connection.search(search_base=entry_dn,817 search_filter='(objectclass=*)',818 search_scope=BASE,819 dereference_aliases=DEREF_NEVER,820 attributes=attributes if attributes else self.attributes,821 get_operational_attributes=self.get_operational_attributes,822 controls=controls)823 if not self.connection.strategy.sync:824 response, result, request = self.connection.get_response(result, get_request=True)825 else:826 if self.connection.strategy.thread_safe:827 _, result, response, request = result828 else:829 response = self.connection.response830 result = self.connection.result831 request = self.connection.request832 833 if result['result'] in [RESULT_SUCCESS]:834 break835 sleep(seconds)836 counter += 1837 self._store_operation_in_history(request, result, response)838 839 if len(response) == 1:840 return self._create_entry(response[0])841 elif len(response) == 0:842 return None843 844 error_message = 'more than 1 entry returned for a single object search'845 if log_enabled(ERROR):846 log(ERROR, '%s for <%s>', error_message, self)847 raise LDAPCursorError(error_message)848 849 def new(self, dn):850 if log_enabled(BASIC):851 log(BASIC, 'creating new entry <%s> for <%s>', dn, self)852 dn = safe_dn(dn)853 for entry in self.entries: # checks if dn is already used in an cursor entry854 if entry.entry_dn == dn:855 error_message = 'dn already present in cursor'856 if log_enabled(ERROR):857 log(ERROR, '%s for <%s>', error_message, self)858 raise LDAPCursorError(error_message)859 rdns = safe_rdn(dn, decompose=True)860 entry = self.entry_class(dn, self) # defines a new empty Entry861 for attr in entry.entry_mandatory_attributes: # defines all mandatory attributes as virtual862 entry._state.attributes[attr] = self.attribute_class(entry._state.definition[attr], entry, self)863 entry.__dict__[attr] = entry._state.attributes[attr]864 entry.objectclass.set(self.definition._object_class)865 for rdn in rdns: # adds virtual attributes from rdns in entry name (should be more than one with + syntax)866 if rdn[0] in entry._state.definition._attributes:867 rdn_name = entry._state.definition._attributes[rdn[0]].name # normalize case folding868 if rdn_name not in entry._state.attributes:869 entry._state.attributes[rdn_name] = self.attribute_class(entry._state.definition[rdn_name], entry, self)870 entry.__dict__[rdn_name] = entry._state.attributes[rdn_name]871 entry.__dict__[rdn_name].set(rdn[1])872 else:873 error_message = 'rdn type \'%s\' not in object class definition' % rdn[0]874 if log_enabled(ERROR):875 log(ERROR, '%s for <%s>', error_message, self)876 raise LDAPCursorError(error_message)877 entry._state.set_status(STATUS_VIRTUAL) # set intial status878 entry._state.set_status(STATUS_PENDING_CHANGES) # tries to change status to PENDING_CHANGES. If mandatory attributes are missing status is reverted to MANDATORY_MISSING879 self.entries.append(entry)880 return entry881 882 def refresh_entry(self, entry, tries=4, seconds=2):883 conf_operational_attribute_prefix = get_config_parameter('ABSTRACTION_OPERATIONAL_ATTRIBUTE_PREFIX')884 885 self._do_not_reset = True886 attr_list = []887 if log_enabled(PROTOCOL):888 log(PROTOCOL, 'refreshing entry <%s> for <%s>', entry, self)889 for attr in entry._state.attributes: # check friendly attribute name in AttrDef, do not check operational attributes890 if attr.lower().startswith(conf_operational_attribute_prefix.lower()):891 continue892 if entry._state.definition[attr].name:893 attr_list.append(entry._state.definition[attr].name)894 else:895 attr_list.append(entry._state.definition[attr].key)896 897 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 definition898 self._do_not_reset = False899 if temp_entry:900 temp_entry._state.origin = entry._state.origin901 entry.__dict__.clear()902 entry.__dict__['_state'] = temp_entry._state903 for attr in entry._state.attributes: # returns the attribute key904 entry.__dict__[attr] = entry._state.attributes[attr]905 906 for attr in entry.entry_attributes: # if any attribute of the class was deleted makes it virtual907 if attr not in entry._state.attributes and attr in entry.entry_definition._attributes:908 entry._state.attributes[attr] = WritableAttribute(entry.entry_definition[attr], entry, self)909 entry.__dict__[attr] = entry._state.attributes[attr]910 entry._state.set_status(entry._state._initial_status)911 return True912 return False913 