From: Frank Brehm Date: Wed, 14 Jun 2023 13:30:41 +0000 (+0200) Subject: Adding bin/clean-empty-ldap-groups and its application module lib/pp_admintools/app... X-Git-Tag: 0.10.0^2~11 X-Git-Url: https://git.uhu-banane.net/?a=commitdiff_plain;h=7f8db5832d5a0917372f5127005a547abf146f3c;p=pixelpark%2Fpp-admin-tools.git Adding bin/clean-empty-ldap-groups and its application module lib/pp_admintools/app/clean_empty_ldap_groups.py --- diff --git a/bin/clean-empty-ldap-groups b/bin/clean-empty-ldap-groups new file mode 100755 index 0000000..0243595 --- /dev/null +++ b/bin/clean-empty-ldap-groups @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +@summary: An application script for removing empty groups from LDAP. + +@author: Frank Brehm +@contact: frank.brehm@pixelpark.com +@copyright: © 2023 by Frank Brehm, Berlin +""" +from __future__ import print_function + +# Standard modules +import locale +import os +import sys + +__exp_py_version_major__ = 3 +__min_py_version_minor__ = 6 + +if sys.version_info[0] != __exp_py_version_major__: + print('This script is intended to use with Python {}.'.format( + __exp_py_version_major__), file=sys.stderr) + print('You are using Python: {0}.{1}.{2}-{3}-{4}.'.format( + *sys.version_info) + '\n', file=sys.stderr) + sys.exit(1) + +if sys.version_info[1] < __min_py_version_minor__: + print('A minimal Python version of {maj}.{min} is necessary to execute this script.'.format( + maj=__exp_py_version_major__, min=__min_py_version_minor__), file=sys.stderr) + print('You are using Python: {0}.{1}.{2}-{3}-{4}.'.format( + *sys.version_info) + '\n', file=sys.stderr) + sys.exit(1) + +# Standard modules +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path + +__author__ = 'Frank Brehm ' +__copyright__ = '(C) 2023 by Frank Brehm, Digitas Pixelpark GmbH, Berlin' + +# own modules: + +my_path = Path(__file__) +my_real_path = my_path.resolve() +bin_path = my_real_path.parent +base_dir = bin_path.parent +lib_dir = base_dir.joinpath('lib') +module_dir = lib_dir.joinpath('pp_admintools') + +if module_dir.exists(): + sys.path.insert(0, str(lib_dir)) + +from pp_admintools.app.clean_empty_ldap_groups import CleanEmptyLdapGroupsApplication + +appname = os.path.basename(sys.argv[0]) + +locale.setlocale(locale.LC_ALL, '') + +app = CleanEmptyLdapGroupsApplication(appname=appname, base_dir=base_dir) +app.initialized = True + +if app.verbose > 2: + print('{c}-Object:\n{a}'.format(c=app.__class__.__name__, a=app)) + +app() + +sys.exit(0) + +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 diff --git a/lib/pp_admintools/app/clean_empty_ldap_groups.py b/lib/pp_admintools/app/clean_empty_ldap_groups.py new file mode 100644 index 0000000..46faa10 --- /dev/null +++ b/lib/pp_admintools/app/clean_empty_ldap_groups.py @@ -0,0 +1,203 @@ +# -*- coding: utf-8 -*- +""" +@summary: An application module for removing empty groups from LDAP. + +@author: Frank Brehm +@contact: frank.brehm@pixelpark.com +@copyright: © 2023 by Frank Brehm, Berlin +""" +from __future__ import absolute_import + +# Standard modules +import copy +import logging + +# Third party modules +from fb_tools.collections import CIStringSet +from fb_tools.common import is_sequence, to_bool +from fb_tools.xlate import format_list + +from ldap3 import MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE + +# Own modules +from .ldap import BaseLdapApplication +from .ldap import FatalLDAPError, LdapAppError +from .. import pp +from ..argparse_actions import LimitedIntegerOptionAction +from ..xlate import XLATOR + +__version__ = '0.1.0' +LOG = logging.getLogger(__name__) + +_ = XLATOR.gettext +ngettext = XLATOR.ngettext + + +# ============================================================================= +class CleanEmptyLdapGroupsUserError(LdapAppError): + """Special exception class for exceptions inside this module.""" + + pass + + +# ============================================================================= +class CleanEmptyLdapGroupsApplication(BaseLdapApplication): + """Application class for removing empty groups from LDAP.""" + + show_simulate_option = True + show_quiet_option = False + + default_max_cycles = 20 + max_max_cycles = 1000 + + group_object_classes = ('posixGroup', 'groupOfNames', 'uniqueMember') + member_attributes = ('member', 'uniqueMember', 'memberUid', 'mgrpRFC822MailMember') + + # ------------------------------------------------------------------------- + def __init__(self, appname=None, base_dir=None): + """Initialize the CleanEmptyLdapGroupsApplication object.""" + self.use_default_ldap_connection = False + self.use_multiple_ldap_connections = True + self.show_cmdline_ldap_timeout = True + + self.dns_todo = CIStringSet() + self.dns_done = CIStringSet() + + self.current_cycle = 0 + self.last_nr_groups_done = 0 + self._max_cycles = self.default_max_cycles + + desc = _( + 'Removes all LDAP groups, which does not have any members, that means, they are one ' + 'of the following objectClasses:') + desc += ' ' + format_list(self.group_object_classes) + ', ' + desc += _('and they have none of the following attributes:') + desc += ' ' + format_list(self.member_attributes, do_repr=True) + + super(CleanEmptyLdapGroupsApplication, self).__init__( + appname=appname, description=desc, base_dir=base_dir, initialized=False) + + self.initialized = True + + # ------------------------------------------- + @property + def max_cycles(self): + """Define, that the given users will not be removed, bur deactivated instaed.""" + return self._max_cycles + + @max_cycles.setter + def max_cycles(self, value): + if value is None: + self._max_cycles = self.default_max_cycles + return + try: + v = int(value) + if v <= 0 or v > 1000: + msg = _( + 'The maximum number of cycles must not less or equal to zero and mast not be ' + 'greater than {}.').format(self.max_max_cycles) + raise ValueError(msg) + except Exception as e: + msg = _('Got a {} for setting the maximum cycles:').format(e.__class__.__name__) + msg += ' ' + str(e) + raise CleanEmptyLdapGroupsUserError(msg) + + # ------------------------------------------------------------------------- + def as_dict(self, short=True): + """ + Transform the elements of the object into a dict. + + @param short: don't include local properties in resulting dict. + @type short: bool + + @return: structure as dict + @rtype: dict + """ + res = super(CleanEmptyLdapGroupsApplication, self).as_dict(short=short) + + res['default_max_cycles'] = self.default_max_cycles + res['group_object_classes'] = copy.copy(self.group_object_classes) + res['max_cycles'] = self.max_cycles + res['max_max_cycles'] = self.max_max_cycles + res['member_attributes'] = copy.copy(self.member_attributes) + + return res + + # ------------------------------------------------------------------------- + def init_arg_parser(self): + """Initialize specific command line parameters for this application.""" + remove_group = self.arg_parser.add_argument_group(_('Removing options')) + + cycles_help = _( + 'The maximum number of iteration cycles for searching for empty groups. ' + 'It must not be less or equal to zero and must not be greater than {}.') + cycles_help = cycles_help.format(self.max_max_cycles) + cycles_help += ' ' + _('Default: {}.').format(self.default_max_cycles) + + remove_group.add_argument( + '--cycles', dest='cycles', metavar=_('COUNT'), min_val=1, max_val=self.max_max_cycles, + action=LimitedIntegerOptionAction, help=cycles_help) + + super(CleanEmptyLdapGroupsApplication, self).init_arg_parser() + + # ------------------------------------------------------------------------- + def _verify_instances(self): + + super(CleanEmptyLdapGroupsApplication, self)._verify_instances( + is_admin=True, readonly=False) + + # ------------------------------------------------------------------------- + def post_init(self): + """Execute some steps before calling run().""" + super(CleanEmptyLdapGroupsApplication, self).post_init() + + cycles = getattr(self.args, 'cycles', None) + if cycles: + self.max_cycles = cycles + + self.check_instances() + + # ------------------------------------------------------------------------- + def check_instances(self): + """Check given instances for admin and read/write access.""" + msg = _('Checking given instances for admin and read/write access.') + LOG.debug(msg) + + all_ok = True + + for inst_name in self.ldap_instances: + if inst_name not in self.cfg.ldap_connection: + msg = _('LDAP instance {!r} not found in configuration.').format(inst_name) + LOG.error(msg) + all_ok = False + continue + + inst = self.cfg.ldap_connection[inst_name] + + if inst.readonly: + msg = _('LDAP instance {!r} has only readonly access.').format(inst_name) + LOG.error(msg) + all_ok = False + + if not inst.is_admin: + msg = _('No admin access to LDAP instance {!r}.').format(inst_name) + LOG.error(msg) + all_ok = False + + if not all_ok: + self.exit(8) + + # ------------------------------------------------------------------------- + def _run(self): + + LOG.debug(_('Searching for empty groups ...')) + + +# ============================================================================= +if __name__ == '__main__': + + pass + +# ============================================================================= + +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list