Skip to content

Commit 756fba2

Browse files
committed
Draft new response handling API
1 parent a4e5908 commit 756fba2

File tree

9 files changed

+996
-12
lines changed

9 files changed

+996
-12
lines changed

Doc/fake_ldap_module_for_documentation.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,5 @@ def get_option(num):
2828

2929
class LDAPError:
3030
pass
31+
32+
_exceptions = {}

Lib/ldap/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
assert _ldap.__version__==__version__, \
3636
ImportError(f'ldap {__version__} and _ldap {_ldap.__version__} version mismatch!')
3737
from _ldap import *
38+
from _ldap import _exceptions
3839
# call into libldap to initialize it right now
3940
LIBLDAP_API_INFO = _ldap.get_option(_ldap.OPT_API_INFO)
4041

Lib/ldap/connection.py

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
"""
2+
connection.py - wraps class _ldap.LDAPObject
3+
4+
See https://www.python-ldap.org/ for details.
5+
"""
6+
7+
from ldap.pkginfo import __version__, __author__, __license__
8+
9+
__all__ = [
10+
'Connection',
11+
]
12+
13+
14+
from numbers import Real
15+
from typing import Optional, Union
16+
17+
import ldap
18+
from ldap.controls import DecodeControlTuples, RequestControl
19+
from ldap.extop import ExtendedRequest
20+
from ldap.ldapobject import SimpleLDAPObject, NO_UNIQUE_ENTRY
21+
from ldap.response import (
22+
Response,
23+
SearchEntry, SearchReference, SearchResult,
24+
IntermediateResponse, ExtendedResult,
25+
)
26+
27+
RequestControls = Optional[list[RequestControl]]
28+
29+
30+
# TODO: remove _ext and _s functions as we rework request API
31+
class Connection(SimpleLDAPObject):
32+
resp_ctrl_classes = None
33+
34+
def result(self, msgid: int = ldap.RES_ANY, *, all: int = 1,
35+
timeout: Optional[float] = None) -> Optional[list[Response]]:
36+
"""
37+
result([msgid: int = RES_ANY [, all: int = 1 [, timeout :
38+
Optional[float] = None]]]) -> Optional[list[Response]]
39+
40+
This method is used to wait for and return the result of an
41+
operation previously initiated by one of the LDAP asynchronous
42+
operation routines (e.g. search(), modify(), etc.) They all
43+
return an invocation identifier (a message id) upon successful
44+
initiation of their operation. This id is guaranteed to be
45+
unique across an LDAP session, and can be used to request the
46+
result of a specific operation via the msgid parameter of the
47+
result() method.
48+
49+
If the result of a specific operation is required, msgid should
50+
be set to the invocation message id returned when the operation
51+
was initiated; otherwise RES_ANY should be supplied.
52+
53+
The all parameter is used to wait until a final response for
54+
a given operation is received, this is useful with operations
55+
(like search) that generate multiple responses and is used
56+
to select whether a single item should be returned or to wait
57+
for all the responses before returning.
58+
59+
Using search as an example: A search response is made up of
60+
zero or more search entries followed by a search result. If all
61+
is 0, search entries will be returned one at a time as they
62+
come in, via separate calls to result(). If all is 1, the
63+
search response will be returned in its entirety, i.e. after
64+
all entries and the final search result have been received. If
65+
all is 2, all search entries that have been received so far
66+
will be returned.
67+
68+
The method returns a list of messages or None if polling and no
69+
messages arrived yet.
70+
71+
The result() method will block for timeout seconds, or
72+
indefinitely if timeout is negative. A timeout of 0 will
73+
effect a poll. The timeout can be expressed as a floating-point
74+
value. If timeout is None the default in self.timeout is used.
75+
76+
If a timeout occurs, a TIMEOUT exception is raised, unless
77+
polling (timeout = 0), in which case None is returned.
78+
"""
79+
80+
if timeout is None:
81+
timeout = self.timeout
82+
83+
messages = self._ldap_call(self._l.result, msgid, all, timeout)
84+
85+
if messages is None:
86+
return None
87+
88+
results = []
89+
for msgid, msgtype, controls, data in messages:
90+
controls = DecodeControlTuples(controls, self.resp_ctrl_classes)
91+
92+
m = Response(msgid, msgtype, controls, **data)
93+
results.append(m)
94+
95+
return results
96+
97+
def bind_s(self, dn: Optional[str] = None,
98+
cred: Union[None, str, bytes] = None, *,
99+
method: int = ldap.AUTH_SIMPLE,
100+
ctrls: RequestControls = None) -> ldap.response.BindResult:
101+
msgid = self.bind(dn, cred, method)
102+
responses = self.result(msgid)
103+
result, = responses
104+
return result
105+
106+
def compare_s(self, dn: str, attr: str, value: bytes, *,
107+
ctrls: RequestControls = None
108+
) -> ldap.response.CompareResult:
109+
"TODO: remove _s functions introducing a better request API"
110+
msgid = self.compare_ext(dn, attr, value, serverctrls=ctrls)
111+
responses = self.result(msgid)
112+
result, = responses
113+
return bool(result)
114+
115+
def delete_s(self, dn: str, *,
116+
ctrls: RequestControls = None) -> ldap.response.DeleteResult:
117+
msgid = self.delete_ext(dn, serverctrls=ctrls)
118+
responses = self.result(msgid)
119+
result, = responses
120+
return result
121+
122+
def extop_s(self, oid: Optional[str] = None,
123+
value: Optional[bytes] = None, *,
124+
request: Optional[ExtendedRequest] = None,
125+
ctrls: RequestControls = None
126+
) -> list[Union[IntermediateResponse, ExtendedResult]]:
127+
if request is not None:
128+
oid = request.requestName
129+
value = request.encodedRequestValue()
130+
131+
msgid = self.extop(oid, value, serverctrls=ctrls)
132+
return self.result(msgid)
133+
134+
def search_s(self, base: Optional[str] = None,
135+
scope: int = ldap.SCOPE_SUBTREE,
136+
filter: str = "(objectClass=*)",
137+
attrlist: Optional[list[str]] = None, *,
138+
attrsonly: bool = False,
139+
ctrls: RequestControls = None,
140+
sizelimit: int = 0, timelimit: int = -1,
141+
timeout: Optional[Real] = None
142+
) -> list[Union[SearchEntry, SearchReference]]:
143+
if timeout is None:
144+
timeout = timelimit
145+
146+
msgid = self.search_ext(base, scope, filter, attrlist=attrlist,
147+
attrsonly=attrsonly, serverctrls=ctrls,
148+
sizelimit=sizelimit, timeout=timelimit)
149+
result = self.result(msgid, timeout=timeout)
150+
result[-1].raise_for_result()
151+
return result[:-1]
152+
153+
def search_subschemasubentry_s(
154+
self, dn: Optional[str] = None) -> Optional[str]:
155+
"""
156+
Returns the distinguished name of the sub schema sub entry
157+
for a part of a DIT specified by dn.
158+
159+
None as result indicates that the DN of the sub schema sub entry could
160+
not be determined.
161+
"""
162+
empty_dn = ''
163+
attrname = 'subschemaSubentry'
164+
if dn is None:
165+
dn = empty_dn
166+
try:
167+
r = self.search_s(dn, ldap.SCOPE_BASE, None, [attrname])
168+
except (ldap.NO_SUCH_OBJECT, ldap.NO_SUCH_ATTRIBUTE,
169+
ldap.INSUFFICIENT_ACCESS):
170+
r = []
171+
except ldap.UNDEFINED_TYPE:
172+
return None
173+
174+
attr = r and ldap.cidict.cidict(r[0].attrs).get(attrname)
175+
if attr:
176+
return attr[0].decode('utf-8')
177+
elif dn:
178+
# Try to find sub schema sub entry in root DSE
179+
return self.search_subschemasubentry_s(dn=empty_dn)
180+
else:
181+
# If dn was already rootDSE we can return here
182+
return None
183+
184+
def read_s(self, dn: str, filterstr: Optional[str] = None,
185+
attrlist: Optional[list[str]] = None,
186+
ctrls: RequestControls = None,
187+
timeout: int = -1) -> dict[str, bytes]:
188+
"""
189+
Reads and returns a single entry specified by `dn'.
190+
191+
Other attributes just like those passed to `search_s()'
192+
"""
193+
r = self.search_s(dn, ldap.SCOPE_BASE, filterstr,
194+
attrlist=attrlist, ctrls=ctrls, timeout=timeout)
195+
if r:
196+
return r[0].attrs
197+
else:
198+
return None
199+
200+
def find_unique_entry(self, base: Optional[str] = None,
201+
scope: int = ldap.SCOPE_SUBTREE,
202+
filter: str = "(objectClass=*)",
203+
attrlist: Optional[list[str]] = None, *,
204+
attrsonly: bool = False,
205+
ctrls: RequestControls = None,
206+
timelimit: int = -1,
207+
timeout: Optional[Real] = None
208+
) -> list[Union[SearchEntry, SearchReference]]:
209+
"""
210+
Returns a unique entry, raises exception if not unique
211+
"""
212+
r = self.search_s(base, scope, filter, attrlist=attrlist,
213+
attrsonly=attrsonly, ctrls=ctrls, timeout=timeout,
214+
sizelimit=2)
215+
if len(r) != 1:
216+
raise NO_UNIQUE_ENTRY(f'No or non-unique search result for {filter}')
217+
return r[0]

0 commit comments

Comments
 (0)