diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..072e203 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ + +# Auto-generated +.*.swp +*.pyc +__pycache__/ +.tox + +# shared libs installed by 'setup.py test' +/Lib/*.so* +/Lib/*.dylib +/Lib/*.pyd + +# Build related +*.egg-info +build/ +dist/ +PKG-INFO diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..542377d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,25 @@ +language: python + +python: +- '2.7' +- '3.3' +- '3.4' +- '3.5' +- '3.6' +# Note: when updating Python versions, also change setup.py and tox.ini + +sudo: false + +cache: pip + +addons: + apt: + packages: + - ldap-utils + - slapd + +install: + - pip install "pip>=7.1.0" + - pip install tox-travis tox + +script: tox diff --git a/CHANGES b/CHANGES index b3ef0d5..113eb27 100644 --- a/CHANGES +++ b/CHANGES @@ -1,47 +1,30 @@ ---------------------------------------------------------------- -Released 2.5.2 2017-11-20 - -Changes since 2.5.1: - -Modules/ -* PyBytes_ instead of PyString_ and added PyInt_FromLong compat macro -* moved code from version.c to ldapmodule.c -* removed obsolete back-ward compability constants from common.h -* build checks whether LDAP_API_VERSION is OpenLDAP 2.4.x -* _ldap.__author__ and _ldap.__license__ also set from ldap.pkginfo -* assume C extension API for Python 2.7+ - -Lib/ -* removed all dependencies on modules string and types -* removed use of .has_key() -* removed class ldap.ldapobject.NonblockingLDAPObject -* new global constant ldap.LIBLDAP_API_INFO -* right after importing _ldap there is a call into libldap to initialize it -* method .decodeControlValue() of SSSResponseControl and VLVResponseControl - does not set class attribute result_code anymore -* always use bytes() for UUID() constructor in ldap.syncrepl -* module ldif now uses functions b64encode() and b64decode() -* fixed pickling and restoring of ReconnectLDAPObject - -Tests/ -* scripts do not directly call SlapdTestCase.setUpClass() anymore -* added LDIF test with folded, base64-encoded attribute -* added more tests for sub-module ldap.dn -* added tests for ldap.syncrepl (thanks to Karl Kornel) - ----------------------------------------------------------------- -Released 2.5.1 2017-11-12 +Released 3.0.0 xxxx-xx-xx Changes since 2.4.45: Mandatory prerequisites: -- Python 2.7.x +- Python 2.7.x or 3.3+ - pyasn1 0.3.7+ and pyasn1_modules 0.1.5+ +Python 3 support is merged from the pyldap fork (https://github.com/pyldap) + +Infrastructure: +- Add .gitignore +- Re-format README to ReStructured Text +- Setup for automatic testing using Travis CI + Modules/ +(thanks to Michael Ströder) * removed unused code schema.c +* moved code from version.c to ldapmodule.c +* removed obsolete back-ward compability constants from common.h +* build checks whether LDAP_API_VERSION is OpenLDAP 2.4.x +* _ldap.__author__ and _ldap.__license__ also set from ldap.pkginfo +* assume C extension API for Python 2.7+ Lib/ +(thanks to Michael Ströder) * ldap.__version__, ldap.__author__ and ldap.__license__ now imported from new sub-module ldap.pkginfo also to setup.py * Added safety assertion when importing _ldap: @@ -58,9 +41,35 @@ Lib/ but should not be used in new code because they might be removed in a later release. * removed SSSRequestControl from ldap.controls.KNOWN_RESPONSE_CONTROLS +* removed all dependencies on modules string and types +* removed use of .has_key() +* removed class ldap.ldapobject.NonblockingLDAPObject +* new global constant ldap.LIBLDAP_API_INFO +* right after importing _ldap there is a call into libldap to initialize it +* method .decodeControlValue() of SSSResponseControl and VLVResponseControl + does not set class attribute result_code anymore +* always use bytes() for UUID() constructor in ldap.syncrepl +* module ldif now uses functions b64encode() and b64decode() +* fixed pickling and restoring of ReconnectLDAPObject + +Lib/slapdtest.py +* Automatically try some common locations for SCHEMADIR +* Ensure server is stopped when the process exits Tests/ +(thanks to Michael Ströder) * added explicit reconnect tests for ReconnectLDAPObject +* scripts do not directly call SlapdTestCase.setUpClass() anymore +* added LDIF test with folded, base64-encoded attribute +* added more tests for sub-module ldap.dn +* added tests for ldap.syncrepl (thanks to Karl Kornel) + +Tests/ +(thanks to pyldap contributors): +* Expand cidict membership test +* Add test suite for binds +* Add test suite for edits +* Add a smoke-check for listall() and attribute_types() ---------------------------------------------------------------- Released 2.4.45 2017-10-09 diff --git a/Demo/Lib/ldap/async/deltree.py b/Demo/Lib/ldap/async/deltree.py index 58df3b3..68d3643 100644 --- a/Demo/Lib/ldap/async/deltree.py +++ b/Demo/Lib/ldap/async/deltree.py @@ -1,3 +1,5 @@ +from __future__ import print_function + import ldap,ldap.async class DeleteLeafs(ldap.async.AsyncSearchHandler): @@ -15,7 +17,7 @@ def __init__(self,l): def startSearch(self,searchRoot,searchScope): if not searchScope in [ldap.SCOPE_ONELEVEL,ldap.SCOPE_SUBTREE]: - raise ValueError, "Parameter searchScope must be either ldap.SCOPE_ONELEVEL or ldap.SCOPE_SUBTREE." + raise ValueError("Parameter searchScope must be either ldap.SCOPE_ONELEVEL or ldap.SCOPE_SUBTREE.") self.nonLeafEntries = [] self.deletedEntries = 0 ldap.async.AsyncSearchHandler.startSearch( @@ -28,7 +30,7 @@ def startSearch(self,searchRoot,searchScope): ) def _processSingleResult(self,resultType,resultItem): - if self._entryResultTypes.has_key(resultType): + if resultType in self._entryResultTypes: # Don't process search references dn,entry = resultItem hasSubordinates = entry.get( @@ -45,7 +47,7 @@ def _processSingleResult(self,resultType,resultItem): else: try: self._l.delete_s(dn) - except ldap.NOT_ALLOWED_ON_NONLEAF,e: + except ldap.NOT_ALLOWED_ON_NONLEAF as e: self.nonLeafEntries.append(dn) else: self.deletedEntries = self.deletedEntries+1 @@ -62,7 +64,7 @@ def DelTree(l,dn,scope=ldap.SCOPE_ONELEVEL): non_leaf_entries = leafs_deleter.nonLeafEntries[:] while non_leaf_entries: dn = non_leaf_entries.pop() - print deleted_entries,len(non_leaf_entries),dn + print(deleted_entries,len(non_leaf_entries),dn) leafs_deleter.startSearch(dn,ldap.SCOPE_SUBTREE) leafs_deleter.processResults() deleted_entries = deleted_entries+leafs_deleter.deletedEntries diff --git a/Demo/Lib/ldapurl/urlsearch.py b/Demo/Lib/ldapurl/urlsearch.py index 996e6da..b293aa2 100644 --- a/Demo/Lib/ldapurl/urlsearch.py +++ b/Demo/Lib/ldapurl/urlsearch.py @@ -3,30 +3,30 @@ No output of LDAP data is produced except trace output. """ - +from __future__ import print_function import sys,getpass,ldap,ldapurl try: ldapUrl = ldapurl.LDAPUrl(ldapUrl=sys.argv[1]) except IndexError: - print 'Usage: %s [LDAP URL]' % (sys.argv[0]) + print('Usage: %s [LDAP URL]' % (sys.argv[0])) sys.exit(1) for a in [ 'urlscheme','hostport','dn','attrs','scope', 'filterstr','extensions','who','cred' ]: - print a,repr(getattr(ldapUrl,a)) + print(a,repr(getattr(ldapUrl,a))) l = ldap.initialize(ldapUrl.initializeUrl(),trace_level=1) if ldapUrl.who!=None: if ldapUrl.cred!=None: cred=ldapUrl.cred else: - print 'Enter password for simple bind with',repr(ldapUrl.who) + print('Enter password for simple bind with',repr(ldapUrl.who)) cred=getpass.getpass() l.simple_bind_s(ldapUrl.who,cred) res = l.search_s(ldapUrl.dn,ldapUrl.scope,ldapUrl.filterstr,ldapUrl.attrs) -print len(res),'search results' +print(len(res),'search results') diff --git a/Demo/initialize.py b/Demo/initialize.py index ab2647e..952b3f4 100644 --- a/Demo/initialize.py +++ b/Demo/initialize.py @@ -7,6 +7,7 @@ ldaps://localhost:1391 (LDAP over SSL) ldapi://%2ftmp%2fopenldap2 (domain socket /tmp/openldap2) """ +from __future__ import print_function import sys,os,ldap @@ -23,10 +24,10 @@ # Complete path name of the file containing all trusted CA certs CACERTFILE='/etc/ssl/ca-bundle.pem' -print """################################################################## +print("""################################################################## # LDAPv3 connection with StartTLS ext. op. ################################################################## -""" +""") # Create LDAPObject instance l = ldap.initialize('ldap://localhost:1390',trace_level=ldapmodule_trace_level,trace_file=ldapmodule_trace_file) @@ -44,8 +45,8 @@ # Now try StartTLS extended operation l.start_tls_s() -print '***ldap.OPT_X_TLS_VERSION',l.get_option(ldap.OPT_X_TLS_VERSION) -print '***ldap.OPT_X_TLS_CIPHER',l.get_option(ldap.OPT_X_TLS_CIPHER) +print('***ldap.OPT_X_TLS_VERSION',l.get_option(ldap.OPT_X_TLS_VERSION)) +print('***ldap.OPT_X_TLS_CIPHER',l.get_option(ldap.OPT_X_TLS_CIPHER)) # Try an explicit anon bind to provoke failure l.simple_bind_s('','') @@ -53,10 +54,10 @@ # Close connection l.unbind_s() -print """################################################################## +print("""################################################################## # LDAPv3 connection over SSL ################################################################## -""" +""") # Create LDAPObject instance l = ldap.initialize('ldaps://localhost:1391',trace_level=ldapmodule_trace_level,trace_file=ldapmodule_trace_file) @@ -74,16 +75,16 @@ # Try an explicit anon bind to provoke failure l.simple_bind_s('','') -print '***ldap.OPT_X_TLS_VERSION',l.get_option(ldap.OPT_X_TLS_VERSION) -print '***ldap.OPT_X_TLS_CIPHER',l.get_option(ldap.OPT_X_TLS_CIPHER) +print('***ldap.OPT_X_TLS_VERSION',l.get_option(ldap.OPT_X_TLS_VERSION)) +print('***ldap.OPT_X_TLS_CIPHER',l.get_option(ldap.OPT_X_TLS_CIPHER)) # Close connection l.unbind_s() -print """################################################################## +print("""################################################################## # LDAPv3 connection over Unix domain socket ################################################################## -""" +""") # Create LDAPObject instance l = ldap.initialize('ldapi://%2ftmp%2fopenldap-socket',trace_level=ldapmodule_trace_level,trace_file=ldapmodule_trace_file) diff --git a/Demo/ldapcontrols.py b/Demo/ldapcontrols.py index 214042d..a5ba8d3 100644 --- a/Demo/ldapcontrols.py +++ b/Demo/ldapcontrols.py @@ -1,15 +1,16 @@ +from __future__ import print_function import ldap,ldapurl,pprint from ldap.controls import LDAPControl,BooleanControl l = ldap.initialize('ldap://localhost:1390',trace_level=2) -print 60*'#' +print(60*'#') pprint.pprint(l.get_option(ldap.OPT_SERVER_CONTROLS)) l.manage_dsa_it(1,1) pprint.pprint(l.get_option(ldap.OPT_SERVER_CONTROLS)) -print 60*'#' +print(60*'#') # Search with ManageDsaIT control (which has no value) pprint.pprint(l.search_ext_s( @@ -19,7 +20,7 @@ ['*','+'], serverctrls = [ LDAPControl('2.16.840.1.113730.3.4.2',1,None) ], )) -print 60*'#' +print(60*'#') # Search with Subentries control (which has boolean value) pprint.pprint(l.search_ext_s( @@ -30,4 +31,4 @@ serverctrls = [ BooleanControl('1.3.6.1.4.1.4203.1.10.1',1,1) ], )) -print 60*'#' +print(60*'#') diff --git a/Demo/ldapurl_search.py b/Demo/ldapurl_search.py index 2c63009..07ffbca 100644 --- a/Demo/ldapurl_search.py +++ b/Demo/ldapurl_search.py @@ -1,3 +1,5 @@ +from __future__ import print_function + import sys,pprint,ldap from ldap.ldapobject import LDAPObject @@ -15,7 +17,7 @@ class MyLDAPUrl(LDAPUrl): ldap_url = MyLDAPUrl(sys.argv[1]) trace_level = int(ldap_url.trace_level or '0') -print '***trace_level',trace_level +print('***trace_level',trace_level) ldap.trace_level = trace_level @@ -37,6 +39,6 @@ class MyLDAPUrl(LDAPUrl): pprint.pprint(result) -print '***DIAGNOSTIC_MESSAGE',repr(l.get_option(ldap.OPT_DIAGNOSTIC_MESSAGE)) +print('***DIAGNOSTIC_MESSAGE',repr(l.get_option(ldap.OPT_DIAGNOSTIC_MESSAGE))) l.unbind_s() diff --git a/Demo/matchedvalues.py b/Demo/matchedvalues.py index 4de3e8a..59c594f 100644 --- a/Demo/matchedvalues.py +++ b/Demo/matchedvalues.py @@ -27,16 +27,17 @@ # Matched values control: (mail=*@example.org) # dn: uid=jsmith,ou=People,dc=example,dc=com # mail: jsmith@example.org +from __future__ import print_function import ldap from ldap.controls import MatchedValuesControl def print_result(search_result): for n in range(len(search_result)): - print "dn: %s" % search_result[n][0] + print("dn: %s" % search_result[n][0]) for attr in search_result[n][1].keys(): for i in range(len(search_result[n][1][attr])): - print "%s: %s" % (attr, search_result[n][1][attr][i]) + print("%s: %s" % (attr, search_result[n][1][attr][i])) print @@ -51,13 +52,13 @@ def print_result(search_result): mv = MatchedValuesControl(criticality=True, controlValue=control_filter) res = ld.search_ext_s(base, scope, filter, attrlist = ['mail']) -print "LDAP filter used: %s" % filter -print "Requesting 'mail' attribute back" +print("LDAP filter used: %s" % filter) +print("Requesting 'mail' attribute back") print -print "No matched values control:" +print("No matched values control:") print_result(res) res = ld.search_ext_s(base, scope, filter, attrlist = ['mail'], serverctrls = [mv]) -print "Matched values control: %s" % control_filter +print("Matched values control: %s" % control_filter) print_result(res) diff --git a/Demo/options.py b/Demo/options.py index fb37209..8b4e215 100644 --- a/Demo/options.py +++ b/Demo/options.py @@ -1,27 +1,28 @@ +from __future__ import print_function import ldap host="localhost:1390" -print "API info:",ldap.get_option(ldap.OPT_API_INFO) -print "debug level:",ldap.get_option(ldap.OPT_DEBUG_LEVEL) -#print "Setting debug level to 255..." +print("API info:",ldap.get_option(ldap.OPT_API_INFO)) +print("debug level:",ldap.get_option(ldap.OPT_DEBUG_LEVEL)) +#print("Setting debug level to 255...") #ldap.set_option(ldap.OPT_DEBUG_LEVEL,255) -#print "debug level:",ldap.get_option(ldap.OPT_DEBUG_LEVEL) -print "default size limit:",ldap.get_option(ldap.OPT_SIZELIMIT) -print "Setting default size limit to 10..." +#print("debug level:",ldap.get_option(ldap.OPT_DEBUG_LEVEL)) +print("default size limit:",ldap.get_option(ldap.OPT_SIZELIMIT)) +print("Setting default size limit to 10...") ldap.set_option(ldap.OPT_SIZELIMIT,10) -print "default size limit:",ldap.get_option(ldap.OPT_SIZELIMIT) -print "Creating connection to",host,"..." +print("default size limit:",ldap.get_option(ldap.OPT_SIZELIMIT)) +print("Creating connection to",host,"...") l=ldap.init(host) -print "size limit:",l.get_option(ldap.OPT_SIZELIMIT) -print "Setting connection size limit to 20..." +print("size limit:",l.get_option(ldap.OPT_SIZELIMIT)) +print("Setting connection size limit to 20...") l.set_option(ldap.OPT_SIZELIMIT,20) -print "size limit:",l.get_option(ldap.OPT_SIZELIMIT) -#print "Setting time limit to 60 secs..." +print("size limit:",l.get_option(ldap.OPT_SIZELIMIT)) +#print("Setting time limit to 60 secs...") l.set_option(ldap.OPT_TIMELIMIT,60) -#print "time limit:",l.get_option(ldap.OPT_TIMELIMIT) -print "Binding..." +#print("time limit:",l.get_option(ldap.OPT_TIMELIMIT)) +print("Binding...") l.simple_bind_s("","") diff --git a/Demo/page_control.py b/Demo/page_control.py index 0fc904b..8238ede 100644 --- a/Demo/page_control.py +++ b/Demo/page_control.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import print_function url = "ldap://localhost:1390" base = "dc=stroeder,dc=de" @@ -41,20 +42,20 @@ pages = 0 while True: pages += 1 - print '-'*60 - print "Getting page %d" % (pages) + print('-'*60) + print("Getting page %d" % (pages)) rtype, rdata, rmsgid, serverctrls = l.result3(msgid,resp_ctrl_classes=known_ldap_resp_ctrls) - print '%d results' % len(rdata) - print 'serverctrls=',pprint.pprint(serverctrls) - print 'rdata=',pprint.pprint(rdata) + print('%d results' % len(rdata)) + print('serverctrls=',pprint.pprint(serverctrls)) + print('rdata=',pprint.pprint(rdata)) pctrls = [ c for c in serverctrls if c.controlType == SimplePagedResultsControl.controlType ] if pctrls: - print 'pctrls[0].size',repr(pctrls[0].size) - print 'pctrls[0].cookie',repr(pctrls[0].cookie) + print('pctrls[0].size',repr(pctrls[0].size)) + print('pctrls[0].cookie',repr(pctrls[0].cookie)) if pctrls[0].cookie: # Copy cookie from response control to request control req_ctrl.cookie = pctrls[0].cookie @@ -68,7 +69,7 @@ else: break else: - print "Warning: Server ignores RFC 2696 control." + print("Warning: Server ignores RFC 2696 control.") break l.unbind_s() diff --git a/Demo/paged_search_ext_s.py b/Demo/paged_search_ext_s.py index d0e0982..d0f8291 100644 --- a/Demo/paged_search_ext_s.py +++ b/Demo/paged_search_ext_s.py @@ -1,3 +1,4 @@ +from __future__ import print_function url = "ldap://localhost:1390/" base = "dc=stroeder,dc=de" search_flt = r'(objectClass=*)' @@ -72,7 +73,7 @@ def paged_search_ext_s(self,base,scope,filterstr='(objectClass=*)',attrlist=None else: break # no more pages available - except ldap.SERVER_DOWN,e: + except ldap.SERVER_DOWN as e: try: self.reconnect(self._uri) except AttributeError: @@ -104,4 +105,4 @@ class MyLDAPObject(ReconnectLDAPObject,PagedResultsSearchObject): l.unbind_s() -print 'Received %d results in %d pages.' % (len(all_results),result_pages) +print('Received %d results in %d pages.' % (len(all_results),result_pages)) diff --git a/Demo/passwd_ext_op.py b/Demo/passwd_ext_op.py index 1030f0e..cc5d22c 100644 --- a/Demo/passwd_ext_op.py +++ b/Demo/passwd_ext_op.py @@ -1,6 +1,7 @@ """ Example showing the use of the password extended operation. """ +from __future__ import print_function import sys,ldap,ldapurl,getpass @@ -11,9 +12,9 @@ lu = ldapurl.LDAPUrl(sys.argv[1]) -print 'Old password' +print('Old password') oldpw = getpass.getpass() -print 'New password' +print('New password') newpw = getpass.getpass() # Set path name of file containing all CA certificates diff --git a/Demo/pyasn1/dds.py b/Demo/pyasn1/dds.py index 656b439..c803a1d 100644 --- a/Demo/pyasn1/dds.py +++ b/Demo/pyasn1/dds.py @@ -8,6 +8,7 @@ pyasn1-modules python-ldap 2.4+ """ +from __future__ import print_function from ldap.extop.dds import RefreshRequest,RefreshResponse @@ -16,8 +17,8 @@ try: ldap_url = ldapurl.LDAPUrl(sys.argv[1]) request_ttl = int(sys.argv[2]) -except IndexError,ValueError: - print 'Usage: dds.py ' +except (IndexError, ValueError): + print('Usage: dds.py ') sys.exit(1) # Set debugging level @@ -32,24 +33,24 @@ ) if ldap_url.cred is None: - print 'Password for %s:' % (repr(ldap_url.who)) + print('Password for %s:' % (repr(ldap_url.who))) ldap_url.cred = getpass.getpass() try: ldap_conn.simple_bind_s(ldap_url.who or '',ldap_url.cred or '') -except ldap.INVALID_CREDENTIALS,e: - print 'Simple bind failed:',str(e) +except ldap.INVALID_CREDENTIALS as e: + print('Simple bind failed:',str(e)) sys.exit(1) else: extreq = RefreshRequest(entryName=ldap_url.dn,requestTtl=request_ttl) try: extop_resp_obj = ldap_conn.extop_s(extreq,extop_resp_class=RefreshResponse) - except ldap.LDAPError,e: - print str(e) + except ldap.LDAPError as e: + print(str(e)) else: if extop_resp_obj.responseTtl!=request_ttl: - print 'Different response TTL:',extop_resp_obj.responseTtl + print('Different response TTL:',extop_resp_obj.responseTtl) else: - print 'Response TTL:',extop_resp_obj.responseTtl + print('Response TTL:',extop_resp_obj.responseTtl) diff --git a/Demo/pyasn1/derefcontrol.py b/Demo/pyasn1/derefcontrol.py index 1098529..0e7153d 100644 --- a/Demo/pyasn1/derefcontrol.py +++ b/Demo/pyasn1/derefcontrol.py @@ -3,6 +3,7 @@ This sample script demonstrates the use of the dereference control (see https://tools.ietf.org/html/draft-masarati-ldap-deref) """ +from __future__ import print_function import pprint,ldap,ldap.modlist,ldap.resiter @@ -29,8 +30,8 @@ class MyLDAPObject(ldap.ldapobject.LDAPObject,ldap.resiter.ResultProcessor): } ) -print 'pyasn1 output of request control:' -print dc._derefSpecs().prettyPrint() +print('pyasn1 output of request control:') +print(dc._derefSpecs().prettyPrint()) msg_id = l.search_ext( 'dc=demo1,dc=freeipa,dc=org', @@ -43,6 +44,6 @@ class MyLDAPObject(ldap.ldapobject.LDAPObject,ldap.resiter.ResultProcessor): for res_type,res_data,res_msgid,res_controls in l.allresults(msg_id,add_ctrls=1): for dn,entry,deref_control in res_data: # process dn and entry - print dn,entry['objectClass'] + print(dn,entry['objectClass']) if deref_control: pprint.pprint(deref_control[0].derefRes) diff --git a/Demo/pyasn1/noopsearch.py b/Demo/pyasn1/noopsearch.py index 08635de..2045f50 100644 --- a/Demo/pyasn1/noopsearch.py +++ b/Demo/pyasn1/noopsearch.py @@ -9,6 +9,7 @@ pyasn1-modules python-ldap 2.4+ """ +from __future__ import print_function import sys,ldap,ldapurl,getpass @@ -19,7 +20,7 @@ try: ldap_url = ldapurl.LDAPUrl(sys.argv[1]) except IndexError: - print 'Usage: noopsearch.py ' + print('Usage: noopsearch.py ') sys.exit(1) # Set debugging level @@ -34,14 +35,14 @@ ) if ldap_url.who and ldap_url.cred is None: - print 'Password for %s:' % (repr(ldap_url.who)) + print('Password for %s:' % (repr(ldap_url.who))) ldap_url.cred = getpass.getpass() try: ldap_conn.simple_bind_s(ldap_url.who or '',ldap_url.cred or '') -except ldap.INVALID_CREDENTIALS,e: - print 'Simple bind failed:',str(e) +except ldap.INVALID_CREDENTIALS as e: + print('Simple bind failed:',str(e)) sys.exit(1) try: @@ -58,7 +59,7 @@ ldap.TIMEOUT, ldap.TIMELIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED, - ldap.ADMINLIMIT_EXCEEDED),e: + ldap.ADMINLIMIT_EXCEEDED) as e: ldap_conn.abandon(msg_id) sys.exit(1) @@ -69,5 +70,5 @@ if c.controlType==SearchNoOpControl.controlType ][0] -print 'Number of search results: %d' % noop_srch_ctrl.numSearchResults -print 'Number of search continuations: %d' % noop_srch_ctrl.numSearchContinuations +print('Number of search results: %d' % noop_srch_ctrl.numSearchResults) +print('Number of search continuations: %d' % noop_srch_ctrl.numSearchContinuations) diff --git a/Demo/pyasn1/ppolicy.py b/Demo/pyasn1/ppolicy.py index 8722356..cf6b2ac 100644 --- a/Demo/pyasn1/ppolicy.py +++ b/Demo/pyasn1/ppolicy.py @@ -9,6 +9,7 @@ pyasn1-modules python-ldap 2.4+ """ +from __future__ import print_function import sys,ldap,ldapurl,getpass @@ -16,8 +17,8 @@ try: ldap_url = ldapurl.LDAPUrl(sys.argv[1]) -except IndexError,ValueError: - print 'Usage: ppolicy.py ' +except (IndexError,ValueError): + print('Usage: ppolicy.py ') sys.exit(1) # Set debugging level @@ -32,19 +33,19 @@ ) if ldap_url.cred is None: - print 'Password for %s:' % (repr(ldap_url.who)) + print('Password for %s:' % (repr(ldap_url.who))) ldap_url.cred = getpass.getpass() try: msgid = ldap_conn.simple_bind(ldap_url.who,ldap_url.cred,serverctrls=[PasswordPolicyControl()]) res_type,res_data,res_msgid,res_ctrls = ldap_conn.result3(msgid) -except ldap.INVALID_CREDENTIALS,e: - print 'Simple bind failed:',str(e) +except ldap.INVALID_CREDENTIALS as e: + print('Simple bind failed:',str(e)) sys.exit(1) else: if res_ctrls[0].controlType==PasswordPolicyControl.controlType: ppolicy_ctrl = res_ctrls[0] - print 'PasswordPolicyControl' - print 'error',repr(ppolicy_ctrl.error),(ppolicy_ctrl.error!=None)*repr(PasswordPolicyError(ppolicy_ctrl.error)) - print 'timeBeforeExpiration',repr(ppolicy_ctrl.timeBeforeExpiration) - print 'graceAuthNsRemaining',repr(ppolicy_ctrl.graceAuthNsRemaining) + print('PasswordPolicyControl') + print('error',repr(ppolicy_ctrl.error),(ppolicy_ctrl.error!=None)*repr(PasswordPolicyError(ppolicy_ctrl.error))) + print('timeBeforeExpiration',repr(ppolicy_ctrl.timeBeforeExpiration)) + print('graceAuthNsRemaining',repr(ppolicy_ctrl.graceAuthNsRemaining)) diff --git a/Demo/pyasn1/psearch.py b/Demo/pyasn1/psearch.py index 02852b1..3bd59e6 100644 --- a/Demo/pyasn1/psearch.py +++ b/Demo/pyasn1/psearch.py @@ -10,6 +10,7 @@ pyasn1-modules python-ldap 2.4+ """ +from __future__ import print_function import sys,ldap,ldapurl,getpass @@ -18,7 +19,7 @@ try: ldap_url = ldapurl.LDAPUrl(sys.argv[1]) except IndexError: - print 'Usage: psearch.py ' + print('Usage: psearch.py ') sys.exit(1) # Set debugging level @@ -33,14 +34,14 @@ ) if ldap_url.cred is None: - print 'Password for %s:' % (repr(ldap_url.who)) + print('Password for %s:' % (repr(ldap_url.who))) ldap_url.cred = getpass.getpass() try: ldap_conn.simple_bind_s(ldap_url.who,ldap_url.cred) -except ldap.INVALID_CREDENTIALS,e: - print 'Simple bind failed:',str(e) +except ldap.INVALID_CREDENTIALS as e: + print('Simple bind failed:',str(e)) sys.exit(1) psc = PersistentSearchControl() @@ -64,7 +65,7 @@ resp_ctrl_classes={EntryChangeNotificationControl.controlType:EntryChangeNotificationControl}, ) except ldap.TIMEOUT: - print 'Timeout waiting for results...' + print('Timeout waiting for results...') else: for dn,entry,srv_ctrls in res_data: ecn_ctrls = [ @@ -76,4 +77,4 @@ if ecn_ctrls: changeType,previousDN,changeNumber = ecn_ctrls[0].changeType,ecn_ctrls[0].previousDN,ecn_ctrls[0].changeNumber change_type_desc = CHANGE_TYPES_STR[changeType] - print 'changeType: %s (%d), changeNumber: %s, previousDN: %s' % (change_type_desc,changeType,changeNumber,repr(previousDN)) + print('changeType: %s (%d), changeNumber: %s, previousDN: %s' % (change_type_desc,changeType,changeNumber,repr(previousDN))) diff --git a/Demo/pyasn1/readentrycontrol.py b/Demo/pyasn1/readentrycontrol.py index b52ea91..10faa2b 100644 --- a/Demo/pyasn1/readentrycontrol.py +++ b/Demo/pyasn1/readentrycontrol.py @@ -4,6 +4,7 @@ Originally contributed by Andreas Hasenack """ +from __future__ import print_function import pprint,ldap,ldap.modlist @@ -14,10 +15,10 @@ l = ldap.initialize(uri,trace_level=2) l.simple_bind_s('uid=diradm,ou=schulung,dc=stroeder,dc=local','testsecret') -print """#--------------------------------------------------------------------------- +print("""#--------------------------------------------------------------------------- # Add new entry #--------------------------------------------------------------------------- -""" +""") new_test_dn = "uid=ablume,ou=Users,ou=schulung,dc=stroeder,dc=local" new_test_dn2 = "uid=ablume2,ou=Users,ou=schulung,dc=stroeder,dc=local" @@ -38,13 +39,13 @@ serverctrls = [pr] ) _,_,_,resp_ctrls = l.result3(msg_id) -print "resp_ctrls[0].dn:",resp_ctrls[0].dn -print "resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry) +print("resp_ctrls[0].dn:",resp_ctrls[0].dn) +print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry)) -print """#--------------------------------------------------------------------------- +print("""#--------------------------------------------------------------------------- # Modify entry #--------------------------------------------------------------------------- -""" +""") pr = PreReadControl(criticality=True,attrList=['uidNumber','gidNumber','entryCSN']) @@ -54,8 +55,8 @@ serverctrls = [pr] ) _,_,_,resp_ctrls = l.result3(msg_id) -print "resp_ctrls[0].dn:",resp_ctrls[0].dn -print "resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry) +print("resp_ctrls[0].dn:",resp_ctrls[0].dn) +print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry)) pr = PostReadControl(criticality=True,attrList=['uidNumber','gidNumber','entryCSN']) @@ -65,13 +66,13 @@ serverctrls = [pr] ) _,_,_,resp_ctrls = l.result3(msg_id) -print "resp_ctrls[0].dn:",resp_ctrls[0].dn -print "resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry) +print("resp_ctrls[0].dn:",resp_ctrls[0].dn) +print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry)) -print """#--------------------------------------------------------------------------- +print("""#--------------------------------------------------------------------------- # Rename entry #--------------------------------------------------------------------------- -""" +""") pr = PostReadControl(criticality=True,attrList=['uid']) msg_id = l.rename( @@ -81,8 +82,8 @@ serverctrls = [pr] ) _,_,_,resp_ctrls = l.result3(msg_id) -print "resp_ctrls[0].dn:",resp_ctrls[0].dn -print "resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry) +print("resp_ctrls[0].dn:",resp_ctrls[0].dn) +print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry)) pr = PreReadControl(criticality=True,attrList=['uid']) msg_id = l.rename( @@ -92,13 +93,13 @@ serverctrls = [pr] ) _,_,_,resp_ctrls = l.result3(msg_id) -print "resp_ctrls[0].dn:",resp_ctrls[0].dn -print "resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry) +print("resp_ctrls[0].dn:",resp_ctrls[0].dn) +print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry)) -print """#--------------------------------------------------------------------------- +print("""#--------------------------------------------------------------------------- # Delete entry #--------------------------------------------------------------------------- -""" +""") pr = PreReadControl(criticality=True,attrList=['*','+']) msg_id = l.delete_ext( @@ -106,5 +107,5 @@ serverctrls = [pr] ) _,_,_,resp_ctrls = l.result3(msg_id) -print "resp_ctrls[0].dn:",resp_ctrls[0].dn -print "resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry) +print("resp_ctrls[0].dn:",resp_ctrls[0].dn) +print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry)) diff --git a/Demo/pyasn1/sessiontrack.py b/Demo/pyasn1/sessiontrack.py index 1a1eab1..91909a3 100644 --- a/Demo/pyasn1/sessiontrack.py +++ b/Demo/pyasn1/sessiontrack.py @@ -8,6 +8,8 @@ https://tools.ietf.org/html/draft-wahl-ldap-session-03 """ +from __future__ import print_function + __version__ = '0.1' import sys,getpass,ldap,ldapurl @@ -16,8 +18,8 @@ try: ldap_url = ldapurl.LDAPUrl(sys.argv[1]) -except IndexError,ValueError: - print 'Usage: %s ' % (sys.argv[0]) +except (IndexError, ValueError): + print('Usage: %s ' % (sys.argv[0])) sys.exit(1) # Set debugging level @@ -32,14 +34,14 @@ ) if ldap_url.who and ldap_url.cred is None: - print 'Password for %s:' % (repr(ldap_url.who)) + print('Password for %s:' % (repr(ldap_url.who))) ldap_url.cred = getpass.getpass() try: ldap_conn.simple_bind_s(ldap_url.who or '',ldap_url.cred or '') -except ldap.INVALID_CREDENTIALS,e: - print 'Simple bind failed:',str(e) +except ldap.INVALID_CREDENTIALS as e: + print('Simple bind failed:',str(e)) sys.exit(1) st_ctrl = SessionTrackingControl( diff --git a/Demo/pyasn1/syncrepl.py b/Demo/pyasn1/syncrepl.py index 1454eef..e4c62e8 100644 --- a/Demo/pyasn1/syncrepl.py +++ b/Demo/pyasn1/syncrepl.py @@ -8,6 +8,7 @@ The bound user needs read access to the attributes entryDN and entryCSN. """ +from __future__ import print_function # Import modules from Python standard lib import logging @@ -156,8 +157,8 @@ def commenceShutdown(signum, stack): 'X-BINDPW=password" db.shelve' ).format(script_name=sys.argv[0]) sys.exit(1) -except ValueError,e: - print 'Error parsing command-line arguments:', str(e) +except ValueError as e: + print('Error parsing command-line arguments:',str(e)) sys.exit(1) while watcher_running: @@ -168,7 +169,7 @@ def commenceShutdown(signum, stack): # Now we login to the LDAP server try: ldap_connection.simple_bind_s(ldap_url.who, ldap_url.cred) - except ldap.INVALID_CREDENTIALS, err: + except ldap.INVALID_CREDENTIALS as err: logger.error('Login to LDAP server failed: %s', err) sys.exit(1) except ldap.SERVER_DOWN: @@ -192,7 +193,7 @@ def commenceShutdown(signum, stack): except KeyboardInterrupt: # User asked to exit commenceShutdown(None, None) - except Exception, err: + except Exception as err: # Handle any exception if watcher_running: logger.exception('Unhandled exception, going to retry: %s', err) diff --git a/Demo/rename.py b/Demo/rename.py index 1bb6fd7..91d7528 100644 --- a/Demo/rename.py +++ b/Demo/rename.py @@ -1,10 +1,11 @@ +from __future__ import print_function import ldap from getpass import getpass # Create LDAPObject instance l = ldap.initialize('ldap://localhost:1389',trace_level=1) -print 'Password:' +print('Password:') cred = getpass() try: @@ -15,7 +16,7 @@ # Try a bind to provoke failure if protocol version is not supported l.bind_s('cn=root,dc=stroeder,dc=com',cred,ldap.AUTH_SIMPLE) - print 'Using rename_s():' + print('Using rename_s():') l.rename_s( 'uid=fred,ou=Unstructured testing tree,dc=stroeder,dc=com', diff --git a/Demo/resiter.py b/Demo/resiter.py index ff9fe5a..d6796ba 100644 --- a/Demo/resiter.py +++ b/Demo/resiter.py @@ -4,6 +4,7 @@ See http://www.python-ldap.org for details. """ +from __future__ import print_function import ldap,ldap.resiter @@ -16,6 +17,6 @@ class LDAPObject(ldap.ldapobject.LDAPObject,ldap.resiter.ResultProcessor): result_iter = l.allresults(msgid) for result_type,result_list,result_msgid,result_serverctrls in result_iter: - print result_type,result_list,result_msgid,result_serverctrls + print(result_type,result_list,result_msgid,result_serverctrls) l.unbind_s() diff --git a/Demo/sasl_bind.py b/Demo/sasl_bind.py index 05af652..667221c 100644 --- a/Demo/sasl_bind.py +++ b/Demo/sasl_bind.py @@ -1,5 +1,6 @@ # For documentation, see comments in Module/LDAPObject.c and the # ldap.sasl module documentation. +from __future__ import print_function import ldap,ldap.sasl @@ -60,23 +61,23 @@ ), ]: sasl_auth = ldap.sasl.sasl(sasl_cb_value_dict,sasl_mech) - print 20*'*',sasl_auth.mech,20*'*' + print(20*'*',sasl_auth.mech,20*'*') # Open the LDAP connection l = ldap.initialize(ldap_uri,trace_level=0) # Set protocol version to LDAPv3 to enable SASL bind! l.protocol_version = 3 try: l.sasl_interactive_bind_s("", sasl_auth) - except ldap.LDAPError,e: - print 'Error using SASL mechanism',sasl_auth.mech,str(e) + except ldap.LDAPError as e: + print('Error using SASL mechanism',sasl_auth.mech,str(e)) else: - print 'Sucessfully bound using SASL mechanism:',sasl_auth.mech + print('Sucessfully bound using SASL mechanism:',sasl_auth.mech) try: - print 'Result of Who Am I? ext. op:',repr(l.whoami_s()) - except ldap.LDAPError,e: - print 'Error using SASL mechanism',sasl_auth.mech,str(e) + print('Result of Who Am I? ext. op:',repr(l.whoami_s())) + except ldap.LDAPError as e: + print('Error using SASL mechanism',sasl_auth.mech,str(e)) try: - print 'OPT_X_SASL_USERNAME',repr(l.get_option(ldap.OPT_X_SASL_USERNAME)) + print('OPT_X_SASL_USERNAME',repr(l.get_option(ldap.OPT_X_SASL_USERNAME))) except AttributeError: pass diff --git a/Demo/schema.py b/Demo/schema.py index c8103f3..4d350f0 100644 --- a/Demo/schema.py +++ b/Demo/schema.py @@ -1,3 +1,4 @@ +from __future__ import print_function import sys,ldap,ldap.schema schema_attrs = ldap.schema.SCHEMA_ATTRS @@ -9,55 +10,55 @@ subschemasubentry_dn,schema = ldap.schema.urlfetch(sys.argv[-1]) if subschemasubentry_dn is None: - print 'No sub schema sub entry found!' + print('No sub schema sub entry found!') sys.exit(1) if schema.non_unique_oids: - print '*** Schema errors ***' - print 'non-unique OIDs:\n','\r\n'.join(schema.non_unique_oids) + print('*** Schema errors ***') + print('non-unique OIDs:\n','\r\n'.join(schema.non_unique_oids)) -print '*** Schema from',repr(subschemasubentry_dn) +print('*** Schema from',repr(subschemasubentry_dn)) # Display schema for attr_type,schema_class in ldap.schema.SCHEMA_CLASS_MAPPING.items(): - print '*'*20,attr_type,'*'*20 + print('*'*20,attr_type,'*'*20) for element_id in schema.listall(schema_class): se_orig = schema.get_obj(schema_class,element_id) - print attr_type,str(se_orig) -print '*** Testing object class inetOrgPerson ***' + print(attr_type,str(se_orig)) +print('*** Testing object class inetOrgPerson ***') drink = schema.get_obj(ldap.schema.AttributeType,'favouriteDrink') if not drink is None: - print '*** drink ***' - print 'drink.names',repr(drink.names) - print 'drink.collective',repr(drink.collective) + print('*** drink ***') + print('drink.names',repr(drink.names)) + print('drink.collective',repr(drink.collective)) inetOrgPerson = schema.get_obj(ldap.schema.ObjectClass,'inetOrgPerson') if not inetOrgPerson is None: - print inetOrgPerson.must,inetOrgPerson.may + print(inetOrgPerson.must,inetOrgPerson.may) -print '*** person,organizationalPerson,inetOrgPerson ***' +print('*** person,organizationalPerson,inetOrgPerson ***') try: - print schema.attribute_types( + print(schema.attribute_types() ['person','organizationalPerson','inetOrgPerson'] ) - print schema.attribute_types( + print(schema.attribute_types() ['person','organizationalPerson','inetOrgPerson'], attr_type_filter = [ ('no_user_mod',[0]), ('usage',range(2)), ] ) -except KeyError,e: - print '***KeyError',str(e) +except KeyError as e: + print('***KeyError',str(e)) schema.ldap_entry() -print str(schema.get_obj(ldap.schema.MatchingRule,'2.5.13.0')) -print str(schema.get_obj(ldap.schema.MatchingRuleUse,'2.5.13.0')) +print(str(schema.get_obj(ldap.schema.MatchingRule,'2.5.13.0'))) +print(str(schema.get_obj(ldap.schema.MatchingRuleUse,'2.5.13.0'))) -print str(schema.get_obj(ldap.schema.AttributeType,'name')) -print str(schema.get_inheritedobj(ldap.schema.AttributeType,'cn',['syntax','equality','substr','ordering'])) +print(str(schema.get_obj(ldap.schema.AttributeType,'name'))) +print(str(schema.get_inheritedobj(ldap.schema.AttributeType,'cn',['syntax','equality','substr','ordering']))) must_attr,may_attr = schema.attribute_types(['person','organizationalPerson','inetOrgPerson'],raise_keyerror=0) diff --git a/Demo/schema_tree.py b/Demo/schema_tree.py index 3b3a091..79c8a83 100644 --- a/Demo/schema_tree.py +++ b/Demo/schema_tree.py @@ -4,6 +4,7 @@ Usage: schema_oc_tree.py [--html] [LDAP URL] """ +from __future__ import print_function import sys,getopt,ldap,ldap.schema @@ -14,11 +15,11 @@ def PrintSchemaTree(schema,se_class,se_tree,se_oid,level): """ASCII text output for console""" se_obj = schema.get_obj(se_class,se_oid) if se_obj!=None: - print '| '*(level-1)+'+---'*(level>0), \ + print('| '*(level-1)+'+---'*(level>0), \) ', '.join(se_obj.names), \ '(%s)' % se_obj.oid for sub_se_oid in se_tree[se_oid]: - print '| '*(level+1) + print('| '*(level+1)) PrintSchemaTree(schema,se_class,se_tree,sub_se_oid,level+1) @@ -26,17 +27,17 @@ def HTMLSchemaTree(schema,se_class,se_tree,se_oid,level): """HTML output for browser""" se_obj = schema.get_obj(se_class,se_oid) if se_obj!=None: - print """ + print("""
%s (%s)
%s - """ % (', '.join(se_obj.names),se_obj.oid,se_obj.desc) + """ % (', '.join(se_obj.names),se_obj.oid,se_obj.desc)) if se_tree[se_oid]: - print '
' + print('
') for sub_se_oid in se_tree[se_oid]: HTMLSchemaTree(schema,se_class,se_tree,sub_se_oid,level+1) - print '
' - print '
' + print('') + print('') ldap.set_option(ldap.OPT_DEBUG_LEVEL,0) @@ -46,13 +47,13 @@ def HTMLSchemaTree(schema,se_class,se_tree,se_oid,level): subschemasubentry_dn,schema = ldap.schema.urlfetch(sys.argv[-1],ldap.trace_level) if subschemasubentry_dn is None: - print 'No sub schema sub entry found!' + print('No sub schema sub entry found!') sys.exit(1) try: options,args=getopt.getopt(sys.argv[1:],'',['html']) -except getopt.error,e: - print 'Error: %s\nUsage: schema_oc_tree.py [--html] [LDAP URL]' +except getopt.error: + print('Error: %s\nUsage: schema_oc_tree.py [--html] [LDAP URL]') html_output = options and options[0][0]=='--html' @@ -60,41 +61,41 @@ def HTMLSchemaTree(schema,se_class,se_tree,se_oid,level): at_tree = schema.tree(ldap.schema.AttributeType) #for k,v in oc_tree.items(): -# print k,'->',v +# print(k,'->',v) #for k,v in at_tree.items(): -# print k,'->',v +# print(k,'->',v) if html_output: - print """ + print(""" Object class tree

Object class tree

-""" +""") HTMLSchemaTree(schema,ldap.schema.ObjectClass,oc_tree,'2.5.6.0',0) - print """
+ print("""

Attribute type tree

-""" +""") for a in schema.listall(ldap.schema.AttributeType): if at_tree[a]: HTMLSchemaTree(schema,ldap.schema.AttributeType,at_tree,a,0) print - print """
+ print(""" -""" +""") else: - print '*** Object class tree ***\n' + print('*** Object class tree ***\n') print PrintSchemaTree(schema,ldap.schema.ObjectClass,oc_tree,'2.5.6.0',0) - print '\n*** Attribute types tree ***\n' + print('\n*** Attribute types tree ***\n') PrintSchemaTree(schema,ldap.schema.AttributeType,at_tree,'_',0) diff --git a/Demo/simple.py b/Demo/simple.py index e4cf4d0..6004c9e 100644 --- a/Demo/simple.py +++ b/Demo/simple.py @@ -1,3 +1,4 @@ +from __future__ import print_function import sys,getpass import ldap @@ -14,7 +15,7 @@ try: dn = "ou=CSEE,o=UQ,c=AU" - print "Adding", repr(dn) + print("Adding", repr(dn)) l.add_s(dn, [ ("objectclass",["organizationalUnit"]), @@ -32,7 +33,7 @@ # dn = "cn=David Leonard,ou=CSEE,o=UQ,c=AU" -print "Updating", repr(dn) +print("Updating", repr(dn)) try: l.delete_s(dn) @@ -100,7 +101,7 @@ _ldap.SCOPE_SUBTREE, "objectclass=*", ) -print res +print(res) l.unbind() diff --git a/Demo/simplebrowse.py b/Demo/simplebrowse.py index 804d12f..aa88f67 100644 --- a/Demo/simplebrowse.py +++ b/Demo/simplebrowse.py @@ -3,6 +3,7 @@ # # simple LDAP server browsing example # +from __future__ import print_function import ldap from traceback import print_exc @@ -10,7 +11,7 @@ url = "ldap://ldap.openldap.org/" dn = "dc=openldap,dc=org" -print "Connecting to", url +print("Connecting to", url) l = ldap.initialize(url) l.bind_s("", "", ldap.AUTH_SIMPLE); @@ -30,18 +31,18 @@ try: if cmd == "?": - print "cd - change DN to " - print "cd - change DN to number of last 'ls'" - print "cd - - change to previous DN" - print "cd .. - change to one-level higher DN" - print "cd - change to root DN" - print "ls - list children of crrent DN" - print ". - show attributes of current DN" - print "/ - list descendents matching filter " - print "? - show this help" + print( "cd - change DN to ") + print( "cd - change DN to number of last 'ls'") + print( "cd - - change to previous DN") + print( "cd .. - change to one-level higher DN") + print( "cd - change to root DN") + print( "ls - list children of crrent DN") + print( ". - show attributes of current DN") + print( "/ - list descendents matching filter ") + print( "? - show this help") elif cmd == "ls": - print "Children of", `dn`, ":" + print("Children of", `dn`, ":") dnlist = [] # # List the children at one level down from the current dn @@ -58,7 +59,7 @@ shortname = name[:-len(dn)-2]+" +" else: shortname = name - print " %3d. %s" % (len(dnlist), shortname) + print(" %3d. %s" % (len(dnlist), shortname)) dnlist.append(name) elif cmd == "cd": @@ -79,7 +80,7 @@ godn = arg else: if dnlist is None: - print "do an ls first" + print("do an ls first") else: godn = dnlist[i] lastdn = dn @@ -93,10 +94,10 @@ # No attributes are listed, so the default is for # the client to receive all attributes on the DN. # - print "Attributes of", `dn`, ":" + print("Attributes of", `dn`, ":") for name,attrs in l.search_s(dn, ldap.SCOPE_BASE, "objectclass=*"): - print " %-24s" % name + print(" %-24s" % name) for k,vals in attrs.items(): for v in vals: if len(v) > 200: @@ -104,7 +105,7 @@ ("... (%d bytes)" % len(v)) else: v = `v` - print " %-12s: %s" % (k, v) + print(" %-12s: %s" % (k, v)) elif cmd.startswith("/"): # @@ -114,13 +115,13 @@ # that we're not interested in them. # expr = cmd[1:] - print "Descendents matching filter", `expr`, ":" + print("Descendents matching filter", `expr`, ":") for name,attrs in l.search_s(dn, ldap.SCOPE_SUBTREE, expr, []): - print " %24s", name + print(" %24s", name) else: - print "unknown command - try '?' for help" + print("unknown command - try '?' for help") except: print_exc() diff --git a/Doc/index.rst b/Doc/index.rst index 14992a6..1f9bf6a 100644 --- a/Doc/index.rst +++ b/Doc/index.rst @@ -34,6 +34,43 @@ Contents slapdtest.rst + +********************* +Bytes/text management +********************* + +The LDAP protocol states that some fields (distinguised names, relative distinguished names, +attribute names, queries) be encoded in UTF-8; some other (mostly attribute *values*) **MAY** +contain any type of data, and thus be treated as bytes. + +In Python 2, ``python-ldap`` used bytes for all fields, including those guaranteed to be text. +In order to support Python 3, this distinction is made explicit. This is done +through the ``bytes_mode`` flag to ``ldap.initialize()``. + +When porting from ``python-ldap`` 2.x, users are advised to update their code to set ``bytes_mode=False`` +on calls to these methods. +Under Python 2, ``python-pyldap`` aggressively checks the type of provided arguments, and will raise a ``TypeError`` +for any invalid parameter. +However, if the ``bytes_mode`` kwarg isn't provided, ``pyldap`` will only +raise warnings. + +The typical usage is as follows; note that only the result's *values* are of the bytes type: + +.. code-block:: pycon + + >>> import ldap + >>> con = ldap.initialize('ldap://localhost:389', bytes_mode=False) + >>> con.simple_bind_s('login', 'secret_password') + >>> results = con.search_s('ou=people,dc=example,dc=org', ldap.SCOPE_SUBTREE, "(cn=Raphaël)") + >>> results + [ + ("cn=Raphaël,ou=people,dc=example,dc=org", { + 'cn': [b'Rapha\xc3\xabl'], + 'sn': [b'Barrois'], + }), + ] + + ****************** Indices and tables ****************** diff --git a/Doc/ldap-resiter.rst b/Doc/ldap-resiter.rst index 0beabb8..0e72e92 100644 --- a/Doc/ldap-resiter.rst +++ b/Doc/ldap-resiter.rst @@ -46,4 +46,4 @@ processing them in a for-loop. for res_type,res_data,res_msgid,res_controls in l.allresults(msg_id): for dn,entry in res_data: # process dn and entry - print dn,entry['objectClass'] + print(dn,entry['objectClass']) diff --git a/Doc/ldap.rst b/Doc/ldap.rst index e983f64..5160f30 100644 --- a/Doc/ldap.rst +++ b/Doc/ldap.rst @@ -1169,6 +1169,6 @@ subtree search. [('cn=Fred Feuerstein,ou=Testing,dc=stroeder,dc=de', {'cn': ['Fred Feuerstein']})] >>> r = l.search_s('ou=Testing,dc=stroeder,dc=de',ldap.SCOPE_SUBTREE,'(objectClass=*)',['cn','mail']) >>> for dn,entry in r: ->>> print 'Processing',repr(dn) +>>> print('Processing',repr(dn)) >>> handle_ldap_entry(entry) diff --git a/Lib/ldap/__init__.py b/Lib/ldap/__init__.py index c8a1b12..dab97a6 100644 --- a/Lib/ldap/__init__.py +++ b/Lib/ldap/__init__.py @@ -86,9 +86,9 @@ def release(self): # Create module-wide lock for serializing all calls into underlying LDAP lib _ldap_module_lock = LDAPLock(desc='Module wide') -from functions import open,initialize,init,get_option,set_option,escape_str,strf_secs,strp_secs +from ldap.functions import open,initialize,init,get_option,set_option,escape_str,strf_secs,strp_secs -from ldapobject import NO_UNIQUE_ENTRY +from ldap.ldapobject import NO_UNIQUE_ENTRY from ldap.dn import explode_dn,explode_rdn,str2dn,dn2str del str2dn diff --git a/Lib/ldap/cidict.py b/Lib/ldap/cidict.py index 07832ef..fdfba4b 100644 --- a/Lib/ldap/cidict.py +++ b/Lib/ldap/cidict.py @@ -8,7 +8,7 @@ from ldap import __version__ -from UserDict import IterableUserDict +from ldap.compat import IterableUserDict class cidict(IterableUserDict): diff --git a/Lib/ldap/compat.py b/Lib/ldap/compat.py new file mode 100644 index 0000000..de0e110 --- /dev/null +++ b/Lib/ldap/compat.py @@ -0,0 +1,43 @@ +"""Compatibility wrappers for Py2/Py3.""" + +import sys + +if sys.version_info[0] < 3: + from UserDict import UserDict, IterableUserDict + from urllib import quote + from urllib import quote_plus + from urllib import unquote as urllib_unquote + from urllib import urlopen + from urlparse import urlparse + + def unquote(uri): + """Specialized unquote that uses UTF-8 for parsing.""" + uri = uri.encode('ascii') + unquoted = urllib_unquote(uri) + return unquoted.decode('utf-8') + + # Old-style of re-raising an exception is SyntaxError in Python 3, + # so hide behind exec() so the Python 3 parser doesn't see it + exec('''def reraise(exc_type, exc_value, exc_traceback): + """Re-raise an exception given information from sys.exc_info() + + Note that unlike six.reraise, this does not support replacing the + traceback. All arguments must come from a single sys.exc_info() call. + """ + raise exc_type, exc_value, exc_traceback + ''') + +else: + from collections import UserDict + IterableUserDict = UserDict + from urllib.parse import quote, quote_plus, unquote, urlparse + from urllib.request import urlopen + + def reraise(exc_type, exc_value, exc_traceback): + """Re-raise an exception given information from sys.exc_info() + + Note that unlike six.reraise, this does not support replacing the + traceback. All arguments must come from a single sys.exc_info() call. + """ + # In Python 3, all exception info is contained in one object. + raise exc_value diff --git a/Lib/ldap/controls/__init__.py b/Lib/ldap/controls/__init__.py index 56d611b..932fa53 100644 --- a/Lib/ldap/controls/__init__.py +++ b/Lib/ldap/controls/__init__.py @@ -148,9 +148,9 @@ def DecodeControlTuples(ldapControlTuples,knownLDAPControls=None): control.controlType,control.criticality = controlType,criticality try: control.decodeControlValue(encodedControlValue) - except PyAsn1Error,e: + except PyAsn1Error: if criticality: - raise e + raise else: result.append(control) return result diff --git a/Lib/ldap/controls/openldap.py b/Lib/ldap/controls/openldap.py index ee7b575..7108c63 100644 --- a/Lib/ldap/controls/openldap.py +++ b/Lib/ldap/controls/openldap.py @@ -67,7 +67,7 @@ def noop_search_st(self,base,scope=ldap.SCOPE_SUBTREE,filterstr='(objectClass=*) ldap.TIMELIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED, ldap.ADMINLIMIT_EXCEEDED - ),e: + ) as e: self.abandon(msg_id) raise e else: diff --git a/Lib/ldap/dn.py b/Lib/ldap/dn.py index 3298551..00c7b06 100644 --- a/Lib/ldap/dn.py +++ b/Lib/ldap/dn.py @@ -4,6 +4,7 @@ See https://www.python-ldap.org/ for details. """ +import sys from ldap.pkginfo import __version__ import _ldap @@ -46,6 +47,8 @@ def str2dn(dn,flags=0): """ if not dn: return [] + if sys.version_info[0] < 3 and isinstance(dn, unicode): + dn = dn.encode('utf-8') return ldap.functions._ldap_function_call(None,_ldap.str2dn,dn,flags) diff --git a/Lib/ldap/functions.py b/Lib/ldap/functions.py index 6ddab54..b887037 100644 --- a/Lib/ldap/functions.py +++ b/Lib/ldap/functions.py @@ -53,7 +53,7 @@ def _ldap_function_call(lock,func,*args,**kwargs): finally: if lock: lock.release() - except LDAPError,e: + except LDAPError as e: if __debug__ and ldap._trace_level>=2: ldap._trace_file.write('=> LDAPError: %s\n' % (str(e))) raise @@ -62,7 +62,7 @@ def _ldap_function_call(lock,func,*args,**kwargs): return result -def initialize(uri,trace_level=0,trace_file=sys.stdout,trace_stack_limit=None): +def initialize(uri,trace_level=0,trace_file=sys.stdout,trace_stack_limit=None, bytes_mode=None): """ Return LDAPObject instance by opening LDAP connection to LDAP host specified by LDAP URL @@ -76,11 +76,13 @@ def initialize(uri,trace_level=0,trace_file=sys.stdout,trace_stack_limit=None): trace_file File object where to write the trace output to. Default is to use stdout. + bytes_mode + Whether to enable "bytes_mode" for backwards compatibility under Py2. """ - return LDAPObject(uri,trace_level,trace_file,trace_stack_limit) + return LDAPObject(uri,trace_level,trace_file,trace_stack_limit,bytes_mode) -def open(host,port=389,trace_level=0,trace_file=sys.stdout,trace_stack_limit=None): +def open(host,port=389,trace_level=0,trace_file=sys.stdout,trace_stack_limit=None,bytes_mode=None): """ Return LDAPObject instance by opening LDAP connection to specified LDAP host @@ -95,10 +97,12 @@ def open(host,port=389,trace_level=0,trace_file=sys.stdout,trace_stack_limit=Non trace_file File object where to write the trace output to. Default is to use stdout. + bytes_mode + Whether to enable "bytes_mode" for backwards compatibility under Py2. """ import warnings warnings.warn('ldap.open() is deprecated! Use ldap.initialize() instead.', DeprecationWarning,2) - return initialize('ldap://%s:%d' % (host,port),trace_level,trace_file,trace_stack_limit) + return initialize('ldap://%s:%d' % (host,port),trace_level,trace_file,trace_stack_limit,bytes_mode) init = open diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index 805b6e3..01f0044 100644 --- a/Lib/ldap/ldapobject.py +++ b/Lib/ldap/ldapobject.py @@ -4,6 +4,8 @@ See https://www.python-ldap.org/ for details. """ +from __future__ import unicode_literals + from os import strerror from ldap.pkginfo import __version__, __author__, __license__ @@ -20,13 +22,20 @@ import traceback import sys,time,pprint,_ldap,ldap,ldap.sasl,ldap.functions +import warnings from ldap.schema import SCHEMA_ATTRS from ldap.controls import LDAPControl,DecodeControlTuples,RequestControlTuples from ldap.extop import ExtendedRequest,ExtendedResponse +from ldap.compat import reraise from ldap import LDAPError +PY2 = bool(sys.version_info[0] <= 2) +if PY2: + text_type = unicode +else: + text_type = str class NO_UNIQUE_ENTRY(ldap.NO_SUCH_OBJECT): """ @@ -54,7 +63,7 @@ class SimpleLDAPObject: def __init__( self,uri, - trace_level=0,trace_file=None,trace_stack_limit=5 + trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None ): self._trace_level = trace_level self._trace_file = trace_file or sys.stdout @@ -65,6 +74,186 @@ def __init__( self.timeout = -1 self.protocol_version = ldap.VERSION3 + # Bytes mode + # ---------- + + # By default, raise a TypeError when receiving invalid args + self.bytes_mode_hardfail = True + if bytes_mode is None and PY2: + warnings.warn( + "Under Python 2, python-ldap uses bytes by default. " + "This will be removed in Python 3 (no bytes for DN/RDN/field names). " + "Please call initialize(..., bytes_mode=False) explicitly.", + BytesWarning, + stacklevel=2, + ) + bytes_mode = True + # Disable hard failure when running in backwards compatibility mode. + self.bytes_mode_hardfail = False + elif bytes_mode and not PY2: + raise ValueError("bytes_mode is *not* supported under Python 3.") + # On by default on Py2, off on Py3. + self.bytes_mode = bytes_mode + + def _bytesify_input(self, value): + """Adapt a value following bytes_mode in Python 2. + + In Python 3, returns the original value unmodified. + + With bytes_mode ON, takes bytes or None and returns bytes or None. + With bytes_mode OFF, takes unicode or None and returns bytes or None. + + This function should be applied on all text inputs (distinguished names + and attribute names in modlists) to convert them to the bytes expected + by the C bindings. + """ + if not PY2: + return value + + if value is None: + return value + elif self.bytes_mode: + if isinstance(value, bytes): + return value + else: + if self.bytes_mode_hardfail: + raise TypeError("All provided fields *must* be bytes when bytes mode is on; got %r" % (value,)) + else: + warnings.warn( + "Received non-bytes value %r with default (disabled) bytes mode; please choose an explicit " + "option for bytes_mode on your LDAP connection" % (value,), + BytesWarning, + stacklevel=6, + ) + return value.encode('utf-8') + else: + if not isinstance(value, text_type): + raise TypeError("All provided fields *must* be text when bytes mode is off; got %r" % (value,)) + assert not isinstance(value, bytes) + return value.encode('utf-8') + + def _bytesify_inputs(self, *values): + """Adapt values following bytes_mode. + + Applies _bytesify_input on each arg. + + Usage: + >>> a, b, c = self._bytesify_inputs(a, b, c) + """ + if not PY2: + return values + return ( + self._bytesify_input(value) + for value in values + ) + + def _bytesify_modlist(self, modlist, with_opcode): + """Adapt a modlist according to bytes_mode. + + A modlist is a tuple of (op, attr, value), where: + - With bytes_mode ON, attr is checked to be bytes + - With bytes_mode OFF, attr is converted from unicode to bytes + - value is *always* bytes + """ + if not PY2: + return modlist + if with_opcode: + return tuple( + (op, self._bytesify_input(attr), val) + for op, attr, val in modlist + ) + else: + return tuple( + (self._bytesify_input(attr), val) + for attr, val in modlist + ) + + def _unbytesify_text_value(self, value): + """Adapt a 'known text, UTF-8 encoded' returned value following bytes_mode. + + With bytes_mode ON, takes bytes or None and returns bytes or None. + With bytes_mode OFF, takes bytes or None and returns unicode or None. + + This function should only be applied on field *values*; distinguished names + or field *names* are already natively handled in result4. + """ + if value is None: + return value + + # Preserve logic of assertions only under Python 2 + if PY2: + assert isinstance(value, bytes), "Expected bytes value, got text instead (%r)" % (value,) + + if self.bytes_mode: + return value + else: + return value.decode('utf-8') + + def _maybe_rebytesify_text(self, value): + """Re-encodes text to bytes if needed by bytes_mode. + + Takes unicode (and checks for it), and returns: + - bytes under bytes_mode + - unicode otherwise. + """ + if not PY2: + return value + + if value is None: + return value + + assert isinstance(value, text_type), "Should return text, got bytes instead (%r)" % (value,) + if not self.bytes_mode: + return value + else: + return value.encode('utf-8') + + def _bytesify_result_value(self, result_value): + """Applies bytes_mode to a result value. + + Such a value can either be: + - a dict mapping an attribute name to its list of values + (where attribute names are unicode and values bytes) + - a list of referals (which are unicode) + """ + if not PY2: + return result_value + if hasattr(result_value, 'items'): + # It's a attribute_name: [values] dict + return dict( + (self._maybe_rebytesify_text(key), value) + for (key, value) in result_value.items() + ) + elif isinstance(result_value, bytes): + return result_value + else: + # It's a list of referals + # Example value: + # [u'ldap://DomainDnsZones.xxxx.root.local/DC=DomainDnsZones,DC=xxxx,DC=root,DC=local'] + return [self._maybe_rebytesify_text(referal) for referal in result_value] + + def _bytesify_results(self, results, with_ctrls=False): + """Converts a "results" object according to bytes_mode. + + Takes: + - a list of (dn, {field: [values]}) if with_ctrls is False + - a list of (dn, {field: [values]}, ctrls) if with_ctrls is True + + And, if bytes_mode is on, converts dn and fields to bytes. + """ + if not PY2: + return results + if with_ctrls: + return [ + (self._maybe_rebytesify_text(dn), self._bytesify_result_value(fields), ctrls) + for (dn, fields, ctrls) in results + ] + else: + return [ + (self._maybe_rebytesify_text(dn), self._bytesify_result_value(fields)) + for (dn, fields) in results + ] + def _ldap_lock(self,desc=''): if ldap.LIBLDAP_R: return ldap.LDAPLock(desc='%s within %s' %(desc,repr(self))) @@ -96,7 +285,7 @@ def _ldap_call(self,func,*args,**kwargs): diagnostic_message_success = self._l.get_option(ldap.OPT_DIAGNOSTIC_MESSAGE) finally: self._ldap_object_lock.release() - except LDAPError, e: + except LDAPError as e: exc_type,exc_value,exc_traceback = sys.exc_info() try: if 'info' not in e.args[0] and 'errno' in e.args[0]: @@ -105,7 +294,10 @@ def _ldap_call(self,func,*args,**kwargs): pass if __debug__ and self._trace_level>=2: self._trace_file.write('=> LDAPError - %s: %s\n' % (e.__class__.__name__,str(e))) - raise exc_type,exc_value,exc_traceback + try: + reraise(exc_type, exc_value, exc_traceback) + finally: + exc_type = exc_value = exc_traceback = None else: if __debug__ and self._trace_level>=2: if not diagnostic_message_success is None: @@ -125,9 +317,9 @@ def __getattr__(self,name): elif name in self.__dict__: return self.__dict__[name] else: - raise AttributeError,'%s has no attribute %s' % ( + raise AttributeError('%s has no attribute %s' % ( self.__class__.__name__,repr(name) - ) + )) def fileno(self): """ @@ -181,6 +373,8 @@ def add_ext(self,dn,modlist,serverctrls=None,clientctrls=None): The parameter modlist is similar to the one passed to modify(), except that no operation integer need be included in the tuples. """ + dn = self._bytesify_input(dn) + modlist = self._bytesify_modlist(modlist, with_opcode=False) return self._ldap_call(self._l.add_ext,dn,modlist,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def add_ext_s(self,dn,modlist,serverctrls=None,clientctrls=None): @@ -205,6 +399,7 @@ def simple_bind(self,who='',cred='',serverctrls=None,clientctrls=None): """ simple_bind([who='' [,cred='']]) -> int """ + who, cred = self._bytesify_inputs(who, cred) return self._ldap_call(self._l.simple_bind,who,cred,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def simple_bind_s(self,who='',cred='',serverctrls=None,clientctrls=None): @@ -281,6 +476,7 @@ def compare_ext(self,dn,attr,value,serverctrls=None,clientctrls=None): A design bug in the library prevents value from containing nul characters. """ + dn, attr = self._bytesify_inputs(dn, attr) return self._ldap_call(self._l.compare_ext,dn,attr,value,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def compare_ext_s(self,dn,attr,value,serverctrls=None,clientctrls=None): @@ -311,6 +507,7 @@ def delete_ext(self,dn,serverctrls=None,clientctrls=None): form returns the message id of the initiated request, and the result can be obtained from a subsequent call to result(). """ + dn = self._bytesify_input(dn) return self._ldap_call(self._l.delete_ext,dn,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def delete_ext_s(self,dn,serverctrls=None,clientctrls=None): @@ -359,6 +556,8 @@ def modify_ext(self,dn,modlist,serverctrls=None,clientctrls=None): """ modify_ext(dn, modlist[,serverctrls=None[,clientctrls=None]]) -> int """ + dn = self._bytesify_input(dn) + modlist = self._bytesify_modlist(modlist, with_opcode=True) return self._ldap_call(self._l.modify_ext,dn,modlist,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def modify_ext_s(self,dn,modlist,serverctrls=None,clientctrls=None): @@ -412,6 +611,7 @@ def modrdn_s(self,dn,newrdn,delold=1): return self.rename_s(dn,newrdn,None,delold) def passwd(self,user,oldpw,newpw,serverctrls=None,clientctrls=None): + user, oldpw, newpw = self._bytesify_inputs(user, oldpw, newpw) return self._ldap_call(self._l.passwd,user,oldpw,newpw,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def passwd_s(self,user,oldpw,newpw,serverctrls=None,clientctrls=None): @@ -433,6 +633,7 @@ def rename(self,dn,newrdn,newsuperior=None,delold=1,serverctrls=None,clientctrls This actually corresponds to the rename* routines in the LDAP-EXT C API library. """ + dn, newrdn, newsuperior = self._bytesify_inputs(dn, newrdn, newsuperior) return self._ldap_call(self._l.rename,dn,newrdn,newsuperior,delold,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def rename_s(self,dn,newrdn,newsuperior=None,delold=1,serverctrls=None,clientctrls=None): @@ -521,6 +722,8 @@ def result4(self,msgid=ldap.RES_ANY,all=1,timeout=None,add_ctrls=0,add_intermedi if add_ctrls: resp_data = [ (t,r,DecodeControlTuples(c,resp_ctrl_classes)) for t,r,c in resp_data ] decoded_resp_ctrls = DecodeControlTuples(resp_ctrls,resp_ctrl_classes) + if resp_data is not None: + resp_data = self._bytesify_results(resp_data, with_ctrls=add_ctrls) return resp_type, resp_data, resp_msgid, decoded_resp_ctrls, resp_name, resp_value def search_ext(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrsonly=0,serverctrls=None,clientctrls=None,timeout=-1,sizelimit=0): @@ -568,6 +771,9 @@ def search_ext(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrson The amount of search results retrieved can be limited with the sizelimit parameter if non-zero. """ + base, filterstr = self._bytesify_inputs(base, filterstr) + if attrlist is not None: + attrlist = tuple(self._bytesify_inputs(*attrlist)) return self._ldap_call( self._l.search_ext, base,scope,filterstr, @@ -661,6 +867,8 @@ def search_subschemasubentry_s(self,dn=''): None as result indicates that the DN of the sub schema sub entry could not be determined. + + Returns: None or text/bytes depending on bytes_mode. """ try: r = self.search_s( @@ -682,7 +890,9 @@ def search_subschemasubentry_s(self,dn=''): # If dn was already root DSE we can return here return None else: - return search_subschemasubentry_dn + # With legacy bytes mode, return bytes; otherwise, since this is a DN, + # RFCs impose that the field value *can* be decoded to UTF-8. + return self._unbytesify_text_value(search_subschemasubentry_dn) except IndexError: return None @@ -784,7 +994,7 @@ class ReconnectLDAPObject(SimpleLDAPObject): def __init__( self,uri, - trace_level=0,trace_file=None,trace_stack_limit=5, + trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None, retry_max=1,retry_delay=60.0 ): """ @@ -799,12 +1009,12 @@ def __init__( self._uri = uri self._options = [] self._last_bind = None - SimpleLDAPObject.__init__(self,uri,trace_level,trace_file,trace_stack_limit) + SimpleLDAPObject.__init__(self,uri,trace_level,trace_file,trace_stack_limit,bytes_mode) self._reconnect_lock = ldap.LDAPLock(desc='reconnect lock within %s' % (repr(self))) self._retry_max = retry_max self._retry_delay = retry_delay self._start_tls = 0 - self._reconnects_done = 0L + self._reconnects_done = 0 def __getstate__(self): """return data representation for pickled object""" @@ -865,14 +1075,14 @@ def reconnect(self,uri,retry_max=1,retry_delay=60.0): SimpleLDAPObject.start_tls_s(self) # Repeat last simple or SASL bind self._apply_last_bind() - except (ldap.SERVER_DOWN,ldap.TIMEOUT),e: + except (ldap.SERVER_DOWN,ldap.TIMEOUT): if __debug__ and self._trace_level>=1: self._trace_file.write('*** %s reconnect to %s failed\n' % ( counter_text,uri )) reconnect_counter = reconnect_counter-1 if not reconnect_counter: - raise e + raise if __debug__ and self._trace_level>=1: self._trace_file.write('=> delay %s...\n' % (retry_delay)) time.sleep(retry_delay) @@ -882,7 +1092,7 @@ def reconnect(self,uri,retry_max=1,retry_delay=60.0): self._trace_file.write('*** %s reconnect to %s successful => repeat last operation\n' % ( counter_text,uri )) - self._reconnects_done = self._reconnects_done + 1L + self._reconnects_done = self._reconnects_done + 1 break finally: self._reconnect_lock.release() diff --git a/Lib/ldap/modlist.py b/Lib/ldap/modlist.py index 6f4bf1e..a853500 100644 --- a/Lib/ldap/modlist.py +++ b/Lib/ldap/modlist.py @@ -18,7 +18,7 @@ def addModlist(entry,ignore_attr_types=None): # This attribute type is ignored continue # Eliminate empty attr value strings in list - attrvaluelist = filter(lambda x:x!=None,entry[attrtype]) + attrvaluelist = [item for item in entry[attrtype] if item is not None] if attrvaluelist: modlist.append((attrtype,entry[attrtype])) return modlist # addModlist() @@ -58,10 +58,10 @@ def modifyModlist( # This attribute type is ignored continue # Filter away null-strings - new_value = filter(lambda x:x!=None,new_entry[attrtype]) + new_value = [item for item in new_entry[attrtype] if item is not None] if attrtype_lower in attrtype_lower_map: old_value = old_entry.get(attrtype_lower_map[attrtype_lower],[]) - old_value = filter(lambda x:x!=None,old_value) + old_value = [item for item in old_value if item is not None] del attrtype_lower_map[attrtype_lower] else: old_value = [] diff --git a/Lib/ldap/sasl.py b/Lib/ldap/sasl.py index 34d4cb0..fa6f4f5 100644 --- a/Lib/ldap/sasl.py +++ b/Lib/ldap/sasl.py @@ -46,6 +46,8 @@ def __init__(self, cb_value_dict, mech): the SASL mechaninsm to be uesd. """ self.cb_value_dict = cb_value_dict or {} + if not isinstance(mech, bytes): + mech = mech.encode('utf-8') self.mech = mech def callback(self, cb_id, challenge, prompt, defresult): @@ -64,6 +66,8 @@ def callback(self, cb_id, challenge, prompt, defresult): useful for writing generic sasl GUIs, which would need to know all the questions to ask, before the answers are returned to the sasl lib (in contrast to one question at a time). + + Unicode strings are always converted to bytes. """ # The following print command might be useful for debugging @@ -78,6 +82,8 @@ def callback(self, cb_id, challenge, prompt, defresult): repr(defresult), repr(self.cb_value_dict.get(cb_result)) )) + if not isinstance(cb_result, bytes): + cb_result = cb_result.encode('utf-8') return cb_result diff --git a/Lib/ldap/schema/models.py b/Lib/ldap/schema/models.py index 8471954..c0391b4 100644 --- a/Lib/ldap/schema/models.py +++ b/Lib/ldap/schema/models.py @@ -4,7 +4,10 @@ See https://www.python-ldap.org/ for details. """ -import UserDict,ldap.cidict +import sys + +import ldap.cidict +from ldap.compat import IterableUserDict from ldap.schema.tokenizer import split_tokens,extract_tokens @@ -29,6 +32,7 @@ class SchemaElement: schema_element_str String which contains the schema element description to be parsed. + (Bytestrings are decoded using UTF-8) Class attributes: @@ -43,6 +47,8 @@ class SchemaElement: } def __init__(self,schema_element_str=None): + if sys.version_info >= (3, 0) and isinstance(schema_element_str, bytes): + schema_element_str = schema_element_str.decode('utf-8') if schema_element_str: l = split_tokens(schema_element_str) self.set_id(l[1]) @@ -264,9 +270,9 @@ def _set_attrs(self,l,d): self.syntax_len = None for i in l: if i.startswith("{") and i.endswith("}"): - self.syntax_len=long(i[1:-1]) + self.syntax_len = int(i[1:-1]) else: - self.syntax_len = long(syntax_len[:-1]) + self.syntax_len = int(syntax_len[:-1]) self.single_value = d['SINGLE-VALUE']!=None self.collective = d['COLLECTIVE']!=None self.no_user_mod = d['NO-USER-MODIFICATION']!=None @@ -615,7 +621,7 @@ def __str__(self): return '( %s )' % ''.join(result) -class Entry(UserDict.IterableUserDict): +class Entry(IterableUserDict): """ Schema-aware implementation of an LDAP entry class. @@ -628,7 +634,7 @@ def __init__(self,schema,dn,entry): self._attrtype2keytuple = {} self._s = schema self.dn = dn - UserDict.UserDict.__init__(self,{}) + IterableUserDict.IterableUserDict.__init__(self,{}) self.update(entry) def _at2key(self,nameoroid): diff --git a/Lib/ldap/schema/subentry.py b/Lib/ldap/schema/subentry.py index 99a0dc4..2a42b4c 100644 --- a/Lib/ldap/schema/subentry.py +++ b/Lib/ldap/schema/subentry.py @@ -294,7 +294,8 @@ def get_structural_oc(self,oc_list): if oc_se and oc_se.kind==0: struct_ocs[oc_se.oid] = None result = None - struct_oc_list = struct_ocs.keys() + # Build a copy of the oid list, to be cleaned as we go. + struct_oc_list = list(struct_ocs) while struct_oc_list: oid = struct_oc_list.pop() for child_oid in oc_tree[oid]: @@ -417,20 +418,20 @@ def attribute_types( # Remove all mandantory attribute types from # optional attribute type list - for a in r_may.keys(): + for a in list(r_may.keys()): if a in r_must: del r_may[a] # Apply attr_type_filter to results if attr_type_filter: for l in [r_must,r_may]: - for a in l.keys(): + for a in list(l.keys()): for afk,afv in attr_type_filter: try: schema_attr_type = self.sed[AttributeType][a] except KeyError: if raise_keyerror: - raise KeyError,'No attribute type found in sub schema by name %s' % (a) + raise KeyError('No attribute type found in sub schema by name %s' % (a)) # If there's no schema element for this attribute type # but still KeyError is to be ignored we filter it away del l[a] @@ -455,7 +456,9 @@ def urlfetch(uri,trace_level=0): if uri.startswith('ldap:') or uri.startswith('ldaps:') or uri.startswith('ldapi:'): import ldapurl ldap_url = ldapurl.LDAPUrl(uri) - l=ldap.initialize(ldap_url.initializeUrl(),trace_level) + + # This is an internal function; don't enable bytes_mode. + l=ldap.initialize(ldap_url.initializeUrl(),trace_level,bytes_mode=False) l.protocol_version = ldap.VERSION3 l.simple_bind_s(ldap_url.who or '', ldap_url.cred or '') subschemasubentry_dn = l.search_subschemasubentry_s(ldap_url.dn) @@ -472,8 +475,9 @@ def urlfetch(uri,trace_level=0): l.unbind_s() del l else: - import urllib,ldif - ldif_file = urllib.urlopen(uri) + import ldif + from ldap.compat import urlopen + ldif_file = urlopen(uri) ldif_parser = ldif.LDIFRecordList(ldif_file,max_entries=1) ldif_parser.parse() subschemasubentry_dn,s_temp = ldif_parser.all_records[0] diff --git a/Lib/ldapurl.py b/Lib/ldapurl.py index bbd6929..366e177 100644 --- a/Lib/ldapurl.py +++ b/Lib/ldapurl.py @@ -16,9 +16,7 @@ 'LDAPUrlExtension','LDAPUrlExtensions','LDAPUrl' ] -import UserDict - -from urllib import quote,unquote +from ldap.compat import UserDict, quote, unquote LDAP_SCOPE_BASE = 0 LDAP_SCOPE_ONELEVEL = 1 @@ -132,14 +130,14 @@ def __ne__(self,other): return not self.__eq__(other) -class LDAPUrlExtensions(UserDict.UserDict): +class LDAPUrlExtensions(UserDict): """ Models a collection of LDAP URL extensions as dictionary type """ def __init__(self,default=None): - UserDict.UserDict.__init__(self) + UserDict.__init__(self) for k,v in (default or {}).items(): self[k]=v diff --git a/Lib/ldif.py b/Lib/ldif.py index 86b8ac9..82c4e3f 100644 --- a/Lib/ldif.py +++ b/Lib/ldif.py @@ -4,6 +4,8 @@ See https://www.python-ldap.org/ for details. """ +from __future__ import unicode_literals + __version__ = '2.5.2' __all__ = [ @@ -18,15 +20,11 @@ 'LDIFCopy', ] -import urlparse -import urllib import re from base64 import b64encode, b64decode +from io import StringIO -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO +from ldap.compat import urlparse, urlopen attrtype_pattern = r'[\w;.-]+(;[\w_-]+)*' attrvalue_pattern = r'(([^,]|\\,)+|".*?")' @@ -64,7 +62,7 @@ def is_dn(s): return rm!=None and rm.group(0)==s -SAFE_STRING_PATTERN = '(^(\000|\n|\r| |:|<)|[\000\n\r\200-\377]+|[ ]+$)' +SAFE_STRING_PATTERN = b'(^(\000|\n|\r| |:|<)|[\000\n\r\200-\377]+|[ ]+$)' safe_string_re = re.compile(SAFE_STRING_PATTERN) def list_dict(l): @@ -84,7 +82,7 @@ class LDIFWriter: def __init__(self,output_file,base64_attrs=None,cols=76,line_sep='\n'): """ output_file - file object for output + file object for output; should be opened in *text* mode base64_attrs list of attribute types to be base64-encoded in any case cols @@ -125,7 +123,7 @@ def _needs_base64_encoding(self,attr_type,attr_value): returns 1 if attr_value has to be base-64 encoded because of special chars or because attr_type is in self._base64_attrs """ - return self._base64_attrs.has_key(attr_type.lower()) or \ + return attr_type.lower() in self._base64_attrs or \ not safe_string_re.search(attr_value) is None def _unparseAttrTypeandValue(self,attr_type,attr_value): @@ -133,15 +131,17 @@ def _unparseAttrTypeandValue(self,attr_type,attr_value): Write a single attribute type/value pair attr_type - attribute type + attribute type (text) attr_value - attribute value + attribute value (bytes) """ if self._needs_base64_encoding(attr_type,attr_value): # Encode with base64 - self._unfold_lines(':: '.join([attr_type, b64encode(attr_value).replace('\n','')])) + encoded = b64encode(attr_value).decode('ascii') + encoded = encoded.replace('\n','') + self._unfold_lines(':: '.join([attr_type, encoded])) else: - self._unfold_lines(': '.join([attr_type,attr_value])) + self._unfold_lines(': '.join([attr_type, attr_value.decode('ascii')])) return # _unparseAttrTypeandValue() def _unparseEntryRecord(self,entry): @@ -165,13 +165,14 @@ def _unparseChangeRecord(self,modlist): changetype = 'modify' else: raise ValueError("modlist item of wrong length: %d" % (mod_len)) - self._unparseAttrTypeandValue('changetype',changetype) + self._unparseAttrTypeandValue('changetype',changetype.encode('ascii')) for mod in modlist: if mod_len==2: mod_type,mod_vals = mod elif mod_len==3: mod_op,mod_type,mod_vals = mod - self._unparseAttrTypeandValue(MOD_OP_STR[mod_op],mod_type) + self._unparseAttrTypeandValue(MOD_OP_STR[mod_op], + mod_type.encode('ascii')) else: raise ValueError("Subsequent modlist item of wrong length") if mod_vals: @@ -189,7 +190,8 @@ def unparse(self,dn,record): or a list with a modify list like for LDAPObject.modify(). """ # Start with line containing the distinguished name - self._unparseAttrTypeandValue('dn',dn) + dn = dn.encode('utf-8') + self._unparseAttrTypeandValue('dn', dn) # Dispatch to record type specific writers if isinstance(record,dict): self._unparseEntryRecord(record) @@ -264,6 +266,8 @@ def __init__( String used as line separator """ self._input_file = input_file + # Detect whether the file is open in text or bytes mode. + self._file_sends_bytes = isinstance(self._input_file.read(0), bytes) self._max_entries = max_entries self._process_url_schemes = list_dict([s.lower() for s in (process_url_schemes or [])]) self._ignored_attr_types = list_dict([a.lower() for a in (ignored_attr_types or [])]) @@ -291,6 +295,10 @@ def handle(self,dn,entry): def _readline(self): s = self._input_file.readline() + if self._file_sends_bytes: + # The RFC does not allow UTF-8 values; we support it as a + # non-official, backwards compatibility layer + s = s.decode('utf-8') self.line_counter = self.line_counter + 1 self.byte_counter = self.byte_counter + len(s) if not s: @@ -323,6 +331,8 @@ def _next_key_and_value(self): """ Parse a single attribute type and value pair from one or more lines of LDIF data + + Returns attr_type (text) and attr_value (bytes) """ # Reading new attribute line unfolded_line = self._unfold_lines() @@ -335,26 +345,34 @@ def _next_key_and_value(self): return '-',None try: colon_pos = unfolded_line.index(':') - except ValueError,e: + except ValueError as e: raise ValueError('no value-spec in %s' % (repr(unfolded_line))) attr_type = unfolded_line[0:colon_pos] # if needed attribute value is BASE64 decoded value_spec = unfolded_line[colon_pos:colon_pos+2] if value_spec==': ': attr_value = unfolded_line[colon_pos+2:].lstrip() + # All values should be valid ascii; we support UTF-8 as a + # non-official, backwards compatibility layer. + attr_value = attr_value.encode('utf-8') elif value_spec=='::': # attribute value needs base64-decoding - attr_value = self._b64decode(unfolded_line[colon_pos+2:]) + # base64 makes sens only for ascii + attr_value = unfolded_line[colon_pos+2:] + attr_value = attr_value.encode('ascii') + attr_value = self._b64decode(attr_value) elif value_spec==':<': # fetch attribute value from URL url = unfolded_line[colon_pos+2:].strip() attr_value = None if self._process_url_schemes: - u = urlparse.urlparse(url) - if self._process_url_schemes.has_key(u[0]): - attr_value = urllib.urlopen(url).read() + u = urlparse(url) + if u[0] in self._process_url_schemes: + attr_value = urlopen(url).read() else: - attr_value = unfolded_line[colon_pos+1:] + # All values should be valid ascii; we support UTF-8 as a + # non-official, backwards compatibility layer. + attr_value = unfolded_line[colon_pos+1:].encode('utf-8') return attr_type,attr_value def _consume_empty_lines(self): @@ -369,7 +387,7 @@ def _consume_empty_lines(self): # Consume empty lines try: k,v = next_key_and_value() - while k==v==None: + while k is None and v is None: k,v = next_key_and_value() except EOFError: k,v = None,None @@ -387,7 +405,7 @@ def parse_entry_records(self): k,v = self._consume_empty_lines() # Consume 'version' line if k=='version': - self.version = int(v) + self.version = int(v.decode('ascii')) k,v = self._consume_empty_lines() except EOFError: return @@ -398,6 +416,9 @@ def parse_entry_records(self): # Consume first line which must start with "dn: " if k!='dn': raise ValueError('Line %d: First line of record does not start with "dn:": %s' % (self.line_counter,repr(k))) + # Value of a 'dn' field *has* to be valid UTF-8 + # k is text, v is bytes. + v = v.decode('utf-8') if not is_dn(v): raise ValueError('Line %d: Not a valid string-representation for dn: %s.' % (self.line_counter,repr(v))) dn = v @@ -456,6 +477,9 @@ def parse_change_records(self): # Consume first line which must start with "dn: " if k!='dn': raise ValueError('Line %d: First line of record does not start with "dn:": %s' % (self.line_counter,repr(k))) + # Value of a 'dn' field *has* to be valid UTF-8 + # k is text, v is bytes. + v = v.decode('utf-8') if not is_dn(v): raise ValueError('Line %d: Not a valid string-representation for dn: %s.' % (self.line_counter,repr(v))) dn = v @@ -464,6 +488,8 @@ def parse_change_records(self): # Read "control:" lines controls = [] while k!=None and k=='control': + # v is still bytes, spec says it should be valid utf-8; decode it. + v = v.decode('utf-8') try: control_type,criticality,control_value = v.split(' ',2) except ValueError: @@ -476,6 +502,8 @@ def parse_change_records(self): changetype = None # Consume changetype line of record if k=='changetype': + # v is still bytes, spec says it should be valid utf-8; decode it. + v = v.decode('utf-8') if not v in valid_changetype_dict: raise ValueError('Invalid changetype: %s' % repr(v)) changetype = v @@ -495,6 +523,8 @@ def parse_change_records(self): except KeyError: raise ValueError('Line %d: Invalid mod-op string: %s' % (self.line_counter,repr(k))) # we now have the attribute name to be modified + # v is still bytes, spec says it should be valid utf-8; decode it. + v = v.decode('utf-8') modattr = v modvalues = [] try: diff --git a/Lib/slapdtest.py b/Lib/slapdtest.py index 2f98672..fcd36e1 100644 --- a/Lib/slapdtest.py +++ b/Lib/slapdtest.py @@ -5,6 +5,8 @@ See https://www.python-ldap.org/ for details. """ +from __future__ import unicode_literals + __version__ = '2.5.2' import os @@ -12,9 +14,11 @@ import time import subprocess import logging +import atexit from logging.handlers import SysLogHandler import unittest -import urllib + +from ldap.compat import quote_plus # a template string for generating simple slapd.conf file SLAPD_CONF_TEMPLATE = r""" @@ -104,7 +108,14 @@ class SlapdObject(object): TMPDIR = os.environ.get('TMP', os.getcwd()) SBINDIR = os.environ.get('SBIN', '/usr/sbin') BINDIR = os.environ.get('BIN', '/usr/bin') - SCHEMADIR = os.environ.get('SCHEMA', '/etc/openldap/schema') + if 'SCHEMA' in os.environ: + SCHEMADIR = os.environ['SCHEMA'] + elif os.path.isdir("/etc/openldap/schema"): + SCHEMADIR = "/etc/openldap/schema" + elif os.path.isdir("/etc/ldap/schema"): + SCHEMADIR = "/etc/ldap/schema" + else: + PATH_SCHEMA_CORE = None PATH_LDAPADD = os.path.join(BINDIR, 'ldapadd') PATH_LDAPMODIFY = os.path.join(BINDIR, 'ldapmodify') PATH_LDAPWHOAMI = os.path.join(BINDIR, 'ldapwhoami') @@ -125,7 +136,7 @@ def __init__(self): self._db_directory = os.path.join(self.testrundir, "openldap-data") self.ldap_uri = "ldap://%s:%d/" % (LOCALHOST, self._port) ldapi_path = os.path.join(self.testrundir, 'ldapi') - self.ldapi_uri = "ldapi://%s" % urllib.quote_plus(ldapi_path) + self.ldapi_uri = "ldapi://%s" % quote_plus(ldapi_path) def setup_rundir(self): """ @@ -143,6 +154,9 @@ def _cleanup_rundir(self): """ Recursively delete whole directory specified by `path' """ + # cleanup_rundir() is called in atexit handler. Until Python 3.4, + # the rest of the world is already destroyed. + import os, os.path if not os.path.exists(self.testrundir): return self._log.debug('clean-up %s', self.testrundir) @@ -213,9 +227,8 @@ def _ln_schema_files(self, file_names, source_dir): def _write_config(self): """Writes the slapd.conf file out, and returns the path to it.""" self._log.debug('Writing config to %s', self._slapd_conf) - config_file = file(self._slapd_conf, 'wb') - config_file.write(self.gen_config()) - config_file.close() + with open(self._slapd_conf, 'w') as config_file: + config_file.write(self.gen_config()) self._log.info('Wrote config to %s', self._slapd_conf) def _test_config(self): @@ -271,6 +284,7 @@ def start(self): if self._proc is None: # prepare directory structure + atexit.register(self.stop) self._cleanup_rundir() self.setup_rundir() self._write_config() @@ -353,13 +367,15 @@ def ldapadd(self, ldif, extra_args=None): """ Runs ldapadd on this slapd instance, passing it the ldif content """ - self._cli_popen(self.PATH_LDAPADD, extra_args=extra_args, stdin_data=ldif) + self._cli_popen(self.PATH_LDAPADD, extra_args=extra_args, + stdin_data=ldif.encode('utf-8')) def ldapmodify(self, ldif, extra_args=None): """ Runs ldapadd on this slapd instance, passing it the ldif content """ - self._cli_popen(self.PATH_LDAPMODIFY, extra_args=extra_args, stdin_data=ldif) + self._cli_popen(self.PATH_LDAPMODIFY, extra_args=extra_args, + stdin_data=ldif.encode('utf-8')) class SlapdTestCase(unittest.TestCase): @@ -371,11 +387,11 @@ class SlapdTestCase(unittest.TestCase): server = None ldap_object_class = None - def _open_ldap_conn(self, who=None, cred=None): + def _open_ldap_conn(self, who=None, cred=None, **kwargs): """ return a LDAPObject instance after simple bind """ - ldap_conn = self.ldap_object_class(self.server.ldap_uri) + ldap_conn = self.ldap_object_class(self.server.ldap_uri, **kwargs) ldap_conn.protocol_version = 3 #ldap_conn.set_option(ldap.OPT_REFERRALS, 0) ldap_conn.simple_bind_s(who or self.server.root_dn, cred or self.server.root_pw) diff --git a/Modules/LDAPObject.c b/Modules/LDAPObject.c index 95387a0..ce0ff52 100644 --- a/Modules/LDAPObject.c +++ b/Modules/LDAPObject.c @@ -14,8 +14,12 @@ #include "options.h" #ifdef HAVE_SASL +#ifdef __APPLE__ +#include +#else #include #endif +#endif static void free_attrs(char***, PyObject*); @@ -138,7 +142,7 @@ Tuple_to_LDAPMod( PyObject* tup, int no_op ) if (list == Py_None) { /* None indicates a NULL mod_bvals */ - } else if (PyString_Check(list)) { + } else if (PyBytes_Check(list)) { /* Single string is a singleton list */ lm->mod_bvalues = PyMem_NEW(struct berval *, 2); if (lm->mod_bvalues == NULL) @@ -147,8 +151,8 @@ Tuple_to_LDAPMod( PyObject* tup, int no_op ) if (lm->mod_bvalues[0] == NULL) goto nomem; lm->mod_bvalues[1] = NULL; - lm->mod_bvalues[0]->bv_len = PyString_Size(list); - lm->mod_bvalues[0]->bv_val = PyString_AsString(list); + lm->mod_bvalues[0]->bv_len = PyBytes_Size(list); + lm->mod_bvalues[0]->bv_val = PyBytes_AsString(list); } else if (PySequence_Check(list)) { nstrs = PySequence_Length(list); lm->mod_bvalues = PyMem_NEW(struct berval *, nstrs + 1); @@ -162,14 +166,14 @@ Tuple_to_LDAPMod( PyObject* tup, int no_op ) item = PySequence_GetItem(list, i); if (item == NULL) goto error; - if (!PyString_Check(item)) { + if (!PyBytes_Check(item)) { PyErr_SetObject( PyExc_TypeError, Py_BuildValue( "sO", - "expected a string in the list", item)); + "expected a byte string in the list", item)); Py_DECREF(item); goto error; } - lm->mod_bvalues[i]->bv_len = PyString_Size(item); - lm->mod_bvalues[i]->bv_val = PyString_AsString(item); + lm->mod_bvalues[i]->bv_len = PyBytes_Size(item); + lm->mod_bvalues[i]->bv_val = PyBytes_AsString(item); Py_DECREF(item); } if (nstrs == 0) @@ -264,7 +268,11 @@ attrs_from_List( PyObject *attrlist, char***attrsp, PyObject** seq) { if (attrlist == Py_None) { /* None means a NULL attrlist */ - } else if (PyString_Check(attrlist)) { +#if PY_MAJOR_VERSION == 2 + } else if (PyBytes_Check(attrlist)) { +#else + } else if (PyUnicode_Check(attrlist)) { +#endif /* caught by John Benninghoff */ PyErr_SetObject( PyExc_TypeError, Py_BuildValue("sO", "expected *list* of strings, not a string", attrlist )); @@ -285,12 +293,21 @@ attrs_from_List( PyObject *attrlist, char***attrsp, PyObject** seq) { item = PySequence_Fast_GET_ITEM(*seq, i); if (item == NULL) goto error; - if (!PyString_Check(item)) { +#if PY_MAJOR_VERSION == 2 + /* Encoded by Python to UTF-8 */ + if (!PyBytes_Check(item)) { +#else + if (!PyUnicode_Check(item)) { +#endif PyErr_SetObject(PyExc_TypeError, Py_BuildValue("sO", "expected string in list", item)); goto error; } - attrs[i] = PyString_AsString(item); +#if PY_MAJOR_VERSION == 2 + attrs[i] = PyBytes_AsString(item); +#else + attrs[i] = PyUnicode_AsUTF8(item); +#endif } attrs[len] = NULL; } @@ -547,7 +564,7 @@ static int interaction ( unsigned flags, if (result == NULL) /*searching for a better error code */ return LDAP_OPERATIONS_ERROR; - c_result = PyString_AsString(result); /*xxx Error checking?? */ + c_result = PyBytes_AsString(result); /*xxx Error checking?? */ /* according to the sasl docs, we should malloc() the returned string only for calls where interact->id == SASL_CB_PASS, so we @@ -643,7 +660,7 @@ l_ldap_sasl_bind_s( LDAPObject* self, PyObject* args ) if (ldaperror == LDAP_SASL_BIND_IN_PROGRESS) { if (servercred && servercred->bv_val && *servercred->bv_val) - return PyString_FromStringAndSize( servercred->bv_val, servercred->bv_len ); + return PyBytes_FromStringAndSize( servercred->bv_val, servercred->bv_len ); } else if (ldaperror != LDAP_SUCCESS) return LDAPerror( self->ldap, "l_ldap_sasl_bind_s" ); return PyInt_FromLong( ldaperror ); @@ -695,7 +712,7 @@ l_ldap_sasl_interactive_bind_s( LDAPObject* self, PyObject* args ) /* now we extract the sasl mechanism from the SASL Object */ mechanism = PyObject_GetAttrString(SASLObject, "mech"); if (mechanism == NULL) return NULL; - c_mechanism = PyString_AsString(mechanism); + c_mechanism = PyBytes_AsString(mechanism); Py_DECREF(mechanism); mechanism = NULL; @@ -1184,7 +1201,7 @@ l_ldap_whoami_s( LDAPObject* self, PyObject* args ) if ( ldaperror!=LDAP_SUCCESS ) return LDAPerror( self->ldap, "ldap_whoami_s" ); - result = LDAPberval_to_object(bvalue); + result = LDAPberval_to_unicode_object(bvalue); return result; } @@ -1380,23 +1397,6 @@ static PyMethodDef methods[] = { { NULL, NULL } }; -/* get attribute */ - -static PyObject* -getattr(LDAPObject* self, char* name) -{ - return Py_FindMethod(methods, (PyObject*)self, name); -} - -/* set attribute */ - -static int -setattr(LDAPObject* self, char* name, PyObject* value) -{ - PyErr_SetString(PyExc_AttributeError, name); - return -1; -} - /* type entry */ PyTypeObject LDAP_Type = { @@ -1412,8 +1412,8 @@ PyTypeObject LDAP_Type = { /* methods */ (destructor)dealloc, /*tp_dealloc*/ 0, /*tp_print*/ - (getattrfunc)getattr, /*tp_getattr*/ - (setattrfunc)setattr, /*tp_setattr*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ 0, /*tp_compare*/ 0, /*tp_repr*/ 0, /*tp_as_number*/ diff --git a/Modules/berval.c b/Modules/berval.c index b118669..73d7f9b 100644 --- a/Modules/berval.c +++ b/Modules/berval.c @@ -89,7 +89,29 @@ LDAPberval_to_object(const struct berval *bv) Py_INCREF(ret); } else { - ret = PyString_FromStringAndSize(bv->bv_val, bv->bv_len); + ret = PyBytes_FromStringAndSize(bv->bv_val, bv->bv_len); + } + + return ret; +} + +/* + * Same as LDAPberval_to_object, but returns a Unicode PyObject. + * Use when the value is known to be text (for instance a distinguishedName). + * + * Returns a new Python object on success, or NULL on failure. + */ +PyObject * +LDAPberval_to_unicode_object(const struct berval *bv) +{ + PyObject *ret = NULL; + + if (!bv) { + ret = Py_None; + Py_INCREF(ret); + } + else { + ret = PyUnicode_FromStringAndSize(bv->bv_val, bv->bv_len); } return ret; diff --git a/Modules/berval.h b/Modules/berval.h index 514e9f9..2489e45 100644 --- a/Modules/berval.h +++ b/Modules/berval.h @@ -10,5 +10,6 @@ int LDAPberval_from_object(PyObject *obj, struct berval *bv); int LDAPberval_from_object_check(PyObject *obj); void LDAPberval_release(struct berval *bv); PyObject *LDAPberval_to_object(const struct berval *bv); +PyObject *LDAPberval_to_unicode_object(const struct berval *bv); #endif /* __h_berval_ */ diff --git a/Modules/common.h b/Modules/common.h index 1ec232c..0eea1d9 100644 --- a/Modules/common.h +++ b/Modules/common.h @@ -27,5 +27,11 @@ void LDAPadd_methods( PyObject*d, PyMethodDef*methods ); #define PyNone_Check(o) ((o) == Py_None) +/* Py2/3 compatibility */ +#if PY_VERSION_HEX >= 0x03000000 +/* In Python 3, alias PyInt to PyLong */ +#define PyInt_FromLong PyLong_FromLong +#endif + #endif /* __h_common_ */ diff --git a/Modules/constants.c b/Modules/constants.c index 06c249a..7ed9e41 100644 --- a/Modules/constants.c +++ b/Modules/constants.c @@ -307,71 +307,71 @@ LDAPinit_constants( PyObject* d ) PyDict_SetItemString( d, "TLS_AVAIL", obj ); Py_DECREF(obj); - obj = PyString_FromString(LDAP_CONTROL_MANAGEDSAIT); + obj = PyUnicode_FromString(LDAP_CONTROL_MANAGEDSAIT); PyDict_SetItemString( d, "CONTROL_MANAGEDSAIT", obj ); Py_DECREF(obj); - obj = PyString_FromString(LDAP_CONTROL_PROXY_AUTHZ); + obj = PyUnicode_FromString(LDAP_CONTROL_PROXY_AUTHZ); PyDict_SetItemString( d, "CONTROL_PROXY_AUTHZ", obj ); Py_DECREF(obj); - obj = PyString_FromString(LDAP_CONTROL_SUBENTRIES); + obj = PyUnicode_FromString(LDAP_CONTROL_SUBENTRIES); PyDict_SetItemString( d, "CONTROL_SUBENTRIES", obj ); Py_DECREF(obj); - obj = PyString_FromString(LDAP_CONTROL_VALUESRETURNFILTER); + obj = PyUnicode_FromString(LDAP_CONTROL_VALUESRETURNFILTER); PyDict_SetItemString( d, "CONTROL_VALUESRETURNFILTER", obj ); Py_DECREF(obj); - obj = PyString_FromString(LDAP_CONTROL_ASSERT); + obj = PyUnicode_FromString(LDAP_CONTROL_ASSERT); PyDict_SetItemString( d, "CONTROL_ASSERT", obj ); Py_DECREF(obj); - obj = PyString_FromString(LDAP_CONTROL_PRE_READ); + obj = PyUnicode_FromString(LDAP_CONTROL_PRE_READ); PyDict_SetItemString( d, "CONTROL_PRE_READ", obj ); Py_DECREF(obj); - obj = PyString_FromString(LDAP_CONTROL_POST_READ); + obj = PyUnicode_FromString(LDAP_CONTROL_POST_READ); PyDict_SetItemString( d, "CONTROL_POST_READ", obj ); Py_DECREF(obj); - obj = PyString_FromString(LDAP_CONTROL_SORTREQUEST); + obj = PyUnicode_FromString(LDAP_CONTROL_SORTREQUEST); PyDict_SetItemString( d, "CONTROL_SORTREQUEST", obj ); Py_DECREF(obj); - obj = PyString_FromString(LDAP_CONTROL_SORTRESPONSE); + obj = PyUnicode_FromString(LDAP_CONTROL_SORTRESPONSE); PyDict_SetItemString( d, "CONTROL_SORTRESPONSE", obj ); Py_DECREF(obj); - obj = PyString_FromString(LDAP_CONTROL_PAGEDRESULTS); + obj = PyUnicode_FromString(LDAP_CONTROL_PAGEDRESULTS); PyDict_SetItemString( d, "CONTROL_PAGEDRESULTS", obj ); Py_DECREF(obj); - obj = PyString_FromString(LDAP_CONTROL_SYNC); + obj = PyUnicode_FromString(LDAP_CONTROL_SYNC); PyDict_SetItemString( d, "CONTROL_SYNC", obj ); Py_DECREF(obj); - obj = PyString_FromString(LDAP_CONTROL_SYNC_STATE); + obj = PyUnicode_FromString(LDAP_CONTROL_SYNC_STATE); PyDict_SetItemString( d, "CONTROL_SYNC_STATE", obj ); Py_DECREF(obj); - obj = PyString_FromString(LDAP_CONTROL_SYNC_DONE); + obj = PyUnicode_FromString(LDAP_CONTROL_SYNC_DONE); PyDict_SetItemString( d, "CONTROL_SYNC_DONE", obj ); Py_DECREF(obj); - obj = PyString_FromString(LDAP_SYNC_INFO); + obj = PyUnicode_FromString(LDAP_SYNC_INFO); PyDict_SetItemString( d, "SYNC_INFO", obj ); Py_DECREF(obj); - obj = PyString_FromString(LDAP_CONTROL_PASSWORDPOLICYREQUEST); + obj = PyUnicode_FromString(LDAP_CONTROL_PASSWORDPOLICYREQUEST); PyDict_SetItemString( d, "CONTROL_PASSWORDPOLICYREQUEST", obj ); Py_DECREF(obj); - obj = PyString_FromString(LDAP_CONTROL_PASSWORDPOLICYRESPONSE); + obj = PyUnicode_FromString(LDAP_CONTROL_PASSWORDPOLICYRESPONSE); PyDict_SetItemString( d, "CONTROL_PASSWORDPOLICYRESPONSE", obj ); Py_DECREF(obj); - obj = PyString_FromString(LDAP_CONTROL_RELAX); + obj = PyUnicode_FromString(LDAP_CONTROL_RELAX); PyDict_SetItemString( d, "CONTROL_RELAX", obj ); Py_DECREF(obj); diff --git a/Modules/errors.c b/Modules/errors.c index 3731965..e5cb0ee 100644 --- a/Modules/errors.c +++ b/Modules/errors.c @@ -80,7 +80,7 @@ LDAPerror( LDAP *l, char *msg ) if (info == NULL) return NULL; - str = PyString_FromString(ldap_err2string(errnum)); + str = PyUnicode_FromString(ldap_err2string(errnum)); if (str) PyDict_SetItemString( info, "desc", str ); Py_XDECREF(str); @@ -95,7 +95,7 @@ LDAPerror( LDAP *l, char *msg ) if (ldap_get_option(l, LDAP_OPT_MATCHED_DN, &matched) >= 0 && matched != NULL) { if (*matched != '\0') { - str = PyString_FromString(matched); + str = PyUnicode_FromString(matched); if (str) PyDict_SetItemString( info, "matched", str ); Py_XDECREF(str); @@ -104,13 +104,13 @@ LDAPerror( LDAP *l, char *msg ) } if (errnum == LDAP_REFERRAL) { - str = PyString_FromString(msg); + str = PyUnicode_FromString(msg); if (str) PyDict_SetItemString( info, "info", str ); Py_XDECREF(str); } else if (ldap_get_option(l, LDAP_OPT_ERROR_STRING, &error) >= 0) { if (error != NULL && *error != '\0') { - str = PyString_FromString(error); + str = PyUnicode_FromString(error); if (str) PyDict_SetItemString( info, "info", str ); Py_XDECREF(str); diff --git a/Modules/functions.c b/Modules/functions.c index b2dbf83..ffb6765 100644 --- a/Modules/functions.c +++ b/Modules/functions.c @@ -76,9 +76,9 @@ l_ldap_str2dn( PyObject* unused, PyObject *args ) LDAPAVA *ava = rdn[j]; PyObject *tuple; - tuple = Py_BuildValue("(O&O&i)", - LDAPberval_to_object, &ava->la_attr, - LDAPberval_to_object, &ava->la_value, + tuple = Py_BuildValue("(O&O&i)", + LDAPberval_to_unicode_object, &ava->la_attr, + LDAPberval_to_unicode_object, &ava->la_value, ava->la_flags & ~(LDAP_AVA_FREE_ATTR|LDAP_AVA_FREE_VALUE)); if (!tuple) { Py_DECREF(rdnlist); diff --git a/Modules/ldapcontrol.c b/Modules/ldapcontrol.c index 0bf86a1..f3dc92a 100644 --- a/Modules/ldapcontrol.c +++ b/Modules/ldapcontrol.c @@ -102,13 +102,13 @@ Tuple_to_LDAPControl( PyObject* tup ) berbytes.bv_len = 0; berbytes.bv_val = NULL; } - else if (PyString_Check(bytes)) { - berbytes.bv_len = PyString_Size(bytes); - berbytes.bv_val = PyString_AsString(bytes); + else if (PyBytes_Check(bytes)) { + berbytes.bv_len = PyBytes_Size(bytes); + berbytes.bv_val = PyBytes_AsString(bytes); } else { PyErr_SetObject(PyExc_TypeError, Py_BuildValue("sO", - "expected a string", bytes)); + "expected bytes", bytes)); LDAPControl_DEL(lc); return NULL; } diff --git a/Modules/ldapmodule.c b/Modules/ldapmodule.c index a788ae1..46c9c8a 100644 --- a/Modules/ldapmodule.c +++ b/Modules/ldapmodule.c @@ -8,6 +8,12 @@ #include "LDAPObject.h" +#if PY_MAJOR_VERSION >= 3 +PyMODINIT_FUNC PyInit__ldap(void); +#else +PyMODINIT_FUNC init_ldap(void); +#endif + #define _STR(x) #x #define STR(x) _STR(x) @@ -15,38 +21,24 @@ static char version_str[] = STR(LDAPMODULE_VERSION); static char author_str[] = STR(LDAPMODULE_AUTHOR); static char license_str[] = STR(LDAPMODULE_LICENSE); -void -LDAPinit_pkginfo( PyObject* d ) +static void +init_pkginfo( PyObject* m ) { - PyObject *version; - PyObject *author; - PyObject *license; - - version = PyString_FromString(version_str); - author = PyString_FromString(author_str); - license = PyString_FromString(license_str); - - PyDict_SetItemString( d, "__version__", version ); - PyDict_SetItemString(d, "__author__", author); - PyDict_SetItemString(d, "__license__", license); - - Py_DECREF(version); - Py_DECREF(author); - Py_DECREF(license); + PyModule_AddStringConstant(m, "__version__", version_str); + PyModule_AddStringConstant(m, "__author__", author_str); + PyModule_AddStringConstant(m, "__license__", license_str); } -DL_EXPORT(void) init_ldap(void); - /* dummy module methods */ - static PyMethodDef methods[] = { { NULL, NULL } }; /* module initialisation */ -DL_EXPORT(void) -init_ldap() + +/* Common initialization code */ +PyObject* init_ldap_module() { PyObject *m, *d; @@ -55,12 +47,26 @@ init_ldap() #endif /* Create the module and add the functions */ +#if PY_MAJOR_VERSION >= 3 + static struct PyModuleDef ldap_moduledef = { + PyModuleDef_HEAD_INIT, + "_ldap", /* m_name */ + "", /* m_doc */ + -1, /* m_size */ + methods, /* m_methods */ + }; + m = PyModule_Create(&ldap_moduledef); +#else m = Py_InitModule("_ldap", methods); +#endif + + PyType_Ready(&LDAP_Type); /* Add some symbolic constants to the module */ d = PyModule_GetDict(m); - LDAPinit_pkginfo(d); + init_pkginfo(m); + LDAPinit_constants(d); LDAPinit_errors(d); LDAPinit_functions(d); @@ -69,4 +75,17 @@ init_ldap() /* Check for errors */ if (PyErr_Occurred()) Py_FatalError("can't initialize module _ldap"); + + return m; +} + + +#if PY_MAJOR_VERSION < 3 +PyMODINIT_FUNC init_ldap() { + init_ldap_module(); } +#else +PyMODINIT_FUNC PyInit__ldap() { + return init_ldap_module(); +} +#endif diff --git a/Modules/message.c b/Modules/message.c index 33127b6..1a289db 100644 --- a/Modules/message.c +++ b/Modules/message.c @@ -49,6 +49,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, int add_intermedi BerElement *ber = NULL; PyObject* entrytuple; PyObject* attrdict; + PyObject* pydn; dn = ldap_get_dn( ld, entry ); if (dn == NULL) { @@ -91,22 +92,26 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, int add_intermedi attr = ldap_next_attribute( ld, entry, ber ) ) { PyObject* valuelist; + PyObject* pyattr; + pyattr = PyUnicode_FromString(attr); + struct berval ** bvals = ldap_get_values_len( ld, entry, attr ); /* Find which list to append to */ - if ( PyMapping_HasKeyString( attrdict, attr ) ) { - valuelist = PyMapping_GetItemString( attrdict, attr ); + if ( PyDict_Contains( attrdict, pyattr ) ) { + valuelist = PyDict_GetItem( attrdict, pyattr ); } else { valuelist = PyList_New(0); - if (valuelist != NULL && PyMapping_SetItemString(attrdict, - attr, valuelist) == -1) { + if (valuelist != NULL && PyDict_SetItem(attrdict, + pyattr, valuelist) == -1) { Py_DECREF(valuelist); valuelist = NULL; /* catch error later */ } } if (valuelist == NULL) { + Py_DECREF(pyattr); Py_DECREF(attrdict); Py_DECREF(result); if (ber != NULL) @@ -125,6 +130,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, int add_intermedi valuestr = LDAPberval_to_object(bvals[i]); if (PyList_Append( valuelist, valuestr ) == -1) { + Py_DECREF(pyattr); Py_DECREF(attrdict); Py_DECREF(result); Py_DECREF(valuestr); @@ -141,15 +147,25 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, int add_intermedi } ldap_value_free_len(bvals); } + Py_DECREF(pyattr); Py_DECREF( valuelist ); ldap_memfree(attr); } + pydn = PyUnicode_FromString(dn); + if (pydn == NULL) { + Py_DECREF(result); + ldap_msgfree( m ); + ldap_memfree(dn); + return NULL; + } + if (add_ctrls) { - entrytuple = Py_BuildValue("(sOO)", dn, attrdict, pyctrls); + entrytuple = Py_BuildValue("(OOO)", pydn, attrdict, pyctrls); } else { - entrytuple = Py_BuildValue("(sO)", dn, attrdict); + entrytuple = Py_BuildValue("(OO)", pydn, attrdict); } + Py_DECREF(pydn); ldap_memfree(dn); Py_DECREF(attrdict); Py_XDECREF(pyctrls); @@ -191,7 +207,8 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, int add_intermedi if (refs) { Py_ssize_t i; for (i=0; refs[i] != NULL; i++) { - PyObject *refstr = PyString_FromString(refs[i]); + /* A referal is a distinguishedName => unicode */ + PyObject *refstr = PyUnicode_FromString(refs[i]); PyList_Append(reflist, refstr); Py_DECREF(refstr); } @@ -218,6 +235,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, int add_intermedi PyObject* valtuple; PyObject *valuestr; char *retoid = 0; + PyObject *pyoid; struct berval *retdata = 0; if (ldap_parse_intermediate( ld, entry, &retoid, &retdata, &serverctrls, 0 ) != LDAP_SUCCESS) { @@ -240,10 +258,17 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, int add_intermedi valuestr = LDAPberval_to_object(retdata); ber_bvfree( retdata ); - valtuple = Py_BuildValue("(sOO)", retoid, + pyoid = PyUnicode_FromString(retoid); + ldap_memfree( retoid ); + if (pyoid == NULL) { + Py_DECREF(result); + ldap_msgfree( m ); + return NULL; + } + valtuple = Py_BuildValue("(OOO)", pyoid, valuestr ? valuestr : Py_None, pyctrls); - ldap_memfree( retoid ); + Py_DECREF(pyoid); Py_DECREF(valuestr); Py_XDECREF(pyctrls); PyList_Append(result, valtuple); diff --git a/Modules/options.c b/Modules/options.c index a437ca4..7cf996b 100644 --- a/Modules/options.c +++ b/Modules/options.c @@ -204,7 +204,7 @@ LDAP_get_option(LDAPObject *self, int option) extensions = PyTuple_New(num_extensions); for (i = 0; i < num_extensions; i++) PyTuple_SET_ITEM(extensions, i, - PyString_FromString(apiinfo.ldapai_extensions[i])); + PyUnicode_FromString(apiinfo.ldapai_extensions[i])); /* return api info as a dictionary */ v = Py_BuildValue("{s:i, s:i, s:i, s:s, s:i, s:O}", @@ -321,7 +321,7 @@ LDAP_get_option(LDAPObject *self, int option) Py_INCREF(Py_None); return Py_None; } - v = PyString_FromString(strval); + v = PyUnicode_FromString(strval); ldap_memfree(strval); return v; diff --git a/PKG-INFO b/PKG-INFO deleted file mode 100644 index e69de29..0000000 diff --git a/README b/README index d8e88bb..431559d 100644 --- a/README +++ b/README @@ -3,6 +3,7 @@ python-ldap: LDAP client API for Python --------------------------------------- What is python-ldap? +==================== python-ldap provides an object-oriented API to access LDAP directory servers from Python programs. Mainly it wraps the @@ -22,79 +23,106 @@ For module documentation, see: https://www.python-ldap.org/ Quick usage example: +==================== + +.. code-block:: python + import ldap l = ldap.initialize("ldap://my_ldap_server.my_domain") l.simple_bind_s("","") l.search_s("o=My Organisation, c=AU", ldap.SCOPE_SUBTREE, "objectclass=*") -See directory Demo/ of source distribution package for more +See directory ``Demo/`` of source distribution package for more example code. Author(s) contact and documentation: +==================================== https://www.python-ldap.org/ - If you are looking for help, please try the mailing list archives - first, then send a question to the mailing list. - Be warned that questions will be ignored if they can be - trivially answered by referring to the documentation. +If you are looking for help, please try the mailing list archives +first, then send a question to the mailing list. +Be warned that questions will be ignored if they can be +trivially answered by referring to the documentation. - If you are interested in helping, please contact the mailing list. - If you want new features or upgrades, please check the mailing list - archives and then enquire about any progress. +If you are interested in helping, please contact the mailing list. +If you want new features or upgrades, please check the mailing list +archives and then enquire about any progress. Acknowledgements: - - Thanks to Konstantin Chuguev - and Steffen Ries for working - on support for OpenLDAP 2.0.x features. - - Thanks to Michael Stroeder for the - modules ldif, ldapurl, ldap/schema/*.py, ldap/*.py and ldap/controls/*.py. - - Thanks to Hans Aschauer - for the C wrapper schema and SASL support. - - Thanks to Mauro Cicognini for the - WIN32/MSVC6 bits, and the pre-built WIN32 ldap.pyd. - - Thanks to Waldemar Osuch for contributing - the new-style docs based on reStructuredText. - - Thanks to Torsten Kurbad for the - easy_install support. - - Thanks to James Andrewartha for - significant contribution to Doc/*.tex. - - Thanks to Rich Megginson for extending - support for LDAPv3 controls and adding support for LDAPv3 extended - operations. - - Thanks to Peter Gietz, DAASI for funding some control modules. - - Thanks to Chris Mikkelson for various fixes and ldap.syncrepl. - - These very kind people have supplied patches or suggested changes: - - Federico Di Gregorio - John Benninghoff - Donn Cave - Jason Gunthorpe - gurney_j - Eric S. Johansson - David Margrave - Uche Ogbuji - Neale Pickett - Blake Weston - Wido Depping - Deepak Giridharagopal - Ingo Steuwer - Andreas Hasenack - Matej Vela - - Thanks to all the guys on the python-ldap mailing list for - their contributions and input into this package. +================= + +Thanks to Konstantin Chuguev +and Steffen Ries for working +on support for OpenLDAP 2.0.x features. + +Thanks to Michael Stroeder for the +modules ``ldif``, ``ldapurl``, ``ldap/schema/*.py``, ``ldap/*.py`` and ``ldap/controls/*.py``. + +Thanks to Hans Aschauer +for the C wrapper schema and SASL support. + +Thanks to Mauro Cicognini for the +WIN32/MSVC6 bits, and the pre-built WIN32 ``ldap.pyd``. + +Thanks to Waldemar Osuch for contributing +the new-style docs based on reStructuredText. + +Thanks to Torsten Kurbad for the +easy_install support. + +Thanks to James Andrewartha for +significant contribution to ``Doc/*.tex``. + +Thanks to Rich Megginson for extending +support for LDAPv3 controls and adding support for LDAPv3 extended +operations. + +Thanks to Peter Gietz, DAASI for funding some control modules. + +Thanks to Chris Mikkelson for various fixes and ldap.syncrepl. + +These very kind people have supplied patches or suggested changes: + +* Federico Di Gregorio +* John Benninghoff +* Donn Cave +* Jason Gunthorpe +* gurney_j +* Eric S. Johansson +* David Margrave +* Uche Ogbuji +* Neale Pickett +* Blake Weston +* Wido Depping +* Deepak Giridharagopal +* Ingo Steuwer +* Andreas Hasenack +* Matej Vela + +These people contributed to Python 3 porting (at https://github.com/pyldap/): +* A. Karl Kornel +* Alex Willmer +* Aymeric Augustin +* Bradley Baetz +* Christian Heimes +* Dirk Mueller +* Jon Dufresne +* Martin Basti +* Miro Hroncok +* Paul Aurich +* Petr Viktorin +* Pieterjan De Potter +* Raphaël Barrois +* Robert Kuska +* Stanislav Láznička +* Tobias Bräutigam +* Tom van Dijk +* Wentao Han +* William Brown + +Thanks to all the guys on the python-ldap mailing list for +their contributions and input into this package. Thanks! We may have missed someone: please mail us if we have omitted your name. diff --git a/README.rst b/README.rst new file mode 120000 index 0000000..100b938 --- /dev/null +++ b/README.rst @@ -0,0 +1 @@ +README \ No newline at end of file diff --git a/Tests/__init__.py b/Tests/__init__.py index b7b5378..46b3861 100644 --- a/Tests/__init__.py +++ b/Tests/__init__.py @@ -5,14 +5,18 @@ See https://www.python-ldap.org/ for details. """ -import t_cext -import t_cidict -import t_ldap_dn -import t_ldap_filter -import t_ldap_functions -import t_ldap_modlist -import t_ldap_schema_tokenizer -import t_ldapurl -import t_ldif -import t_ldapobject -import t_ldap_schema_subentry +from __future__ import absolute_import + +from . import t_bind +from . import t_cext +from . import t_cidict +from . import t_ldap_dn +from . import t_ldap_filter +from . import t_ldap_functions +from . import t_ldap_modlist +from . import t_ldap_schema_tokenizer +from . import t_ldapurl +from . import t_ldif +from . import t_ldapobject +from . import t_edit +from . import t_ldap_schema_subentry diff --git a/Tests/t_bind.py b/Tests/t_bind.py new file mode 100644 index 0000000..373688b --- /dev/null +++ b/Tests/t_bind.py @@ -0,0 +1,82 @@ +from __future__ import unicode_literals + +import sys + +if sys.version_info[0] <= 2: + PY2 = True + text_type = unicode +else: + PY2 = False + text_type = str + +import ldap, unittest +from slapdtest import SlapdObject +from ldap.ldapobject import LDAPObject + +server = None + + +class TestBinds(unittest.TestCase): + + def setUp(self): + global server + if server is None: + server = SlapdObject() + server.start() + + self.server = server + self.unicode_val = "abc\U0001f498def" + self.unicode_val_bytes = self.unicode_val.encode('utf-8') + + self.dn_unicode = "CN=" + self.unicode_val + self.dn_bytes = self.dn_unicode.encode('utf-8') + + def _get_ldapobject(self, bytes_mode=None): + l = LDAPObject(self.server.ldap_uri, bytes_mode=bytes_mode) + l.protocol_version = 3 + l.set_option(ldap.OPT_REFERRALS,0) + return l + + def test_simple_bind(self): + l = self._get_ldapobject(False) + with self.assertRaises(ldap.INVALID_CREDENTIALS): + l.simple_bind_s(self.dn_unicode, self.unicode_val) + + def test_unicode_bind(self): + l = self._get_ldapobject(False) + l.simple_bind(self.dn_unicode, "ascii") + + l = self._get_ldapobject(False) + l.simple_bind("CN=user", self.unicode_val) + + @unittest.skipUnless(PY2, "no bytes_mode under Py3") + def test_unicode_bind_bytesmode(self): + l = self._get_ldapobject(True) + with self.assertRaises(TypeError): + l.simple_bind_s(self.dn_unicode, self.unicode_val_bytes) + + with self.assertRaises(TypeError): + l.simple_bind_s(self.dn_bytes, self.unicode_val) + + # Works when encoded to UTF-8 + with self.assertRaises(ldap.INVALID_CREDENTIALS): + l.simple_bind_s(self.dn_bytes, self.unicode_val_bytes) + + def test_unicode_bind_no_bytesmode(self): + l = self._get_ldapobject(False) + with self.assertRaises(TypeError): + l.simple_bind_s(self.dn_bytes, self.unicode_val) + + # Works fine in Python 3 because 'cred' (the password) is read in + # using the "s#" format which, unlike "s", accepts either a str + # (unicode) *or* bytes. + # + # with self.assertRaises(TypeError): + # l.simple_bind_s(self.dn_unicode, self.unicode_val_bytes) + + with self.assertRaises(ldap.INVALID_CREDENTIALS): + l.simple_bind_s(self.dn_unicode, self.unicode_val) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/t_cext.py b/Tests/t_cext.py index cac661e..d4740d0 100644 --- a/Tests/t_cext.py +++ b/Tests/t_cext.py @@ -5,6 +5,8 @@ See https://www.python-ldap.org/ for details. """ +from __future__ import unicode_literals + import os import unittest from slapdtest import SlapdTestCase @@ -64,10 +66,10 @@ def _open_conn(self, bind=True): return l def assertNotNone(self, expr, msg=None): - self.failIf(expr is None, msg or repr(expr)) + self.assertFalse(expr is None, msg or repr(expr)) def assertNone(self, expr, msg=None): - self.failIf(expr is not None, msg or repr(expr)) + self.assertFalse(expr is not None, msg or repr(expr)) # Test for the existence of a whole bunch of constants # that the C module is supposed to export @@ -195,7 +197,7 @@ def test_anon_rootdse_search(self): '', _ldap.SCOPE_BASE, '(objectClass=*)', - ['objectClass', 'namingContexts'], + [str('objectClass'), str('namingContexts')], ) self.assertEqual(type(m), type(0)) result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ALL, self.timeout) @@ -205,9 +207,9 @@ def test_anon_rootdse_search(self): self.assertEqual(ctrls, []) root_dse = pmsg[0][1] self.assertTrue('objectClass' in root_dse) - self.assertTrue('OpenLDAProotDSE' in root_dse['objectClass']) + self.assertTrue(b'OpenLDAProotDSE' in root_dse['objectClass']) self.assertTrue('namingContexts' in root_dse) - self.assertEqual(root_dse['namingContexts'], [self.server.suffix]) + self.assertEqual(root_dse['namingContexts'], [self.server.suffix.encode('ascii')]) def test_unbind(self): l = self._open_conn() @@ -235,8 +237,8 @@ def test_search_ext_individual(self): self.assertEqual(len(pmsg[0]), 2) self.assertEqual(pmsg[0][0], self.server.suffix) self.assertEqual(pmsg[0][0], self.server.suffix) - self.assertTrue('dcObject' in pmsg[0][1]['objectClass']) - self.assertTrue('organization' in pmsg[0][1]['objectClass']) + self.assertTrue(b'dcObject' in pmsg[0][1]['objectClass']) + self.assertTrue(b'organization' in pmsg[0][1]['objectClass']) self.assertEqual(msgid, m) self.assertEqual(ctrls, []) @@ -253,7 +255,7 @@ def test_abandon(self): self.assertNone(ret) try: r = l.result4(m, _ldap.MSG_ALL, 0.3) # (timeout /could/ be longer) - except _ldap.TIMEOUT, e: + except _ldap.TIMEOUT as e: pass else: self.fail("expected TIMEOUT, got %r" % r) @@ -278,9 +280,9 @@ def test_add(self): m = l.add_ext( "cn=Foo," + self.server.suffix, [ - ('objectClass', 'organizationalRole'), - ('cn', 'Foo'), - ('description', 'testing'), + ('objectClass', b'organizationalRole'), + ('cn', b'Foo'), + ('description', b'testing'), ] ) self.assertEqual(type(m), type(0)) @@ -302,9 +304,9 @@ def test_add(self): ( 'cn=Foo,'+self.server.suffix, { - 'objectClass': ['organizationalRole'], - 'cn': ['Foo'], - 'description': ['testing'], + 'objectClass': [b'organizationalRole'], + 'cn': [b'Foo'], + 'description': [b'testing'], } ) ) @@ -319,10 +321,10 @@ def test_compare(self): m = l.add_ext( dn, [ - ('objectClass', 'person'), - ('sn', 'CompareTest'), - ('cn', 'CompareTest'), - ('userPassword', 'the_password'), + ('objectClass', b'person'), + ('sn', b'CompareTest'), + ('cn', b'CompareTest'), + ('userPassword', b'the_password'), ], ) self.assertEqual(type(m), type(0)) @@ -373,8 +375,8 @@ def test_delete(self): m = l.add_ext( dn, [ - ('objectClass', 'organizationalRole'), - ('cn', 'Deleteme'), + ('objectClass', b'organizationalRole'), + ('cn', b'Deleteme'), ] ) self.assertEqual(type(m), type(0)) @@ -395,7 +397,7 @@ def test_modify_no_such_object(self): m = l.modify_ext( "cn=DoesNotExist,"+self.server.suffix, [ - (_ldap.MOD_ADD, 'description', ['blah']), + (_ldap.MOD_ADD, 'description', [b'blah']), ] ) try: @@ -413,7 +415,7 @@ def test_modify_no_such_object_empty_attrs(self): m = l.modify_ext( "cn=DoesNotExist,"+self.server.suffix, [ - (_ldap.MOD_ADD, 'description', ['dummy']), + (_ldap.MOD_ADD, 'description', [b'dummy']), ] ) self.assertTrue(isinstance(m, int)) @@ -434,10 +436,10 @@ def test_modify(self): m = l.add_ext( dn, [ - ('objectClass', 'person'), - ('cn', 'AddToMe'), - ('sn', 'Modify'), - ('description', 'a description'), + ('objectClass', b'person'), + ('cn', b'AddToMe'), + ('sn', b'Modify'), + ('description', b'a description'), ] ) self.assertEqual(type(m), type(0)) @@ -447,7 +449,7 @@ def test_modify(self): m = l.modify_ext( dn, [ - (_ldap.MOD_ADD, 'description', ['b desc', 'c desc']), + (_ldap.MOD_ADD, 'description', [b'b desc', b'c desc']), ] ) result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ALL, self.timeout) @@ -466,7 +468,7 @@ def test_modify(self): self.assertEqual(pmsg[0][0], dn) d = list(pmsg[0][1]['description']) d.sort() - self.assertEqual(d, ['a description', 'b desc', 'c desc']) + self.assertEqual(d, [b'a description', b'b desc', b'c desc']) def test_rename(self): l = self._open_conn() @@ -474,8 +476,8 @@ def test_rename(self): m = l.add_ext( dn, [ - ('objectClass', 'organizationalRole'), - ('cn', 'RenameMe'), + ('objectClass', b'organizationalRole'), + ('cn', b'RenameMe'), ] ) self.assertEqual(type(m), type(0)) @@ -507,15 +509,15 @@ def test_rename(self): self.assertEqual(ctrls, []) self.assertEqual(len(pmsg), 1) self.assertEqual(pmsg[0][0], dn2) - self.assertEqual(pmsg[0][1]['cn'], ['IAmRenamed']) + self.assertEqual(pmsg[0][1]['cn'], [b'IAmRenamed']) # create the container containerDn = "ou=RenameContainer,"+self.server.suffix m = l.add_ext( containerDn, [ - ('objectClass', 'organizationalUnit'), - ('ou', 'RenameContainer'), + ('objectClass', b'organizationalUnit'), + ('ou', b'RenameContainer'), ] ) result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ALL, self.timeout) @@ -551,7 +553,7 @@ def test_rename(self): self.assertEqual(ctrls, []) self.assertEqual(len(pmsg), 1) self.assertEqual(pmsg[0][0], dn3) - self.assertEqual(pmsg[0][1]['cn'], ['IAmRenamedAgain']) + self.assertEqual(pmsg[0][1]['cn'], [b'IAmRenamedAgain']) def test_whoami(self): @@ -576,6 +578,13 @@ def test_whoami_anonymous(self): r = l.whoami_s() self.assertEqual("", r) + def test_whoami_after_unbind(self): + # https://github.com/pyldap/pyldap/issues/29 + l = self._open_conn(bind=True) + l.unbind_ext() + with self.assertRaises(_ldap.LDAPError): + l.whoami_s() + def test_passwd(self): l = self._open_conn() # first, create a user to change password on @@ -583,10 +592,10 @@ def test_passwd(self): m = l.add_ext( dn, [ - ('objectClass', 'person'), - ('sn', 'PasswordTest'), - ('cn', 'PasswordTest'), - ('userPassword', 'initial'), + ('objectClass', b'person'), + ('sn', b'PasswordTest'), + ('cn', b'PasswordTest'), + ('userPassword', b'initial'), ] ) self.assertEqual(type(m), type(0)) @@ -683,7 +692,7 @@ def test_errno107(self): try: m = l.simple_bind("", "") r = l.result4(m, _ldap.MSG_ALL, self.timeout) - except _ldap.SERVER_DOWN, ldap_err: + except _ldap.SERVER_DOWN as ldap_err: errno = ldap_err.args[0]['errno'] if errno != 107: self.fail("expected errno=107, got %d" % errno) diff --git a/Tests/t_cidict.py b/Tests/t_cidict.py index c8812f2..00d0726 100644 --- a/Tests/t_cidict.py +++ b/Tests/t_cidict.py @@ -32,16 +32,17 @@ def test_cidict(self): cix["xYZ"] = 987 self.assertEqual(cix["XyZ"], 987) self.assertEqual(cix.get("xyz", None), 987) - cix_keys = cix.keys() - cix_keys.sort() + cix_keys = sorted(cix.keys()) self.assertEqual(cix_keys, ['AbCDeF','xYZ']) - cix_items = cix.items() - cix_items.sort() + cix_items = sorted(cix.items()) self.assertEqual(cix_items, [('AbCDeF',123), ('xYZ',987)]) del cix["abcdEF"] - self.assertEqual("abcdef" in cix, False) + self.assertEqual("abcdef" in cix._keys, False) self.assertEqual("AbCDef" in cix._keys, False) + self.assertEqual("abcdef" in cix, False) + self.assertEqual("AbCDef" in cix, False) self.assertEqual(cix.has_key("abcdef"), False) + self.assertEqual(cix.has_key("AbCDef"), False) if __name__ == '__main__': diff --git a/Tests/t_edit.py b/Tests/t_edit.py new file mode 100644 index 0000000..9aee43e --- /dev/null +++ b/Tests/t_edit.py @@ -0,0 +1,89 @@ +from __future__ import unicode_literals + +import sys + +if sys.version_info[0] <= 2: + PY2 = True + text_type = unicode +else: + PY2 = False + text_type = str + +import ldap, unittest +from slapdtest import SlapdObject + +from ldap.ldapobject import LDAPObject + +server = None + + +class EditionTests(unittest.TestCase): + + def setUp(self): + global server + if server is None: + server = SlapdObject() + server.start() + base = server.suffix + suffix_dc = base.split(',')[0][3:] + + # insert some Foo* objects via ldapadd + server.ldapadd("\n".join([ + 'dn: '+server.suffix, + 'objectClass: dcObject', + 'objectClass: organization', + 'dc: '+suffix_dc, + 'o: '+suffix_dc, + '', + 'dn: '+server.root_dn, + 'objectClass: applicationProcess', + 'cn: '+server.root_cn, + '', + "dn: cn=Foo1,"+base, + "objectClass: organizationalRole", + "cn: Foo1", + "", + "dn: cn=Foo2,"+base, + "objectClass: organizationalRole", + "cn: Foo2", + "", + "dn: cn=Foo3,"+base, + "objectClass: organizationalRole", + "cn: Foo3", + "", + "dn: ou=Container,"+base, + "objectClass: organizationalUnit", + "ou: Container", + "", + "dn: cn=Foo4,ou=Container,"+base, + "objectClass: organizationalRole", + "cn: Foo4", + "", + ])+"\n") + + l = LDAPObject(server.ldap_uri, bytes_mode=False) + l.protocol_version = 3 + l.set_option(ldap.OPT_REFERRALS,0) + l.simple_bind_s(server.root_dn, + server.root_pw) + self.ldap = l + self.server = server + + def test_add_object(self): + base = self.server.suffix + dn = "cn=Added,ou=Container," + base + self.ldap.add_ext_s(dn, [ + ("objectClass", [b'organizationalRole']), + ("cn", [b'Added']), + ]) + + # Lookup the object + result = self.ldap.search_s(base, ldap.SCOPE_SUBTREE, '(cn=Added)', ['*']) + self.assertEqual(result, [ + ("cn=Added,ou=Container," + base, + {'cn': [b'Added'], 'objectClass': [b'organizationalRole']}), + ]) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/t_ldap_dn.py b/Tests/t_ldap_dn.py index 97274f3..459c1dc 100644 --- a/Tests/t_ldap_dn.py +++ b/Tests/t_ldap_dn.py @@ -5,6 +5,8 @@ See https://www.python-ldap.org/ for details. """ +from __future__ import unicode_literals + # from Python's standard lib import unittest @@ -27,9 +29,10 @@ def test_is_dn(self): self.assertEqual(ldap.dn.is_dn(',cn=foobar,ou=ae-dir'), False) self.assertEqual(ldap.dn.is_dn('cn=foobar,ou=ae-dir,'), False) self.assertEqual(ldap.dn.is_dn('uid=xkcd,cn=foobar,ou=ae-dir'), True) + self.assertEqual(ldap.dn.is_dn('cn=äöüÄÖÜß,o=äöüÄÖÜß'), True) self.assertEqual( ldap.dn.is_dn( - 'cn=\xc3\xa4\xc3\xb6\xc3\xbc\xc3\x84\xc3\x96\xc3\x9c.o=\xc3\xa4\xc3\xb6\xc3\xbc\xc3\x84\xc3\x96\xc3\x9c\xc3\x9f' + r'cn=\c3\a4\c3\b6\c3\bc\c3\84\c3\96\c3\9c\c3\9f,o=\c3\a4\c3\b6\c3\bc\c3\84\c3\96\c3\9c\c3\9f' ), True ) @@ -97,9 +100,9 @@ def test_str2dn(self): ] ) self.assertEqual( - ldap.dn.str2dn('cn=\xc3\xa4\xc3\xb6\xc3\xbc\xc3\x84\xc3\x96\xc3\x9c\xc3\x9f,dc=example,dc=com', flags=0), + ldap.dn.str2dn('cn=äöüÄÖÜß,dc=example,dc=com', flags=0), [ - [('cn', '\xc3\xa4\xc3\xb6\xc3\xbc\xc3\x84\xc3\x96\xc3\x9c\xc3\x9f', 4)], + [('cn', 'äöüÄÖÜß', 4)], [('dc', 'example', 1)], [('dc', 'com', 1)] ] @@ -107,7 +110,7 @@ def test_str2dn(self): self.assertEqual( ldap.dn.str2dn('cn=\\c3\\a4\\c3\\b6\\c3\\bc\\c3\\84\\c3\\96\\c3\\9c\\c3\\9f,dc=example,dc=com', flags=0), [ - [('cn', '\xc3\xa4\xc3\xb6\xc3\xbc\xc3\x84\xc3\x96\xc3\x9c\xc3\x9f', 4)], + [('cn', 'äöüÄÖÜß', 4)], [('dc', 'example', 1)], [('dc', 'com', 1)] ] @@ -156,19 +159,11 @@ def test_dn2str(self): ) self.assertEqual( ldap.dn.dn2str([ - [('cn', '\xc3\xa4\xc3\xb6\xc3\xbc\xc3\x84\xc3\x96\xc3\x9c\xc3\x9f', 4)], - [('dc', 'example', 1)], - [('dc', 'com', 1)] - ]), - 'cn=\xc3\xa4\xc3\xb6\xc3\xbc\xc3\x84\xc3\x96\xc3\x9c\xc3\x9f,dc=example,dc=com' - ) - self.assertEqual( - ldap.dn.dn2str([ - [('cn', '\xc3\xa4\xc3\xb6\xc3\xbc\xc3\x84\xc3\x96\xc3\x9c\xc3\x9f', 4)], + [('cn', 'äöüÄÖÜß', 4)], [('dc', 'example', 1)], [('dc', 'com', 1)] ]), - 'cn=\xc3\xa4\xc3\xb6\xc3\xbc\xc3\x84\xc3\x96\xc3\x9c\xc3\x9f,dc=example,dc=com' + 'cn=äöüÄÖÜß,dc=example,dc=com' ) def test_explode_dn(self): @@ -197,12 +192,12 @@ def test_explode_dn(self): ['uid=test\\, 42', 'ou=Testing', 'dc=example', 'dc=com'] ) self.assertEqual( - ldap.dn.explode_dn('cn=\xc3\xa4\xc3\xb6\xc3\xbc\xc3\x84\xc3\x96\xc3\x9c\xc3\x9f,dc=example,dc=com', flags=0), - ['cn=\xc3\xa4\xc3\xb6\xc3\xbc\xc3\x84\xc3\x96\xc3\x9c\xc3\x9f', 'dc=example', 'dc=com'] + ldap.dn.explode_dn('cn=äöüÄÖÜß,dc=example,dc=com', flags=0), + ['cn=äöüÄÖÜß', 'dc=example', 'dc=com'] ) self.assertEqual( ldap.dn.explode_dn('cn=\\c3\\a4\\c3\\b6\\c3\\bc\\c3\\84\\c3\\96\\c3\\9c\\c3\\9f,dc=example,dc=com', flags=0), - ['cn=\xc3\xa4\xc3\xb6\xc3\xbc\xc3\x84\xc3\x96\xc3\x9c\xc3\x9f', 'dc=example', 'dc=com'] + ['cn=äöüÄÖÜß', 'dc=example', 'dc=com'] ) def test_explode_rdn(self): @@ -239,12 +234,12 @@ def test_explode_rdn(self): ['uid=test\\+ 42'] ) self.assertEqual( - ldap.dn.explode_rdn('cn=\xc3\xa4\xc3\xb6\xc3\xbc\xc3\x84\xc3\x96\xc3\x9c\xc3\x9f', flags=0), - ['cn=\xc3\xa4\xc3\xb6\xc3\xbc\xc3\x84\xc3\x96\xc3\x9c\xc3\x9f'] + ldap.dn.explode_rdn('cn=äöüÄÖÜß', flags=0), + ['cn=äöüÄÖÜß'] ) self.assertEqual( ldap.dn.explode_rdn('cn=\\c3\\a4\\c3\\b6\\c3\\bc\\c3\\84\\c3\\96\\c3\\9c\\c3\\9f', flags=0), - ['cn=\xc3\xa4\xc3\xb6\xc3\xbc\xc3\x84\xc3\x96\xc3\x9c\xc3\x9f'] + ['cn=äöüÄÖÜß'] ) diff --git a/Tests/t_ldap_schema_subentry.py b/Tests/t_ldap_schema_subentry.py index de6421a..83ed006 100644 --- a/Tests/t_ldap_schema_subentry.py +++ b/Tests/t_ldap_schema_subentry.py @@ -10,6 +10,7 @@ import ldif import ldap.schema +from ldap.schema.models import ObjectClass TEST_SUBSCHEMA_FILES = ( 'Tests/ldif/subschema-ipa.demo1.freeipa.org.ldif', @@ -30,6 +31,15 @@ def test_subschema_file(self): _, subschema_subentry = ldif_parser.all_records[0] sub_schema = ldap.schema.SubSchema(subschema_subentry) + # Smoke-check for listall() and attribute_types() + for objclass in sub_schema.listall(ObjectClass): + must, may = sub_schema.attribute_types([objclass]) + + for oid, attributetype in must.items(): + self.assertEqual(attributetype.oid, oid) + for oid, attributetype in may.items(): + self.assertEqual(attributetype.oid, oid) + if __name__ == '__main__': unittest.main() diff --git a/Tests/t_ldap_syncrepl.py b/Tests/t_ldap_syncrepl.py index 8f39c67..831c063 100644 --- a/Tests/t_ldap_syncrepl.py +++ b/Tests/t_ldap_syncrepl.py @@ -81,34 +81,34 @@ # NOTE: For the dict, it needs to be kept up-to-date as we make changes! LDAP_ENTRIES = { 'ou=Container,dc=slapd-test,dc=python-ldap,dc=org': { - 'objectClass': ['organizationalUnit'], - 'ou': ['Container'] + 'objectClass': [b'organizationalUnit'], + 'ou': [b'Container'] }, 'cn=Foo2,dc=slapd-test,dc=python-ldap,dc=org': { - 'objectClass': ['organizationalRole'], - 'cn': ['Foo2'] + 'objectClass': [b'organizationalRole'], + 'cn': [b'Foo2'] }, 'cn=Foo4,ou=Container,dc=slapd-test,dc=python-ldap,dc=org': { - 'objectClass': ['organizationalRole'], - 'cn': ['Foo4'] + 'objectClass': [b'organizationalRole'], + 'cn': [b'Foo4'] }, 'cn=Manager,dc=slapd-test,dc=python-ldap,dc=org': { - 'objectClass': ['applicationProcess', 'simpleSecurityObject'], - 'userPassword': ['password'], - 'cn': ['Manager'] + 'objectClass': [b'applicationProcess', b'simpleSecurityObject'], + 'userPassword': [b'password'], + 'cn': [b'Manager'] }, 'cn=Foo3,dc=slapd-test,dc=python-ldap,dc=org': { - 'objectClass': ['organizationalRole'], - 'cn': ['Foo3'] + 'objectClass': [b'organizationalRole'], + 'cn': [b'Foo3'] }, 'cn=Foo1,dc=slapd-test,dc=python-ldap,dc=org': { - 'objectClass': ['organizationalRole'], - 'cn': ['Foo1'] + 'objectClass': [b'organizationalRole'], + 'cn': [b'Foo1'] }, 'dc=slapd-test,dc=python-ldap,dc=org': { - 'objectClass': ['dcObject', 'organization'], - 'dc': ['slapd-test'], - 'o': ['slapd-test'] + 'objectClass': [b'dcObject', b'organization'], + 'dc': [b'slapd-test'], + 'o': [b'slapd-test'] } } diff --git a/Tests/t_ldapobject.py b/Tests/t_ldapobject.py index 196a9f3..d55d018 100644 --- a/Tests/t_ldapobject.py +++ b/Tests/t_ldapobject.py @@ -5,6 +5,17 @@ See https://www.python-ldap.org/ for details. """ +from __future__ import unicode_literals + +import sys + +if sys.version_info[0] <= 2: + PY2 = True + text_type = unicode +else: + PY2 = False + text_type = str + import os import unittest import pickle @@ -83,7 +94,101 @@ def setUp(self): self._ldap_conn except AttributeError: # open local LDAP connection - self._ldap_conn = self._open_ldap_conn() + self._ldap_conn = self._open_ldap_conn(bytes_mode=False) + + def test_reject_bytes_base(self): + base = self.server.suffix + l = self._ldap_conn + + with self.assertRaises(TypeError): + l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, '(cn=Foo*)', ['*']) + with self.assertRaises(TypeError): + l.search_s(base, ldap.SCOPE_SUBTREE, b'(cn=Foo*)', ['*']) + with self.assertRaises(TypeError): + l.search_s(base, ldap.SCOPE_SUBTREE, '(cn=Foo*)', [b'*']) + + def test_search_keys_are_text(self): + base = self.server.suffix + l = self._ldap_conn + result = l.search_s(base, ldap.SCOPE_SUBTREE, '(cn=Foo*)', ['*']) + result.sort() + dn, fields = result[0] + self.assertEqual(dn, 'cn=Foo1,%s' % base) + self.assertEqual(type(dn), text_type) + for key, values in fields.items(): + self.assertEqual(type(key), text_type) + for value in values: + self.assertEqual(type(value), bytes) + + def _get_bytes_ldapobject(self, explicit=True): + if explicit: + kwargs = {'bytes_mode': True} + else: + kwargs = {} + return self._open_ldap_conn( + who=self.server.root_dn.encode('utf-8'), + cred=self.server.root_pw.encode('utf-8'), + **kwargs + ) + + @unittest.skipUnless(PY2, "no bytes_mode under Py3") + def test_bytesmode_search_requires_bytes(self): + l = self._get_bytes_ldapobject() + base = self.server.suffix + + with self.assertRaises(TypeError): + l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, '(cn=Foo*)', [b'*']) + with self.assertRaises(TypeError): + l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, b'(cn=Foo*)', ['*']) + with self.assertRaises(TypeError): + l.search_s(base, ldap.SCOPE_SUBTREE, b'(cn=Foo*)', [b'*']) + + @unittest.skipUnless(PY2, "no bytes_mode under Py3") + def test_bytesmode_search_results_have_bytes(self): + l = self._get_bytes_ldapobject() + base = self.server.suffix + result = l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, b'(cn=Foo*)', [b'*']) + result.sort() + dn, fields = result[0] + self.assertEqual(dn, b'cn=Foo1,%s' % base) + self.assertEqual(type(dn), bytes) + for key, values in fields.items(): + self.assertEqual(type(key), bytes) + for value in values: + self.assertEqual(type(value), bytes) + + @unittest.skipUnless(PY2, "no bytes_mode under Py3") + def test_unset_bytesmode_search_warns_bytes(self): + l = self._get_bytes_ldapobject(explicit=False) + base = self.server.suffix + + l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, '(cn=Foo*)', [b'*']) + l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, b'(cn=Foo*)', ['*']) + l.search_s(base, ldap.SCOPE_SUBTREE, b'(cn=Foo*)', [b'*']) + + def test_search_accepts_unicode_dn(self): + base = self.server.suffix + l = self._ldap_conn + + with self.assertRaises(ldap.NO_SUCH_OBJECT): + result = l.search_s("CN=abc\U0001f498def", ldap.SCOPE_SUBTREE) + + def test_filterstr_accepts_unicode(self): + l = self._ldap_conn + base = self.server.suffix + result = l.search_s(base, ldap.SCOPE_SUBTREE, '(cn=abc\U0001f498def)', ['*']) + self.assertEqual(result, []) + + def test_attrlist_accepts_unicode(self): + base = self.server.suffix + result = self._ldap_conn.search_s( + base, ldap.SCOPE_SUBTREE, + '(cn=Foo*)', ['abc', 'abc\U0001f498def']) + result.sort() + + for dn, attrs in result: + self.assertIsInstance(dn, text_type) + self.assertEqual(attrs, {}) def test001_search_subtree(self): result = self._ldap_conn.search_s( @@ -98,19 +203,19 @@ def test001_search_subtree(self): [ ( 'cn=Foo1,'+self.server.suffix, - {'cn': ['Foo1'], 'objectClass': ['organizationalRole']} + {'cn': [b'Foo1'], 'objectClass': [b'organizationalRole']} ), ( 'cn=Foo2,'+self.server.suffix, - {'cn': ['Foo2'], 'objectClass': ['organizationalRole']} + {'cn': [b'Foo2'], 'objectClass': [b'organizationalRole']} ), ( 'cn=Foo3,'+self.server.suffix, - {'cn': ['Foo3'], 'objectClass': ['organizationalRole']} + {'cn': [b'Foo3'], 'objectClass': [b'organizationalRole']} ), ( 'cn=Foo4,ou=Container,'+self.server.suffix, - {'cn': ['Foo4'], 'objectClass': ['organizationalRole']} + {'cn': [b'Foo4'], 'objectClass': [b'organizationalRole']} ), ] ) @@ -128,15 +233,15 @@ def test002_search_onelevel(self): [ ( 'cn=Foo1,'+self.server.suffix, - {'cn': ['Foo1'], 'objectClass': ['organizationalRole']} + {'cn': [b'Foo1'], 'objectClass': [b'organizationalRole']} ), ( 'cn=Foo2,'+self.server.suffix, - {'cn': ['Foo2'], 'objectClass': ['organizationalRole']} + {'cn': [b'Foo2'], 'objectClass': [b'organizationalRole']} ), ( 'cn=Foo3,'+self.server.suffix, - {'cn': ['Foo3'], 'objectClass': ['organizationalRole']} + {'cn': [b'Foo3'], 'objectClass': [b'organizationalRole']} ), ] ) @@ -151,15 +256,28 @@ def test003_search_oneattr(self): result.sort() self.assertEqual( result, - [('cn=Foo4,ou=Container,'+self.server.suffix, {'cn': ['Foo4']})] + [('cn=Foo4,ou=Container,'+self.server.suffix, {'cn': [b'Foo4']})] ) + def test_search_subschema(self): + l = self._ldap_conn + dn = l.search_subschemasubentry_s() + self.assertIsInstance(dn, text_type) + self.assertEqual(dn, "cn=Subschema") + + @unittest.skipUnless(PY2, "no bytes_mode under Py3") + def test_search_subschema_have_bytes(self): + l = self._get_bytes_ldapobject(explicit=False) + dn = l.search_subschemasubentry_s() + self.assertIsInstance(dn, bytes) + self.assertEqual(dn, b"cn=Subschema") + def test004_errno107(self): l = self.ldap_object_class('ldap://127.0.0.1:42') try: m = l.simple_bind_s("", "") r = l.result4(m, ldap.MSG_ALL, self.timeout) - except ldap.SERVER_DOWN, ldap_err: + except ldap.SERVER_DOWN as ldap_err: errno = ldap_err.args[0]['errno'] if errno != 107: self.fail("expected errno=107, got %d" % errno) @@ -228,20 +346,22 @@ def test103_reconnect_get_state(self): self.assertEqual( l1.__getstate__(), { - '_last_bind': ( + str('_last_bind'): ( 'simple_bind_s', (bind_dn, 'user1_pw'), {} ), - '_options': [(17, 3)], - '_reconnects_done': 0L, - '_retry_delay': 60.0, - '_retry_max': 1, - '_start_tls': 0, - '_trace_level': 0, - '_trace_stack_limit': 5, - '_uri': self.server.ldapi_uri, - 'timeout': -1, + str('_options'): [(17, 3)], + str('_reconnects_done'): 0, + str('_retry_delay'): 60.0, + str('_retry_max'): 1, + str('_start_tls'): 0, + str('_trace_level'): 0, + str('_trace_stack_limit'): 5, + str('_uri'): self.server.ldapi_uri, + str('bytes_mode'): l1.bytes_mode, + str('bytes_mode_hardfail'): l1.bytes_mode_hardfail, + str('timeout'): -1, }, ) diff --git a/Tests/t_ldapurl.py b/Tests/t_ldapurl.py index c87b752..2be03f6 100644 --- a/Tests/t_ldapurl.py +++ b/Tests/t_ldapurl.py @@ -5,8 +5,10 @@ See https://www.python-ldap.org/ for details. """ +from __future__ import unicode_literals + import unittest -import urllib +from ldap.compat import quote import ldapurl from ldapurl import LDAPUrl @@ -178,16 +180,16 @@ def test_ldapurl(self): class TestLDAPUrl(unittest.TestCase): def assertNone(self, expr, msg=None): - self.failIf(expr is not None, msg or ("%r" % expr)) + self.assertFalse(expr is not None, msg or ("%r" % expr)) def test_combo(self): u = MyLDAPUrl( "ldap://127.0.0.1:1234/dc=example,dc=com" + "?attr1,attr2,attr3" + "?sub" - + "?" + urllib.quote("(objectClass=*)") - + "?bindname=" + urllib.quote("cn=d,c=au") - + ",X-BINDPW=" + urllib.quote("???") + + "?" + quote("(objectClass=*)") + + "?bindname=" + quote("cn=d,c=au") + + ",X-BINDPW=" + quote("???") + ",trace=8" ) self.assertEqual(u.urlscheme, "ldap") @@ -274,7 +276,7 @@ def test_parse_dn(self): u = LDAPUrl("ldap:///dn=foo%3f") self.assertEqual(u.dn, "dn=foo?") u = LDAPUrl("ldap:///dn=str%c3%b6der.com") - self.assertEqual(u.dn, "dn=str\xc3\xb6der.com") + self.assertEqual(u.dn, "dn=str\xf6der.com") def test_parse_attrs(self): u = LDAPUrl("ldap:///?") @@ -338,7 +340,7 @@ def test_parse_filter(self): u = LDAPUrl("ldap:///???(cn=Q%3f)") self.assertEqual(u.filterstr, "(cn=Q?)") u = LDAPUrl("ldap:///???(sn=Str%c3%b6der)") # (possibly bad?) - self.assertEqual(u.filterstr, "(sn=Str\xc3\xb6der)") + self.assertEqual(u.filterstr, "(sn=Str\xf6der)") u = LDAPUrl("ldap:///???(sn=Str\\c3\\b6der)") self.assertEqual(u.filterstr, "(sn=Str\\c3\\b6der)") # (recommended) u = LDAPUrl("ldap:///???(cn=*\\2a*)") diff --git a/Tests/t_ldif.py b/Tests/t_ldif.py index 3da213f..adf0d26 100644 --- a/Tests/t_ldif.py +++ b/Tests/t_ldif.py @@ -5,6 +5,8 @@ See https://www.python-ldap.org/ for details. """ +from __future__ import unicode_literals + # from Python's standard lib import unittest import textwrap @@ -184,7 +186,7 @@ def test_folded(self): value attrib2: %s - """ % (b'asdf.'*20), [ + """ % ('asdf.'*20), [ ( 'cn=x,cn=y,cn=z', { @@ -273,6 +275,26 @@ def test_big_binary(self): ) def test_unicode(self): + # Encode "Ströder" as UTF-8+Base64 + # Putting "Ströder" in a single line would be an invalid LDIF file + # per https://tools.ietf.org/html/rfc2849 (only safe ascii is allowed in a file) + self.check_records( + """ + dn: cn=Michael Stroeder,dc=stroeder,dc=com + lastname:: U3Ryw7ZkZXI= + + """, + [ + ( + 'cn=Michael Stroeder,dc=stroeder,dc=com', + {'lastname': [b'Str\303\266der']}, + ), + ] + ) + + def test_unencoded_unicode(self): + # Encode "Ströder" as UTF-8, without base64 + # This is an invalid LDIF file, but such files are often found in the wild. self.check_records( """ dn: cn=Michael Stroeder,dc=stroeder,dc=com @@ -662,7 +684,7 @@ def test_bad_change_records(self): ldif_string = textwrap.dedent(bad_ldif_string).lstrip() + '\n' try: res = self._parse_records(ldif_string) - except ValueError, value_error: + except ValueError as value_error: pass else: self.fail("should have raised ValueError: %r" % bad_ldif_string) diff --git a/setup.cfg b/setup.cfg index 10dafc9..59816b7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,7 @@ optimize = 1 provides = python-ldap requires = python libldap-2_4 vendor = python-ldap project -packager = Michael Ströder +packager = python-ldap team distribution_name = openSUSE 11.x release = 1 doc_files = CHANGES README INSTALL TODO Demo/ diff --git a/setup.py b/setup.py index 6bbf595..89351b5 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,8 @@ See https://www.python-ldap.org/ for details. """ +import sys,os + has_setuptools = False try: from setuptools import setup, Extension @@ -11,8 +13,15 @@ except ImportError: from distutils.core import setup, Extension -from ConfigParser import ConfigParser -import sys,os,time +if sys.version_info[0] == 2 and sys.version_info[1] < 7: + raise RuntimeError('This software requires Python 2.7 or 3.x.') +if sys.version_info[0] >= 3 and sys.version_info < (3, 3): + raise RuntimeError('The C API from Python 3.3+ is required.') + +if sys.version_info[0] >= 3: + from configparser import ConfigParser +else: + from ConfigParser import ConfigParser sys.path.insert(0, os.path.join(os.getcwd(), 'Lib/ldap')) import pkginfo @@ -59,7 +68,8 @@ class OpenLDAP2: kwargs = dict( include_package_data = True, install_requires = ['setuptools'], - zip_safe = False + zip_safe = False, + python_requires = '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*', ) setup( @@ -75,7 +85,7 @@ class OpenLDAP2: (e.g. processing LDIF, LDAPURLs, LDAPv3 schema, LDAPv3 extended operations and controls, etc.). """, - author = pkginfo.__author__, + author = 'python-ldap project', author_email = 'python-ldap@python.org', url = 'https://www.python-ldap.org/', download_url = 'https://pypi.python.org/pypi/python-ldap/', @@ -88,8 +98,17 @@ class OpenLDAP2: 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX', 'Programming Language :: C', + 'Programming Language :: Python', 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + # Note: when updating Python versions, also change .travis.yml and tox.ini + 'Topic :: Database', 'Topic :: Internet', 'Topic :: Software Development :: Libraries :: Python Modules', @@ -137,6 +156,7 @@ class OpenLDAP2: 'ldap', 'slapdtest', 'ldap.async', + 'ldap.compat', 'ldap.controls', 'ldap.controls.deref', 'ldap.controls.libldap', diff --git a/tox.ini b/tox.ini index d11ce1c..61a6aed 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,8 @@ # and then run "tox" from this directory. [tox] -envlist = py27 +# Note: when updating Python versions, also change setup.py and .travis.yml +envlist = py27,py33,py34,py35,py36 [testenv] commands = {envpython} setup.py test