# -*- encoding: utf-8 -*- import asyncio import logging import typing as T from bonsai import LDAPEntry, LDAPModOp, NoSuchObjectError # type: ignore from phi.logging import get_logger from phi.security import hash_pass log = get_logger(__name__) alog = logging.getLogger("asyncio") alog.setLevel(logging.DEBUG) def get_class(obj): if type(obj) is type: return obj return obj.__class__ def recall(cls): """ Prints the name of the parent class. """ _cls = get_class(cls) return _cls.__bases__[0] def call_if_callable(cls, attr): """ Tell between methods and properties. Call if method. """ _attr = getattr(cls, attr) if "__call__" in dir(_attr): return _attr() return _attr def inheritance(obj, attr, root_cls=object): """ Concatenates the value obtained from invoking the method attr on the class and on any parent class until root_cls. """ res = call_if_callable(obj, attr) base = get_class(obj) while base is not root_cls: if base is not get_class(obj): res += ",{}".format(call_if_callable(base, attr)) base = recall(base) return res.strip(",") async def iter_children(children): return [child async for child in children] class Entry(object): """ LDAP Entry. Interface to LDAP. """ kind: T.Union[None, str] = None _name: T.Union[None, str] = None @classmethod def name(cls): if "_name" in dir(cls) and cls._name is not None: return cls._name return cls.__name__ @classmethod def qualified_name(cls): return "{}={}".format(cls.kind, cls.name()) def __init__(self, client): self.client = client self.base_dn = client.base_dn def __repr__(self): return f"<{call_if_callable(self, 'name')} {self.dn}>" def __aiter__(self): return self 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 children(self): """ Synchronous property to enumerate the children. """ return asyncio.run(iter_children(self.get_children())) @property def dn(self): return "{},{}".format(inheritance(self, "qualified_name", Entry), self.base_dn) async def describe(self): async with self.client.connect(is_async=True) as conn: res = await conn.search(self.dn, 0) return res 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 Hackers(Entry): """ This class is where Users belong. """ kind = "ou" _instances: T.Dict[str, Entry] = dict() def __new__(cls, client, *args, **kwargs): _name = f"{id(client)}" if _name not in cls._instances: cls._instances[_name] = object.__new__(cls) return cls._instances[_name] def __init__(self, client, *args, **kwargs): super().__init__(client) self._hackers = build_heritage(self, User) async def __anext__(self): try: return await self._hackers.__anext__() except StopAsyncIteration: self._hackers = build_heritage(self, User) raise async def get_by_attr(self, attr, value): async with self.client.connect(is_async=True) as conn: res = await conn.search("{}={},{}".format(attr, value, self.dn), 0) if len(res) == 0: return None return [User(r["uid"][0], self.client) for r in res] async def get_by_uid(self, uid): res = await self.get_by_attr("uid", uid) return res[0] class Robots(Entry): """ This class is where Services belong. """ kind = "ou" _name = "Services" _instances: T.Dict[str, Entry] = dict() def __new__(cls, client, *args, **kwargs): _name = f"{id(client)}" if _name not in cls._instances: cls._instances[_name] = object.__new__(cls) return cls._instances[_name] def __init__(self, client, *args, **kwargs): super().__init__(client) self._robots = build_heritage(self, Service) async def __anext__(self): try: return await self._robots.__anext__() except StopAsyncIteration: self._robots = build_heritage(self, Service) raise class Congregations(Entry): """ This class is where Groups belong. """ kind = "ou" _name = "Groups" _instances: T.Dict[str, Entry] = dict() def __new__(cls, client, *args, **kwargs): _name = f"{id(client)}" if _name not in cls._instances: cls._instances[_name] = object.__new__(cls) return cls._instances[_name] def __init__(self, client, *args, **kwargs): super().__init__(client) self._groups = build_heritage(self, Group, attribute_id="cn") async def __anext__(self): try: return await self._groups.__anext__() except StopAsyncIteration: self._groups = build_heritage(self, Group, attribute_id="cn") raise async def create_new_(self, **kwargs): entry = LDAPEntry(self.dn) entry["objectClass"] = self.object_class for k, w in kwargs.items(): entry[k] = w async with self.client.connect(is_async=True) as conn: await conn.add(entry) return entry class User(Hackers): """ This class models a user. Users may have attributes and belong one or more Group(s). """ kind = "uid" _instances: T.Dict[str, Entry] = dict() object_class = ["inetOrgPerson", "organizationalPerson", "person", "top"] def __new__(cls, name, client, *args, **kwargs): _name = f"{name}-{id(client)}" if _name not in cls._instances: cls._instances[_name] = object.__new__(cls) return cls._instances[_name] def __init__(self, name, client, *args, **kwargs): super().__init__(client, *args, **kwargs) self._name = name self._entry = LDAPEntry(self.dn) def __repr__(self): return f"<{get_class(self).__name__}({self.name}) {self.dn}>" def qualified_name(self): return "{}={}".format(self.kind, self.name) @property def name(self): return self._name @name.setter def name(self, name): raise RuntimeError("Name property is not modifiable.") async def create(self, mail, password=None, sn=None, cn=None): async with self.client.connect(is_async=True) as conn: res = await conn.search(self.dn, 0) if len(res) > 0: raise PhiEntryExists(self.dn) _sn = sn if sn is not None else self.name _cn = cn if cn is not None else self.name hashed = hash_pass(password) self._entry = await create_new_( self, uid=self.name, mail=mail, sn=_sn, cn=_cn, userPassword=hashed ) alog.info("User(%s): created", self.name) async def sync(self): async with self.client.connect(is_async=True) as conn: res = await conn.search(self.dn, 0) if len(res) == 0: raise PhiEntryDoesNotExist(self.dn) for k, v in res[0].items(): if not k == "dn": self._entry[k] = v alog.info("User(%s): synced", self.name) async def modify(self, key, value, append=False): async with self.client.connect(is_async=True) as conn: self._entry.connection = conn try: if not append: del self._entry[key] self._entry[key] = value except KeyError: raise PhiAttributeMissing(self.dn, key) await self._entry.modify() alog.info("User(%s): modified (%s)", self.name, key) async def remove(self): async with self.client.connect(is_async=True) as conn: self._entry.connection = conn try: await self._entry.delete() alog.info("User(%s): removed", self.name) except NoSuchObjectError: raise PhiEntryDoesNotExist(self.dn) async def modify_password(self, new_pass, old_pass=None): try: await self.modify("userPassword", hash_pass(new_pass)) except PhiAttributeMissing: raise PhiUnauthorized( user=self.client.username, ) alog.info("User(%s): password modified", self.name) async def verify_password(self, given_pass): async with self.client.connect(is_async=True) as conn: res = await conn.search(self.dn, 0) if len(res) == 0: raise PhiEntryDoesNotExist(self.dn) try: match_pass = res[0]["userPassword"][0] == hash_pass(given_pass) except KeyError: raise PhiUnauthorized( user=self.client.username, ) return match_pass class PhiEntryExists(Exception): def __init__(self, dn): self.dn = dn class PhiEntryDoesNotExist(Exception): def __init__(self, dn): self.dn = dn class PhiAttributeMissing(Exception): def __init__(self, dn, attr): self.dn = dn self.attr = attr class PhiUnauthorized(Exception): def __init__(self, user, *args, **kwargs): super().__init__(*args, **kwargs) self.user = user class Service(Robots): """ This class models a system user (i.e. users that are ancillary to services on a machine). System user may have attributes and belong to one or more Group(s). """ kind = "uid" _instances: T.Dict[str, Entry] = dict() object_class = ["account", "top", "simpleSecurityObject"] def __new__(cls, name, client, *args, **kwargs): _name = f"{name}-{id(client)}" if _name not in cls._instances: cls._instances[_name] = object.__new__(cls) return cls._instances[_name] def __init__(self, name, *args, **kwargs): super().__init__(*args, **kwargs) self._name = name def __repr__(self): return f"<{get_class(self).__name__}({self.name}) {self.dn}>" def qualified_name(self): return "{}={}".format(self.kind, self.name) @property def name(self): return self._name @name.setter def name(self, name): raise RuntimeError("Name property is not modifiable.") async def create(self, password): async with self.client.connect(is_async=True) as conn: res = await conn.search(self.dn, 0) if len(res) > 0: raise PhiEntryExists(self.dn) self._entry = await create_new_(self, uid=self.name, userPassword=password) alog.info("Service(%s): created", self.name) async def sync(self): async with self.client.connect(is_async=True) as conn: res = await conn.search(self.dn, 0) if len(res) == 0: raise PhiEntryDoesNotExist(self.dn) for k, v in res[0].items(): if not k == "dn": self._entry[k] = v alog.info("Service(%s): synced", self.name) async def remove(self): pass async def modify_password(self, new_pass, old_pass=None): pass async def verify_password(self, given_pass): pass class Group(Congregations): """ This class models a group. Groups may have attributes and may have Users and Services belonging to them. """ kind = "cn" _instances: T.Dict[str, Entry] = dict() object_class = ["groupOfNames", "top"] def __new__(cls, name, client, *args, **kwargs): _name = f"{name}-{id(client)}" if _name not in cls._instances: cls._instances[_name] = object.__new__(cls) return cls._instances[_name] def __init__(self, name, *args, **kwargs): super().__init__(*args, **kwargs) self._name = name def __repr__(self): return f"<{get_class(self).__name__}({self.name}) {self.dn}>" def qualified_name(self): return "{}={}".format(self.kind, self.name) @property def name(self): return self._name @name.setter def name(self, name): raise RuntimeError("Name property is not modifiable.")