2020-08-31 11:01:46 +02:00
|
|
|
# -*- encoding: utf-8 -*-
|
|
|
|
from bonsai import LDAPEntry, LDAPModOp, NoSuchObjectError # type: ignore
|
2020-11-11 22:25:02 +01:00
|
|
|
from bonsai.ldapvaluelist import LDAPValueList
|
|
|
|
import bonsai.errors
|
2020-08-31 11:01:46 +02:00
|
|
|
|
|
|
|
from phi.exceptions import (
|
|
|
|
PhiEntryDoesNotExist,
|
|
|
|
PhiEntryExists,
|
|
|
|
PhiUnexpectedRuntimeValue,
|
2020-11-11 22:25:02 +01:00
|
|
|
PhiCannotExecute,
|
2020-08-31 11:01:46 +02:00
|
|
|
)
|
|
|
|
|
2020-11-11 22:25:02 +01:00
|
|
|
from phi.security import hash_pass, handle_password
|
2020-08-31 11:01:46 +02:00
|
|
|
|
|
|
|
|
2020-12-27 19:48:57 +01:00
|
|
|
def de_listify(elems):
|
|
|
|
if not isinstance(elems, list):
|
|
|
|
return elems
|
|
|
|
if len(elems) == 1:
|
|
|
|
return elems[0]
|
|
|
|
else:
|
|
|
|
return elems
|
|
|
|
|
|
|
|
|
2020-08-31 11:01:46 +02:00
|
|
|
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]
|
2020-11-11 22:25:02 +01:00
|
|
|
yield child_class(obj.client, _name)
|
2020-08-31 11:01:46 +02:00
|
|
|
|
|
|
|
|
|
|
|
class Singleton(object):
|
|
|
|
"""
|
2020-11-11 22:25:02 +01:00
|
|
|
Mixin to singletonize a class. The class is crafted to be used with the mixins
|
2020-08-31 11:01:46 +02:00
|
|
|
that implement the compatible __init__.
|
|
|
|
"""
|
|
|
|
|
2020-11-11 22:25:02 +01:00
|
|
|
def __new__(cls, client, *args, **kwargs):
|
|
|
|
if "name" in kwargs:
|
2020-08-31 11:01:46 +02:00
|
|
|
name = f"{cls.__name__}-{args['name']}-{id(client)}"
|
2020-11-11 22:25:02 +01:00
|
|
|
elif args:
|
|
|
|
name = f"{cls.__name__}-{args[0]}-{id(client)}"
|
2020-08-31 11:01:46 +02:00
|
|
|
else:
|
|
|
|
name = f"{cls.__name__}-{id(client)}"
|
2022-01-25 23:46:45 +01:00
|
|
|
if not hasattr(cls, "_instances"):
|
|
|
|
cls._instances = dict()
|
2020-08-31 11:01:46 +02:00
|
|
|
if name not in cls._instances:
|
|
|
|
cls._instances[name] = object.__new__(cls)
|
|
|
|
return cls._instances[name]
|
|
|
|
|
|
|
|
|
|
|
|
class Entry(object):
|
|
|
|
"""
|
2020-11-11 22:25:02 +01:00
|
|
|
Mixin to interact with LDAP.
|
2020-08-31 11:01:46 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return f"<{self.__class__.__name__} {self.dn}>"
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return f"<{self.__class__.__name__} {self.dn}>"
|
|
|
|
|
|
|
|
async def _create_new(self):
|
2020-11-11 22:25:02 +01:00
|
|
|
self._entry["objectClass"] = self.object_class
|
2020-08-31 11:01:46 +02:00
|
|
|
async with self.client.connect(is_async=True) as conn:
|
2020-11-11 22:25:02 +01:00
|
|
|
await conn.add(self._entry)
|
|
|
|
return self._entry
|
2020-08-31 11:01:46 +02:00
|
|
|
|
|
|
|
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:
|
2020-12-27 19:48:57 +01:00
|
|
|
# the one we are interested in.
|
2020-08-31 11:01:46 +02:00
|
|
|
_res = await conn.search(self.dn, 0)
|
|
|
|
if len(_res) == 0:
|
2020-12-27 19:48:57 +01:00
|
|
|
raise PhiEntryDoesNotExist(self.dn)
|
2020-08-31 11:01:46 +02:00
|
|
|
elif len(_res) > 1:
|
|
|
|
raise PhiUnexpectedRuntimeValue(
|
|
|
|
"return value should be no more than one", res
|
|
|
|
)
|
|
|
|
return _res[0]
|
|
|
|
|
2020-11-11 22:25:02 +01:00
|
|
|
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:
|
2020-12-27 19:48:57 +01:00
|
|
|
await conn.delete(self.dn, recursive=self.delete_cascade)
|
2020-11-11 22:25:02 +01:00
|
|
|
|
2020-08-31 11:01:46 +02:00
|
|
|
async def describe(self):
|
2020-12-27 19:48:57 +01:00
|
|
|
_internal = await self._get()
|
|
|
|
values = dict((k, de_listify(_internal[k])) for k in self.ldap_attributes)
|
|
|
|
if "userPassword" in self.ldap_attributes:
|
|
|
|
values.pop("userPassword")
|
|
|
|
values["dn"] = self.dn
|
|
|
|
return values
|
2020-11-11 22:25:02 +01:00
|
|
|
|
|
|
|
@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
|
2020-08-31 11:01:46 +02:00
|
|
|
|
|
|
|
|
|
|
|
class OrganizationalUnit(object):
|
2020-11-27 19:41:09 +01:00
|
|
|
"""
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
|
2020-11-11 22:25:02 +01:00
|
|
|
object_class = ["organizationalUnit", "top"]
|
|
|
|
|
|
|
|
def __init__(self, client, **kwargs):
|
2020-08-31 11:01:46 +02:00
|
|
|
self.client = client
|
|
|
|
self.base_dn = client.base_dn
|
|
|
|
self.name = self.__class__.__name__
|
2020-11-11 22:25:02 +01:00
|
|
|
self.children = build_heritage(self, self.child_class, self.child_class.id_tag)
|
2020-08-31 11:01:46 +02:00
|
|
|
self._entry = LDAPEntry(self.dn)
|
2020-11-11 22:25:02 +01:00
|
|
|
for k, v in kwargs.items():
|
2020-12-27 19:48:57 +01:00
|
|
|
if k in self.ldap_attributes:
|
|
|
|
self._entry[k] = v
|
|
|
|
if "delete_cascade" in kwargs:
|
|
|
|
self.delete_cascade = delete_cascade
|
2020-08-31 11:01:46 +02:00
|
|
|
|
|
|
|
def __aiter__(self):
|
|
|
|
return self
|
|
|
|
|
|
|
|
async def __anext__(self):
|
|
|
|
try:
|
|
|
|
return await self.children.__anext__()
|
|
|
|
except StopAsyncIteration:
|
2020-11-11 22:25:02 +01:00
|
|
|
self.children = build_heritage(
|
|
|
|
self, self.child_class, self.child_class.id_tag
|
|
|
|
)
|
2020-08-31 11:01:46 +02:00
|
|
|
raise
|
|
|
|
|
|
|
|
async def get_children(self):
|
|
|
|
async with self.client.connect(is_async=True) as conn:
|
2020-11-11 22:25:02 +01:00
|
|
|
for el in await conn.search(self.dn, 1):
|
2020-08-31 11:01:46 +02:00
|
|
|
yield el
|
|
|
|
|
|
|
|
@property
|
|
|
|
def dn(self):
|
2020-11-11 22:25:02 +01:00
|
|
|
return f"ou={self.ou},{self.base_dn}"
|
2020-08-31 11:01:46 +02:00
|
|
|
|
2020-11-11 22:25:02 +01:00
|
|
|
async def save(self):
|
2020-12-27 19:48:57 +01:00
|
|
|
"""
|
|
|
|
This function iterates over the OU's children and invokes its `save` method,
|
|
|
|
ignoring errors from yet existing ones.
|
|
|
|
"""
|
|
|
|
async for child in self:
|
|
|
|
try:
|
|
|
|
await child.save()
|
|
|
|
except PhiEntryExists:
|
|
|
|
pass
|
2020-08-31 11:01:46 +02:00
|
|
|
|
2020-11-11 22:25:02 +01:00
|
|
|
async def search(self, member_name):
|
2020-12-27 19:48:57 +01:00
|
|
|
"""
|
|
|
|
This function allows one to search through the OU's children. The search
|
|
|
|
function is the one from the underlying library (bonsai) and is strict as such.
|
|
|
|
"""
|
2020-11-11 22:25:02 +01:00
|
|
|
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])
|
2020-08-31 11:01:46 +02:00
|
|
|
|
2020-11-11 22:25:02 +01:00
|
|
|
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")
|
|
|
|
|
|
|
|
|
2020-11-27 19:41:09 +01:00
|
|
|
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.
|
|
|
|
"""
|
2020-11-11 22:25:02 +01:00
|
|
|
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):
|
2020-11-27 19:41:09 +01:00
|
|
|
"""
|
|
|
|
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.
|
2020-12-27 19:48:57 +01:00
|
|
|
|
|
|
|
## Usage
|
|
|
|
|
|
|
|
The initialization needs an `phi.async_ldap.AsyncClient` and a `name`, that is used
|
|
|
|
as value in the identification attribute (i.e. `uid`).
|
|
|
|
This inits an object in memory that may or may not exist in the ldap database yet.
|
|
|
|
To test it, one can invoke the async method `exists` or may try to `sync`, handling
|
|
|
|
the corresponding exception (`PhiEntryDoesNotExist`).
|
|
|
|
To save a new instance, one can `save`. The instance accepts dict-like get and set
|
|
|
|
on the aforementioned `ldap_attributes`. Once an attribute value has been modified,
|
|
|
|
one can invoke `modify` to persist the changes.
|
|
|
|
To remove an instance from the database, one can invoke `delete`.
|
|
|
|
|
|
|
|
## Comparisons
|
|
|
|
|
|
|
|
The comparison operation with a `Member` is quite loose: it returns `True` with
|
|
|
|
either:
|
|
|
|
- an instance of the same `type` (i.e. the same class whose this mixin is used
|
|
|
|
into) whose `dn` matches
|
|
|
|
- an `LDAPEntry` whose `dn` matches
|
|
|
|
- a string matching the `dn`
|
2020-11-27 19:41:09 +01:00
|
|
|
"""
|
|
|
|
|
2020-11-11 22:25:02 +01:00
|
|
|
def __init__(self, client, name, **kwargs):
|
2020-12-27 19:48:57 +01:00
|
|
|
super().__init__()
|
2020-08-31 11:01:46 +02:00
|
|
|
self.client = client
|
|
|
|
self.base_dn = client.base_dn
|
|
|
|
self.name = name
|
|
|
|
self._entry = LDAPEntry(self.dn)
|
2020-11-30 19:16:36 +01:00
|
|
|
self[self.id_tag] = name
|
|
|
|
self._entry["ou"] = self.ou
|
2020-11-11 22:25:02 +01:00
|
|
|
if kwargs:
|
2020-11-27 19:41:09 +01:00
|
|
|
_hydrate(self, kwargs)
|
2020-11-11 22:25:02 +01:00
|
|
|
|
|
|
|
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):
|
2020-12-27 19:48:57 +01:00
|
|
|
return other["dn"] == self.dn
|
2020-11-11 22:25:02 +01:00
|
|
|
else:
|
|
|
|
return False
|
2020-08-31 11:01:46 +02:00
|
|
|
|
|
|
|
@property
|
|
|
|
def dn(self):
|
2020-11-11 22:25:02 +01:00
|
|
|
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(
|
2020-12-27 19:48:57 +01:00
|
|
|
f"{attr} is not an allowed ldap attribute: {self.ldap_attributes}"
|
2020-11-11 22:25:02 +01:00
|
|
|
)
|
|
|
|
self._entry[attr] = val
|
|
|
|
|
|
|
|
def __getitem__(self, attr):
|
|
|
|
if attr not in self.ldap_attributes:
|
|
|
|
raise PhiCannotExecute(
|
2020-12-27 19:48:57 +01:00
|
|
|
f"{attr} is not an allowed ldap attribute: {self.ldap_attributes}"
|
2020-11-11 22:25:02 +01:00
|
|
|
)
|
|
|
|
return self._entry[attr][0]
|
|
|
|
|
|
|
|
async def save(self):
|
2020-12-27 19:48:57 +01:00
|
|
|
"""
|
|
|
|
This method persists on the ldap database an inited instance. Raises
|
|
|
|
`PhiEntryExists` in case of a yet existing instance. Raises a specific error if
|
|
|
|
the instance misses any of the needed attributes (accoding to
|
|
|
|
`ldap_attributes`).
|
|
|
|
"""
|
2020-11-11 22:25:02 +01:00
|
|
|
try:
|
|
|
|
await self._create_new()
|
|
|
|
except bonsai.errors.AlreadyExists:
|
|
|
|
raise PhiEntryExists(self.dn)
|
|
|
|
|
|
|
|
async def modify(self):
|
2020-12-27 19:48:57 +01:00
|
|
|
"""
|
|
|
|
This method saves the changes made to the instance on the ldap database. Raises
|
|
|
|
`PhiEntryDoesNotExist` in case of an instance not yet persisted.
|
|
|
|
"""
|
2020-11-11 22:25:02 +01:00
|
|
|
await self._modify()
|
|
|
|
|
|
|
|
async def delete(self):
|
2020-12-27 19:48:57 +01:00
|
|
|
"""
|
|
|
|
This method removes the instance from the database. Raises
|
|
|
|
`PhiEntryDoesNotExist` in case the entry does not exist.
|
|
|
|
"""
|
2020-11-11 22:25:02 +01:00
|
|
|
await self._delete()
|
|
|
|
|
|
|
|
async def sync(self):
|
2020-12-27 19:48:57 +01:00
|
|
|
"""
|
|
|
|
This method reads the `ldap_attributes` of an existing instance from the ldap
|
|
|
|
database and assigns the values to `self`. It is needed at first instantiation
|
|
|
|
of the object, in case an instance exists on the database.
|
|
|
|
"""
|
2020-11-11 22:25:02 +01:00
|
|
|
res = await self._get()
|
2020-11-27 19:41:09 +01:00
|
|
|
_hydrate(self, res)
|
2020-11-11 22:25:02 +01:00
|
|
|
return self
|
2020-12-27 19:48:57 +01:00
|
|
|
|
|
|
|
async def exists(self):
|
|
|
|
"""
|
|
|
|
This method returns `True` if the instance exists on the ldap database, `False`
|
|
|
|
if it does not. It might raise `PhiUnexpectedRuntimeValue` if the ldap state is
|
|
|
|
inconsistent.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
_ = await self.sync()
|
|
|
|
return True
|
|
|
|
except PhiEntryDoesNotExist:
|
|
|
|
return False
|