From a69f48553731929bd62f4c699371277f85c0d5c3 Mon Sep 17 00:00:00 2001 From: Ilias Stamatis Date: Tue, 15 Aug 2017 20:48:57 +0300 Subject: [PATCH] Issue 43 - Add support for Referential Integrity plugin Description: Add dsconf support for configuring the Referential Integrity plugin from the command line, along with functional tests for testing its functionality. https://pagure.io/lib389/issue/43 Author: Ilias95 Review by: ??? --- cli/dsconf | 2 + lib389/cli_conf/plugins/referint.py | 197 +++++++++++++++++++++++++ lib389/plugins.py | 104 ++++++++++++- lib389/tests/cli/conf_plugins/referint_test.py | 119 +++++++++++++++ lib389/tests/plugins/referint_test.py | 83 +++++++++++ 5 files changed, 498 insertions(+), 7 deletions(-) create mode 100644 lib389/cli_conf/plugins/referint.py create mode 100644 lib389/tests/cli/conf_plugins/referint_test.py create mode 100644 lib389/tests/plugins/referint_test.py diff --git a/cli/dsconf b/cli/dsconf index 765b4e5..0f8c09b 100755 --- a/cli/dsconf +++ b/cli/dsconf @@ -25,6 +25,7 @@ from lib389.cli_conf import health as cli_health from lib389.cli_conf.plugins import memberof as cli_memberof from lib389.cli_conf.plugins import usn as cli_usn from lib389.cli_conf.plugins import whoami as cli_whoami +from lib389.cli_conf.plugins import referint as cli_referint from lib389.cli_base import disconnect_instance, connect_instance from lib389.cli_base.dsrc import dsrc_to_ldap, dsrc_arg_concat @@ -69,6 +70,7 @@ if __name__ == '__main__': cli_memberof.create_parser(subparsers) cli_usn.create_parser(subparsers) cli_whoami.create_parser(subparsers) + cli_referint.create_parser(subparsers) args = parser.parse_args() diff --git a/lib389/cli_conf/plugins/referint.py b/lib389/cli_conf/plugins/referint.py new file mode 100644 index 0000000..9d52b89 --- /dev/null +++ b/lib389/cli_conf/plugins/referint.py @@ -0,0 +1,197 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2016-2017 Red Hat, Inc. +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- + +import ldap + +from lib389.plugins import ReferentialIntegrityPlugin +from lib389.cli_conf.plugin import add_generic_plugin_parsers + + +def manage_update_delay(inst, basedn, log, args): + plugin = ReferentialIntegrityPlugin(inst) + if args.value is None: + val = plugin.get_update_delay_formatted() + log.info(val) + else: + plugin.set_update_delay(args.value) + log.info('referint-update-delay set to "{}"'.format(args.value)) + +def display_membership_attr(inst, basedn, log, args): + plugin = ReferentialIntegrityPlugin(inst) + log.info(plugin.get_membership_attr_formatted()) + +def add_membership_attr(inst, basedn, log, args): + plugin = ReferentialIntegrityPlugin(inst) + try: + plugin.add_membership_attr(args.value) + except ldap.TYPE_OR_VALUE_EXISTS: + log.info('Value "{}" already exists.'.format(args.value)) + else: + log.info('successfully added membership attribute "{}"'.format(args.value)) + +def remove_membership_attr(inst, basedn, log, args): + plugin = ReferentialIntegrityPlugin(inst) + try: + plugin.remove_membership_attr(args.value) + except ldap.OPERATIONS_ERROR: + log.error("Error: Failed to delete. At least one value for membership attribute should exist.") + except ldap.NO_SUCH_ATTRIBUTE: + log.error('Error: Failed to delete. No value "{0}" found.'.format(args.value)) + else: + log.info('successfully removed membership attribute "{}"'.format(args.value)) + +def display_scope(inst, basedn, log, args): + plugin = ReferentialIntegrityPlugin(inst) + val = plugin.get_entryscope_formatted() + if not val: + log.info("nsslapd-pluginEntryScope is not set") + else: + log.info(val) + +def add_scope(inst, basedn, log, args): + plugin = ReferentialIntegrityPlugin(inst) + try: + plugin.add_entryscope(args.value) + except ldap.TYPE_OR_VALUE_EXISTS: + log.info('Value "{}" already exists.'.format(args.value)) + else: + log.info('successfully added nsslapd-pluginEntryScope value "{}"'.format(args.value)) + +def remove_scope(inst, basedn, log, args): + plugin = ReferentialIntegrityPlugin(inst) + try: + plugin.remove_entryscope(args.value) + except ldap.NO_SUCH_ATTRIBUTE: + log.error('Error: Failed to delete. No value "{0}" found.'.format(args.value)) + else: + log.info('successfully removed nsslapd-pluginEntryScope value "{}"'.format(args.value)) + +def remove_all_scope(inst, basedn, log, args): + plugin = ReferentialIntegrityPlugin(inst) + plugin.remove_all_entryscope() + log.info('successfully removed all nsslapd-pluginEntryScope values') + +def display_excludescope(inst, basedn, log, args): + plugin = ReferentialIntegrityPlugin(inst) + val = plugin.get_excludescope_formatted() + if not val: + log.info("nsslapd-pluginExcludeEntryScope is not set") + else: + log.info(val) + +def add_excludescope(inst, basedn, log, args): + plugin = ReferentialIntegrityPlugin(inst) + try: + plugin.add_excludescope(args.value) + except ldap.TYPE_OR_VALUE_EXISTS: + log.info('Value "{}" already exists.'.format(args.value)) + else: + log.info('successfully added nsslapd-pluginExcludeEntryScope value "{}"'.format(args.value)) + +def remove_excludescope(inst, basedn, log, args): + plugin = ReferentialIntegrityPlugin(inst) + try: + plugin.remove_excludescope(args.value) + except ldap.NO_SUCH_ATTRIBUTE: + log.error('Error: Failed to delete. No value "{0}" found.'.format(args.value)) + else: + log.info('successfully removed nsslapd-pluginExcludeEntryScope value "{}"'.format(args.value)) + +def remove_all_excludescope(inst, basedn, log, args): + plugin = ReferentialIntegrityPlugin(inst) + plugin.remove_all_excludescope() + log.info('successfully removed all nsslapd-pluginExcludeEntryScope values') + +def display_container_scope(inst, basedn, log, args): + plugin = ReferentialIntegrityPlugin(inst) + val = plugin.get_container_scope_formatted() + if not val: + log.info("nsslapd-pluginContainerScope is not set") + else: + log.info(val) + +def add_container_scope(inst, basedn, log, args): + plugin = ReferentialIntegrityPlugin(inst) + try: + plugin.add_container_scope(args.value) + except ldap.TYPE_OR_VALUE_EXISTS: + log.info('Value "{}" already exists.'.format(args.value)) + else: + log.info('successfully added nsslapd-pluginContainerScope value "{}"'.format(args.value)) + +def remove_container_scope(inst, basedn, log, args): + plugin = ReferentialIntegrityPlugin(inst) + try: + plugin.remove_container_scope(args.value) + except ldap.NO_SUCH_ATTRIBUTE: + log.error('Error: Failed to delete. No value "{0}" found.'.format(args.value)) + else: + log.info('successfully removed nsslapd-pluginContainerScope value "{}"'.format(args.value)) + +def remove_all_container_scope(inst, basedn, log, args): + plugin = ReferentialIntegrityPlugin(inst) + plugin.remove_all_container_scope() + log.info('successfully removed all nsslapd-pluginContainerScope values') + + +def create_parser(subparsers): + referint_parser = subparsers.add_parser('referint', help='Manage and configure Referential Integrity plugin') + + subcommands = referint_parser.add_subparsers(help='action') + + add_generic_plugin_parsers(subcommands, ReferentialIntegrityPlugin) + + delay_parser = subcommands.add_parser('delay', help='get or set update delay') + delay_parser.set_defaults(func=manage_update_delay) + delay_parser.add_argument('value', nargs='?', help='The value to set as update delay') + + attr_parser = subcommands.add_parser('attrs', help='get or manage membership attributes') + attr_parser.set_defaults(func=display_membership_attr) + attr_subcommands = attr_parser.add_subparsers(help='action') + add_attr_parser = attr_subcommands.add_parser('add', help='add membership attribute') + add_attr_parser.set_defaults(func=add_membership_attr) + add_attr_parser.add_argument('value', help='membership attribute to add') + del_attr_parser = attr_subcommands.add_parser('del', help='remove membership attribute') + del_attr_parser.set_defaults(func=remove_membership_attr) + del_attr_parser.add_argument('value', help='membership attribute to remove') + + scope_parser = subcommands.add_parser('scope', help='get or manage referint scope') + scope_parser.set_defaults(func=display_scope) + scope_subcommands = scope_parser.add_subparsers(help='action') + add_scope_parser = scope_subcommands.add_parser('add', help='add entry scope value') + add_scope_parser.set_defaults(func=add_scope) + add_scope_parser.add_argument('value', help='The value to add in referint entry scope') + del_scope_parser = scope_subcommands.add_parser('del', help='remove entry scope value') + del_scope_parser.set_defaults(func=remove_scope) + del_scope_parser.add_argument('value', help='The value to remove from entry scope') + delall_scope_parser = scope_subcommands.add_parser('delall', help='remove all entry scope values') + delall_scope_parser.set_defaults(func=remove_all_scope) + + exclude_parser = subcommands.add_parser('exclude', help='get or manage referint exclude scope') + exclude_parser.set_defaults(func=display_excludescope) + exclude_subcommands = exclude_parser.add_subparsers(help='action') + add_exclude_parser = exclude_subcommands.add_parser('add', help='add exclude scope value') + add_exclude_parser.set_defaults(func=add_excludescope) + add_exclude_parser.add_argument('value', help='The value to add in exclude scope') + del_exclude_parser = exclude_subcommands.add_parser('del', help='remove exclude scope value') + del_exclude_parser.set_defaults(func=remove_excludescope) + del_exclude_parser.add_argument('value', help='The value to remove from exclude scope') + delall_exclude_parser = exclude_subcommands.add_parser('delall', help='remove all exclude scope values') + delall_exclude_parser.set_defaults(func=remove_all_excludescope) + + container_parser = subcommands.add_parser('container', help='get or manage referint container scope') + container_parser.set_defaults(func=display_container_scope) + container_subcommands = container_parser.add_subparsers(help='action') + add_container_parser = container_subcommands.add_parser('add', help='add container scope value') + add_container_parser.set_defaults(func=add_container_scope) + add_container_parser.add_argument('value', help='The value to add in container scope') + del_container_parser = container_subcommands.add_parser('del', help='remove container scope value') + del_container_parser.set_defaults(func=remove_container_scope) + del_container_parser.add_argument('value', help='The value to remove from container scope') + delall_container_parser = container_subcommands.add_parser('delall', help='remove all container scope values') + delall_container_parser.set_defaults(func=remove_all_container_scope) diff --git a/lib389/plugins.py b/lib389/plugins.py index 6bbf164..89ee30c 100644 --- a/lib389/plugins.py +++ b/lib389/plugins.py @@ -8,6 +8,7 @@ import ldap import copy +import os.path from lib389 import tasks from lib389._mapped_object import DSLdapObjects, DSLdapObject @@ -117,16 +118,105 @@ class ManagedEntriesPlugin(Plugin): # Because there are potentially many MEP configs. class ReferentialIntegrityPlugin(Plugin): + _plugin_properties = { + 'cn' : 'referential integrity postoperation', + 'nsslapd-pluginEnabled': 'off', + 'nsslapd-pluginPath': 'libreferint-plugin', + 'nsslapd-pluginInitfunc': 'referint_postop_init', + 'nsslapd-pluginType': 'betxnpostoperation', + 'nsslapd-pluginprecedence': '40', + 'nsslapd-plugin-depends-on-type': 'database', + 'referint-update-delay': '0', + 'referint-membership-attr': ['member', 'uniquemember', 'owner', 'seeAlso',], + 'nsslapd-pluginId' : 'referint', + 'nsslapd-pluginVendor' : '389 Project', + 'nsslapd-pluginVersion' : '1.3.7.0', + 'nsslapd-pluginDescription' : 'referential integrity plugin', + } + def __init__(self, instance, dn="cn=referential integrity postoperation,cn=plugins,cn=config", batch=False): super(ReferentialIntegrityPlugin, self).__init__(instance, dn, batch) + self._create_objectclasses.extend(['extensibleObject']) + self._must_attributes.extend([ + 'referint-update-delay', + 'referint-logfile', + 'referint-membership-attr', + ]) + + def create(self, rdn=None, properties=None, basedn=None): + referint_log = os.path.join(self._instance.ds_paths.log_dir, "referint") + if properties is None: + properties = {'referint-logfile': referint_log} + else: + properties['referint-logfile'] = referint_log + return super(ReferentialIntegrityPlugin, self).create(rdn, properties, basedn) + + def get_update_delay(self): + return self.get_attr_val_int('referint-update-delay') + + def get_update_delay_formatted(self): + return self.display_attr('referint-update-delay') + + def set_update_delay(self, value): + self.set('referint-update-delay', str(value)) + + def get_membership_attr(self, formatted=False): + return self.get_attr_vals_utf8('referint-membership-attr') + + def get_membership_attr_formatted(self): + return self.display_attr('referint-membership-attr') + + def add_membership_attr(self, attr): + self.add('referint-membership-attr', attr) + + def remove_membership_attr(self, attr): + self.remove('referint-membership-attr', attr) + + def get_entryscope(self, formatted=False): + return self.get_attr_vals_utf8('nsslapd-pluginentryscope') + + def get_entryscope_formatted(self): + return self.display_attr('nsslapd-pluginentryscope') + + def add_entryscope(self, attr): + self.add('nsslapd-pluginentryscope', attr) + + def remove_entryscope(self, attr): + self.remove('nsslapd-pluginentryscope', attr) + + def remove_all_entryscope(self): + self.remove_all('nsslapd-pluginentryscope') + + def get_excludescope(self): + return self.get_attr_vals_ut8('nsslapd-pluginexcludeentryscope') + + def get_excludescope_formatted(self): + return self.display_attr('nsslapd-pluginexcludeentryscope') + + def add_excludescope(self, attr): + self.add('nsslapd-pluginexcludeentryscope', attr) + + def remove_excludescope(self, attr): + self.remove('nsslapd-pluginexcludeentryscope', attr) + + def remove_all_excludescope(self): + self.remove_all('nsslapd-pluginexcludeentryscope') + + def get_container_scope(self): + return self.get_attr_vals_ut8('nsslapd-plugincontainerscope') + + def get_container_scope_formatted(self): + return self.display_attr('nsslapd-plugincontainerscope') + + def add_container_scope(self, attr): + self.add('nsslapd-plugincontainerscope', attr) + + def remove_container_scope(self, attr): + self.remove('nsslapd-plugincontainerscope', attr) + + def remove_all_container_scope(self): + self.remove_all('nsslapd-plugincontainerscope') - # referint-update-delay: 0 - # referint-logfile: /opt/dirsrv/var/log/dirsrv/slapd-standalone_2/referint - # referint-logchanges: 0 - # referint-membership-attr: member - # referint-membership-attr: uniquemember - # referint-membership-attr: owner - # referint-membership-attr: seeAlso class SyntaxValidationPlugin(Plugin): def __init__(self, instance, dn="cn=Syntax Validation Task,cn=plugins,cn=config", batch=False): diff --git a/lib389/tests/cli/conf_plugins/referint_test.py b/lib389/tests/cli/conf_plugins/referint_test.py new file mode 100644 index 0000000..8a31ffc --- /dev/null +++ b/lib389/tests/cli/conf_plugins/referint_test.py @@ -0,0 +1,119 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2016-2017 Red Hat, Inc. +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- + +import pytest + +from lib389.tests.cli import topology as default_topology +from lib389.cli_base import LogCapture, FakeArgs +from lib389.plugins import ReferentialIntegrityPlugin +from lib389.cli_conf.plugins import referint as referint_cli + + +@pytest.fixture(scope="module") +def topology(request): + topology = default_topology(request) + + plugin = ReferentialIntegrityPlugin(topology.standalone) + if not plugin.exists(): + plugin.create() + + # we need to restart the server after enabling the plugin + plugin.enable() + topology.standalone.restart() + topology.logcap.flush() + + return topology + + +def test_set_update_delay(topology): + args = FakeArgs() + + args.value = 60 + referint_cli.manage_update_delay(topology.standalone, None, topology.logcap.log, args) + assert topology.logcap.contains('referint-update-delay set to "60"') + topology.logcap.flush() + + args.value = None + referint_cli.manage_update_delay(topology.standalone, None, topology.logcap.log, args) + assert topology.logcap.contains("referint-update-delay: 60") + topology.logcap.flush() + + args.value = 0 + referint_cli.manage_update_delay(topology.standalone, None, topology.logcap.log, args) + assert topology.logcap.contains('referint-update-delay set to "0"') + topology.logcap.flush() + + args.value = None + referint_cli.manage_update_delay(topology.standalone, None, topology.logcap.log, args) + assert topology.logcap.contains("referint-update-delay: 0") + topology.logcap.flush() + +def test_add_membership_attr(topology): + args = FakeArgs() + + args.value = "member2" + referint_cli.add_membership_attr(topology.standalone, None, topology.logcap.log, args) + assert topology.logcap.contains("successfully added membership attribute") + topology.logcap.flush() + + referint_cli.display_membership_attr(topology.standalone, None, topology.logcap.log, args) + assert topology.logcap.contains(": member2") + topology.logcap.flush() + +def test_add_membership_attr_with_value_that_already_exists(topology): + plugin = ReferentialIntegrityPlugin(topology.standalone) + # setup test + if not "uniqueMember" in plugin.get_membership_attr(): + plugin.add_membership_attr("uniqueMember") + + args = FakeArgs() + + args.value = "uniqueMember" + referint_cli.add_membership_attr(topology.standalone, None, topology.logcap.log, args) + assert topology.logcap.contains("already exists") + topology.logcap.flush() + +def test_remove_membership_attr_with_value_that_exists(topology): + plugin = ReferentialIntegrityPlugin(topology.standalone) + # setup test + if not "uniqueMember" in plugin.get_membership_attr(): + plugin.add_membership_attr("uniqueMember") + + args = FakeArgs() + + args.value = "uniqueMember" + referint_cli.remove_membership_attr(topology.standalone, None, topology.logcap.log, args) + assert topology.logcap.contains("successfully removed membership attribute") + topology.logcap.flush() + + referint_cli.display_membership_attr(topology.standalone, None, topology.logcap.log, args) + assert not topology.logcap.contains(": uniqueMember") + topology.logcap.flush() + +def test_remove_membership_attr_with_value_that_doesnt_exist(topology): + args = FakeArgs() + + args.value = "whatever" + referint_cli.remove_membership_attr(topology.standalone, None, topology.logcap.log, args) + assert topology.logcap.contains('No value "{0}" found'.format(args.value)) + topology.logcap.flush() + +def test_try_remove_all_membership_attr_values(topology): + plugin = ReferentialIntegrityPlugin(topology.standalone) + #setup test + membership_values = plugin.get_membership_attr() + assert len(membership_values) > 0 + for val in membership_values[:-1]: + plugin.remove_membership_attr(val) + + args = FakeArgs() + + args.value = membership_values[-1] + referint_cli.remove_membership_attr(topology.standalone, None, topology.logcap.log, args) + assert topology.logcap.contains("Error: Failed to delete. At least one value for membership attribute should exist.") + topology.logcap.flush() diff --git a/lib389/tests/plugins/referint_test.py b/lib389/tests/plugins/referint_test.py new file mode 100644 index 0000000..53500d9 --- /dev/null +++ b/lib389/tests/plugins/referint_test.py @@ -0,0 +1,83 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2016-2017 Red Hat, Inc. +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- +# + +import pytest + +from lib389.topologies import topology_st +from lib389.plugins import ReferentialIntegrityPlugin +from lib389.tests.plugins.utils import ( + create_test_user, create_test_group, delete_objects) + + +@pytest.fixture(scope="module") +def plugin(request): + return ReferentialIntegrityPlugin(topology_st(request).standalone) + + +def test_referint_enable_disable(plugin): + """ + Test that the plugin doesn't do anything while disabled, and functions + properly when enabled. + + NOTICE: This test case leaves the plugin enabled for the following tests. + """ + # assert plugin is disabled (by default) + assert plugin.status() == False + + user1 = create_test_user(plugin._instance) + group = create_test_group(plugin._instance) + group.add_member(user1.dn) + + user1.delete() + # assert that user was not removed from group because the plugin is disabled + assert group.present(attr='member', value=user1.dn) + + # enable the plugin and restart the server for the action to take effect + plugin.enable() + plugin._instance.restart() + assert plugin.status() == True + + user2 = create_test_user(plugin._instance) + group.add_member(user2.dn) + + user2.delete() + # assert that user was removed from the group as well + assert not group.present(attr='member', value=user2.dn) + + # clean up for subsequent test cases + delete_objects([group]) + +def test_membership_attr(plugin): + """ + Test that the plugin performs integrity updates based on the attributes + defined by referint-membership-attr. + """ + # remove a membership attribute + plugin.remove_membership_attr('uniquemember') + + user1 = create_test_user(plugin._instance) + group = create_test_group(plugin._instance, unique_group=True) + group.add_member(user1.dn) + + user1.delete() + # assert that the user was not removed from the group + assert group.present(attr='uniquemember', value=user1.dn) + + # now put this membership attribute back and try again + plugin.add_membership_attr('uniquemember') + + user2 = create_test_user(plugin._instance) + group.add_member(user2.dn) + + user2.delete() + # assert that user was removed from the group as well + assert not group.present(attr='uniquemember', value=user2.dn) + + # clean up for subsequent test cases + delete_objects([group]) -- 2.13.4