From 75a765f82fed2e3f418a1152b3af02f7d1e2629e Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Mon, 30 Oct 2023 09:19:41 +0100 Subject: [PATCH 01/42] refactor: Merge all header files Merge all header files except `constants_generated.h` into a single header file `pythonldap.h`. A single header file makes it far easier to port python-ldap to heap types and module state for Per-Interpreter GIL. `pythonldap.h` uses new macros `PYLDAP_FUNC` and `PYLDAP_DATA` to declare functions and data, which are used across C files. Remove unused macro `streq`. See: https://github.com/python-ldap/python-ldap/issues/540 Signed-off-by: Christian Heimes --- Makefile | 3 +- Modules/LDAPObject.c | 8 +-- Modules/LDAPObject.h | 38 ------------ Modules/berval.c | 3 +- Modules/berval.h | 11 ---- Modules/common.c | 2 +- Modules/common.h | 68 --------------------- Modules/constants.c | 4 +- Modules/constants.h | 24 -------- Modules/functions.c | 7 +-- Modules/functions.h | 9 --- Modules/ldapcontrol.c | 6 +- Modules/ldapcontrol.h | 13 ---- Modules/ldapmodule.c | 7 +-- Modules/message.c | 6 +- Modules/message.h | 11 ---- Modules/options.c | 7 +-- Modules/options.h | 7 --- Modules/pythonldap.h | 137 ++++++++++++++++++++++++++++++++++++++++++ setup.py | 9 +-- 20 files changed, 149 insertions(+), 231 deletions(-) delete mode 100644 Modules/LDAPObject.h delete mode 100644 Modules/berval.h delete mode 100644 Modules/common.h delete mode 100644 Modules/constants.h delete mode 100644 Modules/functions.h delete mode 100644 Modules/ldapcontrol.h delete mode 100644 Modules/message.h delete mode 100644 Modules/options.h create mode 100644 Modules/pythonldap.h diff --git a/Makefile b/Makefile index 577ba883..2b52ddf5 100644 --- a/Makefile +++ b/Makefile @@ -89,7 +89,8 @@ valgrind: build $(PYTHON_SUPP) autoformat: indent black indent: - indent Modules/*.c Modules/*.h + indent Modules/*.c + indent -npsl Modules/pythonldap.h rm -f Modules/*.c~ Modules/*.h~ black: diff --git a/Modules/LDAPObject.c b/Modules/LDAPObject.c index da18d575..eaf831bd 100644 --- a/Modules/LDAPObject.c +++ b/Modules/LDAPObject.c @@ -1,16 +1,10 @@ /* See https://www.python-ldap.org/ for details. */ -#include "common.h" +#include "pythonldap.h" #include "patchlevel.h" #include #include -#include "constants.h" -#include "LDAPObject.h" -#include "ldapcontrol.h" -#include "message.h" -#include "berval.h" -#include "options.h" #ifdef HAVE_SASL #include diff --git a/Modules/LDAPObject.h b/Modules/LDAPObject.h deleted file mode 100644 index 4af0b382..00000000 --- a/Modules/LDAPObject.h +++ /dev/null @@ -1,38 +0,0 @@ -/* See https://www.python-ldap.org/ for details. */ - -#ifndef __h_LDAPObject -#define __h_LDAPObject - -#include "common.h" - -typedef struct { - PyObject_HEAD LDAP *ldap; - PyThreadState *_save; /* for thread saving on referrals */ - int valid; -} LDAPObject; - -extern PyTypeObject LDAP_Type; - -#define LDAPObject_Check(v) (Py_TYPE(v) == &LDAP_Type) - -extern LDAPObject *newLDAPObject(LDAP *); - -/* macros to allow thread saving in the context of an LDAP connection */ - -#define LDAP_BEGIN_ALLOW_THREADS( l ) \ - { \ - LDAPObject *lo = (l); \ - if (lo->_save != NULL) \ - Py_FatalError( "saving thread twice?" ); \ - lo->_save = PyEval_SaveThread(); \ - } - -#define LDAP_END_ALLOW_THREADS( l ) \ - { \ - LDAPObject *lo = (l); \ - PyThreadState *_save = lo->_save; \ - lo->_save = NULL; \ - PyEval_RestoreThread( _save ); \ - } - -#endif /* __h_LDAPObject */ diff --git a/Modules/berval.c b/Modules/berval.c index 6917baef..39cc98a8 100644 --- a/Modules/berval.c +++ b/Modules/berval.c @@ -1,7 +1,6 @@ /* See https://www.python-ldap.org/ for details. */ -#include "common.h" -#include "berval.h" +#include "pythonldap.h" /* * Copies out the data from a berval, and returns it as a new Python object, diff --git a/Modules/berval.h b/Modules/berval.h deleted file mode 100644 index 9c427240..00000000 --- a/Modules/berval.h +++ /dev/null @@ -1,11 +0,0 @@ -/* See https://www.python-ldap.org/ for details. */ - -#ifndef __h_berval -#define __h_berval - -#include "common.h" - -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.c b/Modules/common.c index 9d7001c0..4cfee744 100644 --- a/Modules/common.c +++ b/Modules/common.c @@ -1,7 +1,7 @@ /* Miscellaneous common routines * See https://www.python-ldap.org/ for details. */ -#include "common.h" +#include "pythonldap.h" /* dynamically add the methods into the module dictionary d */ diff --git a/Modules/common.h b/Modules/common.h deleted file mode 100644 index bc554c85..00000000 --- a/Modules/common.h +++ /dev/null @@ -1,68 +0,0 @@ -/* common utility macros - * See https://www.python-ldap.org/ for details. */ - -#ifndef __h_common -#define __h_common - -#define PY_SSIZE_T_CLEAN - -#include "Python.h" - -#if defined(HAVE_CONFIG_H) -#include "config.h" -#endif - -#include -#include -#include - -#if LDAP_VENDOR_VERSION < 20400 -#error Current python-ldap requires OpenLDAP 2.4.x -#endif - -#if LDAP_VENDOR_VERSION >= 20448 - /* openldap.h with ldap_init_fd() was introduced in 2.4.48 - * see https://bugs.openldap.org/show_bug.cgi?id=8671 - */ -#define HAVE_LDAP_INIT_FD 1 -#include -#elif (defined(__APPLE__) && (LDAP_VENDOR_VERSION == 20428)) -/* macOS system libldap 2.4.28 does not have ldap_init_fd symbol */ -#undef HAVE_LDAP_INIT_FD -#else - /* ldap_init_fd() has been around for a very long time - * SSSD has been defining the function for a while, so it's probably OK. - */ -#define HAVE_LDAP_INIT_FD 1 -#define LDAP_PROTO_TCP 1 -#define LDAP_PROTO_UDP 2 -#define LDAP_PROTO_IPC 3 -extern int ldap_init_fd(ber_socket_t fd, int proto, LDAP_CONST char *url, - LDAP **ldp); -#endif - -#if defined(MS_WINDOWS) -#include -#else /* unix */ -#include -#include -#include -#endif - -#include -#define streq( a, b ) \ - ( (*(a)==*(b)) && 0==strcmp(a,b) ) - -extern PyObject *LDAPerror_TypeError(const char *, PyObject *); - -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 8d6f63b0..b70db245 100644 --- a/Modules/constants.c +++ b/Modules/constants.c @@ -1,9 +1,7 @@ /* constants defined for LDAP * See https://www.python-ldap.org/ for details. */ -#include "common.h" -#include "constants.h" -#include "ldapcontrol.h" +#include "pythonldap.h" /* the base exception class */ diff --git a/Modules/constants.h b/Modules/constants.h deleted file mode 100644 index 7b9ce53e..00000000 --- a/Modules/constants.h +++ /dev/null @@ -1,24 +0,0 @@ -/* See https://www.python-ldap.org/ for details. */ - -#ifndef __h_constants_ -#define __h_constants_ - -#include "common.h" - -extern int LDAPinit_constants(PyObject *m); -extern PyObject *LDAPconstant(int); - -extern PyObject *LDAPexception_class; -extern PyObject *LDAPerror(LDAP *); -extern PyObject *LDAPraise_for_message(LDAP *, LDAPMessage *m); -PyObject *LDAPerr(int errnum); - -#ifndef LDAP_CONTROL_PAGE_OID -#define LDAP_CONTROL_PAGE_OID "1.2.840.113556.1.4.319" -#endif /* !LDAP_CONTROL_PAGE_OID */ - -#ifndef LDAP_CONTROL_VALUESRETURNFILTER -#define LDAP_CONTROL_VALUESRETURNFILTER "1.2.826.0.1.3344810.2.3" /* RFC 3876 */ -#endif /* !LDAP_CONTROL_VALUESRETURNFILTER */ - -#endif /* __h_constants_ */ diff --git a/Modules/functions.c b/Modules/functions.c index b811708f..f7d9cf37 100644 --- a/Modules/functions.c +++ b/Modules/functions.c @@ -1,11 +1,6 @@ /* See https://www.python-ldap.org/ for details. */ -#include "common.h" -#include "functions.h" -#include "LDAPObject.h" -#include "berval.h" -#include "constants.h" -#include "options.h" +#include "pythonldap.h" /* ldap_initialize */ diff --git a/Modules/functions.h b/Modules/functions.h deleted file mode 100644 index 2aef9740..00000000 --- a/Modules/functions.h +++ /dev/null @@ -1,9 +0,0 @@ -/* See https://www.python-ldap.org/ for details. */ - -#ifndef __h_functions_ -#define __h_functions_ - -#include "common.h" -extern void LDAPinit_functions(PyObject *); - -#endif /* __h_functions_ */ diff --git a/Modules/ldapcontrol.c b/Modules/ldapcontrol.c index e287e9a3..4a37b614 100644 --- a/Modules/ldapcontrol.c +++ b/Modules/ldapcontrol.c @@ -1,10 +1,6 @@ /* See https://www.python-ldap.org/ for details. */ -#include "common.h" -#include "LDAPObject.h" -#include "ldapcontrol.h" -#include "berval.h" -#include "constants.h" +#include "pythonldap.h" /* Prints to stdout the contents of an array of LDAPControl objects */ diff --git a/Modules/ldapcontrol.h b/Modules/ldapcontrol.h deleted file mode 100644 index 74cae423..00000000 --- a/Modules/ldapcontrol.h +++ /dev/null @@ -1,13 +0,0 @@ -/* See https://www.python-ldap.org/ for details. */ - -#ifndef __h_ldapcontrol -#define __h_ldapcontrol - -#include "common.h" - -void LDAPinit_control(PyObject *d); -void LDAPControl_List_DEL(LDAPControl **); -int LDAPControls_from_object(PyObject *, LDAPControl ***); -PyObject *LDAPControls_to_List(LDAPControl **ldcs); - -#endif /* __h_ldapcontrol */ diff --git a/Modules/ldapmodule.c b/Modules/ldapmodule.c index 34d5a24c..8562337b 100644 --- a/Modules/ldapmodule.c +++ b/Modules/ldapmodule.c @@ -1,11 +1,6 @@ /* See https://www.python-ldap.org/ for details. */ -#include "common.h" -#include "constants.h" -#include "functions.h" -#include "ldapcontrol.h" - -#include "LDAPObject.h" +#include "pythonldap.h" #if PY_MAJOR_VERSION >= 3 PyMODINIT_FUNC PyInit__ldap(void); diff --git a/Modules/message.c b/Modules/message.c index 22aa313c..f1403237 100644 --- a/Modules/message.c +++ b/Modules/message.c @@ -1,10 +1,6 @@ /* See https://www.python-ldap.org/ for details. */ -#include "common.h" -#include "message.h" -#include "berval.h" -#include "ldapcontrol.h" -#include "constants.h" +#include "pythonldap.h" /* * Converts an LDAP message into a Python structure. diff --git a/Modules/message.h b/Modules/message.h deleted file mode 100644 index ed73f32c..00000000 --- a/Modules/message.h +++ /dev/null @@ -1,11 +0,0 @@ -/* See https://www.python-ldap.org/ for details. */ - -#ifndef __h_message -#define __h_message - -#include "common.h" - -extern PyObject *LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, - int add_intermediates); - -#endif /* __h_message_ */ diff --git a/Modules/options.c b/Modules/options.c index a621f81a..df55ed05 100644 --- a/Modules/options.c +++ b/Modules/options.c @@ -1,11 +1,6 @@ /* See https://www.python-ldap.org/ for details. */ -#include "common.h" -#include "constants.h" -#include "LDAPObject.h" -#include "ldapcontrol.h" -#include "options.h" -#include "berval.h" +#include "pythonldap.h" void set_timeval_from_double(struct timeval *tv, double d) diff --git a/Modules/options.h b/Modules/options.h deleted file mode 100644 index fd6a5ce2..00000000 --- a/Modules/options.h +++ /dev/null @@ -1,7 +0,0 @@ -/* 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); -PyObject *LDAP_get_option(LDAPObject *self, int option); - -void set_timeval_from_double(struct timeval *tv, double d); diff --git a/Modules/pythonldap.h b/Modules/pythonldap.h new file mode 100644 index 00000000..ae6a1269 --- /dev/null +++ b/Modules/pythonldap.h @@ -0,0 +1,137 @@ +/* common utility macros + * See https://www.python-ldap.org/ for details. */ + +#ifndef pythonldap_h +#define pythonldap_h + +/* *** common *** */ +#define PY_SSIZE_T_CLEAN + +#include "Python.h" + +#if defined(HAVE_CONFIG_H) +#include "config.h" +#endif + +#include +#include +#include + +/* Py2/3 compatibility */ +#if PY_VERSION_HEX >= 0x03000000 +/* In Python 3, alias PyInt to PyLong */ +#define PyInt_FromLong PyLong_FromLong +#endif + +#if LDAP_VENDOR_VERSION < 20400 +#error Current python-ldap requires OpenLDAP 2.4.x +#endif + +#if LDAP_VENDOR_VERSION >= 20448 + /* openldap.h with ldap_init_fd() was introduced in 2.4.48 + * see https://bugs.openldap.org/show_bug.cgi?id=8671 + */ +#define HAVE_LDAP_INIT_FD 1 +#include +#elif (defined(__APPLE__) && (LDAP_VENDOR_VERSION == 20428)) +/* macOS system libldap 2.4.28 does not have ldap_init_fd symbol */ +#undef HAVE_LDAP_INIT_FD +#else + /* ldap_init_fd() has been around for a very long time + * SSSD has been defining the function for a while, so it's probably OK. + */ +#define HAVE_LDAP_INIT_FD 1 +#define LDAP_PROTO_TCP 1 +#define LDAP_PROTO_UDP 2 +#define LDAP_PROTO_IPC 3 +LDAP_F(int) ldap_init_fd(ber_socket_t fd, int proto, LDAP_CONST char *url, + LDAP **ldp); +#endif + +#if defined(MS_WINDOWS) +#include +#else /* unix */ +#include +#include +#include +#endif + +#define PYLDAP_FUNC(rtype) rtype +#define PYLDAP_DATA(rtype) extern rtype + +PYLDAP_FUNC(PyObject *) LDAPerror_TypeError(const char *, PyObject *); + +PYLDAP_FUNC(void) LDAPadd_methods(PyObject *d, PyMethodDef *methods); + +#define PyNone_Check(o) ((o) == Py_None) + +/* *** berval *** */ +PYLDAP_FUNC(PyObject *) LDAPberval_to_object(const struct berval *bv); +PYLDAP_FUNC(PyObject *) LDAPberval_to_unicode_object(const struct berval *bv); + +/* *** constants *** */ +PYLDAP_FUNC(int) LDAPinit_constants(PyObject *m); + +PYLDAP_DATA(PyObject *) LDAPexception_class; +PYLDAP_FUNC(PyObject *) LDAPerror(LDAP *); +PYLDAP_FUNC(PyObject *) LDAPraise_for_message(LDAP *, LDAPMessage *m); +PYLDAP_FUNC(PyObject *) LDAPerr(int errnum); + +#ifndef LDAP_CONTROL_PAGE_OID +#define LDAP_CONTROL_PAGE_OID "1.2.840.113556.1.4.319" +#endif /* !LDAP_CONTROL_PAGE_OID */ + +#ifndef LDAP_CONTROL_VALUESRETURNFILTER +#define LDAP_CONTROL_VALUESRETURNFILTER "1.2.826.0.1.3344810.2.3" /* RFC 3876 */ +#endif /* !LDAP_CONTROL_VALUESRETURNFILTER */ + +/* *** functions *** */ +PYLDAP_FUNC(void) LDAPinit_functions(PyObject *); + +/* *** ldapcontrol *** */ +PYLDAP_FUNC(void) LDAPinit_control(PyObject *d); +PYLDAP_FUNC(void) LDAPControl_List_DEL(LDAPControl **); +PYLDAP_FUNC(int) LDAPControls_from_object(PyObject *, LDAPControl ***); +PYLDAP_FUNC(PyObject *) LDAPControls_to_List(LDAPControl **ldcs); + +/* *** ldapobject *** */ +typedef struct { + PyObject_HEAD LDAP *ldap; + PyThreadState *_save; /* for thread saving on referrals */ + int valid; +} LDAPObject; + +PYLDAP_DATA(PyTypeObject) LDAP_Type; +PYLDAP_FUNC(LDAPObject *) newLDAPObject(LDAP *); + +/* macros to allow thread saving in the context of an LDAP connection */ + +#define LDAP_BEGIN_ALLOW_THREADS( l ) \ + { \ + LDAPObject *lo = (l); \ + if (lo->_save != NULL) \ + Py_FatalError( "saving thread twice?" ); \ + lo->_save = PyEval_SaveThread(); \ + } + +#define LDAP_END_ALLOW_THREADS( l ) \ + { \ + LDAPObject *lo = (l); \ + PyThreadState *_save = lo->_save; \ + lo->_save = NULL; \ + PyEval_RestoreThread( _save ); \ + } + +/* *** messages *** */ +PYLDAP_FUNC(PyObject *) +LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, + int add_intermediates); + +/* *** options *** */ +PYLDAP_FUNC(int) LDAP_optionval_by_name(const char *name); +PYLDAP_FUNC(int) LDAP_set_option(LDAPObject *self, int option, + PyObject *value); +PYLDAP_FUNC(PyObject *) LDAP_get_option(LDAPObject *self, int option); +PYLDAP_FUNC(void) set_timeval_from_double(struct timeval *tv, double d); + +#endif /* pythonldap_h */ diff --git a/setup.py b/setup.py index 6da3f491..dbf66a04 100644 --- a/setup.py +++ b/setup.py @@ -117,15 +117,8 @@ class OpenLDAP2: 'Modules/berval.c', ], depends = [ - 'Modules/LDAPObject.h', - 'Modules/berval.h', - 'Modules/common.h', + 'Modules/pythonldap.h', 'Modules/constants_generated.h', - 'Modules/constants.h', - 'Modules/functions.h', - 'Modules/ldapcontrol.h', - 'Modules/message.h', - 'Modules/options.h', ], libraries = LDAP_CLASS.libs, include_dirs = ['Modules'] + LDAP_CLASS.include_dirs, From f48101097eb08f902daed5b8e7836c54cf44b0f4 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Mon, 30 Oct 2023 09:40:12 +0100 Subject: [PATCH 02/42] refactor: Remove Python 2 vestiges The C code had a few version checks for Python 2. python-ldap requires Python >= 3.6. Signed-off-by: Christian Heimes --- Modules/LDAPObject.c | 51 +++++++++++--------------------------------- Modules/constants.c | 8 +++---- Modules/ldapmodule.c | 45 ++++++++++---------------------------- Modules/options.c | 2 +- Modules/pythonldap.h | 6 ------ 5 files changed, 28 insertions(+), 84 deletions(-) diff --git a/Modules/LDAPObject.c b/Modules/LDAPObject.c index eaf831bd..71fac73e 100644 --- a/Modules/LDAPObject.c +++ b/Modules/LDAPObject.c @@ -270,13 +270,8 @@ attrs_from_List(PyObject *attrlist, char ***attrsp) if (attrlist == Py_None) { /* None means a NULL attrlist */ -#if PY_MAJOR_VERSION == 2 - } - else if (PyBytes_Check(attrlist)) { -#else } else if (PyUnicode_Check(attrlist)) { -#endif /* caught by John Benninghoff */ LDAPerror_TypeError ("attrs_from_List(): expected *list* of strings, not a string", @@ -287,11 +282,7 @@ attrs_from_List(PyObject *attrlist, char ***attrsp) PyObject *item = NULL; Py_ssize_t i, len, strlen; -#if PY_MAJOR_VERSION >= 3 const char *str; -#else - char *str; -#endif seq = PySequence_Fast(attrlist, "expected list of strings or None"); if (seq == NULL) @@ -309,24 +300,12 @@ attrs_from_List(PyObject *attrlist, char ***attrsp) item = PySequence_Fast_GET_ITEM(seq, i); if (item == NULL) goto error; -#if PY_MAJOR_VERSION == 2 - /* Encoded in Python to UTF-8 */ - if (!PyBytes_Check(item)) { - LDAPerror_TypeError - ("attrs_from_List(): expected bytes in list", item); - goto error; - } - if (PyBytes_AsStringAndSize(item, &str, &strlen) == -1) { - goto error; - } -#else if (!PyUnicode_Check(item)) { LDAPerror_TypeError ("attrs_from_List(): expected string in list", item); goto error; } str = PyUnicode_AsUTF8AndSize(item, &strlen); -#endif /* Make a copy. PyBytes_AsString* / PyUnicode_AsUTF8* return * internal values that must be treated like const char. Python * 3.7 actually returns a const char. @@ -515,7 +494,7 @@ l_ldap_add_ext(LDAPObject *self, PyObject *args) if (ldaperror != LDAP_SUCCESS) return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } /* ldap_simple_bind */ @@ -566,7 +545,7 @@ l_ldap_simple_bind(LDAPObject *self, PyObject *args) if (ldaperror != LDAP_SUCCESS) return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } #ifdef HAVE_SASL @@ -724,7 +703,7 @@ l_ldap_sasl_bind_s(LDAPObject *self, PyObject *args) } else if (ldaperror != LDAP_SUCCESS) return LDAPerror(self->ldap); - return PyInt_FromLong(ldaperror); + return PyLong_FromLong(ldaperror); } static PyObject * @@ -751,15 +730,9 @@ l_ldap_sasl_interactive_bind_s(LDAPObject *self, PyObject *args) * unsigned int, we need to use the "I" flag if we're running Python 2.3+ and a * "i" otherwise. */ -#if (PY_MAJOR_VERSION == 2) && (PY_MINOR_VERSION < 3) - if (!PyArg_ParseTuple - (args, "sOOOi:sasl_interactive_bind_s", &who, &SASLObject, - &serverctrls, &clientctrls, &sasl_flags)) -#else if (!PyArg_ParseTuple (args, "sOOOI:sasl_interactive_bind_s", &who, &SASLObject, &serverctrls, &clientctrls, &sasl_flags)) -#endif return NULL; if (not_valid(self)) @@ -803,7 +776,7 @@ l_ldap_sasl_interactive_bind_s(LDAPObject *self, PyObject *args) if (msgid != LDAP_SUCCESS) return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } #endif @@ -852,7 +825,7 @@ l_ldap_cancel(LDAPObject *self, PyObject *args) if (ldaperror != LDAP_SUCCESS) return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } #endif @@ -906,7 +879,7 @@ l_ldap_compare_ext(LDAPObject *self, PyObject *args) if (ldaperror != LDAP_SUCCESS) return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } /* ldap_delete_ext */ @@ -952,7 +925,7 @@ l_ldap_delete_ext(LDAPObject *self, PyObject *args) if (ldaperror != LDAP_SUCCESS) return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } /* ldap_modify_ext */ @@ -1009,7 +982,7 @@ l_ldap_modify_ext(LDAPObject *self, PyObject *args) if (ldaperror != LDAP_SUCCESS) return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } /* ldap_rename */ @@ -1059,7 +1032,7 @@ l_ldap_rename(LDAPObject *self, PyObject *args) if (ldaperror != LDAP_SUCCESS) return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } /* ldap_result4 */ @@ -1275,7 +1248,7 @@ l_ldap_search_ext(LDAPObject *self, PyObject *args) if (ldaperror != LDAP_SUCCESS) return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } /* ldap_whoami_s (available since OpenLDAP 2.1.13) */ @@ -1445,7 +1418,7 @@ l_ldap_passwd(LDAPObject *self, PyObject *args) if (ldaperror != LDAP_SUCCESS) return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } /* ldap_extended_operation */ @@ -1496,7 +1469,7 @@ l_ldap_extended_operation(LDAPObject *self, PyObject *args) if (ldaperror != LDAP_SUCCESS) return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } /* methods */ diff --git a/Modules/constants.c b/Modules/constants.c index b70db245..f0a0da94 100644 --- a/Modules/constants.c +++ b/Modules/constants.c @@ -105,20 +105,20 @@ LDAPraise_for_message(LDAP *l, LDAPMessage *m) } if (msgtype > 0) { - pyresult = PyInt_FromLong(msgtype); + pyresult = PyLong_FromLong(msgtype); if (pyresult) PyDict_SetItemString(info, "msgtype", pyresult); Py_XDECREF(pyresult); } if (msgid >= 0) { - pyresult = PyInt_FromLong(msgid); + pyresult = PyLong_FromLong(msgid); if (pyresult) PyDict_SetItemString(info, "msgid", pyresult); Py_XDECREF(pyresult); } - pyresult = PyInt_FromLong(errnum); + pyresult = PyLong_FromLong(errnum); if (pyresult) PyDict_SetItemString(info, "result", pyresult); Py_XDECREF(pyresult); @@ -129,7 +129,7 @@ LDAPraise_for_message(LDAP *l, LDAPMessage *m) Py_XDECREF(str); if (myerrno != 0) { - pyerrno = PyInt_FromLong(myerrno); + pyerrno = PyLong_FromLong(myerrno); if (pyerrno) PyDict_SetItemString(info, "errno", pyerrno); Py_XDECREF(pyerrno); diff --git a/Modules/ldapmodule.c b/Modules/ldapmodule.c index 8562337b..cb3f58fb 100644 --- a/Modules/ldapmodule.c +++ b/Modules/ldapmodule.c @@ -2,12 +2,6 @@ #include "pythonldap.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) @@ -28,27 +22,24 @@ static PyMethodDef methods[] = { {NULL, NULL} }; +static struct PyModuleDef ldap_moduledef = { + PyModuleDef_HEAD_INIT, + "_ldap", /* m_name */ + "", /* m_doc */ + -1, /* m_size */ + methods, /* m_methods */ +}; + /* module initialisation */ -/* Common initialization code */ -PyObject * -init_ldap_module(void) +PyMODINIT_FUNC +PyInit__ldap() { PyObject *m, *d; /* 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 + /* Initialize LDAP class */ if (PyType_Ready(&LDAP_Type) < 0) { Py_DECREF(m); @@ -73,17 +64,3 @@ init_ldap_module(void) 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/options.c b/Modules/options.c index df55ed05..4577b075 100644 --- a/Modules/options.c +++ b/Modules/options.c @@ -368,7 +368,7 @@ LDAP_get_option(LDAPObject *self, int option) res = LDAP_int_get_option(self, option, &intval); if (res != LDAP_OPT_SUCCESS) return option_error(res, "ldap_get_option"); - return PyInt_FromLong(intval); + return PyLong_FromLong(intval); #ifdef LDAP_OPT_TCP_USER_TIMEOUT case LDAP_OPT_TCP_USER_TIMEOUT: diff --git a/Modules/pythonldap.h b/Modules/pythonldap.h index ae6a1269..7703af5e 100644 --- a/Modules/pythonldap.h +++ b/Modules/pythonldap.h @@ -17,12 +17,6 @@ #include #include -/* Py2/3 compatibility */ -#if PY_VERSION_HEX >= 0x03000000 -/* In Python 3, alias PyInt to PyLong */ -#define PyInt_FromLong PyLong_FromLong -#endif - #if LDAP_VENDOR_VERSION < 20400 #error Current python-ldap requires OpenLDAP 2.4.x #endif From 16a3a3c0175aa51ee22efda118dbaeb3881cec3c Mon Sep 17 00:00:00 2001 From: Simon Pichugin Date: Fri, 17 Nov 2023 12:29:34 -0800 Subject: [PATCH 03/42] Prepare a new release --- CHANGES | 17 +++++++++++++++++ Lib/ldap/pkginfo.py | 2 +- Lib/ldapurl.py | 2 +- Lib/ldif.py | 2 +- Lib/slapdtest/__init__.py | 2 +- 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 500fa1e7..0491b6ef 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,20 @@ +Released 3.4.4 2022-11-17 + +Fixes: +* Reconnect race condition in ReconnectLDAPObject is now fixed +* Socket ownership is now claimed once we've passed it to libldap +* LDAP_set_option string formats are now compatible with Python 3.12 + +Doc/ +* Security Policy was created +* Broken article links are fixed now +* Bring Conscious Language improvements + +Infrastructure: +* Add testing and document support for Python 3.10, 3.11, and 3.12 + + +---------------------------------------------------------------- Released 3.4.3 2022-09-15 This is a minor release to bring back the removed OPT_X_TLS option. diff --git a/Lib/ldap/pkginfo.py b/Lib/ldap/pkginfo.py index 026e9101..18ead66c 100644 --- a/Lib/ldap/pkginfo.py +++ b/Lib/ldap/pkginfo.py @@ -1,6 +1,6 @@ """ meta attributes for packaging which does not import any dependencies """ -__version__ = '3.4.3' +__version__ = '3.4.4' __author__ = 'python-ldap project' __license__ = 'Python style' diff --git a/Lib/ldapurl.py b/Lib/ldapurl.py index 964076d3..b4dfd890 100644 --- a/Lib/ldapurl.py +++ b/Lib/ldapurl.py @@ -4,7 +4,7 @@ See https://www.python-ldap.org/ for details. """ -__version__ = '3.4.3' +__version__ = '3.4.4' __all__ = [ # constants diff --git a/Lib/ldif.py b/Lib/ldif.py index ae1d643d..fa41321c 100644 --- a/Lib/ldif.py +++ b/Lib/ldif.py @@ -3,7 +3,7 @@ See https://www.python-ldap.org/ for details. """ -__version__ = '3.4.3' +__version__ = '3.4.4' __all__ = [ # constants diff --git a/Lib/slapdtest/__init__.py b/Lib/slapdtest/__init__.py index 7ab7d2bd..7c410180 100644 --- a/Lib/slapdtest/__init__.py +++ b/Lib/slapdtest/__init__.py @@ -4,7 +4,7 @@ See https://www.python-ldap.org/ for details. """ -__version__ = '3.4.3' +__version__ = '3.4.4' from slapdtest._slapdtest import SlapdObject, SlapdTestCase, SysLogHandler from slapdtest._slapdtest import requires_ldapi, requires_sasl, requires_tls From ac5d051ec3bc3dee700ebbe3c1ba68f54fc39bc5 Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Mon, 27 Nov 2023 21:52:26 +0200 Subject: [PATCH 04/42] Update link to unofficial Windows binary builds (#524) --- Doc/installing.rst | 4 ++-- Doc/spelling_wordlist.txt | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Doc/installing.rst b/Doc/installing.rst index e4518c11..6627ce5d 100644 --- a/Doc/installing.rst +++ b/Doc/installing.rst @@ -63,8 +63,8 @@ to get up to date information which versions are available. Windows ------- -Unofficial packages for Windows are available on -`Christoph Gohlke's page `_. +Unofficial binary builds for Windows are provided by Christoph Gohlke, available at +`python-ldap-build `_. `FreeBSD `_ diff --git a/Doc/spelling_wordlist.txt b/Doc/spelling_wordlist.txt index e6c2aedd..8cdd9f16 100644 --- a/Doc/spelling_wordlist.txt +++ b/Doc/spelling_wordlist.txt @@ -25,6 +25,7 @@ changeNumber changesOnly changeType changeTypes +Christoph cidict clientctrls conf @@ -56,6 +57,7 @@ filterstr filterStr formatOID func +Gohlke GPG Heimdal hostport From 10985e38902d170402acdfe6fabece5db70437cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl=20Bourgault?= Date: Thu, 22 Feb 2024 08:19:11 +0100 Subject: [PATCH 05/42] docs: add missing negation in contributing.rst (#552) Current description contains a sentence that miss a negative form, contradicting previous sentence and leaving the reader with an ambiguity. --- Doc/contributing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/contributing.rst b/Doc/contributing.rst index bbaab491..6ef8a5a8 100644 --- a/Doc/contributing.rst +++ b/Doc/contributing.rst @@ -19,7 +19,7 @@ Communication Always keep in mind that python-ldap is developed and maintained by volunteers. We're happy to share our work, and to work with you to make the library better, -but (until you pay someone), there's obligation to provide assistance. +but (until you pay someone), there's no obligation to provide assistance. So, keep it friendly, respectful, and supportive! From 06fdd3dc4d3da82afa8d445cb8f1e8842c824236 Mon Sep 17 00:00:00 2001 From: RafaelWO <38643099+RafaelWO@users.noreply.github.com> Date: Tue, 27 Feb 2024 18:58:54 +0100 Subject: [PATCH 06/42] Update docs on installation requirements (#548) Separate building and testing requirements for Debian --- Doc/installing.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Doc/installing.rst b/Doc/installing.rst index 6627ce5d..03e7a295 100644 --- a/Doc/installing.rst +++ b/Doc/installing.rst @@ -143,10 +143,15 @@ Packages for building:: Debian ------ +Packages for building:: + + # apt-get install build-essential ldap-utils \ + libldap2-dev libsasl2-dev + Packages for building and testing:: - # apt-get install build-essential python3-dev \ - libldap2-dev libsasl2-dev slapd ldap-utils tox \ + # apt-get install build-essential ldap-utils \ + libldap2-dev libsasl2-dev slapd python3-dev tox \ lcov valgrind .. note:: From a58282adbc6b1f5f9755458227e6bb8667b72f6b Mon Sep 17 00:00:00 2001 From: Quanah Gibson-Mount Date: Mon, 22 Apr 2024 22:18:09 +0000 Subject: [PATCH 07/42] Fixes #565 - Use name values instead of raw decimal Use the name values for result types in syncrepl.py rather than the raw decimal values. Signed-off-by: Quanah Gibson-Mount --- Lib/ldap/syncrepl.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/ldap/syncrepl.py b/Lib/ldap/syncrepl.py index 1708b468..fd0c1285 100644 --- a/Lib/ldap/syncrepl.py +++ b/Lib/ldap/syncrepl.py @@ -12,6 +12,7 @@ from ldap.pkginfo import __version__, __author__, __license__ from ldap.controls import RequestControl, ResponseControl, KNOWN_RESPONSE_CONTROLS +from ldap import RES_SEARCH_RESULT, RES_SEARCH_ENTRY, RES_INTERMEDIATE __all__ = [ 'SyncreplConsumer', @@ -407,7 +408,7 @@ def syncrepl_poll(self, msgid=-1, timeout=None, all=0): all=0, ) - if type == 101: + if type == RES_SEARCH_RESULT: # search result. This marks the end of a refreshOnly session. # look for a SyncDone control, save the cookie, and if necessary # delete non-present entries. @@ -420,7 +421,7 @@ def syncrepl_poll(self, msgid=-1, timeout=None, all=0): return False - elif type == 100: + elif type == RES_SEARCH_ENTRY: # search entry with associated SyncState control for m in msg: dn, attrs, ctrls = m @@ -439,7 +440,7 @@ def syncrepl_poll(self, msgid=-1, timeout=None, all=0): self.syncrepl_set_cookie(c.cookie) break - elif type == 121: + elif type == RES_INTERMEDIATE: # Intermediate message. If it is a SyncInfoMessage, parse it for m in msg: rname, resp, ctrls = m From 8b27db3d605974fc308b3b52ec464e354dcbafa0 Mon Sep 17 00:00:00 2001 From: Simon Pichugin Date: Thu, 10 Oct 2024 22:55:29 -0700 Subject: [PATCH 08/42] Add support for Python 3.13 (#576) Update GitHub Actions. Explicitly install python3-setuptools for Tox env runs on Fedora. --- .github/workflows/ci.yml | 4 +++- .github/workflows/tox-fedora.yml | 5 +++-- setup.py | 1 + tox.ini | 5 ++++- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37843f31..2f835d76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,9 @@ jobs: - "3.10" - "3.11" - "3.12" + - "3.13" - "pypy3.9" + - "pypy3.10" image: - "ubuntu-22.04" include: @@ -43,7 +45,7 @@ jobs: - name: Disable AppArmor run: sudo aa-disable /usr/sbin/slapd - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true diff --git a/.github/workflows/tox-fedora.yml b/.github/workflows/tox-fedora.yml index b86303fe..4c4c18f0 100644 --- a/.github/workflows/tox-fedora.yml +++ b/.github/workflows/tox-fedora.yml @@ -9,7 +9,7 @@ jobs: tox_test: name: Tox env "${{matrix.tox_env}}" on Fedora steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Run Tox tests uses: fedora-python/tox-github-action@main with: @@ -17,7 +17,7 @@ jobs: dnf_install: > @c-development openldap-devel python3-devel openldap-servers openldap-clients lcov clang-analyzer valgrind - enchant + enchant python3-setuptools strategy: matrix: tox_env: @@ -28,6 +28,7 @@ jobs: - py310 - py311 - py312 + - py313 - c90-py36 - c90-py37 - py3-nosasltls diff --git a/setup.py b/setup.py index dbf66a04..8e7963a1 100644 --- a/setup.py +++ b/setup.py @@ -93,6 +93,7 @@ class OpenLDAP2: 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', # Note: when updating Python versions, also change tox.ini and .github/workflows/* 'Topic :: Database', diff --git a/tox.ini b/tox.ini index beade024..22752067 100644 --- a/tox.ini +++ b/tox.ini @@ -17,10 +17,12 @@ python = 3.10: py310 3.11: py311 3.12: py312 + 3.13: py313 pypy3.9: pypy3.9 + pypy3.10: pypy3.10 [testenv] -deps = +deps = setuptools passenv = WITH_GCOV # - Enable BytesWarning # - Turn all warnings into exceptions. @@ -98,6 +100,7 @@ deps = markdown sphinx sphinxcontrib-spelling + setuptools commands = {envpython} setup.py check --restructuredtext --metadata --strict {envpython} -m markdown README -f {envtmpdir}/README.html From 326f8708ca6d6fff36893a38f38b645bed9c5e6f Mon Sep 17 00:00:00 2001 From: Simon Pichugin Date: Fri, 11 Oct 2024 08:16:27 -0700 Subject: [PATCH 09/42] Deprecate Pagure repo (#579) --- Doc/contributing.rst | 7 +------ Doc/spelling_wordlist.txt | 1 - 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Doc/contributing.rst b/Doc/contributing.rst index 6ef8a5a8..de63a2e3 100644 --- a/Doc/contributing.rst +++ b/Doc/contributing.rst @@ -72,9 +72,6 @@ If you're used to open-source Python development with Git, here's the gist: .. _the bug tracker: https://github.com/python-ldap/python-ldap/issues .. _tox: https://tox.readthedocs.io/en/latest/ -Or, if you prefer to avoid closed-source services: - -* ``git clone https://pagure.io/python-ldap`` * Send bug reports and patches to the mailing list. * Run tests with `tox`_; ignore Python interpreters you don't have locally. * Read the documentation directly at `Read the Docs`_. @@ -203,8 +200,6 @@ remember: * Consider making the summary line suitable for the CHANGES document, and starting it with a prefix like ``Lib:`` or ``Tests:``. -* Push to Pagure as well. - If you have good reason to break the “rules”, go ahead and break them, but mention why. @@ -224,7 +219,7 @@ If you are tasked with releasing python-ldap, remember to: * Run ``python setup.py sdist``, and smoke-test the resulting package (install in a clean virtual environment, import ``ldap``). * Create GPG-signed Git tag: ``git tag -s python-ldap-{version}``. - Push it to GitHub and Pagure. + Push it to GitHub. * Release the ``sdist`` on PyPI. * Announce the release on the mailing list. Mention the Git hash. diff --git a/Doc/spelling_wordlist.txt b/Doc/spelling_wordlist.txt index 8cdd9f16..e2150d9a 100644 --- a/Doc/spelling_wordlist.txt +++ b/Doc/spelling_wordlist.txt @@ -100,7 +100,6 @@ oc oid oids OpenLDAP -Pagure postalAddress pre previousDN From e628f1582b46269ba6c31e1e3ee8a952cb32bb7d Mon Sep 17 00:00:00 2001 From: Simon Pichugin Date: Tue, 28 Jan 2025 14:26:58 -0800 Subject: [PATCH 10/42] Deprecate EOL Python 3.6, 3.7, 3.8 versions --- .github/workflows/ci.yml | 9 ++------- .github/workflows/tox-fedora.yml | 7 +------ pyproject.toml | 4 ---- setup.py | 5 +---- tox.ini | 7 ++----- 5 files changed, 6 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f835d76..06d0b2ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,7 @@ --- name: CI -on: +on: push: pull_request: schedule: @@ -20,8 +20,6 @@ jobs: fail-fast: false matrix: python-version: - - "3.7" - - "3.8" - "3.9" - "3.10" - "3.11" @@ -29,11 +27,8 @@ jobs: - "3.13" - "pypy3.9" - "pypy3.10" - image: + image: - "ubuntu-22.04" - include: - - python-version: "3.6" - image: "ubuntu-20.04" steps: - name: Checkout uses: "actions/checkout@v4" diff --git a/.github/workflows/tox-fedora.yml b/.github/workflows/tox-fedora.yml index 4c4c18f0..bc6f45c5 100644 --- a/.github/workflows/tox-fedora.yml +++ b/.github/workflows/tox-fedora.yml @@ -21,20 +21,15 @@ jobs: strategy: matrix: tox_env: - - py36 - - py37 - - py38 - py39 - py310 - py311 - py312 - py313 - - c90-py36 - - c90-py37 - py3-nosasltls - py3-trace - pypy3 - doc # Use GitHub's Linux Docker host - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 diff --git a/pyproject.toml b/pyproject.toml index dda8dbc1..75f7c06a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,3 @@ -[tool.black] -line-length = 88 -target-version = ['py36', 'py37', 'py38'] - [tool.isort] line_length=88 known_first_party=['ldap', '_ldap', 'ldapurl', 'ldif', 'slapdtest'] diff --git a/setup.py b/setup.py index 8e7963a1..ea7364cd 100644 --- a/setup.py +++ b/setup.py @@ -86,9 +86,6 @@ class OpenLDAP2: 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', @@ -160,6 +157,6 @@ class OpenLDAP2: 'pyasn1_modules >= 0.1.5', ], zip_safe=False, - python_requires='>=3.6', + python_requires='>=3.9', test_suite = 'Tests', ) diff --git a/tox.ini b/tox.ini index 22752067..0b284a4e 100644 --- a/tox.ini +++ b/tox.ini @@ -5,15 +5,12 @@ [tox] # Note: when updating Python versions, also change setup.py and .github/worlflows/* -envlist = py{36,37,38,39,310,311,312},c90-py{36,37},py3-nosasltls,doc,py3-trace,pypy3.9 +envlist = py{39,310,311,312},py3-nosasltls,doc,py3-trace,pypy3.9 minver = 1.8 [gh-actions] python = - 3.6: py36 - 3.7: py37 - 3.8: py38, doc, py3-nosasltls - 3.9: py39, py3-trace + 3.9: py39, py3-trace, doc, py3-nosasltls 3.10: py310 3.11: py311 3.12: py312 From 30b24d5673095fecc73f652cfa41efc6208f9d72 Mon Sep 17 00:00:00 2001 From: Jiayu Hu <86949267+JennyHo5@users.noreply.github.com> Date: Sun, 13 Jul 2025 01:25:03 -0400 Subject: [PATCH 11/42] Fix typo (#584) The cookie is saved with key `cookie` intead of `ldap_cookie` in the `self.__data` dict --- Demo/pyasn1/syncrepl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Demo/pyasn1/syncrepl.py b/Demo/pyasn1/syncrepl.py index f1f24e19..754b237a 100644 --- a/Demo/pyasn1/syncrepl.py +++ b/Demo/pyasn1/syncrepl.py @@ -76,7 +76,7 @@ def syncrepl_entry(self, dn, attributes, uuid): 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: + if 'cookie' in self.__data: self.perform_application_sync(dn, attributes, previous_attributes) def syncrepl_delete(self,uuids): @@ -98,7 +98,7 @@ def syncrepl_present(self,uuids,refreshDeletes=False): deletedEntries = [ uuid for uuid in self.__data.keys() - if uuid not in self.__presentUUIDs and uuid != 'ldap_cookie' + if uuid not in self.__presentUUIDs and uuid != 'cookie' ] self.syncrepl_delete( deletedEntries ) # Phase is now completed, reset the list From 2880183370f99ead12d25ac4683c958555aa1f8b Mon Sep 17 00:00:00 2001 From: Florian Best Date: Fri, 1 Aug 2025 01:57:17 +0200 Subject: [PATCH 12/42] docs(ldapobject): fix typos in docstring (#590) --- Doc/reference/ldap.rst | 2 +- Lib/ldap/ldapobject.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/reference/ldap.rst b/Doc/reference/ldap.rst index d059dfa4..4911b7c7 100644 --- a/Doc/reference/ldap.rst +++ b/Doc/reference/ldap.rst @@ -1364,7 +1364,7 @@ and wait for and return with the server's result, or with This synchronous method implements the LDAP "Who Am I?" extended operation. - It is useful for finding out to find out which identity + It is useful for finding out which identity is assumed by the LDAP server after a SASL bind. .. seealso:: diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index 7a9c17f6..290d92b3 100644 --- a/Lib/ldap/ldapobject.py +++ b/Lib/ldap/ldapobject.py @@ -521,7 +521,7 @@ def result(self,msgid=ldap.RES_ANY,all=1,timeout=None): The method returns a tuple of the form (result_type, result_data). The result_type is one of the constants RES_*. - See search() for a description of the search result's + See search_ext() for a description of the search result's result_data, otherwise the result_data is normally meaningless. The result() method will block for timeout seconds, or @@ -588,7 +588,7 @@ def search_ext(self,base,scope,filterstr=None,attrlist=None,attrsonly=0,serverct values are stored in a list as dictionary value. The DN in dn is extracted using the underlying ldap_get_dn(), - which may raise an exception of the DN is malformed. + which may raise an exception if the DN is malformed. If attrsonly is non-zero, the values of attrs will be meaningless (they are not transmitted in the result). From 7af31254dcb22a58686cb140ac320af9a6f967fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20Kie=C3=9F?= Date: Wed, 6 Aug 2025 21:05:38 +0200 Subject: [PATCH 13/42] Allow passing None as uri argument to ldap.initialize() (#465) OpenLDAP allows passing NULL to ldap_initialize(). In this case the default URI from ldap.conf will be used. Allow passing None as uri argument to ldap.initialize(). --- Doc/reference/ldap.rst | 3 ++- Modules/functions.c | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Doc/reference/ldap.rst b/Doc/reference/ldap.rst index 4911b7c7..a642f579 100644 --- a/Doc/reference/ldap.rst +++ b/Doc/reference/ldap.rst @@ -38,7 +38,8 @@ This module defines the following functions: The *uri* parameter may be a comma- or whitespace-separated list of URIs containing only the schema, the host, and the port fields. Note that when using multiple URIs you cannot determine to which URI your client - gets connected. + gets connected. If *uri* is :py:const:`None`, the default URIs from + ``ldap.conf`` or :py:const:`OPT_URI` global option will be used. If *fileno* parameter is given then the file descriptor will be used to connect to an LDAP server. The *fileno* must either be a socket file diff --git a/Modules/functions.c b/Modules/functions.c index f7d9cf37..9a977ff7 100644 --- a/Modules/functions.c +++ b/Modules/functions.c @@ -12,7 +12,7 @@ l_ldap_initialize(PyObject *unused, PyObject *args) int ret; PyThreadState *save; - if (!PyArg_ParseTuple(args, "s:initialize", &uri)) + if (!PyArg_ParseTuple(args, "z:initialize", &uri)) return NULL; save = PyEval_SaveThread(); From 6e9f305ab1ac5d1d83968a7c67f2663df681e7ab Mon Sep 17 00:00:00 2001 From: Florian Best Date: Wed, 6 Aug 2025 22:21:59 +0200 Subject: [PATCH 14/42] Allow to pickle a unbound connection (#588) --- Lib/ldap/ldapobject.py | 8 ++++++-- Tests/t_ldapobject.py | 10 +++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index 290d92b3..e8afb726 100644 --- a/Lib/ldap/ldapobject.py +++ b/Lib/ldap/ldapobject.py @@ -877,7 +877,10 @@ def __getstate__(self): 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] + if self._last_bind is None: + state['_last_bind'] = None + else: + state['_last_bind'] = self._last_bind[0].__name__, self._last_bind[1], self._last_bind[2] return state def __setstate__(self,d): @@ -888,7 +891,8 @@ def __setstate__(self,d): else: d.setdefault('bytes_strictness', 'warn') self.__dict__.update(d) - self._last_bind = getattr(SimpleLDAPObject, self._last_bind[0]), self._last_bind[1], self._last_bind[2] + if self._last_bind is not None: + 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))) # XXX cannot pickle file, use default trace file diff --git a/Tests/t_ldapobject.py b/Tests/t_ldapobject.py index ada5f990..ecf163b7 100644 --- a/Tests/t_ldapobject.py +++ b/Tests/t_ldapobject.py @@ -617,12 +617,20 @@ def test103_reconnect_get_state(self): ) def test104_reconnect_restore(self): - l1 = self.ldap_object_class(self.server.ldap_uri) + l0 = self.ldap_object_class(self.server.ldap_uri) + + l0_state = pickle.dumps(l0) + del l0 + l1 = pickle.loads(l0_state) + self.assertEqual(l1.whoami_s(), '') + 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) From e4bc42c962a047ba4281ccacdad8254cd89b6f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Mon, 20 Nov 2023 10:42:38 +0000 Subject: [PATCH 15/42] Add readthedocs configuration file Running without one has apparently been deprecated since September 2023. --- .readthedocs.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..91fb6028 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,22 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: Doc/conf.py + +# We recommend specifying your dependencies to enable reproducible builds: +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: Doc/requirements.txt From d466f394d0c1effc86d76640141d4a99ab98dea7 Mon Sep 17 00:00:00 2001 From: Florian Best Date: Fri, 18 Mar 2022 13:13:26 +0100 Subject: [PATCH 16/42] test(ldap.dn): Add test cases for ldap.dn.dn2str() with different format flags --- Tests/t_ldap_dn.py | 203 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 199 insertions(+), 4 deletions(-) diff --git a/Tests/t_ldap_dn.py b/Tests/t_ldap_dn.py index 86d36403..3d6a950d 100644 --- a/Tests/t_ldap_dn.py +++ b/Tests/t_ldap_dn.py @@ -57,6 +57,7 @@ def test_str2dn(self): test function str2dn() """ self.assertEqual(ldap.dn.str2dn(''), []) + self.assertEqual(ldap.dn.str2dn(None), []) self.assertEqual( ldap.dn.str2dn('uid=test42,ou=Testing,dc=example,dc=com'), [ @@ -105,7 +106,7 @@ def test_str2dn(self): self.assertEqual( ldap.dn.str2dn('cn=äöüÄÖÜß,dc=example,dc=com', flags=0), [ - [('cn', 'äöüÄÖÜß', 4)], + [('cn', 'äöüÄÖÜß', ldap.AVA_NONPRINTABLE)], [('dc', 'example', 1)], [('dc', 'com', 1)] ] @@ -113,7 +114,16 @@ 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', 'äöüÄÖÜß', 4)], + [('cn', 'äöüÄÖÜß', ldap.AVA_NONPRINTABLE)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ] + ) + self.assertEqual( + ldap.dn.str2dn('/dc=com/dc=example/ou=Testing/uid=test42', flags=ldap.DN_FORMAT_DCE), + [ + [('uid', 'test42', 1)], + [('ou', 'Testing', 1)], [('dc', 'example', 1)], [('dc', 'com', 1)] ] @@ -123,7 +133,7 @@ def test_dn2str(self): """ test function dn2str() """ - self.assertEqual(ldap.dn.str2dn(''), []) + self.assertEqual(ldap.dn.dn2str([]), '') self.assertEqual( ldap.dn.dn2str([ [('uid', 'test42', 1)], @@ -162,12 +172,197 @@ def test_dn2str(self): ) self.assertEqual( ldap.dn.dn2str([ - [('cn', 'äöüÄÖÜß', 4)], + [('uid', 'test, 42', 1)], + [('ou', 'Testing', 1)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ], ldap.DN_FORMAT_LDAPV3), + r'uid=test\2C 42,ou=Testing,dc=example,dc=com' + ) + self.assertEqual( + ldap.dn.dn2str([ + [('cn', 'äöüÄÖÜß', ldap.AVA_NONPRINTABLE)], [('dc', 'example', 1)], [('dc', 'com', 1)] ]), 'cn=äöüÄÖÜß,dc=example,dc=com' ) + self.assertEqual( + ldap.dn.dn2str([ + [('cn', 'äöüÄÖÜß', ldap.AVA_NONPRINTABLE)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ], ldap.DN_FORMAT_LDAPV3), + r'cn=\C3\A4\C3\B6\C3\BC\C3\84\C3\96\C3\9C\C3\9F,dc=example,dc=com' + ) + self.assertEqual( + ldap.dn.dn2str([ + [('uid', 'test42', 1), ('cn', 'test42', 1)], + [('ou', 'Testing', 1)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ], ldap.DN_FORMAT_AD_CANONICAL), + 'example.com/Testing/test42,test42' + ) + self.assertEqual( + ldap.dn.dn2str([ + [('uid', 'test42', 1), ('cn', 'test42', 1)], + [('ou', 'Testing', 1)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ], ldap.DN_FORMAT_UFN), + 'test42 + test42, Testing, example.com' + ) + self.assertEqual( + ldap.dn.dn2str([ + [('uid', 'test42', 1), ('cn', 'test42', 1)], + [('ou', 'Testing', 1)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ], ldap.DN_FORMAT_DCE), + '/dc=com/dc=example/ou=Testing/uid=test42,cn=test42' + ) + + self.assertEqual( + ldap.dn.dn2str([ + [('cn', 'äöüÄÖÜß', ldap.AVA_BINARY)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ], ldap.DN_FORMAT_LDAPV3), + 'cn=#C3A4C3B6C3BCC384C396C39CC39F,dc=example,dc=com' + ) + self.assertEqual( + ldap.dn.dn2str([ + [('cn', 'äöüÄÖÜß', ldap.AVA_NULL)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ], ldap.DN_FORMAT_LDAPV3), + r'cn=\C3\A4\C3\B6\C3\BC\C3\84\C3\96\C3\9C\C3\9F,dc=example,dc=com' + ) + self.assertEqual( + ldap.dn.dn2str([ + [('cn', 'äöüÄÖÜß', ldap.AVA_STRING)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ], ldap.DN_FORMAT_LDAPV3), + r'cn=\C3\A4\C3\B6\C3\BC\C3\84\C3\96\C3\9C\C3\9F,dc=example,dc=com' + ) + self.assertEqual( + ldap.dn.dn2str([ + [('cn', 'äöüÄÖÜß', ldap.AVA_NONPRINTABLE)], + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ], ldap.DN_FORMAT_LDAPV3), + r'cn=\C3\A4\C3\B6\C3\BC\C3\84\C3\96\C3\9C\C3\9F,dc=example,dc=com' + ) + + def test_dn_various_lengths(self): + base = [ + [('dc', 'example', 1)], + [('dc', 'com', 1)] + ] + + test_lengths = [1, 10, 100, 500] + for n in test_lengths: + rdn_prefix = [ + [('ou', f'unit{i}', 1)] for i in range(n) + ] + full_dn = rdn_prefix + base + full_dn.insert(0, [('uid', f'user{n}', 1)]) + + result = ldap.dn.dn2str(full_dn, ldap.DN_FORMAT_LDAPV3) + + self.assertTrue(result.startswith(f'uid=user{n},')) + self.assertTrue(result.endswith(',dc=example,dc=com')) + self.assertEqual(result.count(','), n + 2) + + def test_dn2str_errors(self): + """ + test error handling of function dn2str() + """ + with self.assertRaises(RuntimeError): + ldap.dn.dn2str([[('uid', 'test42', 1)]], 142) + + DN_FORMAT_LBER = 0xf0 + with self.assertRaises(RuntimeError): + ldap.dn.dn2str([ + [('dc', 'com', 1)] + ], DN_FORMAT_LBER) + + ldap_format = ldap.DN_FORMAT_LDAPV3 + + with self.assertRaises(TypeError): + ldap.dn.dn2str(None) + + with self.assertRaises(TypeError): + ldap.dn.dn2str(None, ldap_format) + + with self.assertRaises(TypeError): + ldap.dn.dn2str([1], ldap_format) + + with self.assertRaises(TypeError): + ldap.dn.dn2str([[1]], ldap_format) + + with self.assertRaises(TypeError): + ldap.dn.dn2str([[('uid', 'test42', '1')]], ldap_format) + + with self.assertRaises(TypeError): + ldap.dn.dn2str([[('uid', 'test42', 1.0)]], ldap_format) + + with self.assertRaises(TypeError): + ldap.dn.dn2str([[['uid', 'test42', 1]]], ldap_format) + + with self.assertRaises(TypeError): + ldap.dn.dn2str([ + [('uid', 'test42', 1), ('cn', 'test42', 1)], + [('ou', 'Testing', 1)], + [('dc', 'example', '1')], + [('dc', 'com', 1)] + ], ldap_format), + + with self.assertRaises(TypeError): + ldap.dn.dn2str([ + [('ou', 'Testing', 1)], + [('dc', 'example', 1)], + [('uid', 'test42', 1), ('cn', 'test42', '1')], + [('dc', 'com', 1)] + ], ldap_format), + + with self.assertRaises(TypeError): + ldap.dn.dn2str([ + [('dc', 'example', 1)], + [('dc', 'com', None)], + ], ldap_format), + + with self.assertRaises(TypeError): + ldap.dn.dn2str([ + [('dc', 'example', 1)], + [('dc', None, 1)], + ], ldap_format), + + with self.assertRaises(TypeError): + ldap.dn.dn2str([ + [('dc', 'example', 1)], + [(None, 'com', 1)], + ], ldap_format), + + with self.assertRaises(TypeError): + ldap.dn.dn2str([ + [('dc', 'example', 1)], + [None], + ], ldap_format), + + with self.assertRaises(TypeError): + ldap.dn.dn2str([ + [('dc', 'example', 1)], + None, + ], ldap_format), + + with self.assertRaises(TypeError): + ldap.dn.dn2str([ + [('dc', 'example', 1)], + [('dc', 'com', 1), None], + ], ldap_format), def test_explode_dn(self): """ From ef837a246f3141aa8601e21a6c986fb39bdf50a4 Mon Sep 17 00:00:00 2001 From: Florian Best Date: Fri, 18 Mar 2022 13:13:26 +0100 Subject: [PATCH 17/42] feat(ldap.dn): Add support for different formats in `ldap.dn.dn2str()` via flags In C `dn2str()` supports `flags` which works by providing one of `LDAP_DN_FORMAT_UFN`, `LDAP_DN_FORMAT_AD_CANONICAL`, `LDAP_DN_FORMAT_DCE`, `LDAP_DN_FORMAT_LDAPV3`. These symbols do exist in Python, but could not be used ultimately because the Python counterpart was pure Python and did not pass to `dn2str(3)`. Fix #257 --- Lib/ldap/dn.py | 12 ++- Modules/functions.c | 217 ++++++++++++++++++++++++++++++++++++++++++++ Tests/t_ldap_dn.py | 6 ++ 3 files changed, 232 insertions(+), 3 deletions(-) diff --git a/Lib/ldap/dn.py b/Lib/ldap/dn.py index a9d96846..b466a442 100644 --- a/Lib/ldap/dn.py +++ b/Lib/ldap/dn.py @@ -48,12 +48,17 @@ def str2dn(dn,flags=0): return ldap.functions._ldap_function_call(None,_ldap.str2dn,dn,flags) -def dn2str(dn): +def dn2str(dn, flags=0): """ This function takes a decomposed DN as parameter and returns - a single string. It's the inverse to str2dn() but will always - return a DN in LDAPv3 format compliant to RFC 4514. + a single string. It's the inverse to str2dn() but will by default always + return a DN in LDAPv3 format compliant to RFC 4514 if not otherwise specified + via flags. + + See also the OpenLDAP man-page ldap_dn2str(3) """ + if flags: + return ldap.functions._ldap_function_call(None, _ldap.dn2str, dn, flags) return ','.join([ '+'.join([ '='.join((atype,escape_dn_chars(avalue or ''))) @@ -61,6 +66,7 @@ def dn2str(dn): for rdn in dn ]) + def explode_dn(dn, notypes=False, flags=0): """ explode_dn(dn [, notypes=False [, flags=0]]) -> list diff --git a/Modules/functions.c b/Modules/functions.c index 9a977ff7..3f7f7eca 100644 --- a/Modules/functions.c +++ b/Modules/functions.c @@ -155,6 +155,222 @@ l_ldap_str2dn(PyObject *unused, PyObject *args) return result; } +/* ldap_dn2str */ + +static void +_free_dn_structure(LDAPDN dn) +{ + if (dn == NULL) + return; + + for (LDAPRDN *rdn = dn; *rdn != NULL; rdn++) { + for (LDAPAVA **avap = *rdn; *avap != NULL; avap++) { + LDAPAVA *ava = *avap; + + if (ava->la_attr.bv_val) { + free(ava->la_attr.bv_val); + } + if (ava->la_value.bv_val) { + free(ava->la_value.bv_val); + } + free(ava); + } + free(*rdn); + } + free(dn); +} + +/* + * Convert a Python list-of-list-of-(str, str, int) into an LDAPDN and + * call ldap_dn2bv to build a DN string. + * + * Python signature: dn2str(dn: list[list[tuple[str, str, int]]], flags: int) -> str + * Returns the DN string on success, or raises TypeError or RuntimeError on error. + */ +static PyObject * +l_ldap_dn2str(PyObject *self, PyObject *args) +{ + PyObject *dn_list = NULL; + int flags = 0; + LDAPDN dn = NULL; + LDAPAVA *ava; + LDAPAVA **rdn; + BerValue str = { 0, NULL }; + PyObject *py_rdn_seq = NULL, *py_ava_item = NULL; + PyObject *py_name = NULL, *py_value = NULL, *py_encoding = NULL; + PyObject *result = NULL; + Py_ssize_t nrdns = 0, navas = 0, name_len = 0, value_len = 0; + int i = 0, j = 0; + int ldap_err; + const char *name_utf8, *value_utf8; + + const char *type_error_message = "expected list[list[tuple[str, str, int]]]"; + + if (!PyArg_ParseTuple(args, "Oi:dn2str", &dn_list, &flags)) { + return NULL; + } + + if (!PySequence_Check(dn_list)) { + PyErr_SetString(PyExc_TypeError, type_error_message); + return NULL; + } + + nrdns = PySequence_Size(dn_list); + if (nrdns < 0) { + PyErr_SetString(PyExc_TypeError, type_error_message); + return NULL; + } + + /* Allocate array of LDAPRDN pointers (+1 for NULL terminator) */ + dn = (LDAPRDN *) calloc((size_t)nrdns + 1, sizeof(LDAPRDN)); + if (dn == NULL) { + PyErr_NoMemory(); + return NULL; + } + + for (i = 0; i < nrdns; i++) { + py_rdn_seq = PySequence_GetItem(dn_list, i); /* New reference */ + if (py_rdn_seq == NULL) { + goto error_cleanup; + } + if (!PySequence_Check(py_rdn_seq)) { + PyErr_SetString(PyExc_TypeError, type_error_message); + goto error_cleanup; + } + + navas = PySequence_Size(py_rdn_seq); + if (navas < 0) { + PyErr_SetString(PyExc_TypeError, type_error_message); + goto error_cleanup; + } + + /* Allocate array of LDAPAVA* pointers (+1 for NULL terminator) */ + rdn = (LDAPAVA **)calloc((size_t)navas + 1, sizeof(LDAPAVA *)); + if (rdn == NULL) { + PyErr_NoMemory(); + goto error_cleanup; + } + + for (j = 0; j < navas; j++) { + py_ava_item = PySequence_GetItem(py_rdn_seq, j); /* New reference */ + if (py_ava_item == NULL) { + goto error_cleanup; + } + /* Expect a 3‐tuple: (name: str, value: str, encoding: int) */ + if (!PyTuple_Check(py_ava_item) || PyTuple_Size(py_ava_item) != 3) { + PyErr_SetString(PyExc_TypeError, type_error_message); + goto error_cleanup; + } + + py_name = PyTuple_GetItem(py_ava_item, 0); /* Borrowed reference */ + py_value = PyTuple_GetItem(py_ava_item, 1); /* Borrowed reference */ + py_encoding = PyTuple_GetItem(py_ava_item, 2); /* Borrowed reference */ + + if (!PyUnicode_Check(py_name) || !PyUnicode_Check(py_value) || !PyLong_Check(py_encoding)) { + PyErr_SetString(PyExc_TypeError, type_error_message); + goto error_cleanup; + } + + name_len = 0; + value_len = 0; + name_utf8 = PyUnicode_AsUTF8AndSize(py_name, &name_len); + value_utf8 = PyUnicode_AsUTF8AndSize(py_value, &value_len); + if (name_utf8 == NULL || value_utf8 == NULL) { + goto error_cleanup; + } + + ava = (LDAPAVA *) calloc(1, sizeof(LDAPAVA)); + + if (ava == NULL) { + PyErr_NoMemory(); + goto error_cleanup; + } + + ava->la_attr.bv_val = (char *)malloc((size_t)name_len + 1); + if (ava->la_attr.bv_val == NULL) { + free(ava); + PyErr_NoMemory(); + goto error_cleanup; + } + memcpy(ava->la_attr.bv_val, name_utf8, (size_t)name_len); + ava->la_attr.bv_val[name_len] = '\0'; + ava->la_attr.bv_len = (ber_len_t) name_len; + + ava->la_value.bv_val = (char *)malloc((size_t)value_len + 1); + if (ava->la_value.bv_val == NULL) { + free(ava->la_attr.bv_val); + free(ava); + PyErr_NoMemory(); + goto error_cleanup; + } + memcpy(ava->la_value.bv_val, value_utf8, (size_t)value_len); + ava->la_value.bv_val[value_len] = '\0'; + ava->la_value.bv_len = (ber_len_t) value_len; + + ava->la_flags = (int)PyLong_AsLong(py_encoding); + if (PyErr_Occurred()) { + /* Encoding conversion failed */ + free(ava->la_attr.bv_val); + free(ava->la_value.bv_val); + free(ava); + goto error_cleanup; + } + + rdn[j] = ava; + Py_DECREF(py_ava_item); + py_ava_item = NULL; + } + + /* Null‐terminate the RDN */ + rdn[navas] = NULL; + + dn[i] = rdn; + Py_DECREF(py_rdn_seq); + py_rdn_seq = NULL; + } + + /* Null‐terminate the DN */ + dn[nrdns] = NULL; + + /* Call ldap_dn2bv to build a DN string */ + ldap_err = ldap_dn2bv(dn, &str, flags); + if (ldap_err != LDAP_SUCCESS) { + PyErr_SetString(PyExc_RuntimeError, ldap_err2string(ldap_err)); + goto error_cleanup; + } + + result = PyUnicode_FromString(str.bv_val); + if (result == NULL) { + goto error_cleanup; + } + + /* Free the memory allocated by ldap_dn2bv */ + ldap_memfree(str.bv_val); + str.bv_val = NULL; + + /* Free our local DN structure */ + _free_dn_structure(dn); + dn = NULL; + + return result; + + error_cleanup: + /* Free any partially built DN structure */ + _free_dn_structure(dn); + dn = NULL; + + /* If ldap_dn2bv allocated something, free it */ + if (str.bv_val) { + ldap_memfree(str.bv_val); + str.bv_val = NULL; + } + + /* Cleanup Python temporaries */ + Py_XDECREF(py_ava_item); + Py_XDECREF(py_rdn_seq); + return NULL; +} + /* ldap_set_option (global options) */ static PyObject * @@ -191,6 +407,7 @@ static PyMethodDef methods[] = { {"initialize_fd", (PyCFunction)l_ldap_initialize_fd, METH_VARARGS}, #endif {"str2dn", (PyCFunction)l_ldap_str2dn, METH_VARARGS}, + {"dn2str", (PyCFunction)l_ldap_dn2str, METH_VARARGS}, {"set_option", (PyCFunction)l_ldap_set_option, METH_VARARGS}, {"get_option", (PyCFunction)l_ldap_get_option, METH_VARARGS}, {NULL, NULL} diff --git a/Tests/t_ldap_dn.py b/Tests/t_ldap_dn.py index 3d6a950d..abe6acb7 100644 --- a/Tests/t_ldap_dn.py +++ b/Tests/t_ldap_dn.py @@ -255,6 +255,12 @@ def test_dn2str(self): ], ldap.DN_FORMAT_LDAPV3), r'cn=\C3\A4\C3\B6\C3\BC\C3\84\C3\96\C3\9C\C3\9F,dc=example,dc=com' ) + self.assertEqual( + ldap.dn.dn2str([ + [('c', 'DEU', 1)], # country code only allow two-letters + ], ldap.DN_FORMAT_LDAPV3), + r'c=DEU' + ) def test_dn_various_lengths(self): base = [ From 2e2913b6818f849393e133e923d68f5675451097 Mon Sep 17 00:00:00 2001 From: Florian Best Date: Wed, 4 Jun 2025 08:00:13 +0200 Subject: [PATCH 18/42] feat(ldap.dn): add ldap.dn.normalize() --- Lib/ldap/dn.py | 5 +++++ Tests/t_ldap_dn.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/Lib/ldap/dn.py b/Lib/ldap/dn.py index b466a442..dd7278b6 100644 --- a/Lib/ldap/dn.py +++ b/Lib/ldap/dn.py @@ -122,3 +122,8 @@ def is_dn(s,flags=0): return False else: return True + + +def normalize(s, flags=0): + """Returns a normalized distinguished name (DN)""" + return dn2str(str2dn(s, flags), flags) diff --git a/Tests/t_ldap_dn.py b/Tests/t_ldap_dn.py index abe6acb7..a41b74a0 100644 --- a/Tests/t_ldap_dn.py +++ b/Tests/t_ldap_dn.py @@ -222,7 +222,6 @@ def test_dn2str(self): ], ldap.DN_FORMAT_DCE), '/dc=com/dc=example/ou=Testing/uid=test42,cn=test42' ) - self.assertEqual( ldap.dn.dn2str([ [('cn', 'äöüÄÖÜß', ldap.AVA_BINARY)], @@ -446,6 +445,35 @@ def test_explode_rdn(self): ['cn=äöüÄÖÜß'] ) + def test_normalize(self): + """ + test function normalize() + """ + self.assertEqual( + ldap.dn.normalize('uid = test42 , ou = Testing , dc = example , dc = com', flags=ldap.DN_FORMAT_LDAPV3), + 'uid=test42,ou=Testing,dc=example,dc=com' + ) + self.assertEqual( + ldap.dn.normalize('cn=äöüÄÖÜß,dc=example,dc=com', flags=0), + 'cn=äöüÄÖÜß,dc=example,dc=com' + ) + self.assertEqual( + ldap.dn.normalize('cn=#C3A4C3B6C3BCC384C396C39CC39F,dc=example,dc=com', flags=0), + 'cn=äöüÄÖÜß,dc=example,dc=com' + ) + self.assertEqual( + ldap.dn.normalize('cn=#C3A4C3B6C3BCC384C396C39CC39F,dc=example,dc=com', flags=ldap.DN_FORMAT_LDAPV3), + 'cn=#C3A4C3B6C3BCC384C396C39CC39F,dc=example,dc=com' + ) + self.assertEqual( + ldap.dn.normalize('cn=äöüÄÖÜß,dc=example,dc=com', flags=ldap.DN_FORMAT_LDAPV3), + r'cn=\C3\A4\C3\B6\C3\BC\C3\84\C3\96\C3\9C\C3\9F,dc=example,dc=com' + ) + self.assertEqual( + ldap.dn.normalize('/ dc = com / dc = example / ou = Testing / uid = test42 , cn = test42', flags=ldap.DN_FORMAT_DCE), + '/dc=com/dc=example/ou=Testing/uid=test42,cn=test42' + ) + if __name__ == '__main__': unittest.main() From 1d978c667a69482f6e4477cd5b512e9c78c06619 Mon Sep 17 00:00:00 2001 From: Florian Best Date: Wed, 11 Jun 2025 21:42:31 +0200 Subject: [PATCH 19/42] style(Tests/t_ldap_dn): use raw strings instead of escape sequences --- Tests/t_ldap_dn.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Tests/t_ldap_dn.py b/Tests/t_ldap_dn.py index a41b74a0..eafa22b3 100644 --- a/Tests/t_ldap_dn.py +++ b/Tests/t_ldap_dn.py @@ -40,17 +40,17 @@ 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'), r'foo\,bar') + self.assertEqual(ldap.dn.escape_dn_chars('foo=bar'), r'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('#foobar'), r'\#foobar') 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(' '), '\\ ') - self.assertEqual(ldap.dn.escape_dn_chars(' '), '\\ \\ ') - self.assertEqual(ldap.dn.escape_dn_chars('foobar '), 'foobar\\ ') + self.assertEqual(ldap.dn.escape_dn_chars(' foobar'), r'\ foobar') + self.assertEqual(ldap.dn.escape_dn_chars(' '), r'\ ') + self.assertEqual(ldap.dn.escape_dn_chars(' '), r'\ \ ') + self.assertEqual(ldap.dn.escape_dn_chars('foobar '), r'foobar\ ') self.assertEqual(ldap.dn.escape_dn_chars('f+o>o,bo\\,b\\ Date: Fri, 13 Nov 2020 23:52:51 +0100 Subject: [PATCH 20/42] test: Implement test cases for reconnection handling test_106_reconnect_restore() handles a SERVER_DOWN exception manually and tries to re-use the connection afterwards again. This established the connection again but did not bind(), so it now raises ldap.INSUFFICIENT_ACCESS. test_107_reconnect_restore() restarts the LDAP server during searches, which causes a UNAVAILABLE exception. --- Lib/slapdtest/_slapdtest.py | 10 ++++- Tests/t_ldapobject.py | 77 ++++++++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/Lib/slapdtest/_slapdtest.py b/Lib/slapdtest/_slapdtest.py index 36841110..4110d945 100644 --- a/Lib/slapdtest/_slapdtest.py +++ b/Lib/slapdtest/_slapdtest.py @@ -467,8 +467,16 @@ def restart(self): """ Restarts the slapd server with same data """ - self._proc.terminate() + self.terminate() self.wait() + self.resume() + + def terminate(self): + """Terminate slapd server""" + self._proc.terminate() + + def resume(self): + """Start slapd server""" self._start_slapd() def wait(self): diff --git a/Tests/t_ldapobject.py b/Tests/t_ldapobject.py index ecf163b7..87a829a3 100644 --- a/Tests/t_ldapobject.py +++ b/Tests/t_ldapobject.py @@ -9,9 +9,13 @@ import os import re import socket +import threading +import time +import traceback import unittest import pickle + # Switch off processing .ldaprc or ldap.conf before importing _ldap os.environ['LDAPNOINIT'] = '1' @@ -639,7 +643,7 @@ def test105_reconnect_restore(self): bind_dn = 'cn=user1,'+self.server.suffix l1.simple_bind_s(bind_dn, 'user1_pw') self.assertEqual(l1.whoami_s(), 'dn:'+bind_dn) - self.server._proc.terminate() + self.server.terminate() self.server.wait() try: l1.whoami_s() @@ -648,9 +652,78 @@ def test105_reconnect_restore(self): else: self.assertEqual(True, False) finally: - self.server._start_slapd() + self.server.resume() self.assertEqual(l1.whoami_s(), 'dn:'+bind_dn) + def test106_reconnect_restore(self): + """ + The idea of this test is to stop the LDAP server, make a search and ignore the `SERVER_DOWN` exception which happens after the reconnect timeout + and then re-use the same connection when the LDAP server is available again. + After starting the server the LDAP connection can be re-used again as it will reconnect on the next operation. + Prior to fixing PR !267 the connection was reestablished but no `bind()` was done resulting in a anonymous search which caused `INSUFFICIENT_ACCESS` when anonymous seach is disallowed. + """ + lo = self.ldap_object_class(self.server.ldap_uri, retry_max=2, retry_delay=1) + bind_dn = 'cn=user1,' + self.server.suffix + lo.simple_bind_s(bind_dn, 'user1_pw') + + dn = lo.whoami_s()[3:] + + self.server.terminate() + self.server.wait() + + # do a search, wait for the timeout, ignore SERVER_DOWN + with self.assertRaises(ldap.SERVER_DOWN): + lo.search_s(dn, ldap.SCOPE_BASE, '(objectClass=*)') + + self.server.resume() + + # try to use the connection again + lo.search_s(dn, ldap.SCOPE_BASE, '(objectClass=*)') + + def test107_reconnect_restore(self): + """ + The idea of this test is to restart the LDAP-Server while there are ongoing searches. + This causes :class:`ldap.UNAVAILABLE` to be raised (with |OpenLDAP|) for a short time. + To increase the chance of triggering this bug we are starting multiple threads + with a large number of retry attempts in a short amount of time. + """ + excs = [] + thread_count = 10 + run_time = 10.0 + start_barrier = threading.Barrier(thread_count + 1) # +1 for the main thread + + def _reconnect_search_thread(): + lo = self.ldap_object_class(self.server.ldap_uri) + bind_dn = 'cn=user1,' + self.server.suffix + lo.simple_bind_s(bind_dn, 'user1_pw') + lo._retry_max = 10E4 + lo._retry_delay = 0.001 + lo.search_ext_s(self.server.suffix, ldap.SCOPE_SUBTREE, "cn=user1", attrlist=["cn"]) + start_barrier.wait() + end_time = time.time() + run_time + while time.time() < end_time: + lo.search_ext_s(self.server.suffix, ldap.SCOPE_SUBTREE, filterstr="cn=user1", attrlist=["cn"]) + + def reconnect_search_thread(): + try: + _reconnect_search_thread() + except Exception as exc: + excs.append((str(exc), traceback.format_exc())) + + threads = [threading.Thread(target=reconnect_search_thread) for _ in range(thread_count)] + for t in threads: + t.start() + + start_barrier.wait() # wait until all threads are ready to start + self.server.restart() # restart after all threads have started their search loop + + for t in threads: + t.join() + + for exc, tb in excs[:5]: + print('Exception occurred', exc, tb) + self.assertEqual(excs, []) + @requires_init_fd() class Test03_SimpleLDAPObjectWithFileno(Test00_SimpleLDAPObject): From f695daa01b18413c008d5fcd398b2d699b70b8e9 Mon Sep 17 00:00:00 2001 From: Florian Best Date: Sat, 16 Mar 2019 19:00:51 +0100 Subject: [PATCH 21/42] fix(ReconnectLDAPObject): Also reconnect on UNAVILABLE, CONNECT_ERROR and TIMEOUT --- Lib/ldap/ldapobject.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index e8afb726..7e7b8158 100644 --- a/Lib/ldap/ldapobject.py +++ b/Lib/ldap/ldapobject.py @@ -820,8 +820,7 @@ def get_naming_contexts(self): class ReconnectLDAPObject(SimpleLDAPObject): """ :py:class:`SimpleLDAPObject` subclass whose synchronous request methods - automatically reconnect and re-try in case of server failure - (:exc:`ldap.SERVER_DOWN`). + automatically reconnect and re-try in case of server failure. The first arguments are same as for the :py:func:`~ldap.initialize()` function. @@ -833,6 +832,10 @@ class ReconnectLDAPObject(SimpleLDAPObject): * retry_delay: specifies the time in seconds between reconnect attempts. This class also implements the pickle protocol. + + .. versionadded:: 3.5 + The exceptions :py:exc:`ldap.SERVER_DOWN`, :py:exc:`ldap.UNAVAILABLE`, :py:exc:`ldap.CONNECT_ERROR` and + :py:exc:`ldap.TIMEOUT` (configurable via :py:attr:`_reconnect_exceptions`) now trigger a reconnect. """ __transient_attrs__ = { @@ -842,6 +845,7 @@ class ReconnectLDAPObject(SimpleLDAPObject): '_reconnect_lock', '_last_bind', } + _reconnect_exceptions = (ldap.SERVER_DOWN, ldap.UNAVAILABLE, ldap.CONNECT_ERROR, ldap.TIMEOUT) def __init__( self,uri, @@ -974,7 +978,7 @@ def _apply_method_s(self,func,*args,**kwargs): self.reconnect(self._uri,retry_max=self._retry_max,retry_delay=self._retry_delay,force=False) try: return func(self,*args,**kwargs) - except ldap.SERVER_DOWN: + except self._reconnect_exceptions: # Try to reconnect self.reconnect(self._uri,retry_max=self._retry_max,retry_delay=self._retry_delay,force=True) # Re-try last operation From a201133dcd1d4ec052527df5c78ff1c3af5f7e23 Mon Sep 17 00:00:00 2001 From: Florian Best Date: Thu, 7 Aug 2025 09:50:35 +0200 Subject: [PATCH 22/42] fix(controls): make sure msg_id is not undefined in error case --- Lib/ldap/controls/openldap.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Lib/ldap/controls/openldap.py b/Lib/ldap/controls/openldap.py index 24040ed7..26c76868 100644 --- a/Lib/ldap/controls/openldap.py +++ b/Lib/ldap/controls/openldap.py @@ -51,6 +51,7 @@ class SearchNoOpMixIn: """ def noop_search_st(self,base,scope=ldap.SCOPE_SUBTREE,filterstr='(objectClass=*)',timeout=-1): + msg_id = None try: msg_id = self.search_ext( base, @@ -66,9 +67,10 @@ def noop_search_st(self,base,scope=ldap.SCOPE_SUBTREE,filterstr='(objectClass=*) ldap.TIMELIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED, ldap.ADMINLIMIT_EXCEEDED - ) as e: - self.abandon(msg_id) - raise e + ): + if msg_id is not None: + self.abandon(msg_id) + raise else: noop_srch_ctrl = [ c From a73ce552db1d5250e62ea88999a5b2d480cd8153 Mon Sep 17 00:00:00 2001 From: Florian Best Date: Mon, 14 Jul 2025 04:32:41 +0200 Subject: [PATCH 23/42] docs(ldapobject): fix typo in docstring --- Doc/reference/ldap.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/reference/ldap.rst b/Doc/reference/ldap.rst index a642f579..1d095adb 100644 --- a/Doc/reference/ldap.rst +++ b/Doc/reference/ldap.rst @@ -605,13 +605,13 @@ The module defines the following exceptions: .. py:exception:: COMPARE_FALSE A compare operation returned false. - (This exception should only be seen asynchronous operations, because + (This exception should only be seen in asynchronous operations, because :py:meth:`~LDAPObject.compare_s()` returns a boolean result.) .. py:exception:: COMPARE_TRUE A compare operation returned true. - (This exception should only be seen asynchronous operations, because + (This exception should only be seen in asynchronous operations, because :py:meth:`~LDAPObject.compare_s()` returns a boolean result.) .. py:exception:: CONFIDENTIALITY_REQUIRED From 8b4912919f13916f10efadb7cb9261c121083ba8 Mon Sep 17 00:00:00 2001 From: Simon Pichugin Date: Mon, 6 Oct 2025 19:05:09 -0700 Subject: [PATCH 24/42] Package python-ldap with pyproject.toml (#589) --- .coveragerc | 27 ---------- .gitignore | 2 - Doc/installing.rst | 30 ++++++++--- Doc/spelling_wordlist.txt | 4 ++ INSTALL | 3 +- MANIFEST.in | 2 +- Makefile | 1 - pyproject.toml | 110 ++++++++++++++++++++++++++++++++++++-- setup.py | 74 ++----------------------- tox.ini | 1 + 10 files changed, 140 insertions(+), 114 deletions(-) delete mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 738d86fa..00000000 --- a/.coveragerc +++ /dev/null @@ -1,27 +0,0 @@ -[run] -branch = True -source = - ldap - ldif - ldapurl - slapdtest - -[paths] -source = - Lib/ - .tox/*/lib/python*/site-packages/ - -[report] -ignore_errors = False -precision = 1 -exclude_lines = - pragma: no cover - raise NotImplementedError - if 0: - if __name__ == .__main__.: - if PY2 - if not PY2 - -[html] -directory = build/htmlcov -title = python-ldap coverage report diff --git a/.gitignore b/.gitignore index bab21878..75a13538 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,6 @@ *.pyc __pycache__/ .tox -.coverage* -!.coveragerc /.cache /.pytest_cache diff --git a/Doc/installing.rst b/Doc/installing.rst index 03e7a295..1c7ec8c3 100644 --- a/Doc/installing.rst +++ b/Doc/installing.rst @@ -76,12 +76,23 @@ The CVS repository of FreeBSD contains the package macOS ----- -You can install directly with pip:: +You can install directly with pip. First install Xcode command line tools:: $ xcode-select --install - $ pip install python-ldap \ - --global-option=build_ext \ - --global-option="-I$(xcrun --show-sdk-path)/usr/include/sasl" + +Then install python-ldap:: + + $ pip install python-ldap + +For custom installations, you may need to set environment variables:: + + $ export CPPFLAGS="-I$(xcrun --show-sdk-path)/usr/include/sasl" + $ pip install python-ldap + +If using Homebrew:: + + $ brew install openldap + $ pip install python-ldap .. _install-source: @@ -90,11 +101,14 @@ Installing from Source ====================== -python-ldap is built and installed using the Python setuptools. -From a source repository:: +python-ldap is built and installed using modern Python packaging standards +with pyproject.toml configuration. From a source repository:: + + $ pip install . + +For development installation with editable mode:: - $ python -m pip install setuptools - $ python setup.py install + $ pip install -e . If you have more than one Python interpreter installed locally, you should use the same one you plan to use python-ldap with. diff --git a/Doc/spelling_wordlist.txt b/Doc/spelling_wordlist.txt index e2150d9a..4381ebee 100644 --- a/Doc/spelling_wordlist.txt +++ b/Doc/spelling_wordlist.txt @@ -60,6 +60,7 @@ func Gohlke GPG Heimdal +Homebrew hostport hrefTarget hrefText @@ -106,6 +107,7 @@ previousDN processResultsCount Proxied py +pyproject pytest rdn readthedocs @@ -146,6 +148,7 @@ syncrepl syntaxes timelimit TLS +toml tracebacks tuple tuples @@ -162,5 +165,6 @@ userPassword usr uuids Valgrind +Xcode whitespace workflow diff --git a/INSTALL b/INSTALL index b9b13d2d..224df4a4 100644 --- a/INSTALL +++ b/INSTALL @@ -1,8 +1,7 @@ Quick build instructions: edit setup.cfg (see Build/ for platform-specific examples) - python setup.py build - python setup.py install + pip install . Detailed instructions are in Doc/installing.rst, or online at: diff --git a/MANIFEST.in b/MANIFEST.in index 687d2b0c..bedea8d6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ include MANIFEST.in Makefile CHANGES INSTALL LICENCE README TODO -include tox.ini .coveragerc +include tox.ini include Modules/*.c Modules/*.h recursive-include Build *.cfg* recursive-include Lib *.py diff --git a/Makefile b/Makefile index 2b52ddf5..da23b374 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,6 @@ Modules/constants_generated.h: Lib/ldap/constants.py .PHONY: clean clean: rm -rf build dist *.egg-info .tox MANIFEST - rm -f .coverage .coverage.* find . \( -name '*.py[co]' -or -name '*.so*' -or -name '*.dylib' \) \ -delete find . -depth -name __pycache__ -exec rm -rf {} \; diff --git a/pyproject.toml b/pyproject.toml index 75f7c06a..8781155d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,108 @@ +[build-system] +requires = [ + "setuptools", + "setuptools-scm", +] +build-backend = "setuptools.build_meta" + +[project] +name = "python-ldap" +license.text = "python-ldap" # Replace with 'license' once Python 3.8 is dropped +dynamic = ["version"] +description = "Python modules for implementing LDAP clients" +authors = [ + {name = "python-ldap project", email = "python-ldap@python.org"}, +] +readme = "README" +requires-python = ">=3.6" +keywords = ["ldap", "directory", "authentication"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Operating System :: OS Independent", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Programming Language :: C", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Database", + "Topic :: Internet", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP", + "License :: OSI Approved :: Python Software Foundation License", +] +dependencies = [ + "pyasn1 >= 0.3.7", + "pyasn1_modules >= 0.1.5", +] + +[project.urls] +Homepage = "https://www.python-ldap.org/" +Documentation = "https://python-ldap.readthedocs.io/" +Repository = "https://github.com/python-ldap/python-ldap" +Download = "https://pypi.org/project/python-ldap/" +Changelog = "https://github.com/python-ldap/python-ldap/blob/main/CHANGES" + + + +[tool.setuptools] +zip-safe = false +include-package-data = true +license-files = ["LICENCE", "LICENCE.MIT"] +# Explicitly list all Python modules +py-modules = ["ldapurl", "ldif"] + +[tool.setuptools.dynamic] +version = {attr = "ldap.pkginfo.__version__"} + +[tool.setuptools.packages.find] +where = ["Lib"] + +[tool.setuptools.package-dir] +"" = "Lib" + [tool.isort] -line_length=88 -known_first_party=['ldap', '_ldap', 'ldapurl', 'ldif', 'slapdtest'] -sections=['FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER'] +line_length = 88 +known_first_party = ["ldap", "_ldap", "ldapurl", "ldif", "slapdtest"] +sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] + +[tool.coverage.run] +branch = true +source = [ + "ldap", + "ldif", + "ldapurl", + "slapdtest", +] + +[tool.coverage.paths] +source = [ + "Lib/", + ".tox/*/lib/python*/site-packages/", +] + +[tool.coverage.report] +ignore_errors = false +precision = 1 +exclude_lines = [ + "pragma: no cover", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "if PY2", + "if not PY2", +] + +[tool.coverage.html] +directory = "build/htmlcov" +title = "python-ldap coverage report" diff --git a/setup.py b/setup.py index ea7364cd..f2e816be 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,9 @@ """ -setup.py - Setup package with the help Python's DistUtils +setup.py - C extension module configuration for python-ldap See https://www.python-ldap.org/ for details. +This file handles only the C extension modules (_ldap) configuration, +while pyproject.toml handles all project metadata, dependencies, and other settings. """ import sys,os @@ -54,52 +56,8 @@ class OpenLDAP2: LDAP_CLASS.extra_link_args.append('-pg') LDAP_CLASS.libs.append('gcov') -#-- Let distutils/setuptools do the rest -name = 'python-ldap' - +#-- C extension modules configuration only setup( - #-- Package description - name = name, - license=pkginfo.__license__, - version=pkginfo.__version__, - description = 'Python modules for implementing LDAP clients', - long_description = """python-ldap: - python-ldap provides an object-oriented API to access LDAP directory servers - from Python programs. Mainly it wraps the OpenLDAP 2.x libs for that purpose. - Additionally the package contains modules for other LDAP-related stuff - (e.g. processing LDIF, LDAPURLs, LDAPv3 schema, LDAPv3 extended operations - and controls, etc.). - """, - author = 'python-ldap project', - author_email = 'python-ldap@python.org', - url = 'https://www.python-ldap.org/', - download_url = 'https://pypi.org/project/python-ldap/', - classifiers = [ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'Operating System :: OS Independent', - 'Operating System :: MacOS :: MacOS X', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: POSIX', - 'Programming Language :: C', - - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.13', - # Note: when updating Python versions, also change tox.ini and .github/workflows/* - - 'Topic :: Database', - 'Topic :: Internet', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP', - 'License :: OSI Approved :: Python Software Foundation License', - ], - #-- C extension modules ext_modules = [ Extension( '_ldap', @@ -135,28 +93,4 @@ class OpenLDAP2: ] ), ], - #-- Python "stand alone" modules - py_modules = [ - 'ldapurl', - 'ldif', - - ], - packages = [ - 'ldap', - 'ldap.controls', - 'ldap.extop', - 'ldap.schema', - 'slapdtest', - 'slapdtest.certs', - ], - package_dir = {'': 'Lib',}, - data_files = LDAP_CLASS.extra_files, - include_package_data=True, - install_requires=[ - 'pyasn1 >= 0.3.7', - 'pyasn1_modules >= 0.1.5', - ], - zip_safe=False, - python_requires='>=3.9', - test_suite = 'Tests', ) diff --git a/tox.ini b/tox.ini index 0b284a4e..0741ef29 100644 --- a/tox.ini +++ b/tox.ini @@ -33,6 +33,7 @@ commands = {envpython} -bb -Werror \ setenv = CFLAGS=-Wno-int-in-bool-context -Werror -std=c99 + [testenv:py3-nosasltls] basepython = python3 # don't install, install dependencies manually From 6ea80326a34ee6093219628d7690bced50c49a3f Mon Sep 17 00:00:00 2001 From: Simon Pichugin Date: Fri, 10 Oct 2025 10:46:45 -0700 Subject: [PATCH 25/42] Merge commit from fork Update tests to expect \00 and verify RFC-compliant escaping --- Lib/ldap/dn.py | 3 ++- Tests/t_ldap_dn.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/ldap/dn.py b/Lib/ldap/dn.py index dd7278b6..64d7d0e9 100644 --- a/Lib/ldap/dn.py +++ b/Lib/ldap/dn.py @@ -26,7 +26,8 @@ def escape_dn_chars(s): s = s.replace('>' ,'\\>') s = s.replace(';' ,'\\;') s = s.replace('=' ,'\\=') - s = s.replace('\000' ,'\\\000') + # RFC 4514 requires NULL (U+0000) to be escaped as hex pair "\00" + s = s.replace('\x00' ,'\\00') if s[-1]==' ': s = ''.join((s[:-1],'\\ ')) if s[0]=='#' or s[0]==' ': diff --git a/Tests/t_ldap_dn.py b/Tests/t_ldap_dn.py index eafa22b3..c4d9cb6c 100644 --- a/Tests/t_ldap_dn.py +++ b/Tests/t_ldap_dn.py @@ -49,7 +49,7 @@ def test_escape_dn_chars(self): self.assertEqual(ldap.dn.escape_dn_chars(' '), r'\ ') self.assertEqual(ldap.dn.escape_dn_chars(' '), r'\ \ ') self.assertEqual(ldap.dn.escape_dn_chars('foobar '), r'foobar\ ') - self.assertEqual(ldap.dn.escape_dn_chars('f+o>o,bo\\,b\\o,bo\,b\ Date: Fri, 10 Oct 2025 19:47:46 +0200 Subject: [PATCH 26/42] Merge commit from fork --- Lib/ldap/filter.py | 2 ++ Tests/t_ldap_filter.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/Lib/ldap/filter.py b/Lib/ldap/filter.py index 782737aa..5bd41b21 100644 --- a/Lib/ldap/filter.py +++ b/Lib/ldap/filter.py @@ -24,6 +24,8 @@ def escape_filter_chars(assertion_value,escape_mode=0): If 1 all NON-ASCII chars are escaped. If 2 all chars are escaped. """ + if not isinstance(assertion_value, str): + raise TypeError("assertion_value must be of type str.") if escape_mode: r = [] if escape_mode==1: diff --git a/Tests/t_ldap_filter.py b/Tests/t_ldap_filter.py index 313b3733..54312050 100644 --- a/Tests/t_ldap_filter.py +++ b/Tests/t_ldap_filter.py @@ -49,6 +49,10 @@ def test_escape_filter_chars_mode1(self): ), r'\c3\a4\c3\b6\c3\bc\c3\84\c3\96\c3\9c\c3\9f' ) + with self.assertRaises(TypeError): + escape_filter_chars(["abc@*()/xyz"], escape_mode=1) + with self.assertRaises(TypeError): + escape_filter_chars({"abc@*()/xyz": 1}, escape_mode=1) def test_escape_filter_chars_mode2(self): """ From 414ae1de91543a1c0fee0738f97fe1a33d0fe666 Mon Sep 17 00:00:00 2001 From: Florian Best Date: Thu, 14 Aug 2025 02:50:35 +0200 Subject: [PATCH 27/42] fix(extop.dds): fix unset RefreshRequest.requestValue >>> from ldap.extop.dds import RefreshRequest >>> req = RefreshRequest(RefreshRequest.requestName, 'uid=temp,dc=freeiam,dc=org', 86400) >>> repr(req) Traceback (most recent call last): File "", line 1, in File "/usr/lib/python3/dist-packages/ldap/extop/__init__.py", line 31, in __repr__ return f'{self.__class__.__name__}({self.requestName},{self.requestValue})' ^^^^^^^^^^^^^^^^^ AttributeError: 'RefreshRequest' object has no attribute 'requestValue'. Did you mean: 'requestName'? --- Lib/ldap/extop/dds.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/ldap/extop/dds.py b/Lib/ldap/extop/dds.py index 7fab0813..50695ea4 100644 --- a/Lib/ldap/extop/dds.py +++ b/Lib/ldap/extop/dds.py @@ -35,6 +35,7 @@ class RefreshRequestValue(univ.Sequence): ) def __init__(self,requestName=None,entryName=None,requestTtl=None): + super().__init__(requestName, b'') self.entryName = entryName self.requestTtl = requestTtl or self.defaultRequestTtl From 0506a0f0a1824dd3028910ab3d8c94b0a0cfd039 Mon Sep 17 00:00:00 2001 From: Florian Best Date: Tue, 21 Oct 2025 10:07:14 +0200 Subject: [PATCH 28/42] fix(extop.dds): make passing of requestName optional again requestName is already set at class member. It seems there is code out there which is not giving it as value but passing None. in that case, fallback to the class member. Fixes: 414ae1de91543a1c0fee0738f97fe1a33d0fe666 --- Lib/ldap/extop/dds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ldap/extop/dds.py b/Lib/ldap/extop/dds.py index 50695ea4..a970d71d 100644 --- a/Lib/ldap/extop/dds.py +++ b/Lib/ldap/extop/dds.py @@ -35,7 +35,7 @@ class RefreshRequestValue(univ.Sequence): ) def __init__(self,requestName=None,entryName=None,requestTtl=None): - super().__init__(requestName, b'') + super().__init__(requestName or self.requestName, b'') self.entryName = entryName self.requestTtl = requestTtl or self.defaultRequestTtl From 2f0135cfd6004e8d5a10295fe307c5111e8da8ae Mon Sep 17 00:00:00 2001 From: dotlambda Date: Fri, 10 Oct 2025 16:00:13 -0700 Subject: [PATCH 29/42] remove superfluous dependency on setuptools-scm --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8781155d..77783f8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,6 @@ [build-system] requires = [ "setuptools", - "setuptools-scm", ] build-backend = "setuptools.build_meta" From aca9cb5fdbab78918fe6905cfe1cff8549039c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Tue, 25 Jun 2024 12:38:24 +0100 Subject: [PATCH 30/42] Add OpenLDAPSyncreplCookie Fixes: https://github.com/python-ldap/python-ldap/issues/562 --- Doc/reference/ldap-syncrepl.rst | 3 + Lib/ldap/syncrepl.py | 70 +++++++++++++ Lib/slapdtest/_slapdtest.py | 5 + Tests/__init__.py | 1 + Tests/t_ldap_syncrepl.py | 175 +++++++++++++++++++++++++++++++- 5 files changed, 253 insertions(+), 1 deletion(-) diff --git a/Doc/reference/ldap-syncrepl.rst b/Doc/reference/ldap-syncrepl.rst index b3b2cf9a..046b15a9 100644 --- a/Doc/reference/ldap-syncrepl.rst +++ b/Doc/reference/ldap-syncrepl.rst @@ -20,3 +20,6 @@ This module defines the following classes: .. autoclass:: ldap.syncrepl.SyncreplConsumer :members: + +.. autoclass:: ldap.syncrepl.OpenLDAPSyncreplCookie + :members: diff --git a/Lib/ldap/syncrepl.py b/Lib/ldap/syncrepl.py index fd0c1285..0e1b6a3f 100644 --- a/Lib/ldap/syncrepl.py +++ b/Lib/ldap/syncrepl.py @@ -4,6 +4,7 @@ See https://www.python-ldap.org/ for project details. """ +from typing import AnyStr, Dict, List, Tuple, Union from uuid import UUID # Imports from pyasn1 @@ -15,6 +16,7 @@ from ldap import RES_SEARCH_RESULT, RES_SEARCH_ENTRY, RES_INTERMEDIATE __all__ = [ + 'OpenLDAPSyncreplCookie', 'SyncreplConsumer', ] @@ -535,3 +537,71 @@ def syncrepl_refreshdone(self): follows. """ pass + + +class OpenLDAPSyncreplCookie: + """ + OpenLDAPSyncreplCookie - allows a consumer to track a cookie across a + refreshAndPersist syncrepl session against a multi-provider OpenLDAP cluster + """ + + rid: int = 0 + sid: int = 0 + _csnset: Dict[int, str] + + def __init__(self, cookie: AnyStr = "") -> None: + self._csnset = {} + + if cookie: + self.update(cookie) + + def _parse_csn(self, csn: str) -> Tuple[str, str, str, str]: + time, order, sid, other = csn.split('#', 3) + return (time, order, sid, other) + + def _parse_cookie(self, cookie: AnyStr) -> Dict[str, Union[str, List[str]]]: + if isinstance(cookie, bytes): + cookie = cookie.decode() + + result = {} + parts = cookie.split(',') + for part in parts: + if part.startswith('rid='): + result['rid'] = part[4:] + elif part.startswith('sid='): + result['sid'] = part[4:] + elif part.startswith('csn='): + result['csn'] = part[4:].split(';') + elif part.startswith('delcsn='): + result['delcsn'] = part[7:] + else: + # Did not recognize this cookie part + pass + return result + + def update(self, cookie: AnyStr): + """ + Update the CSN set based on a cookie we just received, use in + syncrepl_set_cookie() to track the session state. + """ + components = self._parse_cookie(cookie) + for csn in components.get('csn', []): + _, _, sid, _ = self._parse_csn(csn) + if sid not in self._csnset or self._csnset[sid] < csn: + self._csnset[sid] = csn + + return self + + def unparse(self) -> str: + """ + Return the cookie as a string, use in syncrepl_get_cookie() or when + storing the state for later use. + """ + cookie = 'rid={:03},sid={:03x}'.format(self.rid or 0, self.sid or 0) + if self._csnset: + cookie += ',csn=' + cookie += ';'.join(csn for sid, csn in sorted(self._csnset.items())) + return cookie + + def __str__(self): + return self.unparse() diff --git a/Lib/slapdtest/_slapdtest.py b/Lib/slapdtest/_slapdtest.py index 4110d945..00764ac3 100644 --- a/Lib/slapdtest/_slapdtest.py +++ b/Lib/slapdtest/_slapdtest.py @@ -41,6 +41,11 @@ cn: module olcModuleLoad: back_%(database)s +dn: olcDatabase=config,cn=config +objectClass: olcDatabaseConfig +olcDatabase: config +olcRootDN: %(rootdn)s + dn: olcDatabase=%(database)s,cn=config objectClass: olcDatabaseConfig objectClass: olcMdbConfig diff --git a/Tests/__init__.py b/Tests/__init__.py index ea28d0ce..1a6a8836 100644 --- a/Tests/__init__.py +++ b/Tests/__init__.py @@ -21,3 +21,4 @@ from . import t_untested_mods from . import t_ldap_controls_libldap from . import t_ldap_options +from . import t_ldap_syncrepl diff --git a/Tests/t_ldap_syncrepl.py b/Tests/t_ldap_syncrepl.py index 6acc82c4..ab134a79 100644 --- a/Tests/t_ldap_syncrepl.py +++ b/Tests/t_ldap_syncrepl.py @@ -13,7 +13,8 @@ import ldap from ldap.ldapobject import SimpleLDAPObject -from ldap.syncrepl import SyncreplConsumer, SyncInfoMessage +from ldap.syncrepl import SyncreplConsumer, SyncInfoMessage, \ + OpenLDAPSyncreplCookie from slapdtest import SlapdObject, SlapdTestCase @@ -37,6 +38,10 @@ olcModuleLoad: back_%(database)s olcModuleLoad: syncprov +dn: olcDatabase=config,cn=config +objectClass: olcDatabaseConfig +olcRootDN: %(rootdn)s + dn: olcDatabase=%(database)s,cn=config objectClass: olcDatabaseConfig objectClass: olcMdbConfig @@ -442,6 +447,174 @@ def setUp(self): self.suffix = self.server.suffix +class TestMPRSyncrepl(BaseSyncreplTests, SlapdTestCase): + class MPRClient(SyncreplClient): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.cookie = OpenLDAPSyncreplCookie() + + def syncrepl_set_cookie(self, cookie): + self.cookie.update(cookie) + super().syncrepl_set_cookie(self.cookie.unparse()) + + def setUp(self): + super().setUp() + self.tester = self.MPRClient( + self.server.ldap_uri, + self.server.root_dn, + self.server.root_pw, + bytes_mode=False + ) + self.suffix = self.server.suffix + + # An active MPR should not have a sid=000 server in it + if self.server.server_id == 0: + self.skipTest("Server got serverid 0 assigned") + + def test_mpr_refresh_and_persist(self): + """ + Make sure we process cookie updates from a live MPR cluster correctly + """ + # Assumes that server_id is not used before the call to start() + self.server2 = self.server_class() + if self.server.server_id == self.server2.server_id: + self.server2.server_id += 1 + if self.server2.server_id % 4096 == 0: + self.server2.server_id = 1 + + with self.server2 as server2: + tester2 = self.MPRClient( + self.server2.ldap_uri, + self.server2.root_dn, + self.server2.root_pw, + bytes_mode=False + ) + self.addCleanup(tester2.unbind_s) + + self.tester.search( + self.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) + + # set up replication between both + coords = [(1, self.server.ldap_uri, self.suffix, + self.server.root_dn, self.server.root_pw), + (2, self.server2.ldap_uri, self.suffix, + self.server2.root_dn, self.server2.root_pw), + ] + modifications = [ + (ldap.MOD_ADD, "olcSyncrepl", [ + ('rid=%d provider=%s searchbase="%s" type=refreshAndPersist ' + 'bindmethod=simple binddn="%s" credentials="%s" ' + 'retry="1 +"' % coord).encode() for coord in coords]), + # do we still support 2.4.x? Change to olcMultiProvider if not + (ldap.MOD_REPLACE, "olcMirrorMode", [b"TRUE"]), + ] + + self.tester.modify_s( + "olcDatabase={1}%s,cn=config" % (self.server.database), + modifications) + tester2.modify_s( + "olcDatabase={1}%s,cn=config" % (self.server.database), + modifications) + + tester2.search( + self.suffix, + 'refreshAndPersist', + ) + + # Wait till server2 catches up + while tester2.refresh_done is not True or \ + tester2.cookie.unparse() != self.tester.cookie.unparse(): + try: + poll_result = tester2.poll( + all=0, + timeout=None + ) + self.assertTrue(poll_result) + except ldap.NO_SUCH_OBJECT: + # 2.6+ Allows a refreshAndPersist against an empty DB, but + # with older ones we need to retry until there's at least + # one entry + tester2.search( + self.suffix, + 'refreshAndPersist', + ) + + # Again, server data should not have changed. + self.assertEqual(tester2.dn_attrs, LDAP_ENTRIES) + + # From here on, things get little hairy, server1 might not have + # finished its refresh from 2 and we can't easily confirm this + # without cn=monitor. We just read back our CSNs and make sure + # we've seen both. + + # send some mods to both + modification = [('objectClass', [b'device'])] + self.tester.add_s("cn=server1,%s" % self.suffix, modification) + + csn1 = self.tester.read_s("cn=server1,%s" % self.suffix, + attrlist=['entryCSN'] + )['entryCSN'][0].decode('utf8') + + tester2.add_s("cn=server2,%s" % self.suffix, modification) + csn2 = tester2.read_s("cn=server2,%s" % self.suffix, + attrlist=['entryCSN'] + )['entryCSN'][0].decode('utf8') + + new_state = LDAP_ENTRIES.copy() + new_state["cn=server1,%s" % self.suffix] = { + "objectClass": [b"device"], + "cn": [b"server1"], + } + new_state["cn=server2,%s" % self.suffix] = { + "objectClass": [b"device"], + "cn": [b"server2"], + } + + # Wait for the cookie to sync up, a failure would be that this + # doesn't happen, so impose a timeout + while csn1 not in self.tester.cookie.unparse() or \ + csn2 not in self.tester.cookie.unparse() or \ + csn1 not in tester2.cookie.unparse() or \ + csn2 not in tester2.cookie.unparse(): + if csn1 not in self.tester.cookie.unparse() or \ + csn2 not in self.tester.cookie.unparse(): + poll_result = self.tester.poll( + all=0, + timeout=5 + ) + self.assertTrue(poll_result) + if csn1 not in tester2.cookie.unparse() or \ + csn2 not in tester2.cookie.unparse(): + poll_result = tester2.poll( + all=0, + timeout=5 + ) + self.assertTrue(poll_result) + + self.assertEqual(self.tester.cookie.unparse(), + tester2.cookie.unparse()) + self.assertEqual(self.tester.dn_attrs, new_state) + self.assertEqual(tester2.dn_attrs, new_state) + + # self.tester seems to have been unbound by the time + # self.addCleanup callbacks get called? Cleanup manually... + self.tester.delete_s("cn=server1,%s" % self.suffix) + self.tester.delete_s("cn=server2,%s" % self.suffix) + + class DecodeSyncreplProtoTests(unittest.TestCase): """ Tests of the ASN.1 decoder for tricky cases or past issues to ensure that From 886139877811aaefedff400d85fd81c545385893 Mon Sep 17 00:00:00 2001 From: Simon Pichugin Date: Mon, 6 Oct 2025 19:39:49 -0700 Subject: [PATCH 31/42] Prepare a new release cherry-pick bf666e918615b00dbcd1bbe71542522eedfdffc1 --- CHANGES | 29 +++++++++++++++++++++++++++++ Lib/ldap/cidict.py | 6 +++--- Lib/ldap/ldapobject.py | 2 +- Lib/ldap/pkginfo.py | 2 +- Lib/ldapurl.py | 2 +- Lib/ldif.py | 2 +- Lib/slapdtest/__init__.py | 2 +- 7 files changed, 37 insertions(+), 8 deletions(-) diff --git a/CHANGES b/CHANGES index 0491b6ef..05fcf4b6 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,32 @@ +Released 3.4.5 2025-10-10 + +Security fixes: +* CVE-2025-61911 (GHSA-r7r6-cc7p-4v5m): Enforce ``str`` input in + ``ldap.filter.escape_filter_chars`` with ``escape_mode=1``; ensure proper + escaping. (thanks to lukas-eu) +* CVE-2025-61912 (GHSA-p34h-wq7j-h5v6): Correct NUL escaping in + ``ldap.dn.escape_dn_chars`` to ``\00`` per RFC 4514. (thanks to aradona91) + +Fixes: +* ReconnectLDAPObject now properly reconnects on UNAVAILABLE, CONNECT_ERROR + and TIMEOUT exceptions (previously only SERVER_DOWN), fixing reconnection + issues especially during server restarts +* Fixed syncrepl.py to use named constants instead of raw decimal values + for result types +* Fixed error handling in SearchNoOpMixIn to prevent a undefined variable error + +Tests: +* Added comprehensive reconnection test cases including concurrent operation + handling and server restart scenarios + +Doc/ +* Updated installation docs and fixed various documentation typos +* Added ReadTheDocs configuration file + +Infrastructure: +* Add testing and document support for Python 3.13 + +---------------------------------------------------------------- Released 3.4.4 2022-11-17 Fixes: diff --git a/Lib/ldap/cidict.py b/Lib/ldap/cidict.py index f846fd29..65041e0a 100644 --- a/Lib/ldap/cidict.py +++ b/Lib/ldap/cidict.py @@ -85,7 +85,7 @@ def strlist_minus(a,b): a,b are supposed to be lists of case-insensitive strings. """ warnings.warn( - "strlist functions are deprecated and will be removed in 3.5", + "strlist functions are deprecated and will be removed in 4.0", category=DeprecationWarning, stacklevel=2, ) @@ -105,7 +105,7 @@ def strlist_intersection(a,b): Return intersection of two lists of case-insensitive strings a,b. """ warnings.warn( - "strlist functions are deprecated and will be removed in 3.5", + "strlist functions are deprecated and will be removed in 4.0", category=DeprecationWarning, stacklevel=2, ) @@ -125,7 +125,7 @@ def strlist_union(a,b): Return union of two lists of case-insensitive strings a,b. """ warnings.warn( - "strlist functions are deprecated and will be removed in 3.5", + "strlist functions are deprecated and will be removed in 4.0", category=DeprecationWarning, stacklevel=2, ) diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index 7e7b8158..c94df89d 100644 --- a/Lib/ldap/ldapobject.py +++ b/Lib/ldap/ldapobject.py @@ -833,7 +833,7 @@ class ReconnectLDAPObject(SimpleLDAPObject): This class also implements the pickle protocol. - .. versionadded:: 3.5 + .. versionadded:: 3.4.5 The exceptions :py:exc:`ldap.SERVER_DOWN`, :py:exc:`ldap.UNAVAILABLE`, :py:exc:`ldap.CONNECT_ERROR` and :py:exc:`ldap.TIMEOUT` (configurable via :py:attr:`_reconnect_exceptions`) now trigger a reconnect. """ diff --git a/Lib/ldap/pkginfo.py b/Lib/ldap/pkginfo.py index 18ead66c..2ac6852d 100644 --- a/Lib/ldap/pkginfo.py +++ b/Lib/ldap/pkginfo.py @@ -1,6 +1,6 @@ """ meta attributes for packaging which does not import any dependencies """ -__version__ = '3.4.4' +__version__ = '3.4.5' __author__ = 'python-ldap project' __license__ = 'Python style' diff --git a/Lib/ldapurl.py b/Lib/ldapurl.py index b4dfd890..57900028 100644 --- a/Lib/ldapurl.py +++ b/Lib/ldapurl.py @@ -4,7 +4,7 @@ See https://www.python-ldap.org/ for details. """ -__version__ = '3.4.4' +__version__ = '3.4.5' __all__ = [ # constants diff --git a/Lib/ldif.py b/Lib/ldif.py index fa41321c..7bfe5b4c 100644 --- a/Lib/ldif.py +++ b/Lib/ldif.py @@ -3,7 +3,7 @@ See https://www.python-ldap.org/ for details. """ -__version__ = '3.4.4' +__version__ = '3.4.5' __all__ = [ # constants diff --git a/Lib/slapdtest/__init__.py b/Lib/slapdtest/__init__.py index 7c410180..0fabc4c4 100644 --- a/Lib/slapdtest/__init__.py +++ b/Lib/slapdtest/__init__.py @@ -4,7 +4,7 @@ See https://www.python-ldap.org/ for details. """ -__version__ = '3.4.4' +__version__ = '3.4.5' from slapdtest._slapdtest import SlapdObject, SlapdTestCase, SysLogHandler from slapdtest._slapdtest import requires_ldapi, requires_sasl, requires_tls From 7ab1fa99e95710f9007d5cd5736fd383cb0863c6 Mon Sep 17 00:00:00 2001 From: Iwan Date: Sat, 17 Jan 2026 15:26:55 +0100 Subject: [PATCH 32/42] fix(ldap.schema): Explicitly close url file to avoid ResourceWarning in Python 3.14 --- Lib/ldap/schema/subentry.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/ldap/schema/subentry.py b/Lib/ldap/schema/subentry.py index b83d819b..3f73df71 100644 --- a/Lib/ldap/schema/subentry.py +++ b/Lib/ldap/schema/subentry.py @@ -476,10 +476,10 @@ def urlfetch(uri,trace_level=0): l.unbind_s() del l else: - 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] + with urlopen(uri) as ldif_file: + ldif_parser = ldif.LDIFRecordList(ldif_file,max_entries=1) + ldif_parser.parse() + subschemasubentry_dn,s_temp = ldif_parser.all_records[0] # Work-around for mixed-cased attribute names subschemasubentry_entry = ldap.cidict.cidict() s_temp = s_temp or {} From 142a9ca2f3b01ce8e52a298abae1b542f657a393 Mon Sep 17 00:00:00 2001 From: Florian Best Date: Wed, 6 May 2026 18:04:04 +0200 Subject: [PATCH 33/42] fix(ldif): explicitly close sockets after fetching URLs fixes resource warnings in Python 3.14 --- Lib/ldif.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/ldif.py b/Lib/ldif.py index 7bfe5b4c..356f95ea 100644 --- a/Lib/ldif.py +++ b/Lib/ldif.py @@ -373,7 +373,8 @@ def _next_key_and_value(self): if self._process_url_schemes: u = urlparse(url) if u[0] in self._process_url_schemes: - attr_value = urlopen(url).read() + with urlopen(url) as fd: + attr_value = fd.read() else: # All values should be valid ascii; we support UTF-8 as a # non-official, backwards compatibility layer. From de9119d7a0eb7a81931206f756003800bab5766f Mon Sep 17 00:00:00 2001 From: Florian Best Date: Wed, 6 May 2026 18:26:15 +0200 Subject: [PATCH 34/42] ci(github): update github actions > Ubuntu with Python pypy3.10 > Node.js 20 actions are deprecated. The following actions are running on Node.js 20 and may not work as expected: actions/checkout@v4, actions/setup-python@v5. Actions will be forced to run with Node.js 24 by default starting June 2nd, 2026. Node.js 20 will be removed from the runner on September 16th, 2026. Please check if updated versions of these actions are available that support Node.js 24. To opt into Node.js 24 now, set the FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true environment variable on the runner or in your workflow file. Once Node.js 24 becomes the default, you can temporarily opt out by setting ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. For more information see: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ --- .github/workflows/ci.yml | 4 ++-- .github/workflows/tox-fedora.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06d0b2ed..36fde319 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: - "ubuntu-22.04" steps: - name: Checkout - uses: "actions/checkout@v4" + uses: "actions/checkout@v6" - name: Install apt dependencies run: | set -ex @@ -40,7 +40,7 @@ jobs: - name: Disable AppArmor run: sudo aa-disable /usr/sbin/slapd - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} allow-prereleases: true diff --git a/.github/workflows/tox-fedora.yml b/.github/workflows/tox-fedora.yml index bc6f45c5..cef8df1a 100644 --- a/.github/workflows/tox-fedora.yml +++ b/.github/workflows/tox-fedora.yml @@ -9,7 +9,7 @@ jobs: tox_test: name: Tox env "${{matrix.tox_env}}" on Fedora steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Run Tox tests uses: fedora-python/tox-github-action@main with: From 54be2ee27426d81b6cd7a897b872e9401292c2f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Wed, 29 Oct 2025 13:31:06 +0000 Subject: [PATCH 35/42] test: Switch to using slapd + -T for tool use Using a locally compiled OpenLDAP impossible without this as libtool wrappers eat argv[0] of the symlinked binaries. Using a locally compiled OpenLDAP still often needs changes to the configuration, this should be a reasonably easy step forward. --- Lib/slapdtest/_slapdtest.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Lib/slapdtest/_slapdtest.py b/Lib/slapdtest/_slapdtest.py index 00764ac3..1563659b 100644 --- a/Lib/slapdtest/_slapdtest.py +++ b/Lib/slapdtest/_slapdtest.py @@ -264,7 +264,6 @@ def _find_commands(self): self.PATH_LDAPDELETE = self._find_command('ldapdelete') self.PATH_LDAPMODIFY = self._find_command('ldapmodify') self.PATH_LDAPWHOAMI = self._find_command('ldapwhoami') - self.PATH_SLAPADD = self._find_command('slapadd') self.PATH_SLAPD = os.environ.get('SLAPD', None) if not self.PATH_SLAPD: @@ -281,7 +280,7 @@ def _find_command(self, cmd, in_sbin=False): if command is None: raise ValueError( "Command '{}' not found. Set the {} environment variable to " - "override slapdtest's search path.".format(cmd, var_name) + "override slapdtest's search path: {}.".format(cmd, var_name, path) ) return command @@ -352,6 +351,7 @@ def gen_config(self): 'cafile': self.cafile, 'servercert': self.servercert, 'serverkey': self.serverkey, + 'slapd_path': self.SBIN_PATH, } return self.slapd_conf_template % config_dict @@ -513,14 +513,17 @@ def _cli_auth_args(self): # no cover to avoid spurious coverage changes def _cli_popen(self, ldapcommand, extra_args=None, ldap_uri=None, - stdin_data=None): # pragma: no cover + stdin_data=None, tool=None): # pragma: no cover if ldap_uri is None: ldap_uri = self.default_ldap_uri if ldapcommand.split("/")[-1].startswith("ldap"): args = [ldapcommand, '-H', ldap_uri] + self._cli_auth_args() else: - args = [ldapcommand, '-F', self._slapd_conf] + if tool: + args = [ldapcommand, '-T', tool, '-F', self._slapd_conf] + else: + args = [ldapcommand, '-F', self._slapd_conf] args += (extra_args or []) @@ -579,9 +582,10 @@ def slapadd(self, ldif, extra_args=None): Runs slapadd on this slapd instance, passing it the ldif content """ self._cli_popen( - self.PATH_SLAPADD, + self.PATH_SLAPD, stdin_data=ldif.encode("utf-8") if ldif else None, extra_args=extra_args, + tool='add' ) def __enter__(self): From 0dcaa430f2234abb6cbf53113b0f31b30e68b0f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Wed, 29 Oct 2025 13:40:10 +0000 Subject: [PATCH 36/42] test: Store logs and keep failed run data on failure --- Lib/slapdtest/_slapdtest.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Lib/slapdtest/_slapdtest.py b/Lib/slapdtest/_slapdtest.py index 1563659b..b60313b0 100644 --- a/Lib/slapdtest/_slapdtest.py +++ b/Lib/slapdtest/_slapdtest.py @@ -412,12 +412,17 @@ def _start_slapd(self): '-F', self._slapd_conf, '-h', ' '.join(urls), ] + stderr = None if self._log.isEnabledFor(logging.DEBUG): slapd_args.extend(['-d', '-1']) + stderr = os.open(os.path.join(self.testrundir, 'slapd.log'), os.O_WRONLY|os.O_CREAT) else: slapd_args.extend(['-d', '0']) self._log.info('starting slapd: %r', ' '.join(slapd_args)) - self._proc = subprocess.Popen(slapd_args) + self._proc = subprocess.Popen(slapd_args, stderr=stderr) + if stderr is not None: + os.close(stderr) + stderr = None # Waits until the LDAP server socket is open, or slapd crashed deadline = time.monotonic() + 10 # no cover to avoid spurious coverage changes, see @@ -457,7 +462,7 @@ def start(self): self._proc.pid, self.ldap_uri, self.ldapi_uri ) - def stop(self): + def stop(self, cleanup=True): """ Stops the slapd server, and waits for it to terminate and cleans up """ @@ -465,7 +470,8 @@ def stop(self): self._log.debug('stopping slapd with pid %d', self._proc.pid) self._proc.terminate() self.wait() - self._cleanup_rundir() + if cleanup: + self._cleanup_rundir() atexit.unregister(self.stop) def restart(self): @@ -593,7 +599,7 @@ def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): - self.stop() + self.stop(exc_type is None) class SlapdTestCase(unittest.TestCase): @@ -622,4 +628,4 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - cls.server.stop() + cls.server.stop(False) From 7df168b90581ebf5a1821440f3bfb97b152ac4db Mon Sep 17 00:00:00 2001 From: Florian Best Date: Fri, 8 Aug 2025 09:19:45 +0200 Subject: [PATCH 37/42] feat(ldapobject): allow passing `uri=None` in `SimpleLDAPObject` and `ReconnectLDAPObject` to be consistent with `initialize()`. Fixes: 7af31254dcb22a58686cb140ac320af9a6f967fc Issue: #465 --- Lib/ldap/ldapobject.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index c94df89d..fdbd09bc 100644 --- a/Lib/ldap/ldapobject.py +++ b/Lib/ldap/ldapobject.py @@ -67,7 +67,7 @@ class SimpleLDAPObject: } def __init__( - self,uri, + self,uri=None, trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None, bytes_strictness=None, fileno=None ): @@ -848,7 +848,7 @@ class ReconnectLDAPObject(SimpleLDAPObject): _reconnect_exceptions = (ldap.SERVER_DOWN, ldap.UNAVAILABLE, ldap.CONNECT_ERROR, ldap.TIMEOUT) def __init__( - self,uri, + self,uri=None, trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None, bytes_strictness=None, retry_max=1, retry_delay=60.0, fileno=None ): From 4364ede2b6b2da7bbec474548b6c5850c0d08669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20K=C3=B6niger?= Date: Sun, 7 Jan 2024 09:44:32 +0100 Subject: [PATCH 38/42] test: Test valid and invalid attrlist parameters Add tests test_valid_attrlist_parameter_types and test_invalid_attrlist_parameter_types which test the behaviour when passing different Python types. All iterables which return only strings should pass, everything else should raise a TypeError. --- Tests/t_ldapobject.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/Tests/t_ldapobject.py b/Tests/t_ldapobject.py index 87a829a3..ab334b7b 100644 --- a/Tests/t_ldapobject.py +++ b/Tests/t_ldapobject.py @@ -478,6 +478,7 @@ def test_dse(self): [self.server.suffix.encode('utf-8')] ) + def test_compare_s_true(self): base = self.server.suffix l = self._ldap_conn @@ -569,6 +570,38 @@ def test_slapadd(self): ("myAttribute", b'foobar'), ]) + def test_valid_attrlist_parameter_types(self): + """Tests the case when a valid parameter type is passed to search_ext + + Any iterable which only contains strings should not raise any errors. + """ + + l = self._ldap_conn + + valid_attrlist_parameters = [{"a": "2"}, ["a", "b"], {}, set(), set(["a", "b"])] + + for attrlist in valid_attrlist_parameters: + out = l.search_ext( + "%s" % self.server.suffix, ldap.SCOPE_SUBTREE, attrlist=attrlist + ) + + def test_invalid_attrlist_parameter_types(self): + """Tests the case when an invalid parameter type is passed to search_ext + + Any object type that is either not a interable or does contain something + that isn't a string should raise a TypeError. The exception is the string type itself. + """ + + invalid_attrlist_parameters = [{1: 2}, 0, object(), "string"] + + l = self._ldap_conn + + for attrlist in invalid_attrlist_parameters: + with self.assertRaises(TypeError): + l.search_ext( + "%s" % self.server.suffix, ldap.SCOPE_SUBTREE, attrlist=attrlist + ) + class Test01_ReconnectLDAPObject(Test00_SimpleLDAPObject): """ From f3f792e70d0d231efafb459d346e9f8e4bc1ca65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20K=C3=B6niger?= Date: Sun, 7 Jan 2024 10:14:55 +0100 Subject: [PATCH 39/42] fix(LDAPObject): Prevent memory errors in attrs_from_List Function `PySequence_Length` can return -1 on iterables like `dict`. The following PyMem_NEW still succeeds due `PyMem_NEW(char *, -1 + 1)` being equivalent to `char** PyMem_Malloc(1)`, which then can result in a segmentation fault later on. Solution: Use `seq` and `PySequence_Size` to determine the size of the sequence. This way any iterable which contains only strings can be used. Co-authored-by: Christian Heimes --- Modules/LDAPObject.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Modules/LDAPObject.c b/Modules/LDAPObject.c index 71fac73e..a008b5f9 100644 --- a/Modules/LDAPObject.c +++ b/Modules/LDAPObject.c @@ -288,7 +288,10 @@ attrs_from_List(PyObject *attrlist, char ***attrsp) if (seq == NULL) goto error; - len = PySequence_Length(attrlist); + len = PySequence_Size(seq); + if (len == -1) { + goto error; + } attrs = PyMem_NEW(char *, len + 1); From e6439247eed77dd6ea3bb03b8241ac02278ae62c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Thu, 5 Oct 2023 12:23:57 +0100 Subject: [PATCH 40/42] feat(ldap): retrieve libldap version on load --- Modules/constants.c | 8 ++++++++ Modules/ldapmodule.c | 4 ++++ Modules/pythonldap.h | 2 ++ 3 files changed, 14 insertions(+) diff --git a/Modules/constants.c b/Modules/constants.c index f0a0da94..64804e8d 100644 --- a/Modules/constants.c +++ b/Modules/constants.c @@ -229,6 +229,14 @@ LDAPinit_constants(PyObject *m) if (PyModule_AddIntConstant(m, "LIBLDAP_R", thread_safe) != 0) return -1; + if (ldap_get_option(NULL, LDAP_OPT_API_INFO, &ldap_version_info) != LDAP_SUCCESS) { + PyErr_SetString(PyExc_ImportError, "unrecognised libldap version"); + return -1; + } + if (PyModule_AddIntConstant(m, "_VENDOR_VERSION_RUNTIME", + ldap_version_info.ldapai_vendor_version ) != 0) + return -1; + /* Generated constants -- see Lib/ldap/constants.py */ #define add_err(n) do { \ diff --git a/Modules/ldapmodule.c b/Modules/ldapmodule.c index cb3f58fb..d0735356 100644 --- a/Modules/ldapmodule.c +++ b/Modules/ldapmodule.c @@ -5,6 +5,10 @@ #define _STR(x) #x #define STR(x) _STR(x) +LDAPAPIInfo ldap_version_info = { + .ldapai_info_version = LDAP_API_INFO_VERSION, +}; + static char version_str[] = STR(LDAPMODULE_VERSION); static char author_str[] = STR(LDAPMODULE_AUTHOR); static char license_str[] = STR(LDAPMODULE_LICENSE); diff --git a/Modules/pythonldap.h b/Modules/pythonldap.h index 7703af5e..35ed0d92 100644 --- a/Modules/pythonldap.h +++ b/Modules/pythonldap.h @@ -71,6 +71,8 @@ PYLDAP_FUNC(PyObject *) LDAPerror(LDAP *); PYLDAP_FUNC(PyObject *) LDAPraise_for_message(LDAP *, LDAPMessage *m); PYLDAP_FUNC(PyObject *) LDAPerr(int errnum); +PYLDAP_DATA(LDAPAPIInfo) ldap_version_info; + #ifndef LDAP_CONTROL_PAGE_OID #define LDAP_CONTROL_PAGE_OID "1.2.840.113556.1.4.319" #endif /* !LDAP_CONTROL_PAGE_OID */ From 6e471e3b828dc8a382086c7d54a4f8d3b4fd3a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Mon, 21 Aug 2023 19:09:58 +0100 Subject: [PATCH 41/42] feat(ldapobject): Add wrapper for ldap_connect --- Lib/ldap/ldapobject.py | 7 +++++++ Modules/LDAPObject.c | 33 +++++++++++++++++++++++++++++++++ Tests/t_cext.py | 23 +++++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index fdbd09bc..057fe71a 100644 --- a/Lib/ldap/ldapobject.py +++ b/Lib/ldap/ldapobject.py @@ -171,6 +171,13 @@ def fileno(self): """ return self.get_option(ldap.OPT_DESC) + def connect(self): + """ + connect() -> None + Establishes LDAP connection if needed. + """ + return self._ldap_call(self._l.connect) + def abandon_ext(self,msgid,serverctrls=None,clientctrls=None): """ abandon_ext(msgid[,serverctrls=None[,clientctrls=None]]) -> None diff --git a/Modules/LDAPObject.c b/Modules/LDAPObject.c index a008b5f9..f96a6068 100644 --- a/Modules/LDAPObject.c +++ b/Modules/LDAPObject.c @@ -1475,6 +1475,38 @@ l_ldap_extended_operation(LDAPObject *self, PyObject *args) return PyLong_FromLong(msgid); } +/* ldap_connect */ + +static PyObject * +l_ldap_connect(LDAPObject *self, PyObject Py_UNUSED(args)) +{ +#if LDAP_VENDOR_VERSION >= 20500 + int ldaperror; + + if (ldap_version_info.ldapai_vendor_version < 20500) +#endif + { + PyErr_SetString(PyExc_NotImplementedError, + "loaded libldap doesn't support this feature"); + return NULL; + } + +#if LDAP_VENDOR_VERSION >= 20500 + if (not_valid(self)) + return NULL; + + LDAP_BEGIN_ALLOW_THREADS(self); + ldaperror = ldap_connect(self->ldap); + LDAP_END_ALLOW_THREADS(self); + + if ( ldaperror != LDAP_SUCCESS ) + return LDAPerror(self->ldap); + + Py_INCREF(Py_None); + return Py_None; +#endif +} + /* methods */ static PyMethodDef methods[] = { @@ -1504,6 +1536,7 @@ static PyMethodDef methods[] = { {"cancel", (PyCFunction)l_ldap_cancel, METH_VARARGS}, #endif {"extop", (PyCFunction)l_ldap_extended_operation, METH_VARARGS}, + {"connect", (PyCFunction)l_ldap_connect, METH_NOARGS}, {NULL, NULL} }; diff --git a/Tests/t_cext.py b/Tests/t_cext.py index 33fbf29a..846127f8 100644 --- a/Tests/t_cext.py +++ b/Tests/t_cext.py @@ -280,6 +280,29 @@ def test_simple_anonymous_bind(self): self.assertEqual(pmsg, []) self.assertEqual(ctrls, []) + @unittest.skipUnless( + _ldap.VENDOR_VERSION >= 20500 and \ + _ldap._VENDOR_VERSION_RUNTIME >= 20500, + reason="Test requires libldap 2.5+" + ) + def test_connect(self): + l = self._open_conn(bind=False) + invalid_fileno = l.get_option(_ldap.OPT_DESC) + l.connect() + fileno = l.get_option(_ldap.OPT_DESC) + self.assertNotEqual(invalid_fileno, fileno) + + self._bind_conn(l) + + @unittest.skipUnless( + _ldap._VENDOR_VERSION_RUNTIME < 20500, + reason="Test requires linking to libldap < 2.5" + ) + def test_connect_notimpl(self): + l = self._open_conn(bind=False) + with self.assertRaises(NotImplementedError): + l.connect() + 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) From bd877c978d4f029737c033139d7c8429950a8ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Fri, 6 Oct 2023 11:21:06 +0100 Subject: [PATCH 42/42] feat(ldapobject): Add documentation for LDAPObject.connect() --- Doc/reference/ldap.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Doc/reference/ldap.rst b/Doc/reference/ldap.rst index 1d095adb..397b7663 100644 --- a/Doc/reference/ldap.rst +++ b/Doc/reference/ldap.rst @@ -973,6 +973,15 @@ and wait for and return with the server's result, or with The *dn* and *attr* arguments are text strings; see :ref:`bytes_mode`. +.. py:method:: LDAPObject.connect() -> None + + Opens a connection to the server if one is not established already. If that + fails, an instance of :py:exc:`ldap.LDAPError` is raised. + + Requires libldap 2.5+ and will fail with :py:exc:`NotImplementedError` + if that is not met. + + .. py:method:: LDAPObject.delete(dn) -> int .. py:method:: LDAPObject.delete_s(dn) -> None