# SPDX-FileCopyrightText: 2004-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only
"""|UDM| hook definitions for modifying |LDAP| calls when objects are created, modifier or deleted."""
from __future__ import annotations
import inspect
import os
import sys
import warnings
from types import SimpleNamespace
from typing import TYPE_CHECKING, Any
from univention.admin import localization
if TYPE_CHECKING:
import univention.admin.handlers
AddList = list[tuple[str, list[str]]]
_Mod2 = tuple[str, list[str]]
_Mod3 = tuple[str, list[str], list[str]]
ModList = list[_Mod2 | _Mod3]
from univention.admin.log import log
translation = localization.translation('univention/admin')
_ = translation.translate
[docs]
def import_hook_files() -> None:
"""Load all additional hook files from :file:`.../univention/admin/hooks.d/*.py`"""
for dir_ in sys.path:
hooks_d = os.path.join(dir_, 'univention/admin/hooks.d/')
if os.path.isdir(hooks_d):
hooks_files = (os.path.join(hooks_d, f) for f in os.listdir(hooks_d) if f.endswith('.py'))
for fn in hooks_files:
try:
with open(fn, 'rb') as fd:
env = {
'simpleHook': simpleHook,
'AttributeHook': AttributeHook,
}
exec(fd.read(), env) # noqa: S102
sys.modules[__name__].__dict__.update(
dict(
inspect.getmembers(
SimpleNamespace(**env), lambda m: inspect.isclass(m) and issubclass(m, (simpleHook, AttributeHook)) and m not in (simpleHook, AttributeHook),
),
),
)
log.debug('importing hook', hook=fn)
except Exception:
log.exception('loading hook failed', hook=fn)
[docs]
class simpleHook:
"""Base class for a |UDM| hook performing logging."""
type = 'simpleHook'
#
# To use the LDAP connection of the parent UDM call in any of the following
# methods, use obj.lo and obj.position.
#
[docs]
def hook_open(self, obj: univention.admin.handlers.simpleLdap) -> None:
"""
This method is called by the default open handler just before the current state of all properties is saved.
:param obj: The |UDM| object instance.
"""
log.debug('hook_open called')
[docs]
def hook_ldap_pre_create(self, obj: univention.admin.handlers.simpleLdap) -> None:
"""
This method is called before an |UDM| object is created.
It is called after the module validated all properties but before the add-list is created.
:param obj: The |UDM| object instance.
"""
log.debug('hook_ldap_pre_create called')
[docs]
def hook_ldap_addlist(self, obj: univention.admin.handlers.simpleLdap, al: AddList = []) -> AddList:
"""
This method is called before an |UDM| object is created.
Notice that :py:meth:`hook_ldap_modlist` will also be called next.
:param obj: The |UDM| object instance.
:param al: A list of two-tuples (ldap-attribute-name, list-of-values) which will be used to create the LDAP object.
:returns: The (modified) add-list.
"""
log.debug('hook_ldap_addlist called')
return al
[docs]
def hook_ldap_post_create(self, obj: univention.admin.handlers.simpleLdap) -> None:
"""
This method is called after the object was created in |LDAP|.
:param obj: The |UDM| object instance.
"""
log.debug('hook_ldap_post_create called')
[docs]
def hook_ldap_pre_modify(self, obj: univention.admin.handlers.simpleLdap) -> None:
"""
This method is called before an |UDM| object is modified.
It is called after the module validated all properties but before the modification-list is created.
:param obj: The |UDM| object instance.
"""
log.debug('hook_ldap_pre_modify called')
[docs]
def hook_ldap_modlist(self, obj: univention.admin.handlers.simpleLdap, ml: ModList = []) -> ModList:
"""
This method is called before an |UDM| object is created or modified.
:param obj: The |UDM| object instance.
:param ml: A list of tuples, which are either two-tuples (ldap-attribute-name, list-of-new-values) or three-tuples (ldap-attribute-name, list-of-old-values, list-of-new-values). It will be used to create or modify the |LDAP| object.
:returns: The (modified) modification-list.
"""
log.debug('hook_ldap_modlist called')
return ml
[docs]
def hook_ldap_post_modify(self, obj: univention.admin.handlers.simpleLdap) -> None:
"""
This method is called after the object was modified in |LDAP|.
:param obj: The |UDM| object instance.
"""
log.debug('hook_ldap_post_modify called')
[docs]
def hook_ldap_pre_remove(self, obj: univention.admin.handlers.simpleLdap) -> None:
"""
This method is called before an |UDM| object is removed.
:param obj: The |UDM| object instance.
"""
log.debug('hook_ldap_pre_remove called')
[docs]
def hook_ldap_post_remove(self, obj: univention.admin.handlers.simpleLdap) -> None:
"""
This method is called after the object was removed from |LDAP|.
:param obj: The |UDM| object instance.
"""
log.debug('hook_ldap_post_remove called')
[docs]
class AttributeHook(simpleHook):
"""
Convenience Hook that essentially implements a mapping
between |UDM| and |LDAP| for your extended attributes.
Derive from this class, set :py:attr:`attribute_name` to the name of
the |UDM| attribute and implement :py:meth:`map_attribute_value_to_udm`
and :py:meth:`map_attribute_value_to_ldap`.
.. warning::
Only derive from this class when you are sure
every system in your domain has the update installed that
introduced this hook. (Nov 2018; UCS 4.3-2)
Otherwise you will get errors when you are distributing your new
hook via `ucs_registerLDAPExtension --udm_hook`
"""
udm_attribute_name = None
ldap_attribute_name = None
version = 1 # don't subclass if you don't set version to 2!
[docs]
def hook_open(self, obj: univention.admin.handlers.simpleLdap) -> None:
"""
Open |UDM| object by loading value from |LDAP|.
:param obj: The |UDM| object instance.
"""
log.debug('AttributeHook open: Mapping LDAP -> UDM', ldap=self.ldap_attribute_name, udm=self.udm_attribute_name)
old_value = obj.oldattr.get(self.ldap_attribute_name, [])
if self.version < 2: # TODO: remove in UCS 5.1
warnings.warn('Still using deprecated AttributeHook.version == 1', DeprecationWarning, stacklevel=2)
old_value = obj[self.udm_attribute_name]
new_value = self.map_attribute_value_to_udm(old_value)
log.debug('AttributeHook: Setting UDM value', old=old_value, new=new_value)
obj[self.udm_attribute_name] = new_value
[docs]
def hook_ldap_addlist(self, obj: univention.admin.handlers.simpleLdap, al: AddList) -> AddList:
"""
Extend |LDAP| add list.
:param obj: The |UDM| object instance.
:param al: The add list to extend.
:returns: The extended add list.
"""
return self.hook_ldap_modlist(obj, al)
[docs]
def hook_ldap_modlist(self, obj: univention.admin.handlers.simpleLdap, ml: ModList) -> ModList:
"""
Extend |LDAP| modification list.
:param obj: The |UDM| object instance.
:param ml: The modification list to extend.
:returns: The extended modification list.
"""
if self.version < 2: # TODO: remove in UCS 5.1
warnings.warn('Still using deprecated AttributeHook.version == 1', DeprecationWarning, stacklevel=2)
new_ml = []
for ml_value in ml:
if len(ml_value) == 2:
key, old_value, new_value = ml_value[0], [], ml_value[1]
else:
key, old_value, new_value = ml_value
if key == self.ldap_attribute_name:
log.debug('AttributeHook modlist: Mapping UDM -> LDAP', ldap=self.ldap_attribute_name, udm=self.udm_attribute_name)
old_value = self.map_attribute_value_to_ldap(old_value)
new_new_value = self.map_attribute_value_to_ldap(new_value)
log.debug('AttributeHook: Setting LDAP value', old=new_value, new=new_new_value)
new_value = new_new_value
new_ml.append((key, old_value, new_value))
return new_ml
new_ml = [x for x in ml if x[0] != self.ldap_attribute_name]
if obj.hasChanged(self.udm_attribute_name):
old_value = obj.oldattr.get(self.ldap_attribute_name, [])
new_value = obj.info.get(self.udm_attribute_name)
if new_value is not None:
new_value = self.map_attribute_value_to_ldap(new_value)
new_ml.append((self.ldap_attribute_name, old_value, new_value))
return new_ml
[docs]
def map_attribute_value_to_ldap(self, value: Any) -> list[bytes]:
"""
Return value as it shall be saved in |LDAP|.
:param value: The |UDM| value.
:returns: The |LDAP| value.
"""
return value
[docs]
def map_attribute_value_to_udm(self, value: list[bytes]) -> Any:
"""
Return value as it shall be used in |UDM| objects.
The mapped value needs to be syntax compliant.
:param value: The |LDAP| value.
:returns: The |UDM| value.
"""
return value