From 698d9cab5421d61b7136f6aa39d56897af7d58a7 Mon Sep 17 00:00:00 2001
From: Julian Rother <julian@jrother.eu>
Date: Mon, 1 Nov 2021 23:03:18 +0100
Subject: [PATCH] Fixed schema syntax registration and schema attribute
 matching

---
 ldapserver/objects.py                 |  8 +--
 ldapserver/schema/rfc4517/syntaxes.py | 17 ++++--
 ldapserver/schema/types.py            | 85 ++++++++++++++++++++++++++-
 3 files changed, 100 insertions(+), 10 deletions(-)

diff --git a/ldapserver/objects.py b/ldapserver/objects.py
index dcc32e9..6140f96 100644
--- a/ldapserver/objects.py
+++ b/ldapserver/objects.py
@@ -446,10 +446,10 @@ class SubschemaSubentry(Object):
 		self['subschemaSubentry'] = [self.dn]
 		self['structuralObjectClass'] = ['subtree']
 		self['objectClass'] = ['top', 'subtree', 'subschema']
-		self['objectClasses'] = [item.to_definition() for item in schema.object_classes]
-		self['ldapSyntaxes'] = [item.to_definition() for item in schema.syntaxes]
-		self['matchingRules'] = [item.to_definition() for item in schema.matching_rules]
-		self['attributeTypes'] = [item.to_definition() for item in schema.attribute_types]
+		self['objectClasses'] = schema.object_classes
+		self['ldapSyntaxes'] = schema.syntaxes
+		self['matchingRules'] = schema.matching_rules
+		self['attributeTypes'] = schema.attribute_types
 		# pylint: disable=invalid-name
 		self.AttributeDict = lambda **attributes: AttributeDict(schema, **attributes)
 		self.Object = lambda *args, **attributes: Object(schema, *args, subschemaSubentry=[self.dn], **attributes)
diff --git a/ldapserver/schema/rfc4517/syntaxes.py b/ldapserver/schema/rfc4517/syntaxes.py
index cf63fd1..ea02285 100644
--- a/ldapserver/schema/rfc4517/syntaxes.py
+++ b/ldapserver/schema/rfc4517/syntaxes.py
@@ -23,8 +23,17 @@ class BytesSyntax(Syntax):
 	def decode(schema, raw_value):
 		return raw_value
 
+class SchemaElementSyntax(Syntax):
+	@staticmethod
+	def encode(schema, value):
+		return value.to_definition().encode('utf8')
+
+	@staticmethod
+	def decode(schema, raw_value):
+		return None
+
 # Syntax definitions
-class AttributeTypeDescription(StringSyntax):
+class AttributeTypeDescription(SchemaElementSyntax):
 	oid = '1.3.6.1.4.1.1466.115.121.1.3'
 	desc = 'Attribute Type Description'
 
@@ -174,11 +183,11 @@ class JPEG(BytesSyntax):
 	oid = '1.3.6.1.4.1.1466.115.121.1.28'
 	desc = 'JPEG'
 
-class LDAPSyntaxDescription(StringSyntax):
+class LDAPSyntaxDescription(SchemaElementSyntax):
 	oid = '1.3.6.1.4.1.1466.115.121.1.54'
 	desc = 'LDAP Syntax Description'
 
-class MatchingRuleDescription(StringSyntax):
+class MatchingRuleDescription(SchemaElementSyntax):
 	oid = '1.3.6.1.4.1.1466.115.121.1.30'
 	desc = 'Matching Rule Description'
 
@@ -223,7 +232,7 @@ class NumericString(StringSyntax):
 	oid = '1.3.6.1.4.1.1466.115.121.1.36'
 	desc = 'Numeric String'
 
-class ObjectClassDescription(StringSyntax):
+class ObjectClassDescription(SchemaElementSyntax):
 	oid = '1.3.6.1.4.1.1466.115.121.1.37'
 	desc = 'Object Class Description'
 
diff --git a/ldapserver/schema/types.py b/ldapserver/schema/types.py
index cac17ae..3af0c06 100644
--- a/ldapserver/schema/types.py
+++ b/ldapserver/schema/types.py
@@ -13,6 +13,10 @@ def escape(string):
 	return result
 
 class Syntax:
+	'''LDAP syntax for attribute and assertion values
+
+	Instances of the class represent (optionally length-constrained) syntax
+	refercences as used with `MatchingRule` and `AttributeType`.'''
 	oid: str
 	desc: str
 
@@ -25,15 +29,19 @@ class Syntax:
 
 	@classmethod
 	def get_first_component_oid(cls):
+		'''Used by objectIdentifierFirstComponentMatch'''
 		return cls.oid
 
 	@classmethod
 	def to_definition(cls):
+		'''Get string reperesentation as used in ldapSyntaxes attribute'''
 		return f"( {cls.oid} DESC '{escape(cls.desc)}' )"
 
 	def decode(self, schema, raw_value):
 		'''Decode LDAP-specific encoding of a value to a native value
 
+		:param schema: Schema of the object in whose context decoding takes place
+		:type schema: Schema
 		:param raw_value: LDAP-specific encoding of the value
 		:type raw_value: bytes
 
@@ -44,6 +52,8 @@ class Syntax:
 	def encode(self, schema, value):
 		'''Encode native value to its LDAP-specific encoding
 
+		:param schema: Schema of the object in whose context encoding takes place
+		:type schema: Schema
 		:param value: native value (depends on syntax)
 		:type value: any
 
@@ -52,6 +62,16 @@ class Syntax:
 		raise NotImplementedError()
 
 class MatchingRule:
+	'''Matching rules define how to compare attribute with assertion values
+
+	Instances provide a number of methods to compare a single attribute value
+	to an assertion value. Attribute values are passed as the native type of
+	the attribute type's syntax. Assertion values are always decoded with the
+	matching rule's syntax before being passed to a matching method.
+
+	LDAP differenciates between EQUALITY, SUBSTR and ORDERING matching rules.
+	This class is used for all of them. Inappropriate matching methods should
+	always return None. '''
 	def __init__(self, oid, name, syntax, **kwargs):
 		self.oid = oid
 		self.name = name
@@ -61,27 +81,76 @@ class MatchingRule:
 			setattr(self, key, value)
 
 	def to_definition(self):
+		'''Get string reperesentation as used in matchingRules attribute'''
 		return f"( {self.oid} NAME '{escape(self.name)}' SYNTAX {self.syntax.ref} )"
 
+	def get_first_component_oid(self):
+		'''Used by objectIdentifierFirstComponentMatch'''
+		return self.oid
+
 	def __repr__(self):
 		return f'<ldapserver.schema.MatchingRule {self.oid}>'
 
 	def match_equal(self, schema, attribute_value, assertion_value):
+		'''Return whether attribute value is equal to assertion values
+
+		Only available for EQUALITY matching rules.
+
+		:returns: True if attribute value matches the assertion value, False if it
+		          does not match. If the result is Undefined, None is returned.'''
 		return None
 
 	def match_approx(self, schema, attribute_value, assertion_value):
+		'''Return whether attribute value is approximatly equal to assertion values
+
+		Only available for EQUALITY matching rules.
+
+		:returns: True if attribute value matches the assertion value, False if it
+		          does not match. If the result is Undefined, None is returned.'''
 		return self.match_equal(schema, attribute_value, assertion_value)
 
 	def match_less(self, schema, attribute_value, assertion_value):
+		'''Return whether attribute value is less than assertion values
+
+		Only available for ORDERING matching rules.
+
+		:returns: True if attribute value matches the assertion value, False if it
+		          does not match. If the result is Undefined, None is returned.'''
 		return None
 
 	def match_greater_or_equal(self, schema, attribute_value, assertion_value):
+		'''Return whether attribute value is greater than or equal to assertion values
+
+		Only available for ORDERING matching rules.
+
+		:returns: True if attribute value matches the assertion value, False if it
+		          does not match. If the result is Undefined, None is returned.'''
 		return None
 
 	def match_substr(self, schema, attribute_value, inital_substring, any_substrings, final_substring):
+		'''Return whether attribute value matches substring assertion
+
+		Only available for SUBSTR matching rules.
+
+		The type of `inital_substring`, `any_substrings` and `final_substring`
+		depends on the syntax of the attribute's equality matching rule!
+
+		:param schema: Schema of the object whose attribute value is matched
+		:type schema: Schema
+		:param attribute_value: Attribute value (type according to attribute's syntax)
+		:type attribute_value: any
+		:param inital_substring: Substring to match the beginning (optional)
+		:type inital_substring: any
+		:param any_substrings: List of substrings to match between initial and final in order
+		:type any_substrings: list of any
+		:param final_substring: Substring to match the end (optional)
+		:type final_substring: any
+		:returns: True if attribute value matches the assertion value, False if it
+		          does not match. If the result is Undefined, None is returned.'''
 		return None
 
 class AttributeTypeUsage(enum.Enum):
+	'''Values for usage argument of `AttributeTypeUsage`'''
 	# pylint: disable=invalid-name
 	# user
 	userApplications = enum.auto()
@@ -93,6 +162,7 @@ class AttributeTypeUsage(enum.Enum):
 	dSAOperation = enum.auto()
 
 class AttributeType:
+	'''Represents an attribute type within a schema'''
 	# pylint: disable=too-many-instance-attributes,too-many-arguments,too-many-branches,too-many-statements
 	def __init__(self, oid, name=None, desc=None, obsolete=None, sup=None,
 	             equality=None, ordering=None, substr=None, syntax=None,
@@ -153,20 +223,24 @@ class AttributeType:
 		self.usage = usage or AttributeTypeUsage.userApplications
 
 	def to_definition(self):
+		'''Get string reperesentation as used in attributeTypes attribute'''
 		return self.schema_encoding
 
 	def get_first_component_oid(self):
+		'''Used by objectIdentifierFirstComponentMatch'''
 		return self.oid
 
 	def __repr__(self):
 		return f'<ldapserver.schema.AttributeType {self.oid}>'
 
 class ObjectClassKind(enum.Enum):
+	'''Values for kind argument of `ObjectClass`'''
 	ABSTRACT = enum.auto()
 	STRUCTURAL = enum.auto()
 	AUXILIARY = enum.auto()
 
 class ObjectClass:
+	'''Representation of an object class wihin a schema'''
 	# pylint: disable=too-many-arguments
 	def __init__(self, oid, name=None, desc=None, obsolete=None, sup=None,
 	             kind=None, must=None, may=None):
@@ -212,9 +286,11 @@ class ObjectClass:
 		self.may = may or []
 
 	def to_definition(self):
+		'''Get string reperesentation as used in objectClasses attribute'''
 		return self.schema_encoding
 
 	def get_first_component_oid(self):
+		'''Used by objectIdentifierFirstComponentMatch'''
 		return self.oid
 
 	def __repr__(self):
@@ -247,6 +323,11 @@ class Schema:
 			self.register_object_class(object_class)
 
 	def extend(self, *schemas, object_classes=None, attribute_types=None, matching_rules=None, syntaxes=None):
+		'''Return new Schema instance with all object classes, attribute types,
+		matching rules and syntaxes from this schema and additional ones.
+
+		:return: Schema object with all schema data combined
+		:rtype: Schema'''
 		object_classes = self.object_classes + (object_classes or [])
 		attribute_types = self.attribute_types + (attribute_types or [])
 		matching_rules = self.matching_rules + (matching_rules or [])
@@ -275,7 +356,7 @@ class Schema:
 		:type matching_rule: MatchingRule'''
 		if matching_rule in self.matching_rules:
 			return
-		self.register_syntax(matching_rule.syntax)
+		self.register_syntax(type(matching_rule.syntax))
 		self.register_oid(matching_rule.oid, *matching_rule.names)
 		self.matching_rules.append(matching_rule)
 
@@ -286,7 +367,7 @@ class Schema:
 		:type attribute_type: AttributeType'''
 		if attribute_type in self.attribute_types:
 			return
-		self.register_syntax(attribute_type.syntax)
+		self.register_syntax(type(attribute_type.syntax))
 		if attribute_type.equality:
 			self.register_matching_rule(attribute_type.equality)
 		if attribute_type.ordering:
-- 
GitLab