Skip to content

Commit a35462a

Browse files
committed
wip
1 parent d093559 commit a35462a

File tree

12 files changed

+903
-68
lines changed

12 files changed

+903
-68
lines changed

Doc/spelling_wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ refreshDeletes
117117
refreshOnly
118118
requestName
119119
requestValue
120+
responseName
120121
resiter
121122
respvalue
122123
ResultProcessor

Lib/ldap/connection.py

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
import ldap
1818
from ldap.controls import DecodeControlTuples, RequestControl
1919
from ldap.extop import ExtendedRequest
20+
from ldap.extop.passwd import PasswordModifyResponse
2021
from ldap.ldapobject import SimpleLDAPObject, NO_UNIQUE_ENTRY
2122
from ldap.response import (
2223
Response,
23-
SearchEntry, SearchReference, SearchResult,
24+
SearchEntry, SearchReference,
2425
IntermediateResponse, ExtendedResult,
2526
)
2627

@@ -32,10 +33,15 @@ class Connection(SimpleLDAPObject):
3233
resp_ctrl_classes = None
3334

3435
def result(self, msgid: int = ldap.RES_ANY, *, all: int = 1,
35-
timeout: Optional[float] = None) -> Optional[list[Response]]:
36+
timeout: Optional[float] = None,
37+
defaultIntermediateClass:
38+
Optional[type[IntermediateResponse]] = None,
39+
defaultExtendedClass: Optional[type[ExtendedResult]] = None
40+
) -> Optional[list[Response]]:
3641
"""
37-
result([msgid: int = RES_ANY [, all: int = 1 [, timeout :
38-
Optional[float] = None]]]) -> Optional[list[Response]]
42+
result([msgid: int = RES_ANY [, all: int = 1 [,
43+
timeout: Optional[float] = None]]])
44+
-> Optional[list[Response]]
3945
4046
This method is used to wait for and return the result of an
4147
operation previously initiated by one of the LDAP asynchronous
@@ -87,13 +93,26 @@ def result(self, msgid: int = ldap.RES_ANY, *, all: int = 1,
8793

8894
results = []
8995
for msgid, msgtype, controls, data in messages:
90-
controls = DecodeControlTuples(controls, self.resp_ctrl_classes)
96+
if controls is not None:
97+
controls = DecodeControlTuples(controls, self.resp_ctrl_classes)
9198

99+
if msgtype == ldap.RES_INTERMEDIATE:
100+
data['defaultClass'] = defaultIntermediateClass
101+
if msgtype == ldap.RES_EXTENDED:
102+
data['defaultClass'] = defaultExtendedClass
92103
m = Response(msgid, msgtype, controls, **data)
93104
results.append(m)
94105

95106
return results
96107

108+
def add_s(self, dn: str,
109+
modlist: list[tuple[str, Union[bytes, list[bytes]]]], *,
110+
ctrls: RequestControls = None) -> ldap.response.AddResult:
111+
msgid = self.add_ext(dn, modlist, serverctrls=ctrls)
112+
responses = self.result(msgid)
113+
result, = responses
114+
return result
115+
97116
def bind_s(self, dn: Optional[str] = None,
98117
cred: Union[None, str, bytes] = None, *,
99118
method: int = ldap.AUTH_SIMPLE,
@@ -119,17 +138,36 @@ def delete_s(self, dn: str, *,
119138
result, = responses
120139
return result
121140

122-
def extop_s(self, oid: Optional[str] = None,
141+
def extop_s(self, name: Optional[str] = None,
123142
value: Optional[bytes] = None, *,
124143
request: Optional[ExtendedRequest] = None,
125-
ctrls: RequestControls = None
144+
ctrls: RequestControls = None,
145+
defaultIntermediateClass: Optional[type[IntermediateResponse]] = None,
146+
defaultExtendedClass: Optional[type[ExtendedResult]] = None
126147
) -> list[Union[IntermediateResponse, ExtendedResult]]:
127148
if request is not None:
128-
oid = request.requestName
149+
name = request.requestName
129150
value = request.encodedRequestValue()
130151

131-
msgid = self.extop(oid, value, serverctrls=ctrls)
132-
return self.result(msgid)
152+
msgid = self.extop(name, value, serverctrls=ctrls)
153+
return self.result(msgid,
154+
defaultIntermediateClass=defaultIntermediateClass,
155+
defaultExtendedClass=defaultExtendedClass)
156+
157+
def modify_s(self, dn: str,
158+
modlist: list[tuple[str, Union[bytes, list[bytes]]]], *,
159+
ctrls: RequestControls = None) -> ldap.response.ModifyResult:
160+
msgid = self.modify_ext(dn, modlist, serverctrls=ctrls)
161+
responses = self.result(msgid)
162+
result, = responses
163+
return result
164+
165+
def passwd_s(self, user: Optional[str] = None,
166+
oldpw: Optional[bytes] = None, newpw: Optional[bytes] = None,
167+
ctrls: RequestControls = None) -> PasswordModifyResponse:
168+
msgid = self.passwd(user, oldpw, newpw, serverctrls=ctrls)
169+
res, = self.result(msgid, defaultExtendedClass=PasswordModifyResponse)
170+
return res
133171

134172
def search_s(self, base: Optional[str] = None,
135173
scope: int = ldap.SCOPE_SUBTREE,
@@ -147,8 +185,11 @@ def search_s(self, base: Optional[str] = None,
147185
attrsonly=attrsonly, serverctrls=ctrls,
148186
sizelimit=sizelimit, timeout=timelimit)
149187
result = self.result(msgid, timeout=timeout)
188+
# FIXME: we want a better way of returning a result with multiple
189+
# messages, always useful in searches but other operations can also
190+
# elicit those (by way of an IntermediateResponse)
150191
result[-1].raise_for_result()
151-
return result[:-1]
192+
return result
152193

153194
def search_subschemasubentry_s(
154195
self, dn: Optional[str] = None) -> Optional[str]:
@@ -212,6 +253,6 @@ def find_unique_entry(self, base: Optional[str] = None,
212253
r = self.search_s(base, scope, filter, attrlist=attrlist,
213254
attrsonly=attrsonly, ctrls=ctrls, timeout=timeout,
214255
sizelimit=2)
215-
if len(r) != 1:
256+
if len(r) != 2:
216257
raise NO_UNIQUE_ENTRY(f'No or non-unique search result for {filter}')
217258
return r[0]

Lib/ldap/extop/__init__.py

Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212
from ldap import __version__
1313
from ldap import KNOWN_EXTENDED_RESPONSES, KNOWN_INTERMEDIATE_RESPONSES
1414

15+
import ldap
16+
import ldap.response
17+
18+
from typing import Optional
19+
20+
_NOTSET = object()
21+
1522

1623
class ExtendedRequest:
1724
"""
@@ -39,7 +46,7 @@ def encodedRequestValue(self):
3946
return self.requestValue
4047

4148

42-
class ExtendedResponse:
49+
class ExtendedResponse(ldap.response.ExtendedResult):
4350
"""
4451
Generic base class for a LDAPv3 extended operation response
4552
@@ -55,9 +62,116 @@ def __init_subclass__(cls):
5562

5663
KNOWN_EXTENDED_RESPONSES.setdefault(cls.responseName, cls)
5764

58-
def __init__(self,responseName,encodedResponseValue):
65+
@classmethod
66+
def __convert_old_api(cls, responseName_or_msgid=_NOTSET,
67+
encodedResponseValue_or_msgtype=_NOTSET,
68+
controls=None, *,
69+
result=_NOTSET, matcheddn=_NOTSET, message=_NOTSET,
70+
referrals=_NOTSET, name=None, value=None,
71+
defaultClass: Optional[type['ExtendedResult']] = None,
72+
msgid=_NOTSET, msgtype=_NOTSET,
73+
responseName=_NOTSET, encodedResponseValue=_NOTSET,
74+
**kwargs):
75+
"""
76+
Implements both old and new API:
77+
__init__(self, responseName, encodedResponseValue)
78+
and
79+
__init__/__new__(self, msgid, msgtype, controls=None, *,
80+
result, matcheddn, message, referrals,
81+
defaultClass=None, **kwargs)
82+
"""
83+
if responseName is not _NOTSET:
84+
name = responseName
85+
value = encodedResponseValue
86+
msgid = None
87+
msgtype = ldap.RES_EXTENDED
88+
result = ldap.SUCCESS.errnum
89+
elif responseName_or_msgid is not _NOTSET and \
90+
isinstance(responseName_or_msgid, (str, type(None))):
91+
if responseName is not _NOTSET:
92+
raise TypeError("responseName passed twice")
93+
if encodedResponseValue_or_msgtype is not _NOTSET and \
94+
encodedResponseValue is not _NOTSET:
95+
raise TypeError("encodedResponseValue passed twice")
96+
name = responseName = responseName_or_msgid
97+
value = encodedResponseValue = encodedResponseValue_or_msgtype
98+
msgid = None
99+
msgtype = ldap.RES_EXTENDED
100+
result = ldap.SUCCESS.errnum
101+
else:
102+
responseName = name
103+
encodedResponseValue = value
104+
if msgid is _NOTSET:
105+
if responseName_or_msgid is _NOTSET:
106+
raise TypeError("msgid parameter not provided")
107+
msgid = responseName_or_msgid
108+
if msgtype is _NOTSET:
109+
if encodedResponseValue_or_msgtype is _NOTSET:
110+
raise TypeError("msgtype parameter not provided")
111+
msgtype = encodedResponseValue_or_msgtype or ldap.RES_EXTENDED
112+
if result is _NOTSET:
113+
raise TypeError("result parameter not provided")
114+
if matcheddn is _NOTSET:
115+
raise TypeError("matcheddn parameter not provided")
116+
if message is _NOTSET:
117+
raise TypeError("message parameter not provided")
118+
if referrals is _NOTSET:
119+
raise TypeError("referrals parameter not provided")
120+
121+
return (
122+
responseName, encodedResponseValue,
123+
(msgid, msgtype, controls),
124+
{'result': result,
125+
'matcheddn': matcheddn,
126+
'message': message,
127+
'referrals': referrals,
128+
'name': name,
129+
'value': value,
130+
'defaultClass': defaultClass,
131+
**kwargs
132+
}
133+
)
134+
135+
def __new__(cls, *args, **kwargs):
136+
"""
137+
Has to support both old and new API:
138+
__new__(cls, responseName: Optional[str],
139+
encodedResponseValue: Optional[bytes])
140+
and
141+
__new__(cls, msgid: int, msgtype: int, controls: Controls = None, *,
142+
result: int, matcheddn: str, message: str, referrals: List[str],
143+
defaultClass: Optional[type[ExtendedResponse]] = None,
144+
**kwargs)
145+
146+
The old API is deprecated and will be removed in 4.0.
147+
"""
148+
# TODO: retire polymorhpism when old API is removed (4.0?)
149+
_, _, args, kwargs = __class__.__convert_old_api(*args, **kwargs)
150+
151+
return super().__new__(cls, *args, **kwargs)
152+
153+
def __init__(self, *args, **kwargs):
154+
"""
155+
Supports both old and new API:
156+
__init__(self, responseName: Optional[str],
157+
encodedResponseValue: Optional[bytes])
158+
and
159+
__init__(self, msgid: int, msgtype: int, controls: Controls = None, *,
160+
result: int, matcheddn: str, message: str, referrals: List[str],
161+
defaultClass: Optional[type[ExtendedResponse]] = None,
162+
**kwargs)
163+
164+
The old API is deprecated and will be removed in 4.0.
165+
"""
166+
# TODO: retire polymorhpism when old API is removed (4.0?)
167+
responseName, encodedResponseValue, _, _ = \
168+
__class__.__convert_old_api(*args, **kwargs)
169+
59170
self.responseName = responseName
60-
self.responseValue = self.decodeResponseValue(encodedResponseValue)
171+
if encodedResponseValue is not None:
172+
self.responseValue = self.decodeResponseValue(encodedResponseValue)
173+
else:
174+
self.responseValue = None
61175

62176
def __repr__(self):
63177
return f'{self.__class__.__name__}({self.responseName},{self.responseValue})'
@@ -70,7 +184,7 @@ def decodeResponseValue(self,value):
70184
return value
71185

72186

73-
class IntermediateResponse:
187+
class IntermediateResponse(ldap.response.IntermediateResponse):
74188
"""
75189
Generic base class for a LDAPv3 intermediate response message
76190

Lib/ldap/ldapobject.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -380,14 +380,17 @@ def extop_result(self,msgid=ldap.RES_ANY,all=1,timeout=None):
380380

381381
def extop_s(self,extreq,serverctrls=None,clientctrls=None,extop_resp_class=None):
382382
msgid = self.extop(extreq,serverctrls,clientctrls)
383-
res = self.extop_result(msgid,all=1,timeout=self.timeout)
383+
resulttype,_,msgid,respctrls,respoid,respvalue = self.extop_result(msgid,all=1,timeout=self.timeout)
384+
extop_resp_class = extop_resp_class or KNOWN_EXTENDED_RESPONSES.get(respoid)
384385
if extop_resp_class:
385-
respoid,respvalue = res
386386
if extop_resp_class.responseName!=respoid:
387387
raise ldap.PROTOCOL_ERROR(f"Wrong OID in extended response! Expected {extop_resp_class.responseName}, got {respoid}")
388-
return extop_resp_class(extop_resp_class.responseName,respvalue)
388+
return extop_resp_class(msgid, resulttype, respctrls,
389+
result=0, matcheddn=None,
390+
message=None, referrals=None,
391+
name=respoid, value=respvalue)
389392
else:
390-
return res
393+
return respoid, respvalue
391394

392395
def modify_ext(self,dn,modlist,serverctrls=None,clientctrls=None):
393396
"""

0 commit comments

Comments
 (0)