From af8cda081319336a49a3993a90fb1c692f79fccc Mon Sep 17 00:00:00 2001 From: William Brown Date: Jan 23 2018 01:24:10 +0000 Subject: Ticket 49544 - cli release preperation, group improvements Bug Description: Improvements to the cli getting ready for user testing. The group handling did not have tests nor the ability to add or remove members. Fix Description: This adds support for adding and removing users to groups, as well as testing that. To improve the tests, duplicate code from topology was removed in the cli side, and placed into topologies.py https://pagure.io/389-ds-base/issue/49544 Author: wibrown Review by: mreynolds, spichugi (Thanks!) --- diff --git a/src/lib389/lib389/__init__.py b/src/lib389/lib389/__init__.py index 0514fb7..5509c5a 100644 --- a/src/lib389/lib389/__init__.py +++ b/src/lib389/lib389/__init__.py @@ -541,7 +541,7 @@ class DirSrv(SimpleLDAPObject, object): self.binddn = args.get(SER_ROOT_DN, DN_DM) self.bindpw = args.get(SER_ROOT_PW, PW_DM) - self.creation_suffix = args.get(SER_CREATION_SUFFIX, DEFAULT_SUFFIX) + self.creation_suffix = args.get(SER_CREATION_SUFFIX, None) # These settings are only needed on a local connection. if self.isLocal: self.userid = args.get(SER_USER_ID) @@ -913,12 +913,14 @@ class DirSrv(SimpleLDAPObject, object): slapd = slapd_options.collect() # In order to work by "default" for tests, we need to create a backend. - userroot = { - 'cn': 'userRoot', - 'nsslapd-suffix': self.creation_suffix, - BACKEND_SAMPLE_ENTRIES: version, - } - backends = [userroot,] + backends = [] + if self.creation_suffix is not None: + userroot = { + 'cn': 'userRoot', + 'nsslapd-suffix': self.creation_suffix, + BACKEND_SAMPLE_ENTRIES: version, + } + backends = [userroot,] # Go! sds.create_from_args(general, slapd, backends, None) diff --git a/src/lib389/lib389/cli_base/__init__.py b/src/lib389/lib389/cli_base/__init__.py index 4f5e7ad..2736305 100644 --- a/src/lib389/lib389/cli_base/__init__.py +++ b/src/lib389/lib389/cli_base/__init__.py @@ -177,3 +177,42 @@ class FakeArgs(object): def __len__(self): return len(self.__dict__.keys()) +log_simple_handler = logging.StreamHandler() +log_simple_handler.setFormatter( + logging.Formatter('%(message)s') +) + +log_verbose_handler = logging.StreamHandler() +log_verbose_handler.setFormatter( + logging.Formatter('%(levelname)s: %(message)s') +) + +def reset_get_logger(name, verbose=False): + """Reset the python logging system for STDOUT, and attach a new + console logger with cli expected formatting. + + :param name: Name of the logger + :type name: str + :param verbose: Enable verbose format of messages + :type verbose: bool + :return: logging.logger + """ + root = logging.getLogger() + if root.handlers: + for handler in root.handlers: + root.removeHandler(handler) + + if verbose: + root.addHandler(log_verbose_handler) + else: + root.addHandler(log_simple_handler) + + log = logging.getLogger(name) + + if verbose: + log.setLevel(logging.DEBUG) + else: + log.setLevel(logging.INFO) + + return log + diff --git a/src/lib389/lib389/cli_idm/group.py b/src/lib389/lib389/cli_idm/group.py index a64eb86..de99385 100644 --- a/src/lib389/lib389/cli_idm/group.py +++ b/src/lib389/lib389/cli_idm/group.py @@ -36,18 +36,47 @@ def get(inst, basedn, log, args): _generic_get(inst, basedn, log.getChild('_generic_get'), MANY, rdn) def get_dn(inst, basedn, log, args): - dn = lambda args: _get_arg( args.dn, msg="Enter dn to retrieve") + dn = _get_arg( args.dn, msg="Enter dn to retrieve") _generic_get_dn(inst, basedn, log.getChild('_generic_get_dn'), MANY, dn) def create(inst, basedn, log, args): kwargs = _get_attributes(args, MUST_ATTRIBUTES) _generic_create(inst, basedn, log.getChild('_generic_create'), MANY, kwargs) -def delete(inst, basedn, log, args): - dn = _get_arg( args, msg="Enter dn to delete") - _warn(dn, msg="Deleting %s %s" % (SINGULAR.__name__, dn)) +def delete(inst, basedn, log, args, warn=True): + dn = _get_arg( args.dn , msg="Enter dn to delete") + if warn: + _warn(dn, msg="Deleting %s %s" % (SINGULAR.__name__, dn)) _generic_delete(inst, basedn, log.getChild('_generic_delete'), SINGULAR, dn) +def members(inst, basedn, log, args): + cn = _get_arg( args.cn, msg="Enter %s of group" % RDN) + groups = MANY(inst, basedn) + group = groups.get(cn) + # Display members? + member_list = group.list_members() + if len(member_list) == 0: + log.info('No members to display') + else: + for m in member_list: + log.info('dn: %s' % m) + +def add_member(inst, basedn, log, args): + cn = _get_arg( args.cn, msg="Enter %s of group to add member too" % RDN) + dn = _get_arg( args.dn, msg="Enter dn to add as member") + groups = MANY(inst, basedn) + group = groups.get(cn) + group.add_member(dn) + log.info('added member: %s' % dn) + +def remove_member(inst, basedn, log, args): + cn = _get_arg( args.cn, msg="Enter %s of group to remove member from" % RDN) + dn = _get_arg( args.dn, msg="Enter dn to remove as member") + groups = MANY(inst, basedn) + group = groups.get(cn) + group.remove_member(dn) + log.info('removed member: %s' % dn) + def create_parser(subparsers): group_parser = subparsers.add_parser('group', help='Manage groups') @@ -72,5 +101,19 @@ def create_parser(subparsers): delete_parser.set_defaults(func=delete) delete_parser.add_argument('dn', nargs='?', help='The dn to delete') + members_parser = subcommands.add_parser('members', help="List member dns of a group") + members_parser.set_defaults(func=members) + members_parser.add_argument('cn', nargs='?', help="cn of group to list members of") + + add_member_parser = subcommands.add_parser('add_member', help="Add a member to a group") + add_member_parser.set_defaults(func=add_member) + add_member_parser.add_argument('cn', nargs='?', help="cn of group to add member to") + add_member_parser.add_argument('dn', nargs='?', help="dn of object to add to group as member") + + remove_member_parser = subcommands.add_parser('remove_member', help="Remove a member from a group") + remove_member_parser.set_defaults(func=remove_member) + remove_member_parser.add_argument('cn', nargs='?', help="cn of group to remove member from") + remove_member_parser.add_argument('dn', nargs='?', help="dn of object to remove from group as member") + # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 diff --git a/src/lib389/lib389/idm/group.py b/src/lib389/lib389/idm/group.py index d1716a6..29ca17f 100644 --- a/src/lib389/lib389/idm/group.py +++ b/src/lib389/lib389/idm/group.py @@ -36,6 +36,12 @@ class Group(DSLdapObject): self._create_objectclasses.append('nsMemberOf') self._protected = False + def list_members(self): + """List the members of this group. + + """ + return self.get_attr_vals_utf8('member') + def is_member(self, dn): """Check if DN is a member diff --git a/src/lib389/lib389/tests/cli/__init__.py b/src/lib389/lib389/tests/cli/__init__.py index 02cfc51..5a2da52 100644 --- a/src/lib389/lib389/tests/cli/__init__.py +++ b/src/lib389/lib389/tests/cli/__init__.py @@ -15,69 +15,27 @@ from lib389.instance.setup import SetupDs from lib389.instance.options import General2Base, Slapd2Base from lib389._constants import * -from lib389.configurations import get_sample_entries - -INSTANCE_PORT = 54321 -INSTANCE_SERVERID = 'standalone' - -DEBUGGING = True +from lib389.topologies import create_topology, DEBUGGING -class TopologyInstance(object): - def __init__(self, standalone, logcap): - # For these tests, we don't want to open the instance. - # instance.open() - self.standalone = standalone - self.logcap = logcap +from lib389.utils import generate_ds_params +from lib389.configurations import get_sample_entries -# Need a teardown to destroy the instance. @pytest.fixture(scope="module") def topology(request): - lc = LogCapture() - instance = DirSrv(verbose=DEBUGGING) - instance.log.debug("Instance allocated") - args = {SER_PORT: INSTANCE_PORT, - SER_SERVERID_PROP: INSTANCE_SERVERID} - instance.allocate(args) - if instance.exists(): - instance.delete() - - # This will need to change to instance.create in the future - # when it's linked up! - sds = SetupDs(verbose=DEBUGGING, dryrun=False, log=lc.log) - - # Get the dicts from Type2Base, as though they were from _validate_ds_2_config - # IE get the defaults back just from Slapd2Base.collect - # Override instance name, root password, port and secure port. - - general_options = General2Base(lc.log) - general_options.verify() - general = general_options.collect() - - # Need an args -> options2 ... - slapd_options = Slapd2Base(lc.log) - slapd_options.set('instance_name', INSTANCE_SERVERID) - slapd_options.set('port', INSTANCE_PORT) - slapd_options.set('root_password', PW_DM) - slapd_options.verify() - slapd = slapd_options.collect() - - sds.create_from_args(general, slapd, {}, None) - insts = instance.list(serverid=INSTANCE_SERVERID) - # Assert we did change the system. - assert(len(insts) == 1) - # Make sure we can connect - instance.open(connOnly=True) - + topology = create_topology({ReplicaRole.STANDALONE: 1}, None) def fin(): - if instance.exists() and not DEBUGGING: - instance.delete() + if DEBUGGING: + topology.standalone.stop() + else: + topology.standalone.delete() request.addfinalizer(fin) - return TopologyInstance(instance, lc) - + topology.logcap = LogCapture() + return topology @pytest.fixture(scope="module") -def topology_be_latest(topology): +def topology_be_latest(request): + topology = create_topology({ReplicaRole.STANDALONE: 1}, None) be = topology.standalone.backends.create(properties={ 'cn': 'userRoot', 'suffix' : DEFAULT_SUFFIX, @@ -86,11 +44,20 @@ def topology_be_latest(topology): centries = get_sample_entries(INSTALL_LATEST_CONFIG) cent = centries(topology.standalone, DEFAULT_SUFFIX) cent.apply() - return topology + def fin(): + if DEBUGGING: + topology.standalone.stop() + else: + topology.standalone.delete() + request.addfinalizer(fin) + + topology.logcap = LogCapture() + return topology @pytest.fixture(scope="module") -def topology_be_001003006(topology): +def topology_be_001003006(request): + topology = create_topology({ReplicaRole.STANDALONE: 1}, None) be = topology.standalone.backends.create(properties={ 'cn': 'userRoot', 'suffix' : DEFAULT_SUFFIX, @@ -99,6 +66,15 @@ def topology_be_001003006(topology): centries = get_sample_entries('001003006') cent = centries(topology.standalone, DEFAULT_SUFFIX) cent.apply() + + def fin(): + if DEBUGGING: + topology.standalone.stop() + else: + topology.standalone.delete() + request.addfinalizer(fin) + + topology.logcap = LogCapture() return topology diff --git a/src/lib389/lib389/tests/cli/idm_group_test.py b/src/lib389/lib389/tests/cli/idm_group_test.py new file mode 100644 index 0000000..51ad85e --- /dev/null +++ b/src/lib389/lib389/tests/cli/idm_group_test.py @@ -0,0 +1,90 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2018 Red Hat, Inc. +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- + +import pytest +import ldap + +from lib389._constants import DEFAULT_SUFFIX, INSTALL_LATEST_CONFIG + +from lib389.cli_conf.backend import backend_create +from lib389.cli_idm.initialise import initialise +from lib389.cli_idm.group import get, create, delete, members, add_member, remove_member +from lib389.cli_idm.user import create as create_user + +from lib389.cli_base import LogCapture, FakeArgs +from lib389.tests.cli import topology_be_latest as topology + +from lib389.utils import ds_is_older +pytestmark = pytest.mark.skipif(ds_is_older('1.4.0'), reason="Not implemented") + +# Topology is pulled from __init__.py +def test_group_tasks(topology): + # First check that our test group isn't there: + topology.logcap.flush() + g_args = FakeArgs() + g_args.selector = 'testgroup' + with pytest.raises(ldap.NO_SUCH_OBJECT): + get(topology.standalone, DEFAULT_SUFFIX, topology.logcap.log, g_args) + + # Create a group + topology.logcap.flush() + g_args.cn = 'testgroup' + create(topology.standalone, DEFAULT_SUFFIX, topology.logcap.log, g_args) + assert(topology.logcap.contains("Sucessfully created testgroup")) + + # Assert it exists + topology.logcap.flush() + g_args = FakeArgs() + g_args.selector = 'testgroup' + get(topology.standalone, DEFAULT_SUFFIX, topology.logcap.log, g_args) + assert(topology.logcap.contains("dn: cn=testgroup,ou=groups,dc=example,dc=com")) + + # Add a user + topology.logcap.flush() + u_args = FakeArgs() + u_args.uid = 'testuser' + u_args.cn = 'Test User' + u_args.displayName = 'Test User' + u_args.homeDirectory = '/home/testuser' + u_args.uidNumber = '5000' + u_args.gidNumber = '5000' + create_user(topology.standalone, DEFAULT_SUFFIX, topology.logcap.log, u_args) + assert(topology.logcap.contains("Sucessfully created testuser")) + + # Add them to the group as a member + topology.logcap.flush() + g_args.cn = "testgroup" + g_args.dn = "uid=testuser,ou=people,dc=example,dc=com" + add_member(topology.standalone, DEFAULT_SUFFIX, topology.logcap.log, g_args) + assert(topology.logcap.contains("added member")) + + # Check they are a member + topology.logcap.flush() + g_args.cn = "testgroup" + members(topology.standalone, DEFAULT_SUFFIX, topology.logcap.log, g_args) + assert(topology.logcap.contains("uid=testuser,ou=people,dc=example,dc=com")) + + # Remove them from the group + topology.logcap.flush() + g_args.cn = "testgroup" + g_args.dn = "uid=testuser,ou=people,dc=example,dc=com" + remove_member(topology.standalone, DEFAULT_SUFFIX, topology.logcap.log, g_args) + assert(topology.logcap.contains("removed member")) + + # Check they are not a member + topology.logcap.flush() + g_args.cn = "testgroup" + members(topology.standalone, DEFAULT_SUFFIX, topology.logcap.log, g_args) + assert(topology.logcap.contains("No members to display")) + + # Delete the group + topology.logcap.flush() + g_args.dn = "cn=testgroup,ou=groups,dc=example,dc=com" + delete(topology.standalone, DEFAULT_SUFFIX, topology.logcap.log, g_args, warn=False) + assert(topology.logcap.contains("Sucessfully deleted cn=testgroup,ou=groups,dc=example,dc=com")) + diff --git a/src/lib389/lib389/tests/cli/idm_user_test.py b/src/lib389/lib389/tests/cli/idm_user_test.py index 537e12d..42abd02 100644 --- a/src/lib389/lib389/tests/cli/idm_user_test.py +++ b/src/lib389/lib389/tests/cli/idm_user_test.py @@ -23,7 +23,6 @@ pytestmark = pytest.mark.skipif(ds_is_older('1.4.0'), reason="Not implemented") # Topology is pulled from __init__.py def test_user_tasks(topology): - # be_args = FakeArgs() be_args.cn = 'userRoot' diff --git a/src/lib389/lib389/topologies.py b/src/lib389/lib389/topologies.py index ab9b2bc..08dcada 100644 --- a/src/lib389/lib389/topologies.py +++ b/src/lib389/lib389/topologies.py @@ -34,7 +34,7 @@ else: log = logging.getLogger(__name__) -def create_topology(topo_dict): +def create_topology(topo_dict, suffix=DEFAULT_SUFFIX): """Create a requested topology. Cascading replication scenario isn't supported @param topo_dict - dictionary {ReplicaRole.STANDALONE: num, ReplicaRole.MASTER: num, @@ -72,7 +72,14 @@ def create_topology(topo_dict): args_instance[SER_PORT] = instance_data[SER_PORT] args_instance[SER_SECURE_PORT] = instance_data[SER_SECURE_PORT] args_instance[SER_SERVERID_PROP] = instance_data[SER_SERVERID_PROP] - args_instance[SER_CREATION_SUFFIX] = DEFAULT_SUFFIX + # It's required to be able to make a suffix-less install for + # some cli tests. It's invalid to require replication with + # no suffix however .... + if suffix is not None: + args_instance[SER_CREATION_SUFFIX] = suffix + elif role != ReplicaRole.STANDALONE: + raise AssertionError("Invalid request to make suffix-less replicated environment") + instance.allocate(args_instance)