diff --git a/src/phi/async_ldap/mixins.py b/src/phi/async_ldap/mixins.py new file mode 100644 index 0000000..89b6b9b --- /dev/null +++ b/src/phi/async_ldap/mixins.py @@ -0,0 +1,136 @@ +# -*- encoding: utf-8 -*- +from bonsai import LDAPEntry, LDAPModOp, NoSuchObjectError # type: ignore + +from phi.exceptions import ( + PhiAttributeMissing, + PhiEntryDoesNotExist, + PhiEntryExists, + PhiUnauthorized, + PhiUnexpectedRuntimeValue, +) + +ATTR_TAG = "_ldap_" +ATTR_TAG_LEN = len(ATTR_TAG) + + +async def build_heritage(obj, child_class, attribute_id="uid"): + """ + Given the object and the child class, yields the + instances of the children. + """ + async for child in obj.get_children(): + if attribute_id in child: + _name = child[attribute_id][0] + yield child_class(_name, obj.client) + + +class Singleton(object): + """ + to singletonize a class. The class is crafted to be used with the mixins + that implement the compatible __init__. + """ + + def __new__(cls, client, *args): + if "name" in args: + name = f"{cls.__name__}-{args['name']}-{id(client)}" + else: + name = f"{cls.__name__}-{id(client)}" + if name not in cls._instances: + cls._instances[name] = object.__new__(cls) + return cls._instances[name] + + +class Entry(object): + """ + to interact with LDAP. + """ + + def get_all_ldap_attributes(self): + return [attr for attr in dir(self) if attr.startswith(ATTR_TAG)] + + def __repr__(self): + return f"<{self.__class__.__name__} {self.dn}>" + + def __str__(self): + return f"<{self.__class__.__name__} {self.dn}>" + + def __iter__(self): + for attr in self.get_all_ldap_attributes(): + if not isinstance(getattr(type(self), attr), property) and callable( + getattr(type(self), attr) + ): + yield attr[ATTR_TAG_LEN:], getattr(self, attr)() + else: + yield attr[ATTR_TAG_LEN:], getattr(self, attr) + + def __dict__(self): + return dict(self) + + async def _create_new(self): + entry = LDAPEntry(self.dn) + entry["objectClass"] = self.object_class + entry.update(self.get_all_ldap_attributes()) + async with self.client.connect(is_async=True) as conn: + await conn.add(entry) + return entry + + async def _get(self): + async with self.client.connect(is_async=True) as conn: + # This returns a list of dicts. It should always contain only one item: + # the one we are interested. + _res = await conn.search(self.dn, 0) + if len(_res) == 0: + return + elif len(_res) > 1: + raise PhiUnexpectedRuntimeValue( + "return value should be no more than one", res + ) + return _res[0] + + async def describe(self): + res = dict(await self._get()) + res["dn"] = self.dn + res["name"] = self.name + return res + + +class OrganizationalUnit(object): + def __init__(self, client): + self.client = client + self.base_dn = client.base_dn + self.name = self.__class__.__name__ + self.children = build_heritage(self, getattr(self, "child_class")) + self._entry = LDAPEntry(self.dn) + + def __aiter__(self): + return self + + async def __anext__(self): + try: + return await self.children.__anext__() + except StopAsyncIteration: + self.children = build_heritage(self, getattr(self, "child_class")) + raise + + async def get_children(self): + async with self.client.connect(is_async=True) as conn: + for el in await conn.search(self.dn, 2): + yield el + + @property + def dn(self): + return f"ou={self.ou_name},{self.base_dn}" + + +class Child(object): + object_class = ["inetOrgPerson", "organizationalPerson", "person", "top"] + + def __init__(self, client, name): + self.client = client + self.base_dn = client.base_dn + self.name = name + self._entry = LDAPEntry(self.dn) + + @property + def dn(self): + return f"{self.id_tag}={self.name},ou={self.ou_name},{self.base_dn}" diff --git a/src/phi/async_ldap/new_model.py b/src/phi/async_ldap/new_model.py new file mode 100644 index 0000000..098e2c7 --- /dev/null +++ b/src/phi/async_ldap/new_model.py @@ -0,0 +1,15 @@ +# -*- encoding: utf-8 -*- + +from phi.async_ldap import mixins + + +class User(mixins.Singleton, mixins.Entry, mixins.Child): + _instances = dict() + id_tag = "uid" + ou_name = "Hackers" + + +class Hackers(mixins.Singleton, mixins.Entry, mixins.OrganizationalUnit): + _instances = dict() + ou_name = "Hackers" + child_class = User