From 0fdbf17bf4204ac65e10f06bc994c53840a0dcd2 Mon Sep 17 00:00:00 2001
From: Julian Rother <julian@jrother.eu>
Date: Fri, 26 Nov 2021 00:01:49 +0100
Subject: [PATCH] Template interface cleanup ([WILDCARD_VALUE] -> WILDCARD)

---
 docs/api.rst          | 12 +++---------
 ldapserver/entries.py | 20 ++++++++++++++------
 tests/test_entries.py | 34 +++++++++++++++++-----------------
 3 files changed, 34 insertions(+), 32 deletions(-)

diff --git a/docs/api.rst b/docs/api.rst
index 3284158..23ba4f4 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -47,8 +47,9 @@ There are different kinds of entries: Object entries holding actual data objects
 .. autoclass:: ldapserver.SubschemaSubentry
 	:members:
 
-.. autodata:: ldapserver.WILDCARD_VALUE
-	:no-value:
+.. py:data:: ldapserver.WILDCARD
+
+	Special wildcard singleton for :class:`EntryTemplate`
 
 .. autoclass:: ldapserver.EntryTemplate
 	:members:
@@ -235,13 +236,6 @@ the diagnostic message.
 .. autoexception:: ldapserver.exceptions.LDAPAffectsMultipleDSAs
 .. autoexception:: ldapserver.exceptions.LDAPOther
 
-Indices and tables
-------------------
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
-
 .. _RFC 4513: https://tools.ietf.org/html/rfc4513
 .. _RFC 4513 5.1.1.: https://tools.ietf.org/html/rfc4513#section-5.1.1
 .. _RFC 4513 5.1.2.: https://tools.ietf.org/html/rfc4513#section-5.1.2
diff --git a/ldapserver/entries.py b/ldapserver/entries.py
index 5a6c931..18bf7e7 100644
--- a/ldapserver/entries.py
+++ b/ldapserver/entries.py
@@ -4,7 +4,7 @@ import enum
 from . import ldap, exceptions
 from .dn import DN, RDN, RDNAssertion
 
-__all__ = ['AttributeDict', 'Entry', 'ObjectEntry', 'RootDSE', 'WILDCARD_VALUE', 'EntryTemplate', 'SubschemaSubentry']
+__all__ = ['AttributeDict', 'Entry', 'ObjectEntry', 'RootDSE', 'WILDCARD', 'EntryTemplate', 'SubschemaSubentry']
 
 class TypeKeysView(collections.abc.Set):
 	def __init__(self, attributes):
@@ -315,9 +315,10 @@ class RootDSE(Entry):
 class WildcardValue:
 	pass
 
-#: Special wildcard value for :class:`EntryTemplate`
 WILDCARD_VALUE = WildcardValue()
 
+WILDCARD = (WILDCARD_VALUE,)
+
 class TemplateFilterResult(enum.Enum):
 	TRUE = enum.auto()
 	FALSE = enum.auto()
@@ -325,6 +326,13 @@ class TemplateFilterResult(enum.Enum):
 	MAYBE_TRUE = enum.auto()
 
 class EntryTemplate(AttributeDict):
+	'''Utility class for dynamically generated object entries
+
+	Set all entry-specific attributes to the special :data:`WILDCARD` value
+	and call :any:EntryTemplate.match_search` to decide if any entries based
+	on the template might match a SEARCH request. Call
+	:any:EntryTemplate.create_entry` with the actual values to create entries
+	based on a template.'''
 	def __init__(self, schema, parent_dn, rdn_attribute, **attributes):
 		super().__init__(schema, **attributes)
 		self.parent_dn = DN(schema, parent_dn)
@@ -507,7 +515,7 @@ class EntryTemplate(AttributeDict):
 		Example:
 
 			>>> subschema = SubschemaSubentry(schema.RFC4519_SCHEMA, 'cn=Subschema')
-			>>> template = subschema.EntryTemplate('ou=users,dc=example,dc=com', 'cn', objectclass=['person', 'top'], sn=[WILDCARD_VALUE])
+			>>> template = subschema.EntryTemplate('ou=users,dc=example,dc=com', 'cn', objectclass=['person', 'top'], sn=WILDCARD)
 			>>> template.extract_search_constraints('cn=test,ou=users,dc=example,dc=com', ldap.SearchScope.baseObject, ldap.FilterEqual('sn', b'foobar'))
 			subschema.AttributeDict(cn=['test'], sn=['foobar'])
 
@@ -527,13 +535,13 @@ class EntryTemplate(AttributeDict):
 		:type rdn_value: any
 		:rtype: Entry
 
-		Template attributes set to :any:`WILDCARD_VALUE` are stripped. Only
-		template attributes set to :any:`WILDCARD_VALUE` may be overwritten
+		Template attributes set to :any:`WILDCARD` are stripped. Only
+		template attributes set to :any:`WILDCARD` may be overwritten
 		with `attributes`.'''
 		obj = Entry(self.schema, DN(self.schema, self.parent_dn, **{self.rdn_attribute: rdn_value}))
 		for key, values in attributes.items():
 			if WILDCARD_VALUE not in self[key]:
-				raise ValueError(f'Cannot set attribute "{key}" that is not set to [WILDCARD_VALUE] in the template')
+				raise ValueError(f'Cannot set attribute "{key}" that is not set to WILDCARD in the template')
 			obj[key] = values
 		for attribute_type, values in self.items():
 			if WILDCARD_VALUE not in values:
diff --git a/tests/test_entries.py b/tests/test_entries.py
index e1d7529..5f87436 100644
--- a/tests/test_entries.py
+++ b/tests/test_entries.py
@@ -2,7 +2,7 @@ import unittest
 import datetime
 
 import ldapserver
-from ldapserver import DN, AttributeDict, ObjectEntry, RootDSE, SubschemaSubentry, EntryTemplate, WILDCARD_VALUE, ldap
+from ldapserver import DN, AttributeDict, ObjectEntry, RootDSE, SubschemaSubentry, EntryTemplate, WILDCARD, ldap
 
 schema = ldapserver.schema.RFC4519_SCHEMA
 
@@ -315,7 +315,7 @@ class TestObjectEntry(unittest.TestCase):
 		obj = ObjectEntry(schema, 'cn=foo,dc=example,dc=com', cn=['foo', 'test'], uid=['foobar'], objectclass=['top'])
 		dn = 'cn=foo,dc=example,dc=com'
 		scope = ldap.SearchScope.baseObject
-		obj = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE], uid=['foobar'])
+		obj = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=WILDCARD, uid=['foobar'])
 		dn = 'dc=example,dc=com'
 		scope = ldap.SearchScope.wholeSubtree
 		# True
@@ -403,7 +403,7 @@ class TestEntryTemplate(unittest.TestCase):
 		obj = EntryTemplate(schema, 'ou=users,dc=example,dc=com', 'uid', cn=['foo', 'bar'], uid=[])
 
 	def test_match_search_dn(self):
-		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE])
+		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=WILDCARD)
 		true_filter = ldap.FilterPresent('objectClass')
 
 		scope = ldap.SearchScope.baseObject
@@ -425,7 +425,7 @@ class TestEntryTemplate(unittest.TestCase):
 		self.assertFalse(template.match_search('cn=test,cn=foo,dc=example,dc=com', scope, true_filter))
 
 	def test_match_search_filter_present(self):
-		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE])
+		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=WILDCARD)
 		dn = 'dc=example,dc=com'
 		scope = ldap.SearchScope.wholeSubtree
 		# True
@@ -439,7 +439,7 @@ class TestEntryTemplate(unittest.TestCase):
 		# We verify in ..._filter_not that Undefined/Maybe are not just False/True
 
 	def test_match_search_filter_not(self):
-		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE])
+		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=WILDCARD)
 		dn = 'dc=example,dc=com'
 		scope = ldap.SearchScope.wholeSubtree
 		# Not True = False
@@ -452,7 +452,7 @@ class TestEntryTemplate(unittest.TestCase):
 		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterPresent('cn'))))
 
 	def test_match_search_filter_and(self):
-		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE])
+		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=WILDCARD)
 		dn = 'dc=example,dc=com'
 		scope = ldap.SearchScope.wholeSubtree
 		true = ldap.FilterPresent('objectclass')
@@ -499,7 +499,7 @@ class TestEntryTemplate(unittest.TestCase):
 		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([undefined, maybe]))))
 
 	def test_match_search_filter_or(self):
-		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE])
+		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=WILDCARD)
 		dn = 'dc=example,dc=com'
 		scope = ldap.SearchScope.wholeSubtree
 		true = ldap.FilterPresent('objectclass')
@@ -546,7 +546,7 @@ class TestEntryTemplate(unittest.TestCase):
 		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([undefined, maybe]))))
 
 	def test_match_search_filter_equal(self):
-		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE])
+		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=WILDCARD)
 		dn = 'dc=example,dc=com'
 		scope = ldap.SearchScope.wholeSubtree
 		# True
@@ -573,7 +573,7 @@ class TestEntryTemplate(unittest.TestCase):
 		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterEqual('name', b'bar')))) # subtype
 
 	def test_match_search_filter_substr(self):
-		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE], uid=['foobar', 'test'])
+		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=WILDCARD, uid=['foobar', 'test'])
 		dn = 'dc=example,dc=com'
 		scope = ldap.SearchScope.wholeSubtree
 		# True
@@ -594,7 +594,7 @@ class TestEntryTemplate(unittest.TestCase):
 		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterSubstrings('cn', [ldap.InitialSubstring(b'foo')]))))
 
 	def test_match_search_filter_le(self):
-		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE], createTimestamp=[datetime.datetime(1994, 12, 16, 10, 32, tzinfo=datetime.timezone.utc)], modifyTimestamp=[WILDCARD_VALUE])
+		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=WILDCARD, createTimestamp=[datetime.datetime(1994, 12, 16, 10, 32, tzinfo=datetime.timezone.utc)], modifyTimestamp=WILDCARD)
 		dn = 'dc=example,dc=com'
 		scope = ldap.SearchScope.wholeSubtree
 		# True
@@ -619,7 +619,7 @@ class TestEntryTemplate(unittest.TestCase):
 		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterLessOrEqual('modifyTimestamp', b'199412161032Z'))))
 
 	def test_match_search_filter_ge(self):
-		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE], createTimestamp=[datetime.datetime(1994, 12, 16, 10, 32, tzinfo=datetime.timezone.utc)], modifyTimestamp=[WILDCARD_VALUE])
+		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=WILDCARD, createTimestamp=[datetime.datetime(1994, 12, 16, 10, 32, tzinfo=datetime.timezone.utc)], modifyTimestamp=WILDCARD)
 		dn = 'dc=example,dc=com'
 		scope = ldap.SearchScope.wholeSubtree
 		# True
@@ -644,7 +644,7 @@ class TestEntryTemplate(unittest.TestCase):
 		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterGreaterOrEqual('modifyTimestamp', b'199412161032Z'))))
 
 	def test_match_search_filter_extensible_attribute_type(self):
-		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE], uid=['foobar'])
+		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=WILDCARD, uid=['foobar'])
 		dn = 'dc=example,dc=com'
 		scope = ldap.SearchScope.wholeSubtree
 		# True
@@ -671,7 +671,7 @@ class TestEntryTemplate(unittest.TestCase):
 		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreMatch', 'cn', b'Foobar', False))))
 
 	def test_match_search_filter_extensible_no_attribute_type(self):
-		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE], uid=['foobar'])
+		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=WILDCARD, uid=['foobar'])
 		dn = 'dc=example,dc=com'
 		scope = ldap.SearchScope.wholeSubtree
 		# True
@@ -694,7 +694,7 @@ class TestEntryTemplate(unittest.TestCase):
 		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreMatch', None, b'foo', False))))
 
 	def test_match_search_filter_extensible_dn(self):
-		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE], uid=['foobar'])
+		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=WILDCARD, uid=['foobar'])
 		dn = 'dc=example,dc=com'
 		scope = ldap.SearchScope.wholeSubtree
 		# True
@@ -719,14 +719,14 @@ class TestEntryTemplate(unittest.TestCase):
 		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreMatch', 'cn', b'foo', True))))
 
 	def test_extract_search_constraints(self):
-		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE], uid=['foobar'])
+		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=WILDCARD, uid=['foobar'])
 		self.assertEqual(dict(template.extract_search_constraints('dc=exapmle,dc=com', ldap.SearchScope.wholeSubtree, ldap.FilterEqual('cn', b'foo')).items()), {'cn': ['foo']})
 		self.assertEqual(dict(template.extract_search_constraints('dc=exapmle,dc=com', ldap.SearchScope.wholeSubtree, ldap.FilterAnd([ldap.FilterEqual('objectclass', b'top'), ldap.FilterEqual('cn', b'foo')])).items()), {'cn': ['foo'], 'objectClass': ['top']})
 		self.assertEqual(dict(template.extract_search_constraints('cn=foo,dc=example,dc=com', ldap.SearchScope.baseObject, ldap.FilterPresent('objectClass')).items()), {'cn': ['foo']})
 		self.assertEqual(dict(template.extract_search_constraints('dc=example,dc=com', ldap.SearchScope.baseObject, ldap.FilterPresent('objectClass')).items()), {})
 
 	def test_create_entry(self):
-		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE], c=[WILDCARD_VALUE], uid=['foobar'])
+		template = EntryTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=WILDCARD, c=WILDCARD, uid=['foobar'])
 		obj = template.create_entry('foo', cn=['foo', 'bar'], c=['DE'])
 		self.assertEqual(obj.dn, DN(schema, 'cn=foo,dc=example,dc=com'))
 		self.assertEqual(dict(obj.items()), {'cn': ['foo', 'bar'], 'uid': ['foobar'], 'c': ['DE'], 'objectClass': ['top']})
@@ -764,7 +764,7 @@ class TestSubschemaSubentry(unittest.TestCase):
 		self.assertIsInstance(rootdse, RootDSE)
 		self.assertIs(rootdse.schema, subschema.schema)
 		self.assertEqual(rootdse['cn'], ['foo'])
-		template = subschema.EntryTemplate('dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE])
+		template = subschema.EntryTemplate('dc=example,dc=com', 'cn', objectclass=['top'], cn=WILDCARD)
 		self.assertIsInstance(template, EntryTemplate)
 		self.assertIs(template.schema, subschema.schema)
 		self.assertEqual(template['subschemaSubentry'], [DN(schema, 'cn=Subschema')])
-- 
GitLab