diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0cb9044de9701e8ff7b807f45defa4eb5ffac5f2..1e833fdabc7c40d402a9de69ab1a1af58f047741 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,6 +2,8 @@ image: registry.git.cccv.de/infra/uffd/docker-images/buster
 
 variables:
   DEBIAN_FRONTEND: noninteractive 
+  GIT_SUBMODULE_STRATEGY: normal
+  PYTHONPATH: deps/ldapalchemy
 
 before_script:
   - python3 -V
@@ -28,7 +30,7 @@ unittests:
   stage: test
   script:
   - service slapd start
-  - UNITTEST_OPENLDAP=1 python3-coverage run --include './*.py' --omit 'tests/*.py' -m pytest --junitxml=report.xml || true
+  - UNITTEST_OPENLDAP=1 python3-coverage run --include 'uffd/*.py' -m pytest --junitxml=report.xml || true
   - python3-coverage report -m
   - python3-coverage html
   - python3-coverage xml
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000000000000000000000000000000000..51e8aed7051feb4f6e990a67fdbd2aa5b24e7957
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "deps/ldapalchemy"]
+	path = deps/ldapalchemy
+	url = ../ldapalchemy.git
diff --git a/deps/ldapalchemy b/deps/ldapalchemy
new file mode 160000
index 0000000000000000000000000000000000000000..e223f1617e3452d66d20b9368a74d2bdf6cc1ba4
--- /dev/null
+++ b/deps/ldapalchemy
@@ -0,0 +1 @@
+Subproject commit e223f1617e3452d66d20b9368a74d2bdf6cc1ba4
diff --git a/ldap_mapper/__init__.py b/ldap_mapper/__init__.py
deleted file mode 100644
index 3c0730e8c7c033bb53637547e58e81d62b77121a..0000000000000000000000000000000000000000
--- a/ldap_mapper/__init__.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import ldap3
-
-from .core import LDAPCommitError
-from . import model, attribute, relationship
-
-__all__ = ['LDAPMapper', 'LDAPCommitError']
-
-class LDAPMapper:
-	def __init__(self, server=None, bind_dn=None, bind_password=None):
-
-		class Model(model.Model):
-			ldap_mapper = self
-
-		self.Model = Model # pylint: disable=invalid-name
-		self.Session = model.Session # pylint: disable=invalid-name
-		self.Attribute = attribute.Attribute # pylint: disable=invalid-name
-		self.Relationship = relationship.Relationship # pylint: disable=invalid-name
-		self.Backreference = relationship.Backreference # pylint: disable=invalid-name
-
-		if not hasattr(type(self), 'server'):
-			self.server = server
-		if not hasattr(type(self), 'bind_dn'):
-			self.bind_dn = bind_dn
-		if not hasattr(type(self), 'bind_password'):
-			self.bind_password = bind_password
-		if not hasattr(type(self), 'session'):
-			self.session = self.Session(self.get_connection)
-
-	def get_connection(self):
-		return ldap3.Connection(self.server, self.bind_dn, self.bind_password, auto_bind=True)
diff --git a/ldap_mapper/attribute.py b/ldap_mapper/attribute.py
deleted file mode 100644
index bd776313fcfefd93238362a8263a03e40822d3b0..0000000000000000000000000000000000000000
--- a/ldap_mapper/attribute.py
+++ /dev/null
@@ -1,66 +0,0 @@
-from collections.abc import MutableSequence
-
-class AttributeList(MutableSequence):
-	def __init__(self, ldap_object, name, aliases):
-		self.__ldap_object = ldap_object
-		self.__name = name
-		self.__aliases = [name] + aliases
-
-	def __get(self):
-		return list(self.__ldap_object.getattr(self.__name))
-
-	def __set(self, values):
-		for name in self.__aliases:
-			self.__ldap_object.setattr(name, values)
-
-	def __repr__(self):
-		return repr(self.__get())
-
-	def __setitem__(self, key, value):
-		tmp = self.__get()
-		tmp[key] = value
-		self.__set(tmp)
-
-	def __delitem__(self, key):
-		tmp = self.__get()
-		del tmp[key]
-		self.__set(tmp)
-
-	def __len__(self):
-		return len(self.__get())
-
-	def __getitem__(self, key):
-		return self.__get()[key]
-
-	def insert(self, index, value):
-		tmp = self.__get()
-		tmp.insert(index, value)
-		self.__set(tmp)
-
-class Attribute:
-	def __init__(self, name, aliases=None, multi=False, default=None):
-		self.name = name
-		self.aliases = aliases or []
-		self.multi = multi
-		self.default = default
-
-	def add_hook(self, obj):
-		if obj.ldap_object.getattr(self.name) == []:
-			self.__set__(obj, self.default() if callable(self.default) else self.default)
-
-	def __set_name__(self, cls, name):
-		if self.default is not None:
-			cls.ldap_add_hooks = cls.ldap_add_hooks + (self.add_hook,)
-
-	def __get__(self, obj, objtype=None):
-		if obj is None:
-			return self
-		if self.multi:
-			return AttributeList(obj.ldap_object, self.name, self.aliases)
-		return (obj.ldap_object.getattr(self.name) or [None])[0]
-
-	def __set__(self, obj, values):
-		if not self.multi:
-			values = [values]
-		for name in [self.name] + self.aliases:
-			obj.ldap_object.setattr(name, values)
diff --git a/ldap_mapper/core.py b/ldap_mapper/core.py
deleted file mode 100644
index 57c8663ec421998c94f147860d3562512d4c15aa..0000000000000000000000000000000000000000
--- a/ldap_mapper/core.py
+++ /dev/null
@@ -1,277 +0,0 @@
-from ldap3 import MODIFY_REPLACE, MODIFY_DELETE, MODIFY_ADD, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES
-from ldap3.utils.conv import escape_filter_chars
-
-def encode_filter(filter_params):
-	return '(&%s)'%(''.join(['(%s=%s)'%(attr, escape_filter_chars(value)) for attr, value in filter_params]))
-
-def match_dn(dn, base):
-	return dn.endswith(base) # Probably good enougth for all valid dns
-
-def make_cache_key(search_base, filter_params):
-	res = (search_base,)
-	for attr, value in sorted(filter_params):
-		res += ((attr, value),)
-	return res
-
-class LDAPCommitError(Exception):
-	pass
-
-class SessionState:
-	def __init__(self, objects=None, deleted_objects=None, references=None):
-		self.objects = objects or {}
-		self.deleted_objects = deleted_objects or {}
-		self.references = references or {} # {(attr_name, value): {srcobj, ...}, ...}
-
-	def copy(self):
-		objects = self.objects.copy()
-		deleted_objects = self.deleted_objects.copy()
-		references = {key: objs.copy() for key, objs in self.references.items()}
-		return SessionState(objects, deleted_objects, references)
-
-	def ref(self, obj, attr, values):
-		for value in values:
-			key = (attr, value)
-			if key not in self.references:
-				self.references[key] = {obj}
-			else:
-				self.references[key].add(obj)
-
-	def unref(self, obj, attr, values):
-		for value in values:
-			self.references.get((attr, value), set()).discard(obj)
-
-class ObjectState:
-	def __init__(self, session=None, attributes=None, dn=None):
-		self.session = session
-		self.attributes = attributes or {}
-		self.dn = dn
-
-	def copy(self):
-		attributes = {name: values.copy() for name, values in self.attributes.items()}
-		return ObjectState(attributes=attributes, dn=self.dn, session=self.session)
-
-class AddOperation:
-	def __init__(self, obj, dn, object_classes):
-		self.obj = obj
-		self.dn = dn
-		self.object_classes = object_classes
-		self.attributes = {name: values.copy() for name, values in obj.state.attributes.items()}
-
-	def apply_object(self, obj_state):
-		obj_state.dn = self.dn
-		obj_state.attributes = {name: values.copy() for name, values in self.attributes.items()}
-		obj_state.attributes['objectClass'] = obj_state.attributes.get('objectClass', []) + list(self.object_classes)
-
-	def apply_session(self, session_state):
-		assert self.dn not in session_state.objects
-		session_state.objects[self.dn] = self.obj
-		for name, values in self.attributes.items():
-			session_state.ref(self.obj, name, values)
-		session_state.ref(self.obj, 'objectClass', self.object_classes)
-
-	def apply_ldap(self, conn):
-		success = conn.add(self.dn, self.object_classes, self.attributes)
-		if not success:
-			raise LDAPCommitError()
-
-class DeleteOperation:
-	def __init__(self, obj):
-		self.dn = obj.state.dn
-		self.obj = obj
-		self.attributes = {name: values.copy() for name, values in obj.state.attributes.items()}
-
-	def apply_object(self, obj_state):
-		obj_state.dn = None
-
-	def apply_session(self, session_state):
-		assert self.dn in session_state.objects
-		del session_state.objects[self.dn]
-		session_state.deleted_objects[self.dn] = self.obj
-		for name, values in self.attributes.items():
-			session_state.unref(self.obj, name, values)
-
-	def apply_ldap(self, conn):
-		success = conn.delete(self.dn)
-		if not success:
-			raise LDAPCommitError()
-
-class ModifyOperation:
-	def __init__(self, obj, changes):
-		self.obj = obj
-		self.attributes = {name: values.copy() for name, values in obj.state.attributes.items()}
-		self.changes = changes
-
-	def apply_object(self, obj_state):
-		for attr, changes in self.changes.items():
-			for action, values in changes:
-				if action == MODIFY_REPLACE:
-					obj_state.attributes[attr] = values
-				elif action == MODIFY_ADD:
-					obj_state.attributes[attr] += values
-				elif action == MODIFY_DELETE:
-					for value in values:
-						if value in obj_state.attributes[attr]:
-							obj_state.attributes[attr].remove(value)
-
-	def apply_session(self, session_state):
-		for attr, changes in self.changes.items():
-			for action, values in changes:
-				if action == MODIFY_REPLACE:
-					session_state.unref(self.obj, attr, self.attributes.get(attr, []))
-					session_state.ref(self.obj, attr, values)
-				elif action == MODIFY_ADD:
-					session_state.ref(self.obj, attr, values)
-				elif action == MODIFY_DELETE:
-					session_state.unref(self.obj, attr, values)
-
-	def apply_ldap(self, conn):
-		success = conn.modify(self.obj.state.dn, self.changes)
-		if not success:
-			raise LDAPCommitError()
-
-class Session:
-	def __init__(self, get_connection):
-		self.get_connection = get_connection
-		self.committed_state = SessionState()
-		self.state = SessionState()
-		self.changes = []
-		self.cached_searches = set()
-
-	def add(self, obj, dn, object_classes):
-		if self.state.objects.get(dn) == obj:
-			return
-		assert obj.state.session is None
-		oper = AddOperation(obj, dn, object_classes)
-		oper.apply_object(obj.state)
-		obj.state.session = self
-		oper.apply_session(self.state)
-		self.changes.append(oper)
-
-	def delete(self, obj):
-		if obj.state.dn not in self.state.objects:
-			return
-		assert obj.state.session == self
-		oper = DeleteOperation(obj)
-		oper.apply_object(obj.state)
-		obj.state.session = None
-		oper.apply_session(self.state)
-		self.changes.append(oper)
-
-	def record(self, oper):
-		assert oper.obj.state.session == self
-		self.changes.append(oper)
-
-	def commit(self):
-		conn = self.get_connection()
-		while self.changes:
-			oper = self.changes.pop(0)
-			try:
-				oper.apply_ldap(conn)
-			except Exception as err:
-				self.changes.insert(0, oper)
-				raise err
-			oper.apply_object(oper.obj.committed_state)
-			oper.apply_session(self.committed_state)
-		self.committed_state = self.state.copy()
-
-	def rollback(self):
-		for obj in self.state.objects.values():
-			obj.state = obj.committed_state.copy()
-		for obj in self.state.deleted_objects.values():
-			obj.state = obj.committed_state.copy()
-		self.state = self.committed_state.copy()
-		self.changes.clear()
-
-	def get(self, dn, filter_params):
-		if dn in self.state.objects:
-			obj = self.state.objects[dn]
-			return obj if obj.match(filter_params) else None
-		if dn in self.state.deleted_objects:
-			return None
-		conn = self.get_connection()
-		conn.search(dn, encode_filter(filter_params), attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES])
-		if not conn.response:
-			return None
-		assert len(conn.response) == 1
-		assert conn.response[0]['dn'] == dn
-		obj = Object(self, conn.response[0])
-		self.state.objects[dn] = obj
-		self.committed_state.objects[dn] = obj
-		for attr, values in obj.state.attributes.items():
-			self.state.ref(obj, attr, values)
-		return obj
-
-	def filter(self, search_base, filter_params):
-		if not filter_params:
-			matches = self.state.objects.values()
-		else:
-			submatches = [self.state.references.get((attr, value), set()) for attr, value in filter_params]
-			matches = submatches.pop(0)
-			while submatches:
-				matches = matches.intersection(submatches.pop(0))
-		res = [obj for obj in matches if match_dn(obj.state.dn, search_base)]
-		cache_key = make_cache_key(search_base, filter_params)
-		if cache_key in self.cached_searches:
-			return res
-		conn = self.get_connection()
-		conn.search(search_base, encode_filter(filter_params), attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES])
-		for response in conn.response:
-			dn = response['dn']
-			if dn in self.state.objects or dn in self.state.deleted_objects:
-				continue
-			obj = Object(self, response)
-			self.state.objects[dn] = obj
-			self.committed_state.objects[dn] = obj
-			for attr, values in obj.state.attributes.items():
-				self.state.ref(obj, attr, values)
-			res.append(obj)
-		self.cached_searches.add(cache_key)
-		return res
-
-class Object:
-	def __init__(self, session=None, response=None):
-		if response is None:
-			self.committed_state = ObjectState()
-		else:
-			assert session is not None
-			attrs = {attr: value if isinstance(value, list) else [value] for attr, value in response['attributes'].items()}
-			self.committed_state = ObjectState(session, attrs, response['dn'])
-		self.state = self.committed_state.copy()
-
-	@property
-	def dn(self):
-		return self.state.dn
-
-	@property
-	def session(self):
-		return self.state.session
-
-	def getattr(self, name):
-		return self.state.attributes.get(name, [])
-
-	def setattr(self, name, values):
-		oper = ModifyOperation(self, {name: [(MODIFY_REPLACE, values)]})
-		oper.apply_object(self.state)
-		if self.state.session:
-			oper.apply_session(self.state.session.state)
-			self.state.session.changes.append(oper)
-
-	def attr_append(self, name, value):
-		oper = ModifyOperation(self, {name: [(MODIFY_ADD, [value])]})
-		oper.apply_object(self.state)
-		if self.state.session:
-			oper.apply_session(self.state.session.state)
-			self.state.session.changes.append(oper)
-
-	def attr_remove(self, name, value):
-		oper = ModifyOperation(self, {name: [(MODIFY_DELETE, [value])]})
-		oper.apply_object(self.state)
-		if self.state.session:
-			oper.apply_session(self.state.session.state)
-			self.state.session.changes.append(oper)
-
-	def match(self, filter_params):
-		for attr, value in filter_params:
-			if value not in self.getattr(attr):
-				return False
-		return True
diff --git a/ldap_mapper/dbutils.py b/ldap_mapper/dbutils.py
deleted file mode 100644
index 42fccb96bbac3c1ad746b151260251c4260fe8af..0000000000000000000000000000000000000000
--- a/ldap_mapper/dbutils.py
+++ /dev/null
@@ -1,129 +0,0 @@
-from collections.abc import MutableSet
-
-from .model import add_to_session
-
-class DBRelationshipSet(MutableSet):
-	def __init__(self, dbobj, relattr, ldapcls, mapcls):
-		self.__dbobj = dbobj
-		self.__relattr = relattr
-		self.__ldapcls = ldapcls
-		self.__mapcls = mapcls
-
-	def __get_dns(self):
-		return [mapobj.dn for mapobj in getattr(self.__dbobj, self.__relattr)]
-
-	def __repr__(self):
-		return repr(set(self))
-
-	def __contains__(self, value):
-		if value is None or not isinstance(value, self.__ldapcls):
-			return False
-		return value.ldap_object.dn in self.__get_dns()
-
-	def __iter__(self):
-		return iter(filter(lambda obj: obj is not None, [self.__ldapcls.query.get(dn) for dn in self.__get_dns()]))
-
-	def __len__(self):
-		return len(set(self))
-
-	def add(self, value):
-		if not isinstance(value, self.__ldapcls):
-			raise TypeError()
-		if value.ldap_object.session is None:
-			add_to_session(value, self.__ldapcls.ldap_mapper.session.ldap_session)
-		if value.ldap_object.dn not in self.__get_dns():
-			getattr(self.__dbobj, self.__relattr).append(self.__mapcls(dn=value.ldap_object.dn))
-
-	def discard(self, value):
-		if not isinstance(value, self.__ldapcls):
-			raise TypeError()
-		rel = getattr(self.__dbobj, self.__relattr)
-		for mapobj in list(rel):
-			if mapobj.dn == value.ldap_object.dn:
-				rel.remove(mapobj)
-
-class DBRelationship:
-	def __init__(self, relattr, ldapcls, mapcls, backref=None, backattr=None):
-		self.relattr = relattr
-		self.ldapcls = ldapcls
-		self.mapcls = mapcls
-		self.backref = backref
-		self.backattr = backattr
-
-	def __set_name__(self, cls, name):
-		if self.backref:
-			assert self.backattr
-			setattr(self.ldapcls, self.backref, DBBackreference(cls, self.relattr, self.mapcls, self.backattr))
-
-	def __get__(self, obj, objtype=None):
-		if obj is None:
-			return self
-		return DBRelationshipSet(obj, self.relattr, self.ldapcls, self.mapcls)
-
-	def __set__(self, obj, values):
-		tmp = self.__get__(obj)
-		tmp.clear()
-		for value in values:
-			tmp.add(value)
-
-class DBBackreferenceSet(MutableSet):
-	def __init__(self, ldapobj, dbcls, relattr, mapcls, backattr):
-		self.__ldapobj = ldapobj
-		self.__dbcls = dbcls
-		self.__relattr = relattr
-		self.__mapcls = mapcls
-		self.__backattr = backattr
-
-	@property
-	def __dn(self):
-		return self.__ldapobj.ldap_object.dn
-
-	def __get(self):
-		return {getattr(mapobj, self.__backattr) for mapobj in self.__mapcls.query.filter_by(dn=self.__dn)}
-
-	def __repr__(self):
-		return repr(self.__get())
-
-	def __contains__(self, value):
-		return value in self.__get()
-
-	def __iter__(self):
-		return iter(self.__get())
-
-	def __len__(self):
-		return len(self.__get())
-
-	def add(self, value):
-		# TODO: add value to db session if necessary
-		assert self.__ldapobj.ldap_object.session is not None
-		if not isinstance(value, self.__dbcls):
-			raise TypeError()
-		rel = getattr(value, self.__relattr)
-		if self.__dn not in {mapobj.dn for mapobj in rel}:
-			rel.append(self.__mapcls(dn=self.__dn))
-
-	def discard(self, value):
-		if not isinstance(value, self.__dbcls):
-			raise TypeError()
-		rel = getattr(value, self.__relattr)
-		for mapobj in list(rel):
-			if mapobj.dn == self.__dn:
-				rel.remove(mapobj)
-
-class DBBackreference:
-	def __init__(self, dbcls, relattr, mapcls, backattr):
-		self.dbcls = dbcls
-		self.relattr = relattr
-		self.mapcls = mapcls
-		self.backattr = backattr
-
-	def __get__(self, obj, objtype=None):
-		if obj is None:
-			return self
-		return DBBackreferenceSet(obj, self.dbcls, self.relattr, self.mapcls, self.backattr)
-
-	def __set__(self, obj, values):
-		tmp = self.__get__(obj)
-		tmp.clear()
-		for value in values:
-			tmp.add(value)
diff --git a/ldap_mapper/model.py b/ldap_mapper/model.py
deleted file mode 100644
index 0dc9f240251aacf43ae457b3ff69aa7d82cdc0b9..0000000000000000000000000000000000000000
--- a/ldap_mapper/model.py
+++ /dev/null
@@ -1,118 +0,0 @@
-try:
-	# Added in v2.5
-	from ldap3.utils.dn import escape_rdn
-except ImportError:
-	# From ldap3 source code, Copyright Giovanni Cannata, LGPL v3 license
-	def escape_rdn(rdn):
-		# '/' must be handled first or the escape slashes will be escaped!
-		for char in ['\\', ',', '+', '"', '<', '>', ';', '=', '\x00']:
-			rdn = rdn.replace(char, '\\' + char)
-		if rdn[0] == '#' or rdn[0] == ' ':
-			rdn = ''.join(('\\', rdn))
-		if rdn[-1] == ' ':
-			rdn = ''.join((rdn[:-1], '\\ '))
-		return rdn
-
-from . import core
-
-def add_to_session(obj, session):
-	for func in obj.ldap_add_hooks:
-		func(obj)
-	session.add(obj.ldap_object, obj.dn, obj.ldap_object_classes)
-
-class Session:
-	def __init__(self, get_connection):
-		self.ldap_session = core.Session(get_connection)
-
-	def add(self, obj):
-		add_to_session(obj, self.ldap_session)
-
-	def delete(self, obj):
-		self.ldap_session.delete(obj.ldap_object)
-
-	def commit(self):
-		self.ldap_session.commit()
-
-	def rollback(self):
-		self.ldap_session.rollback()
-
-def make_modelobj(obj, model):
-	if obj is None:
-		return None
-	if not hasattr(obj, 'model'):
-		obj.model = model()
-		obj.model.ldap_object = obj
-	if not isinstance(obj.model, model):
-		return None
-	return obj.model
-
-def make_modelobjs(objs, model):
-	modelobjs = []
-	for obj in objs:
-		modelobj = make_modelobj(obj, model)
-		if modelobj is not None:
-			modelobjs.append(modelobj)
-	return modelobjs
-
-class ModelQuery:
-	def __init__(self, model):
-		self.model = model
-
-	def get(self, dn):
-		session = self.model.ldap_mapper.session.ldap_session
-		return make_modelobj(session.get(dn, self.model.ldap_filter_params), self.model)
-
-	def all(self):
-		session = self.model.ldap_mapper.session.ldap_session
-		objs = session.filter(self.model.ldap_search_base, self.model.ldap_filter_params)
-		return make_modelobjs(objs, self.model)
-
-	def filter_by(self, **kwargs):
-		filter_params = self.model.ldap_filter_params
-		filter_params += tuple((getattr(self.model, attr).name, value) for attr, value in kwargs.items())
-		session = self.model.ldap_mapper.session.ldap_session
-		objs = session.filter(self.model.ldap_search_base, filter_params)
-		return make_modelobjs(objs, self.model)
-
-class ModelQueryWrapper:
-	def __get__(self, obj, objtype=None):
-		return ModelQuery(objtype)
-
-class Model:
-	# Overwritten by mapper
-	ldap_mapper = None
-	query = ModelQueryWrapper()
-	ldap_add_hooks = ()
-
-	# Overwritten by models
-	ldap_search_base = None
-	ldap_filter_params = ()
-	ldap_object_classes = ()
-	ldap_dn_base = None
-	ldap_dn_attribute = None
-
-	def __init__(self, **kwargs):
-		self.ldap_object = core.Object()
-		for key, value, in kwargs.items():
-			if not hasattr(self, key):
-				raise Exception()
-			setattr(self, key, value)
-
-	@property
-	def dn(self):
-		if self.ldap_object.dn is not None:
-			return self.ldap_object.dn
-		if self.ldap_dn_base is None or self.ldap_dn_attribute is None:
-			return None
-		values = self.ldap_object.getattr(self.ldap_dn_attribute)
-		if not values:
-			return None
-		# escape_rdn can't handle empty strings
-		rdn = escape_rdn(values[0]) if values[0] else ''
-		return '%s=%s,%s'%(self.ldap_dn_attribute, rdn, self.ldap_dn_base)
-
-	def __repr__(self):
-		cls_name = '%s.%s'%(type(self).__module__, type(self).__name__)
-		if self.dn is not None:
-			return '<%s %s>'%(cls_name, self.dn)
-		return '<%s>'%cls_name
diff --git a/ldap_mapper/relationship.py b/ldap_mapper/relationship.py
deleted file mode 100644
index cf5e42bb055e661bc6a72ba77d14900e38b53b7f..0000000000000000000000000000000000000000
--- a/ldap_mapper/relationship.py
+++ /dev/null
@@ -1,136 +0,0 @@
-from collections.abc import MutableSet
-
-from .model import make_modelobj, make_modelobjs, add_to_session
-
-class UnboundObjectError(Exception):
-	pass
-
-class RelationshipSet(MutableSet):
-	def __init__(self, ldap_object, name, model, destmodel):
-		self.__ldap_object = ldap_object
-		self.__name = name
-		self.__model = model
-		self.__destmodel = destmodel
-
-	def __modify_check(self, value):
-		if self.__ldap_object.session is None:
-			raise UnboundObjectError()
-		if not isinstance(value, self.__destmodel):
-			raise TypeError()
-
-	def __repr__(self):
-		return repr(set(self))
-
-	def __contains__(self, value):
-		if value is None or not isinstance(value, self.__destmodel):
-			return False
-		return value.ldap_object.dn in self.__ldap_object.getattr(self.__name)
-
-	def __iter__(self):
-		def get(dn):
-			return make_modelobj(self.__ldap_object.session.get(dn, self.__destmodel.ldap_filter_params), self.__destmodel)
-		dns = set(self.__ldap_object.getattr(self.__name))
-		return iter(filter(lambda obj: obj is not None, map(get, dns)))
-
-	def __len__(self):
-		return len(set(self))
-
-	def add(self, value):
-		self.__modify_check(value)
-		if value.ldap_object.session is None:
-			add_to_session(value, self.__ldap_object.session)
-		assert value.ldap_object.session == self.__ldap_object.session
-		self.__ldap_object.attr_append(self.__name, value.dn)
-
-	def discard(self, value):
-		self.__modify_check(value)
-		self.__ldap_object.attr_remove(self.__name, value.dn)
-
-	def update(self, values):
-		for value in values:
-			self.add(value)
-
-class Relationship:
-	def __init__(self, name, destmodel, backref=None):
-		self.name = name
-		self.destmodel = destmodel
-		self.backref = backref
-
-	def __set_name__(self, cls, name):
-		if self.backref is not None:
-			setattr(self.destmodel, self.backref, Backreference(self.name, cls))
-
-	def __get__(self, obj, objtype=None):
-		if obj is None:
-			return self
-		return RelationshipSet(obj.ldap_object, self.name, type(obj), self.destmodel)
-
-	def __set__(self, obj, values):
-		tmp = self.__get__(obj)
-		tmp.clear()
-		for value in values:
-			tmp.add(value)
-
-class BackreferenceSet(MutableSet):
-	def __init__(self, ldap_object, name, model, srcmodel):
-		self.__ldap_object = ldap_object
-		self.__name = name
-		self.__model = model
-		self.__srcmodel = srcmodel
-
-	def __modify_check(self, value):
-		if self.__ldap_object.session is None:
-			raise UnboundObjectError()
-		if not isinstance(value, self.__srcmodel):
-			raise TypeError()
-
-	def __get(self):
-		if self.__ldap_object.session is None:
-			return set()
-		filter_params = self.__srcmodel.ldap_filter_params + ((self.__name, self.__ldap_object.dn),)
-		objs = self.__ldap_object.session.filter(self.__srcmodel.ldap_search_base, filter_params)
-		return set(make_modelobjs(objs, self.__srcmodel))
-
-	def __repr__(self):
-		return repr(self.__get())
-
-	def __contains__(self, value):
-		return value in self.__get()
-
-	def __iter__(self):
-		return iter(self.__get())
-
-	def __len__(self):
-		return len(self.__get())
-
-	def add(self, value):
-		self.__modify_check(value)
-		if value.ldap_object.session is None:
-			add_to_session(value, self.__ldap_object.session)
-		assert value.ldap_object.session == self.__ldap_object.session
-		if self.__ldap_object.dn not in value.ldap_object.getattr(self.__name):
-			value.ldap_object.attr_append(self.__name, self.__ldap_object.dn)
-
-	def discard(self, value):
-		self.__modify_check(value)
-		value.ldap_object.attr_remove(self.__name, self.__ldap_object.dn)
-
-	def update(self, values):
-		for value in values:
-			self.add(value)
-
-class Backreference:
-	def __init__(self, name, srcmodel):
-		self.name = name
-		self.srcmodel = srcmodel
-
-	def __get__(self, obj, objtype=None):
-		if obj is None:
-			return self
-		return BackreferenceSet(obj.ldap_object, self.name, type(obj), self.srcmodel)
-
-	def __set__(self, obj, values):
-		tmp = self.__get__(obj)
-		tmp.clear()
-		for value in values:
-			tmp.add(value)
diff --git a/uffd/__init__.py b/uffd/__init__.py
index 3d5ae6262855692f379638f88a0f89b4a6adf067..417b982001f55777c08a946e3fb893c4c1708687 100644
--- a/uffd/__init__.py
+++ b/uffd/__init__.py
@@ -1,13 +1,17 @@
 import os
 import secrets
+import sys
 
 from flask import Flask, redirect, url_for
 from werkzeug.routing import IntegerConverter
 
+sys.path.append('deps/ldapalchemy')
+
+# pylint: disable=wrong-import-position
 from uffd.database import db, SQLAlchemyJSON
 from uffd.template_helper import register_template_helper
 from uffd.navbar import setup_navbar
-
+# pylint: enable=wrong-import-position
 
 def create_app(test_config=None):
 	# create and configure the app
diff --git a/uffd/ldap.py b/uffd/ldap.py
index 2b9fe0a47e2a795a815847d697198774b611aad0..237271e7e004d7103f3b4966a5b4a46f98b5d297 100644
--- a/uffd/ldap.py
+++ b/uffd/ldap.py
@@ -2,7 +2,7 @@ from flask import current_app, request
 
 import ldap3
 
-from ldap_mapper import LDAPMapper, LDAPCommitError # pylint: disable=unused-import
+from ldapalchemy import LDAPMapper, LDAPCommitError # pylint: disable=unused-import
 
 class FlaskLDAPMapper(LDAPMapper):
 	def __init__(self):
diff --git a/uffd/role/models.py b/uffd/role/models.py
index d7e0daf91d4de1b7204d46bbbfea9ee7f5e5e6f9..533d3677f36f885189c7730a84cdfee4e6ecdcb1 100644
--- a/uffd/role/models.py
+++ b/uffd/role/models.py
@@ -2,7 +2,7 @@ from sqlalchemy import Column, String, Integer, Text, ForeignKey
 from sqlalchemy.orm import relationship
 from sqlalchemy.ext.declarative import declared_attr
 
-from ldap_mapper.dbutils import DBRelationship
+from ldapalchemy.dbutils import DBRelationship
 
 from uffd.database import db
 from uffd.user.models import User, Group