You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

922 lines
46 KiB

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