123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895 |
- """
- """
-
- # 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 ..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 = '<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]
- 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)
-
|