902 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			902 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Python
		
	
	
| """
 | |
| """
 | |
| 
 | |
| # 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 <http://www.gnu.org/licenses/>.
 | |
| 
 | |
| 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 ..utils.conv import ldap_escape_to_bytes
 | |
| 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('The value "%s" of type %s for "%s" must be bytes or an offline schema needs to be provided when Mock strategy is used.' % (
 | |
|                 value,
 | |
|                 type(value),
 | |
|                 attribute_type,
 | |
|             ))
 | |
|         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 = '<anonymous>'
 | |
|         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 == '<anonymous>':
 | |
|             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]
 | |
|         escaped_value_to_check = ldap_escape_to_bytes(value_to_check)
 | |
|         for attribute_value in attribute_values:
 | |
|             if self._check_equality(escaped_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)
 | |
| 
 |