From b33c5a7bab619f4dded94285635d9cadbeca75b5 Mon Sep 17 00:00:00 2001 From: Rafael Guterres Jeffman <rjeffman@redhat.com> Date: Thu, 16 Apr 2020 19:22:42 -0300 Subject: [PATCH] New Role management module There is a new role management module placed in the plugins folder: plugins/modules/iparole.py The role module allows to ensure presence or absence of roles and manage role members. Here is the documentation for the module: README-role.md New example playbooks have been added: playbooks/role/role-is-absent.yml playbooks/role/role-is-present.yml playbooks/role/role-member-group-absent.yml playbooks/role/role-member-group-present.yml playbooks/role/role-member-host-absent.yml playbooks/role/role-member-host-present.yml playbooks/role/role-member-hostgroup-absent.yml playbooks/role/role-member-hostgroup-present.yml playbooks/role/role-member-privilege-absent.yml playbooks/role/role-member-privilege-present.yml playbooks/role/role-member-service-absent.yml playbooks/role/role-member-service-present.yml playbooks/role/role-member-user-absent.yml playbooks/role/role-member-user-present.yml playbooks/role/role-members-absent.yml playbooks/role/role-members-present.yml playbooks/role/role-rename.yml New tests for the module: tests/role/test_role.yml tests/role/test_role_service_member.yml --- README-role.md | 264 ++++++++++ README.md | 2 + playbooks/role/role-is-absent.yml | 11 + playbooks/role/role-is-present.yml | 11 + playbooks/role/role-member-group-absent.yml | 14 + playbooks/role/role-member-group-present.yml | 13 + playbooks/role/role-member-host-absent.yml | 14 + playbooks/role/role-member-host-present.yml | 13 + .../role/role-member-hostgroup-absent.yml | 14 + .../role/role-member-hostgroup-present.yml | 13 + .../role/role-member-privilege-absent.yml | 15 + .../role/role-member-privilege-present.yml | 14 + playbooks/role/role-member-service-absent.yml | 14 + .../role/role-member-service-present.yml | 13 + playbooks/role/role-member-user-absent.yml | 14 + playbooks/role/role-member-user-present.yml | 13 + playbooks/role/role-members-absent.yml | 25 + playbooks/role/role-members-present.yml | 23 + playbooks/role/role-rename.yml | 11 + plugins/modules/iparole.py | 485 ++++++++++++++++++ tests/role/env_cleanup.yml | 38 ++ tests/role/env_facts.yml | 14 + tests/role/env_setup.yml | 34 ++ tests/role/test_role.yml | 388 ++++++++++++++ tests/role/test_role_service_member.yml | 95 ++++ 25 files changed, 1565 insertions(+) create mode 100644 README-role.md create mode 100644 playbooks/role/role-is-absent.yml create mode 100644 playbooks/role/role-is-present.yml create mode 100644 playbooks/role/role-member-group-absent.yml create mode 100644 playbooks/role/role-member-group-present.yml create mode 100644 playbooks/role/role-member-host-absent.yml create mode 100644 playbooks/role/role-member-host-present.yml create mode 100644 playbooks/role/role-member-hostgroup-absent.yml create mode 100644 playbooks/role/role-member-hostgroup-present.yml create mode 100644 playbooks/role/role-member-privilege-absent.yml create mode 100644 playbooks/role/role-member-privilege-present.yml create mode 100644 playbooks/role/role-member-service-absent.yml create mode 100644 playbooks/role/role-member-service-present.yml create mode 100644 playbooks/role/role-member-user-absent.yml create mode 100644 playbooks/role/role-member-user-present.yml create mode 100644 playbooks/role/role-members-absent.yml create mode 100644 playbooks/role/role-members-present.yml create mode 100644 playbooks/role/role-rename.yml create mode 100644 plugins/modules/iparole.py create mode 100644 tests/role/env_cleanup.yml create mode 100644 tests/role/env_facts.yml create mode 100644 tests/role/env_setup.yml create mode 100644 tests/role/test_role.yml create mode 100644 tests/role/test_role_service_member.yml diff --git a/README-role.md b/README-role.md new file mode 100644 index 00000000..49e3e581 --- /dev/null +++ b/README-role.md @@ -0,0 +1,264 @@ +Service module +============== + +Description +----------- + +The role module allows to ensure presence, absence of roles and members of roles. + +The role module is as compatible as possible to the Ansible upstream `ipa_role` module, but additionally offers role member management. + + +Features +-------- + +* Role management + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.4.0 and up are supported by the iparole module. + + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ + +**Node** +* Supported FreeIPA version (see above) + + +Usage +===== + +Example inventory file + +```ini +[ipaserver] +ipaserver.test.local +``` + + +Example playbook to make sure role is present with all members: + +```yaml +--- +- name: Playbook to manage IPA role with members. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + user: + - pinky + group: + - group01 + host: + - host01.example.com + hostgroup: + - hostgroup01 + privilege: + - Group Administrators + - User Administrators + service: + - service01 +``` + +Example playbook to rename a role: + +```yaml +- iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + rename: anotherrole +``` + +Example playbook to make sure role is absent: + +```yaml +--- +- name: Playbook to manage IPA role. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + state: absent +``` + +Example playbook to ensure a user is a member of a role: + +```yaml +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + user: + - pinky + action: member +``` + +Example playbook to ensure a group is a member of a role: + +```yaml +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + host: + - host01.example.com + action: member +``` + +Example playbook to ensure a host is a member of a role: + +```yaml +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + host: + - host01.example.com + action: member +``` + +Example playbook to ensure a hostgroup is a member of a role: + +```yaml +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + hostgroup: + - hostgroup01 + action: member +``` + +Example playbook to ensure a service is a member of a role: + +```yaml +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + service: + - service01 + action: member +``` + +Example playbook to ensure a privilege is a member of a role: + +```yaml +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + privilege: + - Group Administrators + - User Administrators + action: member +``` + +Example playbook to ensure that different members are not associated with a role. + +```yaml +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + user: + - pinky + group: + - group01 + host: + - host01.example.com + hostgroup: + - hostgroup01 + privilege: + - Group Administrators + - User Administrators + service: + - service01 + action: member + state: absent +``` + + +Variables +--------- + +iparole +------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no +`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no +`name` \| `cn` | The list of role name strings. | yes +`description` | A description for the role. | no +`rename` | Rename the role object. | no +`privileges` | Privileges associated to this role. | no +`user` | List of users to be assigned or not assigned to the role. | no +`group` | List of groups to be assigned or not assigned to the role. | no +`host` | List of hosts to be assigned or not assigned to the role. | no +`hostgroup` | List of hostgroups to be assigned or not assigned to the role. | no +`service` | List of services to be assigned or not assigned to the role. | no +`action` | Work on role or member level. It can be on of `member` or `role` and defaults to `role`. | no +`state` | The state to ensure. It can be one of `present`, `absent`, default: `present`. | no + + +Authors +======= + +Rafael Jeffman diff --git a/README.md b/README.md index 246a8b4c..4cc23e93 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Features * Modules for host management * Modules for hostgroup management * Modules for pwpolicy management +* Modules for role management * Modules for service management * Modules for sudocmd management * Modules for sudocmdgroup management @@ -421,6 +422,7 @@ Modules in plugin/modules * [ipahost](README-host.md) * [ipahostgroup](README-hostgroup.md) * [ipapwpolicy](README-pwpolicy.md) +* [iparole](README-role.md) * [ipaservice](README-service.md) * [ipasudocmd](README-sudocmd.md) * [ipasudocmdgroup](README-sudocmdgroup.md) diff --git a/playbooks/role/role-is-absent.yml b/playbooks/role/role-is-absent.yml new file mode 100644 index 00000000..d8d88a1d --- /dev/null +++ b/playbooks/role/role-is-absent.yml @@ -0,0 +1,11 @@ +--- +- name: Playbook to manage IPA role. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + state: absent diff --git a/playbooks/role/role-is-present.yml b/playbooks/role/role-is-present.yml new file mode 100644 index 00000000..89ae6b61 --- /dev/null +++ b/playbooks/role/role-is-present.yml @@ -0,0 +1,11 @@ +--- +- name: Playbook to manage IPA role. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + description: A role in IPA. diff --git a/playbooks/role/role-member-group-absent.yml b/playbooks/role/role-member-group-absent.yml new file mode 100644 index 00000000..c4695f9b --- /dev/null +++ b/playbooks/role/role-member-group-absent.yml @@ -0,0 +1,14 @@ +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + group: + - group01 + action: member + state: absent diff --git a/playbooks/role/role-member-group-present.yml b/playbooks/role/role-member-group-present.yml new file mode 100644 index 00000000..c14c7ec2 --- /dev/null +++ b/playbooks/role/role-member-group-present.yml @@ -0,0 +1,13 @@ +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + group: + - group01 + action: member diff --git a/playbooks/role/role-member-host-absent.yml b/playbooks/role/role-member-host-absent.yml new file mode 100644 index 00000000..8acaeb28 --- /dev/null +++ b/playbooks/role/role-member-host-absent.yml @@ -0,0 +1,14 @@ +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + host: + - host01.example.com + action: member + state: absent diff --git a/playbooks/role/role-member-host-present.yml b/playbooks/role/role-member-host-present.yml new file mode 100644 index 00000000..58359797 --- /dev/null +++ b/playbooks/role/role-member-host-present.yml @@ -0,0 +1,13 @@ +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + host: + - host01.example.com + action: member diff --git a/playbooks/role/role-member-hostgroup-absent.yml b/playbooks/role/role-member-hostgroup-absent.yml new file mode 100644 index 00000000..ee07f97d --- /dev/null +++ b/playbooks/role/role-member-hostgroup-absent.yml @@ -0,0 +1,14 @@ +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + hostgroup: + - hostgroup01 + action: member + state: absent diff --git a/playbooks/role/role-member-hostgroup-present.yml b/playbooks/role/role-member-hostgroup-present.yml new file mode 100644 index 00000000..2caf9a2d --- /dev/null +++ b/playbooks/role/role-member-hostgroup-present.yml @@ -0,0 +1,13 @@ +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + hostgroup: + - hostgroup01 + action: member diff --git a/playbooks/role/role-member-privilege-absent.yml b/playbooks/role/role-member-privilege-absent.yml new file mode 100644 index 00000000..f6033904 --- /dev/null +++ b/playbooks/role/role-member-privilege-absent.yml @@ -0,0 +1,15 @@ +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + privilege: + - Group Administrators + - User Administrators + action: member + state: absent diff --git a/playbooks/role/role-member-privilege-present.yml b/playbooks/role/role-member-privilege-present.yml new file mode 100644 index 00000000..837e989f --- /dev/null +++ b/playbooks/role/role-member-privilege-present.yml @@ -0,0 +1,14 @@ +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + privilege: + - Group Administrators + - User Administrators + action: member diff --git a/playbooks/role/role-member-service-absent.yml b/playbooks/role/role-member-service-absent.yml new file mode 100644 index 00000000..595047cf --- /dev/null +++ b/playbooks/role/role-member-service-absent.yml @@ -0,0 +1,14 @@ +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + service: + - http/www.example.com + action: member + state: absent diff --git a/playbooks/role/role-member-service-present.yml b/playbooks/role/role-member-service-present.yml new file mode 100644 index 00000000..98dc9bea --- /dev/null +++ b/playbooks/role/role-member-service-present.yml @@ -0,0 +1,13 @@ +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + service: + - service01 + action: member diff --git a/playbooks/role/role-member-user-absent.yml b/playbooks/role/role-member-user-absent.yml new file mode 100644 index 00000000..3efda216 --- /dev/null +++ b/playbooks/role/role-member-user-absent.yml @@ -0,0 +1,14 @@ +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + user: + - pinky + action: member + state: absent diff --git a/playbooks/role/role-member-user-present.yml b/playbooks/role/role-member-user-present.yml new file mode 100644 index 00000000..02a39be8 --- /dev/null +++ b/playbooks/role/role-member-user-present.yml @@ -0,0 +1,13 @@ +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + user: + - pinky + action: member diff --git a/playbooks/role/role-members-absent.yml b/playbooks/role/role-members-absent.yml new file mode 100644 index 00000000..aedd81cb --- /dev/null +++ b/playbooks/role/role-members-absent.yml @@ -0,0 +1,25 @@ +--- +- name: Playbook to manage IPA role member. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + user: + - pinky + group: + - group01 + host: + - host01.example.com + hostgroup: + - hostgroup01 + privilege: + - Group Administrators + - User Administrators + service: + - service01 + action: member + state: absent diff --git a/playbooks/role/role-members-present.yml b/playbooks/role/role-members-present.yml new file mode 100644 index 00000000..d659c1f5 --- /dev/null +++ b/playbooks/role/role-members-present.yml @@ -0,0 +1,23 @@ +--- +- name: Playbook to manage IPA role with members. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + user: + - pinky + group: + - group01 + host: + - host01.example.com + hostgroup: + - hostgroup01 + privilege: + - Group Administrators + - User Administrators + service: + - service01 diff --git a/playbooks/role/role-rename.yml b/playbooks/role/role-rename.yml new file mode 100644 index 00000000..9d078f52 --- /dev/null +++ b/playbooks/role/role-rename.yml @@ -0,0 +1,11 @@ +--- +- name: Playbook to manage IPA role. + hosts: ipaserver + become: yes + gather_facts: no + + tasks: + - iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + rename: anotherrole diff --git a/plugins/modules/iparole.py b/plugins/modules/iparole.py new file mode 100644 index 00000000..cc6f6a8f --- /dev/null +++ b/plugins/modules/iparole.py @@ -0,0 +1,485 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""ansible-freeipa iparole module implementation.""" + +# Authors: +# Rafael Guterres Jeffman <rjeffman@redhat.com> +# +# Copyright (C) 2020 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + + +DOCUMENTATION = """ +--- +module: ipaservice +short description: Manage FreeIPA service +description: Manage FreeIPA service +options: + ipaadmin_principal: + description: The admin principal. + default: admin + ipaadmin_password: + description: The admin password. + required: false + role: + description: The list of role name strings. + required: true + aliases: ["cn"] + description: + descrpition: A description for the role. + required: false + rename: + descrpition: Rename the role object. + required: false + user: + description: List of users. + required: false + group: + description: List of groups. + required: false + host: + description: List of hosts. + required: false + hostgroup: + description: List of hostgroups. + required: false + service: + description: List of services. + required: false + action: + description: Work on service or member level. + choices: ["role", "member"] + default: role + required: false + state: + description: The state to ensure. + choices: ["present", "absent"] + default: present + required: true +""" + +EXAMPLES = """ +- name: Ensure a role named `somerole` is present. + iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + +- name: Ensure user `pinky` is a memmer of role `somerole`. + iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + user: + - pinky + action: member + +- name: Ensure a role named `somerole` is absent. + iparole: + ipaadmin_password: SomeADMINpassword + name: somerole + state: absent +""" + +# pylint: disable=wrong-import-position +# pylint: disable=import-error +# pylint: disable=no-name-in-module +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text +from ansible.module_utils.ansible_freeipa_module import \ + temp_kinit, temp_kdestroy, valid_creds, api_connect, api_command, \ + gen_add_del_lists, compare_args_ipa, module_params_get, api_get_realm +import six + + +if six.PY3: + unicode = str + + +def find_role(module, name): + """Find if a role with the given name already exist.""" + try: + _result = api_command(module, "role_show", name, {"all": True}) + except Exception: # pylint: disable=broad-except + # An exception is raised if role name is not found. + return None + else: + return _result["result"] + + +def gen_args(module): + """Generate arguments for executing commands.""" + arg_map = { + "description": "description", + "rename": "rename", + } + args = {} + + for param, arg in arg_map.items(): + value = module_params_get(module, param) + if value is not None: + args[arg] = value + + return args + + +def check_parameters(module): + """Check if parameters passed for module processing are valid.""" + action = module_params_get(module, "action") + state = module_params_get(module, "state") + + invalid = [] + + if state == "present": + if action == "member": + invalid.extend(['description', 'rename']) + + if state == "absent": + invalid.extend(['description', 'rename']) + if action != "member": + invalid.extend(['privilege']) + + for arg in invalid: + if module_params_get(module, arg) is not None: + module.fail_json( + msg="Argument '%s' can not be used with action '%s'" % + (arg, state)) + + +def verify_credentials(module): + """Ensure there are valid Kerberos credentials.""" + ccache_dir = None + ccache_name = None + + ipaadmin_principal = module_params_get(module, "ipaadmin_principal") + ipaadmin_password = module_params_get(module, "ipaadmin_password") + + if not valid_creds(module, ipaadmin_principal): + ccache_dir, ccache_name = temp_kinit(ipaadmin_principal, + ipaadmin_password) + + return (ccache_dir, ccache_name) + + +def member_intersect(module, attr, memberof, res_find): + """Filter member arguments from role found by intersection.""" + params = module_params_get(module, attr) + if not res_find: + return params + filtered = [] + if params: + existing = res_find.get(memberof, []) + filtered = list(set(params) & set(existing)) + return filtered + + +def member_difference(module, attr, memberof, res_find): + """Filter member arguments from role found by difference.""" + params = module_params_get(module, attr) + if not res_find: + return params + filtered = [] + if params: + existing = res_find.get(memberof, []) + filtered = list(set(params) - set(existing)) + return filtered + + +def ensure_absent_state(module, name, action, res_find): + """Define commands to ensure absent state.""" + commands = [] + + if action == "role": + commands.append([name, 'role_del', {}]) + + if action == "member": + + members = member_intersect( + module, 'privilege', 'memberof_privilege', res_find) + if members: + commands.append([name, "role_remove_privilege", + {"privilege": members}]) + + member_args = {} + for key in ['user', 'group', 'host', 'hostgroup']: + items = member_intersect( + module, key, 'member_%s' % key, res_find) + if items: + member_args[key] = items + + _services = filter_service(module, res_find, + lambda res, svc: res.startswith(svc)) + if _services: + member_args['service'] = _services + + # Only add remove command if there's at least one member no manage. + if member_args: + commands.append([name, "role_remove_member", member_args]) + + return commands + + +def filter_service(module, res_find, predicate): + """ + Filter service based on predicate. + + Compare service name with existing ones matching + at least until `@` from principal name. + + Predicate is a callable that accepts the existing service, and the + modified service to be compared to. + """ + _services = [] + service = module_params_get(module, 'service') + if service: + existing = [to_text(x) for x in res_find.get('member_service', [])] + for svc in service: + svc = svc if '@' in svc else ('%s@' % svc) + found = [x for x in existing if predicate(x, svc)] + _services.extend(found) + return _services + + +def ensure_role_with_members_is_present(module, name, res_find): + """Define commands to ensure member are present for action `role`.""" + commands = [] + privilege_add, privilege_del = gen_add_del_lists( + module_params_get(module, "privilege"), + res_find.get('memberof_privilege', [])) + + if privilege_add: + commands.append([name, "role_add_privilege", + {"privilege": privilege_add}]) + if privilege_del: + commands.append([name, "role_remove_privilege", + {"privilege": privilege_del}]) + + add_members = {} + del_members = {} + + for key in ["user", "group", "host", "hostgroup"]: + add_list, del_list = gen_add_del_lists( + module_params_get(module, key), + res_find.get('member_%s' % key, []) + ) + if add_list: + add_members[key] = add_list + if del_list: + del_members[key] = [to_text(item) for item in del_list] + + service = [ + to_text(svc) if '@' in svc else ('%s@%s' % (svc, api_get_realm())) + for svc in (module_params_get(module, 'service') or []) + ] + existing = [str(svc) for svc in res_find.get('member_service', [])] + add_list, del_list = gen_add_del_lists(service, existing) + if add_list: + add_members['service'] = add_list + if del_list: + del_members['service'] = [to_text(item) for item in del_list] + + if add_members: + commands.append([name, "role_add_member", add_members]) + if del_members: + commands.append([name, "role_remove_member", del_members]) + + return commands + + +def ensure_members_are_present(module, name, res_find): + """Define commands to ensure members are present for action `member`.""" + commands = [] + + members = member_difference( + module, 'privilege', 'memberof_privilege', res_find) + if members: + commands.append([name, "role_add_privilege", + {"privilege": members}]) + + member_args = {} + for key in ['user', 'group', 'host', 'hostgroup']: + items = member_difference( + module, key, 'member_%s' % key, res_find) + if items: + member_args[key] = items + + _services = filter_service(module, res_find, + lambda res, svc: not res.startswith(svc)) + if _services: + member_args['service'] = _services + + if member_args: + commands.append([name, "role_add_member", member_args]) + + return commands + + +def process_command_failures(command, result): + """Process the result of a command, looking for errors.""" + # Get all errors + # All "already a member" and "not a member" failures in the + # result are ignored. All others are reported. + errors = [] + if "failed" in result and len(result["failed"]) > 0: + for item in result["failed"]: + failed_item = result["failed"][item] + for member_type in failed_item: + for member, failure in failed_item[member_type]: + if "already a member" in failure \ + or "not a member" in failure: + continue + errors.append("%s: %s %s: %s" % ( + command, member_type, member, failure)) + return errors + + +def process_commands(module, commands): + """Process the list of IPA API commands.""" + errors = [] + exit_args = {} + changed = False + for name, command, args in commands: + try: + result = api_command(module, command, name, args) + if "completed" in result: + if result["completed"] > 0: + changed = True + else: + changed = True + + errors = process_command_failures(command, result) + except Exception as exception: # pylint: disable=broad-except + module.fail_json( + msg="%s: %s: %s" % (command, name, str(exception))) + + if errors: + module.fail_json(msg=", ".join(errors)) + + return changed, exit_args + + +def role_commands_for_name(module, state, action, name): + """Define commands for the Role module.""" + commands = [] + + rename = module_params_get(module, "rename") + + res_find = find_role(module, name) + + if state == "present": + args = gen_args(module) + + if action == "role": + if res_find is None: + if rename is not None: + module.fail_json(msg="Cannot `rename` inexistent role.") + commands.append([name, 'role_add', args]) + res_find = {} + else: + if not compare_args_ipa(module, args, res_find): + commands.append([name, 'role_mod', args]) + + if action == "member": + if res_find is None: + module.fail_json(msg="No role '%s'" % name) + + cmds = ensure_role_with_members_is_present(module, name, res_find) + commands.extend(cmds) + + if state == "absent" and res_find is not None: + cmds = ensure_absent_state(module, name, action, res_find) + commands.extend(cmds) + + return commands + + +def create_module(): + """Create module description.""" + ansible_module = AnsibleModule( + argument_spec=dict( + # generalgroups + ipaadmin_principal=dict(type="str", default="admin"), + ipaadmin_password=dict(type="str", required=False, no_log=True), + + name=dict(type="list", aliases=["cn"], default=None, + required=True), + # present + description=dict(required=False, type="str", default=None), + rename=dict(required=False, type="str", default=None), + + # members + privilege=dict(required=False, type='list', default=None), + user=dict(required=False, type='list', default=None), + group=dict(required=False, type='list', default=None), + host=dict(required=False, type='list', default=None), + hostgroup=dict(required=False, type='list', default=None), + service=dict(required=False, type='list', default=None), + + # state + action=dict(type="str", default="role", + choices=["role", "member"]), + state=dict(type="str", default="present", + choices=["present", "absent"]), + ), + supports_check_mode=True, + mutually_exclusive=[], + required_one_of=[] + ) + + ansible_module._ansible_debug = True # pylint: disable=protected-access + + return ansible_module + + +def main(): + """Process role module script.""" + ansible_module = create_module() + check_parameters(ansible_module) + + # Init + ccache_dir = None + ccache_name = None + try: + ccache_dir, ccache_name = verify_credentials(ansible_module) + api_connect() + + state = module_params_get(ansible_module, "state") + action = module_params_get(ansible_module, "action") + names = module_params_get(ansible_module, "name") + commands = [] + + for name in names: + cmds = role_commands_for_name(ansible_module, state, action, name) + commands.extend(cmds) + + changed, exit_args = process_commands(ansible_module, commands) + + except Exception as exception: # pylint: disable=broad-except + ansible_module.fail_json(msg=str(exception)) + + finally: + temp_kdestroy(ccache_dir, ccache_name) + + # Done + ansible_module.exit_json(changed=changed, **exit_args) + + +if __name__ == "__main__": + main() diff --git a/tests/role/env_cleanup.yml b/tests/role/env_cleanup.yml new file mode 100644 index 00000000..0a84df30 --- /dev/null +++ b/tests/role/env_cleanup.yml @@ -0,0 +1,38 @@ +--- +- name: Ensure test user is absent. + ipauser: + ipaadmin_password: SomeADMINpassword + name: user01 + state: absent + +- name: Ensure test group is absent. + ipagroup: + ipaadmin_password: SomeADMINpassword + name: group01 + state: absent + +- name: Ensure test hostgroup is absent. + ipahostgroup: + ipaadmin_password: SomeADMINpassword + name: hostgroup01 + state: absent + +- name: Ensure test host is absent. + ipahost: + ipaadmin_password: SomeADMINpassword + name: "{{ host1_fqdn }}" + state: absent + +- name: Ensure test service is absent. + ipaservice: + ipaadmin_password: SomeADMINpassword + name: "service01/{{ host1_fqdn }}" + state: absent + +- name: Ensure test roles are absent. + iparole: + ipaadmin_password: SomeADMINpassword + name: + - renamerole + - testrole + state: absent diff --git a/tests/role/env_facts.yml b/tests/role/env_facts.yml new file mode 100644 index 00000000..785751d2 --- /dev/null +++ b/tests/role/env_facts.yml @@ -0,0 +1,14 @@ +--- +- name: Get Domain from server name + set_fact: + ipaserver_domain: "{{ ansible_fqdn | join ('.') }}" + when: ipaserver_domain is not defined + +- name: Set fact for realm name + set_fact: + ipaserver_realm: "{{ ipaserver_domain }} | upper" + when: ipaserver_domain is not defined + +- name: Create FQDN for host01 + set_fact: + host1_fqdn: "host01.{{ ipaserver_domain }}" diff --git a/tests/role/env_setup.yml b/tests/role/env_setup.yml new file mode 100644 index 00000000..2a876c40 --- /dev/null +++ b/tests/role/env_setup.yml @@ -0,0 +1,34 @@ +--- +- name: Cleanup environment. + import_tasks: env_cleanup.yml + +- name: Ensure test user is present. + ipauser: + ipaadmin_password: SomeADMINpassword + name: user01 + first: First + last: Last + +- name: Ensure test group is present. + ipagroup: + ipaadmin_password: SomeADMINpassword + name: group01 + +- name: Ensure test host is present. + ipahost: + ipaadmin_password: SomeADMINpassword + name: "{{ host1_fqdn }}" + force: yes + +- name: Ensure test hostgroup is present. + ipahostgroup: + ipaadmin_password: SomeADMINpassword + name: hostgroup01 + host: + - "{{ host1_fqdn }}" + +- name: Ensure test service is present. + ipaservice: + ipaadmin_password: SomeADMINpassword + name: "service01/{{ host1_fqdn }}" + force: yes diff --git a/tests/role/test_role.yml b/tests/role/test_role.yml new file mode 100644 index 00000000..f72a9321 --- /dev/null +++ b/tests/role/test_role.yml @@ -0,0 +1,388 @@ +--- +- name: Test role module + hosts: ipaserver + become: yes + gather_facts: yes + + tasks: + - name: Set environment facts. + import_tasks: env_facts.yml + + - name: Setup environment. + import_tasks: env_setup.yml + + # tests + - name: Ensure role is present. + iparole: + ipaadmin_password: SomeADMINpassword + name: renamerole + description: A role in IPA. + register: result + failed_when: not result.changed + + - name: Ensure role is present, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: renamerole + description: A role in IPA. + register: result + failed_when: result.changed + + - name: Rename role. + iparole: + ipaadmin_password: SomeADMINpassword + name: renamerole + rename: testrole + register: result + failed_when: not result.changed + + - name: Rename role, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: renamerole + rename: testrole + register: result + failed_when: result.changed + + - name: Ensure role has member has privileges. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + privilege: + - DNS Servers + - Host Administrators + action: member + register: result + failed_when: not result.changed + + - name: Ensure role has member has privileges, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + privilege: + - DNS Servers + - Host Administrators + action: member + register: result + failed_when: result.changed + + - name: Ensure role has less privileges. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + privilege: + - Host Administrators + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Ensure role has less privileges, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + privilege: + - Host Administrators + action: member + state: absent + register: result + failed_when: result.changed + + - name: Ensure role has member has privileges restored. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + privilege: + - DNS Servers + - Host Administrators + action: member + register: result + failed_when: not result.changed + + - name: Ensure role has member has privileges restored, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + privilege: + - DNS Servers + - Host Administrators + action: member + register: result + failed_when: result.changed + + - name: Ensure role member privileges are absent. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + privilege: + - DNS Servers + - Host Administrators + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Ensure role member privileges are absent, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + privilege: + - DNS Servers + - Host Administrators + action: member + state: absent + register: result + failed_when: result.changed + + - name: Ensure invalid privileged is not assigned to role. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + privilege: Invalid Privilege + action: member + register: result + failed_when: not result.failed or "privilege not found" not in result.msg + + - name: Ensure role has member user present. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + user: + - user01 + action: member + register: result + failed_when: not result.changed + + - name: Ensure role has member user present, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + user: + - user01 + action: member + register: result + failed_when: result.changed + + - name: Ensure role has member user absent. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + user: + - user01 + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Ensure role has member user absent, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + user: + - user01 + action: member + state: absent + register: result + failed_when: result.changed + + - name: Ensure role has member group present. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + group: + - group01 + action: member + register: result + failed_when: not result.changed + + - name: Ensure role has member group present, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + group: + - group01 + action: member + register: result + failed_when: result.changed + + - name: Ensure role has member group absent. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + group: + - group01 + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Ensure role has member group absent, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + group: + - group01 + action: member + state: absent + register: result + failed_when: result.changed + + - name: Ensure role has member host present. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + host: + - "{{ host1_fqdn }}" + action: member + register: result + failed_when: not result.changed + + - name: Ensure role has member host present, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + host: + - "{{ host1_fqdn }}" + action: member + register: result + failed_when: result.changed + + - name: Ensure role has member host absent. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + host: + - "{{ host1_fqdn }}" + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Ensure role has member host absent, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + host: + - "{{ host1_fqdn }}" + action: member + state: absent + register: result + failed_when: result.changed + + - name: Ensure role has member hostgroup present. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + hostgroup: + - hostgroup01 + action: member + register: result + failed_when: not result.changed + + - name: Ensure role has member hostgroup present, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + hostgroup: + - hostgroup01 + action: member + register: result + failed_when: result.changed + + - name: Ensure role has member hostgroup absent. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + hostgroup: + - hostgroup01 + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Ensure role has member hostgroup absent, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + hostgroup: + - hostgroup01 + action: member + state: absent + register: result + failed_when: result.changed + + - name: Ensure role is absent. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + state: absent + register: result + failed_when: not result.changed + + - name: Ensure role is absent, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + state: absent + register: result + failed_when: result.changed + + - name: Ensure role with members is present. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + user: + - user01 + group: + - group01 + host: + - "{{ host1_fqdn }}" + hostgroup: + - hostgroup01 + privilege: + - Group Administrators + - User Administrators + service: + - "service01/{{ host1_fqdn }}" + register: result + failed_when: not result.changed + + - name: Ensure role with members is present, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + user: + - user01 + group: + - group01 + host: + - "{{ host1_fqdn }}" + hostgroup: + - hostgroup01 + privilege: + - Group Administrators + - User Administrators + service: + - "service01/{{ host1_fqdn }}" + register: result + failed_when: result.changed + + - name: Ensure role is absent. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + state: absent + register: result + failed_when: not result.changed + + - name: Ensure role is absent, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + state: absent + register: result + failed_when: result.changed + + # cleanup + - name: Cleanup environment. + include_tasks: env_cleanup.yml diff --git a/tests/role/test_role_service_member.yml b/tests/role/test_role_service_member.yml new file mode 100644 index 00000000..065cbce7 --- /dev/null +++ b/tests/role/test_role_service_member.yml @@ -0,0 +1,95 @@ +--- +- name: Test service member in role module. + hosts: ipaserver + become: yes + gather_facts: yes + + tasks: + - name: Set environment facts. + import_tasks: env_facts.yml + + - name: Setup environment. + import_tasks: env_setup.yml + + # tests + + - name: Ensure role with member service is present. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + service: + - "service01/{{ host1_fqdn }}" + register: result + failed_when: not result.changed + + - name: Ensure role with member service is present, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + service: + - "service01/{{ host1_fqdn }}" + action: member + register: result + failed_when: result.changed + + - name: Ensure role has member service absent. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + service: + - "service01/{{ host1_fqdn }}" + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Ensure role has member service absent, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + service: + - "service01/{{ host1_fqdn }}" + action: member + state: absent + register: result + failed_when: result.changed + + - name: Ensure role has member service with principal name. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + service: + - "service01/{{ host1_fqdn }}@{{ ipaserver_realm }}" + action: member + register: result + failed_when: not result.changed + + - name: Ensure role has member service with principal name, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + service: + - "service01/{{ host1_fqdn }}@{{ ipaserver_realm }}" + action: member + register: result + failed_when: result.changed + + - name: Ensure role is absent. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + state: absent + register: result + failed_when: not result.changed + + - name: Ensure role is absent, again. + iparole: + ipaadmin_password: SomeADMINpassword + name: testrole + state: absent + register: result + failed_when: result.changed + + # cleanup + - name: Cleanup environment. + include_tasks: env_cleanup.yml -- GitLab