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