On Tue, Sep 29, 2015 at 08:27:01AM -0600, Tim Flink wrote: > Long story short, when the batcave upgrade happened on Friday we found > out that rbac_playbook doesn't work on el7 due to an issue with > nss-altfiles. > > I figured out how to sidestep the issue by changing the approach that > rbac_playbook takes. It used to get all the groups for the user running > the script and check for an intersection between those groups and the > required groups for the playbook being run. > > The new version looks at the groups required for the playbook being > run, gathers all the users in those groups and verifies that the user > running rbac_playbook is in that list before proceeding. Thanks for doing this! > diff --git a/ansible_utils.spec b/ansible_utils.spec > index 2e26a70..f097020 100644 > --- a/ansible_utils.spec > +++ b/ansible_utils.spec > @@ -20,12 +20,14 @@ Requires: PyYAML > BuildRequires: python2-devel > BuildRequires: python-setuptools > BuildRequires: python-kitchen > +BuildRequires: python-mock > > # python-dingus isn't built for el7, can't run tests > %if 0%{?rhel} > 6 > BuildRequires: pytest > BuildRequires: PyYAML > BuildRequires: python-dingus > +BuildRequires: python-mock > %endif > > # the cli uses argparse, which is not part of the standard libaray in > python2.6 diff --git a/ansible_utils/rbac_playbook.py > b/ansible_utils/rbac_playbook.py index 42be23f..c6a952c 100755 > --- a/ansible_utils/rbac_playbook.py > +++ b/ansible_utils/rbac_playbook.py > @@ -140,7 +140,7 @@ def generate_message(result, username, > playbook_name, args, checksum): :return: rendered string summarizing > rbac activity """ > subject = "[rbac-playbook] {0} {1} ran {2}".format(result, > - > username.pw_name, > + username, > playbook_name) > body = ['Details:'] > body.extend(['{0}: {1}'.format(key, value) for key, value in > args.iteritems()]) @@ -188,10 +188,13 @@ def > run_sudo_command(playbook_file, playbook_args): full_filename = > os.path.abspath(os.path.join(config['config']['PLAYBOOKS_DIR'], > playbook_file)) > + python_args = ['cd', config['config']['ANSIBLE_DIR'], ';', > + '/usr/bin/python2', > config['config']['ANSIBLE_PLAYBOOK'], > + full_filename] > + python_args.extend(playbook_args) > executable = '/usr/bin/sudo' > - args = ['-i', '/usr/bin/python2', > - config['config']['ANSIBLE_PLAYBOOK'], full_filename] > - args.extend(playbook_args) > + args = ['-i', '/bin/bash', '-i', '-c', > + ' '.join(python_args)] Why the change here? Why the addition of bash? > > # Note: (1) The function used here has to pass the environment to > # ansible-playbook so that it can connect to the ssh-agent > correctly @@ -206,36 +209,56 @@ def run_sudo_command(playbook_file, > playbook_args): print "EXECV: %s %s" % (executable, ' '.join(args)) > os.execv(executable, args) > > +def _get_username(): > + """Retrieve the username for the user which started execution of > rbac_playbook""" > -def can_run(acl, groups, playbook_file): > + user = os.getlogin() > + username = pwd.getpwnam(user) > + return username.pw_name > + > +def _get_group_members(groupname): > + """Find the members of a specific group > + > + :param groupname: name of group to find members of > + :return: list of usernames for members of the given group, empty > list if the group does not exist""" + > + group_data = None > + try: > + group_data = grp.getgrnam(groupname) > + except KeyError: > + return [] > + > + return group_data.gr_mem > + > +def _get_playbook_authorized_users(grouplist): > + """Retrieve a set of all users who are members of one or more > groups + > + :param grouplist: list of one or more group names > + :return: set of usernames corresponding to the union of members > for each group in the grouplist""" + > + userlist = [] > + for groupname in grouplist: > + userlist.extend(_get_group_members(groupname)) > + > + return set(userlist) > + > +def can_run(acl, username, playbook_file): > """ determines whether the current user is allowed to run a > playbook > :param acl: dictionary of acl information > - :param groups: groups of which the user is a member > + :param username: username of user running the utility > :param playbook_file: playbook file that is being run > :return: True if the user is authorized, False if unauthorized > """ > - # exact match quick route > + > + authorized_groups = acl[playbook_file] > + > if playbook_file in acl: > - pb_groups = frozenset(acl[playbook_file]) > - if groups.intersection(pb_groups): > + pb_authorized = > _get_playbook_authorized_users(authorized_groups) > + if username in pb_authorized: > return True > return False It looks like this function used to return False if an invalid playbook_file was passed to it, but now it will raise a KeyError. Is that intentional? I see there is a test case for it, but the `rbac_playbook` function doesn't handle it. > - > -def get_groups(): > - """ retrieve the groups of which the current user is currently a > member > - :return: (username,groups) where groups is a set of groups which > the current user is a member > - """ > - username = os.getlogin() > - user = pwd.getpwnam(username) > - groups = set(g.gr_name for g in grp.getgrall() if username in > g.gr_mem) > - groups.add(grp.getgrgid(user.pw_gid).gr_name) > - groups = frozenset(groups) > - > - return user, groups > - > - > def generate_args(options): > """ Generate ansible-playbook compatible args representing the > information passed into rbac > @@ -272,11 +295,11 @@ def rbac_playbook(playbook_name, options): > :param playbook_name: name of playbook file to run > :param options: dictionary of options > """ > - username, groups = get_groups() > + username = _get_username() > checksum = get_checksum(playbook_name) > > # raise exception if not allowed > - if not can_run(config['acls'], groups, playbook_name): > + if not can_run(config['acls'], username, playbook_name): > notify(generate_message('FAILURE', username, playbook_name, > options, checksum)) msg ="user {0} is not authorized to run > {1}".format(username.pw_name, playbook_name) raise > RbacAuthException(msg) diff --git a/tests/test_rbac_playbook.py > b/tests/test_rbac_playbook.py index 765997d..0b76825 100644 > --- a/tests/test_rbac_playbook.py > +++ b/tests/test_rbac_playbook.py > @@ -1,6 +1,8 @@ > import copy > +import grp > > from dingus import Dingus > +from mock import MagicMock, Mock > > from ansible_utils import rbac_playbook as rbac > > @@ -93,7 +95,7 @@ class TestGeneratePlaybookArgs(object): > test_args = rbac.generate_args(ref_options) > assert test_args == ['-l', ':'.join(ref_limit)] > > - def test_simple_limit(self): > + def test_simple_tag(self): > ref_tags = ['tag', 'tagg', 'tog', 'togg'] > > ref_options = {'tags': ref_tags} > @@ -123,25 +125,101 @@ class TestGeneratePlaybookArgs(object): > '--start-at-task="some task"'] > > > -class TestDetermineCanRun(object): > +class TestCanRun(object): > > def setup_method(self, method): > self.ref_acl = {'group/unicorns.yml': ['sysadmin-unicorn', > 'sysadmin-pony'], 'pony.yml': ['sysadmin-pony'] > } > > - def test_allow_one_of_multiple(self, monkeypatch): > - ref_groups = set(['sysadmin-unicorn']) > - test_can_run = rbac.can_run(self.ref_acl, ref_groups, > 'group/unicorns.yml') > + def test_allow(self, monkeypatch): > + ref_user = 'twilightsparkle' > + ref_authorized = ['twilightsparkle', 'fluttershy'] > + > + stub_authorized = MagicMock(return_value=ref_authorized) > + monkeypatch.setattr(rbac, '_get_playbook_authorized_users', > stub_authorized) + > + test_can_run = rbac.can_run(self.ref_acl, ref_user, > 'group/unicorns.yml') > assert test_can_run > > - def test_allow_one_of_multiple(self, monkeypatch): > - ref_groups = set(['sysadmin-kittycat']) > - test_can_run = rbac.can_run(self.ref_acl, ref_groups, > 'group/unicorns.yml') > + def test_disallow(self, monkeypatch): > + ref_user = 'mittens' > + ref_authorized = ['twilightsparkle', 'fluttershy'] > + > + stub_authorized = MagicMock(return_value=ref_authorized) > + monkeypatch.setattr(rbac, '_get_playbook_authorized_users', > stub_authorized) + > + test_can_run = rbac.can_run(self.ref_acl, ref_user, > 'group/unicorns.yml') > assert not test_can_run > > +class TestGetAuthorizedUsers(object): > + > + def setup_method(self, method): > + self.ref_acl = {'group/unicorns.yml': ['sysadmin-unicorn', > 'sysadmin-pony'], > + 'pony.yml': ['sysadmin-pony'] > + } > + > + def test_get_single_group_users(self, monkeypatch): > + ref_authorized_groups = ['sysadmin-pony'] > + ref_group_members = ['twilightsparkle', 'fluttershy'] > + ref_members = set(ref_group_members) > + > + stub_group_members = MagicMock(return_value=ref_group_members) > + monkeypatch.setattr(rbac, '_get_group_members', > stub_group_members) + > + test_group_members = > rbac._get_playbook_authorized_users(ref_authorized_groups) + > + assert test_group_members == ref_members > + > + def test_get_multiple_group_users(self, monkeypatch): > + ref_authorized_groups = ['sysadmin-pony', 'sysadmin-unicorn'] > + ref_group_members = [['twilightsparkle', 'fluttershy'], > ['charlie', 'buttercup']] > + ref_members = set(ref_group_members[0] + ref_group_members[1]) > + > + stub_group_members = MagicMock(side_effect=ref_group_members) > + monkeypatch.setattr(rbac, '_get_group_members', > stub_group_members) + > + test_group_members = > rbac._get_playbook_authorized_users(ref_authorized_groups) + > + assert test_group_members == ref_members > + > + def test_get_multiple_group_with_overlap(self, monkeypatch): > + ref_authorized_groups = ['sysadmin-pony', 'sysadmin-unicorn'] > + ref_group_members = [['twilightsparkle', 'fluttershy'], > ['charlie', 'buttercup', 'twilightsparkle']] > + ref_members = set(ref_group_members[0] + ref_group_members[1]) > + > + stub_group_members = MagicMock(side_effect=ref_group_members) > + monkeypatch.setattr(rbac, '_get_group_members', > stub_group_members) + > + test_group_members = > rbac._get_playbook_authorized_users(ref_authorized_groups) + > + assert test_group_members == ref_members > + > +class TestGetGroupMembers(object): > + > + def setup_method(self, method): > + self.ref_groupname = 'sysadmin-ponies' > + > + def test_valid_group(self, monkeypatch): > + ref_members = ['twilightsparkle', 'fluttershy'] > + > + stub_getgrnam = Mock(gr_mem=ref_members) > + stub_grp = MagicMock(return_value=stub_getgrnam) > + monkeypatch.setattr(grp, 'getgrnam', stub_grp) > + > + test_group_members = > rbac._get_group_members(self.ref_groupname) + > + assert test_group_members == ref_members > + > + def test_invalid_group(self, monkeypatch): > + stub_grp = MagicMock(side_effect=KeyError) > + monkeypatch.setattr(grp, 'getgrnam', stub_grp) > + > + test_group_members = > rbac._get_group_members(self.ref_groupname) + > + assert test_group_members == [] > > class TestConfigMerge(object): >
Attachment:
signature.asc
Description: PGP signature
_______________________________________________ infrastructure mailing list infrastructure@xxxxxxxxxxxxxxxxxxxxxxx http://lists.fedoraproject.org/postorius/infrastructure@xxxxxxxxxxxxxxxxxxxxxxx