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