From 19a94ac476a49f34333024b1f69465b15ed05a3b Mon Sep 17 00:00:00 2001 From: Rafael Guterres Jeffman <rjeffman@redhat.com> Date: Thu, 20 Aug 2020 19:06:55 -0300 Subject: [PATCH] New privilege management module There is a new privilege management module placed in the plugins folder: plugins/modules/ipaprivilege.py The privilege module allows to ensure presence or absence of privilege and manage privilege permission memebers. Here is the documentation for the module: README-privilege.md New example playbooks have been added: playbooks/privilege/privilege-absent.yml playbooks/privilege/privilege-member-absent.yml playbooks/privilege/privilege-member-present.yml playbooks/privilege/privilege-present.yml New tests for the module: tests/privilege/test_privilege.yml --- README-privilege.md | 147 ++++++++ playbooks/privilege/privilege-absent.yml | 10 + .../privilege/privilege-member-absent.yml | 14 + .../privilege/privilege-member-present.yml | 15 + playbooks/privilege/privilege-present.yml | 11 + plugins/modules/ipaprivilege.py | 357 ++++++++++++++++++ tests/privilege/test_privilege.yml | 151 ++++++++ 7 files changed, 705 insertions(+) create mode 100644 README-privilege.md create mode 100644 playbooks/privilege/privilege-absent.yml create mode 100644 playbooks/privilege/privilege-member-absent.yml create mode 100644 playbooks/privilege/privilege-member-present.yml create mode 100644 playbooks/privilege/privilege-present.yml create mode 100644 plugins/modules/ipaprivilege.py create mode 100644 tests/privilege/test_privilege.yml diff --git a/README-privilege.md b/README-privilege.md new file mode 100644 index 00000000..ddb78a1a --- /dev/null +++ b/README-privilege.md @@ -0,0 +1,147 @@ +Privilege module +================ + +Description +----------- + +The privilege module allows to ensure presence and absence of privileges and privilege members. + +Features +-------- + +* Privilege management + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.4.0 and up are supported by the ipaprivilege 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 privilege "Broad Privilege" is present: + +```yaml +--- +- name: Playbook to manage IPA privilege. + hosts: ipaserver + become: yes + + tasks: + - ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + description: Broad Privilege +``` + +Example playbook to make sure privilege "Broad Privilege" member permission has multiple values: + +```yaml +--- +- name: Playbook to manage IPA privilege permission member. + hosts: ipaserver + become: yes + + tasks: + - ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + permission: + - "Write IPA Configuration" + - "System: Write DNS Configuration" + - "System: Update DNS Entries" + action: member +``` + + +Example playbook to make sure privilege "Broad Privilege" member permission 'Write IPA Configuration' is absent: + + +```yaml +--- +- name: Playbook to manage IPA privilege permission member. + hosts: ipaserver + become: yes + + tasks: + - ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + permission: + - "Write IPA Configuration" + action: member + state: absent +``` + +Example playbook to rename privilege "Broad Privilege" to "DNS Special Privilege": + +```yaml +--- +- name: Playbook to manage IPA privilege. + hosts: ipaserver + become: yes + + tasks: + - ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + rename: DNS Special Privilege + state: renamed +``` + +Example playbook to make sure privilege "DNS Special Privilege" is absent: + +```yaml +--- +- name: Playbook to manage IPA privilege. + hosts: ipaserver + become: yes + - name: Ensure privilege Broad Privilege is absent + ipaadmin_password: SomeADMINpassword + name: DNS Special Privilege + state: absent +``` + + +Variables +--------- + +ipaprivilege +------------ + +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 privilege name strings. | yes +`description` | Privilege description. | no +`rename` \| `new_name` | Rename the privilege object. | no +`permission` | Permissions to be added to the privilege. | no +`action` | Work on privilege or member level. It can be one of `member` or `privilege` and defaults to `privilege`. | no +`state` | The state to ensure. It can be one of `present`, `absent` or `renamed`, default: `present`. | no + + +Authors +======= + +Rafael Guterres Jeffman diff --git a/playbooks/privilege/privilege-absent.yml b/playbooks/privilege/privilege-absent.yml new file mode 100644 index 00000000..6a72536e --- /dev/null +++ b/playbooks/privilege/privilege-absent.yml @@ -0,0 +1,10 @@ +--- +- name: Privilege absent example + hosts: ipaserver + become: true + + tasks: + - name: Ensure privilege "Broad Privilege" is absent + ipaprivilege: + name: Broad Privilege + state: absent diff --git a/playbooks/privilege/privilege-member-absent.yml b/playbooks/privilege/privilege-member-absent.yml new file mode 100644 index 00000000..00fb4ada --- /dev/null +++ b/playbooks/privilege/privilege-member-absent.yml @@ -0,0 +1,14 @@ +--- +- name: Privilege absent example + hosts: ipaserver + become: true + + tasks: + - name: Ensure privilege "Broad Privilege" permission is absent + ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + permission: + - "System: Write IPA Configuration" + action: member + state: absent diff --git a/playbooks/privilege/privilege-member-present.yml b/playbooks/privilege/privilege-member-present.yml new file mode 100644 index 00000000..f6be3030 --- /dev/null +++ b/playbooks/privilege/privilege-member-present.yml @@ -0,0 +1,15 @@ +--- +- name: Privilege member present example + hosts: ipaserver + become: true + + tasks: + - name: Ensure privilege "Broad Privilege" permissions are present + ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + permission: + - "System: Write IPA Configuration" + - "System: Write DNS Configuration" + - "System: Update DNS Entries" + action: member diff --git a/playbooks/privilege/privilege-present.yml b/playbooks/privilege/privilege-present.yml new file mode 100644 index 00000000..4db46352 --- /dev/null +++ b/playbooks/privilege/privilege-present.yml @@ -0,0 +1,11 @@ +--- +- name: Privilege present example + hosts: ipaserver + become: true + + tasks: + - name: Ensure privilege Broad Privilege is present + ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + description: Broad Privilege diff --git a/plugins/modules/ipaprivilege.py b/plugins/modules/ipaprivilege.py new file mode 100644 index 00000000..355e2ca5 --- /dev/null +++ b/plugins/modules/ipaprivilege.py @@ -0,0 +1,357 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# 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-freeipa module to manage FreeIPA privileges.""" + + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + +DOCUMENTATION = """ +--- +module: ipaprivilege +short description: Manage FreeIPA privilege +description: Manage FreeIPA privilege and privilege members +options: + ipaadmin_principal: + description: The admin principal. + default: admin + ipaadmin_password: + description: The admin password. + required: false + name: + description: The list of privilege name strings. + required: true + aliases: ["cn"] + description: + description: Privilege description + required: false + rename: + description: Rename the privilege object. + required: false + aliases: ["new_name"] + permission: + description: Permissions to be added to the privilege. + required: false + action: + description: Work on privilege or member level. + choices: ["privilege", "member"] + default: privilege + required: false + state: + description: The state to ensure. + choices: ["present", "absent", "renamed"] + default: present + required: true +""" + +EXAMPLES = """ +# Ensure privilege "Broad Privilege" is present +- ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + description: Broad Privilege + +# Ensure privilege "Broad Privilege" has permissions set +- ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + permission: + - "Write IPA Configuration" + - "System: Write DNS Configuration" + - "System: Update DNS Entries" + action: member + +# Ensure privilege member permission 'Write IPA Configuration' is absent +- ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + permission: + - "Write IPA Configuration" + action: member + state: absent + +# Rename privilege "Broad Privilege" to "DNS Special Privilege" +- ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + rename: DNS Special Privilege + state: renamed + +# Ensure privilege "DNS Special Privilege" is absent +- ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: DNS Special Privilege + state: absent +""" + +RETURN = """ +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_freeipa_module import \ + temp_kinit, temp_kdestroy, valid_creds, api_connect, api_command, \ + compare_args_ipa, module_params_get, gen_add_del_lists +import six + +if six.PY3: + unicode = str + + +def find_privilege(module, name): + """Find if a privilege with the given name already exist.""" + try: + _result = api_command(module, "privilege_show", name, {"all": True}) + except Exception: # pylint: disable=broad-except + # An exception is raised if privilege name is not found. + return None + else: + return _result["result"] + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # general + 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, + aliases=["new_name"], ), + permission=dict(required=False, type='list', default=None), + action=dict(type="str", default="privilege", + choices=["member", "privilege"]), + # state + state=dict(type="str", default="present", + choices=["present", "absent", "renamed"]), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + + # Get parameters + + # general + ipaadmin_principal = module_params_get(ansible_module, + "ipaadmin_principal") + ipaadmin_password = module_params_get(ansible_module, "ipaadmin_password") + names = module_params_get(ansible_module, "name") + + # present + description = module_params_get(ansible_module, "description") + permission = module_params_get(ansible_module, "permission") + rename = module_params_get(ansible_module, "rename") + action = module_params_get(ansible_module, "action") + + # state + state = module_params_get(ansible_module, "state") + + # Check parameters + invalid = [] + + if state == "present": + if len(names) != 1: + ansible_module.fail_json( + msg="Only one privilege be added at a time.") + if action == "member": + invalid = ["description"] + + if state == "absent": + if len(names) < 1: + ansible_module.fail_json(msg="No name given.") + invalid = ["description", "rename"] + if action == "privilege": + invalid.append("permission") + + if state == "renamed": + if len(names) != 1: + ansible_module.fail_json( + msg="Only one privilege be added at a time.") + invalid = ["description", "permission"] + if action != "privilege": + ansible_module.fail_json( + msg="Action '%s' can not be used with state '%s'" + % (action, state)) + + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with action " + "'%s' and state '%s'" % (x, action, state)) + + # Init + + changed = False + exit_args = {} + ccache_dir = None + ccache_name = None + try: + if not valid_creds(ansible_module, ipaadmin_principal): + ccache_dir, ccache_name = temp_kinit(ipaadmin_principal, + ipaadmin_password) + api_connect() + + commands = [] + for name in names: + # Make sure privilege exists + res_find = find_privilege(ansible_module, name) + + # Create command + if state == "present": + + args = {} + if description: + args['description'] = description + + if action == "privilege": + # Found the privilege + if res_find is not None: + # For all settings is args, check if there are + # different settings in the find result. + # If yes: modify + if not compare_args_ipa(ansible_module, args, + res_find): + commands.append([name, "privilege_mod", args]) + else: + commands.append([name, "privilege_add", args]) + + member_args = {} + if permission: + member_args['permission'] = permission + + if not compare_args_ipa(ansible_module, member_args, + res_find): + + # Generate addition and removal lists + permission_add, permission_del = gen_add_del_lists( + permission, res_find.get("member_permission")) + + # Add members + if len(permission_add) > 0: + commands.append([name, "privilege_add_permission", + { + "permission": permission_add, + }]) + # Remove members + if len(permission_del) > 0: + commands.append([ + name, + "privilege_remove_permission", + {"permission": permission_del} + ]) + + elif action == "member": + if res_find is None: + ansible_module.fail_json( + msg="No privilege '%s'" % name) + + if permission is None: + ansible_module.fail_json(msg="No permission given") + + commands.append([name, "privilege_add_permission", + {"permission": permission}]) + + elif state == "absent": + if action == "privilege": + if res_find is not None: + commands.append([name, "privilege_del", {}]) + + elif action == "member": + if res_find is None: + ansible_module.fail_json( + msg="No privilege '%s'" % name) + + if permission is None: + ansible_module.fail_json(msg="No permission given") + + commands.append([name, "privilege_remove_permission", + { + "permission": permission, + }]) + + elif state == "renamed": + if not rename: + ansible_module.fail_json(msg="No rename value given.") + + if res_find is None: + ansible_module.fail_json( + msg="No privilege found to be renamed: '%s'" % (name)) + + if name != rename: + commands.append( + [name, "privilege_mod", {"rename": rename}]) + + else: + ansible_module.fail_json(msg="Unkown state '%s'" % state) + + # Execute commands + + for name, command, args in commands: + try: + result = api_command(ansible_module, command, name, + args) + if "completed" in result: + if result["completed"] > 0: + changed = True + else: + changed = True + except Exception as e: + ansible_module.fail_json( + msg="%s: %s: %s" % (command, name, str(e))) + # Get all errors + # All "already a member" and "not a member" failures in the + # result are ignored. All others are reported. + errors = [] + for failed_item in result.get("failed", []): + failed = result["failed"][failed_item] + for member_type in failed: + for member, failure in failed[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)) + if len(errors) > 0: + ansible_module.fail_json(msg=", ".join(errors)) + + except Exception as e: + ansible_module.fail_json(msg=str(e)) + + finally: + temp_kdestroy(ccache_dir, ccache_name) + + # Done + + ansible_module.exit_json(changed=changed, **exit_args) + + +if __name__ == "__main__": + main() diff --git a/tests/privilege/test_privilege.yml b/tests/privilege/test_privilege.yml new file mode 100644 index 00000000..2a13187d --- /dev/null +++ b/tests/privilege/test_privilege.yml @@ -0,0 +1,151 @@ +--- +- name: Test privilege + hosts: ipaserver + become: true + + tasks: + + # CLEANUP TEST ITEMS + + - name: Ensure privilege "Broad Privilege" is absent + ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: + - Broad Privilege + - DNS Privilege + state: absent + + # CREATE TEST ITEMS + + # TESTS + + - name: Ensure privilege Broad Privilege is present + ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + description: Broad Privilege + register: result + failed_when: not result.changed or result.failed + + - name: Ensure privilege Broad Privilege is present again + ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + description: Broad Privilege + register: result + failed_when: result.changed or result.failed + + - name: Change privilege Broad Privilege description + ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + description: Broad Privilege description + register: result + failed_when: not result.changed or result.failed + + - name: Ensure privilege Broad Privilege has permissions + ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + permission: + - "Write IPA Configuration" + - "System: Write DNS Configuration" + - "System: Update DNS Entries" + action: member + register: result + failed_when: not result.changed or result.failed + + - name: Ensure privilege Broad Privilege has permissions, again + ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + permission: + - "Write IPA Configuration" + - "System: Write DNS Configuration" + - "System: Update DNS Entries" + action: member + register: result + failed_when: result.changed or result.failed + + - name: Ensure privilege Broad Privilege member permission "Write IPA Configuration" is absent + ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + permission: + - "Write IPA Configuration" + action: member + state: absent + register: result + failed_when: not result.changed or result.failed + + - name: Ensure privilege Broad Privilege member permission "Write IPA Configuration" is absent again + ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + permission: + - "Write IPA Configuration" + action: member + state: absent + register: result + failed_when: result.changed or result.failed + + - name: Ensure privilege Broad Privilege is absent + ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + state: absent + register: result + failed_when: not result.changed or result.failed + + - name: Ensure privilege Broad Privilege is present + ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + register: result + failed_when: not result.changed or result.failed + + - name: Ensure privilege Broad Privilege is renamed to "DNS Privilege" + ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + rename: DNS Privilege + state: renamed + register: result + failed_when: not result.changed or result.failed + + - name: Ensure privilege Broad Privilege cannot be renamed, because it does not exist. + ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: Broad Privilege + rename: DNS Privilege + state: renamed + register: result + failed_when: not result.failed or "No privilege found to be renamed" not in result.msg + + - name: Ensure privilege cannot be renamed to the same name. + ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: DNS Privilege + rename: DNS Privilege + state: renamed + register: result + failed_when: result.changed or result.failed + + - name: Ensure privilege cannot be renamed to the same name. + ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: DNS Privilege + rename: DNS Privilege + state: renamed + register: result + failed_when: result.changed or result.failed + + # CLEANUP TEST ITEMS + + - name: Ensure privilege testing privileges are absent + ipaprivilege: + ipaadmin_password: SomeADMINpassword + name: + - Broad Privilege + - DNS Privilege + state: absent -- GitLab