From 986d7c09db6fa498ab9990ffce36b9ff3afaf5ab Mon Sep 17 00:00:00 2001
From: Julian Rother <julianr@fsmpi.rwth-aachen.de>
Date: Sat, 20 Feb 2021 14:11:56 +0100
Subject: [PATCH] Started ldap3_mapper_new

---
 ldap3_mapper_new/__init__.py |  36 ++++++++
 ldap3_mapper_new/base.py     | 154 +++++++++++++++++++++++++++++++++++
 2 files changed, 190 insertions(+)
 create mode 100644 ldap3_mapper_new/__init__.py
 create mode 100644 ldap3_mapper_new/base.py

diff --git a/ldap3_mapper_new/__init__.py b/ldap3_mapper_new/__init__.py
new file mode 100644
index 00000000..7bb8e9be
--- /dev/null
+++ b/ldap3_mapper_new/__init__.py
@@ -0,0 +1,36 @@
+import ldap3
+
+from .types import LDAPCommitError
+from . import base
+
+class BaseModel(base.SessionObject)::
+	def __init__(self, _ldap_response=None, **kwargs):
+		super().__init__(_ldap_response)
+		for key, value, in kwargs.items():
+			if not hasattr(type(self), key):
+				raise Exception()
+			setattr(self, key, value)
+
+class LDAP3Mapper:
+	def __init__(self, server=None, bind_dn=None, bind_password=None):
+
+		class Session(base.Session):
+			ldap_mapper = self
+
+		class Model(BaseModel):
+			ldap_mapper = self
+
+		self.Session = Session # pylint: disable=invalid-name
+		self.Model = Model # 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()
+
+	def connect(self):
+		return ldap3.Connection(self.server, self.bind_dn, self.bind_password, auto_bind=True)
diff --git a/ldap3_mapper_new/base.py b/ldap3_mapper_new/base.py
new file mode 100644
index 00000000..1da3809e
--- /dev/null
+++ b/ldap3_mapper_new/base.py
@@ -0,0 +1,154 @@
+from enum import Enum
+from copy import deepcopy
+
+class Status(Enum):
+	NEW
+	ADDED
+	DELETED
+
+class State:
+	def __init__(self, status=Status.NEW, attributes=None):
+		self.status = status
+		self.attributes = attributes or {}
+
+	def copy(self):
+		return State(self.status, deepcopy(self.attributes))
+
+class Operation:
+	def apply(self, state):
+		raise NotImplemented()
+
+	def execute(self, conn):
+		raise NotImplemented()
+
+	def extend(self, oper):
+		return False
+
+class AddOperation(Operation):
+	def __init__(self, attributes, ldap_object_classes):
+		self.attributes = deepcopy(attributes)
+		self.ldap_object_classes = ldap_object_classes
+
+	def apply(self, state):
+		state.status = Status.ADDED
+		state.attributes = self.attributes
+
+	def execute(self, dn, conn):
+		success = conn.add(dn, self.ldap_object_classes, self.attributes)
+		if not success:
+			raise LDAPCommitError()
+
+class DeleteOperation(Operation):
+	def apply(self, state):
+		state.status = Status.DELETED
+
+	def execute(self, dn, conn):
+		success = conn.delete(dn)
+		if not success:
+			raise LDAPCommitError()
+
+class ModifyOperation(Operation):
+	def __init__(self, changes):
+		self.changes = deepcopy(changes)
+
+	def apply(self, state):
+		for attr, changes in self.changes.items():
+			for action, values in changes:
+				if action == MODIFY_REPLACE:
+					state.attributes[attr] = values
+				elif action == MODIFY_ADD:
+					state.attributes[attr] += values
+				elif action == MODIFY_DELETE:
+					for value in values:
+						state.attributes[attr].remove(value)
+
+	def execute(self, dn, conn):
+		success = conn.modify(dn, self.changes)
+		if not success:
+			raise LDAPCommitError()
+
+class Session:
+	ldap_mapper = None
+
+	def __init__(self):
+		self.__operations = []
+
+	def record(self, obj, oper):
+		if not self.__operations or self.__operations[0][0] != obj or not self.__operations[0][1].extend(oper):
+			self.__operations.append((obj, oper))
+
+	# TODO: maybe move the implementation to SessionObjectState?
+	def add(self, obj):
+		if obj.ldap_state.current.status != Status.NEW:
+			return
+		oper = AddOperation(obj.ldap_state.current.attributes, obj.ldap_object_classes)
+		oper.apply(obj.ldap_state.current)
+		self.__operations.append((obj, oper))
+
+	# TODO: maybe move the implementation to SessionObjectState?
+	def delete(self, obj):
+		if obj.ldap_state.current.status != Status.ADDED:
+			return
+		oper = DeleteOperation()
+		oper.apply(obj.ldap_state.current)
+		self.__operations.append((obj, oper))
+
+	def commit(self):
+		conn = self.ldap_mapper.connect()
+		while self.__operations:
+			obj, oper = self.__operations.pop(0)
+			try:
+				oper.execute(obj.dn, conn)
+			except e:
+				self.__operations.insert(0, (obj, oper))
+				raise e
+			oper.apply(obj.ldap_state.committed)
+
+	def rollback(self):
+		while self.__operations:
+			obj, oper = self.__operations.pop(0)
+			obj.ldap_state.current = obj.ldap_state.committed.copy()
+
+# This is only a seperate class to keep SessionObject's namespace cleaner
+class SessionObjectState:
+	def __init__(self, obj, response=None):
+		self.obj = obj
+		self.session = obj.ldap_mapper.session
+		if response is not None:
+			self.commited = State()
+		else:
+			self.commited = State(Status.ADDED, response['attributes'])
+		self.current = self.commited.copy()
+
+	def getattr(self, name):
+		return self.current.attributes.get(name, [])
+
+	def setattr(self, name, values):
+		oper = ModifyOperation({name: [(MODIFY_REPLACE, [values])]})
+		if self.current.status == Status.ADDED:
+			self.session.record(self.obj, oper)
+		oper.apply(self.current)
+
+	def attr_append(self, name, value):
+		oper = ModifyOperation({name: [(MODIFY_ADD, [value])]})
+		if self.current.status == Status.ADDED:
+			self.session.record(self.obj, oper)
+		oper.apply(self.current)
+
+	def attr_remove(self, name, value):
+		# TODO: how does LDAP handle MODIFY_DELETE ops with non-existant values?
+		oper = ModifyOperation({name: [(MODIFY_DELETE, [value])]})
+		if self.current.status == Status.ADDED:
+			self.session.record(self.obj, oper)
+		oper.apply(self.current)
+
+class SessionObject:
+	ldap_mapper = None
+	ldap_object_classes = None
+
+	def __init__(self, response=None):
+		self.ldap_state = SessionObjectState(self, response)
+
+	@property
+	def dn(self):
+		raise NotImplemented()
-- 
GitLab