From 686a0389a7bd90e0ddc7b3457afc231b51f42b07 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 1/4] Add wrapper for ldap_connect --- Lib/ldap/ldapobject.py | 9 +++++++++ Modules/LDAPObject.c | 26 ++++++++++++++++++++++++++ Tests/t_cext.py | 13 +++++++++++++ 3 files changed, 48 insertions(+) diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index 7a9c17f6..96bdf557 100644 --- a/Lib/ldap/ldapobject.py +++ b/Lib/ldap/ldapobject.py @@ -171,6 +171,15 @@ def fileno(self): """ return self.get_option(ldap.OPT_DESC) + def connect(self): + """ + connect() -> None + Establishes LDAP connection if needed. + """ + if _ldap.VENDOR_VERSION >= 20500: + return self._ldap_call(self._l.connect) + raise NotImplementedError + 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 da18d575..1ff7d9be 100644 --- a/Modules/LDAPObject.c +++ b/Modules/LDAPObject.c @@ -1505,6 +1505,29 @@ l_ldap_extended_operation(LDAPObject *self, PyObject *args) return PyInt_FromLong(msgid); } +/* ldap_connect */ + +#if LDAP_VENDOR_VERSION >= 20500 +static PyObject * +l_ldap_connect(LDAPObject *self, PyObject Py_UNUSED(args)) +{ + int ldaperror; + + 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[] = { @@ -1534,6 +1557,9 @@ static PyMethodDef methods[] = { {"cancel", (PyCFunction)l_ldap_cancel, METH_VARARGS}, #endif {"extop", (PyCFunction)l_ldap_extended_operation, METH_VARARGS}, +#if LDAP_VENDOR_VERSION >= 20500 + {"connect", (PyCFunction)l_ldap_connect, METH_NOARGS}, +#endif {NULL, NULL} }; diff --git a/Tests/t_cext.py b/Tests/t_cext.py index 33fbf29a..03b927fd 100644 --- a/Tests/t_cext.py +++ b/Tests/t_cext.py @@ -280,6 +280,19 @@ def test_simple_anonymous_bind(self): self.assertEqual(pmsg, []) self.assertEqual(ctrls, []) + @unittest.skipUnless( + _ldap.VENDOR_VERSION >= 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) + 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 f634ae560cafa0816143fd63b4fb641358434e86 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 2/4] WIP+reorder: retrieve libldap version on load --- Modules/constants.c | 8 ++++++++ Modules/constants.h | 2 ++ Modules/ldapmodule.c | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/Modules/constants.c b/Modules/constants.c index 8d6f63b0..6449e1df 100644 --- a/Modules/constants.c +++ b/Modules/constants.c @@ -231,6 +231,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/constants.h b/Modules/constants.h index 7b9ce53e..fe34a12a 100644 --- a/Modules/constants.h +++ b/Modules/constants.h @@ -13,6 +13,8 @@ extern PyObject *LDAPerror(LDAP *); extern PyObject *LDAPraise_for_message(LDAP *, LDAPMessage *m); PyObject *LDAPerr(int errnum); +extern 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 */ diff --git a/Modules/ldapmodule.c b/Modules/ldapmodule.c index 34d5a24c..c2285784 100644 --- a/Modules/ldapmodule.c +++ b/Modules/ldapmodule.c @@ -16,6 +16,10 @@ PyMODINIT_FUNC init_ldap(void); #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); From 0cce151f158f7def1a0066d5cc7e012b7ace49a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Thu, 5 Oct 2023 12:26:32 +0100 Subject: [PATCH 3/4] WIP+fixup: move feature detection to runtime --- Lib/ldap/ldapobject.py | 4 +--- Modules/LDAPObject.c | 15 +++++++++++---- Tests/t_cext.py | 12 +++++++++++- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index 96bdf557..946d0882 100644 --- a/Lib/ldap/ldapobject.py +++ b/Lib/ldap/ldapobject.py @@ -176,9 +176,7 @@ def connect(self): connect() -> None Establishes LDAP connection if needed. """ - if _ldap.VENDOR_VERSION >= 20500: - return self._ldap_call(self._l.connect) - raise NotImplementedError + return self._ldap_call(self._l.connect) def abandon_ext(self,msgid,serverctrls=None,clientctrls=None): """ diff --git a/Modules/LDAPObject.c b/Modules/LDAPObject.c index 1ff7d9be..eb8f3344 100644 --- a/Modules/LDAPObject.c +++ b/Modules/LDAPObject.c @@ -1507,12 +1507,21 @@ l_ldap_extended_operation(LDAPObject *self, PyObject *args) /* ldap_connect */ -#if LDAP_VENDOR_VERSION >= 20500 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; @@ -1525,8 +1534,8 @@ l_ldap_connect(LDAPObject *self, PyObject Py_UNUSED(args)) Py_INCREF(Py_None); return Py_None; -} #endif +} /* methods */ @@ -1557,9 +1566,7 @@ static PyMethodDef methods[] = { {"cancel", (PyCFunction)l_ldap_cancel, METH_VARARGS}, #endif {"extop", (PyCFunction)l_ldap_extended_operation, METH_VARARGS}, -#if LDAP_VENDOR_VERSION >= 20500 {"connect", (PyCFunction)l_ldap_connect, METH_NOARGS}, -#endif {NULL, NULL} }; diff --git a/Tests/t_cext.py b/Tests/t_cext.py index 03b927fd..846127f8 100644 --- a/Tests/t_cext.py +++ b/Tests/t_cext.py @@ -281,7 +281,8 @@ def test_simple_anonymous_bind(self): self.assertEqual(ctrls, []) @unittest.skipUnless( - _ldap.VENDOR_VERSION >= 20500, + _ldap.VENDOR_VERSION >= 20500 and \ + _ldap._VENDOR_VERSION_RUNTIME >= 20500, reason="Test requires libldap 2.5+" ) def test_connect(self): @@ -293,6 +294,15 @@ def test_connect(self): 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 51a8bd178fb5809d5b720d5d830ca6719aded874 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 4/4] Add documentation for LDAPObject.connect() --- Doc/reference/ldap.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Doc/reference/ldap.rst b/Doc/reference/ldap.rst index d059dfa4..733beb09 100644 --- a/Doc/reference/ldap.rst +++ b/Doc/reference/ldap.rst @@ -972,6 +972,16 @@ 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` or appropriate subtype 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