From 94d21331063d36a7ad5a36da360389c1a3e876ab Mon Sep 17 00:00:00 2001 From: Florian Best Date: Sat, 30 Mar 2019 09:35:14 +0100 Subject: [PATCH 1/2] feat(ldap.filter): add `is_filter()` to verify filter syntax --- Lib/ldap/filter.py | 25 +++++++++++++++++++++++++ Modules/functions.c | 28 ++++++++++++++++++++++++++++ Modules/pythonldap.h | 7 +++++++ 3 files changed, 60 insertions(+) diff --git a/Lib/ldap/filter.py b/Lib/ldap/filter.py index 5bd41b21..0f94e0b0 100644 --- a/Lib/ldap/filter.py +++ b/Lib/ldap/filter.py @@ -7,6 +7,7 @@ - Tested with Python 2.0+ """ +import _ldap from ldap import __version__ from ldap.functions import strf_secs @@ -89,3 +90,27 @@ def time_span_filter( until_timestr=strf_secs(until_timestamp), ) # end of time_span_filter() + + +def is_filter(ldap_filter): + """ + Returns True if `ldap_filter' can be parsed as a valid LDAP filter, otherwise False is returned. + """ + if hasattr(_ldap, 'is_filter'): + return _ldap.is_filter(ldap_filter) + + # workaround for libldap <= 2.7 + if '\x00' in ldap_filter: + raise ValueError('embedded null character') + import ldap + lo = ldap.initialize('') + try: + lo.search_ext_s('', ldap.SCOPE_BASE, ldap_filter) + except (ldap.FILTER_ERROR, TypeError, ValueError): + return False + except ldap.SERVER_DOWN: + # the filter syntax is valid, as the connection is not bound we expect SERVER_DOWN here + return True + finally: + lo.unbind() + raise RuntimeError('Could not check filter syntax.') # can not happen diff --git a/Modules/functions.c b/Modules/functions.c index 3f7f7eca..cf69b0c6 100644 --- a/Modules/functions.c +++ b/Modules/functions.c @@ -399,6 +399,31 @@ l_ldap_get_option(PyObject *self, PyObject *args) return LDAP_get_option(NULL, option); } + +#ifdef HAVE_LDAP_PUT_FILTER +/* ldap_is_filter */ +static PyObject *l_ldap_is_filter(PyObject *self, PyObject *args) +{ + const char *filter; + if(!PyArg_ParseTuple(args, "s", &filter)) + return NULL; + + BerElement *ber = ber_alloc_t(LBER_USE_DER); + if(!ber) { + Py_RETURN_FALSE; + } + + int rc = ldap_pvt_put_filter(ber, filter); + ber_free(ber, 1); + + if (rc == 0) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; +} +#endif + + /* methods */ static PyMethodDef methods[] = { @@ -410,6 +435,9 @@ static PyMethodDef methods[] = { {"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}, +#ifdef HAVE_LDAP_PUT_FILTER + {"is_filter", (PyCFunction)l_ldap_is_filter, METH_VARARGS}, +#endif {NULL, NULL} }; diff --git a/Modules/pythonldap.h b/Modules/pythonldap.h index 7703af5e..408670e1 100644 --- a/Modules/pythonldap.h +++ b/Modules/pythonldap.h @@ -42,6 +42,13 @@ LDAP_F(int) ldap_init_fd(ber_socket_t fd, int proto, LDAP_CONST char *url, LDAP **ldp); #endif +#if LDAP_VENDOR_VERSION >= 20700 + /* openldap.h made ldap_pvt_put_filter() public in 2.7.x + * see https://bugs.openldap.org/show_bug.cgi?id=9393 + */ +#define HAVE_LDAP_PUT_FILTER 1 +#endif + #if defined(MS_WINDOWS) #include #else /* unix */ From 2a823e86f8089c7cc6cbda2522ff657b6e470dd4 Mon Sep 17 00:00:00 2001 From: Florian Best Date: Sat, 30 Mar 2019 09:46:07 +0100 Subject: [PATCH 2/2] test(ldap.filter): Implement test cases for `is_filter()` --- Tests/t_ldap_filter.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/Tests/t_ldap_filter.py b/Tests/t_ldap_filter.py index 54312050..af8d6f7b 100644 --- a/Tests/t_ldap_filter.py +++ b/Tests/t_ldap_filter.py @@ -10,14 +10,37 @@ # Switch off processing .ldaprc or ldap.conf before importing _ldap os.environ['LDAPNOINIT'] = '1' -from ldap.filter import escape_filter_chars +from ldap.filter import escape_filter_chars, is_filter, filter_format -class TestDN(unittest.TestCase): +class TestFilter(unittest.TestCase): """ test ldap.functions """ + def test_is_filter(self): + """ + test function is_filter() + """ + self.assertEqual(is_filter(''), True) + self.assertEqual(is_filter('foo='), True) + self.assertEqual(is_filter('foo=bar'), True) + self.assertEqual(is_filter('foo=*'), True) + self.assertEqual(is_filter(filter_format('foo=%s', ['*'])), True) + self.assertEqual(is_filter('(foo=bar)'), True) + self.assertEqual(is_filter('(&(foo=bar))'), True) + self.assertEqual(is_filter('(|(foo=bar))'), True) + self.assertEqual(is_filter(filter_format('foo=%s', ['\x00'])), True) + self.assertEqual(is_filter('foo>='), True) + self.assertEqual(is_filter('(foo>=)'), True) + self.assertEqual(is_filter('foo==bar'), True) + self.assertEqual(is_filter('foobar'), False) + self.assertEqual(is_filter('(foo='), False) + self.assertEqual(is_filter('foo=)'), False) + self.assertEqual(is_filter('=bar'), False) + with self.assertRaises(ValueError): + is_filter('foo=\x00') + def test_escape_filter_chars_mode0(self): """ test function escape_filter_chars() with escape_mode=0