From 6a92648b6f13113fe075dc256c2c7ffc01e5236c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Wed, 6 Apr 2022 11:10:51 +0100 Subject: [PATCH 1/5] Control and extop rework --- Lib/ldap/__init__.py | 6 +++++ Lib/ldap/controls/__init__.py | 11 +++++---- Lib/ldap/controls/deref.py | 4 +--- Lib/ldap/controls/libldap.py | 8 +------ Lib/ldap/controls/openldap.py | 3 --- Lib/ldap/controls/pagedresults.py | 5 +--- Lib/ldap/controls/ppolicy.py | 5 +--- Lib/ldap/controls/psearch.py | 4 +--- Lib/ldap/controls/pwdpolicy.py | 6 +---- Lib/ldap/controls/readentry.py | 6 +---- Lib/ldap/controls/simple.py | 13 +++-------- Lib/ldap/controls/sss.py | 6 +---- Lib/ldap/controls/vlv.py | 7 +----- Lib/ldap/extop/__init__.py | 38 +++++++++++++++++++++++++++++-- Lib/ldap/syncrepl.py | 6 +---- 15 files changed, 62 insertions(+), 66 deletions(-) diff --git a/Lib/ldap/__init__.py b/Lib/ldap/__init__.py index b1797078..c8c74ddc 100644 --- a/Lib/ldap/__init__.py +++ b/Lib/ldap/__init__.py @@ -43,6 +43,12 @@ if k.startswith('OPT_'): OPT_NAMES_DICT[v]=k +# OID to class registries +KNOWN_RESPONSE_CONTROLS = {} +KNOWN_INTERMEDIATE_RESPONSES = {} +KNOWN_EXTENDED_RESPONSES = {} + + class DummyLock: """Define dummy class with methods compatible to threading.Lock""" def __init__(self): diff --git a/Lib/ldap/controls/__init__.py b/Lib/ldap/controls/__init__.py index 73557168..c6de528d 100644 --- a/Lib/ldap/controls/__init__.py +++ b/Lib/ldap/controls/__init__.py @@ -15,12 +15,12 @@ ImportError(f'ldap {__version__} and _ldap {_ldap.__version__} version mismatch!') import ldap +from ldap import KNOWN_RESPONSE_CONTROLS from pyasn1.error import PyAsn1Error __all__ = [ - 'KNOWN_RESPONSE_CONTROLS', # Classes 'AssertionControl', 'BooleanControl', @@ -37,9 +37,6 @@ 'DecodeControlTuples', ] -# response control OID to class registry -KNOWN_RESPONSE_CONTROLS = {} - class RequestControl: """ @@ -77,6 +74,12 @@ class ResponseControl: sets the criticality of the received control (boolean) """ + def __init_subclass__(cls): + if not getattr(cls, 'controlType', None): + return + + KNOWN_RESPONSE_CONTROLS.setdefault(cls.controlType, cls) + def __init__(self,controlType=None,criticality=False): self.controlType = controlType self.criticality = criticality diff --git a/Lib/ldap/controls/deref.py b/Lib/ldap/controls/deref.py index e5b2a7ec..8e58584a 100644 --- a/Lib/ldap/controls/deref.py +++ b/Lib/ldap/controls/deref.py @@ -11,7 +11,7 @@ ] import ldap.controls -from ldap.controls import LDAPControl,KNOWN_RESPONSE_CONTROLS +from ldap.controls import LDAPControl import pyasn1_modules.rfc2251 from pyasn1.type import namedtype,univ,tag @@ -114,5 +114,3 @@ def decodeControlValue(self,encodedControlValue): self.derefRes[str(deref_attr)].append((str(deref_val),partial_attrs_dict)) except KeyError: self.derefRes[str(deref_attr)] = [(str(deref_val),partial_attrs_dict)] - -KNOWN_RESPONSE_CONTROLS[DereferenceControl.controlType] = DereferenceControl diff --git a/Lib/ldap/controls/libldap.py b/Lib/ldap/controls/libldap.py index 9a102379..76c754f0 100644 --- a/Lib/ldap/controls/libldap.py +++ b/Lib/ldap/controls/libldap.py @@ -13,7 +13,7 @@ import ldap -from ldap.controls import RequestControl,LDAPControl,KNOWN_RESPONSE_CONTROLS +from ldap.controls import RequestControl,LDAPControl class AssertionControl(RequestControl): @@ -33,8 +33,6 @@ def __init__(self,criticality=True,filterstr='(objectClass=*)'): def encodeControlValue(self): return _ldap.encode_assertion_control(self.filterstr) -KNOWN_RESPONSE_CONTROLS[ldap.CONTROL_ASSERT] = AssertionControl - class MatchedValuesControl(RequestControl): """ @@ -54,8 +52,6 @@ def __init__(self,criticality=False,filterstr='(objectClass=*)'): def encodeControlValue(self): return _ldap.encode_valuesreturnfilter_control(self.filterstr) -KNOWN_RESPONSE_CONTROLS[ldap.CONTROL_VALUESRETURNFILTER] = MatchedValuesControl - class SimplePagedResultsControl(LDAPControl): """ @@ -77,5 +73,3 @@ def encodeControlValue(self): def decodeControlValue(self,encodedControlValue): self.size,self.cookie = _ldap.decode_page_control(encodedControlValue) - -KNOWN_RESPONSE_CONTROLS[ldap.CONTROL_PAGEDRESULTS] = SimplePagedResultsControl diff --git a/Lib/ldap/controls/openldap.py b/Lib/ldap/controls/openldap.py index 24040ed7..5540989a 100644 --- a/Lib/ldap/controls/openldap.py +++ b/Lib/ldap/controls/openldap.py @@ -39,9 +39,6 @@ def decodeControlValue(self,encodedControlValue): self.numSearchContinuations = int(decodedValue[2]) -ldap.controls.KNOWN_RESPONSE_CONTROLS[SearchNoOpControl.controlType] = SearchNoOpControl - - class SearchNoOpMixIn: """ Mix-in class to be used with class LDAPObject and friends. diff --git a/Lib/ldap/controls/pagedresults.py b/Lib/ldap/controls/pagedresults.py index 12ca573d..ced06e05 100644 --- a/Lib/ldap/controls/pagedresults.py +++ b/Lib/ldap/controls/pagedresults.py @@ -11,7 +11,7 @@ # Imports from python-ldap 2.4+ import ldap.controls -from ldap.controls import RequestControl,ResponseControl,KNOWN_RESPONSE_CONTROLS +from ldap.controls import RequestControl,ResponseControl # Imports from pyasn1 from pyasn1.type import tag,namedtype,univ,constraint @@ -44,6 +44,3 @@ def decodeControlValue(self,encodedControlValue): decodedValue,_ = decoder.decode(encodedControlValue,asn1Spec=PagedResultsControlValue()) self.size = int(decodedValue.getComponentByName('size')) self.cookie = bytes(decodedValue.getComponentByName('cookie')) - - -KNOWN_RESPONSE_CONTROLS[SimplePagedResultsControl.controlType] = SimplePagedResultsControl diff --git a/Lib/ldap/controls/ppolicy.py b/Lib/ldap/controls/ppolicy.py index f3a8416d..6f8eff0c 100644 --- a/Lib/ldap/controls/ppolicy.py +++ b/Lib/ldap/controls/ppolicy.py @@ -11,7 +11,7 @@ # Imports from python-ldap 2.4+ from ldap.controls import ( - ResponseControl, ValueLessRequestControl, KNOWN_RESPONSE_CONTROLS + ResponseControl, ValueLessRequestControl ) # Imports from pyasn1 @@ -100,6 +100,3 @@ def decodeControlValue(self,encodedControlValue): error = ppolicyValue.getComponentByName('error') if error.hasValue(): self.error = int(error) - - -KNOWN_RESPONSE_CONTROLS[PasswordPolicyControl.controlType] = PasswordPolicyControl diff --git a/Lib/ldap/controls/psearch.py b/Lib/ldap/controls/psearch.py index 32900c8b..23d13441 100644 --- a/Lib/ldap/controls/psearch.py +++ b/Lib/ldap/controls/psearch.py @@ -14,7 +14,7 @@ # Imports from python-ldap 2.4+ import ldap.controls -from ldap.controls import RequestControl,ResponseControl,KNOWN_RESPONSE_CONTROLS +from ldap.controls import RequestControl,ResponseControl # Imports from pyasn1 from pyasn1.type import namedtype,namedval,univ,constraint @@ -125,5 +125,3 @@ def decodeControlValue(self,encodedControlValue): else: self.changeNumber = None return (self.changeType,self.previousDN,self.changeNumber) - -KNOWN_RESPONSE_CONTROLS[EntryChangeNotificationControl.controlType] = EntryChangeNotificationControl diff --git a/Lib/ldap/controls/pwdpolicy.py b/Lib/ldap/controls/pwdpolicy.py index 54f1a700..b6fc8c33 100644 --- a/Lib/ldap/controls/pwdpolicy.py +++ b/Lib/ldap/controls/pwdpolicy.py @@ -12,7 +12,7 @@ # Imports from python-ldap 2.4+ import ldap.controls -from ldap.controls import RequestControl,ResponseControl,ValueLessRequestControl,KNOWN_RESPONSE_CONTROLS +from ldap.controls import ResponseControl class PasswordExpiringControl(ResponseControl): @@ -24,8 +24,6 @@ class PasswordExpiringControl(ResponseControl): def decodeControlValue(self,encodedControlValue): self.gracePeriod = int(encodedControlValue) -KNOWN_RESPONSE_CONTROLS[PasswordExpiringControl.controlType] = PasswordExpiringControl - class PasswordExpiredControl(ResponseControl): """ @@ -35,5 +33,3 @@ class PasswordExpiredControl(ResponseControl): def decodeControlValue(self,encodedControlValue): self.passwordExpired = encodedControlValue=='0' - -KNOWN_RESPONSE_CONTROLS[PasswordExpiredControl.controlType] = PasswordExpiredControl diff --git a/Lib/ldap/controls/readentry.py b/Lib/ldap/controls/readentry.py index 7b2a7e89..468d481a 100644 --- a/Lib/ldap/controls/readentry.py +++ b/Lib/ldap/controls/readentry.py @@ -8,7 +8,7 @@ import ldap from pyasn1.codec.ber import encoder,decoder -from ldap.controls import LDAPControl,KNOWN_RESPONSE_CONTROLS +from ldap.controls import LDAPControl from pyasn1_modules.rfc2251 import AttributeDescriptionList,SearchResultEntry @@ -63,8 +63,6 @@ class PreReadControl(ReadEntryControl): """ controlType = ldap.CONTROL_PRE_READ -KNOWN_RESPONSE_CONTROLS[PreReadControl.controlType] = PreReadControl - class PostReadControl(ReadEntryControl): """ @@ -83,5 +81,3 @@ class PostReadControl(ReadEntryControl): after the operation was done by the server """ controlType = ldap.CONTROL_POST_READ - -KNOWN_RESPONSE_CONTROLS[PostReadControl.controlType] = PostReadControl diff --git a/Lib/ldap/controls/simple.py b/Lib/ldap/controls/simple.py index 96837e2a..b6274c9e 100644 --- a/Lib/ldap/controls/simple.py +++ b/Lib/ldap/controls/simple.py @@ -5,7 +5,7 @@ """ import struct,ldap -from ldap.controls import RequestControl,ResponseControl,LDAPControl,KNOWN_RESPONSE_CONTROLS +from ldap.controls import RequestControl,ResponseControl from pyasn1.type import univ from pyasn1.codec.ber import encoder,decoder @@ -31,7 +31,7 @@ def encodeControlValue(self): return None -class OctetStringInteger(LDAPControl): +class OctetStringInteger: """ Base class with controlValue being unsigend integer values @@ -51,7 +51,7 @@ def decodeControlValue(self,encodedControlValue): self.integerValue = struct.unpack('!Q',encodedControlValue)[0] -class BooleanControl(LDAPControl): +class BooleanControl: """ Base class for simple request controls with boolean control value. @@ -82,8 +82,6 @@ class ManageDSAITControl(ValueLessRequestControl): def __init__(self,criticality=False): ValueLessRequestControl.__init__(self,ldap.CONTROL_MANAGEDSAIT,criticality=False) -KNOWN_RESPONSE_CONTROLS[ldap.CONTROL_MANAGEDSAIT] = ManageDSAITControl - class RelaxRulesControl(ValueLessRequestControl): """ @@ -93,8 +91,6 @@ class RelaxRulesControl(ValueLessRequestControl): def __init__(self,criticality=False): ValueLessRequestControl.__init__(self,ldap.CONTROL_RELAX,criticality=False) -KNOWN_RESPONSE_CONTROLS[ldap.CONTROL_RELAX] = RelaxRulesControl - class ProxyAuthzControl(RequestControl): """ @@ -134,9 +130,6 @@ def decodeControlValue(self,encodedControlValue): self.authzId = encodedControlValue -KNOWN_RESPONSE_CONTROLS[AuthorizationIdentityResponseControl.controlType] = AuthorizationIdentityResponseControl - - class GetEffectiveRightsControl(RequestControl): """ Get Effective Rights Control diff --git a/Lib/ldap/controls/sss.py b/Lib/ldap/controls/sss.py index e6ee3686..0dbbf532 100644 --- a/Lib/ldap/controls/sss.py +++ b/Lib/ldap/controls/sss.py @@ -14,9 +14,7 @@ import sys import ldap -from ldap.ldapobject import LDAPObject -from ldap.controls import (RequestControl, ResponseControl, - KNOWN_RESPONSE_CONTROLS, DecodeControlTuples) +from ldap.controls import RequestControl, ResponseControl from pyasn1.type import univ, namedtype, tag, namedval, constraint from pyasn1.codec.ber import encoder, decoder @@ -130,5 +128,3 @@ def decodeControlValue(self, encoded): # backward compatibility class attributes self.result = self.sortResult self.attribute_type_error = self.attributeType - -KNOWN_RESPONSE_CONTROLS[SSSResponseControl.controlType] = SSSResponseControl diff --git a/Lib/ldap/controls/vlv.py b/Lib/ldap/controls/vlv.py index 5fc7ce88..7cb4b482 100644 --- a/Lib/ldap/controls/vlv.py +++ b/Lib/ldap/controls/vlv.py @@ -12,8 +12,7 @@ import ldap from ldap.ldapobject import LDAPObject -from ldap.controls import (RequestControl, ResponseControl, - KNOWN_RESPONSE_CONTROLS, DecodeControlTuples) +from ldap.controls import RequestControl, ResponseControl from pyasn1.type import univ, namedtype, tag, namedval, constraint from pyasn1.codec.ber import encoder, decoder @@ -88,8 +87,6 @@ def encodeControlValue(self): p.setComponentByName('target', target) return encoder.encode(p) -KNOWN_RESPONSE_CONTROLS[VLVRequestControl.controlType] = VLVRequestControl - class VirtualListViewResultType(univ.Enumerated): namedValues = namedval.NamedValues( @@ -138,5 +135,3 @@ def decodeControlValue(self,encoded): self.content_count = self.contentCount self.result = self.virtualListViewResult self.context_id = self.contextID - -KNOWN_RESPONSE_CONTROLS[VLVResponseControl.controlType] = VLVResponseControl diff --git a/Lib/ldap/extop/__init__.py b/Lib/ldap/extop/__init__.py index dc9aea2f..a4383f78 100644 --- a/Lib/ldap/extop/__init__.py +++ b/Lib/ldap/extop/__init__.py @@ -10,6 +10,7 @@ """ from ldap import __version__ +from ldap import KNOWN_EXTENDED_RESPONSES, KNOWN_INTERMEDIATE_RESPONSES class ExtendedRequest: @@ -42,12 +43,18 @@ class ExtendedResponse: """ Generic base class for a LDAPv3 extended operation response - requestName - OID as string of the LDAPv3 extended operation response + responseName + OID as string of the LDAPv3 extended operation response or None encodedResponseValue BER-encoded ASN.1 value of the LDAPv3 extended operation response """ + def __init_subclass__(cls): + if not getattr(cls, 'responseName', None): + return + + KNOWN_EXTENDED_RESPONSES.setdefault(cls.responseName, cls) + def __init__(self,responseName,encodedResponseValue): self.responseName = responseName self.responseValue = self.decodeResponseValue(encodedResponseValue) @@ -63,6 +70,33 @@ def decodeResponseValue(self,value): return value +class IntermediateResponse: + """ + Generic base class for a LDAPv3 intermediate response message + + responseName + OID as string of the LDAPv3 intermediate response message or None + encodedResponseValue + BER-encoded ASN.1 value of the LDAPv3 intermediate response message + """ + + def __init_subclass__(cls): + if not getattr(cls, 'responseName', None): + return + + KNOWN_INTERMEDIATE_RESPONSES.setdefault(cls.responseName, cls) + + def __repr__(self): + return f'{self.__class__.__name__}({self.responseName},{self.responseValue})' + + def decodeResponseValue(self,value): + """ + decodes the BER-encoded ASN.1 extended operation response value and + sets the appropriate class attributes + """ + return value + + # Import sub-modules from ldap.extop.dds import * from ldap.extop.passwd import PasswordModifyResponse diff --git a/Lib/ldap/syncrepl.py b/Lib/ldap/syncrepl.py index fd0c1285..c59641e1 100644 --- a/Lib/ldap/syncrepl.py +++ b/Lib/ldap/syncrepl.py @@ -11,7 +11,7 @@ from pyasn1.codec.ber import encoder, decoder from ldap.pkginfo import __version__, __author__, __license__ -from ldap.controls import RequestControl, ResponseControl, KNOWN_RESPONSE_CONTROLS +from ldap.controls import RequestControl, ResponseControl from ldap import RES_SEARCH_RESULT, RES_SEARCH_ENTRY, RES_INTERMEDIATE __all__ = [ @@ -159,8 +159,6 @@ def decodeControlValue(self, encodedControlValue): self.state = self.__class__.opnames[int(state)] self.entryUUID = str(uuid) -KNOWN_RESPONSE_CONTROLS[SyncStateControl.controlType] = SyncStateControl - class SyncDoneValue(univ.Sequence): """ @@ -200,8 +198,6 @@ def decodeControlValue(self, encodedControlValue): else: self.refreshDeletes = None -KNOWN_RESPONSE_CONTROLS[SyncDoneControl.controlType] = SyncDoneControl - class RefreshDelete(univ.Sequence): """ From f1da4bb6d6cecf2a3d961c1a3e6fda687225c6a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Thu, 24 Mar 2022 11:32:42 +0000 Subject: [PATCH 2/5] Draft new response handling API --- Doc/fake_ldap_module_for_documentation.py | 2 + Lib/ldap/__init__.py | 1 + Lib/ldap/connection.py | 224 +++++++++++ Lib/ldap/response.py | 216 ++++++++++ Modules/LDAPObject.c | 459 ++++++++++++++++++++++ Modules/constants.c | 16 +- Modules/ldapcontrol.c | 66 +++- Modules/ldapmodule.c | 38 ++ Modules/pythonldap.h | 6 + 9 files changed, 1016 insertions(+), 12 deletions(-) create mode 100644 Lib/ldap/connection.py create mode 100644 Lib/ldap/response.py diff --git a/Doc/fake_ldap_module_for_documentation.py b/Doc/fake_ldap_module_for_documentation.py index 30807819..7e10ac29 100644 --- a/Doc/fake_ldap_module_for_documentation.py +++ b/Doc/fake_ldap_module_for_documentation.py @@ -28,3 +28,5 @@ def get_option(num): class LDAPError: pass + +_exceptions = {} diff --git a/Lib/ldap/__init__.py b/Lib/ldap/__init__.py index c8c74ddc..29c5fb99 100644 --- a/Lib/ldap/__init__.py +++ b/Lib/ldap/__init__.py @@ -35,6 +35,7 @@ assert _ldap.__version__==__version__, \ ImportError(f'ldap {__version__} and _ldap {_ldap.__version__} version mismatch!') from _ldap import * +from _ldap import _exceptions # call into libldap to initialize it right now LIBLDAP_API_INFO = _ldap.get_option(_ldap.OPT_API_INFO) diff --git a/Lib/ldap/connection.py b/Lib/ldap/connection.py new file mode 100644 index 00000000..d7e043ca --- /dev/null +++ b/Lib/ldap/connection.py @@ -0,0 +1,224 @@ +""" +connection.py - wraps class _ldap.LDAPObject + +See https://www.python-ldap.org/ for details. +""" + +from ldap.pkginfo import __version__, __author__, __license__ + +__all__ = [ + 'Connection', +] + + +from numbers import Real +from typing import AnyStr, Optional, Union + +import ldap +from ldap.controls import DecodeControlTuples, RequestControl +from ldap.extop import ExtendedRequest +from ldap.ldapobject import SimpleLDAPObject, NO_UNIQUE_ENTRY +from ldap.response import ( + Response, + SearchEntry, SearchReference, SearchResult, + IntermediateResponse, ExtendedResult, +) + +from ldapurl import LDAPUrl + +RequestControls = Optional[list[RequestControl]] + + +# TODO: remove _ext and _s functions as we rework request API +class Connection(SimpleLDAPObject): + resp_ctrl_classes = None + + def __init__(self, uri: Union[LDAPUrl, str, None], **kwargs): + if isinstance(uri, LDAPUrl): + uri = uri.unparse() + super().__init__(uri, **kwargs) + + def result(self, msgid: int = ldap.RES_ANY, *, all: int = 1, + timeout: Optional[float] = None) -> Optional[list[Response]]: + """ + result([msgid: int = RES_ANY [, all: int = 1 [, timeout : + Optional[float] = None]]]) -> Optional[list[Response]] + + This method is used to wait for and return the result of an + operation previously initiated by one of the LDAP asynchronous + operation routines (e.g. search(), modify(), etc.) They all + return an invocation identifier (a message id) upon successful + initiation of their operation. This id is guaranteed to be + unique across an LDAP session, and can be used to request the + result of a specific operation via the msgid parameter of the + result() method. + + If the result of a specific operation is required, msgid should + be set to the invocation message id returned when the operation + was initiated; otherwise RES_ANY should be supplied. + + The all parameter is used to wait until a final response for + a given operation is received, this is useful with operations + (like search) that generate multiple responses and is used + to select whether a single item should be returned or to wait + for all the responses before returning. + + Using search as an example: A search response is made up of + zero or more search entries followed by a search result. If all + is 0, search entries will be returned one at a time as they + come in, via separate calls to result(). If all is 1, the + search response will be returned in its entirety, i.e. after + all entries and the final search result have been received. If + all is 2, all search entries that have been received so far + will be returned. + + The method returns a list of messages or None if polling and no + messages arrived yet. + + The result() method will block for timeout seconds, or + indefinitely if timeout is negative. A timeout of 0 will + effect a poll. The timeout can be expressed as a floating-point + value. If timeout is None the default in self.timeout is used. + + If a timeout occurs, a TIMEOUT exception is raised, unless + polling (timeout = 0), in which case None is returned. + """ + + if timeout is None: + timeout = self.timeout + + messages = self._ldap_call(self._l.result, msgid, all, timeout) + + if messages is None: + return None + + results = [] + for msgid, msgtype, controls, data in messages: + controls = DecodeControlTuples(controls, self.resp_ctrl_classes) + + m = Response(msgid, msgtype, controls, **data) + results.append(m) + + return results + + def bind_s(self, dn: Optional[str] = None, + cred: Optional[AnyStr] = None, *, + method: int = ldap.AUTH_SIMPLE, + ctrls: RequestControls = None) -> ldap.response.BindResult: + msgid = self.bind(dn, cred, method) + responses = self.result(msgid) + result, = responses + return result + + def compare_s(self, dn: str, attr: str, value: bytes, *, + ctrls: RequestControls = None + ) -> ldap.response.CompareResult: + "TODO: remove _s functions introducing a better request API" + msgid = self.compare_ext(dn, attr, value, serverctrls=ctrls) + responses = self.result(msgid) + result, = responses + return bool(result) + + def delete_s(self, dn: str, *, + ctrls: RequestControls = None) -> ldap.response.DeleteResult: + msgid = self.delete_ext(dn, serverctrls=ctrls) + responses = self.result(msgid) + result, = responses + return result + + def extop_s(self, oid: Optional[str] = None, + value: Optional[bytes] = None, *, + request: Optional[ExtendedRequest] = None, + ctrls: RequestControls = None + ) -> list[Union[IntermediateResponse, ExtendedResult]]: + if request is not None: + oid = request.requestName + value = request.encodedRequestValue() + + msgid = self.extop(oid, value, serverctrls=ctrls) + return self.result(msgid) + + def search_s(self, base: Optional[str] = None, + scope: int = ldap.SCOPE_SUBTREE, + filter: str = "(objectClass=*)", + attrlist: Optional[list[str]] = None, *, + attrsonly: bool = False, + ctrls: RequestControls = None, + sizelimit: int = 0, timelimit: int = -1, + timeout: Optional[Real] = None + ) -> list[Union[SearchEntry, SearchReference]]: + if timeout is None: + timeout = timelimit + + msgid = self.search_ext(base, scope, filter, attrlist=attrlist, + attrsonly=attrsonly, serverctrls=ctrls, + sizelimit=sizelimit, timeout=timelimit) + result = self.result(msgid, timeout=timeout) + result[-1].raise_for_result() + return result[:-1] + + def search_subschemasubentry_s( + self, dn: Optional[str] = None) -> Optional[str]: + """ + Returns the distinguished name of the sub schema sub entry + for a part of a DIT specified by dn. + + None as result indicates that the DN of the sub schema sub entry could + not be determined. + """ + empty_dn = '' + attrname = 'subschemaSubentry' + if dn is None: + dn = empty_dn + try: + r = self.search_s(dn, ldap.SCOPE_BASE, None, [attrname]) + except (ldap.NO_SUCH_OBJECT, ldap.NO_SUCH_ATTRIBUTE, + ldap.INSUFFICIENT_ACCESS): + r = [] + except ldap.UNDEFINED_TYPE: + return None + + attr = r and ldap.cidict.cidict(r[0].attrs).get(attrname) + if attr: + return attr[0].decode('utf-8') + elif dn: + # Try to find sub schema sub entry in root DSE + return self.search_subschemasubentry_s(dn=empty_dn) + else: + # If dn was already rootDSE we can return here + return None + + def read_s(self, dn: str, filterstr: Optional[str] = None, + attrlist: Optional[list[str]] = None, + ctrls: RequestControls = None, + timeout: int = -1) -> dict[str, bytes]: + """ + Reads and returns a single entry specified by `dn'. + + Other attributes just like those passed to `search_s()' + """ + r = self.search_s(dn, ldap.SCOPE_BASE, filterstr, + attrlist=attrlist, ctrls=ctrls, timeout=timeout) + if r: + return r[0].attrs + else: + return None + + def find_unique_entry(self, base: Optional[str] = None, + scope: int = ldap.SCOPE_SUBTREE, + filter: str = "(objectClass=*)", + attrlist: Optional[list[str]] = None, *, + attrsonly: bool = False, + ctrls: RequestControls = None, + timelimit: int = -1, + timeout: Optional[Real] = None + ) -> list[Union[SearchEntry, SearchReference]]: + """ + Returns a unique entry, raises exception if not unique + """ + r = self.search_s(base, scope, filter, attrlist=attrlist, + attrsonly=attrsonly, ctrls=ctrls, timeout=timeout, + sizelimit=2) + if len(r) != 1: + raise NO_UNIQUE_ENTRY(f'No or non-unique search result for {filter}') + return r[0] diff --git a/Lib/ldap/response.py b/Lib/ldap/response.py new file mode 100644 index 00000000..1e75c4ad --- /dev/null +++ b/Lib/ldap/response.py @@ -0,0 +1,216 @@ +""" +response.py - classes for LDAP responses + +See https://www.python-ldap.org/ for details. +""" + +from ldap.pkginfo import __version__, __author__, __license__ + +__all__ = [ + 'Response', + 'Result', + + 'SearchEntry', + 'SearchReference', + 'SearchResult', + + 'IntermediateResponse', + 'ExtendedResult', + + 'BindResult', + 'ModifyResult', + 'AddResult', + 'DeleteResult', + 'ModRDNResult', + 'CompareResult', +] + +from typing import Optional + +import ldap +from ldap.controls import ResponseControl +from ldap.extop import ExtendedResponse, Intermediate + + +_SUCCESS_CODES = [ + ldap.SUCCESS.errnum, + ldap.COMPARE_TRUE.errnum, + ldap.COMPARE_FALSE.errnum, + ldap.SASL_BIND_IN_PROGRESS.errnum, +] + + +class Response: + msgid: int + msgtype: int + controls: list[ResponseControl] + + __subclasses: dict[int, type] = {} + + def __init_subclass__(cls): + if not hasattr(cls, 'msgtype'): + return + c = __class__.__subclasses.setdefault(cls.msgtype, cls) + assert issubclass(cls, c) + + def __new__(cls, msgid, msgtype, controls=None, **kwargs): + if cls is not __class__: + instance = super().__new__(cls) + instance.msgid = msgid + instance.msgtype = msgtype + instance.controls = controls + return instance + + c = __class__.__subclasses.get(msgtype) + if c: + return c.__new__(c, msgid, msgtype, controls, **kwargs) + + instance = super().__new__(cls) + instance.msgid = msgid + instance.msgtype = msgtype + instance.controls = controls + return instance + + +class Result(Response): + result: int + matcheddn: str + message: str + referrals: Optional[list[str]] + + def __new__(cls, msgid, msgtype, controls, + result, matcheddn, message, referrals, **kwargs): + instance = super().__new__(cls, msgid, msgtype, controls, **kwargs) + + instance.result = result + instance.matcheddn = matcheddn + instance.message = message + instance.referrals = referrals + + return instance + + def raise_for_result(self) -> 'Result': + if self.result in _SUCCESS_CODES: + return self + raise ldap._exceptions.get(self.result, ldap.LDAPError)(self) + + def __repr__(self): + return (f"{self.__class__.__name__}" + f"(msgid={self.msgid}, result={self.result})") + + +class SearchEntry(Response): + msgtype = ldap.RES_SEARCH_ENTRY + + dn: str + attrs: dict[str, Optional[list[bytes]]] + + def __new__(cls, msgid, msgtype, controls, dn, attrs, **kwargs): + instance = super().__new__(cls, msgid, msgtype, controls, **kwargs) + + instance.dn = dn + instance.attrs = attrs + + return instance + + +class SearchReference(Response): + msgtype = ldap.RES_SEARCH_REFERENCE + + referrals: list[str] + + def __new__(cls, msgid, msgtype, controls, referrals, **kwargs): + instance = super().__new__(cls, msgid, msgtype, controls, **kwargs) + + instance.referrals = referrals + + return instance + + +class SearchResult(Result): + msgtype = ldap.RES_SEARCH_RESULT + + +class IntermediateResponse(Response): + msgtype = ldap.RES_INTERMEDIATE + + oid: Optional[str] + value: Optional[bytes] + + __subclasses: dict[str, type] = {} + + def __new__(cls, msgid, msgtype, controls=None, name=None, + value=None, *, defaultClass: Optional[Intermediate] = None, + **kwargs): + if cls is not __class__: + instance = super().__new__(cls, ) + return instance + + c = __class__.__subclasses.get(msgtype) + if c: + return c.__new__(c, msgid, msgtype, controls, **kwargs) + + instance = super().__new__(cls) + instance.msgid = msgid + instance.msgtype = msgtype + instance.controls = controls + return instance + + +class BindResult(Result): + msgtype = ldap.RES_BIND + + servercreds: Optional[bytes] + + +class ModifyResult(Result): + msgtype = ldap.RES_MODIFY + + +class AddResult(Result): + msgtype = ldap.RES_ADD + + +class DeleteResult(Result): + msgtype = ldap.RES_DELETE + + +class ModRDNResult(Result): + msgtype = ldap.RES_MODRDN + + +class CompareResult(Result): + msgtype = ldap.RES_COMPARE + + def __bool__(self) -> bool: + if self.result == ldap.COMPARE_FALSE.errnum: + return False + if self.result == ldap.COMPARE_TRUE.errnum: + return True + raise ldap._exceptions.get(self.result, ldap.LDAPError)(self) + + +class ExtendedResult(Result): + msgtype = ldap.RES_EXTENDED + + oid: Optional[str] + value: Optional[bytes] + # TODO: how to subclass these dynamically? (UnsolicitedResponse, ...), + # is it just with __new__? + + +class UnsolicitedResponse(ExtendedResult): + msgid = ldap.RES_UNSOLICITED + + __subclasses: dict[str, type] = {} + + def __new__(cls, msgid, msgtype, controls=None, *, + name, value=None, **kwargs): + if cls is __class__: + c = __class__.__subclasses.get(msgtype) + if c: + return c.__new__(c, msgid, msgtype, controls, + name=name, value=value, **kwargs) + + return super().__new__(cls, msgid, msgtype, controls, + name=name, value=value, **kwargs) diff --git a/Modules/LDAPObject.c b/Modules/LDAPObject.c index 71fac73e..779e175f 100644 --- a/Modules/LDAPObject.c +++ b/Modules/LDAPObject.c @@ -10,6 +10,33 @@ #include #endif +PyStructSequence_Field message_fields[] = { + { + .name = "msgid", + }, + { + .name = "type", + }, + { + .name = "controls", + }, + { + .name = "data", + }, + { + .name = NULL, + } +}; + +PyStructSequence_Desc message_tuple_desc = { + .name = "_ldap._RawLDAPMessage", + .doc = "LDAP Message returned from native code", + .fields = message_fields, + .n_in_sequence = 4, +}; + +PyTypeObject message_tuple_type; + static void free_attrs(char ***); /* constructor */ @@ -1035,6 +1062,437 @@ l_ldap_rename(LDAPObject *self, PyObject *args) return PyLong_FromLong(msgid); } +/* Connection.result() */ + +static PyObject * +l_ldap_result(LDAPObject *self, PyObject *args) +{ + PyObject *retval = NULL, *pytmp = NULL; + LDAPMessage *result = NULL, *msg; + BerElement *ber = NULL; + int rc = LDAP_SUCCESS, res_type, msgid = LDAP_RES_ANY; + int count, msg_index = 0, all = 1; + double timeout = -1.0; + struct timeval tv, *tvp = NULL; + + if (!PyArg_ParseTuple + (args, "|iid:result", &msgid, &all, &timeout)) + return NULL; + if (not_valid(self)) + return NULL; + + if (timeout >= 0) { + tvp = &tv; + set_timeval_from_double(tvp, timeout); + } + + LDAP_BEGIN_ALLOW_THREADS(self); + res_type = ldap_result(self->ldap, msgid, all, tvp, &result); + LDAP_END_ALLOW_THREADS(self); + + /* LDAP or system error */ + if ( res_type < 0 ) { + result = NULL; + rc = res_type; + goto error; + } + + if ( res_type == 0 ) { + /* Polls return None, timeouts raise an exception */ + if ( timeout == 0 ) { + Py_RETURN_NONE; + } else { + rc = LDAP_TIMEOUT; + goto error; + } + } + + count = ldap_count_messages( self->ldap, result ); + if ( (retval = PyList_New(count)) == NULL ) { + goto error; + } + + for ( msg = ldap_first_message( self->ldap, result ); + msg; + msg = ldap_next_message( self->ldap, msg ) ) { + PyObject *msgtuple, *data; + LDAPControl **controls = NULL; + + msgid = ldap_msgid( msg ); + res_type = ldap_msgtype( msg ); + + if ( (pytmp = PyStructSequence_New( &message_tuple_type )) == NULL ) { + goto error; + } + PyList_SET_ITEM( retval, msg_index++, pytmp ); + msgtuple = pytmp; + + if ( (pytmp = PyLong_FromUnsignedLong( msgid )) == NULL ) { + goto error; + } + PyStructSequence_SET_ITEM( msgtuple, 0, pytmp ); + + if ( (pytmp = PyLong_FromUnsignedLong( res_type )) == NULL ) { + goto error; + } + PyStructSequence_SET_ITEM( msgtuple, 1, pytmp ); + + if ( (pytmp = PyDict_New()) == NULL ) { + goto error; + } + PyStructSequence_SET_ITEM( msgtuple, 3, pytmp ); + data = pytmp; + + if ( res_type == LDAP_RES_SEARCH_ENTRY ) { + struct berval dn, attr, *values = NULL; + PyObject *attrdict; + + if ( (rc = ldap_get_dn_ber( self->ldap, msg, &ber, &dn )) != LDAP_SUCCESS ) { + goto error; + } + pytmp = PyUnicode_FromString( dn.bv_val ); + if ( !pytmp || PyDict_SetItemString( data, "dn", pytmp ) ) { + goto error; + } + Py_DECREF( pytmp ); + + pytmp = PyDict_New(); + if ( !pytmp || PyDict_SetItemString( data, "attrs", pytmp ) ) { + goto error; + } + Py_DECREF( pytmp ); + attrdict = pytmp; + pytmp = NULL; + + while ( (rc = ldap_get_attribute_ber( self->ldap, msg, ber, + &attr, &values )) == LDAP_SUCCESS ) { + PyObject *value_list = NULL; + struct berval *bv; + int index = 0; + + if ( attr.bv_val == NULL ) { + break; + } + + /* + * Some servers will send multiple attribute entries with same + * name, be prepared for that and just merge them. + * https://github.com/python-ldap/python-ldap/issues/218 + */ + value_list = PyDict_GetItemString( attrdict, attr.bv_val ); + if ( value_list == NULL ) { + count = 0; + if ( values ) { + for ( bv = values; bv->bv_val != NULL; bv++ ) count++; + } + value_list = PyList_New( count ); + if ( value_list == NULL ) { + ldap_memfree( values ); + goto error; + } + } else { + Py_INCREF(value_list); + index = PyList_Size( value_list ); + } + /* + * At this point we have our own reference on value_list + * independent on the one in attrdict, we'll release it after + * assignment. + */ + + for ( bv = values; bv->bv_val != NULL; bv++, index++ ) { + pytmp = LDAPberval_to_object( bv ); + if ( !pytmp || PyList_SetItem( value_list, index, pytmp ) ) { + ldap_memfree( values ); + Py_DECREF(value_list); + goto error; + } + } + pytmp = NULL; + + /* FIXME: deal with attrs-only */ + assert(0); + + ldap_memfree( values ); + if ( PyDict_SetItemString( attrdict, attr.bv_val, value_list ) ) { + Py_DECREF(value_list); + goto error; + } + Py_DECREF(value_list); + } + + if ( (rc = ldap_get_entry_controls( self->ldap, msg, + &controls )) != LDAP_SUCCESS ) { + goto error; + } + pytmp = LDAPControls_to_List( controls ); + ldap_controls_free( controls ); + if ( pytmp == NULL ) { + goto error; + } + PyStructSequence_SET_ITEM( msgtuple, 2, pytmp ); + + pytmp = NULL; + if ( ber != NULL ) { + ber_free( ber, 0 ); + ber = NULL; + } + } else if ( res_type == LDAP_RES_SEARCH_REFERENCE ) { + PyObject *refs_list = NULL; + char **refs = NULL; + int index; + + if ( (rc = ldap_parse_reference( self->ldap, msg, &refs, + &controls, 0 )) != LDAP_SUCCESS ) { + goto error; + } + + count = 0; + if ( refs ) { + char **p; + for ( p = refs; *p; p++ ) count++; + } + + pytmp = PyList_New( count ); + if ( !pytmp || PyDict_SetItemString( data, "referrals", pytmp ) ) { + ldap_memvfree( (void **)refs ); + ldap_controls_free( controls ); + goto error; + } + Py_DECREF( pytmp ); + refs_list = pytmp; + + pytmp = LDAPControls_to_List( controls ); + ldap_controls_free( controls ); + if ( pytmp == NULL ) { + ldap_memvfree( (void **)refs ); + goto error; + } + PyStructSequence_SET_ITEM( msgtuple, 2, pytmp ); + + for ( index = 0; refs[index]; index++ ) { + if ( (pytmp = PyUnicode_FromString( refs[index] )) == NULL ) { + ldap_memvfree( (void **)refs ); + goto error; + } + PyList_SET_ITEM( refs_list, index, pytmp ); + } + ldap_memvfree( (void **)refs ); + } else if ( res_type == LDAP_RES_INTERMEDIATE ) { + char *oid = NULL; + struct berval *value = NULL; + + if ( (rc = ldap_parse_intermediate( self->ldap, msg, &oid, + &value, &controls, 0 )) != LDAP_SUCCESS ) { + goto error; + } + /* + * Given Python 3.6 supports ordered dict, be nice and store the + * fields in order + */ + + if ( oid ) { + pytmp = PyUnicode_FromString( oid ); + ldap_memfree( oid ); + if ( !pytmp || PyDict_SetItemString( data, "name", pytmp ) ) { + ber_bvfree( value ); + ldap_controls_free( controls ); + goto error; + } + Py_DECREF( pytmp ); + } else { + pytmp = NULL; + if ( PyDict_SetItemString( data, "name", Py_None ) ) { + ber_bvfree( value ); + ldap_controls_free( controls ); + goto error; + } + } + + if ( value ) { + pytmp = LDAPberval_to_object( value ); + ber_bvfree( value ); + if ( !pytmp || PyDict_SetItemString( data, "value", pytmp ) ) { + ldap_controls_free( controls ); + goto error; + } + Py_DECREF( pytmp ); + } else { + pytmp = NULL; + if ( PyDict_SetItemString( data, "value", Py_None ) ) { + ldap_controls_free( controls ); + goto error; + } + } + + pytmp = LDAPControls_to_List( controls ); + ldap_controls_free( controls ); + if ( pytmp == NULL ) { + goto error; + } + PyStructSequence_SET_ITEM( msgtuple, 2, pytmp ); + } else { + int index, error = LDAP_SUCCESS; + char *matcheddn = NULL, *errmsg = NULL, **referrals = NULL; + PyObject *refs_list = NULL; + + if ( (rc = ldap_parse_result( self->ldap, msg, &error, &matcheddn, + &errmsg, &referrals, &controls, 0 )) != LDAP_SUCCESS ) { + goto error; + } + + pytmp = PyLong_FromLong( error ); + if ( !pytmp || PyDict_SetItemString( data, "result", pytmp ) ) { + ldap_memfree( matcheddn ); + ldap_memfree( errmsg ); + ldap_memvfree( (void **)referrals ); + ldap_controls_free( controls ); + goto error; + } + Py_DECREF( pytmp ); + + if ( matcheddn ) { + pytmp = PyUnicode_FromString( matcheddn ); + ldap_memfree( matcheddn ); + } else { + pytmp = Py_None; + Py_INCREF(pytmp); + } + if ( !pytmp || PyDict_SetItemString( data, "matcheddn", pytmp ) ) { + ldap_memfree( errmsg ); + ldap_memvfree( (void **)referrals ); + ldap_controls_free( controls ); + goto error; + } + Py_DECREF( pytmp ); + + if ( errmsg ) { + pytmp = PyUnicode_FromString( errmsg ); + ldap_memfree( errmsg ); + } else { + pytmp = Py_None; + Py_INCREF(pytmp); + } + if ( !pytmp || PyDict_SetItemString( data, "message", pytmp ) ) { + ldap_memvfree( (void **)referrals ); + ldap_controls_free( controls ); + goto error; + } + Py_DECREF( pytmp ); + + count = 0; + if ( referrals ) { + char **p; + for ( p = referrals; *p; p++ ) count++; + pytmp = PyList_New( count ); + } else { + pytmp = Py_None; + Py_INCREF(pytmp); + } + if ( !pytmp || PyDict_SetItemString( data, "referrals", pytmp ) ) { + ldap_memvfree( (void **)referrals ); + ldap_controls_free( controls ); + goto error; + } + Py_DECREF( pytmp ); + refs_list = pytmp; + + for ( index = 0; index < count; index++ ) { + if ( (pytmp = PyUnicode_FromString( referrals[index] )) == NULL ) { + ldap_memvfree( (void **)referrals ); + ldap_controls_free( controls ); + goto error; + } + PyList_SET_ITEM( refs_list, index, pytmp ); + } + ldap_memvfree( (void **)referrals ); + + pytmp = LDAPControls_to_List( controls ); + ldap_controls_free( controls ); + if ( pytmp == NULL ) { + goto error; + } + PyStructSequence_SET_ITEM( msgtuple, 2, pytmp ); + pytmp = NULL; + + if ( res_type == LDAP_RES_EXTENDED ) { + char *oid = NULL; + struct berval *value = NULL; + if ( (rc = ldap_parse_extended_result( self->ldap, msg, + &oid, &value, 0 )) != LDAP_SUCCESS ) { + goto error; + } + + if ( oid ) { + pytmp = PyUnicode_FromString( oid ); + ldap_memfree( oid ); + if ( !pytmp || PyDict_SetItemString( data, "name", pytmp ) ) { + ber_bvfree( value ); + goto error; + } + Py_DECREF( pytmp ); + } else { + pytmp = NULL; + if ( PyDict_SetItemString( data, "name", Py_None ) ) { + ber_bvfree( value ); + goto error; + } + } + + if ( value ) { + pytmp = LDAPberval_to_object( value ); + ber_bvfree( value ); + if ( !pytmp || PyDict_SetItemString( data, "value", pytmp ) ) { + goto error; + } + Py_DECREF( pytmp ); + } else { + pytmp = NULL; + if ( PyDict_SetItemString( data, "value", Py_None ) ) { + goto error; + } + } + } else if ( res_type == LDAP_RES_BIND ) { + struct berval *servercred = NULL; + if ( (rc = ldap_parse_sasl_bind_result( self->ldap, msg, + &servercred, 0 )) != LDAP_SUCCESS ) { + goto error; + } + + if ( servercred ) { + pytmp = LDAPberval_to_object( servercred ); + ber_bvfree( servercred ); + if ( !pytmp || PyDict_SetItemString( data, "servercred", pytmp ) ) { + goto error; + } + Py_DECREF( pytmp ); + } else { + pytmp = NULL; + if ( PyDict_SetItemString( data, "servercred", Py_None ) ) { + goto error; + } + } + } + } + pytmp = NULL; + } + + /* Free all messages now */ + ldap_msgfree( result ); + return retval; + +error: + if ( ber != NULL ) { + ber_free( ber, 0 ); + } + ldap_msgfree( result ); + Py_XDECREF(pytmp); + Py_XDECREF(retval); + if ( rc != LDAP_SUCCESS ) + return LDAPerr(rc); + Py_RETURN_NONE; +} + /* ldap_result4 */ static PyObject * @@ -1488,6 +1946,7 @@ static PyMethodDef methods[] = { {"delete_ext", (PyCFunction)l_ldap_delete_ext, METH_VARARGS}, {"modify_ext", (PyCFunction)l_ldap_modify_ext, METH_VARARGS}, {"rename", (PyCFunction)l_ldap_rename, METH_VARARGS}, + {"result", (PyCFunction)l_ldap_result, METH_VARARGS}, {"result4", (PyCFunction)l_ldap_result4, METH_VARARGS}, {"search_ext", (PyCFunction)l_ldap_search_ext, METH_VARARGS}, #ifdef HAVE_TLS diff --git a/Modules/constants.c b/Modules/constants.c index f0a0da94..d51f054c 100644 --- a/Modules/constants.c +++ b/Modules/constants.c @@ -194,7 +194,7 @@ LDAPerror(LDAP *l) int LDAPinit_constants(PyObject *m) { - PyObject *exc, *nobj; + PyObject *exc, *nobj, *exc_dict; struct ldap_apifeature_info info = { 1, "X_OPENLDAP_THREAD_SAFE", 0 }; int thread_safe = 0; @@ -207,6 +207,14 @@ LDAPinit_constants(PyObject *m) /* exceptions */ + exc_dict = PyDict_New(); + if ( exc_dict == NULL ) { + return -1; + } + if (PyModule_AddObject(m, "_exceptions", exc_dict) != 0) { + return -1; + } + LDAPexception_class = PyErr_NewException("ldap.LDAPError", NULL, NULL); if (LDAPexception_class == NULL) { return -1; @@ -241,6 +249,12 @@ LDAPinit_constants(PyObject *m) errobjects[LDAP_##n+LDAP_ERROR_OFFSET] = exc; \ if (PyModule_AddObject(m, #n, exc) != 0) return -1; \ Py_INCREF(exc); \ + if ( LDAP_##n > 0 ) { \ + nobj = PyLong_FromUnsignedLong( LDAP_##n ); \ + if (nobj == NULL) return -1; \ + if (PyDict_SetItem(exc_dict, nobj, exc)) { Py_DECREF(nobj); return -1; } \ + Py_DECREF(nobj); \ + } \ } while (0) #define add_int(n) do { \ diff --git a/Modules/ldapcontrol.c b/Modules/ldapcontrol.c index 4a37b614..da7a1484 100644 --- a/Modules/ldapcontrol.c +++ b/Modules/ldapcontrol.c @@ -20,6 +20,30 @@ LDAPControl_DumpList( LDAPControl** lcs ) { } } */ +PyStructSequence_Field control_fields[] = { + { + .name = "oid", + }, + { + .name = "criticality", + }, + { + .name = "value", + }, + { + .name = NULL, + } +}; + +PyStructSequence_Desc control_tuple_desc = { + .name = "_ldap._RawControl", + .doc = "LDAP Control returned from native code", + .fields = control_fields, + .n_in_sequence = 3, +}; + +PyTypeObject control_tuple_type; + /* Free a single LDAPControl object created by Tuple_to_LDAPControl */ static void @@ -165,7 +189,7 @@ LDAPControls_from_object(PyObject *list, LDAPControl ***controls_ret) PyObject * LDAPControls_to_List(LDAPControl **ldcs) { - PyObject *res = 0, *pyctrl; + PyObject *retval = NULL, *pytmp = NULL; LDAPControl **tmp = ldcs; Py_ssize_t num_ctrls = 0, i; @@ -173,22 +197,42 @@ LDAPControls_to_List(LDAPControl **ldcs) while (*tmp++) num_ctrls++; - if ((res = PyList_New(num_ctrls)) == NULL) { + if ((retval = PyList_New(num_ctrls)) == NULL) { return NULL; } for (i = 0; i < num_ctrls; i++) { - pyctrl = Py_BuildValue("sbO&", - ldcs[i]->ldctl_oid, - ldcs[i]->ldctl_iscritical, - LDAPberval_to_object, &ldcs[i]->ldctl_value); - if (pyctrl == NULL) { - Py_DECREF(res); - return NULL; + PyObject *pyctrl; + + if ( (pytmp = PyStructSequence_New( &control_tuple_type )) == NULL ) { + goto error; + } + PyList_SET_ITEM( retval, i, pytmp ); + pyctrl = pytmp; + + if ( (pytmp = PyUnicode_FromString( ldcs[i]->ldctl_oid )) == NULL ) { + goto error; + } + PyStructSequence_SET_ITEM( pyctrl, 0, pytmp ); + + if ( (pytmp = PyBool_FromLong( ldcs[i]->ldctl_iscritical )) == NULL ) { + goto error; + } + PyStructSequence_SET_ITEM( pyctrl, 1, pytmp ); + + if ( (pytmp = LDAPberval_to_object( &ldcs[i]->ldctl_value )) == NULL ) { + goto error; } - PyList_SET_ITEM(res, i, pyctrl); + PyStructSequence_SET_ITEM( pyctrl, 2, pytmp ); + pytmp = NULL; } - return res; + + return retval; + +error: + Py_XDECREF(retval); + Py_XDECREF(pytmp); + return NULL; } /* --------------- en-/decoders ------------- */ diff --git a/Modules/ldapmodule.c b/Modules/ldapmodule.c index cb3f58fb..0f837ca5 100644 --- a/Modules/ldapmodule.c +++ b/Modules/ldapmodule.c @@ -30,6 +30,41 @@ static struct PyModuleDef ldap_moduledef = { methods, /* m_methods */ }; +int +LDAPinit_types( PyObject *d ) +{ + /* PyStructSequence types */ + static struct sequence_types { + PyStructSequence_Desc *desc; + PyTypeObject *where; + } sequence_types[] = { + { + .desc = &control_tuple_desc, + .where = &control_tuple_type, + }, + { + .desc = &message_tuple_desc, + .where = &message_tuple_type, + }, + { + .desc = NULL, + } + }, *type; + + for ( type = sequence_types; type->desc; type++ ) { + /* We'd like to use PyStructSequence_NewType from Stable ABI but can't + * until Python 3.8 because of https://bugs.python.org/issue34784 */ + if ( PyStructSequence_InitType2( type->where, type->desc ) ) { + return -1; + } + if ( PyDict_SetItemString( d, type->desc->name, (PyObject *)type->where ) ) { + return -1; + } + } + + return 0; +} + /* module initialisation */ PyMODINIT_FUNC @@ -57,6 +92,9 @@ PyInit__ldap() LDAPinit_functions(d); LDAPinit_control(d); + if (LDAPinit_types(d) == -1) { + return NULL; + } /* Check for errors */ if (PyErr_Occurred()) diff --git a/Modules/pythonldap.h b/Modules/pythonldap.h index 7703af5e..71bf32f4 100644 --- a/Modules/pythonldap.h +++ b/Modules/pythonldap.h @@ -57,6 +57,12 @@ PYLDAP_FUNC(PyObject *) LDAPerror_TypeError(const char *, PyObject *); PYLDAP_FUNC(void) LDAPadd_methods(PyObject *d, PyMethodDef *methods); +PYLDAP_DATA(PyStructSequence_Desc) control_tuple_desc; +PYLDAP_DATA(PyTypeObject) control_tuple_type; + +PYLDAP_DATA(PyStructSequence_Desc) message_tuple_desc; +PYLDAP_DATA(PyTypeObject) message_tuple_type; + #define PyNone_Check(o) ((o) == Py_None) /* *** berval *** */ From 8b3446c65f6e57ef14f255f7486b370f90627130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Wed, 20 Apr 2022 14:41:35 +0100 Subject: [PATCH 3/5] wip --- Doc/spelling_wordlist.txt | 1 + Lib/ldap/connection.py | 65 +++- Lib/ldap/extop/__init__.py | 122 +++++++- Lib/ldap/ldapobject.py | 11 +- Lib/ldap/response.py | 124 ++++++-- Modules/LDAPObject.c | 10 +- Modules/constants.c | 2 +- Modules/ldapcontrol.c | 12 +- Modules/message.c | 6 +- Modules/options.c | 2 +- Modules/pythonldap.h | 2 +- Tests/t_connection.py | 615 +++++++++++++++++++++++++++++++++++++ 12 files changed, 905 insertions(+), 67 deletions(-) create mode 100644 Tests/t_connection.py diff --git a/Doc/spelling_wordlist.txt b/Doc/spelling_wordlist.txt index e2150d9a..1a3dd2c5 100644 --- a/Doc/spelling_wordlist.txt +++ b/Doc/spelling_wordlist.txt @@ -116,6 +116,7 @@ refreshDeletes refreshOnly requestName requestValue +responseName resiter respvalue ResultProcessor diff --git a/Lib/ldap/connection.py b/Lib/ldap/connection.py index d7e043ca..2f6af4ad 100644 --- a/Lib/ldap/connection.py +++ b/Lib/ldap/connection.py @@ -17,10 +17,11 @@ import ldap from ldap.controls import DecodeControlTuples, RequestControl from ldap.extop import ExtendedRequest +from ldap.extop.passwd import PasswordModifyResponse from ldap.ldapobject import SimpleLDAPObject, NO_UNIQUE_ENTRY from ldap.response import ( Response, - SearchEntry, SearchReference, SearchResult, + SearchEntry, SearchReference, IntermediateResponse, ExtendedResult, ) @@ -39,10 +40,15 @@ def __init__(self, uri: Union[LDAPUrl, str, None], **kwargs): super().__init__(uri, **kwargs) def result(self, msgid: int = ldap.RES_ANY, *, all: int = 1, - timeout: Optional[float] = None) -> Optional[list[Response]]: + timeout: Optional[float] = None, + defaultIntermediateClass: + Optional[type[IntermediateResponse]] = None, + defaultExtendedClass: Optional[type[ExtendedResult]] = None + ) -> Optional[list[Response]]: """ - result([msgid: int = RES_ANY [, all: int = 1 [, timeout : - Optional[float] = None]]]) -> Optional[list[Response]] + result([msgid: int = RES_ANY [, all: int = 1 [, + timeout: Optional[float] = None]]]) + -> Optional[list[Response]] This method is used to wait for and return the result of an operation previously initiated by one of the LDAP asynchronous @@ -94,13 +100,26 @@ def result(self, msgid: int = ldap.RES_ANY, *, all: int = 1, results = [] for msgid, msgtype, controls, data in messages: - controls = DecodeControlTuples(controls, self.resp_ctrl_classes) + if controls is not None: + controls = DecodeControlTuples(controls, self.resp_ctrl_classes) + if msgtype == ldap.RES_INTERMEDIATE: + data['defaultClass'] = defaultIntermediateClass + if msgtype == ldap.RES_EXTENDED: + data['defaultClass'] = defaultExtendedClass m = Response(msgid, msgtype, controls, **data) results.append(m) return results + def add_s(self, dn: str, + modlist: list[tuple[str, Union[bytes, list[bytes]]]], *, + ctrls: RequestControls = None) -> ldap.response.AddResult: + msgid = self.add_ext(dn, modlist, serverctrls=ctrls) + responses = self.result(msgid) + result, = responses + return result + def bind_s(self, dn: Optional[str] = None, cred: Optional[AnyStr] = None, *, method: int = ldap.AUTH_SIMPLE, @@ -126,17 +145,36 @@ def delete_s(self, dn: str, *, result, = responses return result - def extop_s(self, oid: Optional[str] = None, + def extop_s(self, name: Optional[str] = None, value: Optional[bytes] = None, *, request: Optional[ExtendedRequest] = None, - ctrls: RequestControls = None + ctrls: RequestControls = None, + defaultIntermediateClass: Optional[type[IntermediateResponse]] = None, + defaultExtendedClass: Optional[type[ExtendedResult]] = None ) -> list[Union[IntermediateResponse, ExtendedResult]]: if request is not None: - oid = request.requestName + name = request.requestName value = request.encodedRequestValue() - msgid = self.extop(oid, value, serverctrls=ctrls) - return self.result(msgid) + msgid = self.extop(name, value, serverctrls=ctrls) + return self.result(msgid, + defaultIntermediateClass=defaultIntermediateClass, + defaultExtendedClass=defaultExtendedClass) + + def modify_s(self, dn: str, + modlist: list[tuple[str, Union[bytes, list[bytes]]]], *, + ctrls: RequestControls = None) -> ldap.response.ModifyResult: + msgid = self.modify_ext(dn, modlist, serverctrls=ctrls) + responses = self.result(msgid) + result, = responses + return result + + def passwd_s(self, user: Optional[str] = None, + oldpw: Optional[bytes] = None, newpw: Optional[bytes] = None, + ctrls: RequestControls = None) -> PasswordModifyResponse: + msgid = self.passwd(user, oldpw, newpw, serverctrls=ctrls) + res, = self.result(msgid, defaultExtendedClass=PasswordModifyResponse) + return res def search_s(self, base: Optional[str] = None, scope: int = ldap.SCOPE_SUBTREE, @@ -154,8 +192,11 @@ def search_s(self, base: Optional[str] = None, attrsonly=attrsonly, serverctrls=ctrls, sizelimit=sizelimit, timeout=timelimit) result = self.result(msgid, timeout=timeout) + # FIXME: we want a better way of returning a result with multiple + # messages, always useful in searches but other operations can also + # elicit those (by way of an IntermediateResponse) result[-1].raise_for_result() - return result[:-1] + return result def search_subschemasubentry_s( self, dn: Optional[str] = None) -> Optional[str]: @@ -219,6 +260,6 @@ def find_unique_entry(self, base: Optional[str] = None, r = self.search_s(base, scope, filter, attrlist=attrlist, attrsonly=attrsonly, ctrls=ctrls, timeout=timeout, sizelimit=2) - if len(r) != 1: + if len(r) != 2: raise NO_UNIQUE_ENTRY(f'No or non-unique search result for {filter}') return r[0] diff --git a/Lib/ldap/extop/__init__.py b/Lib/ldap/extop/__init__.py index a4383f78..df2be8f1 100644 --- a/Lib/ldap/extop/__init__.py +++ b/Lib/ldap/extop/__init__.py @@ -12,6 +12,13 @@ from ldap import __version__ from ldap import KNOWN_EXTENDED_RESPONSES, KNOWN_INTERMEDIATE_RESPONSES +import ldap +import ldap.response + +from typing import Optional + +_NOTSET = object() + class ExtendedRequest: """ @@ -39,7 +46,7 @@ def encodedRequestValue(self): return self.requestValue -class ExtendedResponse: +class ExtendedResponse(ldap.response.ExtendedResult): """ Generic base class for a LDAPv3 extended operation response @@ -55,9 +62,116 @@ def __init_subclass__(cls): KNOWN_EXTENDED_RESPONSES.setdefault(cls.responseName, cls) - def __init__(self,responseName,encodedResponseValue): + @classmethod + def __convert_old_api(cls, responseName_or_msgid=_NOTSET, + encodedResponseValue_or_msgtype=_NOTSET, + controls=None, *, + result=_NOTSET, matcheddn=_NOTSET, message=_NOTSET, + referrals=_NOTSET, name=None, value=None, + defaultClass: Optional[type['ExtendedResult']] = None, + msgid=_NOTSET, msgtype=_NOTSET, + responseName=_NOTSET, encodedResponseValue=_NOTSET, + **kwargs): + """ + Implements both old and new API: + __init__(self, responseName, encodedResponseValue) + and + __init__/__new__(self, msgid, msgtype, controls=None, *, + result, matcheddn, message, referrals, + defaultClass=None, **kwargs) + """ + if responseName is not _NOTSET: + name = responseName + value = encodedResponseValue + msgid = None + msgtype = ldap.RES_EXTENDED + result = ldap.SUCCESS.errnum + elif responseName_or_msgid is not _NOTSET and \ + isinstance(responseName_or_msgid, (str, type(None))): + if responseName is not _NOTSET: + raise TypeError("responseName passed twice") + if encodedResponseValue_or_msgtype is not _NOTSET and \ + encodedResponseValue is not _NOTSET: + raise TypeError("encodedResponseValue passed twice") + name = responseName = responseName_or_msgid + value = encodedResponseValue = encodedResponseValue_or_msgtype + msgid = None + msgtype = ldap.RES_EXTENDED + result = ldap.SUCCESS.errnum + else: + responseName = name + encodedResponseValue = value + if msgid is _NOTSET: + if responseName_or_msgid is _NOTSET: + raise TypeError("msgid parameter not provided") + msgid = responseName_or_msgid + if msgtype is _NOTSET: + if encodedResponseValue_or_msgtype is _NOTSET: + raise TypeError("msgtype parameter not provided") + msgtype = encodedResponseValue_or_msgtype or ldap.RES_EXTENDED + if result is _NOTSET: + raise TypeError("result parameter not provided") + if matcheddn is _NOTSET: + raise TypeError("matcheddn parameter not provided") + if message is _NOTSET: + raise TypeError("message parameter not provided") + if referrals is _NOTSET: + raise TypeError("referrals parameter not provided") + + return ( + responseName, encodedResponseValue, + (msgid, msgtype, controls), + {'result': result, + 'matcheddn': matcheddn, + 'message': message, + 'referrals': referrals, + 'name': name, + 'value': value, + 'defaultClass': defaultClass, + **kwargs + } + ) + + def __new__(cls, *args, **kwargs): + """ + Has to support both old and new API: + __new__(cls, responseName: Optional[str], + encodedResponseValue: Optional[bytes]) + and + __new__(cls, msgid: int, msgtype: int, controls: Controls = None, *, + result: int, matcheddn: str, message: str, referrals: List[str], + defaultClass: Optional[type[ExtendedResponse]] = None, + **kwargs) + + The old API is deprecated and will be removed in 4.0. + """ + # TODO: retire polymorhpism when old API is removed (4.0?) + _, _, args, kwargs = __class__.__convert_old_api(*args, **kwargs) + + return super().__new__(cls, *args, **kwargs) + + def __init__(self, *args, **kwargs): + """ + Supports both old and new API: + __init__(self, responseName: Optional[str], + encodedResponseValue: Optional[bytes]) + and + __init__(self, msgid: int, msgtype: int, controls: Controls = None, *, + result: int, matcheddn: str, message: str, referrals: List[str], + defaultClass: Optional[type[ExtendedResponse]] = None, + **kwargs) + + The old API is deprecated and will be removed in 4.0. + """ + # TODO: retire polymorhpism when old API is removed (4.0?) + responseName, encodedResponseValue, _, _ = \ + __class__.__convert_old_api(*args, **kwargs) + self.responseName = responseName - self.responseValue = self.decodeResponseValue(encodedResponseValue) + if encodedResponseValue is not None: + self.responseValue = self.decodeResponseValue(encodedResponseValue) + else: + self.responseValue = None def __repr__(self): return f'{self.__class__.__name__}({self.responseName},{self.responseValue})' @@ -70,7 +184,7 @@ def decodeResponseValue(self,value): return value -class IntermediateResponse: +class IntermediateResponse(ldap.response.IntermediateResponse): """ Generic base class for a LDAPv3 intermediate response message diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index 7a9c17f6..a8abeeb9 100644 --- a/Lib/ldap/ldapobject.py +++ b/Lib/ldap/ldapobject.py @@ -380,14 +380,17 @@ def extop_result(self,msgid=ldap.RES_ANY,all=1,timeout=None): def extop_s(self,extreq,serverctrls=None,clientctrls=None,extop_resp_class=None): msgid = self.extop(extreq,serverctrls,clientctrls) - res = self.extop_result(msgid,all=1,timeout=self.timeout) + resulttype,_,msgid,respctrls,respoid,respvalue = self.extop_result(msgid,all=1,timeout=self.timeout) + extop_resp_class = extop_resp_class or KNOWN_EXTENDED_RESPONSES.get(respoid) if extop_resp_class: - respoid,respvalue = res if extop_resp_class.responseName!=respoid: raise ldap.PROTOCOL_ERROR(f"Wrong OID in extended response! Expected {extop_resp_class.responseName}, got {respoid}") - return extop_resp_class(extop_resp_class.responseName,respvalue) + return extop_resp_class(msgid, resulttype, respctrls, + result=0, matcheddn=None, + message=None, referrals=None, + name=respoid, value=respvalue) else: - return res + return respoid, respvalue def modify_ext(self,dn,modlist,serverctrls=None,clientctrls=None): """ diff --git a/Lib/ldap/response.py b/Lib/ldap/response.py index 1e75c4ad..a9221791 100644 --- a/Lib/ldap/response.py +++ b/Lib/ldap/response.py @@ -29,7 +29,6 @@ import ldap from ldap.controls import ResponseControl -from ldap.extop import ExtendedResponse, Intermediate _SUCCESS_CODES = [ @@ -43,7 +42,7 @@ class Response: msgid: int msgtype: int - controls: list[ResponseControl] + controls: Optional[list[ResponseControl]] __subclasses: dict[int, type] = {} @@ -65,12 +64,19 @@ def __new__(cls, msgid, msgtype, controls=None, **kwargs): if c: return c.__new__(c, msgid, msgtype, controls, **kwargs) - instance = super().__new__(cls) + instance = super().__new__(cls, **kwargs) instance.msgid = msgid instance.msgtype = msgtype instance.controls = controls return instance + def __repr__(self): + optional = "" + if self.controls is not None: + optional += f", controls={self.controls}" + return (f"{self.__class__.__name__}(msgid={self.msgid}, " + f"msgtype={self.msgtype}{optional})") + class Result(Response): result: int @@ -78,7 +84,7 @@ class Result(Response): message: str referrals: Optional[list[str]] - def __new__(cls, msgid, msgtype, controls, + def __new__(cls, msgid, msgtype, controls=None, *, result, matcheddn, message, referrals, **kwargs): instance = super().__new__(cls, msgid, msgtype, controls, **kwargs) @@ -95,8 +101,13 @@ def raise_for_result(self) -> 'Result': raise ldap._exceptions.get(self.result, ldap.LDAPError)(self) def __repr__(self): + optional = "" + if self.controls is not None: + optional = f", controls={self.controls}" + if self.message: + optional = f", message={self.message!r}" return (f"{self.__class__.__name__}" - f"(msgid={self.msgid}, result={self.result})") + f"(msgid={self.msgid}, result={self.result}{optional})") class SearchEntry(Response): @@ -105,7 +116,8 @@ class SearchEntry(Response): dn: str attrs: dict[str, Optional[list[bytes]]] - def __new__(cls, msgid, msgtype, controls, dn, attrs, **kwargs): + def __new__(cls, msgid, msgtype, controls=None, *, + dn: str, attrs: dict[str, Optional[list[bytes]]], **kwargs): instance = super().__new__(cls, msgid, msgtype, controls, **kwargs) instance.dn = dn @@ -119,7 +131,8 @@ class SearchReference(Response): referrals: list[str] - def __new__(cls, msgid, msgtype, controls, referrals, **kwargs): + def __new__(cls, msgid, msgtype, controls=None, *, + referrals, **kwargs): instance = super().__new__(cls, msgid, msgtype, controls, **kwargs) instance.referrals = referrals @@ -134,28 +147,43 @@ class SearchResult(Result): class IntermediateResponse(Response): msgtype = ldap.RES_INTERMEDIATE - oid: Optional[str] + name: Optional[str] value: Optional[bytes] - __subclasses: dict[str, type] = {} - - def __new__(cls, msgid, msgtype, controls=None, name=None, - value=None, *, defaultClass: Optional[Intermediate] = None, + def __new__(cls, msgid, msgtype, controls=None, *, + name=None, value=None, + defaultClass: Optional[type['IntermediateResponse']] = None, **kwargs): if cls is not __class__: - instance = super().__new__(cls, ) + instance = super().__new__(cls, msgid, msgtype, controls, **kwargs) + instance.name = name + instance.value = value return instance - c = __class__.__subclasses.get(msgtype) + c = ldap.KNOWN_INTERMEDIATE_RESPONSES.get(name, defaultClass) if c: - return c.__new__(c, msgid, msgtype, controls, **kwargs) + instance = c.__new__(c, msgid, msgtype, controls, + name=name, value=value, **kwargs) + if hasattr(instance, 'decode'): + instance.decode(value) + return instance - instance = super().__new__(cls) - instance.msgid = msgid - instance.msgtype = msgtype - instance.controls = controls + instance = super().__new__(cls, msgid, msgtype, controls, **kwargs) + instance.name = name + instance.value = value return instance + def __repr__(self): + optional = "" + if self.name is not None: + optional += f", name={self.name}" + if self.value is not None: + optional += f", value={self.value}" + if self.controls is not None: + optional += f", controls={self.controls}" + return (f"{self.__class__.__name__}" + f"(msgid={self.msgid}{optional})") + class BindResult(Result): msgtype = ldap.RES_BIND @@ -193,24 +221,52 @@ def __bool__(self) -> bool: class ExtendedResult(Result): msgtype = ldap.RES_EXTENDED - oid: Optional[str] + responseName: Optional[str] value: Optional[bytes] - # TODO: how to subclass these dynamically? (UnsolicitedResponse, ...), - # is it just with __new__? + def __new__(cls, msgid, msgtype, controls=None, *, + result, matcheddn, message, referrals, + name=None, value=None, + defaultClass: Optional[type['ExtendedResult']] = None, + **kwargs): + if cls is not __class__: + instance = super().__new__(cls, msgid, msgtype, controls, + result=result, matcheddn=matcheddn, + message=message, referrals=referrals) + instance.name = name + instance.value = value + return instance -class UnsolicitedResponse(ExtendedResult): - msgid = ldap.RES_UNSOLICITED + c = ldap.KNOWN_EXTENDED_RESPONSES.get(name, defaultClass) + if not c and msgid == ldap.RES_UNSOLICITED: + c = UnsolicitedNotification - __subclasses: dict[str, type] = {} + if c: + return c.__new__(c, msgid, msgtype, controls, + result=result, matcheddn=matcheddn, + message=message, referrals=referrals, + name=name, value=value, **kwargs) + + instance = super().__new__(cls, msgid, msgtype, controls, + result=result, matcheddn=matcheddn, + message=message, referrals=referrals) + instance.name = name + instance.value = value + return instance + + def __repr__(self): + optional = "" + if self.name is not None: + optional += f", name={self.name}" + if self.value is not None: + optional += f", value={self.value}" + if self.message: + optional = f", message={self.message!r}" + if self.controls is not None: + optional += f", controls={self.controls}" + return (f"{self.__class__.__name__}" + f"(msgid={self.msgid}, result={self.result}{optional})") - def __new__(cls, msgid, msgtype, controls=None, *, - name, value=None, **kwargs): - if cls is __class__: - c = __class__.__subclasses.get(msgtype) - if c: - return c.__new__(c, msgid, msgtype, controls, - name=name, value=value, **kwargs) - return super().__new__(cls, msgid, msgtype, controls, - name=name, value=value, **kwargs) +class UnsolicitedNotification(ExtendedResult): + msgid = ldap.RES_UNSOLICITED diff --git a/Modules/LDAPObject.c b/Modules/LDAPObject.c index 779e175f..7c3ade7c 100644 --- a/Modules/LDAPObject.c +++ b/Modules/LDAPObject.c @@ -1225,7 +1225,7 @@ l_ldap_result(LDAPObject *self, PyObject *args) &controls )) != LDAP_SUCCESS ) { goto error; } - pytmp = LDAPControls_to_List( controls ); + pytmp = LDAPControls_to_List( controls, 0 ); ldap_controls_free( controls ); if ( pytmp == NULL ) { goto error; @@ -1262,7 +1262,7 @@ l_ldap_result(LDAPObject *self, PyObject *args) Py_DECREF( pytmp ); refs_list = pytmp; - pytmp = LDAPControls_to_List( controls ); + pytmp = LDAPControls_to_List( controls, 0 ); ldap_controls_free( controls ); if ( pytmp == NULL ) { ldap_memvfree( (void **)refs ); @@ -1325,7 +1325,7 @@ l_ldap_result(LDAPObject *self, PyObject *args) } } - pytmp = LDAPControls_to_List( controls ); + pytmp = LDAPControls_to_List( controls, 0 ); ldap_controls_free( controls ); if ( pytmp == NULL ) { goto error; @@ -1407,7 +1407,7 @@ l_ldap_result(LDAPObject *self, PyObject *args) } ldap_memvfree( (void **)referrals ); - pytmp = LDAPControls_to_List( controls ); + pytmp = LDAPControls_to_List( controls, 0 ); ldap_controls_free( controls ); if ( pytmp == NULL ) { goto error; @@ -1594,7 +1594,7 @@ l_ldap_result4(LDAPObject *self, PyObject *args) return LDAPraise_for_message(self->ldap, msg); } - if (!(pyctrls = LDAPControls_to_List(serverctrls))) { + if (!(pyctrls = LDAPControls_to_List(serverctrls, 1))) { int err = LDAP_NO_MEMORY; LDAP_BEGIN_ALLOW_THREADS(self); diff --git a/Modules/constants.c b/Modules/constants.c index d51f054c..87811def 100644 --- a/Modules/constants.c +++ b/Modules/constants.c @@ -135,7 +135,7 @@ LDAPraise_for_message(LDAP *l, LDAPMessage *m) Py_XDECREF(pyerrno); } - if (!(pyctrls = LDAPControls_to_List(serverctrls))) { + if (!(pyctrls = LDAPControls_to_List(serverctrls, 1))) { int err = LDAP_NO_MEMORY; ldap_set_option(l, LDAP_OPT_ERROR_NUMBER, &err); diff --git a/Modules/ldapcontrol.c b/Modules/ldapcontrol.c index da7a1484..1bf0fdaf 100644 --- a/Modules/ldapcontrol.c +++ b/Modules/ldapcontrol.c @@ -149,6 +149,11 @@ LDAPControls_from_object(PyObject *list, LDAPControl ***controls_ret) LDAPControl *ldc; PyObject *item; + if (PyNone_Check(list)) { + *controls_ret = NULL; + return 0; + } + if (!PySequence_Check(list)) { LDAPerror_TypeError("LDAPControls_from_object(): expected a list", list); @@ -187,15 +192,18 @@ LDAPControls_from_object(PyObject *list, LDAPControl ***controls_ret) } PyObject * -LDAPControls_to_List(LDAPControl **ldcs) +LDAPControls_to_List(LDAPControl **ldcs, int require_list) { PyObject *retval = NULL, *pytmp = NULL; LDAPControl **tmp = ldcs; Py_ssize_t num_ctrls = 0, i; - if (tmp) + if (tmp) { while (*tmp++) num_ctrls++; + } else if (!require_list) { + Py_RETURN_NONE; + } if ((retval = PyList_New(num_ctrls)) == NULL) { return NULL; diff --git a/Modules/message.c b/Modules/message.c index f1403237..c658feaf 100644 --- a/Modules/message.c +++ b/Modules/message.c @@ -69,7 +69,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, } /* convert serverctrls to list of tuples */ - if (!(pyctrls = LDAPControls_to_List(serverctrls))) { + if (!(pyctrls = LDAPControls_to_List(serverctrls, 1))) { int err = LDAP_NO_MEMORY; ldap_set_option(ld, LDAP_OPT_ERROR_NUMBER, &err); @@ -200,7 +200,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, return LDAPerror(ld); } /* convert serverctrls to list of tuples */ - if (!(pyctrls = LDAPControls_to_List(serverctrls))) { + if (!(pyctrls = LDAPControls_to_List(serverctrls, 1))) { int err = LDAP_NO_MEMORY; ldap_set_option(ld, LDAP_OPT_ERROR_NUMBER, &err); @@ -254,7 +254,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, return LDAPerror(ld); } /* convert serverctrls to list of tuples */ - if (!(pyctrls = LDAPControls_to_List(serverctrls))) { + if (!(pyctrls = LDAPControls_to_List(serverctrls, 1))) { int err = LDAP_NO_MEMORY; ldap_set_option(ld, LDAP_OPT_ERROR_NUMBER, &err); diff --git a/Modules/options.c b/Modules/options.c index 4577b075..1e3f39fa 100644 --- a/Modules/options.c +++ b/Modules/options.c @@ -481,7 +481,7 @@ LDAP_get_option(LDAPObject *self, int option) if (res != LDAP_OPT_SUCCESS) return option_error(res, "ldap_get_option"); - v = LDAPControls_to_List(lcs); + v = LDAPControls_to_List(lcs, 1); ldap_controls_free(lcs); return v; diff --git a/Modules/pythonldap.h b/Modules/pythonldap.h index 71bf32f4..7f152402 100644 --- a/Modules/pythonldap.h +++ b/Modules/pythonldap.h @@ -92,7 +92,7 @@ PYLDAP_FUNC(void) LDAPinit_functions(PyObject *); PYLDAP_FUNC(void) LDAPinit_control(PyObject *d); PYLDAP_FUNC(void) LDAPControl_List_DEL(LDAPControl **); PYLDAP_FUNC(int) LDAPControls_from_object(PyObject *, LDAPControl ***); -PYLDAP_FUNC(PyObject *) LDAPControls_to_List(LDAPControl **ldcs); +PYLDAP_FUNC(PyObject *) LDAPControls_to_List(LDAPControl **ldcs, int require_list); /* *** ldapobject *** */ typedef struct { diff --git a/Tests/t_connection.py b/Tests/t_connection.py new file mode 100644 index 00000000..38135084 --- /dev/null +++ b/Tests/t_connection.py @@ -0,0 +1,615 @@ +""" +Automatic tests for python-ldap's module ldap.ldapobject + +See https://www.python-ldap.org/ for details. +""" +import errno +import linecache +import os +import socket +import unittest +import pickle + +# Switch off processing .ldaprc or ldap.conf before importing _ldap +os.environ['LDAPNOINIT'] = '1' + +import ldap +import ldap.response +from ldap.connection import Connection + +from slapdtest import SlapdTestCase +from slapdtest import requires_ldapi, requires_sasl, requires_tls +from slapdtest import requires_init_fd + + +LDIF_TEMPLATE = """dn: %(suffix)s +objectClass: dcObject +objectClass: organization +dc: %(dc)s +o: %(dc)s + +dn: %(rootdn)s +objectClass: applicationProcess +objectClass: simpleSecurityObject +cn: %(rootcn)s +userPassword: %(rootpw)s + +dn: cn=user1,%(suffix)s +objectClass: applicationProcess +objectClass: simpleSecurityObject +cn: user1 +userPassword: user1_pw + +dn: cn=Foo1,%(suffix)s +objectClass: organizationalRole +cn: Foo1 + +dn: cn=Foo2,%(suffix)s +objectClass: organizationalRole +cn: Foo2 + +dn: cn=Foo3,%(suffix)s +objectClass: organizationalRole +cn: Foo3 + +dn: ou=Container,%(suffix)s +objectClass: organizationalUnit +ou: Container + +dn: cn=Foo4,ou=Container,%(suffix)s +objectClass: organizationalRole +cn: Foo4 + +""" + +SCHEMA_TEMPLATE = """dn: cn=mySchema,cn=schema,cn=config +objectClass: olcSchemaConfig +cn: mySchema +olcAttributeTypes: ( 1.3.6.1.4.1.56207.1.1.1 NAME 'myAttribute' + DESC 'fobar attribute' + EQUALITY caseExactMatch + ORDERING caseExactOrderingMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'foobar' ) +olcObjectClasses: ( 1.3.6.1.4.1.56207.1.2.2 NAME 'myClass' + DESC 'foobar objectclass' + SUP top + STRUCTURAL + MUST myAttribute + X-ORIGIN 'foobar' )""" + + +class Test00_Connection(SlapdTestCase): + """ + test LDAP search operations + """ + + ldap_object_class = Connection + + @classmethod + def setUpClass(cls): + super().setUpClass() + # insert some Foo* objects via ldapadd + cls.server.ldapadd( + LDIF_TEMPLATE % { + 'suffix':cls.server.suffix, + 'rootdn':cls.server.root_dn, + 'rootcn':cls.server.root_cn, + 'rootpw':cls.server.root_pw, + 'dc': cls.server.suffix.split(',')[0][3:], + } + ) + + def setUp(self): + try: + self._ldap_conn + except AttributeError: + # open local LDAP connection + self._ldap_conn = self._open_ldap_conn(bytes_mode=False) + + def tearDown(self): + del self._ldap_conn + + def reset_connection(self): + try: + del self._ldap_conn + except AttributeError: + pass + + self._ldap_conn = self._open_ldap_conn(bytes_mode=False) + + def test_typechecks(self): + base = self.server.suffix + l = self._ldap_conn + + with self.assertRaises(TypeError) as e: + l.search_s( + base.encode('utf-8'), ldap.SCOPE_SUBTREE, '(cn=Foo*)', ['*'] + ) + # Python 3.4.x does not include 'search_ext()' in message + self.assertEqual( + "search_ext() argument 1 must be str, not bytes", + str(e.exception) + ) + + with self.assertRaises(TypeError) as e: + l.search_s( + base, ldap.SCOPE_SUBTREE, b'(cn=Foo*)', ['*'] + ) + self.assertEqual( + "search_ext() argument 3 must be str, not bytes", + str(e.exception) + ) + + with self.assertRaises(TypeError) as e: + l.search_s( + base, ldap.SCOPE_SUBTREE, '(cn=Foo*)', [b'*'] + ) + self.assertEqual( + ('attrs_from_List(): expected string in list', b'*'), + e.exception.args + ) + + def test_search_keys_are_text(self): + base = self.server.suffix + l = self._ldap_conn + responses = l.search_s(base, ldap.SCOPE_SUBTREE, '(cn=Foo*)', ['*']) + entries, result = responses[:-1], responses[-1] + entries = sorted((e.dn, e.attrs) for e in entries) + dn, fields = entries[0] + self.assertEqual(dn, 'cn=Foo1,%s' % base) + self.assertEqual(type(dn), str) + for key, values in fields.items(): + self.assertEqual(type(key), str) + for value in values: + self.assertEqual(type(value), bytes) + + def test_search_accepts_unicode_dn(self): + base = self.server.suffix + l = self._ldap_conn + + with self.assertRaises(ldap.NO_SUCH_OBJECT): + result = l.search_s("CN=abc\U0001f498def", ldap.SCOPE_SUBTREE) + + def test_filterstr_accepts_unicode(self): + l = self._ldap_conn + base = self.server.suffix + responses = l.search_s(base, ldap.SCOPE_SUBTREE, '(cn=abc\U0001f498def)', ['*']) + entries, result = responses[:-1], responses[-1] + self.assertEqual(entries, []) + self.assertIsInstance(result, ldap.response.SearchResult) + + def test_attrlist_accepts_unicode(self): + base = self.server.suffix + responses = self._ldap_conn.search_s( + base, ldap.SCOPE_SUBTREE, + '(cn=Foo*)', ['abc', 'abc\U0001f498def']) + entries, result = responses[:-1], responses[-1] + + for entry in entries: + self.assertIsInstance(entry.dn, str) + self.assertEqual(entry.attrs, {}) + + def test001_search_subtree(self): + responses = self._ldap_conn.search_s( + self.server.suffix, + ldap.SCOPE_SUBTREE, + '(cn=Foo*)', + attrlist=['*'], + ) + entries, result = responses[:-1], responses[-1] + entries = sorted((e.dn, e.attrs) for e in entries) + self.assertEqual( + entries, + [ + ( + 'cn=Foo1,'+self.server.suffix, + {'cn': [b'Foo1'], 'objectClass': [b'organizationalRole']} + ), + ( + 'cn=Foo2,'+self.server.suffix, + {'cn': [b'Foo2'], 'objectClass': [b'organizationalRole']} + ), + ( + 'cn=Foo3,'+self.server.suffix, + {'cn': [b'Foo3'], 'objectClass': [b'organizationalRole']} + ), + ( + 'cn=Foo4,ou=Container,'+self.server.suffix, + {'cn': [b'Foo4'], 'objectClass': [b'organizationalRole']} + ), + ] + ) + + def test002_search_onelevel(self): + responses = self._ldap_conn.search_s( + self.server.suffix, + ldap.SCOPE_ONELEVEL, + '(cn=Foo*)', + ['*'], + ) + entries, result = responses[:-1], responses[-1] + entries = sorted((e.dn, e.attrs) for e in entries) + self.assertEqual( + entries, + [ + ( + 'cn=Foo1,'+self.server.suffix, + {'cn': [b'Foo1'], 'objectClass': [b'organizationalRole']} + ), + ( + 'cn=Foo2,'+self.server.suffix, + {'cn': [b'Foo2'], 'objectClass': [b'organizationalRole']} + ), + ( + 'cn=Foo3,'+self.server.suffix, + {'cn': [b'Foo3'], 'objectClass': [b'organizationalRole']} + ), + ] + ) + + def test003_search_oneattr(self): + responses = self._ldap_conn.search_s( + self.server.suffix, + ldap.SCOPE_SUBTREE, + '(cn=Foo4)', + ['cn'], + ) + entries, result = responses[:-1], responses[-1] + entries = sorted((e.dn, e.attrs) for e in entries) + self.assertEqual( + entries, + [('cn=Foo4,ou=Container,'+self.server.suffix, {'cn': [b'Foo4']})] + ) + + def test_find_unique_entry(self): + entry = self._ldap_conn.find_unique_entry( + self.server.suffix, + ldap.SCOPE_SUBTREE, + '(cn=Foo4)', + ['cn'], + ) + self.assertEqual( + (entry.dn, entry.attrs), + ('cn=Foo4,ou=Container,'+self.server.suffix, {'cn': [b'Foo4']}) + ) + with self.assertRaises(ldap.SIZELIMIT_EXCEEDED): + # > 2 entries returned + self._ldap_conn.find_unique_entry( + self.server.suffix, + ldap.SCOPE_ONELEVEL, + '(cn=Foo*)', + ['*'], + ) + with self.assertRaises(ldap.NO_UNIQUE_ENTRY): + # 0 entries returned + self._ldap_conn.find_unique_entry( + self.server.suffix, + ldap.SCOPE_ONELEVEL, + '(cn=Bar*)', + ['*'], + ) + + def test_search_subschema(self): + l = self._ldap_conn + dn = l.search_subschemasubentry_s() + self.assertIsInstance(dn, str) + self.assertEqual(dn, "cn=Subschema") + subschema = l.read_subschemasubentry_s(dn) + self.assertIsInstance(subschema, dict) + self.assertEqual( + sorted(subschema), + [ + 'attributeTypes', + 'ldapSyntaxes', + 'matchingRuleUse', + 'matchingRules', + 'objectClasses' + ] + ) + + def test004_enotconn(self): + l = self.ldap_object_class('ldap://127.0.0.1:42') + try: + m = l.simple_bind_s("", "") + r = l.result(m, ldap.MSG_ALL, self.timeout) + except ldap.SERVER_DOWN as ldap_err: + errno_val = ldap_err.args[0]['errno'] + if errno_val != errno.ENOTCONN: + self.fail("expected errno=%d, got %d" + % (errno.ENOTCONN, errno_val)) + info = ldap_err.args[0]['info'] + expected_info = os.strerror(errno.ENOTCONN) + if info != expected_info: + self.fail(f"expected info={expected_info!r}, got {info!r}") + else: + self.fail("expected SERVER_DOWN, got %r" % r) + + def test005_invalid_credentials(self): + l = self.ldap_object_class(self.server.ldap_uri) + # search with invalid filter + with self.assertRaises(ldap.INVALID_CREDENTIALS): + m = l.simple_bind(self.server.root_dn, self.server.root_pw+'wrong') + r, = l.result(m, all=ldap.MSG_ALL) + r.raise_for_result() + + @requires_sasl() + @requires_ldapi() + def test006_sasl_external_bind_s(self): + l = self.ldap_object_class(self.server.ldapi_uri) + l.sasl_external_bind_s() + self.assertEqual(l.whoami_s(), 'dn:'+self.server.root_dn.lower()) + authz_id = 'dn:cn=Foo2,%s' % (self.server.suffix) + l = self.ldap_object_class(self.server.ldapi_uri) + l.sasl_external_bind_s(authz_id=authz_id) + self.assertEqual(l.whoami_s(), authz_id.lower()) + + @requires_sasl() + @requires_ldapi() + def test006_sasl_options(self): + l = self.ldap_object_class(self.server.ldapi_uri) + + minssf = l.get_option(ldap.OPT_X_SASL_SSF_MIN) + self.assertGreaterEqual(minssf, 0) + self.assertLessEqual(minssf, 256) + maxssf = l.get_option(ldap.OPT_X_SASL_SSF_MAX) + self.assertGreaterEqual(maxssf, 0) + # libldap sets SSF_MAX to INT_MAX + self.assertLessEqual(maxssf, 2**31 - 1) + + l.set_option(ldap.OPT_X_SASL_SSF_MIN, 56) + l.set_option(ldap.OPT_X_SASL_SSF_MAX, 256) + self.assertEqual(l.get_option(ldap.OPT_X_SASL_SSF_MIN), 56) + self.assertEqual(l.get_option(ldap.OPT_X_SASL_SSF_MAX), 256) + + l.sasl_external_bind_s() + with self.assertRaisesRegex(ValueError, "write-only option"): + l.get_option(ldap.OPT_X_SASL_SSF_EXTERNAL) + l.set_option(ldap.OPT_X_SASL_SSF_EXTERNAL, 256) + self.assertEqual(l.whoami_s(), 'dn:' + self.server.root_dn.lower()) + + def test007_timeout(self): + l = self.ldap_object_class(self.server.ldap_uri) + m = l.search_ext(self.server.suffix, ldap.SCOPE_SUBTREE, '(objectClass=*)') + l.abandon(m) + with self.assertRaises(ldap.TIMEOUT): + l.result(m, timeout=0.001) + + def assertIsSubclass(self, cls, other): + self.assertTrue( + issubclass(cls, other), + cls.__mro__ + ) + + def test_simple_bind_noarg(self): + l = self.ldap_object_class(self.server.ldap_uri) + l.simple_bind_s() + self.assertEqual(l.whoami_s(), '') + l = self.ldap_object_class(self.server.ldap_uri) + l.simple_bind_s(None, None) + self.assertEqual(l.whoami_s(), '') + + def _check_byteswarning(self, warning, expected_message): + self.assertIs(warning.category, ldap.LDAPBytesWarning) + self.assertIn(expected_message, str(warning.message)) + + def _normalize(filename): + # Python 2 likes to report the ".pyc" file in warnings, + # tracebacks or __file__. + # Use the corresponding ".py" in that case. + if filename.endswith('.pyc'): + return filename[:-1] + return filename + + # Assert warning points to a line marked CORRECT LINE in this file + self.assertEquals(_normalize(warning.filename), _normalize(__file__)) + self.assertIn( + 'CORRECT LINE', + linecache.getline(warning.filename, warning.lineno) + ) + + @requires_tls() + def test_multiple_starttls(self): + # Test for openldap does not re-register nss shutdown callbacks + # after nss_Shutdown is called + # https://github.com/python-ldap/python-ldap/issues/60 + # https://bugzilla.redhat.com/show_bug.cgi?id=1520990 + for _ in range(10): + l = self.ldap_object_class(self.server.ldap_uri) + l.set_option(ldap.OPT_X_TLS_CACERTFILE, self.server.cafile) + l.set_option(ldap.OPT_X_TLS_NEWCTX, 0) + l.start_tls_s() + l.simple_bind_s(self.server.root_dn, self.server.root_pw) + self.assertEqual(l.whoami_s(), 'dn:' + self.server.root_dn) + + def test_dse(self): + dse = self._ldap_conn.read_rootdse_s() + self.assertIsInstance(dse, dict) + self.assertEqual(dse['supportedLDAPVersion'], [b'3']) + keys = set(dse) + # SASL info may be missing in restricted build environments + keys.discard('supportedSASLMechanisms') + self.assertEqual( + keys, + {'configContext', 'entryDN', 'namingContexts', 'objectClass', + 'structuralObjectClass', 'subschemaSubentry', + 'supportedControl', 'supportedExtension', 'supportedFeatures', + 'supportedLDAPVersion'} + ) + self.assertEqual( + self._ldap_conn.get_naming_contexts(), + [self.server.suffix.encode('utf-8')] + ) + + def test_compare_s_true(self): + base = self.server.suffix + l = self._ldap_conn + result = l.compare_s('cn=Foo1,%s' % base, 'cn', b'Foo1') + self.assertIs(result, True) + + def test_compare_s_false(self): + base = self.server.suffix + l = self._ldap_conn + result = l.compare_s('cn=Foo1,%s' % base, 'cn', b'Foo2') + self.assertIs(result, False) + + def test_compare_s_notfound(self): + base = self.server.suffix + l = self._ldap_conn + with self.assertRaises(ldap.NO_SUCH_OBJECT): + result = l.compare_s('cn=invalid,%s' % base, 'cn', b'Foo2') + + def test_compare_s_invalidattr(self): + base = self.server.suffix + l = self._ldap_conn + with self.assertRaises(ldap.UNDEFINED_TYPE): + result = l.compare_s('cn=Foo1,%s' % base, 'invalidattr', b'invalid') + + def test_result_compare_true(self): + base = self.server.suffix + l = self._ldap_conn + msgid = l.compare('cn=Foo1,%s' % base, 'cn', b'Foo1') + responses = l.result() + result, = responses + self.assertEqual(result.msgid, msgid) + self.assertIsInstance(result, ldap.response.CompareResult) + result.raise_for_result() + self.assertEqual(result.result, ldap.COMPARE_TRUE.errnum) + self.assertTrue(result) + + def test_result_compare_false(self): + base = self.server.suffix + l = self._ldap_conn + msgid = l.compare('cn=Foo1,%s' % base, 'cn', b'Foo2') + responses = l.result() + result, = responses + self.assertEqual(result.msgid, msgid) + self.assertIsInstance(result, ldap.response.CompareResult) + result.raise_for_result() + self.assertEqual(result.result, ldap.COMPARE_FALSE.errnum) + self.assertFalse(result) + + def test_result_compare_notfound(self): + base = self.server.suffix + l = self._ldap_conn + msgid = l.compare('cn=invalid,%s' % base, 'cn', b'Foo2') + responses = l.result() + result, = responses + self.assertEqual(result.msgid, msgid) + self.assertIsInstance(result, ldap.response.CompareResult) + self.assertEqual(result.result, ldap.NO_SUCH_OBJECT.errnum) + with self.assertRaises(ldap.NO_SUCH_OBJECT): + result.raise_for_result() + + with self.assertRaises(ldap.NO_SUCH_OBJECT): + if result: + pass + + def test_result_compare_invalidattr(self): + base = self.server.suffix + l = self._ldap_conn + msgid = l.compare('cn=Foo1,%s' % base, 'invalidattr', b'invalid') + responses = l.result() + result, = responses + + self.assertEqual(result.msgid, msgid) + self.assertIsInstance(result, ldap.response.CompareResult) + self.assertEqual(result.result, ldap.UNDEFINED_TYPE.errnum) + with self.assertRaises(ldap.UNDEFINED_TYPE): + result.raise_for_result() + + with self.assertRaises(ldap.UNDEFINED_TYPE): + if result: + pass + + def test_async_search_no_such_object_exception_contains_message_id(self): + msgid = self._ldap_conn.search("CN=XXX", ldap.SCOPE_SUBTREE) + with self.assertRaises(ldap.NO_SUCH_OBJECT) as cm: + r, = self._ldap_conn.result() + r.raise_for_result() + self.assertEqual(cm.exception.args[0].msgid, msgid) + + def test_passwd_s(self): + l = self._ldap_conn + + # first, create a user to change password on + dn = "cn=PasswordTest," + self.server.suffix + result = l.add_s( + dn, + [ + ('objectClass', b'person'), + ('sn', b'PasswordTest'), + ('cn', b'PasswordTest'), + ('userPassword', b'initial'), + ] + ).raise_for_result() + self.assertIsInstance(result, ldap.response.AddResult) + self.assertEqual(result.msgtype, ldap.RES_ADD) + self.assertIsInstance(result.msgid, int) + self.assertEqual(result.controls, None) + + # try changing password with a wrong old-pw + with self.assertRaises(ldap.UNWILLING_TO_PERFORM): + l.passwd_s(dn, "bogus", "ignored").raise_for_result() + + # have the server generate a new random pw + res = l.passwd_s(dn, "initial", newpw=None).raise_for_result() + self.assertEqual(res.name, None) + + password = res.genPasswd + self.assertIsInstance(password, bytes) + + # try changing password back + res = l.passwd_s(dn, password, "initial").raise_for_result() + self.assertEqual(res.name, None) + self.assertEqual(res.value, None) + + l.delete_s(dn) + + def test_slapadd(self): + with self.assertRaises(ldap.INVALID_DN_SYNTAX): + self._ldap_conn.add_s( + "myAttribute=foobar,ou=Container,%s" % self.server.suffix, [ + ("objectClass", b'myClass'), + ("myAttribute", b'foobar'), + ]).raise_for_result() + + self.server.slapadd(SCHEMA_TEMPLATE, ["-n0"]) + self.server.restart() + self.reset_connection() + + self._ldap_conn.add_s( + "myAttribute=foobar,ou=Container,%s" % self.server.suffix, [ + ("objectClass", b'myClass'), + ("myAttribute", b'foobar'), + ]).raise_for_result() + + +@requires_init_fd() +class Test01_ConnectionWithFileno(Test00_Connection): + def _open_ldap_conn(self, who=None, cred=None, **kwargs): + if hasattr(self, '_sock'): + raise RuntimeError("socket already connected") + self._sock = socket.create_connection( + (self.server.hostname, self.server.port) + ) + return super()._open_ldap_conn( + who=who, cred=cred, fileno=self._sock.fileno(), **kwargs + ) + + def tearDown(self): + self._sock.close() + del self._sock + super().tearDown() + + def reset_connection(self): + self._sock.close() + del self._sock + super().reset_connection() + + +if __name__ == '__main__': + unittest.main() From d6e8889911a15d9317ca222c89412da508a05d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Tue, 25 Jun 2024 13:43:42 +0100 Subject: [PATCH 4/5] TMP: rich support --- Lib/ldap/response.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Lib/ldap/response.py b/Lib/ldap/response.py index a9221791..4ea1a8a1 100644 --- a/Lib/ldap/response.py +++ b/Lib/ldap/response.py @@ -77,6 +77,10 @@ def __repr__(self): return (f"{self.__class__.__name__}(msgid={self.msgid}, " f"msgtype={self.msgtype}{optional})") + def __rich_repr__(self): + yield "msgid", self.msgid + yield "controls", self.controls, None + class Result(Response): result: int @@ -109,6 +113,13 @@ def __repr__(self): return (f"{self.__class__.__name__}" f"(msgid={self.msgid}, result={self.result}{optional})") + def __rich_repr__(self): + super().__rich_repr__() + yield "result", self.result + yield "matcheddn", self.matcheddn, "" + yield "message", self.message, "" + yield "referrals", self.referrals, None + class SearchEntry(Response): msgtype = ldap.RES_SEARCH_ENTRY @@ -125,6 +136,11 @@ def __new__(cls, msgid, msgtype, controls=None, *, return instance + def __rich_repr__(self): + super().__rich_repr__() + yield "dn", self.dn + yield "attrs", self.attrs + class SearchReference(Response): msgtype = ldap.RES_SEARCH_REFERENCE @@ -139,6 +155,10 @@ def __new__(cls, msgid, msgtype, controls=None, *, return instance + def __rich_repr__(self): + super().__rich_repr__() + yield "referrals", self.referrals + class SearchResult(Result): msgtype = ldap.RES_SEARCH_RESULT @@ -184,12 +204,23 @@ def __repr__(self): return (f"{self.__class__.__name__}" f"(msgid={self.msgid}{optional})") + def __rich_repr__(self): + # No super(), we put our values between msgid and controls + yield "msgid", self.msgid + yield "name", self.name, None + yield "value", self.value, None + yield "controls", self.controls, None + class BindResult(Result): msgtype = ldap.RES_BIND servercreds: Optional[bytes] + def __rich_repr__(self): + super().__rich_repr__() + yield "servercreds", self.servercreds, None + class ModifyResult(Result): msgtype = ldap.RES_MODIFY @@ -267,6 +298,13 @@ def __repr__(self): return (f"{self.__class__.__name__}" f"(msgid={self.msgid}, result={self.result}{optional})") + def __rich_repr__(self): + # No super(), we put our values between msgid and controls + yield "msgid", self.msgid + yield "name", self.name, None + yield "value", self.value, None + yield "controls", self.controls, None + class UnsolicitedNotification(ExtendedResult): msgid = ldap.RES_UNSOLICITED From 12c3b61f037c3e3582db100595220cb8ead50f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Thu, 12 Dec 2024 12:55:59 +0000 Subject: [PATCH 5/5] Syncrepl session handling --- Lib/ldap/controls/syncrepl.py | 245 ++++++++++++++++++++++ Lib/ldap/extop/syncrepl.py | 250 ++++++++++++++++++++++ Lib/ldap/syncrepl.py | 377 ++-------------------------------- 3 files changed, 517 insertions(+), 355 deletions(-) create mode 100644 Lib/ldap/controls/syncrepl.py create mode 100644 Lib/ldap/extop/syncrepl.py diff --git a/Lib/ldap/controls/syncrepl.py b/Lib/ldap/controls/syncrepl.py new file mode 100644 index 00000000..7037c2bf --- /dev/null +++ b/Lib/ldap/controls/syncrepl.py @@ -0,0 +1,245 @@ +""" +ldap.controls.syncrepl - classes for the Content Synchronization Operation +(a.k.a. syncrepl) controls (see RFC 4533) + +See https://www.python-ldap.org/ for project details. +""" + +__all__ = [ + 'SyncRequestControl', + 'SyncStateControl', 'SyncDoneControl', +] + +from pyasn1.type import tag, namedtype, namedval, univ, constraint +from pyasn1.codec.ber import encoder, decoder +from uuid import UUID + +import ldap.controls +from ldap.controls import RequestControl, ResponseControl + + +class SyncUUID(univ.OctetString): + """ + syncUUID ::= OCTET STRING (SIZE(16)) + """ + subtypeSpec = constraint.ValueSizeConstraint(16, 16) + + +class SyncCookie(univ.OctetString): + """ + syncCookie ::= OCTET STRING + """ + + +class SyncRequestMode(univ.Enumerated): + """ + mode ENUMERATED { + -- 0 unused + refreshOnly (1), + -- 2 reserved + refreshAndPersist (3) + }, + """ + namedValues = namedval.NamedValues( + ('refreshOnly', 1), + ('refreshAndPersist', 3) + ) + subtypeSpec = univ.Enumerated.subtypeSpec + \ + constraint.SingleValueConstraint(1, 3) + + +class SyncRequestValue(univ.Sequence): + """ + syncRequestValue ::= SEQUENCE { + mode ENUMERATED { + -- 0 unused + refreshOnly (1), + -- 2 reserved + refreshAndPersist (3) + }, + cookie syncCookie OPTIONAL, + reloadHint BOOLEAN DEFAULT FALSE + } + """ + componentType = namedtype.NamedTypes( + namedtype.NamedType('mode', SyncRequestMode()), + namedtype.OptionalNamedType('cookie', SyncCookie()), + namedtype.DefaultedNamedType('reloadHint', univ.Boolean(False)) + ) + + +class SyncRequestControl(RequestControl): + """ + The Sync Request Control is an LDAP Control [RFC4511] where the + controlType is the object identifier 1.3.6.1.4.1.4203.1.9.1.1 and the + controlValue, an OCTET STRING, contains a BER-encoded + syncRequestValue. The criticality field is either TRUE or FALSE. + [..] + The Sync Request Control is only applicable to the SearchRequest + Message. + """ + controlType = '1.3.6.1.4.1.4203.1.9.1.1' + + def __init__(self, criticality=1, cookie=None, mode='refreshOnly', + reloadHint=False): + self.criticality = criticality + self.cookie = cookie + self.mode = mode + self.reloadHint = reloadHint + + def encodeControlValue(self): + rcv = SyncRequestValue() + rcv.setComponentByName('mode', SyncRequestMode(self.mode)) + if self.cookie is not None: + rcv.setComponentByName('cookie', SyncCookie(self.cookie)) + if self.reloadHint is not None: + rcv.setComponentByName('reloadHint', univ.Boolean(self.reloadHint)) + return encoder.encode(rcv) + + def __repr__(self): + return '{}(cookie={!r}, mode={!r}, reloadHint={!r})'.format( + self.__class__.__name__, + self.cookie, + self.mode, + self.reloadHint + ) + + def __rich_repr__(self): + yield 'criticality', self.criticality, 1 + yield 'cookie', self.cookie, None + yield 'mode', self.mode + yield 'reloadHint', self.reloadHint, False + + +class SyncStateOp(univ.Enumerated): + """ + state ENUMERATED { + present (0), + add (1), + modify (2), + delete (3) + }, + """ + namedValues = namedval.NamedValues( + ('present', 0), + ('add', 1), + ('modify', 2), + ('delete', 3) + ) + subtypeSpec = univ.Enumerated.subtypeSpec + \ + constraint.SingleValueConstraint(0, 1, 2, 3) + + +class SyncStateValue(univ.Sequence): + """ + syncStateValue ::= SEQUENCE { + state ENUMERATED { + present (0), + add (1), + modify (2), + delete (3) + }, + entryUUID syncUUID, + cookie syncCookie OPTIONAL + } + """ + componentType = namedtype.NamedTypes( + namedtype.NamedType('state', SyncStateOp()), + namedtype.NamedType('entryUUID', SyncUUID()), + namedtype.OptionalNamedType('cookie', SyncCookie()) + ) + + +class SyncStateControl(ResponseControl): + """ + The Sync State Control is an LDAP Control [RFC4511] where the + controlType is the object identifier 1.3.6.1.4.1.4203.1.9.1.2 and the + controlValue, an OCTET STRING, contains a BER-encoded SyncStateValue. + The criticality is FALSE. + [..] + The Sync State Control is only applicable to SearchResultEntry and + SearchResultReference Messages. + """ + controlType = '1.3.6.1.4.1.4203.1.9.1.2' + + def decodeControlValue(self, encodedControlValue): + d = decoder.decode(encodedControlValue, asn1Spec=SyncStateValue()) + state = d[0].getComponentByName('state') + uuid = UUID(bytes=bytes(d[0].getComponentByName('entryUUID'))) + cookie = d[0].getComponentByName('cookie') + if cookie is not None and cookie.hasValue(): + self.cookie = bytes(cookie) + else: + self.cookie = None + self.state = state.prettyPrint() + self.entryUUID = str(uuid) + + def __repr__(self): + optional = '' + if self.cookie is not None: + optional += ', cookie={!r}'.format(self.cookie) + return '{}(state={!r}, entryUUID={!r}{})'.format( + self.__class__.__name__, + self.state, + self.entryUUID, + optional, + ) + + def __rich_repr__(self): + yield 'state', self.state + yield 'entryUUID', self.entryUUID + yield 'cookie', self.cookie, None + + +class SyncDoneValue(univ.Sequence): + """ + syncDoneValue ::= SEQUENCE { + cookie syncCookie OPTIONAL, + refreshDeletes BOOLEAN DEFAULT FALSE + } + """ + componentType = namedtype.NamedTypes( + namedtype.OptionalNamedType('cookie', SyncCookie()), + namedtype.DefaultedNamedType('refreshDeletes', univ.Boolean(False)) + ) + + +class SyncDoneControl(ResponseControl): + """ + The Sync Done Control is an LDAP Control [RFC4511] where the + controlType is the object identifier 1.3.6.1.4.1.4203.1.9.1.3 and the + controlValue contains a BER-encoded syncDoneValue. The criticality + is FALSE (and hence absent). + [..] + The Sync Done Control is only applicable to the SearchResultDone + Message. + """ + controlType = '1.3.6.1.4.1.4203.1.9.1.3' + + def decodeControlValue(self, encodedControlValue): + d = decoder.decode(encodedControlValue, asn1Spec=SyncDoneValue()) + cookie = d[0].getComponentByName('cookie') + if cookie.hasValue(): + self.cookie = bytes(cookie) + else: + self.cookie = None + refresh_deletes = d[0].getComponentByName('refreshDeletes') + if refresh_deletes.hasValue(): + self.refreshDeletes = bool(refresh_deletes) + else: + self.refreshDeletes = None + + def __repr__(self): + optional = [] + if self.refreshDeletes is not None: + optional.append('refreshDeletes={!r}'.format(self.refreshDeletes)) + if self.cookie is not None: + optional.append('cookie={!r}'.format(self.cookie)) + return '{}({})'.format( + self.__class__.__name__, + ', '.join(optional) + ) + + def __rich_repr__(self): + yield 'refreshDeletes', self.refreshDeletes, None + yield 'cookie', self.cookie, None diff --git a/Lib/ldap/extop/syncrepl.py b/Lib/ldap/extop/syncrepl.py new file mode 100644 index 00000000..ee54096f --- /dev/null +++ b/Lib/ldap/extop/syncrepl.py @@ -0,0 +1,250 @@ +""" +ldap.extop.syncrepl - classes for the Read Entry controls (see RFC 4533) +ldap.extop.syncrepl - Classes for Dynamic Entries extended operations +(see RFC 4533) + +See https://www.python-ldap.org/ for details. +""" + +from typing import Optional, Sequence + +from pyasn1.type import tag, namedtype, namedval, univ, constraint +from pyasn1.codec.ber import encoder, decoder + +from ldap.extop import ExtendedRequest, ExtendedResponse, IntermediateResponse +from ldap.controls.syncrepl import ( + SyncCookie, SyncUUID, +) + + +class RefreshDelete(univ.Sequence): + """ + refreshDelete [1] SEQUENCE { + cookie syncCookie OPTIONAL, + refreshDone BOOLEAN DEFAULT TRUE + }, + """ + componentType = namedtype.NamedTypes( + namedtype.OptionalNamedType('cookie', SyncCookie()), + namedtype.DefaultedNamedType('refreshDone', univ.Boolean(True)) + ) + + +class RefreshPresent(univ.Sequence): + """ + refreshPresent [2] SEQUENCE { + cookie syncCookie OPTIONAL, + refreshDone BOOLEAN DEFAULT TRUE + }, + """ + componentType = namedtype.NamedTypes( + namedtype.OptionalNamedType('cookie', SyncCookie()), + namedtype.DefaultedNamedType('refreshDone', univ.Boolean(True)) + ) + + +class SyncUUIDs(univ.SetOf): + """ + syncUUIDs SET OF syncUUID + """ + componentType = SyncUUID() + + +class SyncIdSet(univ.Sequence): + """ + syncIdSet [3] SEQUENCE { + cookie syncCookie OPTIONAL, + refreshDeletes BOOLEAN DEFAULT FALSE, + syncUUIDs SET OF syncUUID + } + """ + componentType = namedtype.NamedTypes( + namedtype.OptionalNamedType('cookie', SyncCookie()), + namedtype.DefaultedNamedType('refreshDeletes', univ.Boolean(False)), + namedtype.NamedType('syncUUIDs', SyncUUIDs()) + ) + + +class SyncInfoValue(univ.Choice): + """ + syncInfoValue ::= CHOICE { + newcookie [0] syncCookie, + refreshDelete [1] SEQUENCE { + cookie syncCookie OPTIONAL, + refreshDone BOOLEAN DEFAULT TRUE + }, + refreshPresent [2] SEQUENCE { + cookie syncCookie OPTIONAL, + refreshDone BOOLEAN DEFAULT TRUE + }, + syncIdSet [3] SEQUENCE { + cookie syncCookie OPTIONAL, + refreshDeletes BOOLEAN DEFAULT FALSE, + syncUUIDs SET OF syncUUID + } + } + """ + componentType = namedtype.NamedTypes( + namedtype.NamedType( + 'newcookie', + SyncCookie().subtype( + implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0) + ) + ), + namedtype.NamedType( + 'refreshDelete', + RefreshDelete().subtype( + implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1) + ) + ), + namedtype.NamedType( + 'refreshPresent', + RefreshPresent().subtype( + implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2) + ) + ), + namedtype.NamedType( + 'syncIdSet', + SyncIdSet().subtype( + implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3) + ) + ) + ) + + +class SyncInfoMessage(IntermediateResponse): + """ + The Sync Info Message is an LDAP Intermediate Response Message + [RFC4511] where responseName is the object identifier + 1.3.6.1.4.1.4203.1.9.1.4 and responseValue contains a BER-encoded + syncInfoValue. The criticality is FALSE (and hence absent). + """ + responseName = '1.3.6.1.4.1.4203.1.9.1.4' + + def __new__(cls, msgid, msgtype, controls=None, *, + name=None, value=None, + **kwargs): + if cls is not __class__: + return super().__new__(cls, msgid, msgtype, controls, + name=name, value=value) + syncinfo, _ = decoder.decode(value, asn1Spec=SyncInfoValue()) + choice = syncinfo.getName() + if choice == 'newcookie': + child = SyncInfoNewCookie + elif choice == 'refreshDelete': + child = SyncInfoRefreshDelete + elif choice == 'refreshPresent': + child = SyncInfoRefreshPresent + elif choice == 'syncIdSet': + child = SyncInfoIDSet + else: + raise ValueError + return child.__new__(child, msgid, msgtype, controls, + name=name, value=value) + + def decode(self, value: bytes): + self.syncinfo, _ = decoder.decode( + value, + asn1Spec=SyncInfoValue(), + ) + + +class SyncInfoNewCookie(SyncInfoMessage): + cookie: bytes + + def decode(self, value: bytes): + super().decode(value) + self.cookie = bytes(self.syncinfo.getComponent()) + + def __repr__(self): + return '{}(cookie={!r})'.format( + self.__class__.__name__, + self.cookie, + ) + + def __rich_repr__(self): + yield "cookie", self.cookie + + +class SyncInfoRefreshDelete(SyncInfoMessage): + cookie: Optional[bytes] + refreshDone: bool + + def decode(self, value: bytes): + super().decode(value) + component = self.syncinfo.getComponent() + self.cookie = None + cookie = component['cookie'] + if cookie.isValue: + self.cookie = bytes(cookie) + self.refreshDone = bool(component['refreshDone']) + + def __repr__(self): + return '{}(cookie={!r}, refreshDone={!r})'.format( + self.__class__.__name__, + self.cookie, + self.refreshDone, + ) + + def __rich_repr__(self): + yield "cookie", self.cookie, None + yield "refreshDone", self.refreshDone + + +class SyncInfoRefreshPresent(SyncInfoMessage): + cookie: Optional[bytes] + refreshDone: bool + + def decode(self, value: bytes): + super().decode(value) + component = self.syncinfo.getComponent() + self.cookie = None + cookie = component['cookie'] + if cookie.isValue: + self.cookie = bytes(cookie) + self.refreshDone = bool(component['refreshDone']) + + def __repr__(self): + return '{}(cookie={!r}, refreshDone={!r})'.format( + self.__class__.__name__, + self.cookie, + self.refreshDone, + ) + + def __rich_repr__(self): + yield "cookie", self.cookie, None + yield "refreshDone", self.refreshDone + + +class SyncInfoIDSet(SyncInfoMessage): + cookie: Optional[bytes] + refreshDeletes: bool + syncUUIDs: Sequence[str] + + def decode(self, value: bytes): + super().decode(value) + component = self.syncinfo.getComponent() + self.cookie = None + cookie = component['cookie'] + if cookie.isValue: + self.cookie = bytes(cookie) + self.refreshDeletes = bool(component['refreshDeletes']) + + uuids = [] + for syncuuid in component['syncUUIDs']: + uuid = UUID(bytes=bytes(syncuuid)) + uuids.append(str(uuid)) + self.syncUUIDs = uuids + + def __repr__(self): + return '{}(cookie={!r}, refreshDeletes={!r}, syncUUIDs={!r})'.format( + self.__class__.__name__, + self.cookie, + self.refreshDeletes, + self.syncUUIDs, + ) + + def __rich_repr__(self): + yield "cookie", self.cookie, None + yield "refreshDeletes", self.refreshDeletes + yield "syncUUIDs", self.syncUUIDs diff --git a/Lib/ldap/syncrepl.py b/Lib/ldap/syncrepl.py index c59641e1..04f0a040 100644 --- a/Lib/ldap/syncrepl.py +++ b/Lib/ldap/syncrepl.py @@ -4,343 +4,18 @@ See https://www.python-ldap.org/ for project details. """ -from uuid import UUID - -# Imports from pyasn1 -from pyasn1.type import tag, namedtype, namedval, univ, constraint -from pyasn1.codec.ber import encoder, decoder - from ldap.pkginfo import __version__, __author__, __license__ -from ldap.controls import RequestControl, ResponseControl from ldap import RES_SEARCH_RESULT, RES_SEARCH_ENTRY, RES_INTERMEDIATE +from ldap.controls.syncrepl import ( + SyncUUID, SyncCookie, + SyncRequestControl, SyncStateControl, SyncDoneControl, +) __all__ = [ 'SyncreplConsumer', ] -class SyncUUID(univ.OctetString): - """ - syncUUID ::= OCTET STRING (SIZE(16)) - """ - subtypeSpec = constraint.ValueSizeConstraint(16, 16) - - -class SyncCookie(univ.OctetString): - """ - syncCookie ::= OCTET STRING - """ - - -class SyncRequestMode(univ.Enumerated): - """ - mode ENUMERATED { - -- 0 unused - refreshOnly (1), - -- 2 reserved - refreshAndPersist (3) - }, - """ - namedValues = namedval.NamedValues( - ('refreshOnly', 1), - ('refreshAndPersist', 3) - ) - subtypeSpec = univ.Enumerated.subtypeSpec + constraint.SingleValueConstraint(1, 3) - - -class SyncRequestValue(univ.Sequence): - """ - syncRequestValue ::= SEQUENCE { - mode ENUMERATED { - -- 0 unused - refreshOnly (1), - -- 2 reserved - refreshAndPersist (3) - }, - cookie syncCookie OPTIONAL, - reloadHint BOOLEAN DEFAULT FALSE - } - """ - componentType = namedtype.NamedTypes( - namedtype.NamedType('mode', SyncRequestMode()), - namedtype.OptionalNamedType('cookie', SyncCookie()), - namedtype.DefaultedNamedType('reloadHint', univ.Boolean(False)) - ) - - -class SyncRequestControl(RequestControl): - """ - The Sync Request Control is an LDAP Control [RFC4511] where the - controlType is the object identifier 1.3.6.1.4.1.4203.1.9.1.1 and the - controlValue, an OCTET STRING, contains a BER-encoded - syncRequestValue. The criticality field is either TRUE or FALSE. - [..] - The Sync Request Control is only applicable to the SearchRequest - Message. - """ - controlType = '1.3.6.1.4.1.4203.1.9.1.1' - - def __init__(self, criticality=1, cookie=None, mode='refreshOnly', reloadHint=False): - self.criticality = criticality - self.cookie = cookie - self.mode = mode - self.reloadHint = reloadHint - - def encodeControlValue(self): - rcv = SyncRequestValue() - rcv.setComponentByName('mode', SyncRequestMode(self.mode)) - if self.cookie is not None: - rcv.setComponentByName('cookie', SyncCookie(self.cookie)) - if self.reloadHint: - rcv.setComponentByName('reloadHint', univ.Boolean(self.reloadHint)) - return encoder.encode(rcv) - - -class SyncStateOp(univ.Enumerated): - """ - state ENUMERATED { - present (0), - add (1), - modify (2), - delete (3) - }, - """ - namedValues = namedval.NamedValues( - ('present', 0), - ('add', 1), - ('modify', 2), - ('delete', 3) - ) - subtypeSpec = univ.Enumerated.subtypeSpec + constraint.SingleValueConstraint(0, 1, 2, 3) - - -class SyncStateValue(univ.Sequence): - """ - syncStateValue ::= SEQUENCE { - state ENUMERATED { - present (0), - add (1), - modify (2), - delete (3) - }, - entryUUID syncUUID, - cookie syncCookie OPTIONAL - } - """ - componentType = namedtype.NamedTypes( - namedtype.NamedType('state', SyncStateOp()), - namedtype.NamedType('entryUUID', SyncUUID()), - namedtype.OptionalNamedType('cookie', SyncCookie()) - ) - - -class SyncStateControl(ResponseControl): - """ - The Sync State Control is an LDAP Control [RFC4511] where the - controlType is the object identifier 1.3.6.1.4.1.4203.1.9.1.2 and the - controlValue, an OCTET STRING, contains a BER-encoded SyncStateValue. - The criticality is FALSE. - [..] - The Sync State Control is only applicable to SearchResultEntry and - SearchResultReference Messages. - """ - controlType = '1.3.6.1.4.1.4203.1.9.1.2' - opnames = ('present', 'add', 'modify', 'delete') - - def decodeControlValue(self, encodedControlValue): - d = decoder.decode(encodedControlValue, asn1Spec=SyncStateValue()) - state = d[0].getComponentByName('state') - uuid = UUID(bytes=bytes(d[0].getComponentByName('entryUUID'))) - cookie = d[0].getComponentByName('cookie') - if cookie is not None and cookie.hasValue(): - self.cookie = str(cookie) - else: - self.cookie = None - self.state = self.__class__.opnames[int(state)] - self.entryUUID = str(uuid) - - -class SyncDoneValue(univ.Sequence): - """ - syncDoneValue ::= SEQUENCE { - cookie syncCookie OPTIONAL, - refreshDeletes BOOLEAN DEFAULT FALSE - } - """ - componentType = namedtype.NamedTypes( - namedtype.OptionalNamedType('cookie', SyncCookie()), - namedtype.DefaultedNamedType('refreshDeletes', univ.Boolean(False)) - ) - - -class SyncDoneControl(ResponseControl): - """ - The Sync Done Control is an LDAP Control [RFC4511] where the - controlType is the object identifier 1.3.6.1.4.1.4203.1.9.1.3 and the - controlValue contains a BER-encoded syncDoneValue. The criticality - is FALSE (and hence absent). - [..] - The Sync Done Control is only applicable to the SearchResultDone - Message. - """ - controlType = '1.3.6.1.4.1.4203.1.9.1.3' - - def decodeControlValue(self, encodedControlValue): - d = decoder.decode(encodedControlValue, asn1Spec=SyncDoneValue()) - cookie = d[0].getComponentByName('cookie') - if cookie.hasValue(): - self.cookie = str(cookie) - else: - self.cookie = None - refresh_deletes = d[0].getComponentByName('refreshDeletes') - if refresh_deletes.hasValue(): - self.refreshDeletes = bool(refresh_deletes) - else: - self.refreshDeletes = None - - -class RefreshDelete(univ.Sequence): - """ - refreshDelete [1] SEQUENCE { - cookie syncCookie OPTIONAL, - refreshDone BOOLEAN DEFAULT TRUE - }, - """ - componentType = namedtype.NamedTypes( - namedtype.OptionalNamedType('cookie', SyncCookie()), - namedtype.DefaultedNamedType('refreshDone', univ.Boolean(True)) - ) - - -class RefreshPresent(univ.Sequence): - """ - refreshPresent [2] SEQUENCE { - cookie syncCookie OPTIONAL, - refreshDone BOOLEAN DEFAULT TRUE - }, - """ - componentType = namedtype.NamedTypes( - namedtype.OptionalNamedType('cookie', SyncCookie()), - namedtype.DefaultedNamedType('refreshDone', univ.Boolean(True)) - ) - - -class SyncUUIDs(univ.SetOf): - """ - syncUUIDs SET OF syncUUID - """ - componentType = SyncUUID() - - -class SyncIdSet(univ.Sequence): - """ - syncIdSet [3] SEQUENCE { - cookie syncCookie OPTIONAL, - refreshDeletes BOOLEAN DEFAULT FALSE, - syncUUIDs SET OF syncUUID - } - """ - componentType = namedtype.NamedTypes( - namedtype.OptionalNamedType('cookie', SyncCookie()), - namedtype.DefaultedNamedType('refreshDeletes', univ.Boolean(False)), - namedtype.NamedType('syncUUIDs', SyncUUIDs()) - ) - - -class SyncInfoValue(univ.Choice): - """ - syncInfoValue ::= CHOICE { - newcookie [0] syncCookie, - refreshDelete [1] SEQUENCE { - cookie syncCookie OPTIONAL, - refreshDone BOOLEAN DEFAULT TRUE - }, - refreshPresent [2] SEQUENCE { - cookie syncCookie OPTIONAL, - refreshDone BOOLEAN DEFAULT TRUE - }, - syncIdSet [3] SEQUENCE { - cookie syncCookie OPTIONAL, - refreshDeletes BOOLEAN DEFAULT FALSE, - syncUUIDs SET OF syncUUID - } - } - """ - componentType = namedtype.NamedTypes( - namedtype.NamedType( - 'newcookie', - SyncCookie().subtype( - implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0) - ) - ), - namedtype.NamedType( - 'refreshDelete', - RefreshDelete().subtype( - implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1) - ) - ), - namedtype.NamedType( - 'refreshPresent', - RefreshPresent().subtype( - implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2) - ) - ), - namedtype.NamedType( - 'syncIdSet', - SyncIdSet().subtype( - implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3) - ) - ) - ) - - -class SyncInfoMessage: - """ - The Sync Info Message is an LDAP Intermediate Response Message - [RFC4511] where responseName is the object identifier - 1.3.6.1.4.1.4203.1.9.1.4 and responseValue contains a BER-encoded - syncInfoValue. The criticality is FALSE (and hence absent). - """ - responseName = '1.3.6.1.4.1.4203.1.9.1.4' - - def __init__(self, encodedMessage): - d = decoder.decode(encodedMessage, asn1Spec=SyncInfoValue()) - self.newcookie = None - self.refreshDelete = None - self.refreshPresent = None - self.syncIdSet = None - - # Due to the way pyasn1 works, refreshDelete and refreshPresent are both - # valid in the components as they are fully populated defaults. We must - # get the component directly from the message, not by iteration. - attr = d[0].getName() - comp = d[0].getComponent() - - if comp is not None and comp.hasValue(): - if attr == 'newcookie': - self.newcookie = str(comp) - return - - val = {} - - cookie = comp.getComponentByName('cookie') - if cookie.hasValue(): - val['cookie'] = str(cookie) - - if attr.startswith('refresh'): - val['refreshDone'] = bool(comp.getComponentByName('refreshDone')) - elif attr == 'syncIdSet': - uuids = [] - ids = comp.getComponentByName('syncUUIDs') - for i in range(len(ids)): - uuid = UUID(bytes=bytes(ids.getComponentByPosition(i))) - uuids.append(str(uuid)) - val['syncUUIDs'] = uuids - val['refreshDeletes'] = bool(comp.getComponentByName('refreshDeletes')) - - setattr(self, attr, val) - - class SyncreplConsumer: """ SyncreplConsumer - LDAP syncrepl consumer object. @@ -422,8 +97,9 @@ def syncrepl_poll(self, msgid=-1, timeout=None, all=0): for m in msg: dn, attrs, ctrls = m for c in ctrls: - if c.__class__.__name__ != 'SyncStateControl': + if not isinstance(c, SyncStateControl): continue + if c.state == 'present': self.syncrepl_present([c.entryUUID]) elif c.state == 'delete': @@ -432,40 +108,31 @@ def syncrepl_poll(self, msgid=-1, timeout=None, all=0): self.syncrepl_entry(dn, attrs, c.entryUUID) if self.__refreshDone is False: self.syncrepl_present([c.entryUUID]) + if c.cookie is not None: self.syncrepl_set_cookie(c.cookie) break elif type == RES_INTERMEDIATE: - # Intermediate message. If it is a SyncInfoMessage, parse it + # Intermediate message, process any that are SyncInfoMessage for m in msg: - rname, resp, ctrls = m - if rname != SyncInfoMessage.responseName: - continue - sim = SyncInfoMessage(resp) - if sim.newcookie is not None: - self.syncrepl_set_cookie(sim.newcookie) - elif sim.refreshPresent is not None: - self.syncrepl_present(None, refreshDeletes=False) - if 'cookie' in sim.refreshPresent: - self.syncrepl_set_cookie(sim.refreshPresent['cookie']) - if sim.refreshPresent['refreshDone']: - self.__refreshDone = True - self.syncrepl_refreshdone() - elif sim.refreshDelete is not None: - self.syncrepl_present(None, refreshDeletes=True) - if 'cookie' in sim.refreshDelete: - self.syncrepl_set_cookie(sim.refreshDelete['cookie']) - if sim.refreshDelete['refreshDone']: + if isinstance(m, SyncInfoNewCookie): + self.syncrepl_set_cookie(m.cookie) + elif isinstance(m, (SyncInfoRefreshPresent, SyncInfoRefreshDelete)): + refreshDeletes = isinstance(m, SyncInfoRefreshDelete) + self.syncrepl_present(None, refreshDeletes=refreshDeletes) + if m.cookie is not None: + self.syncrepl_set_cookie(m.cookie) + if m.refreshDone: self.__refreshDone = True self.syncrepl_refreshdone() - elif sim.syncIdSet is not None: - if sim.syncIdSet['refreshDeletes'] is True: - self.syncrepl_delete(sim.syncIdSet['syncUUIDs']) + elif isinstance(m, SyncInfoIDSet): + if m.refreshDeletes: + self.syncrepl_delete(m.syncUUIDs) else: - self.syncrepl_present(sim.syncIdSet['syncUUIDs']) - if 'cookie' in sim.syncIdSet: - self.syncrepl_set_cookie(sim.syncIdSet['cookie']) + self.syncrepl_present(m.syncUUIDs) + if m.cookie is not None: + self.syncrepl_set_cookie(m.cookie) if all == 0: return True pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy