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