diff --git a/Doc/reference/ldap.rst b/Doc/reference/ldap.rst index 20994143..fdd5d356 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 equivalent to ``filterstr='(objectClass=*)'``. + .. py:method:: LDAPObject.start_tls_s() -> None diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index f65e09a0..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,12 +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) - filterstr = self._bytesify_input('filterstr', filterstr) + 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, @@ -808,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): @@ -885,7 +897,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 +907,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,None,[attrname] ) except (ldap.NO_SUCH_OBJECT,ldap.NO_SUCH_ATTRIBUTE,ldap.INSUFFICIENT_ACCESS): r = [] @@ -906,11 +926,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 @@ -930,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, @@ -945,18 +965,26 @@ 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 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 """ @@ -964,7 +992,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, @@ -975,14 +1003,20 @@ 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 """ + 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 +1025,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/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,), diff --git a/Tests/t_ldapobject.py b/Tests/t_ldapobject.py index 2e1d3380..243cd86d 100644 --- a/Tests/t_ldapobject.py +++ b/Tests/t_ldapobject.py @@ -161,6 +161,29 @@ 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") + 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,18 +286,70 @@ 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): - 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") + 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 +508,33 @@ 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") + 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): """