Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
mockBase.py922 linesDownload Raw Back to strategy
1"""
2"""
3
4# Created on 2016.04.30
5#
6# Author: Giovanni Cannata
7#
8# Copyright 2016 - 2020 Giovanni Cannata
9#
10# This file is part of ldap3.
11#
12# ldap3 is free software: you can redistribute it and/or modify
13# it under the terms of the GNU Lesser General Public License as published
14# by the Free Software Foundation, either version 3 of the License, or
15# (at your option) any later version.
16#
17# ldap3 is distributed in the hope that it will be useful,
18# but WITHOUT ANY WARRANTY; without even the implied warranty of
19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20# GNU Lesser General Public License for more details.
21#
22# You should have received a copy of the GNU Lesser General Public License
23# along with ldap3 in the COPYING and COPYING.LESSER files.
24# If not, see <http://www.gnu.org/licenses/>.
25
26import json
27import re
28
29from random import SystemRandom
30
31from pyasn1.type.univ import OctetString
32
33from .. import SEQUENCE_TYPES, ALL_ATTRIBUTES
34from ..operation.bind import bind_request_to_dict
35from ..operation.delete import delete_request_to_dict
36from ..operation.add import add_request_to_dict
37from ..operation.compare import compare_request_to_dict
38from ..operation.modifyDn import modify_dn_request_to_dict
39from ..operation.modify import modify_request_to_dict
40from ..operation.extended import extended_request_to_dict
41from ..operation.search import search_request_to_dict, parse_filter, ROOT, AND, OR, NOT, MATCH_APPROX, \
42    MATCH_GREATER_OR_EQUAL, MATCH_LESS_OR_EQUAL, MATCH_EXTENSIBLE, MATCH_PRESENT,\
43    MATCH_SUBSTRING, MATCH_EQUAL
44from ..utils.conv import json_hook, to_unicode, to_raw
45from ..core.exceptions import LDAPDefinitionError, LDAPPasswordIsMandatoryError, LDAPInvalidValueError, LDAPSocketOpenError
46from ..core.results import RESULT_SUCCESS, RESULT_OPERATIONS_ERROR, RESULT_UNAVAILABLE_CRITICAL_EXTENSION, \
47    RESULT_INVALID_CREDENTIALS, RESULT_NO_SUCH_OBJECT, RESULT_ENTRY_ALREADY_EXISTS, RESULT_COMPARE_TRUE, \
48    RESULT_COMPARE_FALSE, RESULT_NO_SUCH_ATTRIBUTE, RESULT_UNWILLING_TO_PERFORM, RESULT_PROTOCOL_ERROR, RESULT_CONSTRAINT_VIOLATION, RESULT_NOT_ALLOWED_ON_RDN
49from ..utils.ciDict import CaseInsensitiveDict
50from ..utils.dn import to_dn, safe_dn, safe_rdn
51from ..protocol.sasl.sasl import validate_simple_password
52from ..protocol.formatters.standard import find_attribute_validator, format_attribute_values
53from ..protocol.rfc2696 import paged_search_control
54from ..utils.log import log, log_enabled, ERROR, BASIC
55from ..utils.asn1 import encode
56from ..utils.conv import ldap_escape_to_bytes
57from ..strategy.base import BaseStrategy  # needed for decode_control() method
58from ..protocol.rfc4511 import LDAPMessage, ProtocolOp, MessageID
59from ..protocol.convert import build_controls_list
60
61
62# LDAPResult ::= SEQUENCE {
63#     resultCode         ENUMERATED {
64#         success                      (0),
65#         operationsError              (1),
66#         protocolError                (2),
67#         timeLimitExceeded            (3),
68#         sizeLimitExceeded            (4),
69#         compareFalse                 (5),
70#         compareTrue                  (6),
71#         authMethodNotSupported       (7),
72#         strongerAuthRequired         (8),
73#              -- 9 reserved --
74#         referral                     (10),
75#         adminLimitExceeded           (11),
76#         unavailableCriticalExtension (12),
77#         confidentialityRequired      (13),
78#         saslBindInProgress           (14),
79#         noSuchAttribute              (16),
80#         undefinedAttributeType       (17),
81#         inappropriateMatching        (18),
82#         constraintViolation          (19),
83#         attributeOrValueExists       (20),
84#         invalidAttributeSyntax       (21),
85#              -- 22-31 unused --
86#         noSuchObject                 (32),
87#         aliasProblem                 (33),
88#         invalidDNSyntax              (34),
89#              -- 35 reserved for undefined isLeaf --
90#         aliasDereferencingProblem    (36),
91#              -- 37-47 unused --
92#         inappropriateAuthentication  (48),
93#         invalidCredentials           (49),
94#         insufficientAccessRights     (50),
95#         busy                         (51),
96#         unavailable                  (52),
97#         unwillingToPerform           (53),
98#         loopDetect                   (54),
99#              -- 55-63 unused --
100#         namingViolation              (64),
101#         objectClassViolation         (65),
102#         notAllowedOnNonLeaf          (66),
103#         notAllowedOnRDN              (67),
104#         entryAlreadyExists           (68),
105#         objectClassModsProhibited    (69),
106#              -- 70 reserved for CLDAP --
107#         affectsMultipleDSAs          (71),
108#              -- 72-79 unused --
109#         other                        (80),
110#         ...  },
111#     matchedDN          LDAPDN,
112#     diagnosticMessage  LDAPString,
113#     referral           [3] Referral OPTIONAL }
114
115# noinspection PyProtectedMember,PyUnresolvedReferences
116
117SEARCH_CONTROLS = ['1.2.840.113556.1.4.319'  # simple paged search [RFC 2696]
118                   ]
119SERVER_ENCODING = 'utf-8'
120
121
122def random_cookie():
123    return to_raw(SystemRandom().random())[-6:]
124
125
126class PagedSearchSet(object):
127    def __init__(self, response, size, criticality):
128        self.size = size
129        self.response = response
130        self.cookie = None
131        self.sent = 0
132        self.done = False
133
134    def next(self, size=None):
135        if size:
136            self.size=size
137
138        message = ''
139        response = self.response[self.sent: self.sent + self.size]
140        self.sent += self.size
141        if self.sent > len(self.response):
142            self.done = True
143            self.cookie = ''
144        else:
145            self.cookie = random_cookie()
146
147        response_control = paged_search_control(False, len(self.response), self.cookie)
148        result = {'resultCode': RESULT_SUCCESS,
149                  'matchedDN': '',
150                  'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
151                  'referral': None,
152                  'controls': [BaseStrategy.decode_control(response_control)]
153                  }
154        return response, result
155
156
157class MockBaseStrategy(object):
158    """
159    Base class for connection strategy
160    """
161
162    def __init__(self):
163        if not hasattr(self.connection.server, 'dit'):  # create entries dict if not already present
164            self.connection.server.dit = CaseInsensitiveDict()
165        self.entries = self.connection.server.dit  # for simpler reference
166        self.no_real_dsa = True
167        self.bound = None
168        self.custom_validators = None
169        self.operational_attributes = ['entryDN']
170        self.add_entry('cn=schema', [], validate=False)  # add default entry for schema
171        self._paged_sets = []  # list of paged search in progress
172        if log_enabled(BASIC):
173            log(BASIC, 'instantiated <%s>: <%s>', self.__class__.__name__, self)
174
175    def _start_listen(self):
176        self.connection.listening = True
177        self.connection.closed = False
178        if self.connection.usage:
179            self.connection._usage.open_sockets += 1
180
181    def _stop_listen(self):
182        self.connection.listening = False
183        self.connection.closed = True
184        if self.connection.usage:
185            self.connection._usage.closed_sockets += 1
186
187    def _prepare_value(self, attribute_type, value, validate=True):
188        """
189        Prepare a value for being stored in the mock DIT
190        :param value: object to store
191        :return: raw value to store in the DIT
192        """
193        if validate:  # if loading from json dump do not validate values:
194            validator = find_attribute_validator(self.connection.server.schema, attribute_type, self.custom_validators)
195            validated = validator(value)
196            if validated is False:
197                raise LDAPInvalidValueError('value non valid for attribute \'%s\'' % attribute_type)
198            elif validated is not True:  # a valid LDAP value equivalent to the actual value
199                value = validated
200        raw_value = to_raw(value)
201        if not isinstance(raw_value, bytes):
202            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.' % (
203                value,
204                type(value),
205                attribute_type,
206            ))
207        return raw_value
208
209    def _update_attribute(self, dn, attribute_type, value):
210        pass
211
212    def add_entry(self, dn, attributes, validate=True):
213        with self.connection.server.dit_lock:
214            escaped_dn = safe_dn(dn)
215            if escaped_dn not in self.connection.server.dit:
216                new_entry = CaseInsensitiveDict()
217                for attribute in attributes:
218                    if attribute in self.operational_attributes:  # no restore of operational attributes, should be computed at runtime
219                        continue
220                    if not isinstance(attributes[attribute], SEQUENCE_TYPES):  # entry attributes are always lists of bytes values
221                        attributes[attribute] = [attributes[attribute]]
222                    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
223                        return False
224                    if attribute.lower() == 'objectclass' and self.connection.server.schema:  # builds the objectClass hierarchy only if schema is present
225                        class_set = set()
226                        for object_class in attributes[attribute]:
227                            if self.connection.server.schema.object_classes and object_class not in self.connection.server.schema.object_classes:
228                                return False
229                            # walkups the class hierarchy and buils a set of all classes in it
230                            class_set.add(object_class)
231                            class_set_size = 0
232                            while class_set_size != len(class_set):
233                                new_classes = set()
234                                class_set_size = len(class_set)
235                                for class_name in class_set:
236                                    if self.connection.server.schema.object_classes[class_name].superior:
237                                        new_classes.update(self.connection.server.schema.object_classes[class_name].superior)
238                                class_set.update(new_classes)
239                            new_entry['objectClass'] = [to_raw(value) for value in class_set]
240                    else:
241                        new_entry[attribute] = [self._prepare_value(attribute, value, validate) for value in attributes[attribute]]
242                for rdn in safe_rdn(escaped_dn, decompose=True):  # adds rdns to entry attributes
243                    if rdn[0] not in new_entry:  # if rdn attribute is missing adds attribute and its value
244                        new_entry[rdn[0]] = [to_raw(rdn[1])]
245                    else:
246                        raw_rdn = to_raw(rdn[1])
247                        if raw_rdn not in new_entry[rdn[0]]:  # add rdn value if rdn attribute is present but value is missing
248                            new_entry[rdn[0]].append(raw_rdn)
249                new_entry['entryDN'] = [to_raw(escaped_dn)]
250                self.connection.server.dit[escaped_dn] = new_entry
251                return True
252            return False
253
254    def remove_entry(self, dn):
255        with self.connection.server.dit_lock:
256            escaped_dn = safe_dn(dn)
257            if escaped_dn in self.connection.server.dit:
258                del self.connection.server.dit[escaped_dn]
259                return True
260            return False
261
262    def entries_from_json(self, json_entry_file):
263        target = open(json_entry_file, 'r')
264        definition = json.load(target, object_hook=json_hook)
265        if 'entries' not in definition:
266            self.connection.last_error = 'invalid JSON definition, missing "entries" section'
267            if log_enabled(ERROR):
268                log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
269            raise LDAPDefinitionError(self.connection.last_error)
270        if not self.connection.server.dit:
271            self.connection.server.dit = CaseInsensitiveDict()
272        for entry in definition['entries']:
273            if 'raw' not in entry:
274                self.connection.last_error = 'invalid JSON definition, missing "raw" section'
275                if log_enabled(ERROR):
276                    log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
277                raise LDAPDefinitionError(self.connection.last_error)
278            if 'dn' not in entry:
279                self.connection.last_error = 'invalid JSON definition, missing "dn" section'
280                if log_enabled(ERROR):
281                    log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
282                raise LDAPDefinitionError(self.connection.last_error)
283            self.add_entry(entry['dn'], entry['raw'], validate=False)
284        target.close()
285
286    def mock_bind(self, request_message, controls):
287        # BindRequest ::= [APPLICATION 0] SEQUENCE {
288        #     version                 INTEGER (1 ..  127),
289        #     name                    LDAPDN,
290        #     authentication          AuthenticationChoice }
291        #
292        # BindResponse ::= [APPLICATION 1] SEQUENCE {
293        #     COMPONENTS OF LDAPResult,
294        #     serverSaslCreds    [7] OCTET STRING OPTIONAL }
295        #
296        # request: version, name, authentication
297        # response: LDAPResult + serverSaslCreds
298        request = bind_request_to_dict(request_message)
299        identity = request['name']
300        if 'simple' in request['authentication']:
301            try:
302                password = validate_simple_password(request['authentication']['simple'])
303            except LDAPPasswordIsMandatoryError:
304                password = ''
305                identity = '<anonymous>'
306        else:
307            self.connection.last_error = 'only Simple Bind allowed in Mock strategy'
308            if log_enabled(ERROR):
309                log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
310            raise LDAPDefinitionError(self.connection.last_error)
311        # checks userPassword for password. userPassword must be a text string or a list of text strings
312        if identity in self.connection.server.dit:
313            if 'userPassword' in self.connection.server.dit[identity]:
314                # if self.connection.server.dit[identity]['userPassword'] == password or password in self.connection.server.dit[identity]['userPassword']:
315                if self.equal(identity, 'userPassword', password):
316                    result_code = RESULT_SUCCESS
317                    message = ''
318                    self.bound = identity
319                else:
320                    result_code = RESULT_INVALID_CREDENTIALS
321                    message = 'invalid credentials'
322            else:  # no user found, returns invalidCredentials
323                result_code = RESULT_INVALID_CREDENTIALS
324                message = 'missing userPassword attribute'
325        elif identity == '<anonymous>':
326            result_code = RESULT_SUCCESS
327            message = ''
328            self.bound = identity
329        else:
330            result_code = RESULT_INVALID_CREDENTIALS
331            message = 'missing object'
332
333        return {'resultCode': result_code,
334                'matchedDN': '',
335                'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
336                'referral': None,
337                'serverSaslCreds': None
338                }
339
340    def mock_delete(self, request_message, controls):
341        # DelRequest ::= [APPLICATION 10] LDAPDN
342        #
343        # DelResponse ::= [APPLICATION 11] LDAPResult
344        #
345        # request: entry
346        # response: LDAPResult
347        request = delete_request_to_dict(request_message)
348        dn = safe_dn(request['entry'])
349        if dn in self.connection.server.dit:
350            del self.connection.server.dit[dn]
351            result_code = RESULT_SUCCESS
352            message = ''
353        else:
354            result_code = RESULT_NO_SUCH_OBJECT
355            message = 'object not found'
356
357        return {'resultCode': result_code,
358                'matchedDN': '',
359                'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
360                'referral': None
361                }
362
363    def mock_add(self, request_message, controls):
364        # AddRequest ::= [APPLICATION 8] SEQUENCE {
365        #     entry           LDAPDN,
366        #     attributes      AttributeList }
367        #
368        # AddResponse ::= [APPLICATION 9] LDAPResult
369        #
370        # request: entry, attributes
371        # response: LDAPResult
372        request = add_request_to_dict(request_message)
373        dn = safe_dn(request['entry'])
374        attributes = request['attributes']
375        # converts attributes values to bytes
376
377        if dn not in self.connection.server.dit:
378            if self.add_entry(dn, attributes):
379                result_code = RESULT_SUCCESS
380                message = ''
381            else:
382                result_code = RESULT_OPERATIONS_ERROR
383                message = 'error adding entry'
384        else:
385            result_code = RESULT_ENTRY_ALREADY_EXISTS
386            message = 'entry already exist'
387
388        return {'resultCode': result_code,
389                'matchedDN': '',
390                'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
391                'referral': None
392                }
393
394    def mock_compare(self, request_message, controls):
395        # CompareRequest ::= [APPLICATION 14] SEQUENCE {
396        #     entry           LDAPDN,
397        #     ava             AttributeValueAssertion }
398        #
399        # CompareResponse ::= [APPLICATION 15] LDAPResult
400        #
401        # request: entry, attribute, value
402        # response: LDAPResult
403        request = compare_request_to_dict(request_message)
404        dn = safe_dn(request['entry'])
405        attribute = request['attribute']
406        value = to_raw(request['value'])
407        if dn in self.connection.server.dit:
408            if attribute in self.connection.server.dit[dn]:
409                if self.equal(dn, attribute, value):
410                    result_code = RESULT_COMPARE_TRUE
411                    message = ''
412                else:
413                    result_code = RESULT_COMPARE_FALSE
414                    message = ''
415            else:
416                result_code = RESULT_NO_SUCH_ATTRIBUTE
417                message = 'attribute not found'
418        else:
419            result_code = RESULT_NO_SUCH_OBJECT
420            message = 'object not found'
421
422        return {'resultCode': result_code,
423                'matchedDN': '',
424                'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
425                'referral': None
426                }
427
428    def mock_modify_dn(self, request_message, controls):
429        # ModifyDNRequest ::= [APPLICATION 12] SEQUENCE {
430        #     entry           LDAPDN,
431        #     newrdn          RelativeLDAPDN,
432        #     deleteoldrdn    BOOLEAN,
433        #     newSuperior     [0] LDAPDN OPTIONAL }
434        #
435        # ModifyDNResponse ::= [APPLICATION 13] LDAPResult
436        #
437        # request: entry, newRdn, deleteOldRdn, newSuperior
438        # response: LDAPResult
439        request = modify_dn_request_to_dict(request_message)
440        dn = safe_dn(request['entry'])
441        new_rdn = request['newRdn']
442        delete_old_rdn = request['deleteOldRdn']
443        new_superior = safe_dn(request['newSuperior']) if request['newSuperior'] else ''
444        dn_components = to_dn(dn)
445        if dn in self.connection.server.dit:
446            if new_superior and new_rdn:  # performs move in the DIT
447                new_dn = safe_dn(dn_components[0] + ',' + new_superior)
448                self.connection.server.dit[new_dn] = self.connection.server.dit[dn].copy()
449                moved_entry = self.connection.server.dit[new_dn]
450                if delete_old_rdn:
451                    del self.connection.server.dit[dn]
452                result_code = RESULT_SUCCESS
453                message = 'entry moved'
454                moved_entry['entryDN'] = [to_raw(new_dn)]
455            elif new_rdn and not new_superior:  # performs rename
456                new_dn = safe_dn(new_rdn + ',' + safe_dn(dn_components[1:]))
457                self.connection.server.dit[new_dn] = self.connection.server.dit[dn].copy()
458                renamed_entry = self.connection.server.dit[new_dn]
459                del self.connection.server.dit[dn]
460                renamed_entry['entryDN'] = [to_raw(new_dn)]
461
462                for rdn in safe_rdn(new_dn, decompose=True):  # adds rdns to entry attributes
463                    renamed_entry[rdn[0]] = [to_raw(rdn[1])]
464
465                result_code = RESULT_SUCCESS
466                message = 'entry rdn renamed'
467            else:
468                result_code = RESULT_UNWILLING_TO_PERFORM
469                message = 'newRdn or newSuperior missing'
470        else:
471            result_code = RESULT_NO_SUCH_OBJECT
472            message = 'object not found'
473
474        return {'resultCode': result_code,
475                'matchedDN': '',
476                'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
477                'referral': None
478                }
479
480    def mock_modify(self, request_message, controls):
481        # ModifyRequest ::= [APPLICATION 6] SEQUENCE {
482        #     object          LDAPDN,
483        #     changes         SEQUENCE OF change SEQUENCE {
484        #         operation       ENUMERATED {
485        #             add     (0),
486        #             delete  (1),
487        #             replace (2),
488        #             ...  },
489        #         modification    PartialAttribute } }
490        #
491        # ModifyResponse ::= [APPLICATION 7] LDAPResult
492        #
493        # request: entry, changes
494        # response: LDAPResult
495        #
496        # changes is a dictionary in the form {'attribute': [(operation, [val1, ...]), ...], ...}
497        # operation is 0 (add), 1 (delete), 2 (replace), 3 (increment)
498        request = modify_request_to_dict(request_message)
499        dn = safe_dn(request['entry'])
500        changes = request['changes']
501        result_code = 0
502        message = ''
503        rdns = [rdn[0] for rdn in safe_rdn(dn, decompose=True)]
504        if dn in self.connection.server.dit:
505            entry = self.connection.server.dit[dn]
506            original_entry = entry.copy()  # to preserve atomicity of operation
507            for modification in changes:
508                operation = modification['operation']
509                attribute = modification['attribute']['type']
510                elements = modification['attribute']['value']
511                if operation == 0:  # add
512                    if attribute not in entry and elements:  # attribute not present, creates the new attribute and add elements
513                        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
514                            result_code = RESULT_CONSTRAINT_VIOLATION
515                            message = 'attribute is single-valued'
516                        else:
517                            entry[attribute] = [to_raw(element) for element in elements]
518                    else:  # attribute present, adds elements to current values
519                        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
520                            result_code = RESULT_CONSTRAINT_VIOLATION
521                            message = 'attribute is single-valued'
522                        else:
523                            entry[attribute].extend([to_raw(element) for element in elements])
524                elif operation == 1:  # delete
525                    if attribute not in entry:  # attribute must exist
526                        result_code = RESULT_NO_SUCH_ATTRIBUTE
527                        message = 'attribute must exists for deleting its values'
528                    elif attribute in rdns:  # attribute can't be used in dn
529                        result_code = RESULT_NOT_ALLOWED_ON_RDN
530                        message = 'cannot delete an rdn'
531                    else:
532                        if not elements:  # deletes whole attribute if element list is empty
533                            del entry[attribute]
534                        else:
535                            for element in elements:
536                                raw_element = to_raw(element)
537                                if self.equal(dn, attribute, raw_element):  # removes single element
538                                    entry[attribute].remove(raw_element)
539                                else:
540                                    result_code = 1
541                                    message = 'value to delete not found'
542                            if not entry[attribute]:  # removes the whole attribute if no elements remained
543                                del entry[attribute]
544                elif operation == 2:  # replace
545                    if attribute not in entry and elements:  # attribute not present, creates the new attribute and add elements
546                        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
547                            result_code = RESULT_CONSTRAINT_VIOLATION
548                            message = 'attribute is single-valued'
549                        else:
550                            entry[attribute] = [to_raw(element) for element in elements]
551                    elif not elements and attribute in rdns:  # attribute can't be used in dn
552                        result_code = RESULT_NOT_ALLOWED_ON_RDN
553                        message = 'cannot replace an rdn'
554                    elif not elements:  # deletes whole attribute if element list is empty
555                        if attribute in entry:
556                            del entry[attribute]
557                    else:  # substitutes elements
558                        entry[attribute] = [to_raw(element) for element in elements]
559                elif operation == 3:  # increment
560                    if attribute not in entry:  # attribute must exist
561                        result_code = RESULT_NO_SUCH_ATTRIBUTE
562                        message = 'attribute must exists for incrementing its values'
563                    else:
564                        if len(elements) != 1:
565                            result_code = RESULT_PROTOCOL_ERROR
566                            message = 'only one increment value is allowed'
567                        else:
568                            try:
569                                entry[attribute] = [bytes(str(int(value) + int(elements[0])), encoding='utf-8') for value in entry[attribute]]
570                            except:
571                                result_code = RESULT_UNWILLING_TO_PERFORM
572                                message = 'unable to increment value'
573
574            if result_code:  # an error has happened, restores the original dn
575                self.connection.server.dit[dn] = original_entry
576        else:
577            result_code = RESULT_NO_SUCH_OBJECT
578            message = 'object not found'
579
580        return {'resultCode': result_code,
581                'matchedDN': '',
582                'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
583                'referral': None
584                }
585
586    def mock_search(self, request_message, controls):
587        # SearchRequest ::= [APPLICATION 3] SEQUENCE {
588        #     baseObject      LDAPDN,
589        #     scope           ENUMERATED {
590        #         baseObject              (0),
591        #         singleLevel             (1),
592        #         wholeSubtree            (2),
593        #     ...  },
594        #     derefAliases    ENUMERATED {
595        #         neverDerefAliases       (0),
596        #         derefInSearching        (1),
597        #         derefFindingBaseObj     (2),
598        #         derefAlways             (3) },
599        #     sizeLimit       INTEGER (0 ..  maxInt),
600        #     timeLimit       INTEGER (0 ..  maxInt),
601        #     typesOnly       BOOLEAN,
602        #     filter          Filter,
603        #     attributes      AttributeSelection }
604        #
605        # SearchResultEntry ::= [APPLICATION 4] SEQUENCE {
606        #     objectName      LDAPDN,
607        #     attributes      PartialAttributeList }
608        #
609        #
610        # SearchResultReference ::= [APPLICATION 19] SEQUENCE
611        #     SIZE (1..MAX) OF uri URI
612        #
613        # SearchResultDone ::= [APPLICATION 5] LDAPResult
614        #
615        # request: base, scope, dereferenceAlias, sizeLimit, timeLimit, typesOnly, filter, attributes
616        # response_entry: object, attributes
617        # response_done: LDAPResult
618        request = search_request_to_dict(request_message)
619        if controls:
620            decoded_controls = [self.decode_control(control) for control in controls if control]
621            for decoded_control in decoded_controls:
622                if decoded_control[1]['criticality'] and decoded_control[0] not in SEARCH_CONTROLS:
623                    message = 'Critical requested control ' + str(decoded_control[0]) + ' not available'
624                    result = {'resultCode': RESULT_UNAVAILABLE_CRITICAL_EXTENSION,
625                              'matchedDN': '',
626                              'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
627                              'referral': None
628                              }
629                    return [], result
630                elif decoded_control[0] == '1.2.840.113556.1.4.319':  # Simple paged search
631                    if not decoded_control[1]['value']['cookie']:  # new paged search
632                        response, result =  self._execute_search(request)
633                        if result['resultCode'] == RESULT_SUCCESS:  # success
634                            paged_set = PagedSearchSet(response, int(decoded_control[1]['value']['size']), decoded_control[1]['criticality'])
635                            response, result = paged_set.next()
636                            if paged_set.done:  # paged search already completed, no need to store the set
637                                del paged_set
638                            else:
639                                self._paged_sets.append(paged_set)
640                            return response, result
641                        else:
642                            return [], result
643                    else:
644                        for paged_set in self._paged_sets:
645                            if paged_set.cookie == decoded_control[1]['value']['cookie']: # existing paged set
646                                response, result = paged_set.next()  # returns next bunch of entries as per paged set specifications
647                                if paged_set.done:
648                                    self._paged_sets.remove(paged_set)
649                                return response, result
650                        # paged set not found
651                        message = 'Invalid cookie in simple paged search'
652                        result = {'resultCode': RESULT_OPERATIONS_ERROR,
653                                  'matchedDN': '',
654                                  'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
655                                  'referral': None
656                                  }
657                        return [], result
658
659        else:
660            return self._execute_search(request)
661
662    def _execute_search(self, request):
663        responses = []
664        base = safe_dn(request['base'])
665        scope = request['scope']
666        attributes = request['attributes']
667        if '+' in attributes:  # operational attributes requested
668            attributes.extend(self.operational_attributes)
669            attributes.remove('+')
670
671        attributes = [attr.lower() for attr in request['attributes']]
672
673        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)
674        candidates = []
675        if scope == 0:  # base object
676            if base in self.connection.server.dit or base.lower() == 'cn=schema':
677                candidates.append(base)
678        elif scope == 1:  # single level
679            for entry in self.connection.server.dit:
680                if entry.lower().endswith(base.lower()) and ',' not in entry[:-len(base) - 1]:  # only leafs without commas in the remaining dn
681                    candidates.append(entry)
682        elif scope == 2:  # whole subtree
683            for entry in self.connection.server.dit:
684                if entry.lower().endswith(base.lower()):
685                    candidates.append(entry)
686
687        if not candidates:  # incorrect base
688            result_code = RESULT_NO_SUCH_OBJECT
689            message = 'incorrect base object'
690        else:
691            matched = self.evaluate_filter_node(filter_root, candidates)
692            if self.connection.raise_exceptions and 0 < request['sizeLimit'] < len(matched):
693                result_code = 4
694                message = 'size limit exceeded'
695            else:
696                for match in matched:
697                    responses.append({
698                        'object': match,
699                        'attributes': [{'type': attribute,
700                                        'vals': [] if request['typesOnly'] else self.connection.server.dit[match][attribute]}
701                                       for attribute in self.connection.server.dit[match]
702                                       if attribute.lower() in attributes or ALL_ATTRIBUTES in attributes]
703                    })
704                    if '+' not in attributes:  # remove operational attributes
705                        for op_attr in self.operational_attributes:
706                            if op_attr.lower() in attributes:
707                                # if the op_attr was explicitly requested, then keep it
708                                continue
709                            for i, attr in enumerate(responses[len(responses)-1]['attributes']):
710                                if attr['type'] == op_attr:
711                                    del responses[len(responses)-1]['attributes'][i]
712                result_code = 0
713                message = ''
714
715        result = {'resultCode': result_code,
716                  'matchedDN': '',
717                  'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
718                  'referral': None
719                  }
720
721        return responses[:request['sizeLimit']] if request['sizeLimit'] > 0 else responses, result
722
723    def mock_extended(self, request_message, controls):
724        # ExtendedRequest ::= [APPLICATION 23] SEQUENCE {
725        #     requestName      [0] LDAPOID,
726        #     requestValue     [1] OCTET STRING OPTIONAL }
727        #
728        # ExtendedResponse ::= [APPLICATION 24] SEQUENCE {
729        #     COMPONENTS OF LDAPResult,
730        #     responseName     [10] LDAPOID OPTIONAL,
731        #     responseValue    [11] OCTET STRING OPTIONAL }
732        #
733        # IntermediateResponse ::= [APPLICATION 25] SEQUENCE {
734        #     responseName     [0] LDAPOID OPTIONAL,
735        #     responseValue    [1] OCTET STRING OPTIONAL }
736        request = extended_request_to_dict(request_message)
737
738        result_code = RESULT_UNWILLING_TO_PERFORM
739        message = 'not implemented'
740        response_name = None
741        response_value = None
742        if self.connection.server.info:
743            for extension in self.connection.server.info.supported_extensions:
744                if request['name'] == extension[0]:  # server can answer the extended request
745                    if extension[0] == '2.16.840.1.113719.1.27.100.31':  # getBindDNRequest [NOVELL]
746                        result_code = 0
747                        message = ''
748                        response_name = OctetString('2.16.840.1.113719.1.27.100.32')  # getBindDNResponse [NOVELL]
749                        response_value = OctetString(self.bound)
750                    elif extension[0] == '1.3.6.1.4.1.4203.1.11.3':  # WhoAmI [RFC4532]
751                        result_code = 0
752                        message = ''
753                        response_name = OctetString('1.3.6.1.4.1.4203.1.11.3')  # WhoAmI [RFC4532]
754                        response_value = OctetString(self.bound)
755                    break
756
757        return {'resultCode': result_code,
758                'matchedDN': '',
759                'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
760                'referral': None,
761                'responseName': response_name,
762                'responseValue': response_value
763                }
764
765    def evaluate_filter_node(self, node, candidates):
766        """After evaluation each 2 sets are added to each MATCH node, one for the matched object and one for unmatched object.
767        The unmatched object set is needed if a superior node is a NOT that reverts the evaluation. The BOOLEAN nodes mix the sets
768        returned by the MATCH nodes"""
769        node.matched = set()
770        node.unmatched = set()
771
772        if node.elements:
773            for element in node.elements:
774                self.evaluate_filter_node(element, candidates)
775
776        if node.tag == ROOT:
777            return node.elements[0].matched
778        elif node.tag == AND:
779            first_element = node.elements[0]
780            node.matched.update(first_element.matched)
781            node.unmatched.update(first_element.unmatched)
782
783            for element in node.elements[1:]:
784                node.matched.intersection_update(element.matched)
785                node.unmatched.intersection_update(element.unmatched)
786        elif node.tag == OR:
787            for element in node.elements:
788                node.matched.update(element.matched)
789                node.unmatched.update(element.unmatched)
790        elif node.tag == NOT:
791            node.matched = node.elements[0].unmatched
792            node.unmatched = node.elements[0].matched
793        elif node.tag == MATCH_GREATER_OR_EQUAL:
794            attr_name = node.assertion['attr']
795            attr_value = node.assertion['value']
796            for candidate in candidates:
797                if attr_name in self.connection.server.dit[candidate]:
798                    for value in self.connection.server.dit[candidate][attr_name]:
799                        if value.isdigit() and attr_value.isdigit():  # int comparison
800                            if int(value) >= int(attr_value):
801                                node.matched.add(candidate)
802                            else:
803                                node.unmatched.add(candidate)
804                        else:
805                            if to_unicode(value, SERVER_ENCODING).lower() >= to_unicode(attr_value, SERVER_ENCODING).lower():  # case insensitive string comparison
806                                node.matched.add(candidate)
807                            else:
808                                node.unmatched.add(candidate)
809        elif node.tag == MATCH_LESS_OR_EQUAL:
810            attr_name = node.assertion['attr']
811            attr_value = node.assertion['value']
812            for candidate in candidates:
813                if attr_name in self.connection.server.dit[candidate]:
814                    for value in self.connection.server.dit[candidate][attr_name]:
815                        if value.isdigit() and attr_value.isdigit():  # int comparison
816                            if int(value) <= int(attr_value):
817                                node.matched.add(candidate)
818                            else:
819                                node.unmatched.add(candidate)
820                        else:
821                            if to_unicode(value, SERVER_ENCODING).lower() <= to_unicode(attr_value, SERVER_ENCODING).lower():  # case insentive string comparison
822                                node.matched.add(candidate)
823                            else:
824                                node.unmatched.add(candidate)
825        elif node.tag == MATCH_EXTENSIBLE:
826            self.connection.last_error = 'Extensible match not allowed in Mock strategy'
827            if log_enabled(ERROR):
828                log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
829            raise LDAPDefinitionError(self.connection.last_error)
830        elif node.tag == MATCH_PRESENT:
831            attr_name = node.assertion['attr']
832            for candidate in candidates:
833                if attr_name in self.connection.server.dit[candidate]:
834                    node.matched.add(candidate)
835                else:
836                    node.unmatched.add(candidate)
837        elif node.tag == MATCH_SUBSTRING:
838            attr_name = node.assertion['attr']
839            # rebuild the original substring filter
840            if 'initial' in node.assertion and node.assertion['initial'] is not None:
841                substring_filter = re.escape(to_unicode(node.assertion['initial'], SERVER_ENCODING))
842            else:
843                substring_filter = ''
844
845            if 'any' in node.assertion and node.assertion['any'] is not None:
846                for middle in node.assertion['any']:
847                    substring_filter += '.*' + re.escape(to_unicode(middle, SERVER_ENCODING))
848
849            if 'final' in node.assertion and node.assertion['final'] is not None:
850                substring_filter += '.*' + re.escape(to_unicode(node.assertion['final'], SERVER_ENCODING))
851
852            if substring_filter and not node.assertion.get('any', None) and not node.assertion.get('final', None):  # only initial, adds .*
853                substring_filter += '.*'
854
855            regex_filter = re.compile(substring_filter, flags=re.UNICODE | re.IGNORECASE)  # unicode AND ignorecase
856            for candidate in candidates:
857                if attr_name in self.connection.server.dit[candidate]:
858                    for value in self.connection.server.dit[candidate][attr_name]:
859                        if regex_filter.match(to_unicode(value, SERVER_ENCODING)):
860                            node.matched.add(candidate)
861                        else:
862                            node.unmatched.add(candidate)
863                else:
864                    node.unmatched.add(candidate)
865        elif node.tag == MATCH_EQUAL or node.tag == MATCH_APPROX:
866            attr_name = node.assertion['attr']
867            attr_value = node.assertion['value']
868            for candidate in candidates:
869                if attr_name in self.connection.server.dit[candidate] and self.equal(candidate, attr_name, attr_value):
870                    node.matched.add(candidate)
871                else:
872                    node.unmatched.add(candidate)
873
874    def equal(self, dn, attribute_type, value_to_check):
875        # value is the value to match
876        attribute_values = self.connection.server.dit[dn][attribute_type]
877        if not isinstance(attribute_values, SEQUENCE_TYPES):
878            attribute_values = [attribute_values]
879        escaped_value_to_check = ldap_escape_to_bytes(value_to_check)
880        for attribute_value in attribute_values:
881            if self._check_equality(escaped_value_to_check, attribute_value):
882                return True
883            if self._check_equality(self._prepare_value(attribute_type, value_to_check), attribute_value):
884                return True
885        return False
886
887    @staticmethod
888    def _check_equality(value1, value2):
889        if value1 == value2:  # exact matching
890            return True
891        if str(value1).isdigit() and str(value2).isdigit():
892            if int(value1) == int(value2):  # int comparison
893                return True
894        try:
895            if to_unicode(value1, SERVER_ENCODING).lower() == to_unicode(value2, SERVER_ENCODING).lower():  # case insensitive comparison
896                return True
897        except UnicodeError:
898            pass
899
900        return False
901
902    def send(self, message_type, request, controls=None):
903        self.connection.request = self.decode_request(message_type, request, controls)
904        if self.connection.listening:
905            message_id = self.connection.server.next_message_id()
906            if self.connection.usage:  # ldap message is built for updating metrics only
907                ldap_message = LDAPMessage()
908                ldap_message['messageID'] = MessageID(message_id)
909                ldap_message['protocolOp'] = ProtocolOp().setComponentByName(message_type, request)
910                message_controls = build_controls_list(controls)
911                if message_controls is not None:
912                    ldap_message['controls'] = message_controls
913                asn1_request = BaseStrategy.decode_request(message_type, request, controls)
914                self.connection._usage.update_transmitted_message(asn1_request, len(encode(ldap_message)))
915            return message_id, message_type, request, controls
916        else:
917            self.connection.last_error = 'unable to send message, connection is not open'
918            if log_enabled(ERROR):
919                log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
920            raise LDAPSocketOpenError(self.connection.last_error)
921
922 
codekingpro/portable-devtools · Team Ai