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 (
|
|
|
|
PhiAttributeMissing,
|
|
|
|
PhiEntryDoesNotExist,
|
|
|
|
PhiEntryExists,
|
|
|
|
PhiUnauthorized,
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
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)}"
|
|
|
|
if name not in cls._instances:
|
|
|
|
cls._instances[name] = object.__new__(cls)
|
|
|
|
return cls._instances[name]
|
|
|
|
|
|
|
|
|
2020-11-11 22:25:02 +01:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2020-08-31 11:01:46 +02:00
|
|
|
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 get_all_ldap_attributes(self):
|
2020-11-11 22:25:02 +01:00
|
|
|
return [get_value(self, attr) for attr in self.ldap_attributes]
|
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}>"
|
|
|
|
|
|
|
|
def __iter__(self):
|
2020-11-11 22:25:02 +01:00
|
|
|
for k, v in self.get_all_ldap_attributes():
|
|
|
|
yield k, v
|
2020-08-31 11:01:46 +02:00
|
|
|
|
|
|
|
def __dict__(self):
|
|
|
|
return dict(self)
|
|
|
|
|
|
|
|
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:
|
|
|
|
# the one we are interested.
|
|
|
|
_res = await conn.search(self.dn, 0)
|
|
|
|
if len(_res) == 0:
|
2020-11-11 22:25:02 +01:00
|
|
|
raise PhiEntryDoesNotExist()
|
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:
|
|
|
|
await conn.delete(self.dn)
|
|
|
|
|
2020-08-31 11:01:46 +02:00
|
|
|
async def describe(self):
|
2020-11-11 22:25:02 +01:00
|
|
|
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
|
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():
|
|
|
|
self._entry[k] = v
|
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):
|
|
|
|
try:
|
|
|
|
await self._create_new()
|
|
|
|
except bonsai.errors.AlreadyExists:
|
|
|
|
raise PhiEntryExists(self.dn)
|
2020-08-31 11:01:46 +02:00
|
|
|
|
2020-11-11 22:25:02 +01:00
|
|
|
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])
|
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-11-11 22:25:02 +01:00
|
|
|
def __init__(self, client, name, **kwargs):
|
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-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):
|
|
|
|
return str(other) == self.dn
|
|
|
|
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(
|
|
|
|
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()
|
2020-11-27 19:41:09 +01:00
|
|
|
_hydrate(self, res)
|
2020-11-11 22:25:02 +01:00
|
|
|
return self
|