From d39ad2180b0a3fc183eb5120604a96ec91de4eec Mon Sep 17 00:00:00 2001
From: Julian Rother <julian@jrother.eu>
Date: Thu, 2 Dec 2021 18:52:55 +0100
Subject: [PATCH] Support RFC3673 "All Operational Attributes"

---
 ldapserver/entries.py      | 2 ++
 ldapserver/ldap.py         | 3 +++
 ldapserver/schema/types.py | 7 ++++++-
 ldapserver/server.py       | 1 +
 tests/test_entries.py      | 5 +++++
 5 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/ldapserver/entries.py b/ldapserver/entries.py
index 999897b..9bd34b7 100644
--- a/ldapserver/entries.py
+++ b/ldapserver/entries.py
@@ -255,6 +255,8 @@ class Entry(AttributeDict):
 		for selector in attributes or ['*']:
 			if selector == '*':
 				selected_attributes |= self.schema.user_attribute_types
+			elif selector == '+':
+				selected_attributes |= self.schema.operational_attribute_types
 			elif selector == '1.1':
 				continue
 			elif selector in self.schema.attribute_types:
diff --git a/ldapserver/ldap.py b/ldapserver/ldap.py
index 1c04d8b..7386131 100644
--- a/ldapserver/ldap.py
+++ b/ldapserver/ldap.py
@@ -727,5 +727,8 @@ class PagedResultsValue(asn1.Sequence):
 	size: int
 	cookie: bytes
 
+# LDAP All Operational Attributes (RFC3673)
+ALL_OPERATIONAL_ATTRS_OID = '1.3.6.1.4.1.4203.1.5.1'
+
 # 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/schema/types.py b/ldapserver/schema/types.py
index 8eeac16..4f3dcee 100644
--- a/ldapserver/schema/types.py
+++ b/ldapserver/schema/types.py
@@ -205,7 +205,9 @@ class AttributeType:
 		self.is_operational = (definition.usage != AttributeTypeUsage.userApplications)
 		schema._register(self, self.oid, self.ref, *self.names)
 		schema.attribute_types._register(self, self.oid, self.ref, *self.names)
-		if not self.is_operational:
+		if self.is_operational:
+			schema.operational_attribute_types.add(self)
+		else:
 			schema.user_attribute_types.add(self)
 		#: Set of all matching rules in schema that can be applied to values of the attribute type
 		self.compatible_matching_rules = self.syntax.compatible_matching_rules
@@ -462,7 +464,10 @@ class Schema(OIDDict):
 		                              for item in attribute_type_definitions or []]
 		#: Mapping of attribute type OIDs and short descriptive names to :any:`AttributeType` objects
 		self.attribute_types = OIDDict()
+		#: Set of user (non-operational) attribute types
 		self.user_attribute_types = set()
+		#: Set of operational (non-user) attribute types
+		self.operational_attribute_types = set()
 		# Attribute types may refer to other (superior) attribute types. To resolve
 		# these dependencies we cycle through the definitions, each time adding
 		# those not added yet with fulfilled dependencies. Finally we add all the
diff --git a/ldapserver/server.py b/ldapserver/server.py
index cdf2729..03789fc 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.ALL_OPERATIONAL_ATTRS_OID)
 		self.rootdse['supportedFeatures'].append(ldap.ABSOLUTE_TRUE_FALSE_OID)
 		self.rootdse['supportedLDAPVersion'] = ['3']
 		self.bind_object = None
diff --git a/tests/test_entries.py b/tests/test_entries.py
index 20c9e2d..1947dc6 100644
--- a/tests/test_entries.py
+++ b/tests/test_entries.py
@@ -365,6 +365,11 @@ class TestObjectEntry(unittest.TestCase):
 		self.assertEqual(len(result.attributes), 2)
 		self.assertEqual({item.type: item.vals for item in result.attributes},
 		                 {'cn': [b'foo', b'bar'], 'objectClass': [b'top']})
+		result = obj.search('cn=foo,dc=example,dc=com', ldap.SearchScope.baseObject, ldap.FilterPresent('objectclass'), ['+'], False)
+		self.assertEqual(result.objectName, 'cn=foo,dc=example,dc=com')
+		self.assertEqual(len(result.attributes), 1)
+		self.assertEqual({item.type: item.vals for item in result.attributes},
+		                 {'subschemaSubentry': [b'cn=subschema']})
 		result = obj.search('cn=foo,dc=example,dc=com', ldap.SearchScope.baseObject, ldap.FilterPresent('objectclass'), ['1.1'], False)
 		self.assertEqual(result.objectName, 'cn=foo,dc=example,dc=com')
 		self.assertEqual(len(result.attributes), 0)
-- 
GitLab