diff --git a/ldapserver/entries.py b/ldapserver/entries.py index 18bf7e7af27f5d2eb903a01dc38c37bf4eba5956..999897b932925df44eb6021ee7c096adc2c7c277 100644 --- a/ldapserver/entries.py +++ b/ldapserver/entries.py @@ -137,6 +137,7 @@ class Entry(AttributeDict): def __search_match_filter(self, filter_obj): # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements,too-many-nested-blocks if isinstance(filter_obj, ldap.FilterAnd): + # RFC4526: An 'and' filter consisting of an empty set of filters SHALL evaluate to True. result = FilterResult.TRUE for subfilter in filter_obj.filters: subresult = self.__search_match_filter(subfilter) @@ -146,6 +147,7 @@ class Entry(AttributeDict): result = FilterResult.UNDEFINED return result elif isinstance(filter_obj, ldap.FilterOr): + # RFC4526: An 'or' filter consisting of an empty set of filters SHALL evaluate to False. result = FilterResult.FALSE for subfilter in filter_obj.filters: subresult = self.__search_match_filter(subfilter) @@ -364,6 +366,7 @@ class EntryTemplate(AttributeDict): def __search_match_filter(self, filter_obj): # pylint: disable=too-many-return-statements,too-many-branches,too-many-nested-blocks,too-many-statements if isinstance(filter_obj, ldap.FilterAnd): + # RFC4526: An 'and' filter consisting of an empty set of filters SHALL evaluate to True. result = TemplateFilterResult.TRUE for subfilter in filter_obj.filters: subresult = self.__search_match_filter(subfilter) @@ -375,6 +378,7 @@ class EntryTemplate(AttributeDict): result = TemplateFilterResult.MAYBE_TRUE return result elif isinstance(filter_obj, ldap.FilterOr): + # RFC4526: An 'or' filter consisting of an empty set of filters SHALL evaluate to False. result = TemplateFilterResult.FALSE for subfilter in filter_obj.filters: subresult = self.__search_match_filter(subfilter) diff --git a/ldapserver/ldap.py b/ldapserver/ldap.py index 980c3ac8f6375bff8f44f5e76e2e24315f7559e2..1c04d8b816e3d347f933df0199d2718b906c57d7 100644 --- a/ldapserver/ldap.py +++ b/ldapserver/ldap.py @@ -49,7 +49,10 @@ class Filter(asn1.Choice, ABC): raise NotImplementedError() class FilterAnd(asn1.Wrapper, Filter): - '''AND conjunction of multiple filters ``(&filters...)``''' + '''AND conjunction of multiple filters ``(&filters...)`` + + Supports RFC4526: Empty AND filters (``(&)``) are considered valid and + evaluate to TRUE.''' BER_TAG = (2, True, 0) WRAPPED_ATTRIBUTE = 'filters' WRAPPED_TYPE = asn1.Set @@ -65,7 +68,10 @@ class FilterAnd(asn1.Wrapper, Filter): return '(&%s)'%(''.join([str(subfilter) for subfilter in self.filters])) class FilterOr(asn1.Wrapper, Filter): - '''OR conjunction of multiple filters ``(|filters...)``''' + '''OR conjunction of multiple filters ``(|filters...)`` + + Supports RFC4526: Empty OR filters (``(|)``) are considered valid and + evaluate to FALSE.''' BER_TAG = (2, True, 1) WRAPPED_ATTRIBUTE = 'filters' WRAPPED_TYPE = asn1.Set @@ -720,3 +726,6 @@ class PagedResultsValue(asn1.Sequence): size: int cookie: bytes + +# LDAP Absolute True and False Filters (RFC4526) +ABSOLUTE_TRUE_FALSE_OID = '1.3.6.1.4.1.4203.1.5.3' diff --git a/ldapserver/server.py b/ldapserver/server.py index f9923cc438f95250decbc80c05dd55d93a57169b..cdf2729102ee905fd2b8d35392a3fc53a9753de2 100644 --- a/ldapserver/server.py +++ b/ldapserver/server.py @@ -198,6 +198,7 @@ class LDAPRequestHandler(BaseLDAPRequestHandler): self.rootdse['supportedSASLMechanisms'].append('PLAIN') if self.supports_sasl_external: self.rootdse['supportedSASLMechanisms'].append('EXTERNAL') + self.rootdse['supportedFeatures'].append(ldap.ABSOLUTE_TRUE_FALSE_OID) self.rootdse['supportedLDAPVersion'] = ['3'] self.bind_object = None self.__bind_sasl_state = None # Set to (mechanism, iterator) by handle_bind diff --git a/tests/test_entries.py b/tests/test_entries.py index 5f8743625698c2d1f482854a91c99dd8c4447f46..20c9e2d72cacd735890e8107d4bc89c18fe0bb69 100644 --- a/tests/test_entries.py +++ b/tests/test_entries.py @@ -170,6 +170,8 @@ class TestObjectEntry(unittest.TestCase): self.assertFalse(obj.match_search(dn, scope, ldap.FilterAnd([false, undefined]))) # True and Undefined = Undefined (behaves like False) self.assertFalse(obj.match_search(dn, scope, ldap.FilterAnd([true, undefined]))) + # Empty And = True (RFC4526) + self.assertTrue(obj.match_search(dn, scope, ldap.FilterAnd([]))) # Not True = False self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([true])))) @@ -183,6 +185,8 @@ class TestObjectEntry(unittest.TestCase): self.assertTrue(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([false, undefined])))) # Not (True and Undefined) = Undefined (behaves like False) self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([true, undefined])))) + # Not (Empty And) = False (RFC4526) + self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([])))) def test_match_search_filter_or(self): obj = ObjectEntry(schema, 'cn=foo,dc=example,dc=com', cn=['foo', 'bar'], uid=[], objectclass=['top']) @@ -203,6 +207,8 @@ class TestObjectEntry(unittest.TestCase): self.assertTrue(obj.match_search(dn, scope, ldap.FilterOr([true, undefined]))) # False or Undefined = Undefined (behaves like False) self.assertFalse(obj.match_search(dn, scope, ldap.FilterOr([false, undefined]))) + # Empty Or = False (RFC4526) + self.assertFalse(obj.match_search(dn, scope, ldap.FilterOr([]))) # Not True = False self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([true])))) @@ -216,6 +222,8 @@ class TestObjectEntry(unittest.TestCase): self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([true, undefined])))) # Not (False or Undefined) = Undefined (behaves like False) self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([false, undefined])))) + # Not (Empty Or) = True (RFC4526) + self.assertTrue(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([])))) def test_match_search_filter_equal(self): obj = ObjectEntry(schema, 'cn=foo,dc=example,dc=com', cn=['foo', 'bar'], uid=[], objectclass=['top']) @@ -478,6 +486,8 @@ class TestEntryTemplate(unittest.TestCase): self.assertTrue(template.match_search(dn, scope, ldap.FilterAnd([true, maybe]))) # Undefined and Maybe = Undefined (behaves like False) self.assertFalse(template.match_search(dn, scope, ldap.FilterAnd([undefined, maybe]))) + # Empty And = True (RFC4526) + self.assertTrue(template.match_search(dn, scope, ldap.FilterAnd([]))) # Not True = False self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([true])))) @@ -497,6 +507,8 @@ class TestEntryTemplate(unittest.TestCase): self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([true, maybe])))) # Not (Undefined and Maybe) = Undefined (behaves like False) self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([undefined, maybe])))) + # Not (Empty And) = False (RFC4526) + self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([])))) def test_match_search_filter_or(self): template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=WILDCARD) @@ -525,6 +537,8 @@ class TestEntryTemplate(unittest.TestCase): self.assertTrue(template.match_search(dn, scope, ldap.FilterOr([false, maybe]))) # Undefined or Maybe = Maybe (behaves like True) self.assertTrue(template.match_search(dn, scope, ldap.FilterOr([undefined, maybe]))) + # Empty Or = False (RFC4526) + self.assertFalse(template.match_search(dn, scope, ldap.FilterOr([]))) # Not True = False self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([true])))) @@ -544,6 +558,8 @@ class TestEntryTemplate(unittest.TestCase): self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([false, maybe])))) # Not (Undefined or Maybe) = Maybe (behaves like True) self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([undefined, maybe])))) + # Not (Empty Or) = True (RFC4526) + self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([])))) def test_match_search_filter_equal(self): template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=WILDCARD)