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)
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