Development of an internal social media platform with personalised dashboards for students
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.

mockBase.py 44KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895
  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 ..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('added values must be bytes if no offline schema is provided in Mock strategies')
  181. return raw_value
  182. def _update_attribute(self, dn, attribute_type, value):
  183. pass
  184. def add_entry(self, dn, attributes, validate=True):
  185. with self.connection.server.dit_lock:
  186. escaped_dn = safe_dn(dn)
  187. if escaped_dn not in self.connection.server.dit:
  188. new_entry = CaseInsensitiveDict()
  189. for attribute in attributes:
  190. if attribute in self.operational_attributes: # no restore of operational attributes, should be computed at runtime
  191. continue
  192. if not isinstance(attributes[attribute], SEQUENCE_TYPES): # entry attributes are always lists of bytes values
  193. attributes[attribute] = [attributes[attribute]]
  194. 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
  195. return False
  196. if attribute.lower() == 'objectclass' and self.connection.server.schema: # builds the objectClass hierarchy only if schema is present
  197. class_set = set()
  198. for object_class in attributes['objectClass']:
  199. if self.connection.server.schema.object_classes and object_class not in self.connection.server.schema.object_classes:
  200. return False
  201. # walkups the class hierarchy and buils a set of all classes in it
  202. class_set.add(object_class)
  203. class_set_size = 0
  204. while class_set_size != len(class_set):
  205. new_classes = set()
  206. class_set_size = len(class_set)
  207. for class_name in class_set:
  208. if self.connection.server.schema.object_classes[class_name].superior:
  209. new_classes.update(self.connection.server.schema.object_classes[class_name].superior)
  210. class_set.update(new_classes)
  211. new_entry['objectClass'] = [to_raw(value) for value in class_set]
  212. else:
  213. new_entry[attribute] = [self._prepare_value(attribute, value, validate) for value in attributes[attribute]]
  214. for rdn in safe_rdn(escaped_dn, decompose=True): # adds rdns to entry attributes
  215. if rdn[0] not in new_entry: # if rdn attribute is missing adds attribute and its value
  216. new_entry[rdn[0]] = [to_raw(rdn[1])]
  217. else:
  218. raw_rdn = to_raw(rdn[1])
  219. if raw_rdn not in new_entry[rdn[0]]: # add rdn value if rdn attribute is present but value is missing
  220. new_entry[rdn[0]].append(raw_rdn)
  221. new_entry['entryDN'] = [to_raw(escaped_dn)]
  222. self.connection.server.dit[escaped_dn] = new_entry
  223. return True
  224. return False
  225. def remove_entry(self, dn):
  226. with self.connection.server.dit_lock:
  227. escaped_dn = safe_dn(dn)
  228. if escaped_dn in self.connection.server.dit:
  229. del self.connection.server.dit[escaped_dn]
  230. return True
  231. return False
  232. def entries_from_json(self, json_entry_file):
  233. target = open(json_entry_file, 'r')
  234. definition = json.load(target, object_hook=json_hook)
  235. if 'entries' not in definition:
  236. self.connection.last_error = 'invalid JSON definition, missing "entries" section'
  237. if log_enabled(ERROR):
  238. log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
  239. raise LDAPDefinitionError(self.connection.last_error)
  240. if not self.connection.server.dit:
  241. self.connection.server.dit = CaseInsensitiveDict()
  242. for entry in definition['entries']:
  243. if 'raw' not in entry:
  244. self.connection.last_error = 'invalid JSON definition, missing "raw" section'
  245. if log_enabled(ERROR):
  246. log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
  247. raise LDAPDefinitionError(self.connection.last_error)
  248. if 'dn' not in entry:
  249. self.connection.last_error = 'invalid JSON definition, missing "dn" 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. self.add_entry(entry['dn'], entry['raw'], validate=False)
  254. target.close()
  255. def mock_bind(self, request_message, controls):
  256. # BindRequest ::= [APPLICATION 0] SEQUENCE {
  257. # version INTEGER (1 .. 127),
  258. # name LDAPDN,
  259. # authentication AuthenticationChoice }
  260. #
  261. # BindResponse ::= [APPLICATION 1] SEQUENCE {
  262. # COMPONENTS OF LDAPResult,
  263. # serverSaslCreds [7] OCTET STRING OPTIONAL }
  264. #
  265. # request: version, name, authentication
  266. # response: LDAPResult + serverSaslCreds
  267. request = bind_request_to_dict(request_message)
  268. identity = request['name']
  269. if 'simple' in request['authentication']:
  270. try:
  271. password = validate_simple_password(request['authentication']['simple'])
  272. except LDAPPasswordIsMandatoryError:
  273. password = ''
  274. identity = '<anonymous>'
  275. else:
  276. self.connection.last_error = 'only Simple Bind allowed in Mock strategy'
  277. if log_enabled(ERROR):
  278. log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
  279. raise LDAPDefinitionError(self.connection.last_error)
  280. # checks userPassword for password. userPassword must be a text string or a list of text strings
  281. if identity in self.connection.server.dit:
  282. if 'userPassword' in self.connection.server.dit[identity]:
  283. # if self.connection.server.dit[identity]['userPassword'] == password or password in self.connection.server.dit[identity]['userPassword']:
  284. if self.equal(identity, 'userPassword', password):
  285. result_code = RESULT_SUCCESS
  286. message = ''
  287. self.bound = identity
  288. else:
  289. result_code = RESULT_INVALID_CREDENTIALS
  290. message = 'invalid credentials'
  291. else: # no user found, returns invalidCredentials
  292. result_code = RESULT_INVALID_CREDENTIALS
  293. message = 'missing userPassword attribute'
  294. elif identity == '<anonymous>':
  295. result_code = RESULT_SUCCESS
  296. message = ''
  297. self.bound = identity
  298. else:
  299. result_code = RESULT_INVALID_CREDENTIALS
  300. message = 'missing object'
  301. return {'resultCode': result_code,
  302. 'matchedDN': '',
  303. 'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
  304. 'referral': None,
  305. 'serverSaslCreds': None
  306. }
  307. def mock_delete(self, request_message, controls):
  308. # DelRequest ::= [APPLICATION 10] LDAPDN
  309. #
  310. # DelResponse ::= [APPLICATION 11] LDAPResult
  311. #
  312. # request: entry
  313. # response: LDAPResult
  314. request = delete_request_to_dict(request_message)
  315. dn = safe_dn(request['entry'])
  316. if dn in self.connection.server.dit:
  317. del self.connection.server.dit[dn]
  318. result_code = RESULT_SUCCESS
  319. message = ''
  320. else:
  321. result_code = RESULT_NO_SUCH_OBJECT
  322. message = 'object not found'
  323. return {'resultCode': result_code,
  324. 'matchedDN': '',
  325. 'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
  326. 'referral': None
  327. }
  328. def mock_add(self, request_message, controls):
  329. # AddRequest ::= [APPLICATION 8] SEQUENCE {
  330. # entry LDAPDN,
  331. # attributes AttributeList }
  332. #
  333. # AddResponse ::= [APPLICATION 9] LDAPResult
  334. #
  335. # request: entry, attributes
  336. # response: LDAPResult
  337. request = add_request_to_dict(request_message)
  338. dn = safe_dn(request['entry'])
  339. attributes = request['attributes']
  340. # converts attributes values to bytes
  341. if dn not in self.connection.server.dit:
  342. if self.add_entry(dn, attributes):
  343. result_code = RESULT_SUCCESS
  344. message = ''
  345. else:
  346. result_code = RESULT_OPERATIONS_ERROR
  347. message = 'error adding entry'
  348. else:
  349. result_code = RESULT_ENTRY_ALREADY_EXISTS
  350. message = 'entry already exist'
  351. return {'resultCode': result_code,
  352. 'matchedDN': '',
  353. 'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
  354. 'referral': None
  355. }
  356. def mock_compare(self, request_message, controls):
  357. # CompareRequest ::= [APPLICATION 14] SEQUENCE {
  358. # entry LDAPDN,
  359. # ava AttributeValueAssertion }
  360. #
  361. # CompareResponse ::= [APPLICATION 15] LDAPResult
  362. #
  363. # request: entry, attribute, value
  364. # response: LDAPResult
  365. request = compare_request_to_dict(request_message)
  366. dn = safe_dn(request['entry'])
  367. attribute = request['attribute']
  368. value = to_raw(request['value'])
  369. if dn in self.connection.server.dit:
  370. if attribute in self.connection.server.dit[dn]:
  371. if self.equal(dn, attribute, value):
  372. result_code = RESULT_COMPARE_TRUE
  373. message = ''
  374. else:
  375. result_code = RESULT_COMPARE_FALSE
  376. message = ''
  377. else:
  378. result_code = RESULT_NO_SUCH_ATTRIBUTE
  379. message = 'attribute not found'
  380. else:
  381. result_code = RESULT_NO_SUCH_OBJECT
  382. message = 'object not found'
  383. return {'resultCode': result_code,
  384. 'matchedDN': '',
  385. 'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
  386. 'referral': None
  387. }
  388. def mock_modify_dn(self, request_message, controls):
  389. # ModifyDNRequest ::= [APPLICATION 12] SEQUENCE {
  390. # entry LDAPDN,
  391. # newrdn RelativeLDAPDN,
  392. # deleteoldrdn BOOLEAN,
  393. # newSuperior [0] LDAPDN OPTIONAL }
  394. #
  395. # ModifyDNResponse ::= [APPLICATION 13] LDAPResult
  396. #
  397. # request: entry, newRdn, deleteOldRdn, newSuperior
  398. # response: LDAPResult
  399. request = modify_dn_request_to_dict(request_message)
  400. dn = safe_dn(request['entry'])
  401. new_rdn = request['newRdn']
  402. delete_old_rdn = request['deleteOldRdn']
  403. new_superior = safe_dn(request['newSuperior']) if request['newSuperior'] else ''
  404. dn_components = to_dn(dn)
  405. if dn in self.connection.server.dit:
  406. if new_superior and new_rdn: # performs move in the DIT
  407. new_dn = safe_dn(dn_components[0] + ',' + new_superior)
  408. self.connection.server.dit[new_dn] = self.connection.server.dit[dn].copy()
  409. moved_entry = self.connection.server.dit[new_dn]
  410. if delete_old_rdn:
  411. del self.connection.server.dit[dn]
  412. result_code = RESULT_SUCCESS
  413. message = 'entry moved'
  414. moved_entry['entryDN'] = [to_raw(new_dn)]
  415. elif new_rdn and not new_superior: # performs rename
  416. new_dn = safe_dn(new_rdn + ',' + safe_dn(dn_components[1:]))
  417. self.connection.server.dit[new_dn] = self.connection.server.dit[dn].copy()
  418. renamed_entry = self.connection.server.dit[new_dn]
  419. del self.connection.server.dit[dn]
  420. renamed_entry['entryDN'] = [to_raw(new_dn)]
  421. for rdn in safe_rdn(new_dn, decompose=True): # adds rdns to entry attributes
  422. renamed_entry[rdn[0]] = [to_raw(rdn[1])]
  423. result_code = RESULT_SUCCESS
  424. message = 'entry rdn renamed'
  425. else:
  426. result_code = RESULT_UNWILLING_TO_PERFORM
  427. message = 'newRdn or newSuperior missing'
  428. else:
  429. result_code = RESULT_NO_SUCH_OBJECT
  430. message = 'object not found'
  431. return {'resultCode': result_code,
  432. 'matchedDN': '',
  433. 'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
  434. 'referral': None
  435. }
  436. def mock_modify(self, request_message, controls):
  437. # ModifyRequest ::= [APPLICATION 6] SEQUENCE {
  438. # object LDAPDN,
  439. # changes SEQUENCE OF change SEQUENCE {
  440. # operation ENUMERATED {
  441. # add (0),
  442. # delete (1),
  443. # replace (2),
  444. # ... },
  445. # modification PartialAttribute } }
  446. #
  447. # ModifyResponse ::= [APPLICATION 7] LDAPResult
  448. #
  449. # request: entry, changes
  450. # response: LDAPResult
  451. #
  452. # changes is a dictionary in the form {'attribute': [(operation, [val1, ...]), ...], ...}
  453. # operation is 0 (add), 1 (delete), 2 (replace), 3 (increment)
  454. request = modify_request_to_dict(request_message)
  455. dn = safe_dn(request['entry'])
  456. changes = request['changes']
  457. result_code = 0
  458. message = ''
  459. rdns = [rdn[0] for rdn in safe_rdn(dn, decompose=True)]
  460. if dn in self.connection.server.dit:
  461. entry = self.connection.server.dit[dn]
  462. original_entry = entry.copy() # to preserve atomicity of operation
  463. for modification in changes:
  464. operation = modification['operation']
  465. attribute = modification['attribute']['type']
  466. elements = modification['attribute']['value']
  467. if operation == 0: # add
  468. if attribute not in entry and elements: # attribute not present, creates the new attribute and add elements
  469. 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
  470. result_code = 19
  471. message = 'attribute is single-valued'
  472. else:
  473. entry[attribute] = [to_raw(element) for element in elements]
  474. else: # attribute present, adds elements to current values
  475. 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
  476. result_code = 19
  477. message = 'attribute is single-valued'
  478. else:
  479. entry[attribute].extend([to_raw(element) for element in elements])
  480. elif operation == 1: # delete
  481. if attribute not in entry: # attribute must exist
  482. result_code = RESULT_NO_SUCH_ATTRIBUTE
  483. message = 'attribute must exists for deleting its values'
  484. elif attribute in rdns: # attribute can't be used in dn
  485. result_code = 67
  486. message = 'cannot delete an rdn'
  487. else:
  488. if not elements: # deletes whole attribute if element list is empty
  489. del entry[attribute]
  490. else:
  491. for element in elements:
  492. raw_element = to_raw(element)
  493. if self.equal(dn, attribute, raw_element): # removes single element
  494. entry[attribute].remove(raw_element)
  495. else:
  496. result_code = 1
  497. message = 'value to delete not found'
  498. if not entry[attribute]: # removes the whole attribute if no elements remained
  499. del entry[attribute]
  500. elif operation == 2: # replace
  501. if attribute not in entry and elements: # attribute not present, creates the new attribute and add elements
  502. 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
  503. result_code = 19
  504. message = 'attribute is single-valued'
  505. else:
  506. entry[attribute] = [to_raw(element) for element in elements]
  507. elif not elements and attribute in rdns: # attribute can't be used in dn
  508. result_code = 67
  509. message = 'cannot replace an rdn'
  510. elif not elements: # deletes whole attribute if element list is empty
  511. if attribute in entry:
  512. del entry[attribute]
  513. else: # substitutes elements
  514. entry[attribute] = [to_raw(element) for element in elements]
  515. if result_code: # an error has happened, restores the original dn
  516. self.connection.server.dit[dn] = original_entry
  517. else:
  518. result_code = RESULT_NO_SUCH_OBJECT
  519. message = 'object not found'
  520. return {'resultCode': result_code,
  521. 'matchedDN': '',
  522. 'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
  523. 'referral': None
  524. }
  525. def mock_search(self, request_message, controls):
  526. # SearchRequest ::= [APPLICATION 3] SEQUENCE {
  527. # baseObject LDAPDN,
  528. # scope ENUMERATED {
  529. # baseObject (0),
  530. # singleLevel (1),
  531. # wholeSubtree (2),
  532. # ... },
  533. # derefAliases ENUMERATED {
  534. # neverDerefAliases (0),
  535. # derefInSearching (1),
  536. # derefFindingBaseObj (2),
  537. # derefAlways (3) },
  538. # sizeLimit INTEGER (0 .. maxInt),
  539. # timeLimit INTEGER (0 .. maxInt),
  540. # typesOnly BOOLEAN,
  541. # filter Filter,
  542. # attributes AttributeSelection }
  543. #
  544. # SearchResultEntry ::= [APPLICATION 4] SEQUENCE {
  545. # objectName LDAPDN,
  546. # attributes PartialAttributeList }
  547. #
  548. #
  549. # SearchResultReference ::= [APPLICATION 19] SEQUENCE
  550. # SIZE (1..MAX) OF uri URI
  551. #
  552. # SearchResultDone ::= [APPLICATION 5] LDAPResult
  553. #
  554. # request: base, scope, dereferenceAlias, sizeLimit, timeLimit, typesOnly, filter, attributes
  555. # response_entry: object, attributes
  556. # response_done: LDAPResult
  557. request = search_request_to_dict(request_message)
  558. if controls:
  559. decoded_controls = [self.decode_control(control) for control in controls if control]
  560. for decoded_control in decoded_controls:
  561. if decoded_control[1]['criticality'] and decoded_control[0] not in SEARCH_CONTROLS:
  562. message = 'Critical requested control ' + str(decoded_control[0]) + ' not available'
  563. result = {'resultCode': RESULT_UNAVAILABLE_CRITICAL_EXTENSION,
  564. 'matchedDN': '',
  565. 'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
  566. 'referral': None
  567. }
  568. return [], result
  569. elif decoded_control[0] == '1.2.840.113556.1.4.319': # Simple paged search
  570. if not decoded_control[1]['value']['cookie']: # new paged search
  571. response, result = self._execute_search(request)
  572. if result['resultCode'] == RESULT_SUCCESS: # success
  573. paged_set = PagedSearchSet(response, int(decoded_control[1]['value']['size']), decoded_control[1]['criticality'])
  574. response, result = paged_set.next()
  575. if paged_set.done: # paged search already completed, no need to store the set
  576. del paged_set
  577. else:
  578. self._paged_sets.append(paged_set)
  579. return response, result
  580. else:
  581. return [], result
  582. else:
  583. for paged_set in self._paged_sets:
  584. if paged_set.cookie == decoded_control[1]['value']['cookie']: # existing paged set
  585. response, result = paged_set.next() # returns next bunch of entries as per paged set specifications
  586. if paged_set.done:
  587. self._paged_sets.remove(paged_set)
  588. return response, result
  589. # paged set not found
  590. message = 'Invalid cookie in simple paged search'
  591. result = {'resultCode': RESULT_OPERATIONS_ERROR,
  592. 'matchedDN': '',
  593. 'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
  594. 'referral': None
  595. }
  596. return [], result
  597. else:
  598. return self._execute_search(request)
  599. def _execute_search(self, request):
  600. responses = []
  601. base = safe_dn(request['base'])
  602. scope = request['scope']
  603. attributes = request['attributes']
  604. if '+' in attributes: # operational attributes requested
  605. attributes.extend(self.operational_attributes)
  606. attributes.remove('+')
  607. attributes = [attr.lower() for attr in request['attributes']]
  608. 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)
  609. candidates = []
  610. if scope == 0: # base object
  611. if base in self.connection.server.dit or base.lower() == 'cn=schema':
  612. candidates.append(base)
  613. elif scope == 1: # single level
  614. for entry in self.connection.server.dit:
  615. if entry.lower().endswith(base.lower()) and ',' not in entry[:-len(base) - 1]: # only leafs without commas in the remaining dn
  616. candidates.append(entry)
  617. elif scope == 2: # whole subtree
  618. for entry in self.connection.server.dit:
  619. if entry.lower().endswith(base.lower()):
  620. candidates.append(entry)
  621. if not candidates: # incorrect base
  622. result_code = RESULT_NO_SUCH_OBJECT
  623. message = 'incorrect base object'
  624. else:
  625. matched = self.evaluate_filter_node(filter_root, candidates)
  626. if self.connection.raise_exceptions and 0 < request['sizeLimit'] < len(matched):
  627. result_code = 4
  628. message = 'size limit exceeded'
  629. else:
  630. for match in matched:
  631. responses.append({
  632. 'object': match,
  633. 'attributes': [{'type': attribute,
  634. 'vals': [] if request['typesOnly'] else self.connection.server.dit[match][attribute]}
  635. for attribute in self.connection.server.dit[match]
  636. if attribute.lower() in attributes or ALL_ATTRIBUTES in attributes]
  637. })
  638. result_code = 0
  639. message = ''
  640. result = {'resultCode': result_code,
  641. 'matchedDN': '',
  642. 'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
  643. 'referral': None
  644. }
  645. return responses[:request['sizeLimit']] if request['sizeLimit'] > 0 else responses, result
  646. def mock_extended(self, request_message, controls):
  647. # ExtendedRequest ::= [APPLICATION 23] SEQUENCE {
  648. # requestName [0] LDAPOID,
  649. # requestValue [1] OCTET STRING OPTIONAL }
  650. #
  651. # ExtendedResponse ::= [APPLICATION 24] SEQUENCE {
  652. # COMPONENTS OF LDAPResult,
  653. # responseName [10] LDAPOID OPTIONAL,
  654. # responseValue [11] OCTET STRING OPTIONAL }
  655. #
  656. # IntermediateResponse ::= [APPLICATION 25] SEQUENCE {
  657. # responseName [0] LDAPOID OPTIONAL,
  658. # responseValue [1] OCTET STRING OPTIONAL }
  659. request = extended_request_to_dict(request_message)
  660. result_code = RESULT_UNWILLING_TO_PERFORM
  661. message = 'not implemented'
  662. response_name = None
  663. response_value = None
  664. if self.connection.server.info:
  665. for extension in self.connection.server.info.supported_extensions:
  666. if request['name'] == extension[0]: # server can answer the extended request
  667. if extension[0] == '2.16.840.1.113719.1.27.100.31': # getBindDNRequest [NOVELL]
  668. result_code = 0
  669. message = ''
  670. response_name = '2.16.840.1.113719.1.27.100.32' # getBindDNResponse [NOVELL]
  671. response_value = OctetString(self.bound)
  672. elif extension[0] == '1.3.6.1.4.1.4203.1.11.3': # WhoAmI [RFC4532]
  673. result_code = 0
  674. message = ''
  675. response_name = '1.3.6.1.4.1.4203.1.11.3' # WhoAmI [RFC4532]
  676. response_value = OctetString(self.bound)
  677. break
  678. return {'resultCode': result_code,
  679. 'matchedDN': '',
  680. 'diagnosticMessage': to_unicode(message, SERVER_ENCODING),
  681. 'referral': None,
  682. 'responseName': response_name,
  683. 'responseValue': response_value
  684. }
  685. def evaluate_filter_node(self, node, candidates):
  686. """After evaluation each 2 sets are added to each MATCH node, one for the matched object and one for unmatched object.
  687. The unmatched object set is needed if a superior node is a NOT that reverts the evaluation. The BOOLEAN nodes mix the sets
  688. returned by the MATCH nodes"""
  689. node.matched = set()
  690. node.unmatched = set()
  691. if node.elements:
  692. for element in node.elements:
  693. self.evaluate_filter_node(element, candidates)
  694. if node.tag == ROOT:
  695. return node.elements[0].matched
  696. elif node.tag == AND:
  697. first_element = node.elements[0]
  698. node.matched.update(first_element.matched)
  699. node.unmatched.update(first_element.unmatched)
  700. for element in node.elements[1:]:
  701. node.matched.intersection_update(element.matched)
  702. node.unmatched.intersection_update(element.unmatched)
  703. elif node.tag == OR:
  704. for element in node.elements:
  705. node.matched.update(element.matched)
  706. node.unmatched.update(element.unmatched)
  707. elif node.tag == NOT:
  708. node.matched = node.elements[0].unmatched
  709. node.unmatched = node.elements[0].matched
  710. elif node.tag == MATCH_GREATER_OR_EQUAL:
  711. attr_name = node.assertion['attr']
  712. attr_value = node.assertion['value']
  713. for candidate in candidates:
  714. if attr_name in self.connection.server.dit[candidate]:
  715. for value in self.connection.server.dit[candidate][attr_name]:
  716. if value.isdigit() and attr_value.isdigit(): # int comparison
  717. if int(value) >= int(attr_value):
  718. node.matched.add(candidate)
  719. else:
  720. node.unmatched.add(candidate)
  721. else:
  722. if to_unicode(value, SERVER_ENCODING).lower() >= to_unicode(attr_value, SERVER_ENCODING).lower(): # case insensitive string comparison
  723. node.matched.add(candidate)
  724. else:
  725. node.unmatched.add(candidate)
  726. elif node.tag == MATCH_LESS_OR_EQUAL:
  727. attr_name = node.assertion['attr']
  728. attr_value = node.assertion['value']
  729. for candidate in candidates:
  730. if attr_name in self.connection.server.dit[candidate]:
  731. for value in self.connection.server.dit[candidate][attr_name]:
  732. if value.isdigit() and attr_value.isdigit(): # int comparison
  733. if int(value) <= int(attr_value):
  734. node.matched.add(candidate)
  735. else:
  736. node.unmatched.add(candidate)
  737. else:
  738. if to_unicode(value, SERVER_ENCODING).lower() <= to_unicode(attr_value, SERVER_ENCODING).lower(): # case insentive string comparison
  739. node.matched.add(candidate)
  740. else:
  741. node.unmatched.add(candidate)
  742. elif node.tag == MATCH_EXTENSIBLE:
  743. self.connection.last_error = 'Extensible match not allowed in Mock strategy'
  744. if log_enabled(ERROR):
  745. log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
  746. raise LDAPDefinitionError(self.connection.last_error)
  747. elif node.tag == MATCH_PRESENT:
  748. attr_name = node.assertion['attr']
  749. for candidate in candidates:
  750. if attr_name in self.connection.server.dit[candidate]:
  751. node.matched.add(candidate)
  752. else:
  753. node.unmatched.add(candidate)
  754. elif node.tag == MATCH_SUBSTRING:
  755. attr_name = node.assertion['attr']
  756. # rebuild the original substring filter
  757. if 'initial' in node.assertion and node.assertion['initial'] is not None:
  758. substring_filter = re.escape(to_unicode(node.assertion['initial'], SERVER_ENCODING))
  759. else:
  760. substring_filter = ''
  761. if 'any' in node.assertion and node.assertion['any'] is not None:
  762. for middle in node.assertion['any']:
  763. substring_filter += '.*' + re.escape(to_unicode(middle, SERVER_ENCODING))
  764. if 'final' in node.assertion and node.assertion['final'] is not None:
  765. substring_filter += '.*' + re.escape(to_unicode(node.assertion['final'], SERVER_ENCODING))
  766. if substring_filter and not node.assertion.get('any', None) and not node.assertion.get('final', None): # only initial, adds .*
  767. substring_filter += '.*'
  768. regex_filter = re.compile(substring_filter, flags=re.UNICODE | re.IGNORECASE) # unicode AND ignorecase
  769. for candidate in candidates:
  770. if attr_name in self.connection.server.dit[candidate]:
  771. for value in self.connection.server.dit[candidate][attr_name]:
  772. if regex_filter.match(to_unicode(value, SERVER_ENCODING)):
  773. node.matched.add(candidate)
  774. else:
  775. node.unmatched.add(candidate)
  776. else:
  777. node.unmatched.add(candidate)
  778. elif node.tag == MATCH_EQUAL or node.tag == MATCH_APPROX:
  779. attr_name = node.assertion['attr']
  780. attr_value = node.assertion['value']
  781. for candidate in candidates:
  782. # if attr_name in self.connection.server.dit[candidate] and attr_value in self.connection.server.dit[candidate][attr_name]:
  783. if attr_name in self.connection.server.dit[candidate] and self.equal(candidate, attr_name, attr_value):
  784. node.matched.add(candidate)
  785. else:
  786. node.unmatched.add(candidate)
  787. def equal(self, dn, attribute_type, value_to_check):
  788. # value is the value to match
  789. attribute_values = self.connection.server.dit[dn][attribute_type]
  790. if not isinstance(attribute_values, SEQUENCE_TYPES):
  791. attribute_values = [attribute_values]
  792. for attribute_value in attribute_values:
  793. if self._check_equality(value_to_check, attribute_value):
  794. return True
  795. if self._check_equality(self._prepare_value(attribute_type, value_to_check), attribute_value):
  796. return True
  797. return False
  798. @staticmethod
  799. def _check_equality(value1, value2):
  800. if value1 == value2: # exact matching
  801. return True
  802. if str(value1).isdigit() and str(value2).isdigit():
  803. if int(value1) == int(value2): # int comparison
  804. return True
  805. try:
  806. if to_unicode(value1, SERVER_ENCODING).lower() == to_unicode(value2, SERVER_ENCODING).lower(): # case insensitive comparison
  807. return True
  808. except UnicodeError:
  809. pass
  810. return False
  811. def send(self, message_type, request, controls=None):
  812. self.connection.request = self.decode_request(message_type, request, controls)
  813. if self.connection.listening:
  814. message_id = self.connection.server.next_message_id()
  815. if self.connection.usage: # ldap message is built for updating metrics only
  816. ldap_message = LDAPMessage()
  817. ldap_message['messageID'] = MessageID(message_id)
  818. ldap_message['protocolOp'] = ProtocolOp().setComponentByName(message_type, request)
  819. message_controls = build_controls_list(controls)
  820. if message_controls is not None:
  821. ldap_message['controls'] = message_controls
  822. asn1_request = BaseStrategy.decode_request(message_type, request, controls)
  823. self.connection._usage.update_transmitted_message(asn1_request, len(encode(ldap_message)))
  824. return message_id, message_type, request, controls
  825. else:
  826. self.connection.last_error = 'unable to send message, connection is not open'
  827. if log_enabled(ERROR):
  828. log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
  829. raise LDAPSocketOpenError(self.connection.last_error)