Blame plugins/modules/iparole.py

Packit Service a166ed
# -*- coding: utf-8 -*-
Packit Service a166ed
"""ansible-freeipa iparole module implementation."""
Packit Service a166ed
Packit Service a166ed
# Authors:
Packit Service a166ed
#   Rafael Guterres Jeffman <rjeffman@redhat.com>
Packit Service a166ed
#
Packit Service a166ed
# Copyright (C) 2020 Red Hat
Packit Service a166ed
# see file 'COPYING' for use and warranty information
Packit Service a166ed
#
Packit Service a166ed
# This program is free software; you can redistribute it and/or modify
Packit Service a166ed
# it under the terms of the GNU General Public License as published by
Packit Service a166ed
# the Free Software Foundation, either version 3 of the License, or
Packit Service a166ed
# (at your option) any later version.
Packit Service a166ed
#
Packit Service a166ed
# This program is distributed in the hope that it will be useful,
Packit Service a166ed
# but WITHOUT ANY WARRANTY; without even the implied warranty of
Packit Service a166ed
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
Packit Service a166ed
# GNU General Public License for more details.
Packit Service a166ed
#
Packit Service a166ed
# You should have received a copy of the GNU General Public License
Packit Service a166ed
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
Packit Service a166ed
Packit Service a166ed
ANSIBLE_METADATA = {
Packit Service a166ed
    "metadata_version": "1.0",
Packit Service a166ed
    "supported_by": "community",
Packit Service a166ed
    "status": ["preview"],
Packit Service a166ed
}
Packit Service a166ed
Packit Service a166ed
Packit Service a166ed
DOCUMENTATION = """
Packit Service a166ed
---
Packit Service a166ed
module: iparole
Packit Service a166ed
short description: Manage FreeIPA role
Packit Service a166ed
description: Manage FreeIPA role
Packit Service a166ed
options:
Packit Service a166ed
  ipaadmin_principal:
Packit Service a166ed
    description: The admin principal.
Packit Service a166ed
    default: admin
Packit Service a166ed
  ipaadmin_password:
Packit Service a166ed
    description: The admin password.
Packit Service a166ed
    required: false
Packit Service a166ed
  role:
Packit Service a166ed
    description: The list of role name strings.
Packit Service a166ed
    required: true
Packit Service a166ed
    aliases: ["cn"]
Packit Service a166ed
  description:
Packit Service a166ed
    description: A description for the role.
Packit Service a166ed
    required: false
Packit Service a166ed
  rename:
Packit Service a166ed
    description: Rename the role object.
Packit Service a166ed
    required: false
Packit Service a166ed
  user:
Packit Service a166ed
    description: List of users.
Packit Service a166ed
    required: false
Packit Service a166ed
  group:
Packit Service a166ed
    description: List of groups.
Packit Service a166ed
    required: false
Packit Service a166ed
  host:
Packit Service a166ed
    description: List of hosts.
Packit Service a166ed
    required: false
Packit Service a166ed
  hostgroup:
Packit Service a166ed
    description: List of hostgroups.
Packit Service a166ed
    required: false
Packit Service a166ed
  service:
Packit Service a166ed
    description: List of services.
Packit Service a166ed
    required: false
Packit Service a166ed
  action:
Packit Service a166ed
    description: Work on role or member level.
Packit Service a166ed
    choices: ["role", "member"]
Packit Service a166ed
    default: role
Packit Service a166ed
    required: false
Packit Service a166ed
  state:
Packit Service a166ed
    description: The state to ensure.
Packit Service a166ed
    choices: ["present", "absent"]
Packit Service a166ed
    default: present
Packit Service a166ed
    required: true
Packit Service a166ed
"""
Packit Service a166ed
Packit Service a166ed
EXAMPLES = """
Packit Service a166ed
- name: Ensure a role named `somerole` is present.
Packit Service a166ed
  iparole:
Packit Service a166ed
    ipaadmin_password: SomeADMINpassword
Packit Service a166ed
    name: somerole
Packit Service a166ed
Packit Service a166ed
- name: Ensure user `pinky` is a memmer of role `somerole`.
Packit Service a166ed
  iparole:
Packit Service a166ed
    ipaadmin_password: SomeADMINpassword
Packit Service a166ed
    name: somerole
Packit Service a166ed
    user:
Packit Service a166ed
    - pinky
Packit Service a166ed
    action: member
Packit Service a166ed
Packit Service a166ed
- name: Ensure a role named `somerole` is absent.
Packit Service a166ed
  iparole:
Packit Service a166ed
    ipaadmin_password: SomeADMINpassword
Packit Service a166ed
    name: somerole
Packit Service a166ed
    state: absent
Packit Service a166ed
"""
Packit Service a166ed
Packit Service a166ed
# pylint: disable=wrong-import-position
Packit Service a166ed
# pylint: disable=import-error
Packit Service a166ed
# pylint: disable=no-name-in-module
Packit Service a166ed
from ansible.module_utils.basic import AnsibleModule
Packit Service a166ed
from ansible.module_utils._text import to_text
Packit Service a166ed
from ansible.module_utils.ansible_freeipa_module import \
Packit Service a166ed
    temp_kinit, temp_kdestroy, valid_creds, api_connect, api_command, \
Packit Service a166ed
    gen_add_del_lists, compare_args_ipa, module_params_get, api_get_realm
Packit Service a166ed
import six
Packit Service a166ed
Packit Service a166ed
Packit Service a166ed
if six.PY3:
Packit Service a166ed
    unicode = str
Packit Service a166ed
Packit Service a166ed
Packit Service a166ed
def find_role(module, name):
Packit Service a166ed
    """Find if a role with the given name already exist."""
Packit Service a166ed
    try:
Packit Service a166ed
        _result = api_command(module, "role_show", name, {"all": True})
Packit Service a166ed
    except Exception:  # pylint: disable=broad-except
Packit Service a166ed
        # An exception is raised if role name is not found.
Packit Service a166ed
        return None
Packit Service a166ed
    else:
Packit Service a166ed
        return _result["result"]
Packit Service a166ed
Packit Service a166ed
Packit Service a166ed
def gen_args(module):
Packit Service a166ed
    """Generate arguments for executing commands."""
Packit Service a166ed
    arg_map = {
Packit Service a166ed
        "description": "description",
Packit Service a166ed
        "rename": "rename",
Packit Service a166ed
    }
Packit Service a166ed
    args = {}
Packit Service a166ed
Packit Service a166ed
    for param, arg in arg_map.items():
Packit Service a166ed
        value = module_params_get(module, param)
Packit Service a166ed
        if value is not None:
Packit Service a166ed
            args[arg] = value
Packit Service a166ed
Packit Service a166ed
    return args
Packit Service a166ed
Packit Service a166ed
Packit Service a166ed
def check_parameters(module):
Packit Service a166ed
    """Check if parameters passed for module processing are valid."""
Packit Service a166ed
    action = module_params_get(module, "action")
Packit Service a166ed
    state = module_params_get(module, "state")
Packit Service a166ed
Packit Service a166ed
    invalid = []
Packit Service a166ed
Packit Service a166ed
    if state == "present":
Packit Service a166ed
        if action == "member":
Packit Service a166ed
            invalid.extend(['description', 'rename'])
Packit Service a166ed
Packit Service a166ed
    if state == "absent":
Packit Service a166ed
        invalid.extend(['description', 'rename'])
Packit Service a166ed
        if action != "member":
Packit Service a166ed
            invalid.extend(['privilege'])
Packit Service a166ed
Packit Service a166ed
    for arg in invalid:
Packit Service a166ed
        if module_params_get(module, arg) is not None:
Packit Service a166ed
            module.fail_json(
Packit Service a166ed
                msg="Argument '%s' can not be used with action '%s'" %
Packit Service a166ed
                (arg, state))
Packit Service a166ed
Packit Service a166ed
Packit Service a166ed
def verify_credentials(module):
Packit Service a166ed
    """Ensure there are valid Kerberos credentials."""
Packit Service a166ed
    ccache_dir = None
Packit Service a166ed
    ccache_name = None
Packit Service a166ed
Packit Service a166ed
    ipaadmin_principal = module_params_get(module, "ipaadmin_principal")
Packit Service a166ed
    ipaadmin_password = module_params_get(module, "ipaadmin_password")
Packit Service a166ed
Packit Service a166ed
    if not valid_creds(module, ipaadmin_principal):
Packit Service a166ed
        ccache_dir, ccache_name = temp_kinit(ipaadmin_principal,
Packit Service a166ed
                                             ipaadmin_password)
Packit Service a166ed
Packit Service a166ed
    return (ccache_dir, ccache_name)
Packit Service a166ed
Packit Service a166ed
Packit Service a166ed
def member_intersect(module, attr, memberof, res_find):
Packit Service a166ed
    """Filter member arguments from role found by intersection."""
Packit Service a166ed
    params = module_params_get(module, attr)
Packit Service a166ed
    if not res_find:
Packit Service a166ed
        return params
Packit Service a166ed
    filtered = []
Packit Service a166ed
    if params:
Packit Service a166ed
        existing = res_find.get(memberof, [])
Packit Service a166ed
        filtered = list(set(params) & set(existing))
Packit Service a166ed
    return filtered
Packit Service a166ed
Packit Service a166ed
Packit Service a166ed
def member_difference(module, attr, memberof, res_find):
Packit Service a166ed
    """Filter member arguments from role found by difference."""
Packit Service a166ed
    params = module_params_get(module, attr)
Packit Service a166ed
    if not res_find:
Packit Service a166ed
        return params
Packit Service a166ed
    filtered = []
Packit Service a166ed
    if params:
Packit Service a166ed
        existing = res_find.get(memberof, [])
Packit Service a166ed
        filtered = list(set(params) - set(existing))
Packit Service a166ed
    return filtered
Packit Service a166ed
Packit Service a166ed
Packit Service a166ed
def ensure_absent_state(module, name, action, res_find):
Packit Service a166ed
    """Define commands to ensure absent state."""
Packit Service a166ed
    commands = []
Packit Service a166ed
Packit Service a166ed
    if action == "role":
Packit Service a166ed
        commands.append([name, 'role_del', {}])
Packit Service a166ed
Packit Service a166ed
    if action == "member":
Packit Service a166ed
Packit Service a166ed
        members = member_intersect(
Packit Service a166ed
            module, 'privilege', 'memberof_privilege', res_find)
Packit Service a166ed
        if members:
Packit Service a166ed
            commands.append([name, "role_remove_privilege",
Packit Service a166ed
                             {"privilege": members}])
Packit Service a166ed
Packit Service a166ed
        member_args = {}
Packit Service a166ed
        for key in ['user', 'group', 'host', 'hostgroup']:
Packit Service a166ed
            items = member_intersect(
Packit Service a166ed
                        module, key, 'member_%s' % key, res_find)
Packit Service a166ed
            if items:
Packit Service a166ed
                member_args[key] = items
Packit Service a166ed
Packit Service a166ed
        _services = filter_service(module, res_find,
Packit Service a166ed
                                   lambda res, svc: res.startswith(svc))
Packit Service a166ed
        if _services:
Packit Service a166ed
            member_args['service'] = _services
Packit Service a166ed
Packit Service a166ed
        # Only add remove command if there's at least one member no manage.
Packit Service a166ed
        if member_args:
Packit Service a166ed
            commands.append([name, "role_remove_member", member_args])
Packit Service a166ed
Packit Service a166ed
    return commands
Packit Service a166ed
Packit Service a166ed
Packit Service a166ed
def filter_service(module, res_find, predicate):
Packit Service a166ed
    """
Packit Service a166ed
    Filter service based on predicate.
Packit Service a166ed
Packit Service a166ed
    Compare service name with existing ones matching
Packit Service a166ed
    at least until `@` from principal name.
Packit Service a166ed
Packit Service a166ed
    Predicate is a callable that accepts the existing service, and the
Packit Service a166ed
    modified service to be compared to.
Packit Service a166ed
    """
Packit Service a166ed
    _services = []
Packit Service a166ed
    service = module_params_get(module, 'service')
Packit Service a166ed
    if service:
Packit Service a166ed
        existing = [to_text(x) for x in res_find.get('member_service', [])]
Packit Service a166ed
        for svc in service:
Packit Service a166ed
            svc = svc if '@' in svc else ('%s@' % svc)
Packit Service a166ed
            found = [x for x in existing if predicate(x, svc)]
Packit Service a166ed
            _services.extend(found)
Packit Service a166ed
    return _services
Packit Service a166ed
Packit Service a166ed
Packit Service a166ed
def ensure_role_with_members_is_present(module, name, res_find):
Packit Service a166ed
    """Define commands to ensure member are present for action `role`."""
Packit Service a166ed
    commands = []
Packit Service a166ed
    privilege_add, privilege_del = gen_add_del_lists(
Packit Service a166ed
        module_params_get(module, "privilege"),
Packit Service a166ed
        res_find.get('memberof_privilege', []))
Packit Service a166ed
Packit Service a166ed
    if privilege_add:
Packit Service a166ed
        commands.append([name, "role_add_privilege",
Packit Service a166ed
                         {"privilege": privilege_add}])
Packit Service a166ed
    if privilege_del:
Packit Service a166ed
        commands.append([name, "role_remove_privilege",
Packit Service a166ed
                         {"privilege": privilege_del}])
Packit Service a166ed
Packit Service a166ed
    add_members = {}
Packit Service a166ed
    del_members = {}
Packit Service a166ed
Packit Service a166ed
    for key in ["user", "group", "host", "hostgroup"]:
Packit Service a166ed
        add_list, del_list = gen_add_del_lists(
Packit Service a166ed
            module_params_get(module, key),
Packit Service a166ed
            res_find.get('member_%s' % key, [])
Packit Service a166ed
        )
Packit Service a166ed
        if add_list:
Packit Service a166ed
            add_members[key] = add_list
Packit Service a166ed
        if del_list:
Packit Service a166ed
            del_members[key] = [to_text(item) for item in del_list]
Packit Service a166ed
Packit Service a166ed
    service = [
Packit Service a166ed
        to_text(svc) if '@' in svc else ('%s@%s' % (svc, api_get_realm()))
Packit Service a166ed
        for svc in (module_params_get(module, 'service') or [])
Packit Service a166ed
    ]
Packit Service a166ed
    existing = [str(svc) for svc in res_find.get('member_service', [])]
Packit Service a166ed
    add_list, del_list = gen_add_del_lists(service, existing)
Packit Service a166ed
    if add_list:
Packit Service a166ed
        add_members['service'] = add_list
Packit Service a166ed
    if del_list:
Packit Service a166ed
        del_members['service'] = [to_text(item) for item in del_list]
Packit Service a166ed
Packit Service a166ed
    if add_members:
Packit Service a166ed
        commands.append([name, "role_add_member", add_members])
Packit Service a166ed
    if del_members:
Packit Service a166ed
        commands.append([name, "role_remove_member", del_members])
Packit Service a166ed
Packit Service a166ed
    return commands
Packit Service a166ed
Packit Service a166ed
Packit Service a166ed
def ensure_members_are_present(module, name, res_find):
Packit Service a166ed
    """Define commands to ensure members are present for action `member`."""
Packit Service a166ed
    commands = []
Packit Service a166ed
Packit Service a166ed
    members = member_difference(
Packit Service a166ed
        module, 'privilege', 'memberof_privilege', res_find)
Packit Service a166ed
    if members:
Packit Service a166ed
        commands.append([name, "role_add_privilege",
Packit Service a166ed
                         {"privilege": members}])
Packit Service a166ed
Packit Service a166ed
    member_args = {}
Packit Service a166ed
    for key in ['user', 'group', 'host', 'hostgroup']:
Packit Service a166ed
        items = member_difference(
Packit Service a166ed
                    module, key, 'member_%s' % key, res_find)
Packit Service a166ed
        if items:
Packit Service a166ed
            member_args[key] = items
Packit Service a166ed
Packit Service a166ed
    _services = filter_service(module, res_find,
Packit Service a166ed
                               lambda res, svc: not res.startswith(svc))
Packit Service a166ed
    if _services:
Packit Service a166ed
        member_args['service'] = _services
Packit Service a166ed
Packit Service a166ed
    if member_args:
Packit Service a166ed
        commands.append([name, "role_add_member", member_args])
Packit Service a166ed
Packit Service a166ed
    return commands
Packit Service a166ed
Packit Service a166ed
Packit Service a166ed
def process_command_failures(command, result):
Packit Service a166ed
    """Process the result of a command, looking for errors."""
Packit Service a166ed
    # Get all errors
Packit Service a166ed
    # All "already a member" and "not a member" failures in the
Packit Service a166ed
    # result are ignored. All others are reported.
Packit Service a166ed
    errors = []
Packit Service a166ed
    if "failed" in result and len(result["failed"]) > 0:
Packit Service a166ed
        for item in result["failed"]:
Packit Service a166ed
            failed_item = result["failed"][item]
Packit Service a166ed
            for member_type in failed_item:
Packit Service a166ed
                for member, failure in failed_item[member_type]:
Packit Service a166ed
                    if "already a member" in failure \
Packit Service a166ed
                       or "not a member" in failure:
Packit Service a166ed
                        continue
Packit Service a166ed
                    errors.append("%s: %s %s: %s" % (
Packit Service a166ed
                        command, member_type, member, failure))
Packit Service a166ed
    return errors
Packit Service a166ed
Packit Service a166ed
Packit Service a166ed
def process_commands(module, commands):
Packit Service a166ed
    """Process the list of IPA API commands."""
Packit Service a166ed
    errors = []
Packit Service a166ed
    exit_args = {}
Packit Service a166ed
    changed = False
Packit Service a166ed
    for name, command, args in commands:
Packit Service a166ed
        try:
Packit Service a166ed
            result = api_command(module, command, name, args)
Packit Service a166ed
            if "completed" in result:
Packit Service a166ed
                if result["completed"] > 0:
Packit Service a166ed
                    changed = True
Packit Service a166ed
            else:
Packit Service a166ed
                changed = True
Packit Service a166ed
Packit Service a166ed
            errors = process_command_failures(command, result)
Packit Service a166ed
        except Exception as exception:  # pylint: disable=broad-except
Packit Service a166ed
            module.fail_json(
Packit Service a166ed
                msg="%s: %s: %s" % (command, name, str(exception)))
Packit Service a166ed
Packit Service a166ed
    if errors:
Packit Service a166ed
        module.fail_json(msg=", ".join(errors))
Packit Service a166ed
Packit Service a166ed
    return changed, exit_args
Packit Service a166ed
Packit Service a166ed
Packit Service a166ed
def role_commands_for_name(module, state, action, name):
Packit Service a166ed
    """Define commands for the Role module."""
Packit Service a166ed
    commands = []
Packit Service a166ed
Packit Service a166ed
    rename = module_params_get(module, "rename")
Packit Service a166ed
Packit Service a166ed
    res_find = find_role(module, name)
Packit Service a166ed
Packit Service a166ed
    if state == "present":
Packit Service a166ed
        args = gen_args(module)
Packit Service a166ed
Packit Service a166ed
        if action == "role":
Packit Service a166ed
            if res_find is None:
Packit Service a166ed
                if rename is not None:
Packit Service a166ed
                    module.fail_json(msg="Cannot `rename` inexistent role.")
Packit Service a166ed
                commands.append([name, 'role_add', args])
Packit Service a166ed
                res_find = {}
Packit Service a166ed
            else:
Packit Service a166ed
                if not compare_args_ipa(module, args, res_find):
Packit Service a166ed
                    commands.append([name, 'role_mod', args])
Packit Service a166ed
Packit Service a166ed
        if action == "member":
Packit Service a166ed
            if res_find is None:
Packit Service a166ed
                module.fail_json(msg="No role '%s'" % name)
Packit Service a166ed
Packit Service a166ed
        cmds = ensure_role_with_members_is_present(module, name, res_find)
Packit Service a166ed
        commands.extend(cmds)
Packit Service a166ed
Packit Service a166ed
    if state == "absent" and res_find is not None:
Packit Service a166ed
        cmds = ensure_absent_state(module, name, action, res_find)
Packit Service a166ed
        commands.extend(cmds)
Packit Service a166ed
Packit Service a166ed
    return commands
Packit Service a166ed
Packit Service a166ed
Packit Service a166ed
def create_module():
Packit Service a166ed
    """Create module description."""
Packit Service a166ed
    ansible_module = AnsibleModule(
Packit Service a166ed
        argument_spec=dict(
Packit Service a166ed
            # generalgroups
Packit Service a166ed
            ipaadmin_principal=dict(type="str", default="admin"),
Packit Service a166ed
            ipaadmin_password=dict(type="str", required=False, no_log=True),
Packit Service a166ed
Packit Service a166ed
            name=dict(type="list", aliases=["cn"], default=None,
Packit Service a166ed
                      required=True),
Packit Service a166ed
            # present
Packit Service a166ed
            description=dict(required=False, type="str", default=None),
Packit Service a166ed
            rename=dict(required=False, type="str", default=None),
Packit Service a166ed
Packit Service a166ed
            # members
Packit Service a166ed
            privilege=dict(required=False, type='list', default=None),
Packit Service a166ed
            user=dict(required=False, type='list', default=None),
Packit Service a166ed
            group=dict(required=False, type='list', default=None),
Packit Service a166ed
            host=dict(required=False, type='list', default=None),
Packit Service a166ed
            hostgroup=dict(required=False, type='list', default=None),
Packit Service a166ed
            service=dict(required=False, type='list', default=None),
Packit Service a166ed
Packit Service a166ed
            # state
Packit Service a166ed
            action=dict(type="str", default="role",
Packit Service a166ed
                        choices=["role", "member"]),
Packit Service a166ed
            state=dict(type="str", default="present",
Packit Service a166ed
                       choices=["present", "absent"]),
Packit Service a166ed
        ),
Packit Service a166ed
        supports_check_mode=True,
Packit Service a166ed
        mutually_exclusive=[],
Packit Service a166ed
        required_one_of=[]
Packit Service a166ed
    )
Packit Service a166ed
Packit Service a166ed
    ansible_module._ansible_debug = True  # pylint: disable=protected-access
Packit Service a166ed
Packit Service a166ed
    return ansible_module
Packit Service a166ed
Packit Service a166ed
Packit Service a166ed
def main():
Packit Service a166ed
    """Process role module script."""
Packit Service a166ed
    ansible_module = create_module()
Packit Service a166ed
    check_parameters(ansible_module)
Packit Service a166ed
Packit Service a166ed
    # Init
Packit Service a166ed
    ccache_dir = None
Packit Service a166ed
    ccache_name = None
Packit Service a166ed
    try:
Packit Service a166ed
        ccache_dir, ccache_name = verify_credentials(ansible_module)
Packit Service a166ed
        api_connect()
Packit Service a166ed
Packit Service a166ed
        state = module_params_get(ansible_module, "state")
Packit Service a166ed
        action = module_params_get(ansible_module, "action")
Packit Service a166ed
        names = module_params_get(ansible_module, "name")
Packit Service a166ed
        commands = []
Packit Service a166ed
Packit Service a166ed
        for name in names:
Packit Service a166ed
            cmds = role_commands_for_name(ansible_module, state, action, name)
Packit Service a166ed
            commands.extend(cmds)
Packit Service a166ed
Packit Service a166ed
        changed, exit_args = process_commands(ansible_module, commands)
Packit Service a166ed
Packit Service a166ed
    except Exception as exception:  # pylint: disable=broad-except
Packit Service a166ed
        ansible_module.fail_json(msg=str(exception))
Packit Service a166ed
Packit Service a166ed
    finally:
Packit Service a166ed
        temp_kdestroy(ccache_dir, ccache_name)
Packit Service a166ed
Packit Service a166ed
    # Done
Packit Service a166ed
    ansible_module.exit_json(changed=changed, **exit_args)
Packit Service a166ed
Packit Service a166ed
Packit Service a166ed
if __name__ == "__main__":
Packit Service a166ed
    main()