1# -*- coding: utf-8 -*- 2""" 3ldap0.ldapobject - wrapper classes above _libldap0.LDAPObject 4""" 5 6import sys 7import time 8import logging 9from os import strerror 10from typing import Any, Iterator, List, Optional, Tuple, Union 11 12import _libldap0 13from _libldap0 import LDAPError 14 15from .err import \ 16 PasswordPolicyChangeAfterReset, \ 17 PasswordPolicyExpirationWarning, \ 18 PasswordPolicyExpiredError 19from . import LIBLDAP_TLS_PACKAGE 20from .controls.simple import AuthorizationIdentityResponseControl 21from .err import NoUniqueEntry 22from .base import encode_list 23from .dn import DNObj 24from .res import SearchResultEntry, LDAPResult 25from .lock import LDAPLock 26from .functions import _libldap0_function_call 27from .schema.subentry import SCHEMA_ATTRS 28from .controls import decode_response_ctrls, encode_request_ctrls 29from .controls.openldap import SearchNoOpControl 30from .controls.ppolicy import PasswordPolicyControl 31from .extop import ExtendedRequest, ExtendedResponse, EXTOP_RESPONSE_REGISTRY 32from .extop.dds import RefreshRequest, RefreshResponse 33from .extop.whoami import WhoAmIRequest, WhoAmIResponse 34from .extop.passmod import PassmodRequest, PassmodResponse 35from .extop.cancel import CancelRequest 36from .sasl import SaslNoninteractiveAuth 37from .modlist import add_modlist, modify_modlist 38from .ldapurl import LDAPUrl 39from .typehints import AttrList, EntryMixed, EntryStr, RequestControls 40from .cache import Cache 41 42if __debug__: 43 # Tracing is only supported in debugging mode 44 import pprint 45 46__all__ = [ 47 'LDAPObject', 48 'LDAPObject', 49 'ReconnectLDAPObject', 50] 51 52NO_FINAL_RESULT_TYPES = { 53 _libldap0.RES_SEARCH_ENTRY, 54 _libldap0.RES_SEARCH_REFERENCE, 55 _libldap0.RES_INTERMEDIATE, 56} 57 58#----------------------------------------------------------------------- 59# the real connection classes 60#----------------------------------------------------------------------- 61 62class LDAPObject: 63 """ 64 Wrapper class around _libldap0.LDAPObject 65 """ 66 __slots__ = ( 67 '_cache', 68 '_cache_ttl', 69 'encoding', 70 '_l', 71 '_libldap0_lock', 72 '_msgid_funcs', 73 '_outstanding_requests', 74 'res_call_args', 75 'timeout', 76 '_trace_level', 77 'uri', 78 '_whoami_dn', 79 ) 80 81 def __init__( 82 self, 83 uri: str, 84 trace_level: int = 0, 85 cache_ttl: Union[int, float] = 0.0, 86 ): 87 self.encoding = 'utf-8' 88 self.res_call_args = False 89 self._trace_level = trace_level 90 if isinstance(uri, str): 91 ldap_url = LDAPUrl(uri) 92 elif isinstance(uri, LDAPUrl): 93 ldap_url = uri 94 else: 95 raise TypeError('Expected uri to be of type LDAPUrl or str, got %r' % (uri)) 96 self._libldap0_lock = LDAPLock( 97 '_libldap0_lock in %r' % self, 98 trace_level=self._trace_level, 99 ) 100 self.uri = None 101 conn_uri = ldap_url.connect_uri() 102 self._l = _libldap0_function_call(_libldap0._initialize, conn_uri.encode('ascii')) 103 self._msgid_funcs = { 104 self._l.add_ext, 105 self._l.simple_bind, 106 self._l.compare_ext, 107 self._l.delete_ext, 108 self._l.extop, 109 self._l.modify_ext, 110 self._l.rename, 111 self._l.search_ext, 112 self._l.unbind_ext, 113 } 114 self._outstanding_requests = {} 115 self.uri = conn_uri 116 self._whoami_dn = None 117 self.timeout = -1 118 self.protocol_version = _libldap0.VERSION3 119 self._cache_ttl = cache_ttl 120 self._cache = Cache(ttl=cache_ttl) 121 self.flush_cache() 122 # Do not restart connections 123 self.set_option(_libldap0.OPT_RESTART, False) 124 # Switch off automatic alias dereferencing 125 self.set_option(_libldap0.OPT_DEREF, _libldap0.DEREF_NEVER) 126 # Switch off automatic referral chasing 127 self.set_option(_libldap0.OPT_REFERRALS, False) 128 # always require full TLS server validation 129 self.set_option(_libldap0.OPT_X_TLS_REQUIRE_CERT, _libldap0.OPT_X_TLS_HARD) 130 131 @property 132 def protocol_version(self) -> int: 133 return self.get_option(_libldap0.OPT_PROTOCOL_VERSION) 134 135 @protocol_version.setter 136 def protocol_version(self, val) -> None: 137 return self.set_option(_libldap0.OPT_PROTOCOL_VERSION, val) 138 139 @property 140 def deref(self) -> int: 141 return self.get_option(_libldap0.OPT_DEREF) 142 143 @deref.setter 144 def deref(self, val) -> None: 145 return self.set_option(_libldap0.OPT_DEREF, val) 146 147 @property 148 def referrals(self) -> int: 149 return self.get_option(_libldap0.OPT_REFERRALS) 150 151 @referrals.setter 152 def referrals(self, val) -> None: 153 return self.set_option(_libldap0.OPT_REFERRALS, val) 154 155 @property 156 def timelimit(self) -> int: 157 return self.get_option(_libldap0.OPT_TIMELIMIT) 158 159 @timelimit.setter 160 def timelimit(self, val) -> None: 161 return self.set_option(_libldap0.OPT_TIMELIMIT, val) 162 163 @property 164 def sizelimit(self) -> int: 165 return self.get_option(_libldap0.OPT_SIZELIMIT) 166 167 @sizelimit.setter 168 def sizelimit(self, val) -> None: 169 return self.set_option(_libldap0.OPT_SIZELIMIT, val) 170 171 @property 172 def network_timeout(self) -> int: 173 return self.get_option(_libldap0.OPT_NETWORK_TIMEOUT) 174 175 @network_timeout.setter 176 def network_timeout(self, val) -> None: 177 return self.set_option(_libldap0.OPT_NETWORK_TIMEOUT, val) 178 179 def _ldap_call(self, func, *args, **kwargs): 180 """ 181 Wrapper method mainly for serializing calls into OpenLDAP libs 182 and trace logs 183 """ 184 with self._libldap0_lock: 185 if __debug__: 186 if self._trace_level >= 1: 187 logging.debug( 188 '%r %s - %s.%s(%s)', 189 self, 190 self.uri, 191 self.__class__.__name__, 192 func.__name__, 193 pprint.pformat((args, kwargs)), 194 exc_info=(self._trace_level >= 9), 195 ) 196 try: 197 result = func(*args, **kwargs) 198 except LDAPError as ldap_err: 199 if ( 200 hasattr(ldap_err, 'args') 201 and ldap_err.args 202 and 'info' not in ldap_err.args[0] 203 and 'errno' in ldap_err.args[0] 204 ): 205 ldap_err.args[0]['info'] = strerror(ldap_err.args[0]['errno']).encode('ascii') 206 if __debug__ and self._trace_level >= 2: 207 logging.debug( 208 '-> LDAPError - %s', 209 ldap_err, 210 ) 211 raise ldap_err 212 if __debug__ and self._trace_level >= 2 and func.__name__ != "unbind_ext": 213 diag_msg_ok = self._l.get_option(_libldap0.OPT_DIAGNOSTIC_MESSAGE) 214 if diag_msg_ok: 215 logging.debug( 216 '-> diagnosticMessage: %r', 217 diag_msg_ok, 218 ) 219 logging.debug('-> %s', pprint.pformat(result)) 220 if func in self._msgid_funcs: 221 self._outstanding_requests[result] = (func, args, kwargs) 222 return result 223 224 def __enter__(self): 225 return self 226 227 def __exit__(self, *args): 228 try: 229 self.unbind_s() 230 except LDAPError: 231 pass 232 233 def set_tls_options( 234 self, 235 cacert_filename=None, 236 client_cert_filename=None, 237 client_key_filename=None, 238 req_cert=_libldap0.OPT_X_TLS_DEMAND, 239 crl_check=_libldap0.OPT_X_TLS_CRL_NONE, 240 ): 241 """ 242 set TLS options for connection even with checking whether cert/key 243 files are readable 244 """ 245 # Set path names of TLS related files 246 for tls_option, tls_pathname in ( 247 (_libldap0.OPT_X_TLS_CACERTFILE, cacert_filename), 248 (_libldap0.OPT_X_TLS_CERTFILE, client_cert_filename), 249 (_libldap0.OPT_X_TLS_KEYFILE, client_key_filename), 250 ): 251 try: 252 if tls_pathname: 253 # Check whether option file can be read 254 with open(tls_pathname, 'rb'): 255 pass 256 self._l.set_option(tls_option, tls_pathname.encode('utf-8')) 257 except ValueError as value_error: 258 if sys.platform != 'darwin' and \ 259 str(value_error) != 'ValueError: option error': 260 raise 261 # Force server cert validation 262 self._l.set_option(_libldap0.OPT_X_TLS_REQUIRE_CERT, req_cert) 263 # CRL check 264 if LIBLDAP_TLS_PACKAGE == 'OpenSSL': 265 self._l.set_option(_libldap0.OPT_X_TLS_CRLCHECK, crl_check) 266 # this has to be the last option set to let libldap reinitialize TLS context 267 self._l.set_option(_libldap0.OPT_X_TLS_NEWCTX, 0) 268 # end of set_tls_options() 269 270 def fileno(self) -> int: 271 return self.get_option(_libldap0.OPT_DESC) 272 273 def abandon( 274 self, 275 msgid: int, 276 req_ctrls: Optional[RequestControls] = None, 277 ): 278 if msgid not in self._outstanding_requests: 279 raise _libldap0.NO_SUCH_OPERATION('Unexpected msgid value %s' % (msgid,)) 280 res = self._ldap_call( 281 self._l.abandon_ext, 282 msgid, 283 encode_request_ctrls(req_ctrls), 284 ) 285 del self._outstanding_requests[msgid] 286 return res 287 288 def cancel( 289 self, 290 cancelid: int, 291 req_ctrls: Optional[RequestControls] = None, 292 ) -> int: 293 if cancelid not in self._outstanding_requests: 294 raise _libldap0.NO_SUCH_OPERATION('Unexpected cancelid value %s' % (cancelid,)) 295 return self.extop( 296 CancelRequest(cancelid), 297 req_ctrls=req_ctrls, 298 ) 299 300 def cancel_s( 301 self, 302 cancelid: int, 303 req_ctrls: Optional[RequestControls] = None, 304 ) -> LDAPResult: 305 try: 306 msgid = self.cancel(cancelid, req_ctrls=req_ctrls) 307 res = self.result(msgid, _libldap0.MSG_ALL, self.timeout) 308 assert res.rtype == _libldap0.RES_EXTENDED, ValueError( 309 'Wrong result type %d' % (res.rtype,) 310 ) 311 return res 312 except _libldap0.CANCELLED: 313 pass 314 del self._outstanding_requests[cancelid] 315 316 def add( 317 self, 318 dn: str, 319 entry: EntryMixed, 320 req_ctrls: Optional[RequestControls] = None, 321 ) -> int: 322 self.uncache(dn) 323 entry = add_modlist(entry) 324 return self._ldap_call( 325 self._l.add_ext, 326 dn.encode(self.encoding), 327 entry, 328 encode_request_ctrls(req_ctrls), 329 ) 330 331 def add_s( 332 self, 333 dn: str, 334 entry, 335 req_ctrls: Optional[RequestControls] = None, 336 ) -> LDAPResult: 337 msgid = self.add(dn, entry, req_ctrls) 338 res = self.result(msgid, _libldap0.MSG_ALL, self.timeout) 339 assert res.rtype == _libldap0.RES_ADD, ValueError( 340 'Wrong result type %d' % (res.rtype,) 341 ) 342 return res 343 344 def simple_bind( 345 self, 346 who: str = '', 347 cred: bytes = b'', 348 req_ctrls: Optional[RequestControls] = None, 349 ) -> int: 350 self.flush_cache() 351 return self._ldap_call( 352 self._l.simple_bind, 353 who.encode(self.encoding), 354 cred, 355 encode_request_ctrls(req_ctrls), 356 ) 357 358 def _handle_authzid_ctrl(self, bind_ctrls): 359 authz_id_ctrls = [ 360 ctrl 361 for ctrl in bind_ctrls 362 if ctrl.controlType == AuthorizationIdentityResponseControl.controlType 363 ] 364 if authz_id_ctrls and len(authz_id_ctrls) == 1: 365 authz_id = authz_id_ctrls[0].authzId.decode('utf-8') 366 if authz_id.startswith('dn:'): 367 self._whoami_dn = authz_id[3:] 368 369 def simple_bind_s( 370 self, 371 who: str = '', 372 cred: bytes = b'', 373 req_ctrls: Optional[RequestControls] = None, 374 ) -> LDAPResult: 375 msgid = self.simple_bind(who, cred, req_ctrls) 376 res = self.result(msgid, _libldap0.MSG_ALL, self.timeout) 377 assert res.rtype == _libldap0.RES_BIND, ValueError('Wrong result type %d' % (res.rtype,)) 378 if res.ctrls: 379 self._handle_authzid_ctrl(res.ctrls) 380 # Extract the password policy response control and raise 381 # appropriate exceptions if needed 382 ppolicy_ctrls = [ 383 c 384 for c in res.ctrls 385 if c.controlType == PasswordPolicyControl.controlType 386 ] 387 if ppolicy_ctrls and len(ppolicy_ctrls) == 1: 388 ppolicy_ctrl = ppolicy_ctrls[0] 389 if ppolicy_ctrl.error == 2: 390 raise PasswordPolicyChangeAfterReset( 391 who=who, 392 desc='Password change is needed after reset!', 393 ) 394 if ppolicy_ctrl.timeBeforeExpiration is not None: 395 raise PasswordPolicyExpirationWarning( 396 who=who, 397 desc='Password will expire in %d seconds!' % ( 398 ppolicy_ctrl.timeBeforeExpiration 399 ), 400 timeBeforeExpiration=ppolicy_ctrl.timeBeforeExpiration, 401 ) 402 if ppolicy_ctrl.graceAuthNsRemaining is not None: 403 raise PasswordPolicyExpiredError( 404 who=who, 405 desc='Password expired! %d grace logins left.' % ( 406 ppolicy_ctrl.graceAuthNsRemaining 407 ), 408 graceAuthNsRemaining=ppolicy_ctrl.graceAuthNsRemaining, 409 ) 410 return res 411 412 def sasl_interactive_bind_s( 413 self, 414 sasl_mech: str, 415 auth, 416 req_ctrls: Optional[RequestControls] = None, 417 sasl_flags=_libldap0.SASL_QUIET 418 ): 419 self.flush_cache() 420 return self._ldap_call( 421 self._l.sasl_interactive_bind_s, 422 sasl_mech.encode('ascii'), 423 auth, 424 encode_request_ctrls(req_ctrls), 425 sasl_flags 426 ) 427 428 def sasl_non_interactive_bind_s( 429 self, 430 sasl_mech: str, 431 req_ctrls: Optional[RequestControls] = None, 432 sasl_flags=_libldap0.SASL_QUIET, 433 authz_id: str = '' 434 ): 435 if not authz_id: 436 # short-cut allows really non-interactive SASL bind 437 # without the flaky C call-backs 438 return self.sasl_bind_s(sasl_mech, b'') 439 auth = SaslNoninteractiveAuth( 440 authz_id=authz_id, 441 trace_level=self._trace_level, 442 ) 443 return self.sasl_interactive_bind_s(sasl_mech, auth, req_ctrls, sasl_flags) 444 445 def sasl_bind_s( 446 self, 447 sasl_mech: str, 448 cred: bytes, 449 req_ctrls: Optional[RequestControls] = None, 450 ) -> LDAPResult: 451 self.flush_cache() 452 return self._ldap_call( 453 self._l.sasl_bind_s, 454 sasl_mech.encode('ascii'), 455 cred, 456 encode_request_ctrls(req_ctrls), 457 ) 458 459 def compare( 460 self, 461 dn: str, 462 attr: str, 463 value: bytes, 464 req_ctrls: Optional[RequestControls] = None, 465 ) -> int: 466 return self._ldap_call( 467 self._l.compare_ext, 468 dn.encode(self.encoding), 469 attr.encode('ascii'), 470 value, 471 encode_request_ctrls(req_ctrls), 472 ) 473 474 def compare_s( 475 self, 476 dn: str, 477 attr: str, 478 value: bytes, 479 req_ctrls: Optional[RequestControls] = None, 480 ) -> bool: 481 msgid = self.compare(dn, attr, value, req_ctrls) 482 try: 483 res = self.result(msgid, _libldap0.MSG_ALL, self.timeout) 484 except _libldap0.COMPARE_TRUE: 485 return True 486 except _libldap0.COMPARE_FALSE: 487 return False 488 raise _libldap0.PROTOCOL_ERROR( 489 'Compare operation returned wrong result: %r' % (res,) 490 ) 491 492 def delete( 493 self, 494 dn: str, 495 req_ctrls: Optional[RequestControls] = None, 496 ) -> int: 497 self.uncache(dn) 498 return self._ldap_call( 499 self._l.delete_ext, 500 dn.encode(self.encoding), 501 encode_request_ctrls(req_ctrls), 502 ) 503 504 def delete_s( 505 self, 506 dn: str, 507 req_ctrls: Optional[RequestControls] = None, 508 ) -> LDAPResult: 509 msgid = self.delete(dn, req_ctrls) 510 res = self.result(msgid, _libldap0.MSG_ALL, self.timeout) 511 assert res.rtype == _libldap0.RES_DELETE, ValueError('Wrong result type %d' % (res.rtype,)) 512 return res 513 514 def extop( 515 self, 516 extreq: ExtendedRequest, 517 req_ctrls: Optional[RequestControls] = None, 518 ) -> int: 519 extop_req_value = extreq.encode() 520 assert extop_req_value is None or isinstance(extop_req_value, bytes), TypeError( 521 'Expected extop_req_value to be bytes or None, got %r' % (extop_req_value,) 522 ) 523 return self._ldap_call( 524 self._l.extop, 525 extreq.requestName.encode('ascii'), 526 extop_req_value, 527 encode_request_ctrls(req_ctrls), 528 ) 529 530 def extop_result( 531 self, 532 msgid: int = _libldap0.RES_ANY, 533 extop_resp_class: Optional[Any] = None, 534 timeout: Union[int, float] = -1, 535 ) -> ExtendedResponse: 536 _, _, _, _, respoid, respvalue = self._ldap_call( 537 self._l.result, 538 msgid, 539 1, 540 max(self.timeout, timeout), 541 True, 542 False, 543 True, 544 ) 545 if extop_resp_class is None: 546 extop_resp_class = EXTOP_RESPONSE_REGISTRY.get(respoid) 547 if extop_resp_class is None: 548 extop_response = ExtendedResponse(encodedResponseValue=respvalue) 549 else: 550 if not extop_resp_class.check_resp_name( 551 None if respoid is None else respoid.decode('ascii') 552 ): 553 raise _libldap0.PROTOCOL_ERROR( 554 "Wrong OID in extended response! Expected %s, got %s" % ( 555 extop_resp_class.responseName, 556 respoid 557 ) 558 ) 559 extop_response = extop_resp_class(encodedResponseValue=respvalue) 560 if __debug__ and self._trace_level >= 2: 561 logging.debug('%s.extop_result(): %r', self.__class__.__name__, extop_response) 562 return extop_response 563 564 def extop_s( 565 self, 566 extreq: ExtendedRequest, 567 req_ctrls: Optional[RequestControls] = None, 568 extop_resp_class: Optional[Any] = None, 569 ) -> ExtendedResponse: 570 msgid = self.extop(extreq, req_ctrls) 571 return self.extop_result(msgid, extop_resp_class=extop_resp_class) 572 573 def modify( 574 self, 575 dn: str, 576 modlist, 577 req_ctrls: Optional[RequestControls] = None, 578 ) -> int: 579 self.uncache(dn) 580 return self._ldap_call( 581 self._l.modify_ext, 582 dn.encode(self.encoding), 583 modlist, 584 encode_request_ctrls(req_ctrls), 585 ) 586 587 def modify_s( 588 self, 589 dn: str, 590 modlist, 591 req_ctrls: Optional[RequestControls] = None, 592 ) -> LDAPResult: 593 msgid = self.modify(dn, modlist, req_ctrls) 594 res = self.result(msgid, _libldap0.MSG_ALL, self.timeout) 595 assert res.rtype == _libldap0.RES_MODIFY, ValueError('Wrong result type %d' % (res.rtype,)) 596 return res 597 598 def ensure_entry( 599 self, 600 dn: Optional[str] = None, 601 entry: Optional[dict] = None, 602 old_base: Optional[str] = None, 603 old_filter: Optional[str] = None, 604 old_attrs: Optional[AttrList] = None, 605 old_entry: Optional[EntryMixed] = None, 606 req_ctrls: Optional[RequestControls] = None, 607 del_ignore: bool = True, 608 ) -> List[LDAPResult]: 609 """ 610 Ensure existence or absence of a single LDAP entry 611 """ 612 if entry is None: 613 # ensure LDAP entry is absent 614 if dn is not None: 615 del_dn = dn 616 elif old_base is not None: 617 old_res = self.find_unique_entry( 618 old_base, 619 scope=_libldap0.SCOPE_SUBTREE, 620 filterstr=old_filter or '(objectClass=*)', 621 attrlist=['1.1'] 622 ) 623 del_dn = old_res.dn_s 624 else: 625 raise ValueError('Expected either dn or old_base to be not None!') 626 res = [] 627 try: 628 res.append(self.delete_s(del_dn, req_ctrls=req_ctrls)) 629 except _libldap0.NO_SUCH_OBJECT as ldap_err: 630 if not del_ignore: 631 raise ldap_err 632 return res 633 # first try to read/search old entry 634 if dn is None: 635 raise ValueError('Parameter dn must not be None!') 636 if old_entry is not None: 637 # don't read old entry 638 old_dn = dn 639 elif old_filter is None: 640 try: 641 old_res = self.read_s(dn, attrlist=old_attrs) 642 except _libldap0.NO_SUCH_OBJECT: 643 old_dn = None 644 else: 645 old_dn = dn 646 old_entry = old_res.entry_as 647 else: 648 # forced to search for the old entry 649 if old_base is None: 650 old_base = dn 651 try: 652 old_res = self.find_unique_entry( 653 old_base, 654 scope=_libldap0.SCOPE_SUBTREE, 655 filterstr=old_filter, 656 attrlist=old_attrs, 657 ) 658 except NoUniqueEntry: 659 old_dn = None 660 else: 661 old_dn = old_res.dn_s 662 old_entry = old_res.entry_as 663 if old_dn is None: 664 # new entry has to be added 665 return [self.add_s(dn, entry, req_ctrls=req_ctrls)] 666 # existing entry has to be renamed and/or modified 667 res = [] 668 if old_dn != dn: 669 # rename/move existing entry 670 dn_o = DNObj.from_str(dn) 671 res.append( 672 self.rename_s( 673 old_dn, 674 str(dn_o.rdn()), 675 newsuperior=str(dn_o.parent()), 676 delold=True, 677 req_ctrls=req_ctrls, 678 ) 679 ) 680 # re-read entry because characteristic attribute might have changed 681 old_entry = self.read_s(dn, attrlist=old_attrs).entry_as 682 mod_list = modify_modlist(old_entry, entry) 683 if mod_list: 684 # finally really modify the existing entry 685 res.append(self.modify_s(dn, mod_list, req_ctrls=req_ctrls)) 686 return res 687 688 def passwd( 689 self, 690 user: str = None, 691 oldpw: bytes = None, 692 newpw: bytes = None, 693 req_ctrls: Optional[RequestControls] = None, 694 ) -> int: 695 self.uncache(user) 696 return self.extop( 697 PassmodRequest( 698 userIdentity=user, 699 oldPasswd=oldpw, 700 newPasswd=newpw, 701 ), 702 req_ctrls=req_ctrls, 703 ) 704 705 def passwd_s( 706 self, 707 user: str = None, 708 oldpw: bytes = None, 709 newpw: bytes = None, 710 req_ctrls: Optional[RequestControls] = None, 711 ) -> Union[bytes, None]: 712 msgid = self.passwd( 713 user=user, 714 oldpw=oldpw, 715 newpw=newpw, 716 req_ctrls=req_ctrls, 717 ) 718 res = self.extop_result(msgid, extop_resp_class=PassmodResponse) 719 if res.responseValue is None: 720 return None 721 return res.genPasswd 722 723 def rename( 724 self, 725 dn: str, 726 newrdn: str, 727 newsuperior: Optional[str] = None, 728 delold: bool = True, 729 req_ctrls: Optional[RequestControls] = None, 730 ) -> int: 731 return self._ldap_call( 732 self._l.rename, 733 dn.encode(self.encoding), 734 newrdn.encode(self.encoding), 735 newsuperior.encode(self.encoding) if newsuperior is not None else None, 736 delold, 737 encode_request_ctrls(req_ctrls), 738 ) 739 740 def rename_s( 741 self, 742 dn: str, 743 newrdn: str, 744 newsuperior: Optional[str] = None, 745 delold: bool = True, 746 req_ctrls: Optional[RequestControls] = None, 747 ) -> LDAPResult: 748 msgid = self.rename(dn, newrdn, newsuperior, delold, req_ctrls) 749 res = self.result(msgid, _libldap0.MSG_ALL, self.timeout) 750 assert res.rtype == _libldap0.RES_MODRDN, ValueError('Wrong result type %d' % (res.rtype,)) 751 return res 752 753 def result( 754 self, 755 msgid: int, 756 all_results: int = _libldap0.MSG_ALL, 757 timeout: Union[int, float] = -1, 758 add_intermediates: bool = False, 759 resp_ctrl_classes=None, 760 ) -> LDAPResult: 761 if msgid != _libldap0.RES_ANY and msgid not in self._outstanding_requests: 762 raise _libldap0.NO_SUCH_OPERATION('Unexpected msgid value %s' % (msgid,)) 763 try: 764 res_type, res_data, res_msgid, res_ctrls = self._ldap_call( 765 self._l.result, 766 msgid, 767 all_results, 768 timeout, 769 True, 770 add_intermediates, 771 False 772 ) 773 except LDAPError as ldap_err: 774 try: 775 err_ctrls = ldap_err.args[0]['ctrls'] 776 except (IndexError, KeyError): 777 ldap_err.ctrls = None 778 else: 779 ldap_err.ctrls = decode_response_ctrls(err_ctrls, resp_ctrl_classes) 780 raise ldap_err 781 if res_type is None: 782 return 783 assert msgid == _libldap0.RES_ANY or res_msgid == msgid, ValueError( 784 'Expected LDAP result with msgid %d, got %d' % (msgid, res_msgid) 785 ) 786 if self.res_call_args: 787 call_args = self._outstanding_requests.get(res_msgid, None) 788 else: 789 call_args = None 790 if res_type not in NO_FINAL_RESULT_TYPES: 791 del self._outstanding_requests[res_msgid] 792 return LDAPResult( 793 res_type, 794 [ 795 (dn, entry, decode_response_ctrls(ctrls, resp_ctrl_classes)) 796 for dn, entry, ctrls in res_data 797 ], 798 res_msgid, 799 decode_response_ctrls(res_ctrls, resp_ctrl_classes), 800 call_args=call_args, 801 ) 802 803 def results( 804 self, 805 msgid: int, 806 timeout: Union[int, float] = -1, 807 add_intermediates: bool = False, 808 resp_ctrl_classes=None, 809 ) -> Iterator[LDAPResult]: 810 """ 811 Generator method which returns an iterator for processing all LDAP 812 operation results of the given msgid like retrieved with 813 LDAPObject.result() 814 """ 815 if msgid != _libldap0.RES_ANY and msgid not in self._outstanding_requests: 816 raise _libldap0.NO_SUCH_OPERATION('Unexpected msgid value %s' % (msgid,)) 817 start_time = time.time() 818 if timeout >= 0: 819 end_time = start_time + timeout 820 while timeout < 0 or time.time() <= end_time: 821 try: 822 res = self.result( 823 msgid, 824 all_results=_libldap0.MSG_ONE, 825 timeout=min(timeout, 0.5), 826 add_intermediates=add_intermediates, 827 resp_ctrl_classes=resp_ctrl_classes, 828 ) 829 except _libldap0.TIMEOUT: 830 if timeout >= 0 and time.time() > end_time: 831 raise _libldap0.TIMEOUT('Timeout of %0.4f secs reached.' % (timeout,)) 832 continue 833 yield res 834 if res.rtype not in NO_FINAL_RESULT_TYPES: 835 break 836 # end of results() 837 838 def search( 839 self, 840 base: str, 841 scope: int, 842 filterstr: str = '(objectClass=*)', 843 attrlist: Optional[AttrList] = None, 844 attrsonly: bool = False, 845 req_ctrls: Optional[RequestControls] = None, 846 timeout: Union[int, float] = -1, 847 sizelimit=0 848 ) -> int: 849 assert isinstance(base, str), TypeError( 850 'Expected str for base, got %r' % (base,) 851 ) 852 assert isinstance(filterstr, str), TypeError( 853 'Expected str for filterstr, got %r' % (filterstr,) 854 ) 855 if attrlist is not None: 856 attrlist = encode_list(attrlist, encoding='ascii') 857 else: 858 attrlist = [b'*'] 859 return self._ldap_call( 860 self._l.search_ext, 861 base.encode(self.encoding), 862 scope, 863 filterstr.encode(self.encoding), 864 attrlist, 865 attrsonly, 866 encode_request_ctrls(req_ctrls), 867 timeout, 868 sizelimit, 869 ) 870 871 def cache_hit_ratio(self) -> float: 872 """ 873 Returns percentage of cache hit ratio 874 """ 875 return self._cache.hit_ratio 876 877 def flush_cache(self) -> None: 878 """ 879 reset/flush the internal cache 880 """ 881 try: 882 cache = self._cache 883 except AttributeError: 884 pass 885 else: 886 cache.flush() 887 self._whoami_dn = None 888 889 def uncache(self, entry_dn) -> None: 890 """ 891 remove all cached entries related to dn from internal cache 892 """ 893 entry_dn = entry_dn.lower() 894 remove_keys = set() 895 # first find the items to be removed from cache 896 for cache_key_args in list(self._cache): 897 if cache_key_args[0].lower() == entry_dn: 898 remove_keys.add(cache_key_args) 899 continue 900 try: 901 cached_results = self._cache[cache_key_args] 902 except KeyError: 903 # cached items expired in between 904 pass 905 else: 906 for cached_res in cached_results: 907 if ( 908 isinstance(cached_res, SearchResultEntry) 909 and cached_res.dn_s.lower() == entry_dn 910 ): 911 remove_keys.add(cache_key_args) 912 # finally remove items from cache dict 913 for cache_key_args in remove_keys: 914 try: 915 del self._cache[cache_key_args] 916 except KeyError: 917 pass 918 # end of uncache() 919 920 def search_s( 921 self, 922 base: str, 923 scope: int, 924 filterstr: str = '(objectClass=*)', 925 attrlist: Optional[AttrList] = None, 926 attrsonly: bool = False, 927 req_ctrls: Optional[RequestControls] = None, 928 timeout: Union[int, float] = -1, 929 sizelimit=0, 930 cache_ttl: Optional[Union[int, float]] = None, 931 ) -> List[LDAPResult]: 932 if cache_ttl is None: 933 cache_ttl = self._cache._ttl 934 if cache_ttl > 0: 935 cache_key_args = ( 936 base, 937 scope, 938 filterstr, 939 tuple(attrlist or []), 940 attrsonly, 941 tuple([ 942 c.encode() 943 for c in req_ctrls or [] 944 ]), 945 timeout, 946 sizelimit, 947 ) 948 # first look into cache for non-expired search results 949 try: 950 res = self._cache[cache_key_args] 951 except KeyError: 952 pass 953 else: 954 return res 955 # no cached result 956 msgid = self.search( 957 base, 958 scope, 959 filterstr, 960 attrlist, 961 attrsonly, 962 req_ctrls, 963 timeout, 964 sizelimit 965 ) 966 res = [] 967 for ldap_res in self.results(msgid, timeout=timeout): 968 res.extend(ldap_res.rdata) 969 if cache_ttl > 0: 970 # store result in cache if caching is enabled at all 971 self._cache.cache(cache_key_args, res, cache_ttl) 972 return res 973 # search_s() 974 975 def noop_search( 976 self, 977 base: str, 978 scope: int = _libldap0.SCOPE_SUBTREE, 979 filterstr: str = '(objectClass=*)', 980 req_ctrls: Optional[RequestControls] = None, 981 timeout: Union[int, float] = -1, 982 sizelimit=0, 983 ) -> Tuple[Optional[int], Optional[int]]: 984 req_ctrls = req_ctrls or [] 985 req_ctrls.append(SearchNoOpControl(criticality=True)) 986 try: 987 msg_id = self.search( 988 base, 989 scope, 990 filterstr=filterstr, 991 attrlist=['1.1'], 992 req_ctrls=req_ctrls, 993 timeout=timeout, 994 sizelimit=sizelimit, 995 ) 996 noop_srch_res = list(self.results( 997 msg_id, 998 timeout=timeout, 999 ))[0] 1000 except ( 1001 _libldap0.TIMEOUT, 1002 _libldap0.TIMELIMIT_EXCEEDED, 1003 _libldap0.SIZELIMIT_EXCEEDED, 1004 _libldap0.ADMINLIMIT_EXCEEDED 1005 ) as err: 1006 self.abandon(msg_id) 1007 raise err 1008 else: 1009 noop_srch_ctrl = [ 1010 c 1011 for c in noop_srch_res.ctrls 1012 if c.controlType == SearchNoOpControl.controlType 1013 ] 1014 if noop_srch_ctrl: 1015 return noop_srch_ctrl[0].numSearchResults, noop_srch_ctrl[0].numSearchContinuations 1016 return (None, None) 1017 1018 def start_tls_s(self): 1019 return self._ldap_call(self._l.start_tls_s) 1020 1021 def unbind(self, req_ctrls: Optional[RequestControls] = None) -> int: 1022 res = self._ldap_call( 1023 self._l.unbind_ext, 1024 encode_request_ctrls(req_ctrls), 1025 ) 1026 try: 1027 del self._l 1028 except AttributeError: 1029 pass 1030 return res 1031 1032 def unbind_s(self, req_ctrls: Optional[RequestControls] = None) -> LDAPResult: 1033 msgid = self.unbind(req_ctrls) 1034 if msgid is None: 1035 return None 1036 res = self.result(msgid, _libldap0.MSG_ALL, self.timeout) 1037 return res 1038 1039 def whoami_s(self, req_ctrls: Optional[RequestControls] = None) -> str: 1040 wai = self.extop_s( 1041 WhoAmIRequest(), 1042 extop_resp_class=WhoAmIResponse, 1043 req_ctrls=req_ctrls, 1044 ) 1045 if wai.responseValue and wai.responseValue.lower().startswith('dn:'): 1046 # cache this for later calls to get_whoami_dn() 1047 self._whoami_dn = wai.responseValue[3:] 1048 return wai.responseValue 1049 1050 def get_option(self, option: int) -> Union[int, float, bytes]: 1051 result = self._ldap_call(self._l.get_option, option) 1052 if option == _libldap0.OPT_SERVER_CONTROLS: 1053 result = decode_response_ctrls(result) 1054 return result 1055 1056 def set_option(self, option: int, invalue: Union[int, float, bytes, RequestControls]): 1057 if option == _libldap0.OPT_SERVER_CONTROLS: 1058 invalue = encode_request_ctrls(invalue) 1059 return self._ldap_call(self._l.set_option, option, invalue) 1060 1061 def search_subschemasubentry_s(self, dn='', rootdse=True): 1062 res = None 1063 try: 1064 ldap_res = self.read_s(dn, attrlist=['subschemaSubentry']) 1065 except ( 1066 _libldap0.NO_SUCH_OBJECT, 1067 _libldap0.INSUFFICIENT_ACCESS, 1068 _libldap0.NO_SUCH_ATTRIBUTE, 1069 _libldap0.UNDEFINED_TYPE, 1070 _libldap0.REFERRAL, 1071 ): 1072 pass 1073 else: 1074 if ldap_res is not None and 'subschemaSubentry' in ldap_res.entry_s: 1075 res = ldap_res.entry_s['subschemaSubentry'][0] 1076 if res is None and dn and rootdse: 1077 # fall back reading attribute subschemaSubentry from rootDSE 1078 res = self.search_subschemasubentry_s(dn='') 1079 return res # end of search_subschemasubentry_s() 1080 1081 def read_s( 1082 self, 1083 dn: str, 1084 filterstr: str = '(objectClass=*)', 1085 attrlist: Optional[AttrList] = None, 1086 req_ctrls: Optional[RequestControls] = None, 1087 timeout: Union[int, float] = -1, 1088 cache_ttl: Optional[Union[int, float]] = None, 1089 ) -> SearchResultEntry: 1090 ldap_res = self.search_s( 1091 dn, 1092 _libldap0.SCOPE_BASE, 1093 filterstr, 1094 attrlist=attrlist, 1095 req_ctrls=req_ctrls, 1096 timeout=timeout, 1097 cache_ttl=cache_ttl, 1098 ) 1099 if not ldap_res: 1100 return None 1101 return ldap_res[0] 1102 1103 def read_subschemasubentry_s( 1104 self, 1105 subschemasubentry_dn: str, 1106 attrs: Optional[AttrList] = None, 1107 ) -> EntryStr: 1108 try: 1109 subschemasubentry = self.read_s( 1110 subschemasubentry_dn, 1111 filterstr='(objectClass=subschema)', 1112 attrlist=attrs or SCHEMA_ATTRS 1113 ) 1114 except _libldap0.NO_SUCH_OBJECT: 1115 return None 1116 if subschemasubentry is None: 1117 return None 1118 return subschemasubentry.entry_s 1119 1120 def refresh( 1121 self, 1122 dn: str, 1123 ttl: Optional[Union[str, float]] = None, 1124 ) -> int: 1125 self.uncache(dn) 1126 return self.extop(RefreshRequest(entryName=dn, requestTtl=ttl)) 1127 1128 def refresh_s( 1129 self, 1130 dn: str, 1131 ttl: Optional[Union[str, float]] = None, 1132 ) -> Union[str, float]: 1133 self.uncache(dn) 1134 msgid = self.refresh(dn, ttl=ttl) 1135 res = self.extop_result(msgid, extop_resp_class=RefreshResponse) 1136 return res.responseTtl 1137 1138 def find_unique_entry( 1139 self, 1140 base: str, 1141 scope: int = _libldap0.SCOPE_SUBTREE, 1142 filterstr: str = '(objectClass=*)', 1143 attrlist: Optional[AttrList] = None, 1144 attrsonly: bool = False, 1145 req_ctrls: Optional[RequestControls] = None, 1146 timeout: Union[int, float] = -1, 1147 cache_ttl: Optional[Union[int, float]] = None, 1148 ) -> LDAPResult: 1149 try: 1150 ldap_res = list(self.search_s( 1151 base, 1152 scope, 1153 filterstr, 1154 attrlist=attrlist, 1155 attrsonly=attrsonly, 1156 req_ctrls=req_ctrls, 1157 timeout=timeout, 1158 sizelimit=2, 1159 cache_ttl=cache_ttl, 1160 )) 1161 except _libldap0.SIZELIMIT_EXCEEDED: 1162 raise NoUniqueEntry('Size limit exceeded for %r' % (filterstr,)) 1163 # strip search continuations 1164 ldap_res = [res for res in ldap_res if isinstance(res, SearchResultEntry)] 1165 # now check again for length 1166 if not ldap_res: 1167 raise NoUniqueEntry('No search result for %r' % (filterstr,)) 1168 if len(ldap_res) > 1: 1169 raise NoUniqueEntry( 1170 'Non-unique search results (%d) for %r' % (len(ldap_res), filterstr) 1171 ) 1172 return ldap_res[0] 1173 1174 def read_rootdse_s( 1175 self, 1176 filterstr: str = '(objectClass=*)', 1177 attrlist: Optional[AttrList] = None, 1178 ) -> LDAPResult: 1179 """ 1180 convenience wrapper around read_s() for reading rootDSE 1181 """ 1182 ldap_rootdse = self.read_s( 1183 '', 1184 filterstr=filterstr, 1185 attrlist=attrlist or ['*', '+'], 1186 ) 1187 return ldap_rootdse # read_rootdse_s() 1188 1189 def get_naming_contexts(self) -> List[str]: 1190 return self.read_rootdse_s( 1191 attrlist=['namingContexts'] 1192 ).entry_s.get('namingContexts', []) 1193 1194 def get_whoami_dn(self) -> str: 1195 """ 1196 Return the result of Who Am I extended operation as plain DN 1197 """ 1198 if self._whoami_dn is None: 1199 wai = self.whoami_s() 1200 if wai == '': 1201 self._whoami_dn = wai 1202 elif wai.lower().startswith('dn:'): 1203 self._whoami_dn = wai[3:] 1204 else: 1205 self._whoami_dn = None 1206 raise ValueError('Expected dn: form of Who Am I? result, got %r' % (wai,)) 1207 return self._whoami_dn 1208 1209 1210class ReconnectLDAPObject(LDAPObject): 1211 """ 1212 In case of server failure (_libldap0.SERVER_DOWN) the implementations 1213 of all synchronous operation methods (search_s() etc.) are doing 1214 an automatic reconnect and rebind and will retry the very same 1215 operation. 1216 1217 This is very handy for broken LDAP server implementations 1218 (e.g. in Lotus Domino) which drop connections very often making 1219 it impossible to have a long-lasting control flow in the 1220 application. 1221 """ 1222 __slots__ = ( 1223 '_last_bind', 1224 '_options', 1225 '_reconnect_lock', 1226 '_reconnects_done', 1227 '_retry_delay', 1228 '_retry_max', 1229 '_start_tls', 1230 ) 1231 __transient_attrs__ = { 1232 '_cache', 1233 '_l', 1234 '_libldap0_lock', 1235 '_reconnect_lock', 1236 '_last_bind', 1237 '_outstanding_requests', 1238 '_msgid_funcs', 1239 '_whoami_dn', 1240 } 1241 1242 def __init__( 1243 self, 1244 uri: str, 1245 trace_level: int = 0, 1246 cache_ttl: Union[int, float] = 0.0, 1247 retry_max: int = 1, 1248 retry_delay: Union[int, float] = 60.0, 1249 ) -> None: 1250 """ 1251 Parameters like LDAPObject.__init__() with these 1252 additional arguments: 1253 1254 retry_max 1255 Maximum count of reconnect trials 1256 retry_delay 1257 Time span to wait between two reconnect trials 1258 """ 1259 self._options = [] 1260 self._last_bind = None 1261 LDAPObject.__init__( 1262 self, 1263 uri, 1264 trace_level=trace_level, 1265 cache_ttl=cache_ttl, 1266 ) 1267 self._reconnect_lock = LDAPLock( 1268 '_reconnect_lock in %r' % self, 1269 trace_level=self._trace_level, 1270 ) 1271 self._retry_max = retry_max 1272 self._retry_delay = retry_delay 1273 self._start_tls = 0 1274 self._reconnects_done = 0 1275 1276 def __getstate__(self) -> dict: 1277 """ 1278 return data representation for pickled object 1279 """ 1280 state = {} 1281 for key in self.__slots__ + LDAPObject.__slots__: 1282 if key not in self.__transient_attrs__: 1283 state[key] = getattr(self, key) 1284 state['_last_bind'] = ( 1285 self._last_bind[0].__name__, 1286 self._last_bind[1], 1287 self._last_bind[2], 1288 ) 1289 return state 1290 1291 def __setstate__(self, data) -> None: 1292 """ 1293 set up the object from pickled data 1294 """ 1295 for key, val in data.items(): 1296 setattr(self, key, val) 1297 self._whoami_dn = None 1298 self._cache = Cache(self._cache_ttl) 1299 self._last_bind = ( 1300 getattr(LDAPObject, self._last_bind[0]), 1301 self._last_bind[1], 1302 self._last_bind[2], 1303 ) 1304 self._libldap0_lock = LDAPLock( 1305 '_libldap0_lock in %r' % self, 1306 trace_level=self._trace_level, 1307 ) 1308 self._reconnect_lock = LDAPLock( 1309 '_reconnect_lock in %r' % self, 1310 trace_level=self._trace_level, 1311 ) 1312 self.reconnect(self.uri) 1313 1314 def _store_last_bind(self, method, *args, **kwargs) -> None: 1315 self._last_bind = (method, args, kwargs) 1316 1317 def _apply_last_bind(self) -> None: 1318 if self._last_bind is not None: 1319 func, args, kwargs = self._last_bind 1320 func(self, *args, **kwargs) 1321 else: 1322 # Send explicit anon simple bind request to provoke 1323 # _libldap0.SERVER_DOWN in method reconnect() 1324 LDAPObject.simple_bind_s(self, '', b'') 1325 1326 def _restore_options(self) -> None: 1327 """ 1328 Restore all recorded options 1329 """ 1330 for key, val in self._options: 1331 LDAPObject.set_option(self, key, val) 1332 1333 def passwd_s(self, *args, **kwargs): 1334 return self._apply_method_s(LDAPObject.passwd_s, *args, **kwargs) 1335 1336 def reconnect( 1337 self, 1338 uri: str, 1339 retry_max: int = 1, 1340 retry_delay: Union[int, float] = 60.0, 1341 reset_last_bind: bool = False, 1342 ) -> None: 1343 """ 1344 Drop and clean up old connection completely and reconnect 1345 """ 1346 self._reconnect_lock.acquire() 1347 if reset_last_bind: 1348 self._last_bind = None 1349 try: 1350 reconnect_counter = retry_max 1351 while reconnect_counter: 1352 counter_text = '%d. (of %d)' % (retry_max-reconnect_counter+1, retry_max) 1353 if __debug__ and self._trace_level >= 1: 1354 logging.debug( 1355 'Trying %s reconnect to %s...\n', 1356 counter_text, uri, 1357 ) 1358 try: 1359 # Do the connect 1360 self._l = _libldap0_function_call( 1361 _libldap0._initialize, 1362 uri.encode(self.encoding), 1363 ) 1364 self._msgid_funcs = { 1365 self._l.add_ext, 1366 self._l.simple_bind, 1367 self._l.compare_ext, 1368 self._l.delete_ext, 1369 self._l.extop, 1370 self._l.modify_ext, 1371 self._l.rename, 1372 self._l.search_ext, 1373 self._l.unbind_ext, 1374 } 1375 self._outstanding_requests = {} 1376 self._restore_options() 1377 # StartTLS extended operation in case this was called before 1378 if self._start_tls: 1379 LDAPObject.start_tls_s(self) 1380 # Repeat last simple or SASL bind 1381 self._apply_last_bind() 1382 except (_libldap0.SERVER_DOWN, _libldap0.TIMEOUT) as ldap_error: 1383 if __debug__ and self._trace_level >= 1: 1384 logging.debug( 1385 '%s reconnect to %s failed\n', 1386 counter_text, uri 1387 ) 1388 reconnect_counter -= 1 1389 if not reconnect_counter: 1390 raise ldap_error 1391 if __debug__ and self._trace_level >= 1: 1392 logging.debug('=> delay %s...\n', retry_delay) 1393 time.sleep(retry_delay) 1394 LDAPObject.unbind_s(self) 1395 else: 1396 if __debug__ and self._trace_level >= 1: 1397 logging.debug( 1398 '%s reconnect to %s successful => repeat last operation\n', 1399 counter_text, 1400 uri, 1401 ) 1402 self._reconnects_done += 1 1403 break 1404 finally: 1405 self._reconnect_lock.release() 1406 # end of reconnect() 1407 1408 def _apply_method_s(self, func, *args, **kwargs): 1409 if not hasattr(self, '_l'): 1410 self.reconnect(self.uri, retry_max=self._retry_max, retry_delay=self._retry_delay) 1411 try: 1412 return func(self, *args, **kwargs) 1413 except _libldap0.SERVER_DOWN: 1414 if self._retry_max <= 0: 1415 raise 1416 LDAPObject.unbind_s(self) 1417 # Try to reconnect 1418 self.reconnect(self.uri, retry_max=self._retry_max, retry_delay=self._retry_delay) 1419 # Re-try last operation 1420 return func(self, *args, **kwargs) 1421 1422 def set_option(self, option: int, invalue: Union[int, float, bytes, RequestControls]): 1423 res = LDAPObject.set_option(self, option, invalue) 1424 self._options.append((option, invalue)) 1425 return res 1426 1427 def simple_bind_s(self, *args, **kwargs): 1428 res = self._apply_method_s(LDAPObject.simple_bind_s, *args, **kwargs) 1429 self._store_last_bind(LDAPObject.simple_bind_s, *args, **kwargs) 1430 return res 1431 1432 def start_tls_s(self, *args, **kwargs): 1433 res = self._apply_method_s(LDAPObject.start_tls_s, *args, **kwargs) 1434 self._start_tls = 1 1435 return res 1436 1437 def sasl_interactive_bind_s(self, *args, **kwargs): 1438 """ 1439 sasl_interactive_bind_s(who, auth) -> None 1440 """ 1441 res = self._apply_method_s(LDAPObject.sasl_interactive_bind_s, *args, **kwargs) 1442 self._store_last_bind(LDAPObject.sasl_interactive_bind_s, *args, **kwargs) 1443 return res 1444 1445 def sasl_bind_s(self, *args, **kwargs): 1446 res = self._apply_method_s(LDAPObject.sasl_bind_s, *args, **kwargs) 1447 self._store_last_bind(LDAPObject.sasl_bind_s, *args, **kwargs) 1448 return res 1449 1450 def add_s(self, *args, **kwargs): 1451 return self._apply_method_s(LDAPObject.add_s, *args, **kwargs) 1452 1453 def cancel_s(self, *args, **kwargs): 1454 return self._apply_method_s(LDAPObject.cancel_s, *args, **kwargs) 1455 1456 def compare_s(self, *args, **kwargs): 1457 return self._apply_method_s(LDAPObject.compare_s, *args, **kwargs) 1458 1459 def delete_s(self, *args, **kwargs): 1460 return self._apply_method_s(LDAPObject.delete_s, *args, **kwargs) 1461 1462 def extop_s(self, *args, **kwargs): 1463 return self._apply_method_s(LDAPObject.extop_s, *args, **kwargs) 1464 1465 def modify_s(self, *args, **kwargs): 1466 return self._apply_method_s(LDAPObject.modify_s, *args, **kwargs) 1467 1468 def rename_s(self, *args, **kwargs): 1469 return self._apply_method_s(LDAPObject.rename_s, *args, **kwargs) 1470 1471 def search_s(self, *args, **kwargs): 1472 return self._apply_method_s(LDAPObject.search_s, *args, **kwargs) 1473 1474 def whoami_s(self, *args, **kwargs): 1475 return self._apply_method_s(LDAPObject.whoami_s, *args, **kwargs) 1476