4
4
See https://www.python-ldap.org/ for details.
5
5
"""
6
6
7
+ from __future__ import unicode_literals
8
+
7
9
from os import strerror
8
10
9
11
from ldap .pkginfo import __version__ , __author__ , __license__
20
22
import traceback
21
23
22
24
import sys ,time ,pprint ,_ldap ,ldap ,ldap .sasl ,ldap .functions
25
+ import warnings
23
26
24
27
from ldap .schema import SCHEMA_ATTRS
25
28
from ldap .controls import LDAPControl ,DecodeControlTuples ,RequestControlTuples
28
31
29
32
from ldap import LDAPError
30
33
34
+ PY2 = bool (sys .version_info [0 ] <= 2 )
35
+ if PY2 :
36
+ text_type = unicode
37
+ else :
38
+ text_type = str
31
39
32
40
class NO_UNIQUE_ENTRY (ldap .NO_SUCH_OBJECT ):
33
41
"""
@@ -55,7 +63,7 @@ class SimpleLDAPObject:
55
63
56
64
def __init__ (
57
65
self ,uri ,
58
- trace_level = 0 ,trace_file = None ,trace_stack_limit = 5
66
+ trace_level = 0 ,trace_file = None ,trace_stack_limit = 5 , bytes_mode = None
59
67
):
60
68
self ._trace_level = trace_level
61
69
self ._trace_file = trace_file or sys .stdout
@@ -66,6 +74,186 @@ def __init__(
66
74
self .timeout = - 1
67
75
self .protocol_version = ldap .VERSION3
68
76
77
+ # Bytes mode
78
+ # ----------
79
+
80
+ # By default, raise a TypeError when receiving invalid args
81
+ self .bytes_mode_hardfail = True
82
+ if bytes_mode is None and PY2 :
83
+ warnings .warn (
84
+ "Under Python 2, python-ldap uses bytes by default. "
85
+ "This will be removed in Python 3 (no bytes for DN/RDN/field names). "
86
+ "Please call initialize(..., bytes_mode=False) explicitly." ,
87
+ BytesWarning ,
88
+ stacklevel = 2 ,
89
+ )
90
+ bytes_mode = True
91
+ # Disable hard failure when running in backwards compatibility mode.
92
+ self .bytes_mode_hardfail = False
93
+ elif bytes_mode and not PY2 :
94
+ raise ValueError ("bytes_mode is *not* supported under Python 3." )
95
+ # On by default on Py2, off on Py3.
96
+ self .bytes_mode = bytes_mode
97
+
98
+ def _bytesify_input (self , value ):
99
+ """Adapt a value following bytes_mode in Python 2.
100
+
101
+ In Python 3, returns the original value unmodified.
102
+
103
+ With bytes_mode ON, takes bytes or None and returns bytes or None.
104
+ With bytes_mode OFF, takes unicode or None and returns bytes or None.
105
+
106
+ This function should be applied on all text inputs (distinguished names
107
+ and attribute names in modlists) to convert them to the bytes expected
108
+ by the C bindings.
109
+ """
110
+ if not PY2 :
111
+ return value
112
+
113
+ if value is None :
114
+ return value
115
+ elif self .bytes_mode :
116
+ if isinstance (value , bytes ):
117
+ return value
118
+ else :
119
+ if self .bytes_mode_hardfail :
120
+ raise TypeError ("All provided fields *must* be bytes when bytes mode is on; got %r" % (value ,))
121
+ else :
122
+ warnings .warn (
123
+ "Received non-bytes value %r with default (disabled) bytes mode; please choose an explicit "
124
+ "option for bytes_mode on your LDAP connection" % (value ,),
125
+ BytesWarning ,
126
+ stacklevel = 6 ,
127
+ )
128
+ return value .encode ('utf-8' )
129
+ else :
130
+ if not isinstance (value , text_type ):
131
+ raise TypeError ("All provided fields *must* be text when bytes mode is off; got %r" % (value ,))
132
+ assert not isinstance (value , bytes )
133
+ return value .encode ('utf-8' )
134
+
135
+ def _bytesify_inputs (self , * values ):
136
+ """Adapt values following bytes_mode.
137
+
138
+ Applies _bytesify_input on each arg.
139
+
140
+ Usage:
141
+ >>> a, b, c = self._bytesify_inputs(a, b, c)
142
+ """
143
+ if not PY2 :
144
+ return values
145
+ return (
146
+ self ._bytesify_input (value )
147
+ for value in values
148
+ )
149
+
150
+ def _bytesify_modlist (self , modlist , with_opcode ):
151
+ """Adapt a modlist according to bytes_mode.
152
+
153
+ A modlist is a tuple of (op, attr, value), where:
154
+ - With bytes_mode ON, attr is checked to be bytes
155
+ - With bytes_mode OFF, attr is converted from unicode to bytes
156
+ - value is *always* bytes
157
+ """
158
+ if not PY2 :
159
+ return modlist
160
+ if with_opcode :
161
+ return tuple (
162
+ (op , self ._bytesify_input (attr ), val )
163
+ for op , attr , val in modlist
164
+ )
165
+ else :
166
+ return tuple (
167
+ (self ._bytesify_input (attr ), val )
168
+ for attr , val in modlist
169
+ )
170
+
171
+ def _unbytesify_text_value (self , value ):
172
+ """Adapt a 'known text, UTF-8 encoded' returned value following bytes_mode.
173
+
174
+ With bytes_mode ON, takes bytes or None and returns bytes or None.
175
+ With bytes_mode OFF, takes bytes or None and returns unicode or None.
176
+
177
+ This function should only be applied on field *values*; distinguished names
178
+ or field *names* are already natively handled in result4.
179
+ """
180
+ if value is None :
181
+ return value
182
+
183
+ # Preserve logic of assertions only under Python 2
184
+ if PY2 :
185
+ assert isinstance (value , bytes ), "Expected bytes value, got text instead (%r)" % (value ,)
186
+
187
+ if self .bytes_mode :
188
+ return value
189
+ else :
190
+ return value .decode ('utf-8' )
191
+
192
+ def _maybe_rebytesify_text (self , value ):
193
+ """Re-encodes text to bytes if needed by bytes_mode.
194
+
195
+ Takes unicode (and checks for it), and returns:
196
+ - bytes under bytes_mode
197
+ - unicode otherwise.
198
+ """
199
+ if not PY2 :
200
+ return value
201
+
202
+ if value is None :
203
+ return value
204
+
205
+ assert isinstance (value , text_type ), "Should return text, got bytes instead (%r)" % (value ,)
206
+ if not self .bytes_mode :
207
+ return value
208
+ else :
209
+ return value .encode ('utf-8' )
210
+
211
+ def _bytesify_result_value (self , result_value ):
212
+ """Applies bytes_mode to a result value.
213
+
214
+ Such a value can either be:
215
+ - a dict mapping an attribute name to its list of values
216
+ (where attribute names are unicode and values bytes)
217
+ - a list of referals (which are unicode)
218
+ """
219
+ if not PY2 :
220
+ return result_value
221
+ if hasattr (result_value , 'items' ):
222
+ # It's a attribute_name: [values] dict
223
+ return dict (
224
+ (self ._maybe_rebytesify_text (key ), value )
225
+ for (key , value ) in result_value .items ()
226
+ )
227
+ elif isinstance (result_value , bytes ):
228
+ return result_value
229
+ else :
230
+ # It's a list of referals
231
+ # Example value:
232
+ # [u'ldap://DomainDnsZones.xxxx.root.local/DC=DomainDnsZones,DC=xxxx,DC=root,DC=local']
233
+ return [self ._maybe_rebytesify_text (referal ) for referal in result_value ]
234
+
235
+ def _bytesify_results (self , results , with_ctrls = False ):
236
+ """Converts a "results" object according to bytes_mode.
237
+
238
+ Takes:
239
+ - a list of (dn, {field: [values]}) if with_ctrls is False
240
+ - a list of (dn, {field: [values]}, ctrls) if with_ctrls is True
241
+
242
+ And, if bytes_mode is on, converts dn and fields to bytes.
243
+ """
244
+ if not PY2 :
245
+ return results
246
+ if with_ctrls :
247
+ return [
248
+ (self ._maybe_rebytesify_text (dn ), self ._bytesify_result_value (fields ), ctrls )
249
+ for (dn , fields , ctrls ) in results
250
+ ]
251
+ else :
252
+ return [
253
+ (self ._maybe_rebytesify_text (dn ), self ._bytesify_result_value (fields ))
254
+ for (dn , fields ) in results
255
+ ]
256
+
69
257
def _ldap_lock (self ,desc = '' ):
70
258
if ldap .LIBLDAP_R :
71
259
return ldap .LDAPLock (desc = '%s within %s' % (desc ,repr (self )))
@@ -185,6 +373,8 @@ def add_ext(self,dn,modlist,serverctrls=None,clientctrls=None):
185
373
The parameter modlist is similar to the one passed to modify(),
186
374
except that no operation integer need be included in the tuples.
187
375
"""
376
+ dn = self ._bytesify_input (dn )
377
+ modlist = self ._bytesify_modlist (modlist , with_opcode = False )
188
378
return self ._ldap_call (self ._l .add_ext ,dn ,modlist ,RequestControlTuples (serverctrls ),RequestControlTuples (clientctrls ))
189
379
190
380
def add_ext_s (self ,dn ,modlist ,serverctrls = None ,clientctrls = None ):
@@ -209,6 +399,7 @@ def simple_bind(self,who='',cred='',serverctrls=None,clientctrls=None):
209
399
"""
210
400
simple_bind([who='' [,cred='']]) -> int
211
401
"""
402
+ who , cred = self ._bytesify_inputs (who , cred )
212
403
return self ._ldap_call (self ._l .simple_bind ,who ,cred ,RequestControlTuples (serverctrls ),RequestControlTuples (clientctrls ))
213
404
214
405
def simple_bind_s (self ,who = '' ,cred = '' ,serverctrls = None ,clientctrls = None ):
@@ -285,6 +476,7 @@ def compare_ext(self,dn,attr,value,serverctrls=None,clientctrls=None):
285
476
A design bug in the library prevents value from containing
286
477
nul characters.
287
478
"""
479
+ dn , attr = self ._bytesify_inputs (dn , attr )
288
480
return self ._ldap_call (self ._l .compare_ext ,dn ,attr ,value ,RequestControlTuples (serverctrls ),RequestControlTuples (clientctrls ))
289
481
290
482
def compare_ext_s (self ,dn ,attr ,value ,serverctrls = None ,clientctrls = None ):
@@ -315,6 +507,7 @@ def delete_ext(self,dn,serverctrls=None,clientctrls=None):
315
507
form returns the message id of the initiated request, and the
316
508
result can be obtained from a subsequent call to result().
317
509
"""
510
+ dn = self ._bytesify_input (dn )
318
511
return self ._ldap_call (self ._l .delete_ext ,dn ,RequestControlTuples (serverctrls ),RequestControlTuples (clientctrls ))
319
512
320
513
def delete_ext_s (self ,dn ,serverctrls = None ,clientctrls = None ):
@@ -363,6 +556,8 @@ def modify_ext(self,dn,modlist,serverctrls=None,clientctrls=None):
363
556
"""
364
557
modify_ext(dn, modlist[,serverctrls=None[,clientctrls=None]]) -> int
365
558
"""
559
+ dn = self ._bytesify_input (dn )
560
+ modlist = self ._bytesify_modlist (modlist , with_opcode = True )
366
561
return self ._ldap_call (self ._l .modify_ext ,dn ,modlist ,RequestControlTuples (serverctrls ),RequestControlTuples (clientctrls ))
367
562
368
563
def modify_ext_s (self ,dn ,modlist ,serverctrls = None ,clientctrls = None ):
@@ -416,6 +611,7 @@ def modrdn_s(self,dn,newrdn,delold=1):
416
611
return self .rename_s (dn ,newrdn ,None ,delold )
417
612
418
613
def passwd (self ,user ,oldpw ,newpw ,serverctrls = None ,clientctrls = None ):
614
+ user , oldpw , newpw = self ._bytesify_inputs (user , oldpw , newpw )
419
615
return self ._ldap_call (self ._l .passwd ,user ,oldpw ,newpw ,RequestControlTuples (serverctrls ),RequestControlTuples (clientctrls ))
420
616
421
617
def passwd_s (self ,user ,oldpw ,newpw ,serverctrls = None ,clientctrls = None ):
@@ -437,6 +633,7 @@ def rename(self,dn,newrdn,newsuperior=None,delold=1,serverctrls=None,clientctrls
437
633
This actually corresponds to the rename* routines in the
438
634
LDAP-EXT C API library.
439
635
"""
636
+ dn , newrdn , newsuperior = self ._bytesify_inputs (dn , newrdn , newsuperior )
440
637
return self ._ldap_call (self ._l .rename ,dn ,newrdn ,newsuperior ,delold ,RequestControlTuples (serverctrls ),RequestControlTuples (clientctrls ))
441
638
442
639
def rename_s (self ,dn ,newrdn ,newsuperior = None ,delold = 1 ,serverctrls = None ,clientctrls = None ):
@@ -525,6 +722,8 @@ def result4(self,msgid=ldap.RES_ANY,all=1,timeout=None,add_ctrls=0,add_intermedi
525
722
if add_ctrls :
526
723
resp_data = [ (t ,r ,DecodeControlTuples (c ,resp_ctrl_classes )) for t ,r ,c in resp_data ]
527
724
decoded_resp_ctrls = DecodeControlTuples (resp_ctrls ,resp_ctrl_classes )
725
+ if resp_data is not None :
726
+ resp_data = self ._bytesify_results (resp_data , with_ctrls = add_ctrls )
528
727
return resp_type , resp_data , resp_msgid , decoded_resp_ctrls , resp_name , resp_value
529
728
530
729
def search_ext (self ,base ,scope ,filterstr = '(objectClass=*)' ,attrlist = None ,attrsonly = 0 ,serverctrls = None ,clientctrls = None ,timeout = - 1 ,sizelimit = 0 ):
@@ -572,6 +771,9 @@ def search_ext(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrson
572
771
The amount of search results retrieved can be limited with the
573
772
sizelimit parameter if non-zero.
574
773
"""
774
+ base , filterstr = self ._bytesify_inputs (base , filterstr )
775
+ if attrlist is not None :
776
+ attrlist = tuple (self ._bytesify_inputs (* attrlist ))
575
777
return self ._ldap_call (
576
778
self ._l .search_ext ,
577
779
base ,scope ,filterstr ,
@@ -665,6 +867,8 @@ def search_subschemasubentry_s(self,dn=''):
665
867
666
868
None as result indicates that the DN of the sub schema sub entry could
667
869
not be determined.
870
+
871
+ Returns: None or text/bytes depending on bytes_mode.
668
872
"""
669
873
try :
670
874
r = self .search_s (
@@ -686,7 +890,9 @@ def search_subschemasubentry_s(self,dn=''):
686
890
# If dn was already root DSE we can return here
687
891
return None
688
892
else :
689
- return search_subschemasubentry_dn
893
+ # With legacy bytes mode, return bytes; otherwise, since this is a DN,
894
+ # RFCs impose that the field value *can* be decoded to UTF-8.
895
+ return self ._unbytesify_text_value (search_subschemasubentry_dn )
690
896
except IndexError :
691
897
return None
692
898
@@ -788,7 +994,7 @@ class ReconnectLDAPObject(SimpleLDAPObject):
788
994
789
995
def __init__ (
790
996
self ,uri ,
791
- trace_level = 0 ,trace_file = None ,trace_stack_limit = 5 ,
997
+ trace_level = 0 ,trace_file = None ,trace_stack_limit = 5 ,bytes_mode = None ,
792
998
retry_max = 1 ,retry_delay = 60.0
793
999
):
794
1000
"""
@@ -803,7 +1009,7 @@ def __init__(
803
1009
self ._uri = uri
804
1010
self ._options = []
805
1011
self ._last_bind = None
806
- SimpleLDAPObject .__init__ (self ,uri ,trace_level ,trace_file ,trace_stack_limit )
1012
+ SimpleLDAPObject .__init__ (self ,uri ,trace_level ,trace_file ,trace_stack_limit , bytes_mode )
807
1013
self ._reconnect_lock = ldap .LDAPLock (desc = 'reconnect lock within %s' % (repr (self )))
808
1014
self ._retry_max = retry_max
809
1015
self ._retry_delay = retry_delay
0 commit comments