From 26b2d4d9130a5267943bf8bafc4e3aedbc0b0d92 Mon Sep 17 00:00:00 2001 From: Julian Rother <julian@jrother.eu> Date: Wed, 13 Oct 2021 17:26:16 +0200 Subject: [PATCH] Functional schema support, replaced Directory with Object --- .pylintrc | 2 + examples/passwd.py | 38 + examples/static.py | 16 + ldapserver/__init__.py | 2 +- ldapserver/directory.py | 257 ------- ldapserver/dn.py | 19 +- ldapserver/ldap.py | 108 ++- ldapserver/rfc4518_stringprep.py | 415 +++++++++++ ldapserver/schema.py | 364 --------- ldapserver/schema/__init__.py | 14 + ldapserver/schema/rfc1274/__init__.py | 1 + ldapserver/schema/rfc1274/attribute_types.py | 10 + ldapserver/schema/rfc1274/syntaxes.py | 5 + ldapserver/schema/rfc2079/__init__.py | 1 + ldapserver/schema/rfc2079/attribute_types.py | 8 + ldapserver/schema/rfc2079/matching_rules.py | 5 + ldapserver/schema/rfc2079/object_classes.py | 9 + ldapserver/schema/rfc2079/syntaxes.py | 5 + ldapserver/schema/rfc2252/__init__.py | 1 + ldapserver/schema/rfc2252/syntaxes.py | 19 + ldapserver/schema/rfc2307bis/__init__.py | 1 + .../schema/rfc2307bis/attribute_types.py | 77 ++ .../schema/rfc2307bis/matching_rules.py | 7 + .../schema/rfc2307bis/object_classes.py | 47 ++ ldapserver/schema/rfc2307bis/syntaxes.py | 7 + ldapserver/schema/rfc2798/__init__.py | 1 + ldapserver/schema/rfc2798/attribute_types.py | 37 + ldapserver/schema/rfc2798/matching_rules.py | 4 + ldapserver/schema/rfc2798/object_classes.py | 12 + ldapserver/schema/rfc2798/syntaxes.py | 7 + ldapserver/schema/rfc3112/__init__.py | 1 + ldapserver/schema/rfc3112/attribute_types.py | 10 + ldapserver/schema/rfc3112/matching_rules.py | 16 + ldapserver/schema/rfc3112/object_classes.py | 8 + ldapserver/schema/rfc3112/syntaxes.py | 12 + ldapserver/schema/rfc4512/__init__.py | 1 + ldapserver/schema/rfc4512/attribute_types.py | 54 ++ ldapserver/schema/rfc4512/matching_rules.py | 4 + ldapserver/schema/rfc4512/object_classes.py | 14 + ldapserver/schema/rfc4512/syntaxes.py | 4 + ldapserver/schema/rfc4517/__init__.py | 1 + ldapserver/schema/rfc4517/matching_rules.py | 185 +++++ ldapserver/schema/rfc4517/syntaxes.py | 400 ++++++++++ ldapserver/schema/rfc4519/__init__.py | 1 + ldapserver/schema/rfc4519/attribute_types.py | 96 +++ ldapserver/schema/rfc4519/matching_rules.py | 4 + ldapserver/schema/rfc4519/object_classes.py | 38 + ldapserver/schema/rfc4519/syntaxes.py | 4 + ldapserver/schema/rfc4523/__init__.py | 1 + ldapserver/schema/rfc4523/attribute_types.py | 8 + ldapserver/schema/rfc4523/matching_rules.py | 8 + ldapserver/schema/rfc4523/syntaxes.py | 14 + ldapserver/schema/rfc4524/__init__.py | 1 + ldapserver/schema/rfc4524/attribute_types.py | 60 ++ ldapserver/schema/rfc4524/matching_rules.py | 4 + ldapserver/schema/rfc4524/object_classes.py | 27 + ldapserver/schema/rfc4524/syntaxes.py | 4 + ldapserver/schema/types.py | 698 ++++++++++++++++++ ldapserver/server.py | 45 +- ldapserver/util.py | 47 -- tests/test_server.py | 8 +- 61 files changed, 2578 insertions(+), 699 deletions(-) create mode 100644 examples/passwd.py create mode 100644 examples/static.py delete mode 100644 ldapserver/directory.py create mode 100644 ldapserver/rfc4518_stringprep.py delete mode 100644 ldapserver/schema.py create mode 100644 ldapserver/schema/__init__.py create mode 100644 ldapserver/schema/rfc1274/__init__.py create mode 100644 ldapserver/schema/rfc1274/attribute_types.py create mode 100644 ldapserver/schema/rfc1274/syntaxes.py create mode 100644 ldapserver/schema/rfc2079/__init__.py create mode 100644 ldapserver/schema/rfc2079/attribute_types.py create mode 100644 ldapserver/schema/rfc2079/matching_rules.py create mode 100644 ldapserver/schema/rfc2079/object_classes.py create mode 100644 ldapserver/schema/rfc2079/syntaxes.py create mode 100644 ldapserver/schema/rfc2252/__init__.py create mode 100644 ldapserver/schema/rfc2252/syntaxes.py create mode 100644 ldapserver/schema/rfc2307bis/__init__.py create mode 100644 ldapserver/schema/rfc2307bis/attribute_types.py create mode 100644 ldapserver/schema/rfc2307bis/matching_rules.py create mode 100644 ldapserver/schema/rfc2307bis/object_classes.py create mode 100644 ldapserver/schema/rfc2307bis/syntaxes.py create mode 100644 ldapserver/schema/rfc2798/__init__.py create mode 100644 ldapserver/schema/rfc2798/attribute_types.py create mode 100644 ldapserver/schema/rfc2798/matching_rules.py create mode 100644 ldapserver/schema/rfc2798/object_classes.py create mode 100644 ldapserver/schema/rfc2798/syntaxes.py create mode 100644 ldapserver/schema/rfc3112/__init__.py create mode 100644 ldapserver/schema/rfc3112/attribute_types.py create mode 100644 ldapserver/schema/rfc3112/matching_rules.py create mode 100644 ldapserver/schema/rfc3112/object_classes.py create mode 100644 ldapserver/schema/rfc3112/syntaxes.py create mode 100644 ldapserver/schema/rfc4512/__init__.py create mode 100644 ldapserver/schema/rfc4512/attribute_types.py create mode 100644 ldapserver/schema/rfc4512/matching_rules.py create mode 100644 ldapserver/schema/rfc4512/object_classes.py create mode 100644 ldapserver/schema/rfc4512/syntaxes.py create mode 100644 ldapserver/schema/rfc4517/__init__.py create mode 100644 ldapserver/schema/rfc4517/matching_rules.py create mode 100644 ldapserver/schema/rfc4517/syntaxes.py create mode 100644 ldapserver/schema/rfc4519/__init__.py create mode 100644 ldapserver/schema/rfc4519/attribute_types.py create mode 100644 ldapserver/schema/rfc4519/matching_rules.py create mode 100644 ldapserver/schema/rfc4519/object_classes.py create mode 100644 ldapserver/schema/rfc4519/syntaxes.py create mode 100644 ldapserver/schema/rfc4523/__init__.py create mode 100644 ldapserver/schema/rfc4523/attribute_types.py create mode 100644 ldapserver/schema/rfc4523/matching_rules.py create mode 100644 ldapserver/schema/rfc4523/syntaxes.py create mode 100644 ldapserver/schema/rfc4524/__init__.py create mode 100644 ldapserver/schema/rfc4524/attribute_types.py create mode 100644 ldapserver/schema/rfc4524/matching_rules.py create mode 100644 ldapserver/schema/rfc4524/object_classes.py create mode 100644 ldapserver/schema/rfc4524/syntaxes.py create mode 100644 ldapserver/schema/types.py delete mode 100644 ldapserver/util.py diff --git a/.pylintrc b/.pylintrc index c5388c2..994bce3 100644 --- a/.pylintrc +++ b/.pylintrc @@ -69,6 +69,8 @@ disable=unused-argument, # Too many false-positives, we're implem missing-class-docstring, # Temporarily disabled missing-function-docstring, # Temporarily disabled empty-docstring, # Temporarily disabled + consider-using-f-string, # Temporarily disabled + line-too-long, # Temporarily disabled # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/examples/passwd.py b/examples/passwd.py new file mode 100644 index 0000000..e0fa413 --- /dev/null +++ b/examples/passwd.py @@ -0,0 +1,38 @@ +import socketserver +import pwd +import grp + +import ldapserver + +class RequestHandler(ldapserver.LDAPRequestHandler): + subschema = ldapserver.schema.RFC2307BIS_SUBSCHEMA + + def do_search(self, basedn, scope, filterobj): + yield from super().do_search(basedn, scope, filterobj) + yield self.subschema.Object('dc=example,dc=com', **{ + 'objectClass': ['top', 'dcObject', 'organization'], + 'structuralObjectClass': ['organization'], + }) + user_gids = {} + for user in pwd.getpwall(): + user_gids[user.pw_gid] = user_gids.get(user.pw_gid, set()) | {user.pw_name} + yield self.subschema.Object(ldapserver.dn.DN('ou=users,dc=example,dc=com', uid=user.pw_name), **{ + 'objectClass': ['top', 'inetorgperson', 'organizationalperson', 'person', 'posixaccount'], + 'structuralObjectClass': ['organization'], + 'uid': [user.pw_name], + 'uidNumber': [user.pw_uid], + 'gidNumber': [user.pw_gid], + 'cn': [user.pw_gecos], + }) + for group in grp.getgrall(): + members = set(group.gr_mem) | user_gids.get(group.gr_gid, set()) + yield self.subschema.Object(ldapserver.dn.DN('ou=groups,dc=example,dc=com', cn=group.gr_name), **{ + 'objectClass': ['structuralobjectclass', 'objectclass', 'cn', 'description', 'gidnumber', 'uniquemember'], + 'structuralObjectClass': ['organization'], + 'cn': [group.gr_name], + 'gidNumber': [group.gr_gid], + 'uniqueMember': [ldapserver.dn.DN('ou=user,dc=example,dc=com', uid=name) for name in members], + }) + +if __name__ == '__main__': + socketserver.ThreadingTCPServer(('127.0.0.1', 3890), RequestHandler).serve_forever() diff --git a/examples/static.py b/examples/static.py new file mode 100644 index 0000000..3052141 --- /dev/null +++ b/examples/static.py @@ -0,0 +1,16 @@ +import socketserver +import ldapserver + +class RequestHandler(ldapserver.LDAPRequestHandler): + subschema = ldapserver.schema.RFC2307BIS_SUBSCHEMA + + static_objects = [ + subschema.Object('dc=example,dc=com', + objectClass=['top', 'dcObject', 'organization'], + structuralObjectClass=['organization'], + cn=['foo', 'bar'], + c=['test']) + ] + +if __name__ == '__main__': + socketserver.ThreadingTCPServer(('127.0.0.1', 3890), RequestHandler).serve_forever() diff --git a/ldapserver/__init__.py b/ldapserver/__init__.py index 9ac0ff5..f39804c 100644 --- a/ldapserver/__init__.py +++ b/ldapserver/__init__.py @@ -2,4 +2,4 @@ from . import ldap from . import dn from . import exceptions -from .server import BaseLDAPRequestHandler, SimpleLDAPRequestHandler +from .server import BaseLDAPRequestHandler, LDAPRequestHandler diff --git a/ldapserver/directory.py b/ldapserver/directory.py deleted file mode 100644 index 5dc0411..0000000 --- a/ldapserver/directory.py +++ /dev/null @@ -1,257 +0,0 @@ -from .util import encode_attribute, AttributeDict -from .dn import DN -from . import ldap - -class BaseDirectory: - '''Base class for LDAP directories''' - - def search(self, baseobj, scope, filterobj): - '''Perform search - - :param baseobj: Distinguished name of the LDAP entry relative to which the search is - to be performed - :type baseobj: str - :param scope: Search scope - :type scope: SearchScope - :param filterobj: Filter object - :type filterobj: Filter - - :returns: Iterable of dn, attributes tuples''' - return [] - -class FilterMixin: - '''Mixin for :any:`BaseDirectory` that implements :any:`BaseDirectory.search` by calling appropirate `filter_*` methods''' - def search(self, baseobj, scope, filterobj): - dn_res = self.filter_dn(baseobj, scope) - filter_res = self.search_filter(filterobj) - return self.search_fetch(self.filter_and(dn_res, filter_res)) - - def search_fetch(self, result): - ''' - ''' - return [] - - def search_filter(self, expr): - ''' - ''' - if isinstance(expr, ldap.FilterAnd): - return self.filter_and(*[self.search_filter(subexpr) for subexpr in expr.filters]) - elif isinstance(expr, ldap.FilterOr): - return self.filter_or(*[self.search_filter(subexpr) for subexpr in expr.filters]) - elif isinstance(expr, ldap.FilterNot): - return self.filter_not(self.search_filter(expr.filter)) - elif isinstance(expr, ldap.FilterEqual): - return self.filter_equal(expr.attribute.lower(), expr.value) - elif isinstance(expr, ldap.FilterPresent): - return self.filter_present(expr.attribute.lower()) - else: - return False - - def filter_present(self, attribute): - ''' - ''' - return False - - def filter_equal(self, attribute, value): - ''' - ''' - return False - - def filter_and(self, *subresults): - ''' - ''' - filtered = [] - for subres in subresults: - if subres is True: - continue - if subres is False: - return False - filtered.append(subres) - if not filtered: - return True - if len(filtered) == 1: - return filtered[0] - return self._filter_and(*filtered) - - def _filter_and(self, *subresults): - ''' - ''' - return False - - def filter_or(self, *subresults): - ''' - ''' - filtered = [] - for subres in subresults: - if subres is True: - return True - if subres is False: - continue - filtered.append(subres) - if not filtered: - return False - if len(filtered) == 1: - return filtered[0] - return self._filter_or(*filtered) - - def _filter_or(self, *subresults): - ''' - ''' - return False - - def filter_not(self, subresult): - ''' - ''' - if subresult is True: - return False - if subresult is False: - return True - return self._filter_not(subresult) - - def _filter_not(self, subresult): - ''' - ''' - return False - - def filter_dn(self, base, scope): - ''' - ''' - return False - -class SimpleFilterMixin(FilterMixin): - def filter_present(self, attribute): - if attribute in ['objectclass', 'structuralobjectclass', 'subschemasubentry']: - return True - return ldap.FilterPresent(attribute) - - def filter_equal(self, attribute, value): - if attribute == 'objectclass': - return value.lower() in [s.lower() for s in self.objectclasses] - elif attribute == 'structuralobjectclass': - return value.lower() == self.structuralobjectclass.lower() - return ldap.FilterEqual(attribute, value) - - def _filter_and(self, *subresults): - return ldap.FilterAnd(subresults) - - def _filter_or(self, *subresults): - return ldap.FilterOr(subresults) - - def _filter_not(self, subresult): - return ldap.FilterNot(subresult) - - def filter_dn(self, base, scope): - base = DN(base) - if scope == ldap.SearchScope.baseObject: - if base[1:] != self.dn_base or len(base[0]) != 1 or base[0][0].attribute != self.rdn_attr: - return False - return self.filter_equal(self.rdn_attr, base[0][0].value) - elif scope == ldap.SearchScope.singleLevel: - return base == self.dn_base - elif scope == ldap.SearchScope.wholeSubtree: - if self.dn_base.in_subtree_of(base): - return True - if base[1:] != self.dn_base or len(base[0]) != 1 or base[0][0].attribute != self.rdn_attr: - return False - return self.filter_equal(self.rdn_attr, base[0][0].value) - else: - return False - -class RootDSE(BaseDirectory, AttributeDict): - def search(self, baseobj, scope, filterobj): - if baseobj or scope != ldap.SearchScope.baseObject: - return [] - if not isinstance(filterobj, ldap.FilterPresent) or filterobj.attribute.lower() != 'objectclass': - return [] - attrs = {} - for name, values in self.items(): - if callable(values): - values = values() - if not isinstance(values, list): - values = [values] - if values: - attrs[name] = [encode_attribute(value) for value in values] - return [('', attrs)] - -class Subschema(BaseDirectory, AttributeDict): - def __init__(self, *args, - dn='cn=Subschema', - structuralobjectclass='subentry', - objectclass=('top', 'subschema', 'extensibleObject'), - subtreespecification='{ }', - ldapsyntaxes=tuple(), - matchingrules=tuple(), - objectclasses=tuple(), - attributetypes=tuple(), - **kwargs): - super().__init__(*args, **kwargs) - self.dn = DN(dn) - self['structuralObjectClass'] = [structuralobjectclass] - self['objectClass'] = list(objectclass) - self['subtreeSpecification'] = [subtreespecification] - self['ldapSyntaxes'] = list(ldapsyntaxes) - self['matchingRules'] = list(matchingrules) - self['objectClasses'] = list(objectclasses) - self['attributeTypes'] = list(attributetypes) - - def search(self, baseobj, scope, filterobj): - if DN(baseobj) != self.dn or scope != ldap.SearchScope.baseObject: - return [] - if not isinstance(filterobj, ldap.FilterEqual): - return [] - if filterobj.attribute.lower() != 'objectclass' or filterobj.value.lower() != b'subschema': - return [] - return [(str(self.dn), {key: [encode_attribute(value) for value in values] for key, values in self.items()})] - -def eval_ldap_filter(obj, expr): - '''Return whether LDAP filter expression matches attribute dictionary''' - if expr is True: - return True - elif expr is False: - return False - elif isinstance(expr, ldap.FilterAnd): - for subexpr in expr.filters: - if not eval_ldap_filter(obj, subexpr): - return False - return True - elif isinstance(expr, ldap.FilterOr): - for subexpr in expr.filters: - if eval_ldap_filter(obj, subexpr): - return True - return False - elif isinstance(expr, ldap.FilterNot): - return not eval_ldap_filter(obj, expr.filter) - elif isinstance(expr, ldap.FilterEqual): - return expr.value in obj.get(expr.attribute, []) - elif isinstance(expr, ldap.FilterPresent): - return bool(obj.get(expr.attribute, [])) - else: - return False - -class StaticDirectory(BaseDirectory): - def __init__(self): - self.objects = {} # dn -> attribute dict - - def add(self, dn, attributes): - tmp = AttributeDict() - for key, values in attributes.items(): - tmp[key] = [encode_attribute(value) for value in values] - self.objects[DN(dn)] = tmp - - def search(self, baseobj, scope, filterobj): - baseobj = DN(baseobj) - for dn, attributes in self.objects.items(): - if scope == ldap.SearchScope.baseObject: - if baseobj != dn: - continue - elif scope == ldap.SearchScope.singleLevel: - if not dn.is_direct_child_of(baseobj): - continue - elif scope == ldap.SearchScope.wholeSubtree: - if not dn.in_subtree_of(baseobj): - continue - else: - continue - if not eval_ldap_filter(attributes, filterobj): - continue - yield str(dn), attributes diff --git a/ldapserver/dn.py b/ldapserver/dn.py index 5b09b95..bcb7737 100644 --- a/ldapserver/dn.py +++ b/ldapserver/dn.py @@ -97,13 +97,30 @@ class DN(tuple): def is_direct_child_of(self, base): rchild, rbase = self.__strip_common_suffix(DN(base)) - print(repr(self), repr(base), repr(rchild), repr(rbase)) return not rbase and len(rchild) == 1 def in_subtree_of(self, base): rchild, rbase = self.__strip_common_suffix(DN(base)) # pylint: disable=unused-variable return not rbase + @property + def object_attribute(self): + if len(self) == 0: + return None + return self[0].attribute # pylint: disable=no-member + + @property + def object_value(self): + if len(self) == 0: + return None + return self[0].value # pylint: disable=no-member + + @property + def object_value_normalized(self): + if len(self) == 0: + return None + return self[0].value_normalized # pylint: disable=no-member + class RDN(tuple): '''Relative Distinguished Name consisting of one or more `RDNAssertion` objects''' def __new__(cls, *assertions, **kwargs): diff --git a/ldapserver/ldap.py b/ldapserver/ldap.py index 232172f..96262ac 100644 --- a/ldapserver/ldap.py +++ b/ldapserver/ldap.py @@ -133,14 +133,106 @@ class FilterEqual(asn1.Sequence, Filter): def get_filter_string(self): return '(%s=%s)'%(self.attribute, escape_filter_assertionvalue(self.value)) -class FilterGreaterOrEqual(FilterEqual): +class Substring(asn1.Choice, ABC): + pass + +class InitialSubstring(asn1.Wrapper, Substring): + BER_TAG = (2, False, 0) + + WRAPPED_ATTRIBUTE = 'value' + WRAPPED_TYPE = asn1.OctetString + WRAPPED_DEFAULT = b'' + +class AnySubstring(asn1.Wrapper, Substring): + BER_TAG = (2, False, 1) + + WRAPPED_ATTRIBUTE = 'value' + WRAPPED_TYPE = asn1.OctetString + WRAPPED_DEFAULT = b'' + +class FinalSubstring(asn1.Wrapper, Substring): + BER_TAG = (2, False, 2) + + WRAPPED_ATTRIBUTE = 'value' + WRAPPED_TYPE = asn1.OctetString + WRAPPED_DEFAULT = b'' + +class Substrings(asn1.SequenceOf): + SET_TYPE = Substring + +class FilterSubstrings(asn1.Sequence, Filter): + '''Attribute substrings filter ``(attribute=initial*any*final)`` + + .. py:attribute:: attribute + :type: str + .. py:attribute:: inital + :type: bytes + .. py:attribute:: any + :type: bytes + ''' + BER_TAG = (2, True, 4) + SEQUENCE_FIELDS = [ + (LDAPString, 'attribute', None, False), + (Substrings, 'substrings', lambda: [], False) + ] + + attribute: str + substrings: typing.List[Substring] + + def __init__(self, attribute=None, substrings=None): + super().__init__(attribute=attribute, substrings=substrings) + + @property + def initial_substring(self): + results = [substring.value for substring in self.substrings if isinstance(substring, InitialSubstring)] + if len(results) != 1: + return None + return results[0] + + @property + def any_substrings(self): + return [substring.value for substring in self.substrings if isinstance(substring, AnySubstring)] + + @property + def final_substring(self): + results = [substring.value for substring in self.substrings if isinstance(substring, FinalSubstring)] + if len(results) != 1: + return None + return results[0] + + def get_filter_string(self): + substrings = [self.initial_substring or b''] + self.any_substrings + [self.final_substring or b''] + value = '*'.join(map(escape_filter_assertionvalue, substrings)) + return f'({self.attribute}={value})' + +class FilterGreaterOrEqual(asn1.Sequence, Filter): BER_TAG = (2, True, 5) + SEQUENCE_FIELDS = [ + (LDAPString, 'attribute', None, False), + (asn1.OctetString, 'value', None, False) + ] + + attribute: str + value: bytes + + def __init__(self, attribute=None, value=None): + super().__init__(attribute=attribute, value=value) def get_filter_string(self): return '(%s>=%s)'%(self.attribute, escape_filter_assertionvalue(self.value)) -class FilterLessOrEqual(FilterEqual): +class FilterLessOrEqual(asn1.Sequence, Filter): BER_TAG = (2, True, 6) + SEQUENCE_FIELDS = [ + (LDAPString, 'attribute', None, False), + (asn1.OctetString, 'value', None, False) + ] + + attribute: str + value: bytes + + def __init__(self, attribute=None, value=None): + super().__init__(attribute=attribute, value=value) def get_filter_string(self): return '(%s<=%s)'%(self.attribute, escape_filter_assertionvalue(self.value)) @@ -164,8 +256,18 @@ class FilterPresent(asn1.Wrapper, Filter): def get_filter_string(self): return '(%s=*)'%(self.attribute) -class FilterApproxMatch(FilterEqual): +class FilterApproxMatch(asn1.Sequence, Filter): BER_TAG = (2, True, 8) + SEQUENCE_FIELDS = [ + (LDAPString, 'attribute', None, False), + (asn1.OctetString, 'value', None, False) + ] + + attribute: str + value: bytes + + def __init__(self, attribute=None, value=None): + super().__init__(attribute=attribute, value=value) def get_filter_string(self): return '(%s~=%s)'%(self.attribute, escape_filter_assertionvalue(self.value)) diff --git a/ldapserver/rfc4518_stringprep.py b/ldapserver/rfc4518_stringprep.py new file mode 100644 index 0000000..d4d07b0 --- /dev/null +++ b/ldapserver/rfc4518_stringprep.py @@ -0,0 +1,415 @@ +import unicodedata +import stringprep +import enum + +SPACE = 0x0020 + +MAPPED_TO_NOTHING = { + # SOFT HYPHEN (U+00AD) + 0x00AD, + # MONGOLIAN TODO SOFT HYPHEN (U+1806) + 0x1806, + # COMBINING GRAPHEME JOINER (U+034F) + 0x034F, + # VARIATION SELECTORs (U+180B-180D, FF00-FE0F) + 0x180B, 0x180C, 0x180D, + 0xFE00, 0xFE01, 0xFE02, 0xFE03, 0xFE04, 0xFE05, 0xFE06, 0xFE07, 0xFE08, + 0xFE09, 0xFE0A, 0xFE0B, 0xFE0C, 0xFE0D, 0xFE0E, 0xFE0F, + # OBJECT REPLACEMENT CHARACTER (U+FFFC) + 0xFFFC, + # ZERO WIDTH SPACE (U+200B) + 0x200B, +} + +MAPPED_TO_SPACE = { + # CHARACTER TABULATION (U+0009) + 0x0009, + # LINE FEED (LF) (U+000A) + 0x000A, + # LINE TABULATION (U+000B) + 0x000B, + # FORM FEED (FF) (U+000C) + 0x000C, + # CARRIAGE RETURN (CR) (U+000D) + 0x000D, + # NEXT LINE (NEL) (U+0085) + 0x0085, + # All other control code (e.g., Cc) points or code points with a + # control function (e.g., Cf) + 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, + 0x000E, 0x000F, 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, + 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F, + 0x007F, 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, + 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, + 0x008F, 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, + 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x06DD, + 0x070F, + 0x180E, + 0x200C, 0x200D, 0x200E, 0x200F, + 0x202A, 0x202B, 0x202C, 0x202D, 0x202E, + 0x2060, 0x2061, 0x2062, 0x2063, + 0x206A, 0x206B, 0x206C, 0x206D, 0x206E, 0x206F, + 0xFEFF, + 0xFFF9, 0xFFFA, 0xFFFB, + 0x1D173, 0x1D174, 0x1D175, 0x1D176, 0x1D177, 0x1D178, 0x1D179, 0x1D17A, + 0xE0001, + 0xE0020, 0xE0021, 0xE0022, 0xE0023, 0xE0024, 0xE0025, 0xE0026, 0xE0027, + 0xE0028, 0xE0029, 0xE002A, 0xE002B, 0xE002C, 0xE002D, 0xE002E, 0xE002F, + 0xE0030, 0xE0031, 0xE0032, 0xE0033, 0xE0034, 0xE0035, 0xE0036, 0xE0037, + 0xE0038, 0xE0039, 0xE003A, 0xE003B, 0xE003C, 0xE003D, 0xE003E, 0xE003F, + 0xE0040, 0xE0041, 0xE0042, 0xE0043, 0xE0044, 0xE0045, 0xE0046, 0xE0047, + 0xE0048, 0xE0049, 0xE004A, 0xE004B, 0xE004C, 0xE004D, 0xE004E, 0xE004F, + 0xE0050, 0xE0051, 0xE0052, 0xE0053, 0xE0054, 0xE0055, 0xE0056, 0xE0057, + 0xE0058, 0xE0059, 0xE005A, 0xE005B, 0xE005C, 0xE005D, 0xE005E, 0xE005F, + 0xE0060, 0xE0061, 0xE0062, 0xE0063, 0xE0064, 0xE0065, 0xE0066, 0xE0067, + 0xE0068, 0xE0069, 0xE006A, 0xE006B, 0xE006C, 0xE006D, 0xE006E, 0xE006F, + 0xE0070, 0xE0071, 0xE0072, 0xE0073, 0xE0074, 0xE0075, 0xE0076, 0xE0077, + 0xE0078, 0xE0079, 0xE007A, 0xE007B, 0xE007C, 0xE007D, 0xE007E, 0xE007F, +} + +# unicodedata.combining does not seem to reflect this table and the RFC +# says the embedded table is definitive. +COMBINING_MARKS = { + 0x0300, 0x0301, 0x0302, 0x0303, 0x0304, 0x0305, 0x0306, 0x0307, 0x0308, + 0x0309, 0x030A, 0x030B, 0x030C, 0x030D, 0x030E, 0x030F, 0x0310, 0x0311, + 0x0312, 0x0313, 0x0314, 0x0315, 0x0316, 0x0317, 0x0318, 0x0319, 0x031A, + 0x031B, 0x031C, 0x031D, 0x031E, 0x031F, 0x0320, 0x0321, 0x0322, 0x0323, + 0x0324, 0x0325, 0x0326, 0x0327, 0x0328, 0x0329, 0x032A, 0x032B, 0x032C, + 0x032D, 0x032E, 0x032F, 0x0330, 0x0331, 0x0332, 0x0333, 0x0334, 0x0335, + 0x0336, 0x0337, 0x0338, 0x0339, 0x033A, 0x033B, 0x033C, 0x033D, 0x033E, + 0x033F, 0x0340, 0x0341, 0x0342, 0x0343, 0x0344, 0x0345, 0x0346, 0x0347, + 0x0348, 0x0349, 0x034A, 0x034B, 0x034C, 0x034D, 0x034E, 0x034F, 0x0360, + 0x0361, 0x0362, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367, 0x0368, 0x0369, + 0x036A, 0x036B, 0x036C, 0x036D, 0x036E, 0x036F, 0x0483, 0x0484, 0x0485, + 0x0486, 0x0488, 0x0489, 0x0591, 0x0592, 0x0593, 0x0594, 0x0595, 0x0596, + 0x0597, 0x0598, 0x0599, 0x059A, 0x059B, 0x059C, 0x059D, 0x059E, 0x059F, + 0x05A0, 0x05A1, 0x05A3, 0x05A4, 0x05A5, 0x05A6, 0x05A7, 0x05A8, 0x05A9, + 0x05AA, 0x05AB, 0x05AC, 0x05AD, 0x05AE, 0x05AF, 0x05B0, 0x05B1, 0x05B2, + 0x05B3, 0x05B4, 0x05B5, 0x05B6, 0x05B7, 0x05B8, 0x05B9, 0x05BB, 0x05BC, + 0x05BF, 0x05C1, 0x05C2, 0x05C4, 0x064B, 0x064C, 0x064D, 0x064E, 0x064F, + 0x0650, 0x0651, 0x0652, 0x0653, 0x0654, 0x0655, 0x0670, 0x06D6, 0x06D7, + 0x06D8, 0x06D9, 0x06DA, 0x06DB, 0x06DC, 0x06DE, 0x06DF, 0x06E0, 0x06E1, + 0x06E2, 0x06E3, 0x06E4, 0x06E7, 0x06E8, 0x06EA, 0x06EB, 0x06EC, 0x06ED, + 0x0711, 0x0730, 0x0731, 0x0732, 0x0733, 0x0734, 0x0735, 0x0736, 0x0737, + 0x0738, 0x0739, 0x073A, 0x073B, 0x073C, 0x073D, 0x073E, 0x073F, 0x0740, + 0x0741, 0x0742, 0x0743, 0x0744, 0x0745, 0x0746, 0x0747, 0x0748, 0x0749, + 0x074A, 0x07A6, 0x07A7, 0x07A8, 0x07A9, 0x07AA, 0x07AB, 0x07AC, 0x07AD, + 0x07AE, 0x07AF, 0x07B0, 0x0901, 0x0902, 0x0903, 0x093C, 0x093E, 0x093F, + 0x0940, 0x0941, 0x0942, 0x0943, 0x0944, 0x0945, 0x0946, 0x0947, 0x0948, + 0x0949, 0x094A, 0x094B, 0x094C, 0x094D, 0x094E, 0x094F, 0x0951, 0x0952, + 0x0953, 0x0954, 0x0962, 0x0963, 0x0981, 0x0982, 0x0983, 0x09BC, 0x09BE, + 0x09BF, 0x09C0, 0x09C1, 0x09C2, 0x09C3, 0x09C4, 0x09C7, 0x09C8, 0x09CB, + 0x09CC, 0x09CD, 0x09D7, 0x09E2, 0x09E3, 0x0A02, 0x0A3C, 0x0A3E, 0x0A3F, + 0x0A40, 0x0A41, 0x0A42, 0x0A47, 0x0A48, 0x0A4B, 0x0A4C, 0x0A4D, 0x0A70, + 0x0A71, 0x0A81, 0x0A82, 0x0A83, 0x0ABC, 0x0ABE, 0x0ABF, 0x0AC0, 0x0AC1, + 0x0AC2, 0x0AC3, 0x0AC4, 0x0AC5, 0x0AC7, 0x0AC8, 0x0AC9, 0x0ACB, 0x0ACC, + 0x0ACD, 0x0B01, 0x0B02, 0x0B03, 0x0B3C, 0x0B3E, 0x0B3F, 0x0B40, 0x0B41, + 0x0B42, 0x0B43, 0x0B47, 0x0B48, 0x0B4B, 0x0B4C, 0x0B4D, 0x0B56, 0x0B57, + 0x0B82, 0x0BBE, 0x0BBF, 0x0BC0, 0x0BC1, 0x0BC2, 0x0BC6, 0x0BC7, 0x0BC8, + 0x0BCA, 0x0BCB, 0x0BCC, 0x0BCD, 0x0BD7, 0x0C01, 0x0C02, 0x0C03, 0x0C3E, + 0x0C3F, 0x0C40, 0x0C41, 0x0C42, 0x0C43, 0x0C44, 0x0C46, 0x0C47, 0x0C48, + 0x0C4A, 0x0C4B, 0x0C4C, 0x0C4D, 0x0C55, 0x0C56, 0x0C82, 0x0C83, 0x0CBE, + 0x0CBF, 0x0CC0, 0x0CC1, 0x0CC2, 0x0CC3, 0x0CC4, 0x0CC6, 0x0CC7, 0x0CC8, + 0x0CCA, 0x0CCB, 0x0CCC, 0x0CCD, 0x0CD5, 0x0CD6, 0x0D02, 0x0D03, 0x0D3E, + 0x0D3F, 0x0D40, 0x0D41, 0x0D42, 0x0D43, 0x0D46, 0x0D47, 0x0D48, 0x0D4A, + 0x0D4B, 0x0D4C, 0x0D4D, 0x0D57, 0x0D82, 0x0D83, 0x0DCA, 0x0DCF, 0x0DD0, + 0x0DD1, 0x0DD2, 0x0DD3, 0x0DD4, 0x0DD6, 0x0DD8, 0x0DD9, 0x0DDA, 0x0DDB, + 0x0DDC, 0x0DDD, 0x0DDE, 0x0DDF, 0x0DF2, 0x0DF3, 0x0E31, 0x0E34, 0x0E35, + 0x0E36, 0x0E37, 0x0E38, 0x0E39, 0x0E3A, 0x0E47, 0x0E48, 0x0E49, 0x0E4A, + 0x0E4B, 0x0E4C, 0x0E4D, 0x0E4E, 0x0EB1, 0x0EB4, 0x0EB5, 0x0EB6, 0x0EB7, + 0x0EB8, 0x0EB9, 0x0EBB, 0x0EBC, 0x0EC8, 0x0EC9, 0x0ECA, 0x0ECB, 0x0ECC, + 0x0ECD, 0x0F18, 0x0F19, 0x0F35, 0x0F37, 0x0F39, 0x0F3E, 0x0F3F, 0x0F71, + 0x0F72, 0x0F73, 0x0F74, 0x0F75, 0x0F76, 0x0F77, 0x0F78, 0x0F79, 0x0F7A, + 0x0F7B, 0x0F7C, 0x0F7D, 0x0F7E, 0x0F7F, 0x0F80, 0x0F81, 0x0F82, 0x0F83, + 0x0F84, 0x0F86, 0x0F87, 0x0F90, 0x0F91, 0x0F92, 0x0F93, 0x0F94, 0x0F95, + 0x0F96, 0x0F97, 0x0F99, 0x0F9A, 0x0F9B, 0x0F9C, 0x0F9D, 0x0F9E, 0x0F9F, + 0x0FA0, 0x0FA1, 0x0FA2, 0x0FA3, 0x0FA4, 0x0FA5, 0x0FA6, 0x0FA7, 0x0FA8, + 0x0FA9, 0x0FAA, 0x0FAB, 0x0FAC, 0x0FAD, 0x0FAE, 0x0FAF, 0x0FB0, 0x0FB1, + 0x0FB2, 0x0FB3, 0x0FB4, 0x0FB5, 0x0FB6, 0x0FB7, 0x0FB8, 0x0FB9, 0x0FBA, + 0x0FBB, 0x0FBC, 0x0FC6, 0x102C, 0x102D, 0x102E, 0x102F, 0x1030, 0x1031, + 0x1032, 0x1036, 0x1037, 0x1038, 0x1039, 0x1056, 0x1057, 0x1058, 0x1059, + 0x1712, 0x1713, 0x1714, 0x1732, 0x1733, 0x1734, 0x1752, 0x1753, 0x1772, + 0x1773, 0x17B4, 0x17B5, 0x17B6, 0x17B7, 0x17B8, 0x17B9, 0x17BA, 0x17BB, + 0x17BC, 0x17BD, 0x17BE, 0x17BF, 0x17C0, 0x17C1, 0x17C2, 0x17C3, 0x17C4, + 0x17C5, 0x17C6, 0x17C7, 0x17C8, 0x17C9, 0x17CA, 0x17CB, 0x17CC, 0x17CD, + 0x17CE, 0x17CF, 0x17D0, 0x17D1, 0x17D2, 0x17D3, 0x180B, 0x180C, 0x180D, + 0x18A9, 0x20D0, 0x20D1, 0x20D2, 0x20D3, 0x20D4, 0x20D5, 0x20D6, 0x20D7, + 0x20D8, 0x20D9, 0x20DA, 0x20DB, 0x20DC, 0x20DD, 0x20DE, 0x20DF, 0x20E0, + 0x20E1, 0x20E2, 0x20E3, 0x20E4, 0x20E5, 0x20E6, 0x20E7, 0x20E8, 0x20E9, + 0x20EA, 0x302A, 0x302B, 0x302C, 0x302D, 0x302E, 0x302F, 0x3099, 0x309A, + 0xFB1E, 0xFE00, 0xFE01, 0xFE02, 0xFE03, 0xFE04, 0xFE05, 0xFE06, 0xFE07, + 0xFE08, 0xFE09, 0xFE0A, 0xFE0B, 0xFE0C, 0xFE0D, 0xFE0E, 0xFE0F, 0xFE20, + 0xFE21, 0xFE22, 0xFE23, 0x1D165, 0x1D166, 0x1D167, 0x1D168, 0x1D169, + 0x1D16D, 0x1D16E, 0x1D16F, 0x1D170, 0x1D171, 0x1D172, 0x1D17B, 0x1D17C, + 0x1D17D, 0x1D17E, 0x1D17F, 0x1D180, 0x1D181, 0x1D182, 0x1D185, 0x1D186, + 0x1D187, 0x1D188, 0x1D189, 0x1D18A, 0x1D18B, 0x1D1AA, 0x1D1AB, 0x1D1AC, + 0x1D1AD, +} + +class MatchingType(enum.Enum): + CASE_IGNORE_STRING = enum.auto() + EXACT_STRING = enum.auto() + NUMERIC_STRING = enum.auto() + TELEPHONE_NUMBER = enum.auto() + +class SubstringType(enum.Enum): + NONE = enum.auto() + INITIAL = enum.auto() + ANY = enum.auto() + FINAL = enum.auto() + +def prepare(value, matching_type=MatchingType.EXACT_STRING, + substring_type=SubstringType.NONE): + # Algortihm according to RFC 4518 + # + # 1) Transcode: value is already a Unicode string, no transcoding needed + # 2) Map + value = prepare_map(value, matching_type=matching_type) + # 3) Normalize + value = prepare_normalize(value) + # 4) Prohibit + if prepare_check_prohibited(value): + raise ValueError('Stringprep "prohibit" stage rejected input') + # 5) Check bidi: Bidirectional characters are ignored. + # 6) Insignificant Character Handling + value = prepare_insignificant_characters(value, matching_type=matching_type, + substring_type=substring_type) + return value + +def prepare_map(value, matching_type=MatchingType.EXACT_STRING): + # 2.2. Map + # + # SOFT HYPHEN (U+00AD) and MONGOLIAN TODO SOFT HYPHEN (U+1806) code + # points are mapped to nothing. COMBINING GRAPHEME JOINER (U+034F) and + # VARIATION SELECTORs (U+180B-180D, FF00-FE0F) code points are also + # mapped to nothing. The OBJECT REPLACEMENT CHARACTER (U+FFFC) is + # mapped to nothing. + # + # CHARACTER TABULATION (U+0009), LINE FEED (LF) (U+000A), LINE + # TABULATION (U+000B), FORM FEED (FF) (U+000C), CARRIAGE RETURN (CR) + # (U+000D), and NEXT LINE (NEL) (U+0085) are mapped to SPACE (U+0020). + # + # All other control code (e.g., Cc) points or code points with a + # control function (e.g., Cf) are mapped to nothing. The following is + # a complete list of these code points: U+0000-0008, 000E-001F, 007F- + # 0084, 0086-009F, 06DD, 070F, 180E, 200C-200F, 202A-202E, 2060-2063, + # 206A-206F, FEFF, FFF9-FFFB, 1D173-1D17A, E0001, E0020-E007F. + # + # ZERO WIDTH SPACE (U+200B) is mapped to nothing. All other code + # points with Separator (space, line, or paragraph) property (e.g., Zs, + # Zl, or Zp) are mapped to SPACE (U+0020). The following is a complete + # list of these code points: U+0020, 00A0, 1680, 2000-200A, 2028-2029, + # 202F, 205F, 3000. + # + # For case ignore, numeric, and stored prefix string matching rules, + # characters are case folded per B.2 of [RFC3454]. + # + # The output is the mapped string. + new_value = '' + for char in value: + if char in MAPPED_TO_NOTHING: + continue + if char in MAPPED_TO_SPACE: + char = ' ' + # No idea what "stored prefix string matching" is supposed to be + if matching_type in (MatchingType.CASE_IGNORE_STRING, + MatchingType.NUMERIC_STRING): + char = stringprep.map_table_b2(char) + new_value += char + return new_value + +def prepare_normalize(value): + # 2.3. Normalize + # + # The input string is to be normalized to Unicode Form KC + # (compatibility composed) as described in [UAX15]. The output is the + # normalized string. + return unicodedata.normalize('NFKC', value) + +def prepare_check_prohibited(value): + # 2.4. Prohibit + # + # All Unassigned code points are prohibited. Unassigned code points + # are listed in Table A.1 of [RFC3454]. + # + # Characters that, per Section 5.8 of [RFC3454], change display + # properties or are deprecated are prohibited. These characters are + # listed in Table C.8 of [RFC3454]. + # + # Private Use code points are prohibited. These characters are listed + # in Table C.3 of [RFC3454]. + # + # All non-character code points are prohibited. These code points are + # listed in Table C.4 of [RFC3454]. + # + # Surrogate codes are prohibited. These characters are listed in Table + # C.5 of [RFC3454]. + # + # The REPLACEMENT CHARACTER (U+FFFD) code point is prohibited. + # + # The step fails if the input string contains any prohibited code + # point. Otherwise, the output is the input string. + for char in value: + # pylint: disable=too-many-boolean-expressions + if stringprep.in_table_a1(char) or \ + stringprep.in_table_c8(char) or \ + stringprep.in_table_c3(char) or \ + stringprep.in_table_c4(char) or \ + stringprep.in_table_c5(char) or \ + ord(char) == 0xFFFD: + return True + return False + +def prepare_insignificant_characters(value, + matching_type=MatchingType.EXACT_STRING, + substring_type=SubstringType.NONE): + # 2.6. Insignificant Character Handling + # + # In this step, the string is modified to ensure proper handling of + # characters insignificant to the matching rule. This modification + # differs from matching rule to matching rule. + # + # Section 2.6.1 applies to case ignore and exact string matching. + # Section 2.6.2 applies to numericString matching. + # Section 2.6.3 applies to telephoneNumber matching. + if matching_type in (MatchingType.CASE_IGNORE_STRING, + MatchingType.EXACT_STRING): + return prepare_insignificant_space(value, substring_type=substring_type) + if matching_type == MatchingType.NUMERIC_STRING: + return prepare_insignificant_numeric_string(value) + if matching_type == MatchingType.TELEPHONE_NUMBER: + return prepare_insignificant_telephone_number(value) + raise ValueError('Invalid matching type') + +def prepare_insignificant_space(value, substring_type=SubstringType.NONE): + # 2.6.1. Insignificant Space Handling + # + # For the purposes of this section, a space is defined to be the SPACE + # (U+0020) code point followed by no combining marks. + # + # NOTE - The previous steps ensure that the string cannot contain + # any code points in the separator class, other than SPACE + # (U+0020). + # + # For input strings that are attribute values or non-substring + # assertion values: If the input string contains no non-space + # character, then the output is exactly two SPACEs. Otherwise (the + # input string contains at least one non-space character), the string + # is modified such that the string starts with exactly one space + # character, ends with exactly one SPACE character, and any inner + # (non-empty) sequence of space characters is replaced with exactly two + # SPACE characters. For instance, the input strings + # "foo<SPACE>bar<SPACE><SPACE>", result in the output + # "<SPACE>foo<SPACE><SPACE>bar<SPACE>". + # + # For input strings that are substring assertion values: If the string + # being prepared contains no non-space characters, then the output + # string is exactly one SPACE. Otherwise, the following steps are + # taken: + # + # - Any inner (non-empty) sequence of space characters is replaced + # with exactly two SPACE characters; [ERRATA 1757] + # + # - If the input string is an initial substring, it is modified to + # start with exactly one SPACE character; + # + # - If the input string is an initial or an any substring that ends in + # one or more space characters, it is modified to end with exactly + # one SPACE character; + # + # - If the input string is an any or a final substring that starts in + # one or more space characters, it is modified to start with exactly + # one SPACE character; and + # + # - If the input string is a final substring, it is modified to end + # with exactly one SPACE character. + # + # For instance, for the input string "foo<SPACE>bar<SPACE><SPACE>" as + # an initial substring, the output would be + # "<SPACE>foo<SPACE><SPACE>bar<SPACE>". As an any or final substring, + # the same input would result in "foo<SPACE><SPACE>bar<SPACE>". + # [ERRATA 1758] + + # First we replace all spaces followed by no combining mark with U+FFFD. + # U+FFFD is used because is it one of the prohibited characters. + # Additionally we collapse all sequences of one or more SPACEs into + # exactly two SPACEs. + PLACEHOLDER = '\uFFFD' # pylint: disable=invalid-name + new_value = '' + for i, char in enumerate(value): + if ord(char) != SPACE: + new_value += char + elif i + 1 < len(value) and ord(value[i + 1]) in COMBINING_MARKS: + new_value += ' ' + elif not new_value or new_value[-1] != PLACEHOLDER: + new_value += 2*PLACEHOLDER + value = new_value + if substring_type == SubstringType.NONE: + value = PLACEHOLDER + value.strip(PLACEHOLDER) + PLACEHOLDER + else: + if substring_type == SubstringType.INITIAL: + value = PLACEHOLDER + value.lstrip(PLACEHOLDER) + if substring_type in (SubstringType.INITIAL, SubstringType.ANY) and value.endswith(PLACEHOLDER): + value = value.rstrip(PLACEHOLDER) + PLACEHOLDER + if substring_type in (SubstringType.ANY, SubstringType.FINAL) and value.startswith(PLACEHOLDER): + value = PLACEHOLDER + value.lstrip(PLACEHOLDER) + if substring_type == SubstringType.FINAL: + value = value.rstrip(PLACEHOLDER) + PLACEHOLDER + return value.replace(PLACEHOLDER, ' ') + +def prepare_insignificant_numeric_string(value): + # 2.6.2. numericString Insignificant Character Handling + # + # For the purposes of this section, a space is defined to be the SPACE + # (U+0020) code point followed by no combining marks. + # + # All spaces are regarded as insignificant and are to be removed. + # + # For example, removal of spaces from the Form KC string: + # "<SPACE><SPACE>123<SPACE><SPACE>456<SPACE><SPACE>" + # would result in the output string: + # "123456" + # and the Form KC string: + # "<SPACE><SPACE><SPACE>" + # would result in the output string: + # "" (an empty string). + new_value = '' + # pylint: disable=consider-using-enumerate + for i in range(len(value)): + if ord(value[i]) == SPACE: + if i + 1 >= len(value) or ord(value[i + 1]) not in COMBINING_MARKS: + continue + new_value += value[i] + return new_value + +def prepare_insignificant_telephone_number(value): + # 2.6.3. telephoneNumber Insignificant Character Handling + # + # For the purposes of this section, a hyphen is defined to be a + # HYPHEN-MINUS (U+002D), ARMENIAN HYPHEN (U+058A), HYPHEN (U+2010), + # NON-BREAKING HYPHEN (U+2011), MINUS SIGN (U+2212), SMALL HYPHEN-MINUS + # (U+FE63), or FULLWIDTH HYPHEN-MINUS (U+FF0D) code point followed by + # no combining marks and a space is defined to be the SPACE (U+0020) + # code point followed by no combining marks. + # + # All hyphens and spaces are considered insignificant and are to be + # removed. + # + # For example, removal of hyphens and spaces from the Form KC string: + # "<SPACE><HYPHEN>123<SPACE><SPACE>456<SPACE><HYPHEN>" + # would result in the output string: + # "123456" + # and the Form KC string: + # "<HYPHEN><HYPHEN><HYPHEN>" + # would result in the (empty) output string: + # "". + hyphen_chars = {0x002D, 0x058A, 0x2010, 0x2011, 0x2212, 0xFE63, 0xFF0D} + new_value = '' + # pylint: disable=consider-using-enumerate + for i in range(len(value)): + if ord(value[i]) == SPACE or ord(value[i]) in hyphen_chars: + if i + 1 >= len(value) or ord(value[i + 1]) not in COMBINING_MARKS: + continue + new_value += value[i] + return new_value diff --git a/ldapserver/schema.py b/ldapserver/schema.py deleted file mode 100644 index 8a2beaa..0000000 --- a/ldapserver/schema.py +++ /dev/null @@ -1,364 +0,0 @@ -# pylint: disable=line-too-long - -from . import directory - -CORE_SYNTAXES = ( - # RFC 4517 - "( 1.3.6.1.4.1.1466.115.121.1.3 DESC 'Attribute Type Description' )", - "( 1.3.6.1.4.1.1466.115.121.1.6 DESC 'Bit String' )", - "( 1.3.6.1.4.1.1466.115.121.1.7 DESC 'Boolean' )", - "( 1.3.6.1.4.1.1466.115.121.1.11 DESC 'Country String' )", - "( 1.3.6.1.4.1.1466.115.121.1.14 DESC 'Delivery Method' )", - "( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' )", - "( 1.3.6.1.4.1.1466.115.121.1.16 DESC 'DIT Content Rule Description' )", - "( 1.3.6.1.4.1.1466.115.121.1.17 DESC 'DIT Structure Rule Description' )", - "( 1.3.6.1.4.1.1466.115.121.1.12 DESC 'DN' )", - "( 1.3.6.1.4.1.1466.115.121.1.21 DESC 'Enhanced Guide' )", - "( 1.3.6.1.4.1.1466.115.121.1.22 DESC 'Facsimile Telephone Number')", - "( 1.3.6.1.4.1.1466.115.121.1.23 DESC 'Fax' )", - "( 1.3.6.1.4.1.1466.115.121.1.24 DESC 'Generalized Time' )", - "( 1.3.6.1.4.1.1466.115.121.1.25 DESC 'Guide' )", - "( 1.3.6.1.4.1.1466.115.121.1.26 DESC 'IA5 String' )", - "( 1.3.6.1.4.1.1466.115.121.1.27 DESC 'INTEGER' )", - "( 1.3.6.1.4.1.1466.115.121.1.28 DESC 'JPEG' )", - "( 1.3.6.1.4.1.1466.115.121.1.54 DESC 'LDAP Syntax Description' )", - "( 1.3.6.1.4.1.1466.115.121.1.30 DESC 'Matching Rule Description' )", - "( 1.3.6.1.4.1.1466.115.121.1.31 DESC 'Matching Rule Use Description' )", - "( 1.3.6.1.4.1.1466.115.121.1.34 DESC 'Name And Optional UID' )", - "( 1.3.6.1.4.1.1466.115.121.1.35 DESC 'Name Form Description' )", - "( 1.3.6.1.4.1.1466.115.121.1.36 DESC 'Numeric String' )", - "( 1.3.6.1.4.1.1466.115.121.1.37 DESC 'Object Class Description' )", - "( 1.3.6.1.4.1.1466.115.121.1.40 DESC 'Octet String' )", - "( 1.3.6.1.4.1.1466.115.121.1.38 DESC 'OID' )", - "( 1.3.6.1.4.1.1466.115.121.1.39 DESC 'Other Mailbox' )", - "( 1.3.6.1.4.1.1466.115.121.1.41 DESC 'Postal Address' )", - "( 1.3.6.1.4.1.1466.115.121.1.44 DESC 'Printable String' )", - "( 1.3.6.1.4.1.1466.115.121.1.58 DESC 'Substring Assertion' )", - "( 1.3.6.1.4.1.1466.115.121.1.50 DESC 'Telephone Number' )", - "( 1.3.6.1.4.1.1466.115.121.1.51 DESC 'Teletex Terminal Identifier' )", - "( 1.3.6.1.4.1.1466.115.121.1.52 DESC 'Telex Number' )", - "( 1.3.6.1.4.1.1466.115.121.1.53 DESC 'UTC Time' )", - # RFC 3672 (Subentries in LDAP) - "( 1.3.6.1.4.1.1466.115.121.1.45 DESC 'SubtreeSpecification' )", -) - -CORE_MATCHING_RULES = ( - # RFC 4517 - "( 2.5.13.16 NAME 'bitStringMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.6 )", - "( 2.5.13.13 NAME 'booleanMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 )", - "( 1.3.6.1.4.1.1466.109.114.1 NAME 'caseExactIA5Match' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 2.5.13.5 NAME 'caseExactMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.13.6 NAME 'caseExactOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.13.7 NAME 'caseExactSubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.58 )", - "( 1.3.6.1.4.1.1466.109.114.2 NAME 'caseIgnoreIA5Match' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 1.3.6.1.4.1.1466.109.114.3 NAME 'caseIgnoreIA5SubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.58 )", - "( 2.5.13.11 NAME 'caseIgnoreListMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )", - "( 2.5.13.12 NAME 'caseIgnoreListSubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.58 )", - "( 2.5.13.2 NAME 'caseIgnoreMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.13.3 NAME 'caseIgnoreOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.13.4 NAME 'caseIgnoreSubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.58 )", - "( 2.5.13.31 NAME 'directoryStringFirstComponentMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.13.1 NAME 'distinguishedNameMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", - "( 2.5.13.27 NAME 'generalizedTimeMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )", - "( 2.5.13.28 NAME 'generalizedTimeOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )", - "( 2.5.13.29 NAME 'integerFirstComponentMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )", - "( 2.5.13.14 NAME 'integerMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )", - "( 2.5.13.15 NAME 'integerOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )", - "( 2.5.13.33 NAME 'keywordMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.13.8 NAME 'numericStringMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.36 )", - "( 2.5.13.9 NAME 'numericStringOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.36 )", - "( 2.5.13.10 NAME 'numericStringSubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.58 )", - "( 2.5.13.30 NAME 'objectIdentifierFirstComponentMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )", - "( 2.5.13.0 NAME 'objectIdentifierMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )", - "( 2.5.13.17 NAME 'octetStringMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )", - "( 2.5.13.18 NAME 'octetStringOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )", - "( 2.5.13.20 NAME 'telephoneNumberMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )", - "( 2.5.13.21 NAME 'telephoneNumberSubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.58 )", - "( 2.5.13.23 NAME 'uniqueMemberMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 )", - "( 2.5.13.32 NAME 'wordMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", -) - -# Self-contained object class and attribute type Definitions that describe -# everthing need for the root DSE and subschema entry - -CORE_OBJECT_CLASSES = ( - # RFC 4512 - "( 2.5.6.0 NAME 'top' ABSTRACT MUST objectClass )", - "( 2.5.6.1 NAME 'alias' SUP top STRUCTURAL MUST aliasedObjectName )", - "( 2.5.20.1 NAME 'subschema' AUXILIARY MAY ( dITStructureRules $ nameForms $ ditContentRules $ objectClasses $ attributeTypes $ matchingRules $ matchingRuleUse ) )", - "( 1.3.6.1.4.1.1466.101.120.111 NAME 'extensibleObject' SUP top AUXILIARY )", - # RFC 3672 (Subentries in LDAP) - "( 2.5.17.0 NAME 'subentry' SUP top STRUCTURAL MUST ( cn $ subtreeSpecification ) )", -) - -CORE_ATTRIBUTE_TYPES = ( - # RFC 4512 - "( 2.5.4.1 NAME 'aliasedObjectName' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )", - "( 2.5.4.0 NAME 'objectClass' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )", - "( 2.5.18.3 NAME 'creatorsName' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", - "( 2.5.18.1 NAME 'createTimestamp' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", - "( 2.5.18.4 NAME 'modifiersName' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", - "( 2.5.18.2 NAME 'modifyTimestamp' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", - "( 2.5.21.9 NAME 'structuralObjectClass' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", - "( 2.5.21.10 NAME 'governingStructureRule' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", - "( 2.5.18.10 NAME 'subschemaSubentry' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", - "( 2.5.21.6 NAME 'objectClasses' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.37 USAGE directoryOperation )", - "( 2.5.21.5 NAME 'attributeTypes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.3 USAGE directoryOperation )", - "( 2.5.21.4 NAME 'matchingRules' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.30 USAGE directoryOperation )", - "( 2.5.21.8 NAME 'matchingRuleUse' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.31 USAGE directoryOperation )", - "( 1.3.6.1.4.1.1466.101.120.16 NAME 'ldapSyntaxes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.54 USAGE directoryOperation )", - "( 2.5.21.2 NAME 'dITContentRules' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.16 USAGE directoryOperation )", - "( 2.5.21.1 NAME 'dITStructureRules' EQUALITY integerFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.17 USAGE directoryOperation )", - "( 2.5.21.7 NAME 'nameForms' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.35 USAGE directoryOperation )", - "( 1.3.6.1.4.1.1466.101.120.6 NAME 'altServer' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 USAGE dSAOperation )", - "( 1.3.6.1.4.1.1466.101.120.5 NAME 'namingContexts' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 USAGE dSAOperation )", - "( 1.3.6.1.4.1.1466.101.120.13 NAME 'supportedControl' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )", - "( 1.3.6.1.4.1.1466.101.120.7 NAME 'supportedExtension' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )", - "( 1.3.6.1.4.1.4203.1.3.5 NAME 'supportedFeatures' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )", - "( 1.3.6.1.4.1.1466.101.120.15 NAME 'supportedLDAPVersion' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 USAGE dSAOperation )", - "( 1.3.6.1.4.1.1466.101.120.14 NAME 'supportedSASLMechanisms' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE dSAOperation )", - # RFC 3672 (Subentries in LDAP) - "( 2.5.18.5 NAME 'administrativeRole' EQUALITY objectIdentifierMatch USAGE directoryOperation SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )", - "( 2.5.18.6 NAME 'subtreeSpecification' SINGLE-VALUE USAGE directoryOperation SYNTAX 1.3.6.1.4.1.1466.115.121.1.45 )", - # RFC 4519 (Schema for User Applications) - "( 2.5.4.41 NAME 'name' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.4.3 NAME 'cn' SUP name )", -) - -CORE_SUBSCHEMA = directory.Subschema(ldapsyntaxes=CORE_SYNTAXES, - matchingrules=CORE_MATCHING_RULES, - attributetypes=CORE_ATTRIBUTE_TYPES, - objectclasses=CORE_OBJECT_CLASSES) - -# RFC 4519: Schema for User Applications - -RFC4519_ATTRIBUTE_TYPES = CORE_ATTRIBUTE_TYPES + ( - "( 2.5.4.15 NAME 'businessCategory' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.4.6 NAME 'c' SUP name SYNTAX 1.3.6.1.4.1.1466.115.121.1.11 SINGLE-VALUE )", - # Already included in CORE_ATTRIBUTE_TYPES - #"( 2.5.4.3 NAME 'cn' SUP name )", - "( 0.9.2342.19200300.100.1.25 NAME 'dc' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )", - "( 2.5.4.13 NAME 'description' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.4.27 NAME 'destinationIndicator' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 )", - "( 2.5.4.49 NAME 'distinguishedName' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", - "( 2.5.4.46 NAME 'dnQualifier' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 )", - "( 2.5.4.47 NAME 'enhancedSearchGuide' SYNTAX 1.3.6.1.4.1.1466.115.121.1.21 )", - "( 2.5.4.23 NAME 'facsimileTelephoneNumber' SYNTAX 1.3.6.1.4.1.1466.115.121.1.22 )", - "( 2.5.4.44 NAME 'generationQualifier' SUP name )", - "( 2.5.4.42 NAME 'givenName' SUP name )", - "( 2.5.4.51 NAME 'houseIdentifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.4.43 NAME 'initials' SUP name )", - "( 2.5.4.25 NAME 'internationalISDNNumber' EQUALITY numericStringMatch SUBSTR numericStringSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36 )", - "( 2.5.4.7 NAME 'l' SUP name )", - "( 2.5.4.31 NAME 'member' SUP distinguishedName )", - # Already included in CORE_ATTRIBUTE_TYPES - #"( 2.5.4.41 NAME 'name' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.4.10 NAME 'o' SUP name )", - "( 2.5.4.11 NAME 'ou' SUP name )", - "( 2.5.4.32 NAME 'owner' SUP distinguishedName )", - "( 2.5.4.19 NAME 'physicalDeliveryOfficeName' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.4.16 NAME 'postalAddress' EQUALITY caseIgnoreListMatch SUBSTR caseIgnoreListSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )", - "( 2.5.4.17 NAME 'postalCode' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.4.18 NAME 'postOfficeBox' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.4.28 NAME 'preferredDeliveryMethod' SYNTAX 1.3.6.1.4.1.1466.115.121.1.14 SINGLE-VALUE )", - "( 2.5.4.26 NAME 'registeredAddress' SUP postalAddress SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )", - "( 2.5.4.33 NAME 'roleOccupant' SUP distinguishedName )", - "( 2.5.4.14 NAME 'searchGuide' SYNTAX 1.3.6.1.4.1.1466.115.121.1.25 )", - "( 2.5.4.34 NAME 'seeAlso' SUP distinguishedName )", - "( 2.5.4.5 NAME 'serialNumber' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 )", - "( 2.5.4.4 NAME 'sn' SUP name )", - "( 2.5.4.8 NAME 'st' SUP name )", - "( 2.5.4.9 NAME 'street' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.4.20 NAME 'telephoneNumber' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )", - "( 2.5.4.22 NAME 'teletexTerminalIdentifier' SYNTAX 1.3.6.1.4.1.1466.115.121.1.51 )", - "( 2.5.4.21 NAME 'telexNumber' SYNTAX 1.3.6.1.4.1.1466.115.121.1.52 )", - "( 2.5.4.12 NAME 'title' SUP name )", - "( 0.9.2342.19200300.100.1.1 NAME 'uid' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.4.50 NAME 'uniqueMember' EQUALITY uniqueMemberMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 )", - "( 2.5.4.35 NAME 'userPassword' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )", - "( 2.5.4.24 NAME 'x121Address' EQUALITY numericStringMatch SUBSTR numericStringSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36 )", - "( 2.5.4.45 NAME 'x500UniqueIdentifier' EQUALITY bitStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.6 )", -) - -RFC4519_OBJECT_CLASSES = CORE_OBJECT_CLASSES + ( - "( 2.5.6.11 NAME 'applicationProcess' SUP top STRUCTURAL MUST cn MAY ( seeAlso $ ou $ l $ description ) )", - "( 2.5.6.2 NAME 'country' SUP top STRUCTURAL MUST c MAY ( searchGuide $ description ) )", - "( 1.3.6.1.4.1.1466.344 NAME 'dcObject' SUP top AUXILIARY MUST dc )", - "( 2.5.6.14 NAME 'device' SUP top STRUCTURAL MUST cn MAY ( serialNumber $ seeAlso $ owner $ ou $ o $ l $ description ) )", - "( 2.5.6.9 NAME 'groupOfNames' SUP top STRUCTURAL MUST ( member $ cn ) MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) )", - "( 2.5.6.17 NAME 'groupOfUniqueNames' SUP top STRUCTURAL MUST ( uniqueMember $ cn ) MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) )", - "( 2.5.6.3 NAME 'locality' SUP top STRUCTURAL MAY ( street $ seeAlso $ searchGuide $ st $ l $ description ) )", - "( 2.5.6.4 NAME 'organization' SUP top STRUCTURAL MUST o MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationalISDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) )", - "( 2.5.6.7 NAME 'organizationalPerson' SUP person STRUCTURAL MAY ( title $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationalISDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ ou $ st $ l ) )", - "( 2.5.6.8 NAME 'organizationalRole' SUP top STRUCTURAL MUST cn MAY ( x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationalISDNNumber $ facsimileTelephoneNumber $ seeAlso $ roleOccupant $ preferredDeliveryMethod $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ ou $ st $ l $ description ) )", - "( 2.5.6.5 NAME 'organizationalUnit' SUP top STRUCTURAL MUST ou MAY ( businessCategory $ description $ destinationIndicator $ facsimileTelephoneNumber $ internationalISDNNumber $ l $ physicalDeliveryOfficeName $ postalAddress $ postalCode $ postOfficeBox $ preferredDeliveryMethod $ registeredAddress $ searchGuide $ seeAlso $ st $ street $ telephoneNumber $ teletexTerminalIdentifier $ telexNumber $ userPassword $ x121Address ) )", - "( 2.5.6.6 NAME 'person' SUP top STRUCTURAL MUST ( sn $ cn ) MAY ( userPassword $ telephoneNumber $ seeAlso $ description ) )", - "( 2.5.6.10 NAME 'residentialPerson' SUP person STRUCTURAL MUST l MAY ( businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationalISDNNumber $ facsimileTelephoneNumber $ preferredDeliveryMethod $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l ) )", - "( 1.3.6.1.1.3.1 NAME 'uidObject' SUP top AUXILIARY MUST uid )", -) - -RFC4519_SUBSCHEMA = directory.Subschema(ldapsyntaxes=CORE_SYNTAXES, - matchingrules=CORE_MATCHING_RULES, - attributetypes=RFC4519_ATTRIBUTE_TYPES, - objectclasses=RFC4519_OBJECT_CLASSES) - -# RFC 4524: COSINE LDAP/X.500 Schema - -RFC4524_ATTRIBUTE_TYPES = RFC4519_ATTRIBUTE_TYPES + ( - "( 0.9.2342.19200300.100.1.37 NAME 'associatedDomain' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 0.9.2342.19200300.100.1.38 NAME 'associatedName' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", - "( 0.9.2342.19200300.100.1.48 NAME 'buildingName' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.43 NAME 'co' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 0.9.2342.19200300.100.1.14 NAME 'documentAuthor' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", - "( 0.9.2342.19200300.100.1.11 NAME 'documentIdentifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.15 NAME 'documentLocation' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.56 NAME 'documentPublisher' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 0.9.2342.19200300.100.1.12 NAME 'documentTitle' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.13 NAME 'documentVersion' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.5 NAME 'drink' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.20 NAME 'homePhone' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )", - "( 0.9.2342.19200300.100.1.39 NAME 'homePostalAddress' EQUALITY caseIgnoreListMatch SUBSTR caseIgnoreListSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )", - "( 0.9.2342.19200300.100.1.9 NAME 'host' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.4 NAME 'info' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{2048} )", - "( 0.9.2342.19200300.100.1.3 NAME 'mail' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )", - "( 0.9.2342.19200300.100.1.10 NAME 'manager' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", - "( 0.9.2342.19200300.100.1.41 NAME 'mobile' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )", - "( 0.9.2342.19200300.100.1.45 NAME 'organizationalStatus' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.42 NAME 'pager' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )", - "( 0.9.2342.19200300.100.1.40 NAME 'personalTitle' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.6 NAME 'roomNumber' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.21 NAME 'secretary' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", - "( 0.9.2342.19200300.100.1.44 NAME 'uniqueIdentifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.8 NAME 'userClass' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", -) - -RFC4524_OBJECT_CLASSES = RFC4519_OBJECT_CLASSES + ( - "( 0.9.2342.19200300.100.4.5 NAME 'account' SUP top STRUCTURAL MUST uid MAY ( description $ seeAlso $ l $ o $ ou $ host ) )", - "( 0.9.2342.19200300.100.4.6 NAME 'document' SUP top STRUCTURAL MUST documentIdentifier MAY ( cn $ description $ seeAlso $ l $ o $ ou $ documentTitle $ documentVersion $ documentAuthor $ documentLocation $ documentPublisher ) )", - "( 0.9.2342.19200300.100.4.9 NAME 'documentSeries' SUP top STRUCTURAL MUST cn MAY ( description $ l $ o $ ou $ seeAlso $ telephonenumber ) )", - "( 0.9.2342.19200300.100.4.13 NAME 'domain' SUP top STRUCTURAL MUST dc MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l $ description $ o $ associatedName ) )", - "( 0.9.2342.19200300.100.4.17 NAME 'domainRelatedObject' SUP top AUXILIARY MUST associatedDomain )", - "( 0.9.2342.19200300.100.4.18 NAME 'friendlyCountry' SUP country STRUCTURAL MUST co )", - "( 0.9.2342.19200300.100.4.14 NAME 'rFC822localPart' SUP domain STRUCTURAL MAY ( cn $ description $ destinationIndicator $ facsimileTelephoneNumber $ internationaliSDNNumber $ physicalDeliveryOfficeName $ postalAddress $ postalCode $ postOfficeBox $ preferredDeliveryMethod $ registeredAddress $ seeAlso $ sn $ street $ telephoneNumber $ teletexTerminalIdentifier $ telexNumber $ x121Address ) )", - "( 0.9.2342.19200300.100.4.7 NAME 'room' SUP top STRUCTURAL MUST cn MAY ( roomNumber $ description $ seeAlso $ telephoneNumber ) )", - "( 0.9.2342.19200300.100.4.19 NAME 'simpleSecurityObject' SUP top AUXILIARY MUST userPassword )", -) - -RFC4524_SUBSCHEMA = directory.Subschema(ldapsyntaxes=CORE_SYNTAXES, - matchingrules=CORE_MATCHING_RULES, - attributetypes=RFC4524_ATTRIBUTE_TYPES, - objectclasses=RFC4524_OBJECT_CLASSES) -COSINE_SUBSCHEMA = RFC4524_SUBSCHEMA - -# RFC 2307: NIS Schema - -RFC2307_ATTRIBUTE_TYPES = RFC4519_ATTRIBUTE_TYPES + ( - "( 1.3.6.1.1.1.1.0 NAME 'uidNumber' DESC 'An integer uniquely identifying a user in an administrative domain' EQUALITY integerMatch SYNTAX 'INTEGER' SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.1 NAME 'gidNumber' DESC 'An integer uniquely identifying a group in an administrative domain' EQUALITY integerMatch SYNTAX 'INTEGER' SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.2 NAME 'gecos' DESC 'The GECOS field; the common name' EQUALITY caseIgnoreIA5Match SUBSTRINGS caseIgnoreIA5SubstringsMatch SYNTAX 'IA5String' SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.3 NAME 'homeDirectory' DESC 'The absolute path to the home directory' EQUALITY caseExactIA5Match SYNTAX 'IA5String' SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.4 NAME 'loginShell' DESC 'The path to the login shell' EQUALITY caseExactIA5Match SYNTAX 'IA5String' SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.5 NAME 'shadowLastChange' EQUALITY integerMatch SYNTAX 'INTEGER' SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.6 NAME 'shadowMin' EQUALITY integerMatch SYNTAX 'INTEGER' SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.7 NAME 'shadowMax' EQUALITY integerMatch SYNTAX 'INTEGER' SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.8 NAME 'shadowWarning' EQUALITY integerMatch SYNTAX 'INTEGER' SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.9 NAME 'shadowInactive' EQUALITY integerMatch SYNTAX 'INTEGER' SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.10 NAME 'shadowExpire' EQUALITY integerMatch SYNTAX 'INTEGER' SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.11 NAME 'shadowFlag' EQUALITY integerMatch SYNTAX 'INTEGER' SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.12 NAME 'memberUid' EQUALITY caseExactIA5Match SUBSTRINGS caseExactIA5SubstringsMatch SYNTAX 'IA5String' )", - "( 1.3.6.1.1.1.1.13 NAME 'memberNisNetgroup' EQUALITY caseExactIA5Match SUBSTRINGS caseExactIA5SubstringsMatch SYNTAX 'IA5String' )", - "( 1.3.6.1.1.1.1.14 NAME 'nisNetgroupTriple' DESC 'Netgroup triple' SYNTAX 'nisNetgroupTripleSyntax' ) ( 1.3.6.1.1.1.1.15 NAME 'ipServicePort' EQUALITY integerMatch SYNTAX 'INTEGER' SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.16 NAME 'ipServiceProtocol' SUP name ) ( 1.3.6.1.1.1.1.17 NAME 'ipProtocolNumber' EQUALITY integerMatch SYNTAX 'INTEGER' SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.18 NAME 'oncRpcNumber' EQUALITY integerMatch SYNTAX 'INTEGER' SINGLE-VALUE ) ( 1.3.6.1.1.1.1.19 NAME 'ipHostNumber' DESC 'IP address as a dotted decimal, eg. 192.168.1.1, omitting leading zeros' EQUALITY caseIgnoreIA5Match SYNTAX 'IA5String{128}' )", - "( 1.3.6.1.1.1.1.20 NAME 'ipNetworkNumber' DESC 'IP network as a dotted decimal, eg. 192.168, omitting leading zeros' EQUALITY caseIgnoreIA5Match SYNTAX 'IA5String{128}' SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.21 NAME 'ipNetmaskNumber' DESC 'IP netmask as a dotted decimal, eg. 255.255.255.0, omitting leading zeros' EQUALITY caseIgnoreIA5Match SYNTAX 'IA5String{128}' SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.22 NAME 'macAddress' DESC 'MAC address in maximal, colon separated hex notation, eg. 00:00:92:90:ee:e2' EQUALITY caseIgnoreIA5Match SYNTAX 'IA5String{128}' )", - "( 1.3.6.1.1.1.1.23 NAME 'bootParameter' DESC 'rpc.bootparamd parameter' SYNTAX 'bootParameterSyntax' )", - "( 1.3.6.1.1.1.1.24 NAME 'bootFile' DESC 'Boot image name' EQUALITY caseExactIA5Match SYNTAX 'IA5String' )", - "( 1.3.6.1.1.1.1.26 NAME 'nisMapName' SUP name )", - "( 1.3.6.1.1.1.1.27 NAME 'nisMapEntry' EQUALITY caseExactIA5Match SUBSTRINGS caseExactIA5SubstringsMatch SYNTAX 'IA5String{1024}' SINGLE-VALUE )", -) - -RFC2307_OBJECT_CLASSES = RFC4519_OBJECT_CLASSES + ( - "( 1.3.6.1.1.1.2.0 NAME 'posixAccount' SUP top AUXILIARY DESC 'Abstraction of an account with POSIX attributes' MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory ) MAY ( userPassword $ loginShell $ gecos $ description ) )", - "( 1.3.6.1.1.1.2.1 NAME 'shadowAccount' SUP top AUXILIARY DESC 'Additional attributes for shadow passwords' MUST uid MAY ( userPassword $ shadowLastChange $ shadowMin $ shadowMax $ shadowWarning $ shadowInactive $ shadowExpire $ shadowFlag $ description ) )", - "( 1.3.6.1.1.1.2.2 NAME 'posixGroup' SUP top STRUCTURAL DESC 'Abstraction of a group of accounts' MUST ( cn $ gidNumber ) MAY ( userPassword $ memberUid $ description ) )", - "( 1.3.6.1.1.1.2.3 NAME 'ipService' SUP top STRUCTURAL DESC 'Abstraction an Internet Protocol service. Maps an IP port and protocol (such as tcp or udp) to one or more names; the distinguished value of the cn attribute denotes the service's canonical name' MUST ( cn $ ipServicePort $ ipServiceProtocol ) MAY ( description ) )", - "( 1.3.6.1.1.1.2.4 NAME 'ipProtocol' SUP top STRUCTURAL DESC 'Abstraction of an IP protocol. Maps a protocol number to one or more names. The distinguished value of the cn attribute denotes the protocol's canonical name' MUST ( cn $ ipProtocolNumber $ description ) MAY description )", - "( 1.3.6.1.1.1.2.5 NAME 'oncRpc' SUP top STRUCTURAL DESC 'Abstraction of an Open Network Computing (ONC) [RFC1057] Remote Procedure Call (RPC) binding. This class maps an ONC RPC number to a name. The distinguished value of the cn attribute denotes the RPC service's canonical name' MUST ( cn $ oncRpcNumber $ description ) MAY description )", - "( 1.3.6.1.1.1.2.6 NAME 'ipHost' SUP top AUXILIARY DESC 'Abstraction of a host, an IP device. The distinguished value of the cn attribute denotes the host's canonical name. Device SHOULD be used as a structural class' MUST ( cn $ ipHostNumber ) MAY ( l $ description $ manager ) )", - "( 1.3.6.1.1.1.2.7 NAME 'ipNetwork' SUP top STRUCTURAL DESC 'Abstraction of a network. The distinguished value of the cn attribute denotes the network's canonical name' MUST ( cn $ ipNetworkNumber ) MAY ( ipNetmaskNumber $ l $ description $ manager ) )", - "( 1.3.6.1.1.1.2.8 NAME 'nisNetgroup' SUP top STRUCTURAL DESC 'Abstraction of a netgroup. May refer to other netgroups' MUST cn MAY ( nisNetgroupTriple $ memberNisNetgroup $ description ) )", - "( 1.3.6.1.1.1.2.09 NAME 'nisMap' SUP top STRUCTURAL DESC 'A generic abstraction of a NIS map' MUST nisMapName MAY description )", - "( 1.3.6.1.1.1.2.10 NAME 'nisObject' SUP top STRUCTURAL DESC 'An entry in a NIS map' MUST ( cn $ nisMapEntry $ nisMapName ) MAY description )", - "( 1.3.6.1.1.1.2.11 NAME 'ieee802Device' SUP top AUXILIARY DESC 'A device with a MAC address; device SHOULD be used as a structural class' MAY macAddress )", - "( 1.3.6.1.1.1.2.12 NAME 'bootableDevice' SUP top AUXILIARY DESC 'A device with boot parameters; device SHOULD be used as a structural class' MAY ( bootFile $ bootParameter ) )", -) - -RFC2307_SUBSCHEMA = directory.Subschema(ldapsyntaxes=CORE_SYNTAXES, - matchingrules=CORE_MATCHING_RULES, - attributetypes=RFC2307_ATTRIBUTE_TYPES, - objectclasses=RFC2307_OBJECT_CLASSES) -NIS_SUBSCHEMA = RFC2307_SUBSCHEMA - -RFC2307BIS_ATTRIBUTE_TYPES = RFC4519_ATTRIBUTE_TYPES + ( - "( 1.3.6.1.1.1.1.0 NAME 'uidNumber' DESC 'An integer uniquely identifying a user in an administrative domain' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.1 NAME 'gidNumber' DESC 'An integer uniquely identifying a group in an administrative domain' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.2 NAME 'gecos' DESC 'The GECOS field; the common name' EQUALITY caseIgnoreMatch SUBSTRINGS caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.3 NAME 'homeDirectory' DESC 'The absolute path to the home directory' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.4 NAME 'loginShell' DESC 'The path to the login shell' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.5 NAME 'shadowLastChange' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.6 NAME 'shadowMin' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.7 NAME 'shadowMax' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.8 NAME 'shadowWarning' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.9 NAME 'shadowInactive' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.10 NAME 'shadowExpire' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.11 NAME 'shadowFlag' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.12 NAME 'memberUid' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.1.1.1.13 NAME 'memberNisNetgroup' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.1.1.1.14 NAME 'nisNetgroupTriple' DESC 'Netgroup triple' EQUALITY caseIgnoreMatch SUBSTRINGS caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.1.1.1.15 NAME 'ipServicePort' DESC 'Service port number' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.16 NAME 'ipServiceProtocol' DESC 'Service protocol name' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.1.1.1.17 NAME 'ipProtocolNumber' DESC 'IP protocol number' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.18 NAME 'oncRpcNumber' DESC 'ONC RPC number' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.19 NAME 'ipHostNumber' DESC 'IPv4 addresses as a dotted decimal omitting leading zeros or IPv6 addresses as defined in RFC2373' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 1.3.6.1.1.1.1.20 NAME 'ipNetworkNumber' DESC 'IP network omitting leading zeros, eg. 192.168' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.21 NAME 'ipNetmaskNumber' DESC 'IP netmask omitting leading zeros, eg. 255.255.255.0' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.22 NAME 'macAddress' DESC 'MAC address in maximal, colon separated hex notation, eg. 00:00:92:90:ee:e2' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 1.3.6.1.1.1.1.23 NAME 'bootParameter' DESC 'rpc.bootparamd parameter' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 1.3.6.1.1.1.1.24 NAME 'bootFile' DESC 'Boot image name' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 1.3.6.1.1.1.1.26 NAME 'nisMapName' DESC 'Name of a generic NIS map' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{64} )", - "( 1.3.6.1.1.1.1.27 NAME 'nisMapEntry' DESC 'A generic NIS entry' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1024} SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.28 NAME 'nisPublicKey' DESC 'NIS public key' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.29 NAME 'nisSecretKey' DESC 'NIS secret key' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.30 NAME 'nisDomain' DESC 'NIS domain' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )", - "( 1.3.6.1.1.1.1.31 NAME 'automountMapName' DESC 'automount Map Name' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.32 NAME 'automountKey' DESC 'Automount Key value' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.33 NAME 'automountInformation' DESC 'Automount information' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", -) - -RFC2307BIS_OBJECT_CLASSES = RFC4519_OBJECT_CLASSES + ( - "( 1.3.6.1.1.1.2.0 NAME 'posixAccount' SUP top AUXILIARY DESC 'Abstraction of an account with POSIX attributes' MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory ) MAY ( authPassword $ userPassword $ loginShell $ gecos $ description ) )", - "( 1.3.6.1.1.1.2.1 NAME 'shadowAccount' SUP top AUXILIARY DESC 'Additional attributes for shadow passwords' MUST uid MAY ( authPassword $ userPassword $ description $ shadowLastChange $ shadowMin $ shadowMax $ shadowWarning $ shadowInactive $ shadowExpire $ shadowFlag ) )", - "( 1.3.6.1.1.1.2.2 NAME 'posixGroup' SUP top AUXILIARY DESC 'Abstraction of a group of accounts' MUST gidNumber MAY ( authPassword $ userPassword $ memberUid $ description ) )", - "( 1.3.6.1.1.1.2.3 NAME 'ipService' SUP top STRUCTURAL DESC 'Abstraction an Internet Protocol service. Maps an IP port and protocol (such as tcp or udp) to one or more names; the distinguished value of the cn attribute denotes the service's canonical name' MUST ( cn $ ipServicePort $ ipServiceProtocol ) MAY description )", - "( 1.3.6.1.1.1.2.4 NAME 'ipProtocol' SUP top STRUCTURAL DESC 'Abstraction of an IP protocol. Maps a protocol number to one or more names. The distinguished value of the cn attribute denotes the protocol canonical name' MUST ( cn $ ipProtocolNumber ) MAY description )", - "( 1.3.6.1.1.1.2.5 NAME 'oncRpc' SUP top STRUCTURAL DESC 'Abstraction of an Open Network Computing (ONC) [RFC1057] Remote Procedure Call (RPC) binding. This class maps an ONC RPC number to a name. The distinguished value of the cn attribute denotes the RPC service canonical name' MUST ( cn $ oncRpcNumber ) MAY description )", - "( 1.3.6.1.1.1.2.6 NAME 'ipHost' SUP top AUXILIARY DESC 'Abstraction of a host, an IP device. The distinguished value of the cn attribute denotes the host's canonical name. Device SHOULD be used as a structural class' MUST ( cn $ ipHostNumber ) MAY ( authPassword $ userPassword $ l $ description $ manager ) )", - "( 1.3.6.1.1.1.2.7 NAME 'ipNetwork' SUP top STRUCTURAL DESC 'Abstraction of a network. The distinguished value of the cn attribute denotes the network canonical name' MUST ipNetworkNumber MAY ( cn $ ipNetmaskNumber $ l $ description $ manager ) )", - "( 1.3.6.1.1.1.2.8 NAME 'nisNetgroup' SUP top STRUCTURAL DESC 'Abstraction of a netgroup. May refer to other netgroups' MUST cn MAY ( nisNetgroupTriple $ memberNisNetgroup $ description ) )", - "( 1.3.6.1.1.1.2.9 NAME 'nisMap' SUP top STRUCTURAL DESC 'A generic abstraction of a NIS map' MUST nisMapName MAY description )", - "( 1.3.6.1.1.1.2.10 NAME 'nisObject' SUP top STRUCTURAL DESC 'An entry in a NIS map' MUST ( cn $ nisMapEntry $ nisMapName )", - "( 1.3.6.1.1.1.2.11 NAME 'ieee802Device' SUP top AUXILIARY DESC 'A device with a MAC address; device SHOULD be used as a structural class' MAY macAddress )", - "( 1.3.6.1.1.1.2.12 NAME 'bootableDevice' SUP top AUXILIARY DESC 'A device with boot parameters; device SHOULD be used as a structural class' MAY ( bootFile $ bootParameter ) )", - "( 1.3.6.1.1.1.2.14 NAME 'nisKeyObject' SUP top AUXILIARY DESC 'An object with a public and secret key' MUST ( cn $ nisPublicKey $ nisSecretKey ) MAY ( uidNumber $ description ) )", - "( 1.3.6.1.1.1.2.15 NAME 'nisDomainObject' SUP top AUXILIARY DESC 'Associates a NIS domain with a naming context' MUST nisDomain )", - "( 1.3.6.1.1.1.2.16 NAME 'automountMap' SUP top STRUCTURAL MUST ( automountMapName ) MAY description )", - "( 1.3.6.1.1.1.2.17 NAME 'automount' SUP top STRUCTURAL DESC 'Automount information' MUST ( automountKey $ automountInformation ) MAY description )", - "( 1.3.6.1.1.1.2.18 NAME 'groupOfMembers' SUP top STRUCTURAL DESC 'A group with members (DNs)' MUST cn MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description $ member ) )", -) - -RFC2307BIS_SUBSCHEMA = directory.Subschema(ldapsyntaxes=CORE_SYNTAXES, - matchingrules=CORE_MATCHING_RULES, - attributetypes=RFC2307BIS_ATTRIBUTE_TYPES, - objectclasses=RFC2307BIS_OBJECT_CLASSES) diff --git a/ldapserver/schema/__init__.py b/ldapserver/schema/__init__.py new file mode 100644 index 0000000..483d876 --- /dev/null +++ b/ldapserver/schema/__init__.py @@ -0,0 +1,14 @@ +from .types import * +from . import rfc4517, rfc4512, rfc4519, rfc4524, rfc3112, rfc2307bis, rfc2079, rfc2252, rfc2798, rfc4523, rfc1274 + +# Core LDAP Schema +RFC4519_SUBSCHEMA = Subschema('cn=Subschema', rfc4519.object_classes.ALL, rfc4519.attribute_types.ALL, rfc4519.matching_rules.ALL, rfc4519.matching_rules.ALL) + +# COSINE LDAP/X.500 Schema +RFC4524_SUBSCHEMA = Subschema('cn=Subschema', rfc4524.object_classes.ALL, rfc4524.attribute_types.ALL, rfc4524.matching_rules.ALL, rfc4524.matching_rules.ALL) + +# inetOrgPerson Schema +RFC2798_SUBSCHEMA = Subschema('cn=Subschema', rfc2798.object_classes.ALL, rfc2798.attribute_types.ALL, rfc2798.matching_rules.ALL, rfc2798.matching_rules.ALL) + +# Extended RFC2307 (NIS) Schema +RFC2307BIS_SUBSCHEMA = Subschema('cn=Subschema', rfc2307bis.object_classes.ALL, rfc2307bis.attribute_types.ALL, rfc2307bis.matching_rules.ALL, rfc2307bis.matching_rules.ALL) diff --git a/ldapserver/schema/rfc1274/__init__.py b/ldapserver/schema/rfc1274/__init__.py new file mode 100644 index 0000000..15af506 --- /dev/null +++ b/ldapserver/schema/rfc1274/__init__.py @@ -0,0 +1 @@ +from . import syntaxes, attribute_types diff --git a/ldapserver/schema/rfc1274/attribute_types.py b/ldapserver/schema/rfc1274/attribute_types.py new file mode 100644 index 0000000..c2d5c12 --- /dev/null +++ b/ldapserver/schema/rfc1274/attribute_types.py @@ -0,0 +1,10 @@ +from ..types import AttributeType +from . import syntaxes + +audio = AttributeType('0.9.2342.19200300.100.1.55', name='audio', desc='audio (u-law)', syntax=syntaxes.OctetString(25000)) +photo = AttributeType('0.9.2342.19200300.100.1.7', name='photo', desc='photo (G3 fax)', syntax=syntaxes.OctetString(25000)) + +ALL = ( + audio, + photo, +) diff --git a/ldapserver/schema/rfc1274/syntaxes.py b/ldapserver/schema/rfc1274/syntaxes.py new file mode 100644 index 0000000..c03df67 --- /dev/null +++ b/ldapserver/schema/rfc1274/syntaxes.py @@ -0,0 +1,5 @@ +from ..rfc4517.syntaxes import OctetString + +ALL = ( + OctetString, +) diff --git a/ldapserver/schema/rfc2079/__init__.py b/ldapserver/schema/rfc2079/__init__.py new file mode 100644 index 0000000..1727734 --- /dev/null +++ b/ldapserver/schema/rfc2079/__init__.py @@ -0,0 +1 @@ +from . import syntaxes, matching_rules, attribute_types, object_classes diff --git a/ldapserver/schema/rfc2079/attribute_types.py b/ldapserver/schema/rfc2079/attribute_types.py new file mode 100644 index 0000000..4991c5a --- /dev/null +++ b/ldapserver/schema/rfc2079/attribute_types.py @@ -0,0 +1,8 @@ +from ..types import AttributeType +from . import syntaxes, matching_rules + +labeledURI = AttributeType('1.3.6.1.4.1.250.1.57', name='labeledURI', desc='Uniform Resource Identifier with optional label', equality=matching_rules.caseExactMatch, syntax=syntaxes.DirectoryString()) + +ALL = ( + labeledURI, +) diff --git a/ldapserver/schema/rfc2079/matching_rules.py b/ldapserver/schema/rfc2079/matching_rules.py new file mode 100644 index 0000000..77afc5b --- /dev/null +++ b/ldapserver/schema/rfc2079/matching_rules.py @@ -0,0 +1,5 @@ +from ..rfc4517.matching_rules import caseExactMatch + +ALL = ( + caseExactMatch, +) diff --git a/ldapserver/schema/rfc2079/object_classes.py b/ldapserver/schema/rfc2079/object_classes.py new file mode 100644 index 0000000..0254395 --- /dev/null +++ b/ldapserver/schema/rfc2079/object_classes.py @@ -0,0 +1,9 @@ +from ..types import ObjectClass, ObjectClassKind +from ..rfc4512.object_classes import top +from . import attribute_types + +labeledURIObject = ObjectClass('1.3.6.1.4.1.250.3.15', name='labeledURIObject', desc='object that contains the URI attribute type', sup=top, kind=ObjectClassKind.AUXILIARY, may=[attribute_types.labeledURI]) + +ALL = ( + labeledURIObject, +) diff --git a/ldapserver/schema/rfc2079/syntaxes.py b/ldapserver/schema/rfc2079/syntaxes.py new file mode 100644 index 0000000..5d68139 --- /dev/null +++ b/ldapserver/schema/rfc2079/syntaxes.py @@ -0,0 +1,5 @@ +from ..rfc4517.syntaxes import DirectoryString + +ALL = ( + DirectoryString, +) diff --git a/ldapserver/schema/rfc2252/__init__.py b/ldapserver/schema/rfc2252/__init__.py new file mode 100644 index 0000000..2b160bc --- /dev/null +++ b/ldapserver/schema/rfc2252/__init__.py @@ -0,0 +1 @@ +from . import syntaxes diff --git a/ldapserver/schema/rfc2252/syntaxes.py b/ldapserver/schema/rfc2252/syntaxes.py new file mode 100644 index 0000000..d47d85f --- /dev/null +++ b/ldapserver/schema/rfc2252/syntaxes.py @@ -0,0 +1,19 @@ +from ..types import Syntax + +# Only deprecated syntaxes from the old LDAP v3 RFCs + +class Binary(Syntax): + oid = '1.3.6.1.4.1.1466.115.121.1.5' + desc = 'Binary' + + @staticmethod + def encode(value): + return value + + @staticmethod + def decode(raw_value): + return raw_value + +ALL = ( + Binary, +) diff --git a/ldapserver/schema/rfc2307bis/__init__.py b/ldapserver/schema/rfc2307bis/__init__.py new file mode 100644 index 0000000..1727734 --- /dev/null +++ b/ldapserver/schema/rfc2307bis/__init__.py @@ -0,0 +1 @@ +from . import syntaxes, matching_rules, attribute_types, object_classes diff --git a/ldapserver/schema/rfc2307bis/attribute_types.py b/ldapserver/schema/rfc2307bis/attribute_types.py new file mode 100644 index 0000000..9cca890 --- /dev/null +++ b/ldapserver/schema/rfc2307bis/attribute_types.py @@ -0,0 +1,77 @@ + +# pylint: disable=wildcard-import,unused-wildcard-import,unused-import + +from ..types import AttributeType +from ..rfc4524.attribute_types import * +from ..rfc3112.attribute_types import authPassword, ALL as RFC3112_ALL +from . import syntaxes, matching_rules + +uidNumber = AttributeType('1.3.6.1.1.1.1.0', name='uidNumber', desc='An integer uniquely identifying a user in an administrative domain', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True) +gidNumber = AttributeType('1.3.6.1.1.1.1.1', name='gidNumber', desc='An integer uniquely identifying a group in an administrative domain', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True) +gecos = AttributeType('1.3.6.1.1.1.1.2', name='gecos', desc='The GECOS field; the common name', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(), single_value=True) +homeDirectory = AttributeType('1.3.6.1.1.1.1.3', name='homeDirectory', desc='The absolute path to the home directory', equality=matching_rules.caseExactIA5Match, syntax=syntaxes.IA5String(), single_value=True) +loginShell = AttributeType('1.3.6.1.1.1.1.4', name='loginShell', desc='The path to the login shell', equality=matching_rules.caseExactIA5Match, syntax=syntaxes.IA5String(), single_value=True) +shadowLastChange = AttributeType('1.3.6.1.1.1.1.5', name='shadowLastChange', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True) +shadowMin = AttributeType('1.3.6.1.1.1.1.6', name='shadowMin', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True) +shadowMax = AttributeType('1.3.6.1.1.1.1.7', name='shadowMax', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True) +shadowWarning = AttributeType('1.3.6.1.1.1.1.8', name='shadowWarning', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True) +shadowInactive = AttributeType('1.3.6.1.1.1.1.9', name='shadowInactive', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True) +shadowExpire = AttributeType('1.3.6.1.1.1.1.10', name='shadowExpire', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True) +shadowFlag = AttributeType('1.3.6.1.1.1.1.11', name='shadowFlag', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True) +memberUid = AttributeType('1.3.6.1.1.1.1.12', name='memberUid', equality=matching_rules.caseExactMatch, syntax=syntaxes.DirectoryString()) +memberNisNetgroup = AttributeType('1.3.6.1.1.1.1.13', name='memberNisNetgroup', equality=matching_rules.caseExactMatch, syntax=syntaxes.DirectoryString()) +nisNetgroupTriple = AttributeType('1.3.6.1.1.1.1.14', name='nisNetgroupTriple', desc='Netgroup triple', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString()) +ipServicePort = AttributeType('1.3.6.1.1.1.1.15', name='ipServicePort', desc='Service port number', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True) +ipServiceProtocol = AttributeType('1.3.6.1.1.1.1.16', name='ipServiceProtocol', desc='Service protocol name', equality=matching_rules.caseIgnoreMatch, syntax=syntaxes.DirectoryString()) +ipProtocolNumber = AttributeType('1.3.6.1.1.1.1.17', name='ipProtocolNumber', desc='IP protocol number', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True) +oncRpcNumber = AttributeType('1.3.6.1.1.1.1.18', name='oncRpcNumber', desc='ONC RPC number', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True) +ipHostNumber = AttributeType('1.3.6.1.1.1.1.19', name='ipHostNumber', desc='IPv4 addresses as a dotted decimal omitting leading zeros or IPv6 addresses as defined in RFC2373', equality=matching_rules.caseIgnoreIA5Match, syntax=syntaxes.IA5String()) +ipNetworkNumber = AttributeType('1.3.6.1.1.1.1.20', name='ipNetworkNumber', desc='IP network omitting leading zeros, eg. 192.168', equality=matching_rules.caseIgnoreIA5Match, syntax=syntaxes.IA5String(), single_value=True) +ipNetmaskNumber = AttributeType('1.3.6.1.1.1.1.21', name='ipNetmaskNumber', desc='IP netmask omitting leading zeros, eg. 255.255.255.0', equality=matching_rules.caseIgnoreIA5Match, syntax=syntaxes.IA5String(), single_value=True) +macAddress = AttributeType('1.3.6.1.1.1.1.22', name='macAddress', desc='MAC address in maximal, colon separated hex notation, eg. 00:00:92:90:ee:e2', equality=matching_rules.caseIgnoreIA5Match, syntax=syntaxes.IA5String()) +bootParameter = AttributeType('1.3.6.1.1.1.1.23', name='bootParameter', desc='rpc.bootparamd parameter', equality=matching_rules.caseExactIA5Match, syntax=syntaxes.IA5String()) +bootFile = AttributeType('1.3.6.1.1.1.1.24', name='bootFile', desc='Boot image name', equality=matching_rules.caseExactIA5Match, syntax=syntaxes.IA5String()) +nisMapName = AttributeType('1.3.6.1.1.1.1.26', name='nisMapName', desc='Name of a generic NIS map', equality=matching_rules.caseIgnoreMatch, syntax=syntaxes.DirectoryString(64)) +nisMapEntry = AttributeType('1.3.6.1.1.1.1.27', name='nisMapEntry', desc='A generic NIS entry', equality=matching_rules.caseExactMatch, syntax=syntaxes.DirectoryString(1024), single_value=True) +nisPublicKey = AttributeType('1.3.6.1.1.1.1.28', name='nisPublicKey', desc='NIS public key', equality=matching_rules.octetStringMatch, syntax=syntaxes.OctetString(), single_value=True) +nisSecretKey = AttributeType('1.3.6.1.1.1.1.29', name='nisSecretKey', desc='NIS secret key', equality=matching_rules.octetStringMatch, syntax=syntaxes.OctetString(), single_value=True) +nisDomain = AttributeType('1.3.6.1.1.1.1.30', name='nisDomain', desc='NIS domain', equality=matching_rules.caseIgnoreIA5Match, syntax=syntaxes.IA5String(256)) +automountMapName = AttributeType('1.3.6.1.1.1.1.31', name='automountMapName', desc='automount Map Name', equality=matching_rules.caseExactMatch, syntax=syntaxes.DirectoryString(), single_value=True) +automountKey = AttributeType('1.3.6.1.1.1.1.32', name='automountKey', desc='Automount Key value', equality=matching_rules.caseExactMatch, syntax=syntaxes.DirectoryString(), single_value=True) +automountInformation = AttributeType('1.3.6.1.1.1.1.33', name='automountInformation', desc='Automount information', equality=matching_rules.caseExactMatch, syntax=syntaxes.DirectoryString(), single_value=True) + +ALL = ALL + RFC3112_ALL + ( + uidNumber, + gidNumber, + gecos, + homeDirectory, + loginShell, + shadowLastChange, + shadowMin, + shadowMax, + shadowWarning, + shadowInactive, + shadowExpire, + shadowFlag, + memberUid, + memberNisNetgroup, + nisNetgroupTriple, + ipServicePort, + ipServiceProtocol, + ipProtocolNumber, + oncRpcNumber, + ipHostNumber, + ipNetworkNumber, + ipNetmaskNumber, + macAddress, + bootParameter, + bootFile, + nisMapName, + nisMapEntry, + nisPublicKey, + nisSecretKey, + nisDomain, + automountMapName, + automountKey, + automountInformation, +) diff --git a/ldapserver/schema/rfc2307bis/matching_rules.py b/ldapserver/schema/rfc2307bis/matching_rules.py new file mode 100644 index 0000000..adeaf47 --- /dev/null +++ b/ldapserver/schema/rfc2307bis/matching_rules.py @@ -0,0 +1,7 @@ + +# pylint: disable=wildcard-import,unused-wildcard-import + +from ..rfc4524.matching_rules import * +from ..rfc3112.matching_rules import ALL as RFC3112_ALL + +ALL = ALL + RFC3112_ALL diff --git a/ldapserver/schema/rfc2307bis/object_classes.py b/ldapserver/schema/rfc2307bis/object_classes.py new file mode 100644 index 0000000..f61e7d2 --- /dev/null +++ b/ldapserver/schema/rfc2307bis/object_classes.py @@ -0,0 +1,47 @@ + +# pylint: disable=wildcard-import,unused-wildcard-import + +from ..types import ObjectClass, ObjectClassKind +from ..rfc3112.object_classes import ALL as RFC3112_ALL +from ..rfc4524.object_classes import * +from . import attribute_types + +posixAccount = ObjectClass('1.3.6.1.1.1.2.0', name='posixAccount', sup=top, kind=ObjectClassKind.AUXILIARY, desc='Abstraction of an account with POSIX attributes', must=[attribute_types.cn, attribute_types.uid, attribute_types.uidNumber, attribute_types.gidNumber, attribute_types.homeDirectory], may=[attribute_types.authPassword, attribute_types.userPassword, attribute_types.loginShell, attribute_types.gecos, attribute_types.description]) +shadowAccount = ObjectClass('1.3.6.1.1.1.2.1', name='shadowAccount', sup=top, kind=ObjectClassKind.AUXILIARY, desc='Additional attributes for shadow passwords', must=[attribute_types.uid], may=[attribute_types.authPassword, attribute_types.userPassword, attribute_types.description, attribute_types.shadowLastChange, attribute_types.shadowMin, attribute_types.shadowMax, attribute_types.shadowWarning, attribute_types.shadowInactive, attribute_types.shadowExpire, attribute_types.shadowFlag]) +posixGroup = ObjectClass('1.3.6.1.1.1.2.2', name='posixGroup', sup=top, kind=ObjectClassKind.AUXILIARY, desc='Abstraction of a group of accounts', must=[attribute_types.gidNumber], may=[attribute_types.authPassword, attribute_types.userPassword, attribute_types.memberUid, attribute_types.description]) +ipService = ObjectClass('1.3.6.1.1.1.2.3', name='ipService', sup=top, kind=ObjectClassKind.STRUCTURAL, desc='Abstraction an Internet Protocol service. Maps an IP port and protocol (such as tcp or udp) to one or more names; the distinguished value of the cn attribute denotes the service\'s canonical name', must=[attribute_types.cn, attribute_types.ipServicePort, attribute_types.ipServiceProtocol], may=[attribute_types.description]) +ipProtocol = ObjectClass('1.3.6.1.1.1.2.4', name='ipProtocol', sup=top, kind=ObjectClassKind.STRUCTURAL, desc='Abstraction of an IP protocol. Maps a protocol number to one or more names. The distinguished value of the cn attribute denotes the protocol canonical name', must=[attribute_types.cn, attribute_types.ipProtocolNumber], may=[attribute_types.description]) +oncRpc = ObjectClass('1.3.6.1.1.1.2.5', name='oncRpc', sup=top, kind=ObjectClassKind.STRUCTURAL, desc='Abstraction of an Open Network Computing (ONC) [RFC1057] Remote Procedure Call (RPC) binding. This class maps an ONC RPC number to a name. The distinguished value of the cn attribute denotes the RPC service canonical name', must=[attribute_types.cn, attribute_types.oncRpcNumber], may=[attribute_types.description]) +ipHost = ObjectClass('1.3.6.1.1.1.2.6', name='ipHost', sup=top, kind=ObjectClassKind.AUXILIARY, desc='Abstraction of a host, an IP device. The distinguished value of the cn attribute denotes the host\'s canonical name. Device SHOULD be used as a structural class', must=[attribute_types.cn, attribute_types.ipHostNumber], may=[attribute_types.authPassword, attribute_types.userPassword, attribute_types.l, attribute_types.description, attribute_types.manager]) +ipNetwork = ObjectClass('1.3.6.1.1.1.2.7', name='ipNetwork', sup=top, kind=ObjectClassKind.STRUCTURAL, desc='Abstraction of a network. The distinguished value of the cn attribute denotes the network canonical name', must=[attribute_types.ipNetworkNumber], may=[attribute_types.cn, attribute_types.ipNetmaskNumber, attribute_types.l, attribute_types.description, attribute_types.manager]) +nisNetgroup = ObjectClass('1.3.6.1.1.1.2.8', name='nisNetgroup', sup=top, kind=ObjectClassKind.STRUCTURAL, desc='Abstraction of a netgroup. May refer to other netgroups', must=[attribute_types.cn], may=[attribute_types.nisNetgroupTriple, attribute_types.memberNisNetgroup, attribute_types.description]) +nisMap = ObjectClass('1.3.6.1.1.1.2.9', name='nisMap', sup=top, kind=ObjectClassKind.STRUCTURAL, desc='A generic abstraction of a NIS map', must=[attribute_types.nisMapName], may=[attribute_types.description]) +nisObject = ObjectClass('1.3.6.1.1.1.2.10', name='nisObject', sup=top, kind=ObjectClassKind.STRUCTURAL, desc='An entry in a NIS map', must=[attribute_types.cn, attribute_types.nisMapEntry, attribute_types.nisMapName]) +ieee802Device = ObjectClass('1.3.6.1.1.1.2.11', name='ieee802Device', sup=top, kind=ObjectClassKind.AUXILIARY, desc='A device with a MAC address; device SHOULD be used as a structural class', may=[attribute_types.macAddress]) +bootableDevice = ObjectClass('1.3.6.1.1.1.2.12', name='bootableDevice', sup=top, kind=ObjectClassKind.AUXILIARY, desc='A device with boot parameters; device SHOULD be used as a structural class', may=[attribute_types.bootFile, attribute_types.bootParameter]) +nisKeyObject = ObjectClass('1.3.6.1.1.1.2.14', name='nisKeyObject', sup=top, kind=ObjectClassKind.AUXILIARY, desc='An object with a public and secret key', must=[attribute_types.cn, attribute_types.nisPublicKey, attribute_types.nisSecretKey], may=[attribute_types.uidNumber, attribute_types.description]) +nisDomainObject = ObjectClass('1.3.6.1.1.1.2.15', name='nisDomainObject', sup=top, kind=ObjectClassKind.AUXILIARY, desc='Associates a NIS domain with a naming context', must=[attribute_types.nisDomain]) +automountMap = ObjectClass('1.3.6.1.1.1.2.16', name='automountMap', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.automountMapName], may=[attribute_types.description]) +automount = ObjectClass('1.3.6.1.1.1.2.17', name='automount', sup=top, kind=ObjectClassKind.STRUCTURAL, desc='Automount information', must=[attribute_types.automountKey, attribute_types.automountInformation], may=[attribute_types.description]) +groupOfMembers = ObjectClass('1.3.6.1.1.1.2.18', name='groupOfMembers', sup=top, kind=ObjectClassKind.STRUCTURAL, desc='A group with members (DNs)', must=[attribute_types.cn], may=[attribute_types.businessCategory, attribute_types.seeAlso, attribute_types.owner, attribute_types.ou, attribute_types.o, attribute_types.description, attribute_types.member]) + +ALL = ALL + RFC3112_ALL + ( + posixAccount, + shadowAccount, + posixGroup, + ipService, + ipProtocol, + oncRpc, + ipHost, + ipNetwork, + nisNetgroup, + nisMap, + nisObject, + ieee802Device, + bootableDevice, + nisKeyObject, + nisDomainObject, + automountMap, + automount, + groupOfMembers, +) diff --git a/ldapserver/schema/rfc2307bis/syntaxes.py b/ldapserver/schema/rfc2307bis/syntaxes.py new file mode 100644 index 0000000..8156f9b --- /dev/null +++ b/ldapserver/schema/rfc2307bis/syntaxes.py @@ -0,0 +1,7 @@ + +# pylint: disable=wildcard-import,unused-wildcard-import + +from ..rfc4524.syntaxes import * +from ..rfc3112.syntaxes import ALL as RFC3112_ALL + +ALL = ALL + RFC3112_ALL diff --git a/ldapserver/schema/rfc2798/__init__.py b/ldapserver/schema/rfc2798/__init__.py new file mode 100644 index 0000000..1727734 --- /dev/null +++ b/ldapserver/schema/rfc2798/__init__.py @@ -0,0 +1 @@ +from . import syntaxes, matching_rules, attribute_types, object_classes diff --git a/ldapserver/schema/rfc2798/attribute_types.py b/ldapserver/schema/rfc2798/attribute_types.py new file mode 100644 index 0000000..67ecd33 --- /dev/null +++ b/ldapserver/schema/rfc2798/attribute_types.py @@ -0,0 +1,37 @@ + +# pylint: disable=wildcard-import,unused-wildcard-import,unused-import + +from ..types import AttributeType +# RFC2798 is originally based on the old LDAPv3 RFC2256, the old +# COSINE RFC1274 and RFC2079 (for labeledURI). RFC2256 and RFC1274 +# were obsoleted by RFC4524 and RFC4519. They also updated RFC2798. +from ..rfc4524.attribute_types import * +from ..rfc2079.attribute_types import labeledURI, ALL as RFC2079_ALL +from ..rfc4523.attribute_types import userCertificate +from ..rfc1274.attribute_types import audio, photo +from . import syntaxes, matching_rules + +carLicense = AttributeType('2.16.840.1.113730.3.1.1', name='carLicense', desc='vehicle license or registration plate', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString()) +departmentNumber = AttributeType('2.16.840.1.113730.3.1.2', name='departmentNumber', desc='identifies a department within an organization', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString()) +displayName = AttributeType('2.16.840.1.113730.3.1.241', name='displayName', desc='preferred name of a person to be used when displaying entries', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(), single_value=True) +employeeNumber = AttributeType('2.16.840.1.113730.3.1.3', name='employeeNumber', desc='numerically identifies an employee within an organization', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(), single_value=True) +employeeType = AttributeType('2.16.840.1.113730.3.1.4', name='employeeType', desc='type of employment for a person', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString()) +jpegPhoto = AttributeType('0.9.2342.19200300.100.1.60', name='jpegPhoto', desc='a JPEG image', syntax=syntaxes.JPEG()) +preferredLanguage = AttributeType('2.16.840.1.113730.3.1.39', name='preferredLanguage', desc='preferred written or spoken language for a person', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(), single_value=True) +userSMIMECertificate = AttributeType('2.16.840.1.113730.3.1.40', name='userSMIMECertificate', desc='PKCS#7 SignedData used to support S/MIME', syntax=syntaxes.Binary()) +userPKCS12 = AttributeType('2.16.840.1.113730.3.1.216', name='userPKCS12', desc='PKCS #12 PFX PDU for exchange of personal identity information', syntax=syntaxes.Binary()) + +ALL = ALL + RFC2079_ALL + ( + userCertificate, # RFC4523 + audio, # RFC1274 + photo, # RFC1274 + carLicense, + departmentNumber, + displayName, + employeeNumber, + employeeType, + jpegPhoto, + preferredLanguage, + userSMIMECertificate, + userPKCS12, +) diff --git a/ldapserver/schema/rfc2798/matching_rules.py b/ldapserver/schema/rfc2798/matching_rules.py new file mode 100644 index 0000000..cea092d --- /dev/null +++ b/ldapserver/schema/rfc2798/matching_rules.py @@ -0,0 +1,4 @@ + +# pylint: disable=wildcard-import,unused-wildcard-import + +from ..rfc4524.matching_rules import * diff --git a/ldapserver/schema/rfc2798/object_classes.py b/ldapserver/schema/rfc2798/object_classes.py new file mode 100644 index 0000000..370759b --- /dev/null +++ b/ldapserver/schema/rfc2798/object_classes.py @@ -0,0 +1,12 @@ + +# pylint: disable=wildcard-import,unused-wildcard-import + +from ..types import ObjectClass, ObjectClassKind +from ..rfc4524.object_classes import * +from . import attribute_types + +inetOrgPerson = ObjectClass('2.16.840.1.113730.3.2.2', name='inetOrgPerson', sup=organizationalPerson, kind=ObjectClassKind.STRUCTURAL, may=[attribute_types.businessCategory, attribute_types.carLicense, attribute_types.departmentNumber, attribute_types.displayName, attribute_types.employeeNumber, attribute_types.employeeType, attribute_types.givenName, attribute_types.homePhone, attribute_types.homePostalAddress, attribute_types.initials, attribute_types.jpegPhoto, attribute_types.labeledURI, attribute_types.mail, attribute_types.manager, attribute_types.mobile, attribute_types.o, attribute_types.pager, attribute_types.roomNumber, attribute_types.secretary, attribute_types.uid, attribute_types.x500UniqueIdentifier, attribute_types.preferredLanguage, attribute_types.userSMIMECertificate, attribute_types.userPKCS12, attribute_types.userCertificate, attribute_types.audio, attribute_types.photo]) + +ALL = ALL + ( + inetOrgPerson, +) diff --git a/ldapserver/schema/rfc2798/syntaxes.py b/ldapserver/schema/rfc2798/syntaxes.py new file mode 100644 index 0000000..ee6a435 --- /dev/null +++ b/ldapserver/schema/rfc2798/syntaxes.py @@ -0,0 +1,7 @@ + +# pylint: disable=wildcard-import,unused-wildcard-import + +from ..rfc4524.syntaxes import * +from ..rfc2252.syntaxes import Binary + +ALL = ALL + (Binary,) diff --git a/ldapserver/schema/rfc3112/__init__.py b/ldapserver/schema/rfc3112/__init__.py new file mode 100644 index 0000000..1727734 --- /dev/null +++ b/ldapserver/schema/rfc3112/__init__.py @@ -0,0 +1 @@ +from . import syntaxes, matching_rules, attribute_types, object_classes diff --git a/ldapserver/schema/rfc3112/attribute_types.py b/ldapserver/schema/rfc3112/attribute_types.py new file mode 100644 index 0000000..534b574 --- /dev/null +++ b/ldapserver/schema/rfc3112/attribute_types.py @@ -0,0 +1,10 @@ +from ..types import AttributeType, AttributeTypeUsage +from . import syntaxes, matching_rules + +supportedAuthPasswordSchemes = AttributeType('1.3.6.1.4.1.4203.1.3.3', name='supportedAuthPasswordSchemes', desc='supported password storage schemes', equality=matching_rules.caseIgnoreIA5Match, syntax=syntaxes.IA5String(32), usage=AttributeTypeUsage.dSAOperation) +authPassword = AttributeType('1.3.6.1.4.1.4203.1.3.4', name='authPassword', desc='password authentication information', equality=matching_rules.authPasswordExactMatch, syntax=syntaxes.AuthPasswordSyntax()) + +ALL = ( + supportedAuthPasswordSchemes, + authPassword, +) diff --git a/ldapserver/schema/rfc3112/matching_rules.py b/ldapserver/schema/rfc3112/matching_rules.py new file mode 100644 index 0000000..d0aee5f --- /dev/null +++ b/ldapserver/schema/rfc3112/matching_rules.py @@ -0,0 +1,16 @@ + +# pylint: disable=unused-import + +from ..types import MatchingRule +from ..rfc4517.matching_rules import caseIgnoreIA5Match +from . import syntaxes + +authPasswordExactMatch = MatchingRule('1.3.6.1.4.1.4203.1.2.2', name='authPasswordExactMatch', desc='authentication password exact matching rule', syntax=syntaxes.AuthPasswordSyntax()) + +# We won't implement any actual schemes here, so the default behaviour of MatchingRule (return UNDEFINED) is fine. +authPasswordMatch = MatchingRule('1.3.6.1.4.1.4203.1.2.3', name='authPasswordMatch', desc='authentication password matching rule', syntax=syntaxes.OctetString(128)) + +ALL = ( + authPasswordExactMatch, + authPasswordMatch, +) diff --git a/ldapserver/schema/rfc3112/object_classes.py b/ldapserver/schema/rfc3112/object_classes.py new file mode 100644 index 0000000..749d91f --- /dev/null +++ b/ldapserver/schema/rfc3112/object_classes.py @@ -0,0 +1,8 @@ +from ..types import ObjectClass, ObjectClassKind +from . import attribute_types + +authPasswordObject = ObjectClass('1.3.6.1.4.1.4203.1.4.7', name='authPasswordObject', desc='authentication password mix in class', kind=ObjectClassKind.AUXILIARY, may=[attribute_types.authPassword]) + +ALL = ( + authPasswordObject, +) diff --git a/ldapserver/schema/rfc3112/syntaxes.py b/ldapserver/schema/rfc3112/syntaxes.py new file mode 100644 index 0000000..c954aef --- /dev/null +++ b/ldapserver/schema/rfc3112/syntaxes.py @@ -0,0 +1,12 @@ + +# pylint: disable=unused-import + +from ..rfc4517.syntaxes import IA5String, OctetString, BytesSyntax + +class AuthPasswordSyntax(BytesSyntax): + oid = '1.3.6.1.4.1.4203.1.1.2' + desc = 'authentication password syntax' + +ALL = ( + AuthPasswordSyntax, +) diff --git a/ldapserver/schema/rfc4512/__init__.py b/ldapserver/schema/rfc4512/__init__.py new file mode 100644 index 0000000..1727734 --- /dev/null +++ b/ldapserver/schema/rfc4512/__init__.py @@ -0,0 +1 @@ +from . import syntaxes, matching_rules, attribute_types, object_classes diff --git a/ldapserver/schema/rfc4512/attribute_types.py b/ldapserver/schema/rfc4512/attribute_types.py new file mode 100644 index 0000000..dc1f340 --- /dev/null +++ b/ldapserver/schema/rfc4512/attribute_types.py @@ -0,0 +1,54 @@ +from ..types import AttributeType, AttributeTypeUsage +from . import syntaxes, matching_rules + +aliasedObjectName = AttributeType('2.5.4.1', name='aliasedObjectName', equality=matching_rules.distinguishedNameMatch, syntax=syntaxes.DN(), single_value=True) +objectClass = AttributeType('2.5.4.0', name='objectClass', equality=matching_rules.objectIdentifierMatch, syntax=syntaxes.OID()) +creatorsName = AttributeType('2.5.18.3', name='creatorsName', equality=matching_rules.distinguishedNameMatch, syntax=syntaxes.DN(), single_value=True, no_user_modification=True, usage=AttributeTypeUsage.directoryOperation) +createTimestamp = AttributeType('2.5.18.1', name='createTimestamp', equality=matching_rules.generalizedTimeMatch, ordering=matching_rules.generalizedTimeOrderingMatch, syntax=syntaxes.GeneralizedTime(), single_value=True, no_user_modification=True, usage=AttributeTypeUsage.directoryOperation) +modifiersName = AttributeType('2.5.18.4', name='modifiersName', equality=matching_rules.distinguishedNameMatch, syntax=syntaxes.DN(), single_value=True, no_user_modification=True, usage=AttributeTypeUsage.directoryOperation) +modifyTimestamp = AttributeType('2.5.18.2', name='modifyTimestamp', equality=matching_rules.generalizedTimeMatch, ordering=matching_rules.generalizedTimeOrderingMatch, syntax=syntaxes.GeneralizedTime(), single_value=True, no_user_modification=True, usage=AttributeTypeUsage.directoryOperation) +structuralObjectClass = AttributeType('2.5.21.9', name='structuralObjectClass', equality=matching_rules.objectIdentifierMatch, syntax=syntaxes.OID(), single_value=True, no_user_modification=True, usage=AttributeTypeUsage.directoryOperation) +governingStructureRule = AttributeType('2.5.21.10', name='governingStructureRule', equality=matching_rules.integerMatch, syntax=syntaxes.INTEGER(), single_value=True, no_user_modification=True, usage=AttributeTypeUsage.directoryOperation) +subschemaSubentry = AttributeType('2.5.18.10', name='subschemaSubentry', equality=matching_rules.distinguishedNameMatch, syntax=syntaxes.DN(), single_value=True, no_user_modification=True, usage=AttributeTypeUsage.directoryOperation) +objectClasses = AttributeType('2.5.21.6', name='objectClasses', equality=matching_rules.objectIdentifierFirstComponentMatch, syntax=syntaxes.ObjectClassDescription(), usage=AttributeTypeUsage.directoryOperation) +attributeTypes = AttributeType('2.5.21.5', name='attributeTypes', equality=matching_rules.objectIdentifierFirstComponentMatch, syntax=syntaxes.AttributeTypeDescription(), usage=AttributeTypeUsage.directoryOperation) +matchingRules = AttributeType('2.5.21.4', name='matchingRules', equality=matching_rules.objectIdentifierFirstComponentMatch, syntax=syntaxes.MatchingRuleDescription(), usage=AttributeTypeUsage.directoryOperation) +matchingRuleUse = AttributeType('2.5.21.8', name='matchingRuleUse', equality=matching_rules.objectIdentifierFirstComponentMatch, syntax=syntaxes.MatchingRuleUseDescription(), usage=AttributeTypeUsage.directoryOperation) +ldapSyntaxes = AttributeType('1.3.6.1.4.1.1466.101.120.16', name='ldapSyntaxes', equality=matching_rules.objectIdentifierFirstComponentMatch, syntax=syntaxes.LDAPSyntaxDescription(), usage=AttributeTypeUsage.directoryOperation) +dITContentRules = AttributeType('2.5.21.2', name='dITContentRules', equality=matching_rules.objectIdentifierFirstComponentMatch, syntax=syntaxes.DITContentRuleDescription(), usage=AttributeTypeUsage.directoryOperation) +dITStructureRules = AttributeType('2.5.21.1', name='dITStructureRules', equality=matching_rules.integerFirstComponentMatch, syntax=syntaxes.DITStructureRuleDescription(), usage=AttributeTypeUsage.directoryOperation) +nameForms = AttributeType('2.5.21.7', name='nameForms', equality=matching_rules.objectIdentifierFirstComponentMatch, syntax=syntaxes.NameFormDescription(), usage=AttributeTypeUsage.directoryOperation) +altServer = AttributeType('1.3.6.1.4.1.1466.101.120.6', name='altServer', syntax=syntaxes.IA5String(), usage=AttributeTypeUsage.dSAOperation) +namingContexts = AttributeType('1.3.6.1.4.1.1466.101.120.5', name='namingContexts', syntax=syntaxes.DN(), usage=AttributeTypeUsage.dSAOperation) +supportedControl = AttributeType('1.3.6.1.4.1.1466.101.120.13', name='supportedControl', syntax=syntaxes.OID(), usage=AttributeTypeUsage.dSAOperation) +supportedExtension = AttributeType('1.3.6.1.4.1.1466.101.120.7', name='supportedExtension', syntax=syntaxes.OID(), usage=AttributeTypeUsage.dSAOperation) +supportedFeatures = AttributeType('1.3.6.1.4.1.4203.1.3.5', name='supportedFeatures', equality=matching_rules.objectIdentifierMatch, syntax=syntaxes.OID(), usage=AttributeTypeUsage.dSAOperation) +supportedLDAPVersion = AttributeType('1.3.6.1.4.1.1466.101.120.15', name='supportedLDAPVersion', syntax=syntaxes.INTEGER(), usage=AttributeTypeUsage.dSAOperation) +supportedSASLMechanisms = AttributeType('1.3.6.1.4.1.1466.101.120.14', name='supportedSASLMechanisms', syntax=syntaxes.DirectoryString(), usage=AttributeTypeUsage.dSAOperation) + +ALL = ( + aliasedObjectName, + objectClass, + creatorsName, + createTimestamp, + modifiersName, + modifyTimestamp, + structuralObjectClass, + governingStructureRule, + subschemaSubentry, + objectClasses, + attributeTypes, + matchingRules, + matchingRuleUse, + ldapSyntaxes, + dITContentRules, + dITStructureRules, + nameForms, + altServer, + namingContexts, + supportedControl, + supportedExtension, + supportedFeatures, + supportedLDAPVersion, + supportedSASLMechanisms, +) diff --git a/ldapserver/schema/rfc4512/matching_rules.py b/ldapserver/schema/rfc4512/matching_rules.py new file mode 100644 index 0000000..ee6bd4a --- /dev/null +++ b/ldapserver/schema/rfc4512/matching_rules.py @@ -0,0 +1,4 @@ + +# pylint: disable=wildcard-import,unused-wildcard-import + +from ..rfc4517.matching_rules import * diff --git a/ldapserver/schema/rfc4512/object_classes.py b/ldapserver/schema/rfc4512/object_classes.py new file mode 100644 index 0000000..8f8f9d3 --- /dev/null +++ b/ldapserver/schema/rfc4512/object_classes.py @@ -0,0 +1,14 @@ +from ..types import ObjectClass, ObjectClassKind +from . import attribute_types + +top = ObjectClass('2.5.6.0', 'top', kind=ObjectClassKind.ABSTRACT, must=[attribute_types.objectClass]) +alias = ObjectClass('2.5.6.1', 'alias', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.aliasedObjectName]) +subschema = ObjectClass('2.5.20.1', 'subschema', kind=ObjectClassKind.AUXILIARY, may=[attribute_types.dITStructureRules, attribute_types.nameForms, attribute_types.dITContentRules, attribute_types.objectClasses, attribute_types.attributeTypes, attribute_types.matchingRules, attribute_types.matchingRuleUse]) +extensibleObject = ObjectClass('1.3.6.1.4.1.1466.101.120.111', 'extensibleObject', sup=top, kind=ObjectClassKind.AUXILIARY) + +ALL = ( + top, + alias, + subschema, + extensibleObject, +) diff --git a/ldapserver/schema/rfc4512/syntaxes.py b/ldapserver/schema/rfc4512/syntaxes.py new file mode 100644 index 0000000..657f66e --- /dev/null +++ b/ldapserver/schema/rfc4512/syntaxes.py @@ -0,0 +1,4 @@ + +# pylint: disable=wildcard-import,unused-wildcard-import + +from ..rfc4517.syntaxes import * diff --git a/ldapserver/schema/rfc4517/__init__.py b/ldapserver/schema/rfc4517/__init__.py new file mode 100644 index 0000000..051bbed --- /dev/null +++ b/ldapserver/schema/rfc4517/__init__.py @@ -0,0 +1 @@ +from . import syntaxes, matching_rules diff --git a/ldapserver/schema/rfc4517/matching_rules.py b/ldapserver/schema/rfc4517/matching_rules.py new file mode 100644 index 0000000..bd43cb6 --- /dev/null +++ b/ldapserver/schema/rfc4517/matching_rules.py @@ -0,0 +1,185 @@ +from ..types import MatchingRule, FilterResult +from ... import rfc4518_stringprep +from . import syntaxes + +class GenericMatchingRule(MatchingRule): + def match_equal(self, attribute_value, assertion_value): + if attribute_value == assertion_value: + return FilterResult.TRUE + else: + return FilterResult.FALSE + + def match_less(self, attribute_value, assertion_value): + if attribute_value < assertion_value: + return FilterResult.TRUE + else: + return FilterResult.FALSE + + def match_greater_or_equal(self, attribute_value, assertion_value): + if attribute_value >= assertion_value: + return FilterResult.TRUE + else: + return FilterResult.FALSE + +class StringMatchingRule(MatchingRule): + def __init__(self, oid, name, syntax, matching_type=rfc4518_stringprep.MatchingType.EXACT_STRING): + super().__init__(oid, name, syntax) + self.matching_type = matching_type + + def match_equal(self, attribute_value, assertion_value): + try: + attribute_value = rfc4518_stringprep.prepare(attribute_value, self.matching_type) + assertion_value = rfc4518_stringprep.prepare(assertion_value, self.matching_type) + except ValueError: + return FilterResult.UNDEFINED + if attribute_value == assertion_value: + return FilterResult.TRUE + else: + return FilterResult.FALSE + + def match_less(self, attribute_value, assertion_value): + try: + attribute_value = rfc4518_stringprep.prepare(attribute_value, self.matching_type) + assertion_value = rfc4518_stringprep.prepare(assertion_value, self.matching_type) + except ValueError: + return FilterResult.UNDEFINED + if attribute_value < assertion_value: + return FilterResult.TRUE + else: + return FilterResult.FALSE + + def match_greater_or_equal(self, attribute_value, assertion_value): + try: + attribute_value = rfc4518_stringprep.prepare(attribute_value, self.matching_type) + assertion_value = rfc4518_stringprep.prepare(assertion_value, self.matching_type) + except ValueError: + return FilterResult.UNDEFINED + if attribute_value >= assertion_value: + return FilterResult.TRUE + else: + return FilterResult.FALSE + + def match_substr(self, attribute_value, inital_substring, any_substrings, final_substring): + try: + attribute_value = rfc4518_stringprep.prepare(attribute_value, self.matching_type) + if inital_substring: + inital_substring = rfc4518_stringprep.prepare(inital_substring, self.matching_type, rfc4518_stringprep.SubstringType.INITIAL) + any_substrings = [rfc4518_stringprep.prepare(substring, self.matching_type, rfc4518_stringprep.SubstringType.ANY) for substring in any_substrings] + if final_substring: + final_substring = rfc4518_stringprep.prepare(final_substring, self.matching_type, rfc4518_stringprep.SubstringType.FINAL) + except ValueError: + return FilterResult.UNDEFINED + if inital_substring: + if not attribute_value.startswith(inital_substring): + return FilterResult.FALSE + attribute_value = attribute_value[len(inital_substring):] + if final_substring: + if not attribute_value.endswith(final_substring): + return FilterResult.FALSE + attribute_value = attribute_value[:-len(final_substring)] + for substring in any_substrings: + index = attribute_value.find(substring) + if index == -1: + return FilterResult.FALSE + attribute_value = attribute_value[index+len(substring):] + return FilterResult.TRUE + +class StringListMatchingRule(MatchingRule): + def __init__(self, oid, name, syntax, matching_type=rfc4518_stringprep.MatchingType.EXACT_STRING): + super().__init__(oid, name, syntax) + self.matching_type = matching_type + + # Values are both lists of str + def match_equal(self, attribute_value, assertion_value): + try: + attribute_value = [rfc4518_stringprep.prepare(line, self.matching_type) for line in attribute_value] + assertion_value = [rfc4518_stringprep.prepare(line, self.matching_type) for line in assertion_value] + except ValueError: + return FilterResult.UNDEFINED + if attribute_value == assertion_value: + return FilterResult.TRUE + else: + return FilterResult.FALSE + +class FirstComponentMatchingRule(MatchingRule): + def __init__(self, oid, name, syntax, attribute_name): + super().__init__(oid, name, syntax) + self.attribute_name = attribute_name + + def match_equal(self, attribute_value, assertion_value): + if not hasattr(attribute_value, self.attribute_name): + return None + if getattr(attribute_value, self.attribute_name)() == assertion_value: + return FilterResult.TRUE + else: + return FilterResult.FALSE + +bitStringMatch = GenericMatchingRule('2.5.13.16', name='bitStringMatch', syntax=syntaxes.BitString()) +booleanMatch = GenericMatchingRule('2.5.13.13', name='booleanMatch', syntax=syntaxes.Boolean()) +caseExactIA5Match = StringMatchingRule('1.3.6.1.4.1.1466.109.114.1', name='caseExactIA5Match', syntax=syntaxes.IA5String(), matching_type=rfc4518_stringprep.MatchingType.EXACT_STRING) +caseExactMatch = StringMatchingRule('2.5.13.5', name='caseExactMatch', syntax=syntaxes.DirectoryString(), matching_type=rfc4518_stringprep.MatchingType.EXACT_STRING) +caseExactOrderingMatch = StringMatchingRule('2.5.13.6', name='caseExactOrderingMatch', syntax=syntaxes.DirectoryString(), matching_type=rfc4518_stringprep.MatchingType.EXACT_STRING) +caseExactSubstringsMatch = StringMatchingRule('2.5.13.7', name='caseExactSubstringsMatch', syntax=syntaxes.SubstringAssertion(), matching_type=rfc4518_stringprep.MatchingType.EXACT_STRING) +caseIgnoreIA5Match = StringMatchingRule('1.3.6.1.4.1.1466.109.114.2', name='caseIgnoreIA5Match', syntax=syntaxes.IA5String(), matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING) +caseIgnoreIA5SubstringsMatch = StringMatchingRule('1.3.6.1.4.1.1466.109.114.3', name='caseIgnoreIA5SubstringsMatch', syntax=syntaxes.SubstringAssertion(), matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING) +caseIgnoreListMatch = StringListMatchingRule('2.5.13.11', name='caseIgnoreListMatch', syntax=syntaxes.PostalAddress(), matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING) +caseIgnoreListSubstringsMatch = StringListMatchingRule('2.5.13.12', name='caseIgnoreListSubstringsMatch', syntax=syntaxes.SubstringAssertion(), matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING) +caseIgnoreMatch = StringMatchingRule('2.5.13.2', name='caseIgnoreMatch', syntax=syntaxes.DirectoryString(), matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING) +caseIgnoreOrderingMatch = StringMatchingRule('2.5.13.3', name='caseIgnoreOrderingMatch', syntax=syntaxes.DirectoryString(), matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING) +caseIgnoreSubstringsMatch = StringMatchingRule('2.5.13.4', name='caseIgnoreSubstringsMatch', syntax=syntaxes.SubstringAssertion(), matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING) +directoryStringFirstComponentMatch = FirstComponentMatchingRule('2.5.13.31', name='directoryStringFirstComponentMatch', syntax=syntaxes.DirectoryString(), attribute_name='get_first_component_string') +distinguishedNameMatch = GenericMatchingRule('2.5.13.1', name='distinguishedNameMatch', syntax=syntaxes.DN()) +generalizedTimeMatch = GenericMatchingRule('2.5.13.27', name='generalizedTimeMatch', syntax=syntaxes.GeneralizedTime()) +generalizedTimeOrderingMatch = GenericMatchingRule('2.5.13.28', name='generalizedTimeOrderingMatch', syntax=syntaxes.GeneralizedTime()) +integerFirstComponentMatch = FirstComponentMatchingRule('2.5.13.29', name='integerFirstComponentMatch', syntax=syntaxes.INTEGER(), attribute_name='get_first_component_integer') +integerMatch = GenericMatchingRule('2.5.13.14', name='integerMatch', syntax=syntaxes.INTEGER()) +integerOrderingMatch = GenericMatchingRule('2.5.13.15', name='integerOrderingMatch', syntax=syntaxes.INTEGER()) +# Optional and implementation-specific, we simply never match +keywordMatch = MatchingRule('2.5.13.33', name='keywordMatch', syntax=syntaxes.DirectoryString()) +numericStringMatch = StringMatchingRule('2.5.13.8', name='numericStringMatch', syntax=syntaxes.NumericString(), matching_type=rfc4518_stringprep.MatchingType.NUMERIC_STRING) +numericStringOrderingMatch = StringMatchingRule('2.5.13.9', name='numericStringOrderingMatch', syntax=syntaxes.NumericString(), matching_type=rfc4518_stringprep.MatchingType.NUMERIC_STRING) +numericStringSubstringsMatch = StringMatchingRule('2.5.13.10', name='numericStringSubstringsMatch', syntax=syntaxes.SubstringAssertion(), matching_type=rfc4518_stringprep.MatchingType.NUMERIC_STRING) +objectIdentifierFirstComponentMatch = FirstComponentMatchingRule('2.5.13.30', name='objectIdentifierFirstComponentMatch', syntax=syntaxes.OID(), attribute_name='get_first_component_oid') +objectIdentifierMatch = StringMatchingRule('2.5.13.0', name='objectIdentifierMatch', syntax=syntaxes.OID(), matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING) +octetStringMatch = GenericMatchingRule('2.5.13.17', name='octetStringMatch', syntax=syntaxes.OctetString()) +octetStringOrderingMatch = GenericMatchingRule('2.5.13.18', name='octetStringOrderingMatch', syntax=syntaxes.OctetString()) +telephoneNumberMatch = StringMatchingRule('2.5.13.20', name='telephoneNumberMatch', syntax=syntaxes.TelephoneNumber(), matching_type=rfc4518_stringprep.MatchingType.TELEPHONE_NUMBER) +telephoneNumberSubstringsMatch = StringMatchingRule('2.5.13.21', name='telephoneNumberSubstringsMatch', syntax=syntaxes.SubstringAssertion(), matching_type=rfc4518_stringprep.MatchingType.TELEPHONE_NUMBER) +uniqueMemberMatch = GenericMatchingRule('2.5.13.23', name='uniqueMemberMatch', syntax=syntaxes.NameAndOptionalUID()) +# Optional and implementation-specific, we simply never match +wordMatch = MatchingRule('2.5.13.32', name='wordMatch', syntax=syntaxes.DirectoryString()) + +ALL = ( + bitStringMatch, + booleanMatch, + caseExactIA5Match, + caseExactMatch, + caseExactOrderingMatch, + caseExactSubstringsMatch, + caseIgnoreIA5Match, + caseIgnoreIA5SubstringsMatch, + caseIgnoreListMatch, + caseIgnoreListSubstringsMatch, + caseIgnoreMatch, + caseIgnoreOrderingMatch, + caseIgnoreSubstringsMatch, + directoryStringFirstComponentMatch, + distinguishedNameMatch, + generalizedTimeMatch, + generalizedTimeOrderingMatch, + integerFirstComponentMatch, + integerMatch, + integerOrderingMatch, + #keywordMatch, + numericStringMatch, + numericStringOrderingMatch, + numericStringSubstringsMatch, + objectIdentifierFirstComponentMatch, + objectIdentifierMatch, + octetStringMatch, + octetStringOrderingMatch, + telephoneNumberMatch, + telephoneNumberSubstringsMatch, + uniqueMemberMatch, + #wordMatch, +) diff --git a/ldapserver/schema/rfc4517/syntaxes.py b/ldapserver/schema/rfc4517/syntaxes.py new file mode 100644 index 0000000..a25c4a0 --- /dev/null +++ b/ldapserver/schema/rfc4517/syntaxes.py @@ -0,0 +1,400 @@ +import re +import datetime + +from ..types import Syntax +from ... import dn + +# Base classes +class StringSyntax(Syntax): + @staticmethod + def encode(value): + return value.encode('utf8') + + @staticmethod + def decode(raw_value): + return raw_value.decode('utf8') + +class BytesSyntax(Syntax): + @staticmethod + def encode(value): + return value + + @staticmethod + def decode(raw_value): + return raw_value + +# Syntax definitions +class AttributeTypeDescription(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.3' + desc = 'Attribute Type Description' + +class BitString(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.6' + desc = 'Bit String' + +class Boolean(Syntax): + oid = '1.3.6.1.4.1.1466.115.121.1.7' + desc = 'Boolean' + + @staticmethod + def encode(value): + return b'TRUE' if value else b'FALSE' + + @staticmethod + def decode(raw_value): + if raw_value == b'TRUE': + return True + elif raw_value == b'FALSE': + return False + else: + return None + +class CountryString(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.11' + desc = 'Country String' + +class DeliveryMethod(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.14' + desc = 'Delivery Method' + +class DirectoryString(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.15' + desc = 'Directory String' + +class DITContentRuleDescription(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.16' + desc = 'DIT Content Rule Description' + +class DITStructureRuleDescription(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.17' + desc = 'DIT Structure Rule Description' + +class DN(Syntax): + oid = '1.3.6.1.4.1.1466.115.121.1.12' + desc = 'DN' + + @staticmethod + def encode(value): + return str(value).encode('utf8') + + @staticmethod + def decode(raw_value): + try: + return dn.DN.from_str(raw_value.decode('utf8')) + except (UnicodeDecodeError, TypeError, ValueError): + return None + +class EnhancedGuide(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.21' + desc = 'Enhanced Guide' + +class FacsimileTelephoneNumber(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.22' + desc = 'Facsimile Telephone Number' + +class Fax(BytesSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.23' + desc = 'Fax' + +class GeneralizedTime(Syntax): + oid = '1.3.6.1.4.1.1466.115.121.1.24' + desc = 'Generalized Time' + + @staticmethod + def encode(value): + str_value = value.strftime('%Y%m%d%H%M%S.%f') + if value.tzinfo == datetime.timezone.utc: + str_value += 'Z' + elif value.tzinfo is not None: + delta_seconds = value.tzinfo.utcoffset(value).total_seconds() + if delta_seconds < 0: + str_value += '-' + delta_seconds = -delta_seconds + else: + str_value += '+' + hour = delta_seconds // 3600 + minute = (delta_seconds % 3600) // 60 + str_value += '%02d%02d'%(hour, minute) + return str_value.encode('ascii') + + @staticmethod + def decode(raw_value): + try: + raw_value = raw_value.decode('utf8') + except UnicodeDecodeError: + return None + match = re.fullmatch(r'([0-9]{10})(|[0-9]{2}|[0-9]{4})(|[,.][0-9]+)(Z|[+-][0-9]{2}|[+-][0-9]{4})', raw_value) + if match is None: + return None + main, minute_second, fraction, timezone = match.groups() + fraction = float('0.' + (fraction[1:] or '0')) + result = datetime.datetime.strptime(main, '%Y%m%d%H') + if not minute_second: + result += datetime.timedelta(hours=fraction) + if len(minute_second) == 2: + result += datetime.timedelta(minutes=int(minute_second)+fraction) + elif len(minute_second) == 4: + minute = minute_second[:2] + second = minute_second[2:4] + result += datetime.timedelta(minutes=int(minute), seconds=int(second)+fraction) + if timezone == 'Z': + result = result.replace(tzinfo=datetime.timezone.utc) + elif timezone: + sign, hour, minute = timezone[0], timezone[1:3], (timezone[3:5] or '00') + delta = datetime.timedelta(hours=int(hour), minutes=int(minute)) + if sign == '+': + result = result.replace(tzinfo=datetime.timezone(delta)) + else: + result = result.replace(tzinfo=datetime.timezone(-delta)) + return result + +class Guide(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.25' + desc = 'Guide' + +class IA5String(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.26' + desc = 'IA5 String' + +class INTEGER(Syntax): + oid = '1.3.6.1.4.1.1466.115.121.1.27' + desc = 'INTEGER' + + @staticmethod + def encode(value): + return str(value).encode('utf8') + + @staticmethod + def decode(raw_value): + if not raw_value or not raw_value.split(b'-', 1)[-1].isdigit(): + return None + return int(raw_value.decode('utf8')) + +class JPEG(BytesSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.28' + desc = 'JPEG' + +class LDAPSyntaxDescription(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.54' + desc = 'LDAP Syntax Description' + +class MatchingRuleDescription(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.30' + desc = 'Matching Rule Description' + +class MatchingRuleUseDescription(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.31' + desc = 'Matching Rule Use Description' + +class NameAndOptionalUID(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.34' + desc = 'Name And Optional UID' + + @staticmethod + def encode(value): + return DN.encode(value) + + @staticmethod + def decode(raw_value): + escaped = False + dn_part = raw_value + bitstr_part = b'' # pylint: disable=unused-variable + for index, byte in enumerate(raw_value): + byte = bytes((byte,)) + if escaped: + escaped = False + elif byte == b'\\': + escaped = True + elif byte == b'#': + dn_part = raw_value[:index] + bitstr_part = raw_value[index+1:] + break + # We need to find a good representation of this type, maybe a subclass + # of dn.DN that carries the bitstring part as an attribute. + #if bitstr_part: + # return DN.decode(dn_part), BitString.decode(bitstr_part) + return DN.decode(dn_part) + +class NameFormDescription(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.35' + desc = 'Name Form Description' + +class NumericString(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.36' + desc = 'Numeric String' + +class ObjectClassDescription(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.37' + desc = 'Object Class Description' + +class OctetString(BytesSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.40' + desc = 'Octet String' + +class OID(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.38' + desc = 'OID' + +class OtherMailbox(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.39' + desc = 'Other Mailbox' + +class PostalAddress(Syntax): + # 3.3.28. Postal Address + # + # A value of the Postal Address syntax is a sequence of strings of one + # or more arbitrary UCS characters, which form an address in a physical + # mail system. + # + # The LDAP-specific encoding of a value of this syntax is defined by + # the following ABNF: + # + # + # PostalAddress = line *( DOLLAR line ) + # line = 1*line-char + # line-char = %x00-23 + # / (%x5C "24") ; escaped "$" + # / %x25-5B + # / (%x5C "5C") ; escaped "\" + # / %x5D-7F + # / UTFMB + # + # Each character string (i.e., <line>) of a postal address value is + # encoded as a UTF-8 [RFC3629] string, except that "\" and "$" + # characters, if they occur in the string, are escaped by a "\" + # character followed by the two hexadecimal digit code for the + # character. The <DOLLAR> and <UTFMB> rules are defined in [RFC4512]. + # + # Many servers limit the postal address to no more than six lines of no + # more than thirty characters each. + # + # Example: + # 1234 Main St.$Anytown, CA 12345$USA + # \241,000,000 Sweepstakes$PO Box 1000000$Anytown, CA 12345$USA + # + # The LDAP definition for the Postal Address syntax is: + # + # ( 1.3.6.1.4.1.1466.115.121.1.41 DESC 'Postal Address' ) + # + # This syntax corresponds to the PostalAddress ASN.1 type from [X.520]; + # that is + # + # PostalAddress ::= SEQUENCE SIZE(1..ub-postal-line) OF + # DirectoryString { ub-postal-string } + # + # The values of ub-postal-line and ub-postal-string (both integers) are + # implementation defined. Non-normative definitions appear in [X.520]. + + oid = '1.3.6.1.4.1.1466.115.121.1.41' + desc = 'Postal Address' + + # Native values are lists of str + @staticmethod + def encode(value): + return '$'.join([line.replace('\\', '\\5C').replace('$', '\\24') for line in value]).encode('utf8') + + @staticmethod + def decode(raw_value): + return [line.replace('\\24', '$').replace('\\5C', '\\') for line in raw_value.decode('utf8').split('$')] + +class PrintableString(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.44' + desc = 'Printable String' + +class SubstringAssertion(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.58' + desc = 'Substring Assertion' + +class TelephoneNumber(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.50' + desc = 'Telephone Number' + +class TeletexTerminalIdentifier(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.51' + desc = 'Teletex Terminal Identifier' + +class TelexNumber(StringSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.52' + desc = 'Telex Number' + +class UTCTime(Syntax): + oid = '1.3.6.1.4.1.1466.115.121.1.53' + desc = 'UTC Time' + + @staticmethod + def encode(value): + str_value = value.strftime('%y%m%d%H%M%S') + if value.tzinfo == datetime.timezone.utc: + str_value += 'Z' + elif value.tzinfo is not None: + delta_seconds = value.tzinfo.utcoffset(value).total_seconds() + if delta_seconds < 0: + str_value += '-' + delta_seconds = -delta_seconds + else: + str_value += '+' + hour = delta_seconds // 3600 + minute = (delta_seconds % 3600) // 60 + str_value += '%02d%02d'%(hour, minute) + return str_value.encode('ascii') + + @staticmethod + def decode(raw_value): + try: + raw_value = raw_value.decode('utf8') + except UnicodeDecodeError: + return None + match = re.fullmatch(r'([0-9]{10})(|[0-9]{2})(|Z|[+-][0-9]{4})', raw_value) + if match is None: + return None + main, seconds, timezone = match.groups() + result = datetime.datetime.strptime(main, '%y%m%d%H%M') + if seconds: + result = result.replace(second=int(seconds)) + if timezone == 'Z': + result = result.replace(tzinfo=datetime.timezone.utc) + elif timezone: + sign, hour, minute = timezone[0], timezone[1:3], timezone[3:5] + delta = datetime.timedelta(hours=int(hour), minutes=int(minute)) + if sign == '+': + result = result.replace(tzinfo=datetime.timezone(delta)) + else: + result = result.replace(tzinfo=datetime.timezone(-delta)) + return result + +ALL = ( + AttributeTypeDescription, + BitString, + Boolean, + CountryString, + DeliveryMethod, + DirectoryString, + DITContentRuleDescription, + DITStructureRuleDescription, + DN, + EnhancedGuide, + FacsimileTelephoneNumber, + Fax, + GeneralizedTime, + Guide, + IA5String, + INTEGER, + JPEG, + LDAPSyntaxDescription, + MatchingRuleDescription, + MatchingRuleUseDescription, + NameAndOptionalUID, + NameFormDescription, + NumericString, + ObjectClassDescription, + OctetString, + OID, + OtherMailbox, + PostalAddress, + PrintableString, + SubstringAssertion, + TelephoneNumber, + TeletexTerminalIdentifier, + TelexNumber, + UTCTime, +) diff --git a/ldapserver/schema/rfc4519/__init__.py b/ldapserver/schema/rfc4519/__init__.py new file mode 100644 index 0000000..1727734 --- /dev/null +++ b/ldapserver/schema/rfc4519/__init__.py @@ -0,0 +1 @@ +from . import syntaxes, matching_rules, attribute_types, object_classes diff --git a/ldapserver/schema/rfc4519/attribute_types.py b/ldapserver/schema/rfc4519/attribute_types.py new file mode 100644 index 0000000..d4e4745 --- /dev/null +++ b/ldapserver/schema/rfc4519/attribute_types.py @@ -0,0 +1,96 @@ + +# pylint: disable=wildcard-import,unused-wildcard-import + +from ..types import AttributeType +from ..rfc4512.attribute_types import * +from . import syntaxes, matching_rules + +name = AttributeType('2.5.4.41', name='name', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString()) # Defined first, so sup=name works +businessCategory = AttributeType('2.5.4.15', name='businessCategory', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString()) +c = AttributeType('2.5.4.6', name='c', sup=name, syntax=syntaxes.CountryString(), single_value=True) +cn = AttributeType('2.5.4.3', name='cn', sup=name) +dc = AttributeType('0.9.2342.19200300.100.1.25', name='dc', equality=matching_rules.caseIgnoreIA5Match, substr=matching_rules.caseIgnoreIA5SubstringsMatch, syntax=syntaxes.IA5String(), single_value=True) +description = AttributeType('2.5.4.13', name='description', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString()) +destinationIndicator = AttributeType('2.5.4.27', name='destinationIndicator', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.PrintableString()) +distinguishedName = AttributeType('2.5.4.49', name='distinguishedName', equality=matching_rules.distinguishedNameMatch, syntax=syntaxes.DN()) +dnQualifier = AttributeType('2.5.4.46', name='dnQualifier', equality=matching_rules.caseIgnoreMatch, ordering=matching_rules.caseIgnoreOrderingMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.PrintableString()) +enhancedSearchGuide = AttributeType('2.5.4.47', name='enhancedSearchGuide', syntax=syntaxes.EnhancedGuide()) +facsimileTelephoneNumber = AttributeType('2.5.4.23', name='facsimileTelephoneNumber', syntax=syntaxes.FacsimileTelephoneNumber()) +generationQualifier = AttributeType('2.5.4.44', name='generationQualifier', sup=name) +givenName = AttributeType('2.5.4.42', name='givenName', sup=name) +houseIdentifier = AttributeType('2.5.4.51', name='houseIdentifier', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString()) +initials = AttributeType('2.5.4.43', name='initials', sup=name) +internationalISDNNumber = AttributeType('2.5.4.25', name='internationalISDNNumber', equality=matching_rules.numericStringMatch, substr=matching_rules.numericStringSubstringsMatch, syntax=syntaxes.NumericString()) +l = AttributeType('2.5.4.7', name='l', sup=name) +member = AttributeType('2.5.4.31', name='member', sup=distinguishedName) +o = AttributeType('2.5.4.10', name='o', sup=name) +ou = AttributeType('2.5.4.11', name='ou', sup=name) +owner = AttributeType('2.5.4.32', name='owner', sup=distinguishedName) +physicalDeliveryOfficeName = AttributeType('2.5.4.19', name='physicalDeliveryOfficeName', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString()) +postalAddress = AttributeType('2.5.4.16', name='postalAddress', equality=matching_rules.caseIgnoreListMatch, substr=matching_rules.caseIgnoreListSubstringsMatch, syntax=syntaxes.PostalAddress()) +postalCode = AttributeType('2.5.4.17', name='postalCode', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString()) +postOfficeBox = AttributeType('2.5.4.18', name='postOfficeBox', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString()) +preferredDeliveryMethod = AttributeType('2.5.4.28', name='preferredDeliveryMethod', syntax=syntaxes.DeliveryMethod(), single_value=True) +registeredAddress = AttributeType('2.5.4.26', name='registeredAddress', sup=postalAddress, syntax=syntaxes.PostalAddress()) +roleOccupant = AttributeType('2.5.4.33', name='roleOccupant', sup=distinguishedName) +searchGuide = AttributeType('2.5.4.14', name='searchGuide', syntax=syntaxes.Guide()) +seeAlso = AttributeType('2.5.4.34', name='seeAlso', sup=distinguishedName) +serialNumber = AttributeType('2.5.4.5', name='serialNumber', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.PrintableString()) +sn = AttributeType('2.5.4.4', name='sn', sup=name) +st = AttributeType('2.5.4.8', name='st', sup=name) +street = AttributeType('2.5.4.9', name='street', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString()) +telephoneNumber = AttributeType('2.5.4.20', name='telephoneNumber', equality=matching_rules.telephoneNumberMatch, substr=matching_rules.telephoneNumberSubstringsMatch, syntax=syntaxes.TelephoneNumber()) +teletexTerminalIdentifier = AttributeType('2.5.4.22', name='teletexTerminalIdentifier', syntax=syntaxes.TeletexTerminalIdentifier()) +telexNumber = AttributeType('2.5.4.21', name='telexNumber', syntax=syntaxes.TelexNumber()) +title = AttributeType('2.5.4.12', name='title', sup=name) +uid = AttributeType('0.9.2342.19200300.100.1.1', name='uid', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString()) +uniqueMember = AttributeType('2.5.4.50', name='uniqueMember', equality=matching_rules.uniqueMemberMatch, syntax=syntaxes.NameAndOptionalUID()) +userPassword = AttributeType('2.5.4.35', name='userPassword', equality=matching_rules.octetStringMatch, syntax=syntaxes.OctetString()) +x121Address = AttributeType('2.5.4.24', name='x121Address', equality=matching_rules.numericStringMatch, substr=matching_rules.numericStringSubstringsMatch, syntax=syntaxes.NumericString()) +x500UniqueIdentifier = AttributeType('2.5.4.45', name='x500UniqueIdentifier', equality=matching_rules.bitStringMatch, syntax=syntaxes.BitString()) + +ALL = ALL + ( + name, + businessCategory, + c, + cn, + dc, + description, + destinationIndicator, + distinguishedName, + dnQualifier, + enhancedSearchGuide, + facsimileTelephoneNumber, + generationQualifier, + givenName, + houseIdentifier, + initials, + internationalISDNNumber, + l, + member, + o, + ou, + owner, + physicalDeliveryOfficeName, + postalAddress, + postalCode, + postOfficeBox, + preferredDeliveryMethod, + registeredAddress, + roleOccupant, + searchGuide, + seeAlso, + serialNumber, + sn, + st, + street, + telephoneNumber, + teletexTerminalIdentifier, + telexNumber, + title, + uid, + uniqueMember, + userPassword, + x121Address, + x500UniqueIdentifier, +) diff --git a/ldapserver/schema/rfc4519/matching_rules.py b/ldapserver/schema/rfc4519/matching_rules.py new file mode 100644 index 0000000..1ef1df9 --- /dev/null +++ b/ldapserver/schema/rfc4519/matching_rules.py @@ -0,0 +1,4 @@ + +# pylint: disable=wildcard-import,unused-wildcard-import + +from ..rfc4512.matching_rules import * diff --git a/ldapserver/schema/rfc4519/object_classes.py b/ldapserver/schema/rfc4519/object_classes.py new file mode 100644 index 0000000..412bc59 --- /dev/null +++ b/ldapserver/schema/rfc4519/object_classes.py @@ -0,0 +1,38 @@ + +# pylint: disable=wildcard-import,unused-wildcard-import + +from ..types import ObjectClass, ObjectClassKind +from ..rfc4512.object_classes import * +from . import attribute_types + +person = ObjectClass('2.5.6.6', name='person', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.sn, attribute_types.cn], may=[attribute_types.userPassword, attribute_types.telephoneNumber, attribute_types.seeAlso, attribute_types.description]) # defined first, so sup=person works +applicationProcess = ObjectClass('2.5.6.11', name='applicationProcess', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.cn], may=[attribute_types.seeAlso, attribute_types.ou, attribute_types.l, attribute_types.description]) +country = ObjectClass('2.5.6.2', name='country', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.c], may=[attribute_types.searchGuide, attribute_types.description]) +dcObject = ObjectClass('1.3.6.1.4.1.1466.344', name='dcObject', sup=top, kind=ObjectClassKind.AUXILIARY, must=[attribute_types.dc]) +device = ObjectClass('2.5.6.14', name='device', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.cn], may=[attribute_types.serialNumber, attribute_types.seeAlso, attribute_types.owner, attribute_types.ou, attribute_types.o, attribute_types.l, attribute_types.description]) +groupOfNames = ObjectClass('2.5.6.9', name='groupOfNames', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.member, attribute_types.cn], may=[attribute_types.businessCategory, attribute_types.seeAlso, attribute_types.owner, attribute_types.ou, attribute_types.o, attribute_types.description]) +groupOfUniqueNames = ObjectClass('2.5.6.17', name='groupOfUniqueNames', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.uniqueMember, attribute_types.cn], may=[attribute_types.businessCategory, attribute_types.seeAlso, attribute_types.owner, attribute_types.ou, attribute_types.o, attribute_types.description]) +locality = ObjectClass('2.5.6.3', name='locality', sup=top, kind=ObjectClassKind.STRUCTURAL, may=[attribute_types.street, attribute_types.seeAlso, attribute_types.searchGuide, attribute_types.st, attribute_types.l, attribute_types.description]) +organization = ObjectClass('2.5.6.4', name='organization', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.o], may=[attribute_types.userPassword, attribute_types.searchGuide, attribute_types.seeAlso, attribute_types.businessCategory, attribute_types.x121Address, attribute_types.registeredAddress, attribute_types.destinationIndicator, attribute_types.preferredDeliveryMethod, attribute_types.telexNumber, attribute_types.teletexTerminalIdentifier, attribute_types.telephoneNumber, attribute_types.internationalISDNNumber, attribute_types.facsimileTelephoneNumber, attribute_types.street, attribute_types.postOfficeBox, attribute_types.postalCode, attribute_types.postalAddress, attribute_types.physicalDeliveryOfficeName, attribute_types.st, attribute_types.l, attribute_types.description]) +organizationalPerson = ObjectClass('2.5.6.7', name='organizationalPerson', sup=person, kind=ObjectClassKind.STRUCTURAL, may=[attribute_types.title, attribute_types.x121Address, attribute_types.registeredAddress, attribute_types.destinationIndicator, attribute_types.preferredDeliveryMethod, attribute_types.telexNumber, attribute_types.teletexTerminalIdentifier, attribute_types.telephoneNumber, attribute_types.internationalISDNNumber, attribute_types.facsimileTelephoneNumber, attribute_types.street, attribute_types.postOfficeBox, attribute_types.postalCode, attribute_types.postalAddress, attribute_types.physicalDeliveryOfficeName, attribute_types.ou, attribute_types.st, attribute_types.l]) +organizationalRole = ObjectClass('2.5.6.8', name='organizationalRole', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.cn], may=[attribute_types.x121Address, attribute_types.registeredAddress, attribute_types.destinationIndicator, attribute_types.preferredDeliveryMethod, attribute_types.telexNumber, attribute_types.teletexTerminalIdentifier, attribute_types.telephoneNumber, attribute_types.internationalISDNNumber, attribute_types.facsimileTelephoneNumber, attribute_types.seeAlso, attribute_types.roleOccupant, attribute_types.preferredDeliveryMethod, attribute_types.street, attribute_types.postOfficeBox, attribute_types.postalCode, attribute_types.postalAddress, attribute_types.physicalDeliveryOfficeName, attribute_types.ou, attribute_types.st, attribute_types.l, attribute_types.description]) +organizationalUnit = ObjectClass('2.5.6.5', name='organizationalUnit', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.ou], may=[attribute_types.businessCategory, attribute_types.description, attribute_types.destinationIndicator, attribute_types.facsimileTelephoneNumber, attribute_types.internationalISDNNumber, attribute_types.l, attribute_types.physicalDeliveryOfficeName, attribute_types.postalAddress, attribute_types.postalCode, attribute_types.postOfficeBox, attribute_types.preferredDeliveryMethod, attribute_types.registeredAddress, attribute_types.searchGuide, attribute_types.seeAlso, attribute_types.st, attribute_types.street, attribute_types.telephoneNumber, attribute_types.teletexTerminalIdentifier, attribute_types.telexNumber, attribute_types.userPassword, attribute_types.x121Address]) +residentialPerson = ObjectClass('2.5.6.10', name='residentialPerson', sup=person, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.l], may=[attribute_types.businessCategory, attribute_types.x121Address, attribute_types.registeredAddress, attribute_types.destinationIndicator, attribute_types.preferredDeliveryMethod, attribute_types.telexNumber, attribute_types.teletexTerminalIdentifier, attribute_types.telephoneNumber, attribute_types.internationalISDNNumber, attribute_types.facsimileTelephoneNumber, attribute_types.preferredDeliveryMethod, attribute_types.street, attribute_types.postOfficeBox, attribute_types.postalCode, attribute_types.postalAddress, attribute_types.physicalDeliveryOfficeName, attribute_types.st, attribute_types.l]) +uidObject = ObjectClass('1.3.6.1.1.3.1', name='uidObject', sup=top, kind=ObjectClassKind.AUXILIARY, must=[attribute_types.uid]) + +ALL = ALL + ( + person, + applicationProcess, + country, + dcObject, + device, + groupOfNames, + groupOfUniqueNames, + locality, + organization, + organizationalPerson, + organizationalRole, + organizationalUnit, + residentialPerson, + uidObject, +) diff --git a/ldapserver/schema/rfc4519/syntaxes.py b/ldapserver/schema/rfc4519/syntaxes.py new file mode 100644 index 0000000..d6e0781 --- /dev/null +++ b/ldapserver/schema/rfc4519/syntaxes.py @@ -0,0 +1,4 @@ + +# pylint: disable=wildcard-import,unused-wildcard-import + +from ..rfc4512.syntaxes import * diff --git a/ldapserver/schema/rfc4523/__init__.py b/ldapserver/schema/rfc4523/__init__.py new file mode 100644 index 0000000..ecb7ec8 --- /dev/null +++ b/ldapserver/schema/rfc4523/__init__.py @@ -0,0 +1 @@ +from . import syntaxes, matching_rules, attribute_types diff --git a/ldapserver/schema/rfc4523/attribute_types.py b/ldapserver/schema/rfc4523/attribute_types.py new file mode 100644 index 0000000..f8585d5 --- /dev/null +++ b/ldapserver/schema/rfc4523/attribute_types.py @@ -0,0 +1,8 @@ +from ..types import AttributeType +from . import syntaxes, matching_rules + +userCertificate = AttributeType('2.5.4.36', name='userCertificate', desc='X.509 user certificate', equality=matching_rules.certificateExactMatch, syntax=syntaxes.X509Certificate()) + +ALL = ( + userCertificate, +) diff --git a/ldapserver/schema/rfc4523/matching_rules.py b/ldapserver/schema/rfc4523/matching_rules.py new file mode 100644 index 0000000..a556255 --- /dev/null +++ b/ldapserver/schema/rfc4523/matching_rules.py @@ -0,0 +1,8 @@ +from ..types import MatchingRule +from . import syntaxes + +certificateExactMatch = MatchingRule('2.5.13.34', name='certificateExactMatch', desc='X.509 Certificate Exact Match', syntax=syntaxes.X509CertificateExactAssertion()) + +ALL = ( + certificateExactMatch, +) diff --git a/ldapserver/schema/rfc4523/syntaxes.py b/ldapserver/schema/rfc4523/syntaxes.py new file mode 100644 index 0000000..851edc6 --- /dev/null +++ b/ldapserver/schema/rfc4523/syntaxes.py @@ -0,0 +1,14 @@ +from ..rfc4517.syntaxes import BytesSyntax + +class X509Certificate(BytesSyntax): + oid = '1.3.6.1.4.1.1466.115.121.1.8' + desc = 'X.509 Certificate' + +class X509CertificateExactAssertion(BytesSyntax): + oid = '1.3.6.1.1.15.1' + desc = 'X.509 Certificate Exact Assertion' + +ALL = ( + X509Certificate, + X509CertificateExactAssertion, +) diff --git a/ldapserver/schema/rfc4524/__init__.py b/ldapserver/schema/rfc4524/__init__.py new file mode 100644 index 0000000..1727734 --- /dev/null +++ b/ldapserver/schema/rfc4524/__init__.py @@ -0,0 +1 @@ +from . import syntaxes, matching_rules, attribute_types, object_classes diff --git a/ldapserver/schema/rfc4524/attribute_types.py b/ldapserver/schema/rfc4524/attribute_types.py new file mode 100644 index 0000000..bdd2c68 --- /dev/null +++ b/ldapserver/schema/rfc4524/attribute_types.py @@ -0,0 +1,60 @@ + +# pylint: disable=wildcard-import,unused-wildcard-import + +from ..types import AttributeType +from ..rfc4519.attribute_types import * +from . import syntaxes, matching_rules + +associatedDomain = AttributeType('0.9.2342.19200300.100.1.37', name='associatedDomain', equality=matching_rules.caseIgnoreIA5Match, substr=matching_rules.caseIgnoreIA5SubstringsMatch, syntax=syntaxes.IA5String()) +associatedName = AttributeType('0.9.2342.19200300.100.1.38', name='associatedName', equality=matching_rules.distinguishedNameMatch, syntax=syntaxes.DN()) +buildingName = AttributeType('0.9.2342.19200300.100.1.48', name='buildingName', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256)) +co = AttributeType('0.9.2342.19200300.100.1.43', name='co', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString()) +documentAuthor = AttributeType('0.9.2342.19200300.100.1.14', name='documentAuthor', equality=matching_rules.distinguishedNameMatch, syntax=syntaxes.DN()) +documentIdentifier = AttributeType('0.9.2342.19200300.100.1.11', name='documentIdentifier', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256)) +documentLocation = AttributeType('0.9.2342.19200300.100.1.15', name='documentLocation', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256)) +documentPublisher = AttributeType('0.9.2342.19200300.100.1.56', name='documentPublisher', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString()) +documentTitle = AttributeType('0.9.2342.19200300.100.1.12', name='documentTitle', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256)) +documentVersion = AttributeType('0.9.2342.19200300.100.1.13', name='documentVersion', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256)) +drink = AttributeType('0.9.2342.19200300.100.1.5', name='drink', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256)) +homePhone = AttributeType('0.9.2342.19200300.100.1.20', name='homePhone', equality=matching_rules.telephoneNumberMatch, substr=matching_rules.telephoneNumberSubstringsMatch, syntax=syntaxes.TelephoneNumber()) +homePostalAddress = AttributeType('0.9.2342.19200300.100.1.39', name='homePostalAddress', equality=matching_rules.caseIgnoreListMatch, substr=matching_rules.caseIgnoreListSubstringsMatch, syntax=syntaxes.PostalAddress()) +host = AttributeType('0.9.2342.19200300.100.1.9', name='host', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256)) +info = AttributeType('0.9.2342.19200300.100.1.4', name='info', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(2048)) +mail = AttributeType('0.9.2342.19200300.100.1.3', name='mail', equality=matching_rules.caseIgnoreIA5Match, substr=matching_rules.caseIgnoreIA5SubstringsMatch, syntax=syntaxes.IA5String(256)) +manager = AttributeType('0.9.2342.19200300.100.1.10', name='manager', equality=matching_rules.distinguishedNameMatch, syntax=syntaxes.DN()) +mobile = AttributeType('0.9.2342.19200300.100.1.41', name='mobile', equality=matching_rules.telephoneNumberMatch, substr=matching_rules.telephoneNumberSubstringsMatch, syntax=syntaxes.TelephoneNumber()) +organizationalStatus = AttributeType('0.9.2342.19200300.100.1.45', name='organizationalStatus', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256)) +pager = AttributeType('0.9.2342.19200300.100.1.42', name='pager', equality=matching_rules.telephoneNumberMatch, substr=matching_rules.telephoneNumberSubstringsMatch, syntax=syntaxes.TelephoneNumber()) +personalTitle = AttributeType('0.9.2342.19200300.100.1.40', name='personalTitle', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256)) +roomNumber = AttributeType('0.9.2342.19200300.100.1.6', name='roomNumber', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256)) +secretary = AttributeType('0.9.2342.19200300.100.1.21', name='secretary', equality=matching_rules.distinguishedNameMatch, syntax=syntaxes.DN()) +uniqueIdentifier = AttributeType('0.9.2342.19200300.100.1.44', name='uniqueIdentifier', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256)) +UserClass = AttributeType('0.9.2342.19200300.100.1.8', name='userClass', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256)) + +ALL = ALL + ( + associatedDomain, + associatedName, + buildingName, + co, + documentAuthor, + documentIdentifier, + documentLocation, + documentPublisher, + documentTitle, + documentVersion, + drink, + homePhone, + homePostalAddress, + host, + info, + mail, + manager, + mobile, + organizationalStatus, + pager, + personalTitle, + roomNumber, + secretary, + uniqueIdentifier, + UserClass, +) diff --git a/ldapserver/schema/rfc4524/matching_rules.py b/ldapserver/schema/rfc4524/matching_rules.py new file mode 100644 index 0000000..e3a7ec2 --- /dev/null +++ b/ldapserver/schema/rfc4524/matching_rules.py @@ -0,0 +1,4 @@ + +# pylint: disable=wildcard-import,unused-wildcard-import + +from ..rfc4519.matching_rules import * diff --git a/ldapserver/schema/rfc4524/object_classes.py b/ldapserver/schema/rfc4524/object_classes.py new file mode 100644 index 0000000..584a0cf --- /dev/null +++ b/ldapserver/schema/rfc4524/object_classes.py @@ -0,0 +1,27 @@ +# pylint: disable=wildcard-import,unused-wildcard-import + +from ..types import ObjectClass, ObjectClassKind +from ..rfc4519.object_classes import * +from . import attribute_types + +account = ObjectClass('0.9.2342.19200300.100.4.5', name='account', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.uid], may=[attribute_types.description, attribute_types.seeAlso, attribute_types.l, attribute_types.o, attribute_types.ou, attribute_types.host] ) +document = ObjectClass('0.9.2342.19200300.100.4.6', name='document', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.documentIdentifier], may=[attribute_types.cn, attribute_types.description, attribute_types.seeAlso, attribute_types.l, attribute_types.o, attribute_types.ou, attribute_types.documentTitle, attribute_types.documentVersion, attribute_types.documentAuthor, attribute_types.documentLocation, attribute_types.documentPublisher] ) +documentSeries = ObjectClass('0.9.2342.19200300.100.4.9', name='documentSeries', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.cn], may=[attribute_types.description, attribute_types.l, attribute_types.o, attribute_types.ou, attribute_types.seeAlso, attribute_types.telephoneNumber] ) +domain = ObjectClass('0.9.2342.19200300.100.4.13', name='domain', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.dc], may=[attribute_types.userPassword, attribute_types.searchGuide, attribute_types.seeAlso, attribute_types.businessCategory, attribute_types.x121Address, attribute_types.registeredAddress, attribute_types.destinationIndicator, attribute_types.preferredDeliveryMethod, attribute_types.telexNumber, attribute_types.teletexTerminalIdentifier, attribute_types.telephoneNumber, attribute_types.internationalISDNNumber, attribute_types.facsimileTelephoneNumber, attribute_types.street, attribute_types.postOfficeBox, attribute_types.postalCode, attribute_types.postalAddress, attribute_types.physicalDeliveryOfficeName, attribute_types.st, attribute_types.l, attribute_types.description, attribute_types.o, attribute_types.associatedName] ) +domainRelatedObject = ObjectClass('0.9.2342.19200300.100.4.17', name='domainRelatedObject', sup=top, kind=ObjectClassKind.AUXILIARY, must=[attribute_types.associatedDomain]) +friendlyCountry = ObjectClass('0.9.2342.19200300.100.4.18', name='friendlyCountry', sup=country, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.co]) +rFC822localPart = ObjectClass('0.9.2342.19200300.100.4.14', name='rFC822localPart', sup=domain, kind=ObjectClassKind.STRUCTURAL, may=[attribute_types.cn, attribute_types.description, attribute_types.destinationIndicator, attribute_types.facsimileTelephoneNumber, attribute_types.internationalISDNNumber, attribute_types.physicalDeliveryOfficeName, attribute_types.postalAddress, attribute_types.postalCode, attribute_types.postOfficeBox, attribute_types.preferredDeliveryMethod, attribute_types.registeredAddress, attribute_types.seeAlso, attribute_types.sn, attribute_types.street, attribute_types.telephoneNumber, attribute_types.teletexTerminalIdentifier, attribute_types.telexNumber, attribute_types.x121Address] ) +room = ObjectClass('0.9.2342.19200300.100.4.7', name='room', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.cn], may=[attribute_types.roomNumber, attribute_types.description, attribute_types.seeAlso, attribute_types.telephoneNumber] ) +simpleSecurityObject = ObjectClass('0.9.2342.19200300.100.4.19', name='simpleSecurityObject', sup=top, kind=ObjectClassKind.AUXILIARY, must=[attribute_types.userPassword]) + +ALL = ALL + ( + account, + document, + documentSeries, + domain, + domainRelatedObject, + friendlyCountry, + rFC822localPart, + room, + simpleSecurityObject, +) diff --git a/ldapserver/schema/rfc4524/syntaxes.py b/ldapserver/schema/rfc4524/syntaxes.py new file mode 100644 index 0000000..c26e869 --- /dev/null +++ b/ldapserver/schema/rfc4524/syntaxes.py @@ -0,0 +1,4 @@ + +# pylint: disable=wildcard-import,unused-wildcard-import + +from ..rfc4519.syntaxes import * diff --git a/ldapserver/schema/types.py b/ldapserver/schema/types.py new file mode 100644 index 0000000..718c578 --- /dev/null +++ b/ldapserver/schema/types.py @@ -0,0 +1,698 @@ +import enum + +from .. import ldap +from ..dn import DN + +__all__ = [ + 'FilterResult', + 'Syntax', + 'MatchingRule', + 'AttributeTypeUsage', + 'AttributeType', + 'ObjectClassKind', + 'ObjectClass', + 'Object', + 'RootDSE', + 'Subschema', + 'WILDCARD_VALUE', + 'ObjectTemplate', +] + +class FilterResult(enum.Enum): + TRUE = enum.auto() + FALSE = enum.auto() + UNDEFINED = enum.auto() + MAYBE_TRUE = enum.auto() # used by ObjectTemplate + +def escape(string): + result = '' + for char in string: + if char == '\'': + result += '\\27' + elif char == '\\': + result += '\\5C' + else: + result += char + return result + +class Syntax: + oid: str + desc: str + + def __init__(self, max_len=None): + self.max_len = max_len + if max_len is None: + self.ref = self.oid + else: + self.ref = self.oid + '{' + str(max_len) + '}' + + @classmethod + def get_first_component_oid(cls): + return cls.oid + + @classmethod + def encode_syntax_definition(cls): + return f"( {cls.oid} DESC '{escape(cls.desc)}' )" + + @staticmethod + def decode(raw_value): + '''Decode LDAP-specific encoding of a value to a native value + + :param raw_value: LDAP-specific encoding of the value + :type raw_value: bytes + + :returns: native value (depends on syntax), None if raw_value is invalid + :rtype: any or None''' + return None + + @staticmethod + def encode(value): + '''Encode native value to its LDAP-specific encoding + + :param value: native value (depends on syntax) + :type value: any + + :returns: LDAP-specific encoding of the value + :rtype: bytes''' + raise NotImplementedError() + +class MatchingRule: + def __init__(self, oid, name, syntax, **kwargs): + self.oid = oid + self.name = name + self.syntax = syntax + for key, value in kwargs.items(): + setattr(self, key, value) + + def encode_syntax_definition(self): + return f"( {self.oid} NAME '{escape(self.name)}' SYNTAX {self.syntax.ref} )" + + def __repr__(self): + return f'<ldapserver.schema.MatchingRule {self.encode_syntax_definition()}>' + + def match_equal(self, attribute_value, assertion_value): + return FilterResult.UNDEFINED + + def match_approx(self, attribute_value, assertion_value): + return self.match_equal(attribute_value, assertion_value) + + def match_less(self, attribute_value, assertion_value): + return FilterResult.UNDEFINED + + def match_greater_or_equal(self, attribute_value, assertion_value): + return FilterResult.UNDEFINED + + def match_substr(self, attribute_value, inital_substring, any_substrings, final_substring): + return FilterResult.UNDEFINED + +class AttributeTypeUsage(enum.Enum): + # pylint: disable=invalid-name + # user + userApplications = enum.auto() + # directory operational + directoryOperation = enum.auto() + # DSA-shared operational + distributedOperation = enum.auto() + # DSA-specific operational + dSAOperation = enum.auto() + +class AttributeType: + # 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, + single_value=None, collective=None, no_user_modification=None, + usage=None): + if sup is None and syntax is None: + raise ValueError('Either SUP or, syntax=syntax.must, be specified') + tokens = ['(', oid] + if name is not None: + tokens += ['NAME', "'"+escape(name)+"'"] # name is actually a list + if desc is not None: + tokens += ['DESC', "'"+escape(desc)+"'"] + if obsolete is not None: + tokens += ['OBSOLETE', obsolete] + if sup is not None: + tokens += ['SUP', sup.oid] + if equality is not None: + tokens += ['EQUALITY', equality.oid] + if ordering is not None: + tokens += ['ORDERING', ordering.oid] + if substr is not None: + tokens += ['SUBSTR', substr.oid] + if syntax is not None: + tokens += ['SYNTAX', syntax.ref] + if single_value is not None: + tokens += ['SINGLE-VALUE'] + if collective is not None: + tokens += ['COLLECTIVE'] + if no_user_modification is not None: + tokens += ['NO-USER-MODIFICATION'] + if usage is not None: + tokens += ['USAGE', usage.name] + tokens += [')'] + self.schema_encoding = ' '.join(tokens) + self.oid = oid + self.name = name + self.names = set() + if name is not None: + self.names.add(name) + self.obsolete = obsolete or False + self.sup = sup + if self.sup is not None: + self.names |= self.sup.names + self.equality = equality + if self.equality is None and self.sup is not None: + self.equality = self.sup.equality + self.ordering = ordering + if self.ordering is None and self.sup is not None: + self.ordering = self.sup.ordering + self.substr = substr + if self.substr is None and self.sup is not None: + self.substr = self.sup.substr + self.syntax = syntax + if self.syntax is None and self.sup is not None: + self.syntax = self.sup.syntax + self.single_value = single_value or False + self.collective = collective or False + self.no_user_modification = no_user_modification or False + self.usage = usage or AttributeTypeUsage.userApplications + + def get_first_component_oid(self): + return self.oid + + def __repr__(self): + return f'<ldapserver.schema.AttributeType {self.schema_encoding}>' + +class ObjectClassKind(enum.Enum): + ABSTRACT = enum.auto() + STRUCTURAL = enum.auto() + AUXILIARY = enum.auto() + +class ObjectClass: + # pylint: disable=too-many-arguments + def __init__(self, oid, name=None, desc=None, obsolete=None, sup=None, + kind=None, must=None, may=None): + tokens = ['(', oid] + if name is not None: + tokens += ['NAME', "'"+escape(name)+"'"] # name is actually a list + if desc is not None: + tokens += ['DESC', "'"+escape(desc)+"'"] + if obsolete is not None: + tokens += ['OBSOLETE', obsolete] + if sup is not None: + tokens += ['SUP', sup.name] + if kind is not None: + tokens += [kind.name] + if must and len(must) == 1: + tokens += ['MUST', must[0].name] + elif must and len(must) > 1: + tokens += ['MUST', '('] + for index, attr in enumerate(must): + if index > 0: + tokens += ['$'] + tokens += [attr.name] + tokens += [')'] + if may and len(may) == 1: + tokens += ['MAY', may[0].name] + elif may and len(may) > 1: + tokens += ['MAY', '('] + for index, attr in enumerate(may): + if index > 0: + tokens += ['$'] + tokens += [attr.name] + tokens += [')'] + tokens += [')'] + self.schema_encoding = ' '.join(tokens) + self.oid = oid + self.name = name + self.desc = desc + self.obsolete = obsolete or False + self.sup = sup + self.kind = kind or ObjectClassKind.STRUCTURAL + self.must = must or [] + self.may = may or [] + + def get_first_component_oid(self): + return self.oid + + def __repr__(self): + return f'<ldapserver.schema.ObjectClass {self.schema_encoding}>' + +def any_3value(iterable): + '''Extended three-valued logic equivalent of any builtin + + If all items are TRUE, return TRUE. Otherwise if any item is MAYBE_TRUE, + return MAYBE_TRUE. If neither TRUE nor MAYBE_TRUE are in items, but any + item is UNDEFINED, return UNDEFINED. Otherwise (all items are FALSE), + return FALSE.''' + result = FilterResult.FALSE + for item in iterable: + if item == FilterResult.TRUE: + return FilterResult.TRUE + elif item == FilterResult.MAYBE_TRUE: + result = FilterResult.MAYBE_TRUE + elif item == FilterResult.UNDEFINED and result == FilterResult.FALSE: + result = FilterResult.UNDEFINED + return result + +def all_3value(iterable): + '''Extended three-valued logic equivalent of all builtin + + If all items are TRUE, return TRUE. If any item is FALSE, return FALSE. + If no item is FALSE and any item is UNDEFINED, return UNDEFINED. + Otherwise (not item is FALSE or UNDEFINED and not all items are TRUE, + so at least one item is MAYBE_TRUE), return MAYBE_TRUE.''' + result = FilterResult.TRUE + for item in iterable: + if item == FilterResult.FALSE: + return FilterResult.FALSE + elif item == FilterResult.UNDEFINED: + result = FilterResult.UNDEFINED + elif item == FilterResult.MAYBE_TRUE and result == FilterResult.TRUE: + result = FilterResult.MAYBE_TRUE + return result + +class AttributeDict(dict): + def __init__(self, subschema, **attributes): + super().__init__() + self.subschema = subschema + for key, value in attributes.items(): + self[key] = value + + def __contains__(self, key): + return super().__contains__(self.subschema.lookup_attribute(key)) + + def __setitem__(self, key, value): + super().__setitem__(self.subschema.lookup_attribute(key, fail_if_not_found=True), value) + + def __getitem__(self, key): + key = self.subschema.lookup_attribute(key, fail_if_not_found=True) + if key not in self: + super().__setitem__(key, []) + result = super().__getitem__(key) + if callable(result): + return result() + return result + + def setdefault(self, key, default=None): + key = self.subschema.lookup_attribute(key, fail_if_not_found=True) + return super().setdefault(key, default) + + def get(self, key, default=None): + key = self.subschema.lookup_attribute(key, fail_if_not_found=True) + if key in self: + return self[key] + return default + + def get_all(self, key): + result = [] + for attr in self.subschema.lookup_attribute_list(key): + result += self[attr] + return result + + def match_present(self, key): + attribute_type = self.subschema.lookup_attribute(key) + if attribute_type is None: + return FilterResult.UNDEFINED + if self[attribute_type] != []: + return FilterResult.TRUE + else: + return FilterResult.FALSE + + def match_equal(self, key, assertion_value): + attribute_type = self.subschema.lookup_attribute(key) + if attribute_type is None or attribute_type.equality is None: + return FilterResult.UNDEFINED + assertion_value = attribute_type.equality.syntax.decode(assertion_value) + if assertion_value is None: + return FilterResult.UNDEFINED + return any_3value(map(lambda attrval: attribute_type.equality.match_equal(attrval, assertion_value), self.get_all(key))) + + def match_substr(self, key, inital_substring, any_substrings, final_substring): + attribute_type = self.subschema.lookup_attribute(key) + if attribute_type is None or attribute_type.equality is None or attribute_type.substr is None: + return FilterResult.UNDEFINED + if inital_substring: + inital_substring = attribute_type.equality.syntax.decode(inital_substring) + if inital_substring is None: + return FilterResult.UNDEFINED + any_substrings = [attribute_type.equality.syntax.decode(substring) for substring in any_substrings] + if None in any_substrings: + return FilterResult.UNDEFINED + if final_substring: + final_substring = attribute_type.equality.syntax.decode(final_substring) + if final_substring is None: + return FilterResult.UNDEFINED + return any_3value(map(lambda attrval: attribute_type.substr.match_substr(attrval, inital_substring, any_substrings, final_substring), self.get_all(key))) + + def match_approx(self, key, assertion_value): + attribute_type = self.subschema.lookup_attribute(key) + if attribute_type is None or attribute_type.equality is None: + return FilterResult.UNDEFINED + assertion_value = attribute_type.equality.syntax.decode(assertion_value) + if assertion_value is None: + return FilterResult.UNDEFINED + return any_3value(map(lambda attrval: attribute_type.equality.match_approx(attrval, assertion_value), self.get_all(key))) + + def match_greater_or_equal(self, key, assertion_value): + attribute_type = self.subschema.lookup_attribute(key) + if attribute_type is None or attribute_type.ordering is None: + return FilterResult.UNDEFINED + assertion_value = attribute_type.ordering.syntax.decode(assertion_value) + if assertion_value is None: + return FilterResult.UNDEFINED + return any_3value(map(lambda attrval: attribute_type.ordering.match_greater_or_equal(attrval, assertion_value), self.get_all(key))) + + def match_less(self, key, assertion_value): + attribute_type = self.subschema.lookup_attribute(key) + if attribute_type is None or attribute_type.ordering is None: + return FilterResult.UNDEFINED + assertion_value = attribute_type.ordering.syntax.decode(assertion_value) + if assertion_value is None: + return FilterResult.UNDEFINED + return any_3value(map(lambda attrval: attribute_type.ordering.match_less(attrval, assertion_value), self.get_all(key))) + + def match_less_or_equal(self, key, assertion_value): + return any_3value((self.match_equal(key, assertion_value), + self.match_less(key, assertion_value))) + + def match_filter(self, filter_obj): + if isinstance(filter_obj, ldap.FilterAnd): + return all_3value(map(self.match_filter, filter_obj.filters)) + elif isinstance(filter_obj, ldap.FilterOr): + return any_3value(map(self.match_filter, filter_obj.filters)) + elif isinstance(filter_obj, ldap.FilterNot): + subresult = self.match_filter(filter_obj.filter) + if subresult == FilterResult.TRUE: + return FilterResult.FALSE + elif subresult == FilterResult.FALSE: + return FilterResult.TRUE + else: + return subresult + elif isinstance(filter_obj, ldap.FilterPresent): + return self.match_present(filter_obj.attribute) + elif isinstance(filter_obj, ldap.FilterEqual): + return self.match_equal(filter_obj.attribute, filter_obj.value) + elif isinstance(filter_obj, ldap.FilterSubstrings): + return self.match_substr(filter_obj.attribute, filter_obj.initial_substring, + filter_obj.any_substrings, filter_obj.final_substring) + elif isinstance(filter_obj, ldap.FilterApproxMatch): + return self.match_approx(filter_obj.attribute, filter_obj.value) + elif isinstance(filter_obj, ldap.FilterGreaterOrEqual): + return self.match_greater_or_equal(filter_obj.attribute, filter_obj.value) + elif isinstance(filter_obj, ldap.FilterLessOrEqual): + return self.match_less_or_equal(filter_obj.attribute, filter_obj.value) + else: + return FilterResult.UNDEFINED + +class Object(AttributeDict): + def __init__(self, subschema, dn, **attributes): + super().__init__(subschema, **attributes) + self.dn = DN(dn) + self.setdefault('subschemaSubentry', [self.subschema.dn]) + + def match_dn(self, basedn, scope): + if scope == ldap.SearchScope.baseObject: + return self.dn == basedn + elif scope == ldap.SearchScope.singleLevel: + return self.dn.is_direct_child_of(basedn) + elif scope == ldap.SearchScope.wholeSubtree: + return self.dn.in_subtree_of(basedn) + else: + return False + + def match_search(self, base_obj, scope, filter_obj): + return self.match_dn(DN.from_str(base_obj), scope) and self.match_filter(filter_obj) == FilterResult.TRUE + + def get_search_result_entry(self, attributes=None, types_only=False): + selected_attributes = set() + for selector in attributes or ['*']: + if selector == '*': + selected_attributes |= self.subschema.user_attribute_types + elif selector == '1.1': + continue + else: + attribute = self.subschema.lookup_attribute(selector) + if attribute is not None: + selected_attributes.add(attribute) + partial_attributes = [] + for attribute in self: + if attribute not in selected_attributes: + continue + values = self[attribute] + if values != []: + if types_only: + values = [] + partial_attributes.append(ldap.PartialAttribute(attribute.name, [attribute.syntax.encode(value) for value in values])) + return ldap.SearchResultEntry(str(self.dn), partial_attributes) + +class RootDSE(Object): + def __init__(self, subschema, *args, **kwargs): + super().__init__(subschema, DN(), *args, **kwargs) + + def match_search(self, base_obj, scope, filter_obj): + return not base_obj and scope == ldap.SearchScope.baseObject and \ + isinstance(filter_obj, ldap.FilterPresent) and \ + filter_obj.attribute.lower() == 'objectclass' + +class WildcardValue: + pass + +WILDCARD_VALUE = WildcardValue() + +class ObjectTemplate(AttributeDict): + def __init__(self, subschema, parent_dn, rdn_attribute, **attributes): + super().__init__(subschema, **attributes) + self.parent_dn = parent_dn + self.rdn_attribute = rdn_attribute + + def match_present(self, key): + attribute_type = self.subschema.lookup_attribute(key) + if attribute_type is None: + return FilterResult.UNDEFINED + values = self[attribute_type] + if values == []: + return FilterResult.FALSE + elif WILDCARD_VALUE in values: + return FilterResult.MAYBE_TRUE + else: + return FilterResult.TRUE + + def match_equal(self, key, assertion_value): + attribute_type = self.subschema.lookup_attribute(key) + if attribute_type is None or attribute_type.equality is None: + return FilterResult.UNDEFINED + assertion_value = attribute_type.equality.syntax.decode(assertion_value) + if assertion_value is None: + return FilterResult.UNDEFINED + values = self.get_all(key) + if WILDCARD_VALUE in values: + return FilterResult.MAYBE_TRUE + return any_3value(map(lambda attrval: attribute_type.equality.match_equal(attrval, assertion_value), values)) + + def match_substr(self, key, inital_substring, any_substrings, final_substring): + attribute_type = self.subschema.lookup_attribute(key) + if attribute_type is None or attribute_type.equality is None or attribute_type.substr is None: + return FilterResult.UNDEFINED + if inital_substring: + inital_substring = attribute_type.equality.syntax.decode(inital_substring) + if inital_substring is None: + return FilterResult.UNDEFINED + any_substrings = [attribute_type.equality.syntax.decode(substring) for substring in any_substrings] + if None in any_substrings: + return FilterResult.UNDEFINED + if final_substring: + final_substring = attribute_type.equality.syntax.decode(final_substring) + if final_substring is None: + return FilterResult.UNDEFINED + values = self.get_all(key) + if WILDCARD_VALUE in values: + return FilterResult.MAYBE_TRUE + return any_3value(map(lambda attrval: attribute_type.substr.match_substr(attrval, inital_substring, any_substrings, final_substring), values)) + + def match_approx(self, key, assertion_value): + attribute_type = self.subschema.lookup_attribute(key) + if attribute_type is None or attribute_type.equality is None: + return FilterResult.UNDEFINED + assertion_value = attribute_type.equality.syntax.decode(assertion_value) + if assertion_value is None: + return FilterResult.UNDEFINED + values = self.get_all(key) + if WILDCARD_VALUE in values: + return FilterResult.MAYBE_TRUE + return any_3value(map(lambda attrval: attribute_type.equality.match_approx(attrval, assertion_value), values)) + + def match_greater_or_equal(self, key, assertion_value): + attribute_type = self.subschema.lookup_attribute(key) + if attribute_type is None or attribute_type.ordering is None: + return FilterResult.UNDEFINED + assertion_value = attribute_type.ordering.syntax.decode(assertion_value) + if assertion_value is None: + return FilterResult.UNDEFINED + values = self.get_all(key) + if WILDCARD_VALUE in values: + return FilterResult.MAYBE_TRUE + return any_3value(map(lambda attrval: attribute_type.ordering.match_greater_or_equal(attrval, assertion_value), values)) + + def match_less(self, key, assertion_value): + attribute_type = self.subschema.lookup_attribute(key) + if attribute_type is None or attribute_type.ordering is None: + return FilterResult.UNDEFINED + assertion_value = attribute_type.ordering.syntax.decode(assertion_value) + if assertion_value is None: + return FilterResult.UNDEFINED + values = self.get_all(key) + if WILDCARD_VALUE in values: + return FilterResult.MAYBE_TRUE + return any_3value(map(lambda attrval: attribute_type.ordering.match_less(attrval, assertion_value), values)) + + def __extract_dn_constraints(self, basedn, scope): + if scope == ldap.SearchScope.baseObject: + if basedn[1:] != self.parent_dn or basedn.object_attribute != self.rdn_attribute: + return False, AttributeDict(self.subschema) + return True, AttributeDict(self.subschema, **{self.rdn_attribute: [basedn.object_value]}) + elif scope == ldap.SearchScope.singleLevel: + return basedn == self.parent_dn, AttributeDict(self.subschema) + elif scope == ldap.SearchScope.wholeSubtree: + if self.parent_dn.in_subtree_of(basedn): + return True, AttributeDict(self.subschema) + if basedn[1:] != self.parent_dn or basedn.object_attribute != self.rdn_attribute: + return False, AttributeDict(self.subschema) + return True, AttributeDict(self.subschema, **{self.rdn_attribute: [basedn.object_value]}) + else: + return False, AttributeDict(self.subschema) + + def match_dn(self, basedn, scope): + '''Return whether objects from this template might match the provided parameters''' + return self.__extract_dn_constraints(basedn, scope)[0] + + def extract_dn_constraints(self, basedn, scope): + return self.__extract_dn_constraints(basedn, scope)[1] + + def extract_filter_constraints(self, filter_obj): + if isinstance(filter_obj, ldap.FilterEqual): + attribute_type = self.subschema.lookup_attribute(filter_obj.attribute) + if attribute_type is None or attribute_type.equality is None: + return AttributeDict(self.subschema) + assertion_value = attribute_type.equality.syntax.decode(filter_obj.value) + if assertion_value is None: + return AttributeDict(self.subschema) + return AttributeDict(self.subschema, **{filter_obj.attribute: [assertion_value]}) + if isinstance(filter_obj, ldap.FilterAnd): + result = AttributeDict(self.subschema) + for subfilter in filter_obj.filters: + for name, values in self.extract_filter_constraints(subfilter).items(): + result[name] += values + return result + return AttributeDict(self.subschema) + + def match_search(self, base_obj, scope, filter_obj): + '''Return whether objects based on this template might match the search parameters''' + return self.match_dn(DN.from_str(base_obj), scope) and self.match_filter(filter_obj) in (FilterResult.TRUE, FilterResult.MAYBE_TRUE) + + def extract_search_constraints(self, base_obj, scope, filter_obj): + constraints = self.extract_filter_constraints(filter_obj) + for key, values in self.extract_dn_constraints(DN.from_str(base_obj), scope).items(): + constraints[key] += values + return constraints + + def create_object(self, rdn_value, **attributes): + obj = Object(self.subschema, DN(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') + obj[key] = values + for attribute_type, values in self.items(): + if WILDCARD_VALUE not in values: + obj[attribute_type] = values + return obj + +class Subschema(Object): + def __init__(self, dn, object_classes=None, attribute_types=None, matching_rules=None, syntaxes=None): + # Setup schema data before calling super().__init__(), because we are our own schema + attribute_types = list(attribute_types or []) + matching_rules = list(matching_rules or []) + syntaxes = list(syntaxes or []) + self.object_classes = {} + for objectclass in object_classes or []: + self.object_classes[objectclass.oid] = objectclass + attribute_types += objectclass.must + objectclass.may + self.attribute_types = {} + self.attribute_types_by_name = {} + self.attribute_types_by_unique_name = {} + self.user_attribute_types = set() + for attribute_type in attribute_types: + self.attribute_types[attribute_type.oid] = attribute_type + for name in attribute_type.names: + name = name.lower() + self.attribute_types_by_name[name] = \ + self.attribute_types_by_name.get(name, set()) | {attribute_type} + self.attribute_types_by_unique_name[attribute_type.name.lower()] = attribute_type + if attribute_type.usage == AttributeTypeUsage.userApplications: + self.user_attribute_types.add(attribute_type) + if attribute_type.equality is not None: + matching_rules += [attribute_type.equality] + if attribute_type.ordering is not None: + matching_rules += [attribute_type.ordering] + if attribute_type.substr is not None: + matching_rules += [attribute_type.substr] + syntaxes += [type(attribute_type.syntax)] + self.matching_rules = {} + for matching_rule in matching_rules: + self.matching_rules[matching_rule.oid] = matching_rule + syntaxes += [type(matching_rule.syntax)] + self.syntaxes = {} + for syntax in syntaxes: + self.syntaxes[syntax.oid] = syntax + + super().__init__(subschema=self, dn=dn) + # pylint: disable=invalid-name + self.AttributeDict = lambda **attributes: AttributeDict(self, **attributes) + self.Object = lambda dn, **attributes: Object(self, dn, **attributes) + self.RootDSE = lambda **attributes: RootDSE(self, **attributes) + self.ObjectTemplate = lambda *args, **kwargs: ObjectTemplate(self, *args, **kwargs) + self['objectClass'] = [objectclass.schema_encoding for objectclass in self.object_classes.values()] + self['ldapSyntaxes'] = [syntax.encode_syntax_definition() for syntax in self.syntaxes.values()] + self['matchingRules'] = [matching_rule.encode_syntax_definition() for matching_rule in self.matching_rules.values()] + self['attributeTypes'] = [attribute_type.schema_encoding for attribute_type in self.attribute_types.values()] + + def extend(self, *subschemas, dn=None, object_classes=None, attribute_types=None, matching_rules=None, syntaxes=None): + if dn is None: + dn = self.dn + object_classes = list(self.object_classes.values()) + list(object_classes or []) + attribute_types = list(self.attribute_types.values()) + list(attribute_types or []) + matching_rules = list(self.matching_rules.values()) + list(matching_rules or []) + syntaxes = list(self.syntaxes.values()) + list(syntaxes or []) + for subschema in subschemas: + object_classes += list(subschema.object_classes.values()) + attribute_types += list(subschema.attribute_types.values()) + matching_rules += list(subschema.matching_rules.values()) + syntaxes += list(subschema.syntaxes.values()) + return Subschema(dn, object_classes, attribute_types, matching_rules, syntaxes) + + def lookup_attribute(self, oid_or_name, fail_if_not_found=False): + if isinstance(oid_or_name, AttributeType): + if self.attribute_types.get(oid_or_name.oid) != oid_or_name: + raise Exception() + return oid_or_name + if oid_or_name in self.attribute_types: + return self.attribute_types[oid_or_name] + result = self.attribute_types_by_unique_name.get(oid_or_name.lower()) + if result is None and fail_if_not_found: + raise Exception(f'Attribute "{oid_or_name}" not in schema') + return result + + def lookup_attribute_list(self, oid_or_name): + if oid_or_name in self.attribute_types_by_name: + return list(self.attribute_types_by_name[oid_or_name]) + result = self.lookup_attribute(oid_or_name) + if result is None: + return [] + return [result] + + def match_search(self, base_obj, scope, filter_obj): + return DN.from_str(base_obj) == self.dn and \ + scope == ldap.SearchScope.baseObject and \ + isinstance(filter_obj, ldap.FilterEqual) and \ + filter_obj.attribute.lower() == 'objectclass' and \ + filter_obj.value.lower() == b'subschema' diff --git a/ldapserver/server.py b/ldapserver/server.py index 4b6d4bd..76f0437 100644 --- a/ldapserver/server.py +++ b/ldapserver/server.py @@ -3,7 +3,7 @@ import ssl import socketserver import typing -from . import asn1, exceptions, ldap, schema, directory +from . import asn1, exceptions, ldap, schema def reject_critical_controls(controls=None): for control in controls or []: @@ -116,36 +116,38 @@ class BaseLDAPRequestHandler(socketserver.BaseRequestHandler): reject_critical_controls(controls) raise exceptions.LDAPProtocolError() -class SimpleLDAPRequestHandler(BaseLDAPRequestHandler): +class LDAPRequestHandler(BaseLDAPRequestHandler): ''' .. py:attribute:: rootdse - Special single-object :any:`directory.BaseDirectory` that contains information + Special :any:`LDAPObject` that contains information about the server, such as supported extentions and SASL authentication mechansims. Attributes can be accessed in a dict-like fashion. ''' - subschema = schema.CORE_SUBSCHEMA + subschema = schema.RFC4519_SUBSCHEMA ''' .. py:attribute:: subschema - Special single-object :any:`directory.BaseDirectory` that describes the schema. + Special :any:`LDAPObject` that describes the schema. Per default the subschema includes standard syntaxes, standard matching rules and objectclasses/attributetypes for the rootdse and subschema. It does not include objectclasses/attributetypes for actual data - (e.g. users and groups). See :any:`directory.Subschema` for details. + (e.g. users and groups). See :any:`Subschema` for details. If `subschema` is not `None`, the subschemaSubentry attribute is automatically added to all results returned by :any:`do_search`. ''' + static_objects = tuple() + def setup(self): super().setup() - self.rootdse = directory.RootDSE() - self.rootdse['objectClass'] = [b'top'] + self.rootdse = self.subschema.RootDSE() + self.rootdse['objectClass'] = ['top'] self.rootdse['supportedSASLMechanisms'] = self.get_sasl_mechanisms self.rootdse['supportedExtension'] = self.get_extentions - self.rootdse['supportedLDAPVersion'] = [b'3'] + self.rootdse['supportedLDAPVersion'] = ['3'] self.bind_object = None self.bind_sasl_state = None @@ -158,11 +160,11 @@ class SimpleLDAPRequestHandler(BaseLDAPRequestHandler): Called whenever the root DSE attribute "supportedExtension" is queried.''' res = [] if self.supports_starttls: - res.append(ldap.EXT_STARTTLS_OID.encode()) + res.append(ldap.EXT_STARTTLS_OID) if self.supports_whoami: - res.append(ldap.EXT_WHOAMI_OID.encode()) + res.append(ldap.EXT_WHOAMI_OID) if self.supports_password_modify: - res.append(ldap.EXT_PASSWORD_MODIFY_OID.encode()) + res.append(ldap.EXT_PASSWORD_MODIFY_OID) return res def get_sasl_mechanisms(self): @@ -176,11 +178,11 @@ class SimpleLDAPRequestHandler(BaseLDAPRequestHandler): Called whenever the root DSE attribute "supportedSASLMechanisms" is queried.''' res = [] if self.supports_sasl_anonymous: - res.append(b'ANONYMOUS') + res.append('ANONYMOUS') if self.supports_sasl_plain: - res.append(b'PLAIN') + res.append('PLAIN') if self.supports_sasl_external: - res.append(b'EXTERNAL') + res.append('EXTERNAL') return res def handle_bind(self, op, controls=None): @@ -396,11 +398,9 @@ class SimpleLDAPRequestHandler(BaseLDAPRequestHandler): def handle_search(self, op, controls=None): reject_critical_controls(controls) - for dn, attributes in self.do_search(op.baseObject, op.scope, op.filter): - pattributes = [ldap.PartialAttribute(name, values) for name, values in attributes.items()] - if 'subschemaSubentry' not in attributes and self.subschema is not None: - pattributes.append(ldap.PartialAttribute('subschemaSubentry', [bytes(self.subschema.dn)])) - yield ldap.SearchResultEntry(dn, pattributes) + for obj in self.do_search(op.baseObject, op.scope, op.filter): + if obj.match_search(op.baseObject, op.scope, op.filter): + yield obj.get_search_result_entry(op.attributes, op.typesOnly) yield ldap.SearchResultDone(ldap.LDAPResultCode.success) def do_search(self, baseobj, scope, filterobj): @@ -420,8 +420,9 @@ class SimpleLDAPRequestHandler(BaseLDAPRequestHandler): The default implementation returns matching objects from the root dse and the subschema.''' - yield from self.rootdse.search(baseobj, scope, filterobj) - yield from self.subschema.search(baseobj, scope, filterobj) + yield self.rootdse + yield self.subschema + yield from self.static_objects def handle_unbind(self, op, controls=None): reject_critical_controls(controls) diff --git a/ldapserver/util.py b/ldapserver/util.py deleted file mode 100644 index 7c03663..0000000 --- a/ldapserver/util.py +++ /dev/null @@ -1,47 +0,0 @@ -def encode_attribute(value): - if isinstance(value, bool): - value = b'TRUE' if value else b'FALSE' - if isinstance(value, int): - value = str(value) - if isinstance(value, str): - value = value.encode() - if not isinstance(value, bytes): - value = bytes(value) - return value - -class CaseInsensitiveKey(str): - def __hash__(self): - return hash(self.lower()) - - def __eq__(self, value): - return self.lower() == value.lower() - -class CaseInsensitiveDict(dict): - def __init__(self, *args, **kwargs): - if len(args) == 1 and isinstance(args[0], dict): - kwargs = {CaseInsensitiveKey(k): v for k, v in args[0].items()} - args = [] - else: - kwargs = {CaseInsensitiveKey(k): v for k, v in kwargs.items()} - args = [(CaseInsensitiveKey(k), v) for k, v in args] - super().__init__(*args, **kwargs) - - def __contains__(self, key): - return super().__contains__(CaseInsensitiveKey(key)) - - def __setitem__(self, key, value): - super().__setitem__(CaseInsensitiveKey(key), value) - - def __getitem__(self, key): - return super().__getitem__(CaseInsensitiveKey(key)) - - def get(self, key, default=None): - if key in self: - return self[key] - return default - -class AttributeDict(CaseInsensitiveDict): - def __getitem__(self, key): - if key not in self: - self[key] = [] - return super().__getitem__(key) diff --git a/tests/test_server.py b/tests/test_server.py index ca9b8ee..639b5de 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,6 +1,6 @@ import unittest -from ldapserver import BaseLDAPRequestHandler, SimpleLDAPRequestHandler, ldap +from ldapserver import BaseLDAPRequestHandler, LDAPRequestHandler, ldap class MockConnection: def __init__(self, data, chunksize): @@ -61,9 +61,9 @@ class TestBaseLDAPRequestHandler(unittest.TestCase): with self.assertRaises(ValueError): BaseLDAPRequestHandler(conn, '', None).handle() -class TestSimpleLDAPRequestHandler(unittest.TestCase): +class TestLDAPRequestHandler(unittest.TestCase): def test_session_python_ldap3(self): - class RequestHandler(SimpleLDAPRequestHandler): + class RequestHandler(LDAPRequestHandler): def handle(self): pass def do_bind_simple_authenticated(self, dn, password): @@ -96,7 +96,7 @@ class TestSimpleLDAPRequestHandler(unittest.TestCase): self.assertEqual(len(resps), 0) def test_session_openldap_utils(self): - class RequestHandler(SimpleLDAPRequestHandler): + class RequestHandler(LDAPRequestHandler): def handle(self): pass supports_sasl_plain = True -- GitLab