Skip to content

Commit f1da4bb

Browse files
committed
Draft new response handling API
1 parent 6a92648 commit f1da4bb

File tree

9 files changed

+1016
-12
lines changed

9 files changed

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