diff --git a/CHANGES b/CHANGES index b706563..b3ef0d5 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,34 @@ +---------------------------------------------------------------- +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 diff --git a/Demo/Lib/ldap/async/ldifwriter.py b/Demo/Lib/ldap/async/ldifwriter.py index 8cc4aa5..9671762 100644 --- a/Demo/Lib/ldap/async/ldifwriter.py +++ b/Demo/Lib/ldap/async/ldifwriter.py @@ -7,9 +7,6 @@ This example translates the naming context of data read from input, sanitizes some attributes, maps/removes object classes, maps/removes attributes., etc. It's far from being complete though. - -Python compability note: -Tested on Python 2.0+, should run on Python 1.5.x. """ import sys,ldap,ldap.async diff --git a/Demo/Lib/ldap/async/sizelimit.py b/Demo/Lib/ldap/async/sizelimit.py index 05e439b..11ed7b6 100644 --- a/Demo/Lib/ldap/async/sizelimit.py +++ b/Demo/Lib/ldap/async/sizelimit.py @@ -8,9 +8,6 @@ This example translates the naming context of data read from input, sanitizes some attributes, maps/removes object classes, maps/removes attributes., etc. It's far from being complete though. - -Python compability note: -Tested on Python 2.0+, should run on Python 1.5.x. """ import sys,ldap,ldap.async diff --git a/Demo/Lib/ldif/ldifcopy.py b/Demo/Lib/ldif/ldifcopy.py index 498b857..62cb391 100644 --- a/Demo/Lib/ldif/ldifcopy.py +++ b/Demo/Lib/ldif/ldifcopy.py @@ -7,9 +7,6 @@ This example translates the naming context of data read from input, sanitizes some attributes, maps/removes object classes, maps/removes attributes., etc. It's far from being complete though. - -Python compability note: -Tested on Python 2.0+, should run on Python 1.5.x. """ import sys,ldif diff --git a/Demo/pyasn1/syncrepl.py b/Demo/pyasn1/syncrepl.py index 1177d18..1454eef 100644 --- a/Demo/pyasn1/syncrepl.py +++ b/Demo/pyasn1/syncrepl.py @@ -7,16 +7,14 @@ Notes: The bound user needs read access to the attributes entryDN and entryCSN. - -This needs the following software: -Python -pyasn1 0.1.4+ -pyasn1-modules -python-ldap 2.4.10+ """ # Import modules from Python standard lib -import shelve,signal,time,sys,logging +import logging +import shelve +import signal +import sys +import time # Import the python-ldap modules import ldap @@ -25,28 +23,34 @@ from ldap.ldapobject import ReconnectLDAPObject from ldap.syncrepl import SyncreplConsumer +logger = logging.getLogger('syncrepl') +logger.setLevel(logging.DEBUG) +logger.addHandler(logging.StreamHandler()) # Global state watcher_running = True ldap_connection = False -class SyncReplConsumer(ReconnectLDAPObject, SyncreplConsumer): +class SyncReplClient(ReconnectLDAPObject, SyncreplConsumer): """ - Syncrepl Consumer interface + Syncrepl Consumer Client """ def __init__(self, db_path, *args, **kwargs): # Initialise the LDAP Connection first ldap.ldapobject.ReconnectLDAPObject.__init__(self, *args, **kwargs) # Now prepare the data store - self.__data = shelve.open(db_path, 'c') + if db_path: + self.__data = shelve.open(db_path, 'c') + else: + self.__data = dict() # We need this for later internal use self.__presentUUIDs = dict() def close_db(self): - # Close the data store properly to avoid corruption - self.__data.close() + # Close the data store properly to avoid corruption + self.__data.close() def syncrepl_get_cookie(self): if 'cookie' in self.__data: @@ -55,7 +59,8 @@ def syncrepl_get_cookie(self): def syncrepl_set_cookie(self,cookie): self.__data['cookie'] = cookie - def syncrepl_entry(self,dn,attributes,uuid): + def syncrepl_entry(self, dn, attributes, uuid): + logger.debug('dn=%r attributes=%r uuid=%r', dn, attributes, uuid) # First we determine the type of change we have here # (and store away the previous data for later if needed) previous_attributes = dict() @@ -69,18 +74,18 @@ def syncrepl_entry(self,dn,attributes,uuid): attributes['dn'] = dn self.__data[uuid] = attributes # Debugging - print 'Detected', change_type, 'of entry:', dn + logger.debug('Detected %s of entry %r', change_type, dn) # If we have a cookie then this is not our first time being run, # so it must be a change if 'ldap_cookie' in self.__data: - self.perform_application_sync(dn, attributes, previous_attributes) + self.perform_application_sync(dn, attributes, previous_attributes) def syncrepl_delete(self,uuids): # Make sure we know about the UUID being deleted, just in case... uuids = [uuid for uuid in uuids if uuid in self.__data] # Delete all the UUID values we know of for uuid in uuids: - print 'Detected deletion of entry:', self.__data[uuid]['dn'] + logger.debug('Detected deletion of entry %r', self.__data[uuid]['dn']) del self.__data[uuid] def syncrepl_present(self,uuids,refreshDeletes=False): @@ -105,10 +110,10 @@ def syncrepl_present(self,uuids,refreshDeletes=False): self.__presentUUIDs[uuid] = True def syncrepl_refreshdone(self): - print 'Initial synchronization is now done, persist phase begins' + logger.info('Initial synchronization is now done, persist phase begins') def perform_application_sync(self,dn,attributes,previous_attributes): - print 'Performing application sync for:', dn + logger.info('Performing application sync for %r', dn) return True @@ -116,67 +121,69 @@ def perform_application_sync(self,dn,attributes,previous_attributes): def commenceShutdown(signum, stack): # Declare the needed global variables global watcher_running, ldap_connection - print 'Shutting down!' + logger.warn('Shutting down!') # We are no longer running watcher_running = False # Tear down the server connection - if( ldap_connection ): - ldap_connection.close_db() - ldap_connection.unbind_s() - del ldap_connection + if ldap_connection: + ldap_connection.close_db() + ldap_connection.unbind_s() + del ldap_connection # Shutdown sys.exit(0) # Time to actually begin execution # Install our signal handlers -signal.signal(signal.SIGTERM,commenceShutdown) -signal.signal(signal.SIGINT,commenceShutdown) +signal.signal(signal.SIGTERM, commenceShutdown) +signal.signal(signal.SIGINT, commenceShutdown) try: - ldap_url = ldapurl.LDAPUrl(sys.argv[1]) - database_path = sys.argv[2] + ldap_url = ldapurl.LDAPUrl(sys.argv[1]) + database_path = sys.argv[2] except IndexError,e: - print 'Usage:' - print sys.argv[0], ' ' - print sys.argv[0], '\'ldap://127.0.0.1/cn=users,dc=test'\ - '?*'\ - '?sub'\ - '?(objectClass=*)'\ - '?bindname=uid=admin%2ccn=users%2cdc=test,'\ - 'X-BINDPW=password\' db.shelve' + print ( + 'Usage:\n' + '{script_name} \n' + '{script_name} "ldap://127.0.0.1/cn=users,dc=test' + '?*' + '?sub' + '?(objectClass=*)' + '?bindname=uid=admin%2ccn=users%2cdc=test,' + '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) - sys.exit(1) + print 'Error parsing command-line arguments:', str(e) + sys.exit(1) while watcher_running: - print 'Connecting to LDAP server now...' + logger.info('Connecting to %s now...', ldap_url.initializeUrl()) # Prepare the LDAP server connection (triggers the connection as well) - ldap_connection = SyncReplConsumer(database_path, ldap_url.initializeUrl()) + ldap_connection = SyncReplClient(database_path, ldap_url.initializeUrl()) # Now we login to the LDAP server try: ldap_connection.simple_bind_s(ldap_url.who, ldap_url.cred) - except ldap.INVALID_CREDENTIALS, e: - print 'Login to LDAP server failed: ', str(e) + except ldap.INVALID_CREDENTIALS, err: + logger.error('Login to LDAP server failed: %s', err) sys.exit(1) except ldap.SERVER_DOWN: - print 'LDAP server is down, going to retry.' + logger.warn('LDAP server is down, going to retry.') time.sleep(5) continue # Commence the syncing - print 'Commencing sync process' + logger.debug('Commencing sync process') ldap_search = ldap_connection.syncrepl_search( - ldap_url.dn or '', - ldap_url.scope or ldap.SCOPE_SUBTREE, - mode = 'refreshAndPersist', - attrlist=ldap_url.attrs, - filterstr = ldap_url.filterstr or '(objectClass=*)' + ldap_url.dn or '', + ldap_url.scope or ldap.SCOPE_SUBTREE, + mode = 'refreshAndPersist', + attrlist=ldap_url.attrs, + filterstr = ldap_url.filterstr or '(objectClass=*)' ) try: @@ -185,10 +192,9 @@ def commenceShutdown(signum, stack): except KeyboardInterrupt: # User asked to exit commenceShutdown(None, None) - pass - except Exception, e: + except Exception, err: # Handle any exception if watcher_running: - print 'Encountered a problem, going to retry. Error:', str(e) + logger.exception('Unhandled exception, going to retry: %s', err) + logger.info('Going to retry after 5 secs') time.sleep(5) - pass diff --git a/Demo/resiter.py b/Demo/resiter.py index 3394524..ff9fe5a 100644 --- a/Demo/resiter.py +++ b/Demo/resiter.py @@ -3,9 +3,6 @@ written by Michael Stroeder See http://www.python-ldap.org for details. - -Python compability note: -Requires Python 2.3+ """ import ldap,ldap.resiter diff --git a/Demo/simplebrowse.py b/Demo/simplebrowse.py index f8e7182..804d12f 100644 --- a/Demo/simplebrowse.py +++ b/Demo/simplebrowse.py @@ -5,7 +5,6 @@ # import ldap -import string from traceback import print_exc url = "ldap://ldap.openldap.org/" @@ -71,8 +70,8 @@ if arg == '-': lastdn,dn = dn,lastdn elif arg == '..': - dn = string.join(ldap.explode_dn(dn)[1:], ",") - dn = string.strip(dn) + dn = ldap.explode_dn(dn)[1:].join(",") + dn = dn.strip() else: try: i = int(arg) diff --git a/Doc/conf.py b/Doc/conf.py index ab7c509..9c322de 100644 --- a/Doc/conf.py +++ b/Doc/conf.py @@ -42,7 +42,7 @@ # The short X.Y version. version = '2.5' # The full version, including alpha/beta/rc tags. -release = '2.5.1.0' +release = '2.5.2.0' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: diff --git a/Doc/ldap-dn.rst b/Doc/ldap-dn.rst index d78f070..c22a64c 100644 --- a/Doc/ldap-dn.rst +++ b/Doc/ldap-dn.rst @@ -49,7 +49,7 @@ The :mod:`ldap.dn` module defines the following functions: function :func:`escape_dn_chars`. -.. function:: explode_dn(dn [, notypes=0[, flags=0]]) -> list +.. function:: explode_dn(dn [, notypes=False[, flags=0]]) -> list This function takes *dn* and breaks it up into its component parts. Each part is known as an RDN (Relative Distinguished Name). The optional *notypes* @@ -60,7 +60,7 @@ The :mod:`ldap.dn` module defines the following functions: deprecated. -.. function:: explode_rdn(rdn [, notypes=0[, flags=0]]) -> list +.. function:: explode_rdn(rdn [, notypes=False[, flags=0]]) -> list This function takes a (multi-valued) *rdn* and breaks it up into a list of characteristic attributes. The optional *notypes* parameter is used to specify @@ -85,26 +85,26 @@ Splitting a LDAPv3 DN to AVA level. Note that both examples have the same result but in the first example the non-ASCII chars are passed as is (byte buffer string) whereas in the second example the hex-encoded DN representation are passed to the function. ->>> ldap.dn.str2dn('cn=Michael Str\xc3\xb6der,dc=stroeder,dc=com',flags=ldap.DN_FORMAT_LDAPV3) -[[('cn', 'Michael Str\xc3\xb6der', 4)], [('dc', 'stroeder', 1)], [('dc', 'com', 1)]] ->>> ldap.dn.str2dn('cn=Michael Str\C3\B6der,dc=stroeder,dc=com',flags=ldap.DN_FORMAT_LDAPV3) -[[('cn', 'Michael Str\xc3\xb6der', 4)], [('dc', 'stroeder', 1)], [('dc', 'com', 1)]] +>>> ldap.dn.str2dn('cn=Michael Str\xc3\xb6der,dc=example,dc=com',flags=ldap.DN_FORMAT_LDAPV3) +[[('cn', 'Michael Str\xc3\xb6der', 4)], [('dc', 'example', 1)], [('dc', 'com', 1)]] +>>> ldap.dn.str2dn('cn=Michael Str\C3\B6der,dc=example,dc=com',flags=ldap.DN_FORMAT_LDAPV3) +[[('cn', 'Michael Str\xc3\xb6der', 4)], [('dc', 'example', 1)], [('dc', 'com', 1)]] Splitting a LDAPv2 DN into RDN parts: ->>> ldap.dn.explode_dn('cn=Michael Stroeder;dc=stroeder;dc=com',flags=ldap.DN_FORMAT_LDAPV2) -['cn=Michael Stroeder', 'dc=stroeder', 'dc=com'] +>>> ldap.dn.explode_dn('cn=John Doe;dc=example;dc=com',flags=ldap.DN_FORMAT_LDAPV2) +['cn=John Doe', 'dc=example', 'dc=com'] Splitting a multi-valued RDN: ->>> ldap.dn.explode_rdn('cn=Michael Stroeder+mail=michael@stroeder.com',flags=ldap.DN_FORMAT_LDAPV2) -['cn=Michael Stroeder', 'mail=michael@stroeder.com'] +>>> ldap.dn.explode_rdn('cn=John Doe+mail=john.doe@example.com',flags=ldap.DN_FORMAT_LDAPV2) +['cn=John Doe', 'mail=john.doe@example.com'] Splitting a LDAPv3 DN with a multi-valued RDN into its AVA parts: ->>> ldap.dn.str2dn('cn=Michael Stroeder+mail=michael@stroeder.com,dc=stroeder,dc=com') -[[('cn', 'Michael Stroeder', 1), ('mail', 'michael@stroeder.com', 1)], [('dc', 'stroeder', 1)], [('dc', 'com', 1)]] +>>> ldap.dn.str2dn('cn=John Doe+mail=john.doe@example.com,dc=example,dc=com') +[[('cn', 'John Doe', 1), ('mail', 'john.doe@example.com', 1)], [('dc', 'example', 1)], [('dc', 'com', 1)]] diff --git a/Lib/ldap/__init__.py b/Lib/ldap/__init__.py index c1c5ef1..c8a1b12 100644 --- a/Lib/ldap/__init__.py +++ b/Lib/ldap/__init__.py @@ -23,6 +23,8 @@ assert _ldap.__version__==__version__, \ ImportError('ldap %s and _ldap %s version mismatch!' % (__version__,_ldap.__version__)) from _ldap import * +# call into libldap to initialize it right now +LIBLDAP_API_INFO = _ldap.get_option(_ldap.OPT_API_INFO) OPT_NAMES_DICT = {} for k,v in vars(_ldap).items(): diff --git a/Lib/ldap/async.py b/Lib/ldap/async.py index 81824e4..0dd4940 100644 --- a/Lib/ldap/async.py +++ b/Lib/ldap/async.py @@ -2,26 +2,22 @@ ldap.async - handle async LDAP operations See https://www.python-ldap.org/ for details. - -Python compability note: -Tested on Python 2.0+ but should run on Python 1.5.x. """ import ldap from ldap import __version__ +SEARCH_RESULT_TYPES = set([ + ldap.RES_SEARCH_ENTRY, + ldap.RES_SEARCH_RESULT, + ldap.RES_SEARCH_REFERENCE, +]) -_searchResultTypes={ - ldap.RES_SEARCH_ENTRY:None, - ldap.RES_SEARCH_RESULT:None, - ldap.RES_SEARCH_REFERENCE:None, -} - -_entryResultTypes={ - ldap.RES_SEARCH_ENTRY:None, - ldap.RES_SEARCH_RESULT:None, -} +ENTRY_RESULT_TYPES = set([ + ldap.RES_SEARCH_ENTRY, + ldap.RES_SEARCH_RESULT, +]) class WrongResultType(Exception): @@ -137,8 +133,8 @@ def processResults(self,ignoreResultsNumber=0,processResultsCount=0,timeout=-1): self._afterFirstResult = 0 if not result_list: break - if not _searchResultTypes.has_key(result_type): - raise WrongResultType(result_type,_searchResultTypes.keys()) + if result_type not in SEARCH_RESULT_TYPES: + raise WrongResultType(result_type,SEARCH_RESULT_TYPES) # Loop over list of search results for result_item in result_list: if result_counter list + explode_dn(dn [, notypes=False [, flags=0]]) -> list This function takes a DN and breaks it up into its component parts. The notypes parameter is used to specify that only the component's @@ -91,9 +88,9 @@ def explode_dn(dn,notypes=0,flags=0): return rdn_list -def explode_rdn(rdn,notypes=0,flags=0): +def explode_rdn(rdn, notypes=False, flags=0): """ - explode_rdn(rdn [, notypes=0]) -> list + explode_rdn(rdn [, notypes=0 [, flags=0]]) -> list This function takes a RDN and breaks it up into its component parts if it is a multi-valued RDN. diff --git a/Lib/ldap/functions.py b/Lib/ldap/functions.py index ea763fa..6ddab54 100644 --- a/Lib/ldap/functions.py +++ b/Lib/ldap/functions.py @@ -2,18 +2,6 @@ functions.py - wraps functions of module _ldap See https://www.python-ldap.org/ for details. - -Compability: -- Tested with Python 2.0+ but should work with Python 1.5.x -- functions should behave exactly the same like in _ldap - -Usage: -Directly imported by ldap/__init__.py. The symbols of _ldap are -overridden. - -Thread-lock: -Basically calls into the LDAP lib are serialized by the module-wide -lock _ldapmodule_lock. """ from ldap import __version__ diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index 70ecec7..805b6e3 100644 --- a/Lib/ldap/ldapobject.py +++ b/Lib/ldap/ldapobject.py @@ -2,28 +2,15 @@ ldapobject.py - wraps class _ldap.LDAPObject See https://www.python-ldap.org/ for details. - -Compability: -- Tested with Python 2.0+ but should work with Python 1.5.x -- LDAPObject class should be exactly the same like _ldap.LDAPObject - -Usage: -Directly imported by ldap/__init__.py. The symbols of _ldap are -overridden. - -Thread-lock: -Basically calls into the LDAP lib are serialized by the module-wide -lock self._ldap_object_lock. """ from os import strerror -from ldap import __version__ +from ldap.pkginfo import __version__, __author__, __license__ __all__ = [ 'LDAPObject', 'SimpleLDAPObject', - 'NonblockingLDAPObject', 'ReconnectLDAPObject', ] @@ -112,7 +99,7 @@ def _ldap_call(self,func,*args,**kwargs): except LDAPError, e: exc_type,exc_value,exc_traceback = sys.exc_info() try: - if not e.args[0].has_key('info') and e.args[0].has_key('errno'): + if 'info' not in e.args[0] and 'errno' in e.args[0]: e.args[0]['info'] = strerror(e.args[0]['errno']) except IndexError: pass @@ -127,15 +114,15 @@ def _ldap_call(self,func,*args,**kwargs): return result def __setattr__(self,name,value): - if self.CLASSATTR_OPTION_MAPPING.has_key(name): + if name in self.CLASSATTR_OPTION_MAPPING: self.set_option(self.CLASSATTR_OPTION_MAPPING[name],value) else: self.__dict__[name] = value def __getattr__(self,name): - if self.CLASSATTR_OPTION_MAPPING.has_key(name): + if name in self.CLASSATTR_OPTION_MAPPING: return self.get_option(self.CLASSATTR_OPTION_MAPPING[name]) - elif self.__dict__.has_key(name): + elif name in self.__dict__: return self.__dict__[name] else: raise AttributeError,'%s has no attribute %s' % ( @@ -299,12 +286,14 @@ def compare_ext(self,dn,attr,value,serverctrls=None,clientctrls=None): def compare_ext_s(self,dn,attr,value,serverctrls=None,clientctrls=None): msgid = self.compare_ext(dn,attr,value,serverctrls,clientctrls) try: - resp_type, resp_data, resp_msgid, resp_ctrls = self.result3(msgid,all=1,timeout=self.timeout) + ldap_res = self.result3(msgid,all=1,timeout=self.timeout) except ldap.COMPARE_TRUE: return 1 except ldap.COMPARE_FALSE: return 0 - return None + raise ldap.PROTOCOL_ERROR( + 'Compare operation returned wrong result: %r' % (ldap_res) + ) def compare(self,dn,attr,value): return self.compare_ext(dn,attr,value,None,None) @@ -772,40 +761,6 @@ def get_naming_contexts(self): ).get('namingContexts', []) -class NonblockingLDAPObject(SimpleLDAPObject): - - def __init__(self,uri,trace_level=0,trace_file=None,result_timeout=-1): - self._result_timeout = result_timeout - SimpleLDAPObject.__init__(self,uri,trace_level,trace_file) - - def result(self,msgid=ldap.RES_ANY,all=1,timeout=-1): - """ - """ - ldap_result = self._ldap_call(self._l.result,msgid,0,self._result_timeout) - if not all: - return ldap_result - start_time = time.time() - all_results = [] - while all: - while ldap_result[0] is None: - if (timeout>=0) and (time.time()-start_time>timeout): - self._ldap_call(self._l.abandon,msgid) - raise ldap.TIMEOUT( - "LDAP time limit (%d secs) exceeded." % (timeout) - ) - time.sleep(0.00001) - ldap_result = self._ldap_call(self._l.result,msgid,0,self._result_timeout) - if ldap_result[1] is None: - break - all_results.extend(ldap_result[1]) - ldap_result = None,None - return all_results - - def search_st(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrsonly=0,timeout=-1): - msgid = self.search(base,scope,filterstr,attrlist,attrsonly) - return self.result(msgid,all=1,timeout=timeout) - - class ReconnectLDAPObject(SimpleLDAPObject): """ In case of server failure (ldap.SERVER_DOWN) the implementations @@ -819,12 +774,13 @@ class ReconnectLDAPObject(SimpleLDAPObject): application. """ - __transient_attrs__ = { - '_l':None, - '_ldap_object_lock':None, - '_trace_file':None, - '_reconnect_lock':None, - } + __transient_attrs__ = set([ + '_l', + '_ldap_object_lock', + '_trace_file', + '_reconnect_lock', + '_last_bind', + ]) def __init__( self,uri, @@ -852,15 +808,18 @@ def __init__( def __getstate__(self): """return data representation for pickled object""" - d = {} - for k,v in self.__dict__.items(): - if not self.__transient_attrs__.has_key(k): - d[k] = v - return d + state = dict([ + (k,v) + for k,v in self.__dict__.items() + if k not in self.__transient_attrs__ + ]) + state['_last_bind'] = self._last_bind[0].__name__, self._last_bind[1], self._last_bind[2] + return state def __setstate__(self,d): """set up the object from pickled data""" self.__dict__.update(d) + self._last_bind = getattr(SimpleLDAPObject, self._last_bind[0]), self._last_bind[1], self._last_bind[2] self._ldap_object_lock = self._ldap_lock() self._reconnect_lock = ldap.LDAPLock(desc='reconnect lock within %s' % (repr(self))) self._trace_file = sys.stdout @@ -930,7 +889,7 @@ def reconnect(self,uri,retry_max=1,retry_delay=60.0): return # reconnect() def _apply_method_s(self,func,*args,**kwargs): - if not self.__dict__.has_key('_l'): + if not hasattr(self,'_l'): self.reconnect(self._uri,retry_max=self._retry_max,retry_delay=self._retry_delay) try: return func(self,*args,**kwargs) diff --git a/Lib/ldap/logger.py b/Lib/ldap/logger.py index d955c2e..4db961e 100644 --- a/Lib/ldap/logger.py +++ b/Lib/ldap/logger.py @@ -5,15 +5,15 @@ import logging -class logging_file_class: +class logging_file_class(object): - def __init__(self,logging_level): - self._logging_level = logging_level + def __init__(self, logging_level): + self._logging_level = logging_level - def write(self,msg): - logging.log(self._logging_level,msg[:-1]) + def write(self, msg): + logging.log(self._logging_level, msg[:-1]) - def flush(self): - return + def flush(self): + return logging_file_obj = logging_file_class(logging.DEBUG) diff --git a/Lib/ldap/modlist.py b/Lib/ldap/modlist.py index 0d1ac40..6f4bf1e 100644 --- a/Lib/ldap/modlist.py +++ b/Lib/ldap/modlist.py @@ -2,39 +2,19 @@ ldap.modlist - create add/modify modlist's See https://www.python-ldap.org/ for details. - -Python compability note: -This module is known to work with Python 2.0+ but should work -with Python 1.5.2 as well. """ from ldap import __version__ -import string,ldap,ldap.cidict - - -def list_dict(l,case_insensitive=0): - """ - return a dictionary with all items of l being the keys of the dictionary - - If argument case_insensitive is non-zero ldap.cidict.cidict will be - used for case-insensitive string keys - """ - if case_insensitive: - d = ldap.cidict.cidict() - else: - d = {} - for i in l: - d[i]=None - return d +import ldap def addModlist(entry,ignore_attr_types=None): """Build modify list for call of method LDAPObject.add()""" - ignore_attr_types = list_dict(map(string.lower,(ignore_attr_types or []))) + ignore_attr_types = set(map(str.lower,ignore_attr_types or [])) modlist = [] for attrtype in entry.keys(): - if ignore_attr_types.has_key(string.lower(attrtype)): + if attrtype.lower() in ignore_attr_types: # This attribute type is ignored continue # Eliminate empty attr value strings in list @@ -66,20 +46,20 @@ def modifyModlist( List of attribute type names for which comparison will be made case-insensitive """ - ignore_attr_types = list_dict(map(string.lower,(ignore_attr_types or []))) - case_ignore_attr_types = list_dict(map(string.lower,(case_ignore_attr_types or []))) + ignore_attr_types = set(map(str.lower,ignore_attr_types or [])) + case_ignore_attr_types = set(map(str.lower,case_ignore_attr_types or [])) modlist = [] attrtype_lower_map = {} for a in old_entry.keys(): - attrtype_lower_map[string.lower(a)]=a + attrtype_lower_map[str.lower(a)]=a for attrtype in new_entry.keys(): - attrtype_lower = string.lower(attrtype) - if ignore_attr_types.has_key(attrtype_lower): + attrtype_lower = str.lower(attrtype) + if attrtype_lower in ignore_attr_types: # This attribute type is ignored continue # Filter away null-strings new_value = filter(lambda x:x!=None,new_entry[attrtype]) - if attrtype_lower_map.has_key(attrtype_lower): + 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) del attrtype_lower_map[attrtype_lower] @@ -92,20 +72,14 @@ def modifyModlist( # Replace existing attribute replace_attr_value = len(old_value)!=len(new_value) if not replace_attr_value: - case_insensitive = case_ignore_attr_types.has_key(attrtype_lower) - old_value_dict=list_dict(old_value,case_insensitive) - new_value_dict=list_dict(new_value,case_insensitive) - delete_values = [] - for v in old_value: - if not new_value_dict.has_key(v): - replace_attr_value = 1 - break - add_values = [] - if not replace_attr_value: - for v in new_value: - if not old_value_dict.has_key(v): - replace_attr_value = 1 - break + if attrtype_lower in case_ignore_attr_types: + norm_func = str.lower + old_value_set = set(map(str.lower,old_value)) + new_value_set = set(map(str.lower,new_value)) + else: + old_value_set = set(old_value) + new_value_set = set(new_value) + replace_attr_value = new_value_set != old_value_set if replace_attr_value: modlist.append((ldap.MOD_DELETE,attrtype,None)) modlist.append((ldap.MOD_ADD,attrtype,new_value)) @@ -116,7 +90,7 @@ def modifyModlist( # Remove all attributes of old_entry which are not present # in new_entry at all for a in attrtype_lower_map.keys(): - if ignore_attr_types.has_key(a): + if a in ignore_attr_types: # This attribute type is ignored continue attrtype = attrtype_lower_map[a] diff --git a/Lib/ldap/pkginfo.py b/Lib/ldap/pkginfo.py index 39a572b..da891b3 100644 --- a/Lib/ldap/pkginfo.py +++ b/Lib/ldap/pkginfo.py @@ -2,6 +2,6 @@ """ meta attributes for packaging which does not import any dependencies """ -__version__ = '2.5.1' +__version__ = '2.5.2' __author__ = u'python-ldap project' __license__ = 'Python style' diff --git a/Lib/ldap/resiter.py b/Lib/ldap/resiter.py index d8c1368..dc912eb 100644 --- a/Lib/ldap/resiter.py +++ b/Lib/ldap/resiter.py @@ -2,26 +2,40 @@ ldap.resiter - processing LDAP results with iterators See https://www.python-ldap.org/ for details. - -Python compability note: -Requires Python 2.3+ """ +from ldap.pkginfo import __version__, __author__, __license__ -class ResultProcessor: - """ - Mix-in class used with ldap.ldapopbject.LDAPObject or derived classes. - """ - def allresults(self,msgid,timeout=-1,add_ctrls=0): +class ResultProcessor: """ - Generator function which returns an iterator for processing all LDAP operation - results of the given msgid retrieved with LDAPObject.result3() -> 4-tuple + Mix-in class used with ldap.ldapopbject.LDAPObject or derived classes. """ - result_type,result_list,result_msgid,result_serverctrls,_,_ = self.result4(msgid,0,timeout,add_ctrls=add_ctrls) - while result_type and result_list: - # Loop over list of search results - for result_item in result_list: - yield (result_type,result_list,result_msgid,result_serverctrls) - result_type,result_list,result_msgid,result_serverctrls,_,_ = self.result4(msgid,0,timeout,add_ctrls=add_ctrls) - return # allresults() + + def allresults(self, msgid, timeout=-1, add_ctrls=0): + """ + Generator function which returns an iterator for processing all LDAP operation + results of the given msgid like retrieved with LDAPObject.result3() -> 4-tuple + """ + result_type, result_list, result_msgid, result_serverctrls, _, _ = \ + self.result4( + msgid, + 0, + timeout, + add_ctrls=add_ctrls + ) + while result_type and result_list: + yield ( + result_type, + result_list, + result_msgid, + result_serverctrls + ) + result_type, result_list, result_msgid, result_serverctrls, _, _ = \ + self.result4( + msgid, + 0, + timeout, + add_ctrls=add_ctrls + ) + return # allresults() diff --git a/Lib/ldap/sasl.py b/Lib/ldap/sasl.py index 81438cc..34d4cb0 100644 --- a/Lib/ldap/sasl.py +++ b/Lib/ldap/sasl.py @@ -10,44 +10,47 @@ LDAPObject's sasl_bind_s() method Implementing support for new sasl mechanism is very easy --- see the examples of digest_md5 and gssapi. - -Compability: -- Tested with Python 2.0+ but should work with Python 1.5.x """ from ldap import __version__ if __debug__: - # Tracing is only supported in debugging mode - import traceback - from ldap import _trace_level,_trace_file,_trace_stack_limit + # Tracing is only supported in debugging mode + from ldap import _trace_level, _trace_file + # These are the SASL callback id's , as defined in sasl.h -CB_USER = 0x4001 -CB_AUTHNAME = 0x4002 -CB_LANGUAGE = 0x4003 -CB_PASS = 0x4004 -CB_ECHOPROMPT = 0x4005 -CB_NOECHOPROMPT= 0x4006 -CB_GETREALM = 0x4008 +CB_USER = 0x4001 +CB_AUTHNAME = 0x4002 +CB_LANGUAGE = 0x4003 +CB_PASS = 0x4004 +CB_ECHOPROMPT = 0x4005 +CB_NOECHOPROMPT = 0x4006 +CB_GETREALM = 0x4008 + class sasl: - """This class handles SASL interactions for authentication. + """ + This class handles SASL interactions for authentication. If an instance of this class is passed to ldap's sasl_bind_s() method, the library will call its callback() method. For specific SASL authentication mechanisms, this method can be - overridden""" + overridden + """ - def __init__(self,cb_value_dict,mech): - """ The (generic) base class takes a cb_value_dictionary of + def __init__(self, cb_value_dict, mech): + """ + The (generic) base class takes a cb_value_dictionary of question-answer pairs. Questions are specified by the respective SASL callback id's. The mech argument is a string that specifies - the SASL mechaninsm to be uesd.""" + the SASL mechaninsm to be uesd. + """ self.cb_value_dict = cb_value_dict or {} self.mech = mech - def callback(self,cb_id,challenge,prompt,defresult): - """ The callback method will be called by the sasl_bind_s() + def callback(self, cb_id, challenge, prompt, defresult): + """ + The callback method will be called by the sasl_bind_s() method several times. Each time it will provide the id, which tells us what kind of information is requested (the CB_ ... constants above). The challenge might be a short (english) text @@ -60,50 +63,66 @@ def callback(self,cb_id,challenge,prompt,defresult): cb_value_dictionary. Note that the current callback interface is not very 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).""" + lib (in contrast to one question at a time). + """ # The following print command might be useful for debugging # new sasl mechanisms. So it is left here - cb_result = self.cb_value_dict.get(cb_id,defresult) or '' + cb_result = self.cb_value_dict.get(cb_id, defresult) or '' if __debug__: - if _trace_level>=1: - _trace_file.write("*** id=%d, challenge=%s, prompt=%s, defresult=%s\n-> %s\n" % ( - cb_id, challenge, prompt, repr(defresult), repr(self.cb_value_dict.get(cb_result)) - )) + if _trace_level >= 1: + _trace_file.write("*** id=%d, challenge=%s, prompt=%s, defresult=%s\n-> %s\n" % ( + cb_id, + challenge, + prompt, + repr(defresult), + repr(self.cb_value_dict.get(cb_result)) + )) return cb_result class cram_md5(sasl): - """This class handles SASL CRAM-MD5 authentication.""" + """ + This class handles SASL CRAM-MD5 authentication. + """ - def __init__(self,authc_id, password, authz_id=""): - auth_dict = {CB_AUTHNAME:authc_id, CB_PASS:password, - CB_USER:authz_id} - sasl.__init__(self,auth_dict,"CRAM-MD5") + def __init__(self, authc_id, password, authz_id=""): + auth_dict = { + CB_AUTHNAME: authc_id, + CB_PASS: password, + CB_USER: authz_id, + } + sasl.__init__(self, auth_dict, "CRAM-MD5") class digest_md5(sasl): - """This class handles SASL DIGEST-MD5 authentication.""" + """ + This class handles SASL DIGEST-MD5 authentication. + """ - def __init__(self,authc_id, password, authz_id=""): - auth_dict = {CB_AUTHNAME:authc_id, CB_PASS:password, - CB_USER:authz_id} - sasl.__init__(self,auth_dict,"DIGEST-MD5") + def __init__(self, authc_id, password, authz_id=""): + auth_dict = { + CB_AUTHNAME: authc_id, + CB_PASS: password, + CB_USER: authz_id, + } + sasl.__init__(self, auth_dict, "DIGEST-MD5") class gssapi(sasl): - """This class handles SASL GSSAPI (i.e. Kerberos V) - authentication.""" + """ + This class handles SASL GSSAPI (i.e. Kerberos V) authentication. + """ - def __init__(self,authz_id=""): - sasl.__init__(self, {CB_USER:authz_id},"GSSAPI") + def __init__(self, authz_id=""): + sasl.__init__(self, {CB_USER: authz_id}, "GSSAPI") class external(sasl): - """This class handles SASL EXTERNAL authentication - (i.e. X.509 client certificate)""" - - def __init__(self,authz_id=""): - sasl.__init__(self, {CB_USER:authz_id},"EXTERNAL") - + """ + This class handles SASL EXTERNAL authentication + (i.e. X.509 client certificate) + """ + def __init__(self, authz_id=""): + sasl.__init__(self, {CB_USER: authz_id}, "EXTERNAL") diff --git a/Lib/ldap/schema/models.py b/Lib/ldap/schema/models.py index 383705c..8471954 100644 --- a/Lib/ldap/schema/models.py +++ b/Lib/ldap/schema/models.py @@ -8,25 +8,17 @@ from ldap.schema.tokenizer import split_tokens,extract_tokens -if __debug__: - from types import TupleType,StringType,IntType - try: - from types import BooleanType - except ImportError: - BooleanType = IntType - - -NOT_HUMAN_READABLE_LDAP_SYNTAXES = { - '1.3.6.1.4.1.1466.115.121.1.4':None, # Audio - '1.3.6.1.4.1.1466.115.121.1.5':None, # Binary - '1.3.6.1.4.1.1466.115.121.1.8':None, # Certificate - '1.3.6.1.4.1.1466.115.121.1.9':None, # Certificate List - '1.3.6.1.4.1.1466.115.121.1.10':None, # Certificate Pair - '1.3.6.1.4.1.1466.115.121.1.23':None, # G3 FAX - '1.3.6.1.4.1.1466.115.121.1.28':None, # JPEG - '1.3.6.1.4.1.1466.115.121.1.40':None, # Octet String - '1.3.6.1.4.1.1466.115.121.1.49':None, # Supported Algorithm -} +NOT_HUMAN_READABLE_LDAP_SYNTAXES = set([ + '1.3.6.1.4.1.1466.115.121.1.4', # Audio + '1.3.6.1.4.1.1466.115.121.1.5', # Binary + '1.3.6.1.4.1.1466.115.121.1.8', # Certificate + '1.3.6.1.4.1.1466.115.121.1.9', # Certificate List + '1.3.6.1.4.1.1466.115.121.1.10', # Certificate Pair + '1.3.6.1.4.1.1466.115.121.1.23', # G3 FAX + '1.3.6.1.4.1.1466.115.121.1.28', # JPEG + '1.3.6.1.4.1.1466.115.121.1.40', # Octet String + '1.3.6.1.4.1.1466.115.121.1.49', # Supported Algorithm +]) class SchemaElement: @@ -68,7 +60,7 @@ def get_id(self): return self.oid def key_attr(self,key,value,quoted=0): - assert value is None or type(value)==StringType,TypeError("value has to be of StringType, was %s" % repr(value)) + assert value is None or type(value)==str,TypeError("value has to be of str, was %r" % value) if value: if quoted: return " %s '%s'" % (key,value.replace("'","\\'")) @@ -78,7 +70,7 @@ def key_attr(self,key,value,quoted=0): return "" def key_list(self,key,values,sep=' ',quoted=0): - assert type(values)==TupleType,TypeError("values has to be of ListType") + assert type(values)==tuple,TypeError("values has to be a tuple, was %r" % values) if not values: return '' if quoted: @@ -159,13 +151,6 @@ def _set_attrs(self,l,d): self.sup = ('top',) else: self.sup = d['SUP'] - assert type(self.names)==TupleType - assert self.desc is None or type(self.desc)==StringType - assert type(self.obsolete)==BooleanType and (self.obsolete==0 or self.obsolete==1) - assert type(self.sup)==TupleType - assert type(self.kind)==IntType - assert type(self.must)==TupleType - assert type(self.may)==TupleType return def __str__(self): @@ -286,14 +271,6 @@ def _set_attrs(self,l,d): self.collective = d['COLLECTIVE']!=None self.no_user_mod = d['NO-USER-MODIFICATION']!=None self.usage = AttributeUsage.get(d['USAGE'][0],0) - assert type(self.names)==TupleType - assert self.desc is None or type(self.desc)==StringType - assert type(self.sup)==TupleType,'attribute sup has type %s' % (type(self.sup)) - assert type(self.obsolete)==BooleanType and (self.obsolete==0 or self.obsolete==1) - assert type(self.single_value)==BooleanType and (self.single_value==0 or self.single_value==1) - assert type(self.no_user_mod)==BooleanType and (self.no_user_mod==0 or self.no_user_mod==1) - assert self.syntax is None or type(self.syntax)==StringType - assert self.syntax_len is None or type(self.syntax_len)==type(0L) return def __str__(self): @@ -348,10 +325,9 @@ def _set_attrs(self,l,d): self.desc = d['DESC'][0] self.x_subst = d['X-SUBST'][0] self.not_human_readable = \ - NOT_HUMAN_READABLE_LDAP_SYNTAXES.has_key(self.oid) or \ + self.oid in NOT_HUMAN_READABLE_LDAP_SYNTAXES or \ d['X-NOT-HUMAN-READABLE'][0]=='TRUE' self.x_binary_transfer_required = d['X-BINARY-TRANSFER-REQUIRED'][0]=='TRUE' - assert self.desc is None or type(self.desc)==StringType return def __str__(self): @@ -398,10 +374,6 @@ def _set_attrs(self,l,d): self.desc = d['DESC'][0] self.obsolete = d['OBSOLETE']!=None self.syntax = d['SYNTAX'][0] - assert type(self.names)==TupleType - assert self.desc is None or type(self.desc)==StringType - assert type(self.obsolete)==BooleanType and (self.obsolete==0 or self.obsolete==1) - assert self.syntax is None or type(self.syntax)==StringType return def __str__(self): @@ -448,10 +420,6 @@ def _set_attrs(self,l,d): self.desc = d['DESC'][0] self.obsolete = d['OBSOLETE']!=None self.applies = d['APPLIES'] - assert type(self.names)==TupleType - assert self.desc is None or type(self.desc)==StringType - assert type(self.obsolete)==BooleanType and (self.obsolete==0 or self.obsolete==1) - assert type(self.applies)==TupleType return def __str__(self): @@ -515,13 +483,6 @@ def _set_attrs(self,l,d): self.must = d['MUST'] self.may = d['MAY'] self.nots = d['NOT'] - assert type(self.names)==TupleType - assert self.desc is None or type(self.desc)==StringType - assert type(self.obsolete)==BooleanType and (self.obsolete==0 or self.obsolete==1) - assert type(self.aux)==TupleType - assert type(self.must)==TupleType - assert type(self.may)==TupleType - assert type(self.nots)==TupleType return def __str__(self): @@ -582,11 +543,6 @@ def _set_attrs(self,l,d): self.obsolete = d['OBSOLETE']!=None self.form = d['FORM'][0] self.sup = d['SUP'] - assert type(self.names)==TupleType - assert self.desc is None or type(self.desc)==StringType - assert type(self.obsolete)==BooleanType and (self.obsolete==0 or self.obsolete==1) - assert type(self.form)==StringType - assert type(self.sup)==TupleType return def __str__(self): @@ -646,12 +602,6 @@ def _set_attrs(self,l,d): self.oc = d['OC'][0] self.must = d['MUST'] self.may = d['MAY'] - assert type(self.names)==TupleType - assert self.desc is None or type(self.desc)==StringType - assert type(self.obsolete)==BooleanType and (self.obsolete==0 or self.obsolete==1) - assert type(self.oc)==StringType - assert type(self.must)==TupleType - assert type(self.may)==TupleType return def __str__(self): @@ -665,7 +615,7 @@ def __str__(self): return '( %s )' % ''.join(result) -class Entry(UserDict.UserDict): +class Entry(UserDict.IterableUserDict): """ Schema-aware implementation of an LDAP entry class. @@ -702,8 +652,8 @@ def update(self,dict): for key in dict.keys(): self[key] = dict[key] - def __contains__(self,key): - return self.has_key(key) + def __contains__(self,nameoroid): + return self._at2key(nameoroid) in self.data def __getitem__(self,nameoroid): return self.data[self._at2key(nameoroid)] @@ -721,7 +671,7 @@ def __delitem__(self,nameoroid): def has_key(self,nameoroid): k = self._at2key(nameoroid) - return self.data.has_key(k) + return k in self.data def get(self,nameoroid,failobj): try: diff --git a/Lib/ldap/schema/subentry.py b/Lib/ldap/schema/subentry.py index a80f238..99a0dc4 100644 --- a/Lib/ldap/schema/subentry.py +++ b/Lib/ldap/schema/subentry.py @@ -8,8 +8,6 @@ from ldap.schema.models import * -from UserDict import UserDict - SCHEMA_CLASS_MAPPING = ldap.cidict.cidict() SCHEMA_ATTR_MAPPING = {} @@ -300,7 +298,7 @@ def get_structural_oc(self,oc_list): while struct_oc_list: oid = struct_oc_list.pop() for child_oid in oc_tree[oid]: - if struct_ocs.has_key(self.getoid(ObjectClass,child_oid)): + if self.getoid(ObjectClass,child_oid) in struct_ocs: break else: result = oid @@ -367,7 +365,7 @@ def attribute_types( object_class_oid = object_class_oids.pop(0) # Check whether the objectClass with this OID # has already been processed - if oid_cache.has_key(object_class_oid): + if object_class_oid in oid_cache: continue # Cache this OID as already being processed oid_cache[object_class_oid] = None @@ -420,7 +418,7 @@ def attribute_types( # Remove all mandantory attribute types from # optional attribute type list for a in r_may.keys(): - if r_must.has_key(a): + if a in r_must: del r_may[a] # Apply attr_type_filter to results diff --git a/Lib/ldap/schema/tokenizer.py b/Lib/ldap/schema/tokenizer.py index ede7e21..20958c0 100644 --- a/Lib/ldap/schema/tokenizer.py +++ b/Lib/ldap/schema/tokenizer.py @@ -52,16 +52,15 @@ def extract_tokens(l,known_tokens): """ assert l[0].strip()=="(" and l[-1].strip()==")",ValueError(l) result = {} - result_has_key = result.has_key result.update(known_tokens) i = 0 l_len = len(l) while iob_type == &LDAP_Type) +#define LDAPObject_Check(v) (Py_TYPE(v) == &LDAP_Type) extern LDAPObject *newLDAPObject( LDAP* ); diff --git a/Modules/common.h b/Modules/common.h index e29e20d..1ec232c 100644 --- a/Modules/common.h +++ b/Modules/common.h @@ -20,13 +20,6 @@ #include #endif -/* Backwards compability with Python prior 2.5 */ -#if PY_VERSION_HEX < 0x02050000 -typedef int Py_ssize_t; -#define PY_SSIZE_T_MAX INT_MAX -#define PY_SSIZE_T_MIN INT_MIN -#endif - #include #define streq( a, b ) \ ( (*(a)==*(b)) && 0==strcmp(a,b) ) diff --git a/Modules/constants.c b/Modules/constants.c index 2c0ec1d..06c249a 100644 --- a/Modules/constants.c +++ b/Modules/constants.c @@ -280,12 +280,6 @@ LDAPinit_constants( PyObject* d ) add_int(d,URL_ERR_BADSCOPE); add_int(d,URL_ERR_MEM); - /* author */ - - author = PyString_FromString("python-ldap Project"); - PyDict_SetItemString(d, "__author__", author); - Py_DECREF(author); - /* add_int(d,LIBLDAP_R); */ #ifdef HAVE_LIBLDAP_R obj = PyInt_FromLong(1); diff --git a/Modules/ldapmodule.c b/Modules/ldapmodule.c index a570220..a788ae1 100644 --- a/Modules/ldapmodule.c +++ b/Modules/ldapmodule.c @@ -1,7 +1,6 @@ /* See https://www.python-ldap.org/ for details. */ #include "common.h" -#include "version.h" #include "constants.h" #include "errors.h" #include "functions.h" @@ -9,6 +8,33 @@ #include "LDAPObject.h" +#define _STR(x) #x +#define STR(x) _STR(x) + +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 ) +{ + 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); +} + DL_EXPORT(void) init_ldap(void); /* dummy module methods */ @@ -34,7 +60,7 @@ init_ldap() /* Add some symbolic constants to the module */ d = PyModule_GetDict(m); - LDAPinit_version(d); + LDAPinit_pkginfo(d); LDAPinit_constants(d); LDAPinit_errors(d); LDAPinit_functions(d); diff --git a/Modules/message.h b/Modules/message.h index b4f60eb..c3522ac 100644 --- a/Modules/message.h +++ b/Modules/message.h @@ -10,4 +10,3 @@ extern PyObject* LDAPmessage_to_python( LDAP*ld, LDAPMessage*m, int add_ctrls, int add_intermediates ); #endif /* __h_message_ */ - diff --git a/Modules/options.h b/Modules/options.h index dd61320..570fdc1 100644 --- a/Modules/options.h +++ b/Modules/options.h @@ -1,7 +1,7 @@ /* See https://www.python-ldap.org/ for details. */ -int LDAP_optionval_by_name(const char *name); -int LDAP_set_option(LDAPObject *self, int option, PyObject *value); +int LDAP_optionval_by_name(const char *name); +int LDAP_set_option(LDAPObject *self, int option, PyObject *value); PyObject *LDAP_get_option(LDAPObject *self, int option); void set_timeval_from_double( struct timeval *tv, double d ); diff --git a/Modules/version.c b/Modules/version.c deleted file mode 100644 index b706b8e..0000000 --- a/Modules/version.c +++ /dev/null @@ -1,19 +0,0 @@ -/* Set release version - * See https://www.python-ldap.org/ for details. */ - -#include "common.h" - -#define _STR(x) #x -#define STR(x) _STR(x) - -static char version_str[] = STR(LDAPMODULE_VERSION); - -void -LDAPinit_version( PyObject* d ) -{ - PyObject *version; - - version = PyString_FromString(version_str); - PyDict_SetItemString( d, "__version__", version ); - Py_DECREF(version); -} diff --git a/Modules/version.h b/Modules/version.h deleted file mode 100644 index 4d76b34..0000000 --- a/Modules/version.h +++ /dev/null @@ -1,11 +0,0 @@ -/* Set release version - * See https://www.python-ldap.org/ for details. */ - -#ifndef __h_version_ -#define __h_version_ - - -#include "common.h" -extern void LDAPinit_version( PyObject* d ); - -#endif /* __h_version_ */ diff --git a/Tests/t_cext.py b/Tests/t_cext.py index 6add784..cac661e 100644 --- a/Tests/t_cext.py +++ b/Tests/t_cext.py @@ -26,7 +26,7 @@ class TestLdapCExtension(SlapdTestCase): @classmethod def setUpClass(cls): - SlapdTestCase.setUpClass() + super(TestLdapCExtension, cls).setUpClass() # add two initial objects after server was started and is still empty suffix_dc = cls.server.suffix.split(',')[0][3:] cls.server._log.debug( @@ -192,7 +192,7 @@ def test_anon_rootdse_search(self): l = self._open_conn(bind=False) # see if we can get the rootdse with anon search (without prior bind) m = l.search_ext( - "", + '', _ldap.SCOPE_BASE, '(objectClass=*)', ['objectClass', 'namingContexts'], diff --git a/Tests/t_cidict.py b/Tests/t_cidict.py index 3f9e8e4..c8812f2 100644 --- a/Tests/t_cidict.py +++ b/Tests/t_cidict.py @@ -39,8 +39,9 @@ def test_cidict(self): cix_items.sort() self.assertEqual(cix_items, [('AbCDeF',123), ('xYZ',987)]) del cix["abcdEF"] - self.assertEqual(cix._keys.has_key("abcdef"), False) - self.assertEqual(cix._keys.has_key("AbCDef"), False) + self.assertEqual("abcdef" in cix, False) + self.assertEqual("AbCDef" in cix._keys, False) + self.assertEqual(cix.has_key("abcdef"), False) if __name__ == '__main__': diff --git a/Tests/t_ldap_dn.py b/Tests/t_ldap_dn.py index 043cc70..97274f3 100644 --- a/Tests/t_ldap_dn.py +++ b/Tests/t_ldap_dn.py @@ -34,5 +34,219 @@ def test_is_dn(self): True ) + def test_escape_dn_chars(self): + """ + test function escape_dn_chars() + """ + self.assertEqual(ldap.dn.escape_dn_chars('foobar'), 'foobar') + self.assertEqual(ldap.dn.escape_dn_chars('foo,bar'), 'foo\\,bar') + self.assertEqual(ldap.dn.escape_dn_chars('foo=bar'), 'foo\\=bar') + self.assertEqual(ldap.dn.escape_dn_chars('foo#bar'), 'foo#bar') + self.assertEqual(ldap.dn.escape_dn_chars('#foobar'), '\\#foobar') + self.assertEqual(ldap.dn.escape_dn_chars('foo bar'), 'foo bar') + self.assertEqual(ldap.dn.escape_dn_chars(' foobar'), '\\ foobar') + + def test_str2dn(self): + """ + test function str2dn() + """ + self.assertEqual(ldap.dn.str2dn(''), []) + self.assertEqual( + ldap.dn.str2dn('uid=test42,ou=Testing,dc=example,dc=com'), + [ + [('uid', 'test42', 1)], + [('ou', 'Testing', 1)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ] + ) + self.assertEqual( + ldap.dn.str2dn('uid=test42+uidNumber=42,ou=Testing,dc=example,dc=com'), + [ + [('uid', 'test42', 1), ('uidNumber', '42', 1) ], + [('ou', 'Testing', 1)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ] + ) + self.assertEqual( + ldap.dn.str2dn('uid=test42,ou=Testing,dc=example,dc=com', flags=0), + [ + [('uid', 'test42', 1)], + [('ou', 'Testing', 1)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ] + ) + self.assertEqual( + ldap.dn.str2dn('uid=test42; ou=Testing; dc=example; dc=com', flags=ldap.DN_FORMAT_LDAPV2), + [ + [('uid', 'test42', 1)], + [('ou', 'Testing', 1)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ] + ) + self.assertEqual( + ldap.dn.str2dn('uid=test\\, 42,ou=Testing,dc=example,dc=com', flags=0), + [ + [('uid', 'test, 42', 1)], + [('ou', 'Testing', 1)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ] + ) + 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), + [ + [('cn', '\xc3\xa4\xc3\xb6\xc3\xbc\xc3\x84\xc3\x96\xc3\x9c\xc3\x9f', 4)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ] + ) + 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)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ] + ) + + def test_dn2str(self): + """ + test function dn2str() + """ + self.assertEqual(ldap.dn.str2dn(''), []) + self.assertEqual( + ldap.dn.dn2str([ + [('uid', 'test42', 1)], + [('ou', 'Testing', 1)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ]), + 'uid=test42,ou=Testing,dc=example,dc=com', + ) + self.assertEqual( + ldap.dn.dn2str([ + [('uid', 'test42', 1)], + [('ou', 'Testing', 1)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ]), + 'uid=test42,ou=Testing,dc=example,dc=com' + ) + self.assertEqual( + ldap.dn.dn2str([ + [('uid', 'test42', 1), ('uidNumber', '42', 1)], + [('ou', 'Testing', 1)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ]), + 'uid=test42+uidNumber=42,ou=Testing,dc=example,dc=com' + ) + self.assertEqual( + ldap.dn.dn2str([ + [('uid', 'test, 42', 1)], + [('ou', 'Testing', 1)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ]), + 'uid=test\\, 42,ou=Testing,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)], + [('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)], + [('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' + ) + + def test_explode_dn(self): + """ + test function explode_dn() + """ + self.assertEqual(ldap.dn.explode_dn(''), []) + self.assertEqual( + ldap.dn.explode_dn('uid=test42,ou=Testing,dc=example,dc=com'), + ['uid=test42', 'ou=Testing', 'dc=example', 'dc=com'] + ) + self.assertEqual( + ldap.dn.explode_dn('uid=test42,ou=Testing,dc=example,dc=com', flags=0), + ['uid=test42', 'ou=Testing', 'dc=example', 'dc=com'] + ) + self.assertEqual( + ldap.dn.explode_dn('uid=test42; ou=Testing; dc=example; dc=com', flags=ldap.DN_FORMAT_LDAPV2), + ['uid=test42', 'ou=Testing', 'dc=example', 'dc=com'] + ) + self.assertEqual( + ldap.dn.explode_dn('uid=test42,ou=Testing,dc=example,dc=com', notypes=True), + ['test42', 'Testing', 'example', 'com'] + ) + self.assertEqual( + ldap.dn.explode_dn('uid=test\\, 42,ou=Testing,dc=example,dc=com', flags=0), + ['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'] + ) + 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'] + ) + + def test_explode_rdn(self): + """ + test function explode_rdn() + """ + self.assertEqual(ldap.dn.explode_rdn(''), []) + self.assertEqual( + ldap.dn.explode_rdn('uid=test42'), + ['uid=test42'] + ) + self.assertEqual( + ldap.dn.explode_rdn('uid=test42', notypes=False, flags=0), + ['uid=test42'] + ) + self.assertEqual( + ldap.dn.explode_rdn('uid=test42', notypes=0, flags=0), + ['uid=test42'] + ) + self.assertEqual( + ldap.dn.explode_rdn('uid=test42+uidNumber=42', flags=0), + ['uid=test42', 'uidNumber=42'] + ) + self.assertEqual( + ldap.dn.explode_rdn('uid=test42', notypes=True), + ['test42'] + ) + self.assertEqual( + ldap.dn.explode_rdn('uid=test42', notypes=1), + ['test42'] + ) + self.assertEqual( + ldap.dn.explode_rdn('uid=test\\+ 42', flags=0), + ['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'] + ) + 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'] + ) + + if __name__ == '__main__': unittest.main() diff --git a/Tests/t_ldap_syncrepl.py b/Tests/t_ldap_syncrepl.py new file mode 100644 index 0000000..8f39c67 --- /dev/null +++ b/Tests/t_ldap_syncrepl.py @@ -0,0 +1,467 @@ +# -*- coding: utf-8 -*- +""" +Automatic tests for python-ldap's module ldap.syncrepl + +See http://www.python-ldap.org/ for details. +""" + + +import os +import unittest +import shelve + +from slapdtest import SlapdObject, SlapdTestCase + +# Switch off processing .ldaprc or ldap.conf before importing _ldap +os.environ['LDAPNOINIT'] = '1' + +import ldap +from ldap.ldapobject import SimpleLDAPObject +from ldap.syncrepl import SyncreplConsumer + +# a template string for generating simple slapd.conf file +SLAPD_CONF_PROVIDER_TEMPLATE = r""" +serverID %(serverid)s +moduleload back_%(database)s +moduleload syncprov +include "%(schema_prefix)s/core.schema" +loglevel %(loglevel)s +allow bind_v2 + +authz-regexp + "gidnumber=%(root_gid)s\\+uidnumber=%(root_uid)s,cn=peercred,cn=external,cn=auth" + "%(rootdn)s" + +database %(database)s +directory "%(directory)s" +suffix "%(suffix)s" +rootdn "%(rootdn)s" +rootpw "%(rootpw)s" +overlay syncprov +syncprov-checkpoint 100 10 +syncprov-sessionlog 100 +index objectclass,entryCSN,entryUUID eq +""" + +# Define initial data load, both as an LDIF and as a dictionary. +LDIF_TEMPLATE = """dn: %(suffix)s +objectClass: dcObject +objectClass: organization +dc: %(dc)s +o: %(dc)s + +dn: %(rootdn)s +objectClass: applicationProcess +objectClass: simpleSecurityObject +cn: %(rootcn)s +userPassword: %(rootpw)s + +dn: cn=Foo1,%(suffix)s +objectClass: organizationalRole +cn: Foo1 + +dn: cn=Foo2,%(suffix)s +objectClass: organizationalRole +cn: Foo2 + +dn: cn=Foo3,%(suffix)s +objectClass: organizationalRole +cn: Foo3 + +dn: ou=Container,%(suffix)s +objectClass: organizationalUnit +ou: Container + +dn: cn=Foo4,ou=Container,%(suffix)s +objectClass: organizationalRole +cn: Foo4 + +""" + +# 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'] + }, + 'cn=Foo2,dc=slapd-test,dc=python-ldap,dc=org': { + 'objectClass': ['organizationalRole'], + 'cn': ['Foo2'] + }, + 'cn=Foo4,ou=Container,dc=slapd-test,dc=python-ldap,dc=org': { + 'objectClass': ['organizationalRole'], + 'cn': ['Foo4'] + }, + 'cn=Manager,dc=slapd-test,dc=python-ldap,dc=org': { + 'objectClass': ['applicationProcess', 'simpleSecurityObject'], + 'userPassword': ['password'], + 'cn': ['Manager'] + }, + 'cn=Foo3,dc=slapd-test,dc=python-ldap,dc=org': { + 'objectClass': ['organizationalRole'], + 'cn': ['Foo3'] + }, + 'cn=Foo1,dc=slapd-test,dc=python-ldap,dc=org': { + 'objectClass': ['organizationalRole'], + 'cn': ['Foo1'] + }, + 'dc=slapd-test,dc=python-ldap,dc=org': { + 'objectClass': ['dcObject', 'organization'], + 'dc': ['slapd-test'], + 'o': ['slapd-test'] + } +} + + +class SyncreplProvider(SlapdObject): + slapd_conf_template = SLAPD_CONF_PROVIDER_TEMPLATE + + +class SyncreplClient(SimpleLDAPObject, SyncreplConsumer): + """ + This is a very simple class to start up the syncrepl search + and handle callbacks that come in. + + Needs to be separate, because once an LDAP client starts a syncrepl + search, it can't be used for anything else. + """ + server_class = SyncreplProvider + + def __init__(self, uri, dn, password, storage=None): + """ + Set up our object by creating a search client, connecting, and binding. + """ + + if storage is not None: + self.data = shelve.open(storage) + self.uuid_dn = shelve.open(storage + 'uuid_dn') + self.dn_attrs = shelve.open(storage + 'dn_attrs') + self.using_shelve = True + else: + self.data = {} + self.uuid_dn = {} + self.dn_attrs = {} + self.using_shelve = False + + self.data['cookie'] = None + self.present = [] + self.refresh_done = False + + SimpleLDAPObject.__init__(self, uri) + self.simple_bind_s(dn, password) + + + def unbind_s(self): + """ + In addition to unbinding from LDAP, we need to close the shelf. + """ + if self.using_shelve is True: + self.data.close() + self.uuid_dn.close() + self.dn_attrs.close() + SimpleLDAPObject.unbind_s(self) + + + def search(self, search_base, search_mode): + """ + Start a syncrepl search operation, given a base DN and search mode. + """ + self.search_id = self.syncrepl_search( + search_base, + ldap.SCOPE_SUBTREE, + mode=search_mode, + filterstr='(objectClass=*)' + ) + + + def cancel(self): + """ + A simple wrapper to call parent class with syncrepl search ID. + """ + SimpleLDAPObject.cancel(self, self.search_id) + + + def poll(self, timeout=None, all=0): + """ + Take the params, add the syncrepl search ID, and call the proper poll. + """ + return self.syncrepl_poll( + self.search_id, + timeout=timeout, + all=all + ) + + + def syncrepl_get_cookie(self): + """ + Pull cookie from storage, if one exists. + """ + return self.data['cookie'] + + + def syncrepl_set_cookie(self, cookie): + """ + Update stored cookie. + """ + self.data['cookie'] = cookie + + + def syncrepl_refreshdone(self): + """ + Just update a variable. + """ + self.refresh_done = True + + + def syncrepl_delete(self, uuids): + """ + Delete the given items from both maps. + """ + for uuid in uuids: + del self.dn_attrs[self.uuid_dn[uuid]] + del self.uuid_dn[uuid] + + + def syncrepl_entry(self, dn, attrs, uuid): + """ + Handles adds and changes (including DN changes). + """ + if uuid in self.uuid_dn: + # Catch changing DNs. + if dn != self.uuid_dn[uuid]: + # Delete data associated with old DN. + del self.dn_attrs[self.uuid_dn[uuid]] + + # Update both maps. + self.uuid_dn[uuid] = dn + self.dn_attrs[dn] = attrs + + + def syncrepl_present(self, uuids, refreshDeletes=False): + """ + The 'present' message from the LDAP server is the most complicated + part of the refresh phase. Suggest looking here for more info: + http://syncrepl-client.readthedocs.io/en/latest/client.html + """ + if (uuids is not None) and (refreshDeletes is False): + self.present.extend(uuids) + + elif (uuids is None) and (refreshDeletes is False): + deleted_uuids = list() + for uuid in self.uuid_dn.keys(): + if uuid not in self.present: + deleted_uuids.append(uuid) + + if len(deleted_uuids) > 0: + self.syncrepl_delete(deleted_uuids) + + elif (uuids is not None) and (refreshDeletes is True): + self.syncrepl_delete(uuids) + + elif (uuids is None) and (refreshDeletes is True): + pass + + +class Test00_Syncrepl(SlapdTestCase): + """ + This is a test of all the basic Syncrepl operations. It covers starting a + search (both types of search), doing the refresh part of the search, + and checking that we got everything that we expected. We also test that + timeouts and cancellation are working properly. + """ + + server_class = SyncreplProvider + ldap_object_class = SimpleLDAPObject + + @classmethod + def setUpClass(cls): + super(Test00_Syncrepl, cls).setUpClass() + # insert some Foo* objects via ldapadd + cls.server.ldapadd( + LDIF_TEMPLATE % { + 'suffix':cls.server.suffix, + 'rootdn':cls.server.root_dn, + 'rootcn':cls.server.root_cn, + 'rootpw':cls.server.root_pw, + 'dc': cls.server.suffix.split(',')[0][3:], + } + ) + + + def setUp(self): + try: + self._ldap_conn + except AttributeError: + # open local LDAP connection + self._ldap_conn = self._open_ldap_conn() + + + def tearDown(self): + self.tester.unbind_s() + + + def test_refreshOnly_search(self): + ''' + Test to see if we can initialize a syncrepl search. + ''' + self.tester = SyncreplClient( + self.server.ldap_uri, + self.server.root_dn, + self.server.root_pw + ) + self.tester.search( + self.server.suffix, + 'refreshOnly' + ) + + + def test_refreshAndPersist_search(self): + self.tester = SyncreplClient( + self.server.ldap_uri, + self.server.root_dn, + self.server.root_pw + ) + self.tester.search( + self.server.suffix, + 'refreshAndPersist' + ) + + + def test_refreshOnly_poll_full(self): + """ + Test doing a full refresh cycle, and check what we got. + """ + self.tester = SyncreplClient( + self.server.ldap_uri, + self.server.root_dn, + self.server.root_pw + ) + self.tester.search( + self.server.suffix, + 'refreshOnly' + ) + poll_result = self.tester.poll( + all=1, + timeout=None + ) + self.assertFalse(poll_result) + self.assertEqual(self.tester.dn_attrs, LDAP_ENTRIES) + + + def test_refreshAndPersist_poll_only(self): + """ + Test the refresh part of refresh-and-persist, and check what we got. + """ + self.tester = SyncreplClient( + self.server.ldap_uri, + self.server.root_dn, + self.server.root_pw + ) + self.tester.search( + self.server.suffix, + 'refreshAndPersist' + ) + + # Make sure to stop the test before going into persist mode. + while self.tester.refresh_done is not True: + poll_result = self.tester.poll( + all=0, + timeout=None + ) + self.assertTrue(poll_result) + + self.assertEqual(self.tester.dn_attrs, LDAP_ENTRIES) + + + def test_refreshAndPersist_timeout(self): + """ + Make sure refreshAndPersist can handle a search with timeouts. + """ + self.tester = SyncreplClient( + self.server.ldap_uri, + self.server.root_dn, + self.server.root_pw + ) + self.tester.search( + self.server.suffix, + 'refreshAndPersist' + ) + + # Run a quick refresh, that shouldn't have any changes. + while self.tester.refresh_done is not True: + poll_result = self.tester.poll( + all=0, + timeout=None + ) + self.assertTrue(poll_result) + + # Again, server data should not have changed. + self.assertEqual(self.tester.dn_attrs, LDAP_ENTRIES) + + # Run a search with timeout. + # Nothing is changing the server, so it shoud timeout. + self.assertRaises( + ldap.TIMEOUT, + self.tester.poll, + all=0, + timeout=1 + ) + + + def test_refreshAndPersist_cancelled(self): + """ + Make sure refreshAndPersist can handle cancelling a syncrepl search. + """ + self.tester = SyncreplClient( + self.server.ldap_uri, + self.server.root_dn, + self.server.root_pw + ) + self.tester.search( + self.server.suffix, + 'refreshAndPersist' + ) + + # Run a quick refresh, that shouldn't have any changes. + while self.tester.refresh_done is not True: + poll_result = self.tester.poll( + all=0, + timeout=None + ) + self.assertTrue(poll_result) + + # Again, server data should not have changed. + self.assertEqual(self.tester.dn_attrs, LDAP_ENTRIES) + + # Request cancellation. + self.tester.cancel() + + # Run another poll, without timeout, but which should cancel out. + self.assertRaises( + ldap.CANCELLED, + self.tester.poll, + all=1, + timeout=None + ) + + # Server data should still be intact. + self.assertEqual(self.tester.dn_attrs, LDAP_ENTRIES) + + + # TODO: + # * Make a new client, with a data store, and close. Then, load a new + # client with the same datastore, and see if the data store loads OK. + # * Make a new client, with a data store, and close. Then, load a new + # client with the same datastore. Delete an entry, and the cookie. + # Start the sync, and everything should sync up OK. + # * Load the refreshOnly client, using existing data. Make a change + # on the server, and the client should pick it up in the refresh phase. + # * Load the refreshAndPersist client, using existing data. Make a change + # on the server, and the client should pick it up in the refresh phase. + # * Load the refreshAndPersist client, using existing data. Let the + # refresh phase complete. Make a change on the server, and the client + # should pick it up during the persist phase. + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/t_ldapobject.py b/Tests/t_ldapobject.py index e03ca1e..196a9f3 100644 --- a/Tests/t_ldapobject.py +++ b/Tests/t_ldapobject.py @@ -7,6 +7,7 @@ import os import unittest +import pickle from slapdtest import SlapdTestCase # Switch off processing .ldaprc or ldap.conf before importing _ldap @@ -56,7 +57,7 @@ """ -class Test01_SimpleLDAPObject(SlapdTestCase): +class Test00_SimpleLDAPObject(SlapdTestCase): """ test LDAP search operations """ @@ -65,7 +66,7 @@ class Test01_SimpleLDAPObject(SlapdTestCase): @classmethod def setUpClass(cls): - SlapdTestCase.setUpClass() + super(Test00_SimpleLDAPObject, cls).setUpClass() # insert some Foo* objects via ldapadd cls.server.ldapadd( LDIF_TEMPLATE % { @@ -84,7 +85,7 @@ def setUp(self): # open local LDAP connection self._ldap_conn = self._open_ldap_conn() - def test_search_subtree(self): + def test001_search_subtree(self): result = self._ldap_conn.search_s( self.server.suffix, ldap.SCOPE_SUBTREE, @@ -114,7 +115,7 @@ def test_search_subtree(self): ] ) - def test_search_onelevel(self): + def test002_search_onelevel(self): result = self._ldap_conn.search_s( self.server.suffix, ldap.SCOPE_ONELEVEL, @@ -140,7 +141,7 @@ def test_search_onelevel(self): ] ) - def test_search_oneattr(self): + def test003_search_oneattr(self): result = self._ldap_conn.search_s( self.server.suffix, ldap.SCOPE_SUBTREE, @@ -153,7 +154,7 @@ def test_search_oneattr(self): [('cn=Foo4,ou=Container,'+self.server.suffix, {'cn': ['Foo4']})] ) - def test_errno107(self): + def test004_errno107(self): l = self.ldap_object_class('ldap://127.0.0.1:42') try: m = l.simple_bind_s("", "") @@ -168,7 +169,7 @@ def test_errno107(self): else: self.fail("expected SERVER_DOWN, got %r" % r) - def test_invalid_credentials(self): + def test005_invalid_credentials(self): l = self.ldap_object_class(self.server.ldap_uri) # search with invalid filter try: @@ -179,7 +180,7 @@ def test_invalid_credentials(self): else: self.fail("expected INVALID_CREDENTIALS, got %r" % r) - def test_sasl_extenal_bind_s(self): + def test006_sasl_extenal_bind_s(self): l = self.ldap_object_class(self.server.ldapi_uri) l.sasl_external_bind_s() self.assertEqual(l.whoami_s(), 'dn:'+self.server.root_dn.lower()) @@ -187,16 +188,23 @@ def test_sasl_extenal_bind_s(self): l = self.ldap_object_class(self.server.ldapi_uri) l.sasl_external_bind_s(authz_id=authz_id) self.assertEqual(l.whoami_s(), authz_id.lower()) + + def test007_timeout(self): + l = self.ldap_object_class(self.server.ldap_uri) + m = l.search_ext(self.server.suffix, ldap.SCOPE_SUBTREE, '(objectClass=*)') + l.abandon(m) + with self.assertRaises(ldap.TIMEOUT): + result = l.result(m, timeout=0.001) -class Test02_ReconnectLDAPObject(Test01_SimpleLDAPObject): +class Test01_ReconnectLDAPObject(Test00_SimpleLDAPObject): """ test ReconnectLDAPObject by restarting slapd """ ldap_object_class = ReconnectLDAPObject - def test_reconnect_sasl_external(self): + def test101_reconnect_sasl_external(self): l = self.ldap_object_class(self.server.ldapi_uri) l.sasl_external_bind_s() authz_id = l.whoami_s() @@ -204,7 +212,7 @@ def test_reconnect_sasl_external(self): self.server.restart() self.assertEqual(l.whoami_s(), authz_id) - def test_reconnect_simple_bind(self): + def test102_reconnect_simple_bind(self): l = self.ldap_object_class(self.server.ldapi_uri) bind_dn = 'cn=user1,'+self.server.suffix l.simple_bind_s(bind_dn, 'user1_pw') @@ -212,6 +220,41 @@ def test_reconnect_simple_bind(self): self.server.restart() self.assertEqual(l.whoami_s(), 'dn:'+bind_dn) + def test103_reconnect_get_state(self): + l1 = self.ldap_object_class(self.server.ldapi_uri) + bind_dn = 'cn=user1,'+self.server.suffix + l1.simple_bind_s(bind_dn, 'user1_pw') + self.assertEqual(l1.whoami_s(), 'dn:'+bind_dn) + self.assertEqual( + l1.__getstate__(), + { + '_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, + }, + ) + + def test104_reconnect_restore(self): + l1 = self.ldap_object_class(self.server.ldapi_uri) + bind_dn = 'cn=user1,'+self.server.suffix + l1.simple_bind_s(bind_dn, 'user1_pw') + self.assertEqual(l1.whoami_s(), 'dn:'+bind_dn) + l1_state = pickle.dumps(l1) + del l1 + l2 = pickle.loads(l1_state) + self.assertEqual(l2.whoami_s(), 'dn:'+bind_dn) + if __name__ == '__main__': unittest.main() diff --git a/Tests/t_ldif.py b/Tests/t_ldif.py index 4898f76..3da213f 100644 --- a/Tests/t_ldif.py +++ b/Tests/t_ldif.py @@ -248,6 +248,30 @@ def test_binary2(self): ] ) + def test_big_binary(self): + self.check_records( + """ + dn: cn=x,cn=y,cn=z + attrib:: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + = + + """, + [ + ( + 'cn=x,cn=y,cn=z', + {'attrib': [500*b'\0']}, + ), + ] + ) + def test_unicode(self): self.check_records( """ @@ -641,7 +665,7 @@ def test_bad_change_records(self): except ValueError, value_error: pass else: - self.fail("should have raised ValueError: %r" % ldif_str) + self.fail("should have raised ValueError: %r" % bad_ldif_string) def test_mod_increment(self): self.check_records( diff --git a/setup.py b/setup.py index 962d763..6bbf595 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ from distutils.core import setup, Extension from ConfigParser import ConfigParser -import sys,os,string,time +import sys,os,time sys.path.insert(0, os.path.join(os.getcwd(), 'Lib/ldap')) import pkginfo @@ -36,15 +36,14 @@ class OpenLDAP2: if cfg.has_section('_ldap'): for name in dir(LDAP_CLASS): if cfg.has_option('_ldap', name): - print name + ': ' + cfg.get('_ldap', name) - setattr(LDAP_CLASS, name, string.split(cfg.get('_ldap', name))) + setattr(LDAP_CLASS, name, cfg.get('_ldap', name).split()) for i in range(len(LDAP_CLASS.defines)): LDAP_CLASS.defines[i]=((LDAP_CLASS.defines[i],None)) for i in range(len(LDAP_CLASS.extra_files)): - destdir, origfiles = string.split(LDAP_CLASS.extra_files[i], ':') - origfileslist = string.split(origfiles, ',') + destdir, origfiles = LDAP_CLASS.extra_files[i].split(':') + origfileslist = origfiles.split(',') LDAP_CLASS.extra_files[i]=(destdir, origfileslist) #-- Let distutils/setuptools do the rest @@ -110,7 +109,6 @@ class OpenLDAP2: 'Modules/functions.c', 'Modules/ldapmodule.c', 'Modules/message.c', - 'Modules/version.c', 'Modules/options.c', 'Modules/berval.c', ], @@ -125,7 +123,11 @@ class OpenLDAP2: ('ldap_r' in LDAP_CLASS.libs or 'oldap_r' in LDAP_CLASS.libs)*[('HAVE_LIBLDAP_R',None)] + \ ('sasl' in LDAP_CLASS.libs or 'sasl2' in LDAP_CLASS.libs or 'libsasl' in LDAP_CLASS.libs)*[('HAVE_SASL',None)] + \ ('ssl' in LDAP_CLASS.libs and 'crypto' in LDAP_CLASS.libs)*[('HAVE_TLS',None)] + \ - [('LDAPMODULE_VERSION', pkginfo.__version__)] + [ + ('LDAPMODULE_VERSION', pkginfo.__version__), + ('LDAPMODULE_AUTHOR', pkginfo.__author__), + ('LDAPMODULE_LICENSE', pkginfo.__license__), + ] ), ], #-- Python "stand alone" modules @@ -146,6 +148,7 @@ class OpenLDAP2: 'ldap.controls.sessiontrack', 'ldap.controls.simple', 'ldap.controls.sss', + 'ldap.controls.vlv', 'ldap.cidict', 'ldap.dn', 'ldap.extop',