codekingpro/portable-devtools
114k
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 