Source code for univention.udm.udm

# -*- coding: utf-8 -*-
#
# Copyright 2018-2022 Univention GmbH
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention.
#
# This program is provided in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <https://www.gnu.org/licenses/>.

"""
Univention Directory Manager Modules (UDM) API

This is a simplified API for accessing UDM objects.
It consists of UDM modules and UDM object.
UDM modules are factories for UDM objects.
UDM objects manipulate LDAP objects.

The :py:class:`UDM` class is a LDAP connection and UDM module factory.

Usage::

	from univention.udm import UDM

	user_mod = UDM.admin().version(2).get('users/user')

or::

	user_mod = UDM.machine().version(2).get('users/user')

or::

	user_mod = UDM.credentials('myuser', 's3cr3t').version(2).get('users/user')

	obj = user_mod.get(dn)
	obj.props.firstname = 'foo'  # modify property
	obj.position = 'cn=users,cn=example,dc=com'  # move LDAP object
	obj.save()  # apply changes

	obj = user_mod.get(dn)
	obj.delete()

	obj = user_mod.new()
	obj.props.username = 'bar'
	obj.props.lastname = 'baz'
	obj.props.password = 'v3r7s3cr3t'
	obj.props.unixhome = '/home/bar'
	obj.save()

	for obj in user_mod.search('uid=a*'):  # search() returns a generator
		print(obj.props.firstname, obj.props.lastname)

A shortcut exists to get UDM objects directly, without knowing their
univention object type::

	UDM.admin().version(2).obj_by_dn(dn)

A shortcut exists to get UDM objects directly, knowing their univention object
type, but without knowing their DN::

	UDM.admin().version(2).get('groups/group').get_by_id('Domain Users')

The API is versioned. A fixed version must be hard coded in your code. Supply
it as argument to the UDM module factory or via :py:meth:`version()`::

    UDM(lo, 0)              # use API version 0 and an existing LDAP connection object
	UDM.admin().version(1)  # use API version 1
	UDM.credentials('myuser', 's3cr3t').version(2).obj_by_dn(dn)  # get object using API version 2

* Version 0: values of UDM properties are the same as with the low level UDM API: mostly strings.
* Version 1: values of (most) UDM properties are de/encoded to useful Python types (e.g. "0" -> 0 or False)
* Version 2: an encoder for settings/portal_category properties was added.

The LDAP connection to use must be supplies as an argument to the UDM module factory or set via
:py:meth:`admin()`, :py:meth:`machine()`, or :py:meth:`credentials()`::

    UDM(lo)        # use an already existing uldap connection object
    UDM.admin()    # cn=admin connection
    UDM.machine()  # machine connection
    UDM.credentials(identity, password, base=None, server=None, port=None)  # custom connection,
        # `identity` is either a username or a DN. LDAP base, server FQDN/IP and port are optional.
        # If it is a username, a machine connection is used to retrieve the DN it belongs to.
"""

from __future__ import absolute_import, unicode_literals
from operator import itemgetter
from fnmatch import fnmatch

from .exceptions import ApiVersionMustNotChange, ApiVersionNotSupported, NoApiVersionSet, NoObject
from .plugins import Plugins


_MODULES_PATH = 'univention.udm.modules'


[docs]class UDM(object): """ Dynamic factory for creating :py:class:`BaseModule` objects:: group_mod = UDM.admin().version(2).get('groups/group') folder_mod = UDM.machine().version(2).get('mail/folder') user_mod = UDM.credentials('myuser', 's3cr3t').version(2).get('users/user') A shortcut exists to get UDM objects directly:: UDM.admin().version(2).obj_by_dn(dn) """ _module_object_cache = {} def __init__(self, connection, api_version=None): """ Use the provided connection. :param connection: Any connection object (e.g., univention.admin.uldap.access) :param int api_version: load only UDM modules that support the specified version, can be set later using :py:meth:`version()`. :return: None :rtype: None """ self.connection = connection self._api_version = None if api_version is not None: self.version(api_version)
[docs] @classmethod def admin(cls): """ Use a cn=admin connection. :return: a :py:class:`univention.udm.udm.UDM` instance :rtype: univention.udm.udm.UDM :raises univention.udm.exceptions.ConnectionError: Non-Primary systems, server down, etc. """ from .connections import LDAP_connection connection = LDAP_connection.get_admin_connection() return cls(connection)
[docs] @classmethod def machine(cls): """ Use a machine connection. :return: a :py:class:`univention.udm.udm.UDM` instance :rtype: univention.udm.udm.UDM :raises univention.udm.exceptions.ConnectionError: File permissions, server down, etc. """ from .connections import LDAP_connection connection = LDAP_connection.get_machine_connection() return cls(connection)
[docs] @classmethod def credentials( cls, identity, password, base=None, server=None, port=None, ): """ Use the provided credentials to open an LDAP connection. `identity` must be either a username or a DN. If it is a username, a machine connection is used to retrieve the DN it belongs to. :param str identity: username or user dn to use for LDAP connection :param str password: password of user / DN to use for LDAP connection :param str base: optional search base :param str server: optional LDAP server address as FQDN :param int port: optional LDAP server port :return: a :py:class:`univention.udm.udm.UDM` instance :rtype: univention.udm.udm.UDM :raises univention.udm.exceptions.ConnectionError: Invalid credentials, server down, etc. """ from .connections import LDAP_connection connection = LDAP_connection.get_credentials_connection(identity, password, base, server, port) return cls(connection)
[docs] def version(self, api_version): """ Set the version of the API that the UDM modules must support. Use in a chain of methods to get a UDM module:: UDM.get_admin().version(2).get('groups/group') :param int api_version: load only UDM modules that support the specified version :return: self (the :py:class:`univention.udm.udm.UDM` instance) :rtype: univention.udm.udm.UDM :raises univention.udm.exceptions.ApiVersionMustNotChange: if called twice """ if not isinstance(api_version, int): raise ApiVersionNotSupported("Argument 'api_version' must be an int.", requested_version=api_version) if self._api_version is None: self._api_version = api_version else: raise ApiVersionMustNotChange() return self
[docs] def get(self, name): """ Get an object of :py:class:`BaseModule` (or of a subclass) for UDM module `name`. :param str name: UDM module name (e.g. `users/user`) :return: object of a subclass of :py:class:`BaseModule` :rtype: BaseModule :raises univention.udm.exceptions.ApiVersionNotSupported: if the Python module for `name` could not be loaded :raises univention.udm.exceptions.NoApiVersionSet: if the API version has not been set """ key = (name, self._api_version, id(self.connection)) if key not in self._module_object_cache: suitable_modules = [] plugins = Plugins(_MODULES_PATH) for module in plugins: if self.api_version not in module.meta.supported_api_versions: continue for suitable in module.meta.suitable_for: if fnmatch(name, suitable): suitable_modules.append((suitable.count('*'), module)) break suitable_modules.sort(key=itemgetter(0)) try: klass = suitable_modules[0][1] except IndexError: raise ApiVersionNotSupported(module_name=name, requested_version=self.api_version) else: self._module_object_cache[key] = klass(name, self.connection, self.api_version) return self._module_object_cache[key]
[docs] def obj_by_dn(self, dn): """ Try to load an UDM object from LDAP. Guess the required UDM module from the ``univentionObjectType`` LDAP attribute of the LDAP object. :param str dn: DN of the object to load :return: object of a subclass of :py:class:`BaseObject` :rtype: BaseObject :raises univention.udm.exceptions.NoApiVersionSet: if the API version has not been set :raises univention.udm.exceptions.NoObject: if no object is found at `dn` :raises univention.udm.exceptions.ImportError: if the Python module for ``univentionObjectType`` at ``dn`` could not be loaded :raises univention.udm.exceptions.UnknownModuleType: if the LDAP object at ``dn`` had no or empty attribute ``univentionObjectType`` """ if self.connection.__module__ != 'univention.admin.uldap': raise NotImplementedError('obj_by_dn() can only be used with an LDAP connection.') ldap_obj = self.connection.get(dn, attr=[str('univentionObjectType')]) if not ldap_obj: raise NoObject(dn=dn) uot = ldap_obj['univentionObjectType'][0].decode('utf-8') udm_module = self.get(uot) return udm_module.get(dn)
@property def api_version(self): if self._api_version is None: raise NoApiVersionSet() return self._api_version