# -*- encoding: utf-8 -*- from bonsai import LDAPEntry, LDAPModOp, NoSuchObjectError # type: ignore from bonsai.ldapvaluelist import LDAPValueList import bonsai.errors from phi.exceptions import ( PhiAttributeMissing, PhiEntryDoesNotExist, PhiEntryExists, PhiUnauthorized, PhiUnexpectedRuntimeValue, PhiCannotExecute, ) from phi.security import hash_pass, handle_password 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(obj.client, _name) class Singleton(object): """ Mixin to singletonize a class. The class is crafted to be used with the mixins that implement the compatible __init__. """ def __new__(cls, client, *args, **kwargs): if "name" in kwargs: name = f"{cls.__name__}-{args['name']}-{id(client)}" elif args: name = f"{cls.__name__}-{args[0]}-{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] def get_value(obj, attr): """ Return the tuple (attribute_name, attribute_value) from obj. Extract the value, either it being a constant or the result of a function call. """ if not isinstance(getattr(type(obj), attr), property) and callable( getattr(type(obj), attr) ): return attr, getattr(obj, attr)() else: return attr, getattr(obj, attr) class Entry(object): """ Mixin to interact with LDAP. """ def get_all_ldap_attributes(self): return [get_value(self, attr) for attr in self.ldap_attributes] def __repr__(self): return f"<{self.__class__.__name__} {self.dn}>" def __str__(self): return f"<{self.__class__.__name__} {self.dn}>" def __iter__(self): for k, v in self.get_all_ldap_attributes(): yield k, v def __dict__(self): return dict(self) async def _create_new(self): self._entry["objectClass"] = self.object_class async with self.client.connect(is_async=True) as conn: await conn.add(self._entry) return self._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: raise PhiEntryDoesNotExist() elif len(_res) > 1: raise PhiUnexpectedRuntimeValue( "return value should be no more than one", res ) return _res[0] async def _modify(self): _ = await self._get() async with self.client.connect(is_async=True) as conn: self._entry.connection = conn await self._entry.modify() async def _delete(self): async with self.client.connect(is_async=True) as conn: await conn.delete(self.dn) async def describe(self): return dict(await self._get()) @property def delete_cascade(self): if hasattr(self, "_delete_cascade"): return self._delete_cascade return False @delete_cascade.setter def delete_cascade(self, value): if not isinstance(value, bool): raise ValueError("delete_cascade must be a boolean") self._delete_cascade = value class OrganizationalUnit(object): """ Mixin that represents an OrganizationalUnit. It provides the methods to interact with the LDAP db _and_ to supervise its `Member`s. To properly use it, one must specify the `ou` and `child_class` class attributes when inheriting. """ object_class = ["organizationalUnit", "top"] def __init__(self, client, **kwargs): self.client = client self.base_dn = client.base_dn self.name = self.__class__.__name__ self.children = build_heritage(self, self.child_class, self.child_class.id_tag) self._entry = LDAPEntry(self.dn) for k, v in kwargs.items(): self._entry[k] = v def __aiter__(self): return self async def __anext__(self): try: return await self.children.__anext__() except StopAsyncIteration: self.children = build_heritage( self, self.child_class, self.child_class.id_tag ) raise async def get_children(self): async with self.client.connect(is_async=True) as conn: for el in await conn.search(self.dn, 1): yield el @property def dn(self): return f"ou={self.ou},{self.base_dn}" async def save(self): try: await self._create_new() except bonsai.errors.AlreadyExists: raise PhiEntryExists(self.dn) async def search(self, member_name): result = None async with self.client.connect(is_async=True) as conn: result = await conn.search( f"{self.child_class.id_tag}={member_name},{self.dn}", 0 ) if not result: raise PhiEntryDoesNotExist( f"{self.child_class.id_tag}={member_name},{self.dn}" ) if isinstance(result, list) and len(result) > 1: raise PhiUnexpectedRuntimeValue( "return value should be no more than one", result ) return self.child_class(self.client, member_name, **result[0]) async def delete(self): """ Delete all the members of this OU only if `delete_cascade` is set to `True`, raises otherwise. """ if self.delete_cascade: async for member in self: await member.delete() else: raise PhiCannotExecute("Cannot delete an OU and delete_cascade is not set") def _hydrate(obj, data): """ Iterate over the structure of the given `data`. Using the key name, filtering only on the values that the given `obj` accepts (`obj.ldap_attributes`), appropriately set the corresponding value in the given `obj`. In particular: - append to lists - handle password setting - set scalars This is called `_hydrate` because its aim is to fill a structure (the `obj`) with substance. """ for k, v in data.items(): if k in obj.ldap_attributes: if isinstance(v, list) and not isinstance(v, LDAPValueList): obj._entry[k] = [] for _v in v: obj._entry[k].append(_v.dn) elif k == "userPassword": obj._entry[k] = handle_password(v) else: obj._entry[k] = v class Member(object): """ Mixin that represents a generic member of an `OrganizationalUnit`. It provides the methods to interact with the LDAP db. To properly use, `ou`, `object_class` and `ldap_attributes` class attributes must be specified when inheriting. """ def __init__(self, client, name, **kwargs): self.client = client self.base_dn = client.base_dn self.name = name self._entry = LDAPEntry(self.dn) if kwargs: _hydrate(self, kwargs) def __eq__(self, other): if isinstance(other, type(self)): return other.dn == self.dn elif isinstance(other, str): return other == self.dn elif isinstance(other, LDAPEntry): return str(other) == self.dn else: return False @property def dn(self): return f"{self.id_tag}={self.name},ou={self.ou},{self.base_dn}" def __setitem__(self, attr, val): if attr not in self.ldap_attributes: raise PhiCannotExecute( f"{attr} is not an allowed ldap_attribute: {self.ldap_attributes}" ) self._entry[attr] = val def __getitem__(self, attr): if attr not in self.ldap_attributes: raise PhiCannotExecute( f"{attr} is not an allowed ldap_attribute: {self.ldap_attributes}" ) return self._entry[attr][0] async def save(self): try: await self._create_new() except bonsai.errors.AlreadyExists: raise PhiEntryExists(self.dn) async def modify(self): await self._modify() async def delete(self): await self._delete() async def sync(self): res = await self._get() _hydrate(self, res) return self