""" """ # Created on 2016.04.30 # # Author: Giovanni Cannata # # Copyright 2016 - 2018 Giovanni Cannata # # This file is part of ldap3. # # ldap3 is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # ldap3 is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with ldap3 in the COPYING and COPYING.LESSER files. # If not, see . import json import re from threading import Lock from random import SystemRandom from pyasn1.type.univ import OctetString from .. import SEQUENCE_TYPES, ALL_ATTRIBUTES from ..operation.bind import bind_request_to_dict from ..operation.delete import delete_request_to_dict from ..operation.add import add_request_to_dict from ..operation.compare import compare_request_to_dict from ..operation.modifyDn import modify_dn_request_to_dict from ..operation.modify import modify_request_to_dict from ..operation.extended import extended_request_to_dict from ..operation.search import search_request_to_dict, parse_filter, ROOT, AND, OR, NOT, MATCH_APPROX, \ MATCH_GREATER_OR_EQUAL, MATCH_LESS_OR_EQUAL, MATCH_EXTENSIBLE, MATCH_PRESENT,\ MATCH_SUBSTRING, MATCH_EQUAL from ..utils.conv import json_hook, to_unicode, to_raw from ..core.exceptions import LDAPDefinitionError, LDAPPasswordIsMandatoryError, LDAPInvalidValueError, LDAPSocketOpenError from ..core.results import RESULT_SUCCESS, RESULT_OPERATIONS_ERROR, RESULT_UNAVAILABLE_CRITICAL_EXTENSION, \ RESULT_INVALID_CREDENTIALS, RESULT_NO_SUCH_OBJECT, RESULT_ENTRY_ALREADY_EXISTS, RESULT_COMPARE_TRUE, \ RESULT_COMPARE_FALSE, RESULT_NO_SUCH_ATTRIBUTE, RESULT_UNWILLING_TO_PERFORM from ..utils.ciDict import CaseInsensitiveDict from ..utils.dn import to_dn, safe_dn, safe_rdn from ..protocol.sasl.sasl import validate_simple_password from ..protocol.formatters.standard import find_attribute_validator, format_attribute_values from ..protocol.rfc2696 import paged_search_control from ..utils.log import log, log_enabled, ERROR, BASIC from ..utils.asn1 import encode from ..strategy.base import BaseStrategy # needed for decode_control() method from ..protocol.rfc4511 import LDAPMessage, ProtocolOp, MessageID from ..protocol.convert import build_controls_list # LDAPResult ::= SEQUENCE { # resultCode ENUMERATED { # success (0), # operationsError (1), # protocolError (2), # timeLimitExceeded (3), # sizeLimitExceeded (4), # compareFalse (5), # compareTrue (6), # authMethodNotSupported (7), # strongerAuthRequired (8), # -- 9 reserved -- # referral (10), # adminLimitExceeded (11), # unavailableCriticalExtension (12), # confidentialityRequired (13), # saslBindInProgress (14), # noSuchAttribute (16), # undefinedAttributeType (17), # inappropriateMatching (18), # constraintViolation (19), # attributeOrValueExists (20), # invalidAttributeSyntax (21), # -- 22-31 unused -- # noSuchObject (32), # aliasProblem (33), # invalidDNSyntax (34), # -- 35 reserved for undefined isLeaf -- # aliasDereferencingProblem (36), # -- 37-47 unused -- # inappropriateAuthentication (48), # invalidCredentials (49), # insufficientAccessRights (50), # busy (51), # unavailable (52), # unwillingToPerform (53), # loopDetect (54), # -- 55-63 unused -- # namingViolation (64), # objectClassViolation (65), # notAllowedOnNonLeaf (66), # notAllowedOnRDN (67), # entryAlreadyExists (68), # objectClassModsProhibited (69), # -- 70 reserved for CLDAP -- # affectsMultipleDSAs (71), # -- 72-79 unused -- # other (80), # ... }, # matchedDN LDAPDN, # diagnosticMessage LDAPString, # referral [3] Referral OPTIONAL } # noinspection PyProtectedMember,PyUnresolvedReferences SEARCH_CONTROLS = ['1.2.840.113556.1.4.319' # simple paged search [RFC 2696] ] SERVER_ENCODING = 'utf-8' def random_cookie(): return to_raw(SystemRandom().random())[-6:] class PagedSearchSet(object): def __init__(self, response, size, criticality): self.size = size self.response = response self.cookie = None self.sent = 0 self.done = False def next(self, size=None): if size: self.size=size message = '' response = self.response[self.sent: self.sent + self.size] self.sent += self.size if self.sent > len(self.response): self.done = True self.cookie = '' else: self.cookie = random_cookie() response_control = paged_search_control(False, len(self.response), self.cookie) result = {'resultCode': RESULT_SUCCESS, 'matchedDN': '', 'diagnosticMessage': to_unicode(message, SERVER_ENCODING), 'referral': None, 'controls': [BaseStrategy.decode_control(response_control)] } return response, result class MockBaseStrategy(object): """ Base class for connection strategy """ def __init__(self): if not hasattr(self.connection.server, 'dit'): # create entries dict if not already present self.connection.server.dit = CaseInsensitiveDict() self.entries = self.connection.server.dit # for simpler reference self.no_real_dsa = True self.bound = None self.custom_validators = None self.operational_attributes = ['entryDN'] self.add_entry('cn=schema', [], validate=False) # add default entry for schema self._paged_sets = [] # list of paged search in progress if log_enabled(BASIC): log(BASIC, 'instantiated <%s>: <%s>', self.__class__.__name__, self) def _start_listen(self): self.connection.listening = True self.connection.closed = False if self.connection.usage: self.connection._usage.open_sockets += 1 def _stop_listen(self): self.connection.listening = False self.connection.closed = True if self.connection.usage: self.connection._usage.closed_sockets += 1 def _prepare_value(self, attribute_type, value, validate=True): """ Prepare a value for being stored in the mock DIT :param value: object to store :return: raw value to store in the DIT """ if validate: # if loading from json dump do not validate values: validator = find_attribute_validator(self.connection.server.schema, attribute_type, self.custom_validators) validated = validator(value) if validated is False: raise LDAPInvalidValueError('value non valid for attribute \'%s\'' % attribute_type) elif validated is not True: # a valid LDAP value equivalent to the actual value value = validated raw_value = to_raw(value) if not isinstance(raw_value, bytes): raise LDAPInvalidValueError('added values must be bytes if no offline schema is provided in Mock strategies') return raw_value def _update_attribute(self, dn, attribute_type, value): pass def add_entry(self, dn, attributes, validate=True): with self.connection.server.dit_lock: escaped_dn = safe_dn(dn) if escaped_dn not in self.connection.server.dit: new_entry = CaseInsensitiveDict() for attribute in attributes: if attribute in self.operational_attributes: # no restore of operational attributes, should be computed at runtime continue if not isinstance(attributes[attribute], SEQUENCE_TYPES): # entry attributes are always lists of bytes values attributes[attribute] = [attributes[attribute]] if self.connection.server.schema and self.connection.server.schema.attribute_types[attribute].single_value and len(attributes[attribute]) > 1: # multiple values in single-valued attribute return False if attribute.lower() == 'objectclass' and self.connection.server.schema: # builds the objectClass hierarchy only if schema is present class_set = set() for object_class in attributes['objectClass']: if self.connection.server.schema.object_classes and object_class not in self.connection.server.schema.object_classes: return False # walkups the class hierarchy and buils a set of all classes in it class_set.add(object_class) class_set_size = 0 while class_set_size != len(class_set): new_classes = set() class_set_size = len(class_set) for class_name in class_set: if self.connection.server.schema.object_classes[class_name].superior: new_classes.update(self.connection.server.schema.object_classes[class_name].superior) class_set.update(new_classes) new_entry['objectClass'] = [to_raw(value) for value in class_set] else: new_entry[attribute] = [self._prepare_value(attribute, value, validate) for value in attributes[attribute]] for rdn in safe_rdn(escaped_dn, decompose=True): # adds rdns to entry attributes if rdn[0] not in new_entry: # if rdn attribute is missing adds attribute and its value new_entry[rdn[0]] = [to_raw(rdn[1])] else: raw_rdn = to_raw(rdn[1]) if raw_rdn not in new_entry[rdn[0]]: # add rdn value if rdn attribute is present but value is missing new_entry[rdn[0]].append(raw_rdn) new_entry['entryDN'] = [to_raw(escaped_dn)] self.connection.server.dit[escaped_dn] = new_entry return True return False def remove_entry(self, dn): with self.connection.server.dit_lock: escaped_dn = safe_dn(dn) if escaped_dn in self.connection.server.dit: del self.connection.server.dit[escaped_dn] return True return False def entries_from_json(self, json_entry_file): target = open(json_entry_file, 'r') definition = json.load(target, object_hook=json_hook) if 'entries' not in definition: self.connection.last_error = 'invalid JSON definition, missing "entries" section' if log_enabled(ERROR): log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) raise LDAPDefinitionError(self.connection.last_error) if not self.connection.server.dit: self.connection.server.dit = CaseInsensitiveDict() for entry in definition['entries']: if 'raw' not in entry: self.connection.last_error = 'invalid JSON definition, missing "raw" section' if log_enabled(ERROR): log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) raise LDAPDefinitionError(self.connection.last_error) if 'dn' not in entry: self.connection.last_error = 'invalid JSON definition, missing "dn" section' if log_enabled(ERROR): log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) raise LDAPDefinitionError(self.connection.last_error) self.add_entry(entry['dn'], entry['raw'], validate=False) target.close() def mock_bind(self, request_message, controls): # BindRequest ::= [APPLICATION 0] SEQUENCE { # version INTEGER (1 .. 127), # name LDAPDN, # authentication AuthenticationChoice } # # BindResponse ::= [APPLICATION 1] SEQUENCE { # COMPONENTS OF LDAPResult, # serverSaslCreds [7] OCTET STRING OPTIONAL } # # request: version, name, authentication # response: LDAPResult + serverSaslCreds request = bind_request_to_dict(request_message) identity = request['name'] if 'simple' in request['authentication']: try: password = validate_simple_password(request['authentication']['simple']) except LDAPPasswordIsMandatoryError: password = '' identity = '' else: self.connection.last_error = 'only Simple Bind allowed in Mock strategy' if log_enabled(ERROR): log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) raise LDAPDefinitionError(self.connection.last_error) # checks userPassword for password. userPassword must be a text string or a list of text strings if identity in self.connection.server.dit: if 'userPassword' in self.connection.server.dit[identity]: # if self.connection.server.dit[identity]['userPassword'] == password or password in self.connection.server.dit[identity]['userPassword']: if self.equal(identity, 'userPassword', password): result_code = RESULT_SUCCESS message = '' self.bound = identity else: result_code = RESULT_INVALID_CREDENTIALS message = 'invalid credentials' else: # no user found, returns invalidCredentials result_code = RESULT_INVALID_CREDENTIALS message = 'missing userPassword attribute' elif identity == '': result_code = RESULT_SUCCESS message = '' self.bound = identity else: result_code = RESULT_INVALID_CREDENTIALS message = 'missing object' return {'resultCode': result_code, 'matchedDN': '', 'diagnosticMessage': to_unicode(message, SERVER_ENCODING), 'referral': None, 'serverSaslCreds': None } def mock_delete(self, request_message, controls): # DelRequest ::= [APPLICATION 10] LDAPDN # # DelResponse ::= [APPLICATION 11] LDAPResult # # request: entry # response: LDAPResult request = delete_request_to_dict(request_message) dn = safe_dn(request['entry']) if dn in self.connection.server.dit: del self.connection.server.dit[dn] result_code = RESULT_SUCCESS message = '' else: result_code = RESULT_NO_SUCH_OBJECT message = 'object not found' return {'resultCode': result_code, 'matchedDN': '', 'diagnosticMessage': to_unicode(message, SERVER_ENCODING), 'referral': None } def mock_add(self, request_message, controls): # AddRequest ::= [APPLICATION 8] SEQUENCE { # entry LDAPDN, # attributes AttributeList } # # AddResponse ::= [APPLICATION 9] LDAPResult # # request: entry, attributes # response: LDAPResult request = add_request_to_dict(request_message) dn = safe_dn(request['entry']) attributes = request['attributes'] # converts attributes values to bytes if dn not in self.connection.server.dit: if self.add_entry(dn, attributes): result_code = RESULT_SUCCESS message = '' else: result_code = RESULT_OPERATIONS_ERROR message = 'error adding entry' else: result_code = RESULT_ENTRY_ALREADY_EXISTS message = 'entry already exist' return {'resultCode': result_code, 'matchedDN': '', 'diagnosticMessage': to_unicode(message, SERVER_ENCODING), 'referral': None } def mock_compare(self, request_message, controls): # CompareRequest ::= [APPLICATION 14] SEQUENCE { # entry LDAPDN, # ava AttributeValueAssertion } # # CompareResponse ::= [APPLICATION 15] LDAPResult # # request: entry, attribute, value # response: LDAPResult request = compare_request_to_dict(request_message) dn = safe_dn(request['entry']) attribute = request['attribute'] value = to_raw(request['value']) if dn in self.connection.server.dit: if attribute in self.connection.server.dit[dn]: if self.equal(dn, attribute, value): result_code = RESULT_COMPARE_TRUE message = '' else: result_code = RESULT_COMPARE_FALSE message = '' else: result_code = RESULT_NO_SUCH_ATTRIBUTE message = 'attribute not found' else: result_code = RESULT_NO_SUCH_OBJECT message = 'object not found' return {'resultCode': result_code, 'matchedDN': '', 'diagnosticMessage': to_unicode(message, SERVER_ENCODING), 'referral': None } def mock_modify_dn(self, request_message, controls): # ModifyDNRequest ::= [APPLICATION 12] SEQUENCE { # entry LDAPDN, # newrdn RelativeLDAPDN, # deleteoldrdn BOOLEAN, # newSuperior [0] LDAPDN OPTIONAL } # # ModifyDNResponse ::= [APPLICATION 13] LDAPResult # # request: entry, newRdn, deleteOldRdn, newSuperior # response: LDAPResult request = modify_dn_request_to_dict(request_message) dn = safe_dn(request['entry']) new_rdn = request['newRdn'] delete_old_rdn = request['deleteOldRdn'] new_superior = safe_dn(request['newSuperior']) if request['newSuperior'] else '' dn_components = to_dn(dn) if dn in self.connection.server.dit: if new_superior and new_rdn: # performs move in the DIT new_dn = safe_dn(dn_components[0] + ',' + new_superior) self.connection.server.dit[new_dn] = self.connection.server.dit[dn].copy() moved_entry = self.connection.server.dit[new_dn] if delete_old_rdn: del self.connection.server.dit[dn] result_code = RESULT_SUCCESS message = 'entry moved' moved_entry['entryDN'] = [to_raw(new_dn)] elif new_rdn and not new_superior: # performs rename new_dn = safe_dn(new_rdn + ',' + safe_dn(dn_components[1:])) self.connection.server.dit[new_dn] = self.connection.server.dit[dn].copy() renamed_entry = self.connection.server.dit[new_dn] del self.connection.server.dit[dn] renamed_entry['entryDN'] = [to_raw(new_dn)] for rdn in safe_rdn(new_dn, decompose=True): # adds rdns to entry attributes renamed_entry[rdn[0]] = [to_raw(rdn[1])] result_code = RESULT_SUCCESS message = 'entry rdn renamed' else: result_code = RESULT_UNWILLING_TO_PERFORM message = 'newRdn or newSuperior missing' else: result_code = RESULT_NO_SUCH_OBJECT message = 'object not found' return {'resultCode': result_code, 'matchedDN': '', 'diagnosticMessage': to_unicode(message, SERVER_ENCODING), 'referral': None } def mock_modify(self, request_message, controls): # ModifyRequest ::= [APPLICATION 6] SEQUENCE { # object LDAPDN, # changes SEQUENCE OF change SEQUENCE { # operation ENUMERATED { # add (0), # delete (1), # replace (2), # ... }, # modification PartialAttribute } } # # ModifyResponse ::= [APPLICATION 7] LDAPResult # # request: entry, changes # response: LDAPResult # # changes is a dictionary in the form {'attribute': [(operation, [val1, ...]), ...], ...} # operation is 0 (add), 1 (delete), 2 (replace), 3 (increment) request = modify_request_to_dict(request_message) dn = safe_dn(request['entry']) changes = request['changes'] result_code = 0 message = '' rdns = [rdn[0] for rdn in safe_rdn(dn, decompose=True)] if dn in self.connection.server.dit: entry = self.connection.server.dit[dn] original_entry = entry.copy() # to preserve atomicity of operation for modification in changes: operation = modification['operation'] attribute = modification['attribute']['type'] elements = modification['attribute']['value'] if operation == 0: # add if attribute not in entry and elements: # attribute not present, creates the new attribute and add elements if self.connection.server.schema and self.connection.server.schema.attribute_types and self.connection.server.schema.attribute_types[attribute].single_value and len(elements) > 1: # multiple values in single-valued attribute result_code = 19 message = 'attribute is single-valued' else: entry[attribute] = [to_raw(element) for element in elements] else: # attribute present, adds elements to current values if self.connection.server.schema and self.connection.server.schema.attribute_types and self.connection.server.schema.attribute_types[attribute].single_value: # multiple values in single-valued attribute result_code = 19 message = 'attribute is single-valued' else: entry[attribute].extend([to_raw(element) for element in elements]) elif operation == 1: # delete if attribute not in entry: # attribute must exist result_code = RESULT_NO_SUCH_ATTRIBUTE message = 'attribute must exists for deleting its values' elif attribute in rdns: # attribute can't be used in dn result_code = 67 message = 'cannot delete an rdn' else: if not elements: # deletes whole attribute if element list is empty del entry[attribute] else: for element in elements: raw_element = to_raw(element) if self.equal(dn, attribute, raw_element): # removes single element entry[attribute].remove(raw_element) else: result_code = 1 message = 'value to delete not found' if not entry[attribute]: # removes the whole attribute if no elements remained del entry[attribute] elif operation == 2: # replace if attribute not in entry and elements: # attribute not present, creates the new attribute and add elements if self.connection.server.schema and self.connection.server.schema.attribute_types and self.connection.server.schema.attribute_types[attribute].single_value and len(elements) > 1: # multiple values in single-valued attribute result_code = 19 message = 'attribute is single-valued' else: entry[attribute] = [to_raw(element) for element in elements] elif not elements and attribute in rdns: # attribute can't be used in dn result_code = 67 message = 'cannot replace an rdn' elif not elements: # deletes whole attribute if element list is empty if attribute in entry: del entry[attribute] else: # substitutes elements entry[attribute] = [to_raw(element) for element in elements] if result_code: # an error has happened, restores the original dn self.connection.server.dit[dn] = original_entry else: result_code = RESULT_NO_SUCH_OBJECT message = 'object not found' return {'resultCode': result_code, 'matchedDN': '', 'diagnosticMessage': to_unicode(message, SERVER_ENCODING), 'referral': None } def mock_search(self, request_message, controls): # SearchRequest ::= [APPLICATION 3] SEQUENCE { # baseObject LDAPDN, # scope ENUMERATED { # baseObject (0), # singleLevel (1), # wholeSubtree (2), # ... }, # derefAliases ENUMERATED { # neverDerefAliases (0), # derefInSearching (1), # derefFindingBaseObj (2), # derefAlways (3) }, # sizeLimit INTEGER (0 .. maxInt), # timeLimit INTEGER (0 .. maxInt), # typesOnly BOOLEAN, # filter Filter, # attributes AttributeSelection } # # SearchResultEntry ::= [APPLICATION 4] SEQUENCE { # objectName LDAPDN, # attributes PartialAttributeList } # # # SearchResultReference ::= [APPLICATION 19] SEQUENCE # SIZE (1..MAX) OF uri URI # # SearchResultDone ::= [APPLICATION 5] LDAPResult # # request: base, scope, dereferenceAlias, sizeLimit, timeLimit, typesOnly, filter, attributes # response_entry: object, attributes # response_done: LDAPResult request = search_request_to_dict(request_message) if controls: decoded_controls = [self.decode_control(control) for control in controls if control] for decoded_control in decoded_controls: if decoded_control[1]['criticality'] and decoded_control[0] not in SEARCH_CONTROLS: message = 'Critical requested control ' + str(decoded_control[0]) + ' not available' result = {'resultCode': RESULT_UNAVAILABLE_CRITICAL_EXTENSION, 'matchedDN': '', 'diagnosticMessage': to_unicode(message, SERVER_ENCODING), 'referral': None } return [], result elif decoded_control[0] == '1.2.840.113556.1.4.319': # Simple paged search if not decoded_control[1]['value']['cookie']: # new paged search response, result = self._execute_search(request) if result['resultCode'] == RESULT_SUCCESS: # success paged_set = PagedSearchSet(response, int(decoded_control[1]['value']['size']), decoded_control[1]['criticality']) response, result = paged_set.next() if paged_set.done: # paged search already completed, no need to store the set del paged_set else: self._paged_sets.append(paged_set) return response, result else: return [], result else: for paged_set in self._paged_sets: if paged_set.cookie == decoded_control[1]['value']['cookie']: # existing paged set response, result = paged_set.next() # returns next bunch of entries as per paged set specifications if paged_set.done: self._paged_sets.remove(paged_set) return response, result # paged set not found message = 'Invalid cookie in simple paged search' result = {'resultCode': RESULT_OPERATIONS_ERROR, 'matchedDN': '', 'diagnosticMessage': to_unicode(message, SERVER_ENCODING), 'referral': None } return [], result else: return self._execute_search(request) def _execute_search(self, request): responses = [] base = safe_dn(request['base']) scope = request['scope'] attributes = request['attributes'] if '+' in attributes: # operational attributes requested attributes.extend(self.operational_attributes) attributes.remove('+') attributes = [attr.lower() for attr in request['attributes']] filter_root = parse_filter(request['filter'], self.connection.server.schema, auto_escape=True, auto_encode=False, validator=self.connection.server.custom_validator, check_names=self.connection.check_names) candidates = [] if scope == 0: # base object if base in self.connection.server.dit or base.lower() == 'cn=schema': candidates.append(base) elif scope == 1: # single level for entry in self.connection.server.dit: if entry.lower().endswith(base.lower()) and ',' not in entry[:-len(base) - 1]: # only leafs without commas in the remaining dn candidates.append(entry) elif scope == 2: # whole subtree for entry in self.connection.server.dit: if entry.lower().endswith(base.lower()): candidates.append(entry) if not candidates: # incorrect base result_code = RESULT_NO_SUCH_OBJECT message = 'incorrect base object' else: matched = self.evaluate_filter_node(filter_root, candidates) if self.connection.raise_exceptions and 0 < request['sizeLimit'] < len(matched): result_code = 4 message = 'size limit exceeded' else: for match in matched: responses.append({ 'object': match, 'attributes': [{'type': attribute, 'vals': [] if request['typesOnly'] else self.connection.server.dit[match][attribute]} for attribute in self.connection.server.dit[match] if attribute.lower() in attributes or ALL_ATTRIBUTES in attributes] }) result_code = 0 message = '' result = {'resultCode': result_code, 'matchedDN': '', 'diagnosticMessage': to_unicode(message, SERVER_ENCODING), 'referral': None } return responses[:request['sizeLimit']] if request['sizeLimit'] > 0 else responses, result def mock_extended(self, request_message, controls): # ExtendedRequest ::= [APPLICATION 23] SEQUENCE { # requestName [0] LDAPOID, # requestValue [1] OCTET STRING OPTIONAL } # # ExtendedResponse ::= [APPLICATION 24] SEQUENCE { # COMPONENTS OF LDAPResult, # responseName [10] LDAPOID OPTIONAL, # responseValue [11] OCTET STRING OPTIONAL } # # IntermediateResponse ::= [APPLICATION 25] SEQUENCE { # responseName [0] LDAPOID OPTIONAL, # responseValue [1] OCTET STRING OPTIONAL } request = extended_request_to_dict(request_message) result_code = RESULT_UNWILLING_TO_PERFORM message = 'not implemented' response_name = None response_value = None if self.connection.server.info: for extension in self.connection.server.info.supported_extensions: if request['name'] == extension[0]: # server can answer the extended request if extension[0] == '2.16.840.1.113719.1.27.100.31': # getBindDNRequest [NOVELL] result_code = 0 message = '' response_name = '2.16.840.1.113719.1.27.100.32' # getBindDNResponse [NOVELL] response_value = OctetString(self.bound) elif extension[0] == '1.3.6.1.4.1.4203.1.11.3': # WhoAmI [RFC4532] result_code = 0 message = '' response_name = '1.3.6.1.4.1.4203.1.11.3' # WhoAmI [RFC4532] response_value = OctetString(self.bound) break return {'resultCode': result_code, 'matchedDN': '', 'diagnosticMessage': to_unicode(message, SERVER_ENCODING), 'referral': None, 'responseName': response_name, 'responseValue': response_value } def evaluate_filter_node(self, node, candidates): """After evaluation each 2 sets are added to each MATCH node, one for the matched object and one for unmatched object. The unmatched object set is needed if a superior node is a NOT that reverts the evaluation. The BOOLEAN nodes mix the sets returned by the MATCH nodes""" node.matched = set() node.unmatched = set() if node.elements: for element in node.elements: self.evaluate_filter_node(element, candidates) if node.tag == ROOT: return node.elements[0].matched elif node.tag == AND: first_element = node.elements[0] node.matched.update(first_element.matched) node.unmatched.update(first_element.unmatched) for element in node.elements[1:]: node.matched.intersection_update(element.matched) node.unmatched.intersection_update(element.unmatched) elif node.tag == OR: for element in node.elements: node.matched.update(element.matched) node.unmatched.update(element.unmatched) elif node.tag == NOT: node.matched = node.elements[0].unmatched node.unmatched = node.elements[0].matched elif node.tag == MATCH_GREATER_OR_EQUAL: attr_name = node.assertion['attr'] attr_value = node.assertion['value'] for candidate in candidates: if attr_name in self.connection.server.dit[candidate]: for value in self.connection.server.dit[candidate][attr_name]: if value.isdigit() and attr_value.isdigit(): # int comparison if int(value) >= int(attr_value): node.matched.add(candidate) else: node.unmatched.add(candidate) else: if to_unicode(value, SERVER_ENCODING).lower() >= to_unicode(attr_value, SERVER_ENCODING).lower(): # case insensitive string comparison node.matched.add(candidate) else: node.unmatched.add(candidate) elif node.tag == MATCH_LESS_OR_EQUAL: attr_name = node.assertion['attr'] attr_value = node.assertion['value'] for candidate in candidates: if attr_name in self.connection.server.dit[candidate]: for value in self.connection.server.dit[candidate][attr_name]: if value.isdigit() and attr_value.isdigit(): # int comparison if int(value) <= int(attr_value): node.matched.add(candidate) else: node.unmatched.add(candidate) else: if to_unicode(value, SERVER_ENCODING).lower() <= to_unicode(attr_value, SERVER_ENCODING).lower(): # case insentive string comparison node.matched.add(candidate) else: node.unmatched.add(candidate) elif node.tag == MATCH_EXTENSIBLE: self.connection.last_error = 'Extensible match not allowed in Mock strategy' if log_enabled(ERROR): log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) raise LDAPDefinitionError(self.connection.last_error) elif node.tag == MATCH_PRESENT: attr_name = node.assertion['attr'] for candidate in candidates: if attr_name in self.connection.server.dit[candidate]: node.matched.add(candidate) else: node.unmatched.add(candidate) elif node.tag == MATCH_SUBSTRING: attr_name = node.assertion['attr'] # rebuild the original substring filter if 'initial' in node.assertion and node.assertion['initial'] is not None: substring_filter = re.escape(to_unicode(node.assertion['initial'], SERVER_ENCODING)) else: substring_filter = '' if 'any' in node.assertion and node.assertion['any'] is not None: for middle in node.assertion['any']: substring_filter += '.*' + re.escape(to_unicode(middle, SERVER_ENCODING)) if 'final' in node.assertion and node.assertion['final'] is not None: substring_filter += '.*' + re.escape(to_unicode(node.assertion['final'], SERVER_ENCODING)) if substring_filter and not node.assertion.get('any', None) and not node.assertion.get('final', None): # only initial, adds .* substring_filter += '.*' regex_filter = re.compile(substring_filter, flags=re.UNICODE | re.IGNORECASE) # unicode AND ignorecase for candidate in candidates: if attr_name in self.connection.server.dit[candidate]: for value in self.connection.server.dit[candidate][attr_name]: if regex_filter.match(to_unicode(value, SERVER_ENCODING)): node.matched.add(candidate) else: node.unmatched.add(candidate) else: node.unmatched.add(candidate) elif node.tag == MATCH_EQUAL or node.tag == MATCH_APPROX: attr_name = node.assertion['attr'] attr_value = node.assertion['value'] for candidate in candidates: # if attr_name in self.connection.server.dit[candidate] and attr_value in self.connection.server.dit[candidate][attr_name]: if attr_name in self.connection.server.dit[candidate] and self.equal(candidate, attr_name, attr_value): node.matched.add(candidate) else: node.unmatched.add(candidate) def equal(self, dn, attribute_type, value_to_check): # value is the value to match attribute_values = self.connection.server.dit[dn][attribute_type] if not isinstance(attribute_values, SEQUENCE_TYPES): attribute_values = [attribute_values] for attribute_value in attribute_values: if self._check_equality(value_to_check, attribute_value): return True if self._check_equality(self._prepare_value(attribute_type, value_to_check), attribute_value): return True return False @staticmethod def _check_equality(value1, value2): if value1 == value2: # exact matching return True if str(value1).isdigit() and str(value2).isdigit(): if int(value1) == int(value2): # int comparison return True try: if to_unicode(value1, SERVER_ENCODING).lower() == to_unicode(value2, SERVER_ENCODING).lower(): # case insensitive comparison return True except UnicodeError: pass return False def send(self, message_type, request, controls=None): self.connection.request = self.decode_request(message_type, request, controls) if self.connection.listening: message_id = self.connection.server.next_message_id() if self.connection.usage: # ldap message is built for updating metrics only ldap_message = LDAPMessage() ldap_message['messageID'] = MessageID(message_id) ldap_message['protocolOp'] = ProtocolOp().setComponentByName(message_type, request) message_controls = build_controls_list(controls) if message_controls is not None: ldap_message['controls'] = message_controls asn1_request = BaseStrategy.decode_request(message_type, request, controls) self.connection._usage.update_transmitted_message(asn1_request, len(encode(ldap_message))) return message_id, message_type, request, controls else: self.connection.last_error = 'unable to send message, connection is not open' if log_enabled(ERROR): log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) raise LDAPSocketOpenError(self.connection.last_error)