選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 

901 行
44 KiB

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