From 98999bd617aa837f3f57a3add9802b499f3dbba8 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Fri, 5 Jan 2018 17:30:54 +0100 Subject: [PATCH 1/5] Make schema attribute names text Schema attribute names must be text in Python 2, too. Otherwise read_subschemasubentry_s() fails with bytes error. Signed-off-by: Christian Heimes --- Lib/ldap/schema/models.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Lib/ldap/schema/models.py b/Lib/ldap/schema/models.py index c1362a95..9a8cb5b2 100644 --- a/Lib/ldap/schema/models.py +++ b/Lib/ldap/schema/models.py @@ -127,7 +127,7 @@ class ObjectClass(SchemaElement): This list of strings contains NAMEs or OIDs of object classes this object class is derived from """ - schema_attribute = 'objectClasses' + schema_attribute = u'objectClasses' token_defaults = { 'NAME':(()), 'DESC':(None,), @@ -225,7 +225,7 @@ class AttributeType(SchemaElement): This list of strings contains NAMEs or OIDs of attribute types this attribute type is derived from """ - schema_attribute = 'attributeTypes' + schema_attribute = u'attributeTypes' token_defaults = { 'NAME':(()), 'DESC':(None,), @@ -319,7 +319,7 @@ class LDAPSyntax(SchemaElement): Integer flag (0 or 1) indicating whether the attribute type is marked as not human-readable (X-NOT-HUMAN-READABLE) """ - schema_attribute = 'ldapSyntaxes' + schema_attribute = u'ldapSyntaxes' token_defaults = { 'DESC':(None,), 'X-NOT-HUMAN-READABLE':(None,), @@ -367,7 +367,7 @@ class MatchingRule(SchemaElement): syntax String contains OID of the LDAP syntax this matching rule is usable with """ - schema_attribute = 'matchingRules' + schema_attribute = u'matchingRules' token_defaults = { 'NAME':(()), 'DESC':(None,), @@ -413,7 +413,7 @@ class MatchingRuleUse(SchemaElement): This list of strings contains NAMEs or OIDs of attribute types for which this matching rule is used """ - schema_attribute = 'matchingRuleUse' + schema_attribute = u'matchingRuleUse' token_defaults = { 'NAME':(()), 'DESC':(None,), @@ -470,7 +470,7 @@ class DITContentRule(SchemaElement): This list of strings contains NAMEs or OIDs of attributes which may not be present in an entry of the object class """ - schema_attribute = 'dITContentRules' + schema_attribute = u'dITContentRules' token_defaults = { 'NAME':(()), 'DESC':(None,), @@ -527,7 +527,7 @@ class DITStructureRule(SchemaElement): List of strings with NAMEs or OIDs of allowed structural object classes of superior entries in the DIT """ - schema_attribute = 'dITStructureRules' + schema_attribute = u'dITStructureRules' token_defaults = { 'NAME':(()), @@ -591,7 +591,7 @@ class NameForm(SchemaElement): This list of strings contains NAMEs or OIDs of additional attributes an RDN may contain """ - schema_attribute = 'nameForms' + schema_attribute = u'nameForms' token_defaults = { 'NAME':(()), 'DESC':(None,), From 2f2f64c546898cec8cdd076b3114ecd290e2a87e Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Fri, 5 Jan 2018 17:32:42 +0100 Subject: [PATCH 2/5] Add more tests for bytes mode affected functions Tests marked with expected failure are currently affected by bug #147. See: https://github.com/python-ldap/python-ldap/issues/147 Signed-off-by: Christian Heimes --- Tests/t_ldapobject.py | 104 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/Tests/t_ldapobject.py b/Tests/t_ldapobject.py index 2e1d3380..fb480117 100644 --- a/Tests/t_ldapobject.py +++ b/Tests/t_ldapobject.py @@ -161,6 +161,30 @@ def test_bytesmode_search_results_have_bytes(self): for value in values: self.assertEqual(type(value), bytes) + @unittest.skipUnless(PY2, "no bytes_mode under Py3") + @unittest.expectedFailure + def test_bytesmode_search_defaults(self): + l = self._get_bytes_ldapobject() + base = 'cn=Foo1,' + self.server.suffix + kwargs = dict( + base=base.encode('utf-8'), + scope=ldap.SCOPE_SUBTREE, + # filterstr=b'(objectClass=*)' + ) + expected = [ + ( + base, + {'cn': [b'Foo1'], 'objectClass': [b'organizationalRole']} + ), + ] + + result = l.search_s(**kwargs) + self.assertEqual(result, expected) + result = l.search_st(**kwargs) + self.assertEqual(result, expected) + result = l.search_ext_s(**kwargs) + self.assertEqual(result, expected) + @unittest.skipUnless(PY2, "no bytes_mode under Py3") def test_unset_bytesmode_search_warns_bytes(self): l = self._get_bytes_ldapobject(explicit=False) @@ -263,11 +287,51 @@ def test003_search_oneattr(self): [('cn=Foo4,ou=Container,'+self.server.suffix, {'cn': [b'Foo4']})] ) + def test_find_unique_entry(self): + result = self._ldap_conn.find_unique_entry( + self.server.suffix, + ldap.SCOPE_SUBTREE, + '(cn=Foo4)', + ['cn'], + ) + self.assertEqual( + result, + ('cn=Foo4,ou=Container,'+self.server.suffix, {'cn': [b'Foo4']}) + ) + with self.assertRaises(ldap.SIZELIMIT_EXCEEDED): + # > 2 entries returned + self._ldap_conn.find_unique_entry( + self.server.suffix, + ldap.SCOPE_ONELEVEL, + '(cn=Foo*)', + ['*'], + ) + with self.assertRaises(ldap.NO_UNIQUE_ENTRY): + # 0 entries returned + self._ldap_conn.find_unique_entry( + self.server.suffix, + ldap.SCOPE_ONELEVEL, + '(cn=Bar*)', + ['*'], + ) + def test_search_subschema(self): l = self._ldap_conn dn = l.search_subschemasubentry_s() self.assertIsInstance(dn, text_type) self.assertEqual(dn, "cn=Subschema") + subschema = l.read_subschemasubentry_s(dn) + self.assertIsInstance(subschema, dict) + self.assertEqual( + sorted(subschema), + [ + u'attributeTypes', + u'ldapSyntaxes', + u'matchingRuleUse', + u'matchingRules', + u'objectClasses' + ] + ) @unittest.skipUnless(PY2, "no bytes_mode under Py3") def test_search_subschema_have_bytes(self): @@ -275,6 +339,18 @@ def test_search_subschema_have_bytes(self): dn = l.search_subschemasubentry_s() self.assertIsInstance(dn, bytes) self.assertEqual(dn, b"cn=Subschema") + subschema = l.read_subschemasubentry_s(dn) + self.assertIsInstance(subschema, dict) + self.assertEqual( + sorted(subschema), + [ + b'attributeTypes', + b'ldapSyntaxes', + b'matchingRuleUse', + b'matchingRules', + b'objectClasses' + ] + ) def test004_errno107(self): l = self.ldap_object_class('ldap://127.0.0.1:42') @@ -433,6 +509,34 @@ def test_multiple_starttls(self): l.simple_bind_s(self.server.root_dn, self.server.root_pw) self.assertEqual(l.whoami_s(), 'dn:' + self.server.root_dn) + def test_dse(self): + dse = self._ldap_conn.read_rootdse_s() + self.assertIsInstance(dse, dict) + self.assertEqual(dse[u'supportedLDAPVersion'], [b'3']) + self.assertEqual( + sorted(dse), + [u'configContext', u'entryDN', u'namingContexts', u'objectClass', + u'structuralObjectClass', u'subschemaSubentry', + u'supportedControl', u'supportedExtension', u'supportedFeatures', + u'supportedLDAPVersion', u'supportedSASLMechanisms'] + ) + self.assertEqual( + self._ldap_conn.get_naming_contexts(), + [self.server.suffix.encode('utf-8')] + ) + + @unittest.skipUnless(PY2, "no bytes_mode under Py3") + @unittest.expectedFailure + def test_dse_bytes(self): + l = self._get_bytes_ldapobject() + dse = l.read_rootdse_s() + self.assertIsInstance(dse, dict) + self.assertEqual(dse[u'supportedLDAPVersion'], [b'3']) + self.assertEqual( + l.get_naming_contexts(), + [self.server.suffix.encode('utf-8')] + ) + class Test01_ReconnectLDAPObject(Test00_SimpleLDAPObject): """ From e56f61753e6b58b32f571ba9510f4e14ea666786 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Fri, 5 Jan 2018 17:57:13 +0100 Subject: [PATCH 3/5] Add workarounds for default argument types Several default arguments are not compatible with bytes mode. Default to bytes in bytes mode. See: https://github.com/python-ldap/python-ldap/issues/147 Signed-off-by: Christian Heimes --- Lib/ldap/ldapobject.py | 55 +++++++++++++++++++++++++++++++++--------- Tests/t_ldapobject.py | 4 +-- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index f65e09a0..71d298a6 100644 --- a/Lib/ldap/ldapobject.py +++ b/Lib/ldap/ldapobject.py @@ -795,7 +795,12 @@ def search_ext(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrson """ if PY2: base = self._bytesify_input('base', base) - filterstr = self._bytesify_input('filterstr', filterstr) + # workaround for default argument, + # see https://github.com/python-ldap/python-ldap/issues/147 + if self.bytes_mode and filterstr == '(objectClass=*)': + filterstr = b'(objectClass=*)' + else: + filterstr = self._bytesify_input('filterstr', filterstr) if attrlist is not None: attrlist = tuple(self._bytesify_input('attrlist', a) for a in attrlist) @@ -885,7 +890,7 @@ def set_option(self,option,invalue): invalue = RequestControlTuples(invalue) return self._ldap_call(self._l.set_option,option,invalue) - def search_subschemasubentry_s(self,dn=''): + def search_subschemasubentry_s(self,dn=None): """ Returns the distinguished name of the sub schema sub entry for a part of a DIT specified by dn. @@ -895,9 +900,17 @@ def search_subschemasubentry_s(self,dn=''): Returns: None or text/bytes depending on bytes_mode. """ + if self.bytes_mode: + empty_dn = b'' + attrname = b'subschemaSubentry' + else: + empty_dn = u'' + attrname = u'subschemaSubentry' + if dn is None: + dn = empty_dn try: r = self.search_s( - dn,ldap.SCOPE_BASE,'(objectClass=*)',['subschemaSubentry'] + dn,ldap.SCOPE_BASE,'(objectClass=*)',[attrname] ) except (ldap.NO_SUCH_OBJECT,ldap.NO_SUCH_ATTRIBUTE,ldap.INSUFFICIENT_ACCESS): r = [] @@ -906,11 +919,11 @@ def search_subschemasubentry_s(self,dn=''): try: if r: e = ldap.cidict.cidict(r[0][1]) - search_subschemasubentry_dn = e.get('subschemaSubentry',[None])[0] + search_subschemasubentry_dn = e.get(attrname,[None])[0] if search_subschemasubentry_dn is None: if dn: # Try to find sub schema sub entry in root DSE - return self.search_subschemasubentry_s(dn='') + return self.search_subschemasubentry_s(dn=empty_dn) else: # If dn was already root DSE we can return here return None @@ -945,11 +958,19 @@ def read_subschemasubentry_s(self,subschemasubentry_dn,attrs=None): """ Returns the sub schema sub entry's data """ + if self.bytes_mode: + filterstr = b'(objectClass=subschema)' + if attrs is None: + attrs = [attr.encode('utf-8') for attr in SCHEMA_ATTRS] + else: + filterstr = u'(objectClass=subschema)' + if attrs is None: + attrs = SCHEMA_ATTRS try: subschemasubentry = self.read_s( subschemasubentry_dn, - filterstr='(objectClass=subschema)', - attrlist=attrs or SCHEMA_ATTRS + filterstr=filterstr, + attrlist=attrs ) except ldap.NO_SUCH_OBJECT: return None @@ -964,7 +985,7 @@ def find_unique_entry(self,base,scope=ldap.SCOPE_SUBTREE,filterstr='(objectClass base, scope, filterstr, - attrlist=attrlist or ['*'], + attrlist=attrlist, attrsonly=attrsonly, serverctrls=serverctrls, clientctrls=clientctrls, @@ -979,10 +1000,16 @@ def read_rootdse_s(self, filterstr='(objectClass=*)', attrlist=None): """ convenience wrapper around read_s() for reading rootDSE """ + if self.bytes_mode: + base = b'' + attrlist = attrlist or [b'*', b'+'] + else: + base = u'' + attrlist = attrlist or [u'*', u'+'] ldap_rootdse = self.read_s( - '', + base, filterstr=filterstr, - attrlist=attrlist or ['*', '+'], + attrlist=attrlist, ) return ldap_rootdse # read_rootdse_s() @@ -991,9 +1018,13 @@ def get_naming_contexts(self): returns all attribute values of namingContexts in rootDSE if namingContexts is not present (not readable) then empty list is returned """ + if self.bytes_mode: + name = b'namingContexts' + else: + name = u'namingContexts' return self.read_rootdse_s( - attrlist=['namingContexts'] - ).get('namingContexts', []) + attrlist=[name] + ).get(name, []) class ReconnectLDAPObject(SimpleLDAPObject): diff --git a/Tests/t_ldapobject.py b/Tests/t_ldapobject.py index fb480117..243cd86d 100644 --- a/Tests/t_ldapobject.py +++ b/Tests/t_ldapobject.py @@ -162,7 +162,6 @@ def test_bytesmode_search_results_have_bytes(self): self.assertEqual(type(value), bytes) @unittest.skipUnless(PY2, "no bytes_mode under Py3") - @unittest.expectedFailure def test_bytesmode_search_defaults(self): l = self._get_bytes_ldapobject() base = 'cn=Foo1,' + self.server.suffix @@ -335,7 +334,7 @@ def test_search_subschema(self): @unittest.skipUnless(PY2, "no bytes_mode under Py3") def test_search_subschema_have_bytes(self): - l = self._get_bytes_ldapobject(explicit=False) + l = self._get_bytes_ldapobject() dn = l.search_subschemasubentry_s() self.assertIsInstance(dn, bytes) self.assertEqual(dn, b"cn=Subschema") @@ -526,7 +525,6 @@ def test_dse(self): ) @unittest.skipUnless(PY2, "no bytes_mode under Py3") - @unittest.expectedFailure def test_dse_bytes(self): l = self._get_bytes_ldapobject() dse = l.read_rootdse_s() From 274d826f660ef7cd49c0662126d3c41758800cf1 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Fri, 5 Jan 2018 18:07:23 +0100 Subject: [PATCH 4/5] Better hack: filterstr=None Signed-off-by: Christian Heimes --- Doc/reference/ldap.rst | 3 +++ Lib/ldap/ldapobject.py | 33 ++++++++++++++++++++------------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/Doc/reference/ldap.rst b/Doc/reference/ldap.rst index 20994143..467f1d78 100644 --- a/Doc/reference/ldap.rst +++ b/Doc/reference/ldap.rst @@ -1068,7 +1068,10 @@ and wait for and return with the server's result, or with or :py:meth:`search_ext_s()` (client-side search limit). If non-zero not more than *sizelimit* results are returned by the server. + .. versionchanged:: 3.0 + ``filterstr=None`` is equal to ``filterstr='(objectClass=*)'``. + .. py:method:: LDAPObject.start_tls_s() -> None diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index 71d298a6..69431eff 100644 --- a/Lib/ldap/ldapobject.py +++ b/Lib/ldap/ldapobject.py @@ -748,7 +748,7 @@ def result4(self,msgid=ldap.RES_ANY,all=1,timeout=None,add_ctrls=0,add_intermedi resp_data = self._bytesify_results(resp_data, with_ctrls=add_ctrls) return resp_type, resp_data, resp_msgid, decoded_resp_ctrls, resp_name, resp_value - def search_ext(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrsonly=0,serverctrls=None,clientctrls=None,timeout=-1,sizelimit=0): + def search_ext(self,base,scope,filterstr=None,attrlist=None,attrsonly=0,serverctrls=None,clientctrls=None,timeout=-1,sizelimit=0): """ search(base, scope [,filterstr='(objectClass=*)' [,attrlist=None [,attrsonly=0]]]) -> int search_s(base, scope [,filterstr='(objectClass=*)' [,attrlist=None [,attrsonly=0]]]) @@ -793,17 +793,24 @@ def search_ext(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrson The amount of search results retrieved can be limited with the sizelimit parameter if non-zero. """ + if PY2: base = self._bytesify_input('base', base) - # workaround for default argument, - # see https://github.com/python-ldap/python-ldap/issues/147 - if self.bytes_mode and filterstr == '(objectClass=*)': - filterstr = b'(objectClass=*)' + if filterstr is None: + # workaround for default argument, + # see https://github.com/python-ldap/python-ldap/issues/147 + if self.bytes_mode: + filterstr = b'(objectClass=*)' + else: + filterstr = u'(objectClass=*)' else: filterstr = self._bytesify_input('filterstr', filterstr) if attrlist is not None: attrlist = tuple(self._bytesify_input('attrlist', a) for a in attrlist) + else: + if filterstr is None: + filterstr = '(objectClass=*)' return self._ldap_call( self._l.search_ext, base,scope,filterstr, @@ -813,17 +820,17 @@ def search_ext(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrson timeout,sizelimit, ) - def search_ext_s(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrsonly=0,serverctrls=None,clientctrls=None,timeout=-1,sizelimit=0): + def search_ext_s(self,base,scope,filterstr=None,attrlist=None,attrsonly=0,serverctrls=None,clientctrls=None,timeout=-1,sizelimit=0): msgid = self.search_ext(base,scope,filterstr,attrlist,attrsonly,serverctrls,clientctrls,timeout,sizelimit) return self.result(msgid,all=1,timeout=timeout)[1] - def search(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrsonly=0): + def search(self,base,scope,filterstr=None,attrlist=None,attrsonly=0): return self.search_ext(base,scope,filterstr,attrlist,attrsonly,None,None) - def search_s(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrsonly=0): + def search_s(self,base,scope,filterstr=None,attrlist=None,attrsonly=0): return self.search_ext_s(base,scope,filterstr,attrlist,attrsonly,None,None,timeout=self.timeout) - def search_st(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrsonly=0,timeout=-1): + def search_st(self,base,scope,filterstr=None,attrlist=None,attrsonly=0,timeout=-1): return self.search_ext_s(base,scope,filterstr,attrlist,attrsonly,None,None,timeout) def start_tls_s(self): @@ -910,7 +917,7 @@ def search_subschemasubentry_s(self,dn=None): dn = empty_dn try: r = self.search_s( - dn,ldap.SCOPE_BASE,'(objectClass=*)',[attrname] + dn,ldap.SCOPE_BASE,None,[attrname] ) except (ldap.NO_SUCH_OBJECT,ldap.NO_SUCH_ATTRIBUTE,ldap.INSUFFICIENT_ACCESS): r = [] @@ -943,7 +950,7 @@ def read_s(self,dn,filterstr=None,attrlist=None,serverctrls=None,clientctrls=Non r = self.search_ext_s( dn, ldap.SCOPE_BASE, - filterstr or '(objectClass=*)', + filterstr, attrlist=attrlist, serverctrls=serverctrls, clientctrls=clientctrls, @@ -977,7 +984,7 @@ def read_subschemasubentry_s(self,subschemasubentry_dn,attrs=None): else: return subschemasubentry - def find_unique_entry(self,base,scope=ldap.SCOPE_SUBTREE,filterstr='(objectClass=*)',attrlist=None,attrsonly=0,serverctrls=None,clientctrls=None,timeout=-1): + def find_unique_entry(self,base,scope=ldap.SCOPE_SUBTREE,filterstr=None,attrlist=None,attrsonly=0,serverctrls=None,clientctrls=None,timeout=-1): """ Returns a unique entry, raises exception if not unique """ @@ -996,7 +1003,7 @@ def find_unique_entry(self,base,scope=ldap.SCOPE_SUBTREE,filterstr='(objectClass raise NO_UNIQUE_ENTRY('No or non-unique search result for %s' % (repr(filterstr))) return r[0] - def read_rootdse_s(self, filterstr='(objectClass=*)', attrlist=None): + def read_rootdse_s(self, filterstr=None, attrlist=None): """ convenience wrapper around read_s() for reading rootDSE """ From f8f8c9c024fe1b9809c173a1f51e9f055cb6fec3 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 10 Jan 2018 10:59:28 +0100 Subject: [PATCH 5/5] Wording change in search* docs --- Doc/reference/ldap.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/reference/ldap.rst b/Doc/reference/ldap.rst index 467f1d78..fdd5d356 100644 --- a/Doc/reference/ldap.rst +++ b/Doc/reference/ldap.rst @@ -1070,7 +1070,7 @@ and wait for and return with the server's result, or with .. versionchanged:: 3.0 - ``filterstr=None`` is equal to ``filterstr='(objectClass=*)'``. + ``filterstr=None`` is equivalent to ``filterstr='(objectClass=*)'``. .. py:method:: LDAPObject.start_tls_s() -> None