diff --git a/README-pwpolicy.md b/README-pwpolicy.md new file mode 100644 index 0000000000000000000000000000000000000000..16306b7496e0a0a6a3b6d85c63698565f8f2f242 --- /dev/null +++ b/README-pwpolicy.md @@ -0,0 +1,102 @@ +Pwpolicy module +=============== + +Description +----------- + +The pwpolicy module allows to ensure presence and absence of pwpolicies. + + +Features +-------- +* Pwpolicy management + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.4.0 and up are supported by the ipapwpolicy 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 ensure presence of pwpolicies for exisiting group ops: + +```yaml + tasks: + - name: Ensure presence of pwpolicies for group ops + ipapwpolicy: + ipaadmin_password: MyPassword123 + name: ops + minlife: 7 + maxlife: 49 + history: 5 + priority: 1 + lockouttime: 300 + minlength: 8 + maxfail: 3 +``` + +Example playbook to ensure absence of pwpolicies for group ops + +```yaml +--- +- name: Playbook to handle pwpolicies + hosts: ipaserver + become: true + + tasks: + # Ensure absence of pwpolicies for group ops + - ipapwpolicy: + ipaadmin_password: MyPassword123 + name: ops + state: absent +``` + + +Variables +========= + +ipapwpolicy +------- + +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 pwpolicy name strings. | no +`maxlife` \| `krbmaxpwdlife` | Maximum password lifetime in days. (int) | no +`minlife` \| `krbminpwdlife` | Minimum password lifetime in hours. (int) | no +`history` \| `krbpwdhistorylength` | Password history size. (int) | no +`minclasses` \| `krbpwdmindiffchars` | Minimum number of character classes. (int) | no +`minlength` \| `krbpwdminlength` | Minimum length of password. (int) | no +`priority` \| `cospriority` | Priority of the policy, higher number means lower priority. (int) | no +`maxfail` \| `krbpwdmaxfailure` | Consecutive failures before lockout. (int) | no +`failinterval` \| `krbpwdfailurecountinterval` | Period after which failure count will be reset in seconds. (int) | no +`lockouttime` \| `krbpwdlockoutduration` | Period for which lockout is enforced in seconds. (int) | no +`state` | The state to ensure. It can be one of `present` or `absent`, default: `present`. | yes + + +Authors +======= + +Thomas Woerner diff --git a/README.md b/README.md index bc54c23cbd41773593ef0900eb513c91873bca58..9094f206676bcaa3aa31975355ec0a677ec1b6c1 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Features * Modules for group management * Modules for host management * Modules for hostgroup management +* Modules for pwpolicy management * Modules for sudocmd management * Modules for sudocmdgroup management * Modules for topology management @@ -393,6 +394,7 @@ Modules in plugin/modules * [ipagroup](README-group.md) * [ipahost](README-host.md) * [ipahostgroup](README-hostgroup.md) +* [ipapwpolicy](README-pwpolicy.md) * [ipasudocmd](README-sudocmd.md) * [ipasudocmdgroup](README-sudocmdgroup.md) * [ipatopologysegment](README-topology.md) diff --git a/playbooks/pwpolicy/pwpolicy_absent.yml b/playbooks/pwpolicy/pwpolicy_absent.yml new file mode 100644 index 0000000000000000000000000000000000000000..4c61e535d3f8fb0e63e7c27fe6ae304074eb108f --- /dev/null +++ b/playbooks/pwpolicy/pwpolicy_absent.yml @@ -0,0 +1,12 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure absence of pwpolicies for group ops + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + name: ops + state: absent diff --git a/playbooks/pwpolicy/pwpolicy_present.yml b/playbooks/pwpolicy/pwpolicy_present.yml new file mode 100644 index 0000000000000000000000000000000000000000..fab29c45b60f66a7b581638d42a1b4c477dacd6f --- /dev/null +++ b/playbooks/pwpolicy/pwpolicy_present.yml @@ -0,0 +1,20 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure presence of pwpolicies for group ops + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + name: ops + minlife: 7 + maxlife: 49 + history: 5 + priority: 1 + lockouttime: 300 + minlength: 8 + minclasses: 5 + maxfail: 3 + failinterval: 5 diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py index eedb5c285cd7fe8fdaa81aff2f77550ce174723a..12c2b29ce00c5ee00cef89fba69f2ca58d1b1f91 100644 --- a/plugins/module_utils/ansible_freeipa_module.py +++ b/plugins/module_utils/ansible_freeipa_module.py @@ -38,6 +38,11 @@ from ipapython.ipautil import run from ipaplatform.paths import paths from ipalib.krb_utils import get_credentials_if_valid from ansible.module_utils._text import to_text +import six + + +if six.PY3: + unicode = str def valid_creds(module, principal): @@ -185,8 +190,13 @@ def compare_args_ipa(module, args, ipa): # are lists, but not all. if isinstance(ipa_arg, tuple): ipa_arg = list(ipa_arg) - if isinstance(ipa_arg, list) and not isinstance(arg, list): - arg = [arg] + if isinstance(ipa_arg, list): + if not isinstance(arg, list): + arg = [arg] + if isinstance(ipa_arg[0], str) and isinstance(arg[0], int): + arg = [to_text(_arg) for _arg in arg] + if isinstance(ipa_arg[0], unicode) and isinstance(arg[0], int): + arg = [to_text(_arg) for _arg in arg] # module.warn("%s <=> %s" % (arg, ipa_arg)) if set(arg) != set(ipa_arg): # module.warn("DIFFERENT") diff --git a/plugins/modules/ipapwpolicy.py b/plugins/modules/ipapwpolicy.py new file mode 100644 index 0000000000000000000000000000000000000000..9437b5953ec2c047fa526f7d08c39ec837ab6954 --- /dev/null +++ b/plugins/modules/ipapwpolicy.py @@ -0,0 +1,304 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner <twoerner@redhat.com> +# +# Copyright (C) 2019 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: ipapwpolicy +short description: Manage FreeIPA pwpolicies +description: Manage FreeIPA pwpolicies +options: + ipaadmin_principal: + description: The admin principal + default: admin + ipaadmin_password: + description: The admin password + required: false + name: + description: The group name + required: false + aliases: ["cn"] + maxlife: + description: Maximum password lifetime (in days) + type: int + required: false + aliases: ["krbmaxpwdlife"] + minlife: + description: Minimum password lifetime (in hours) + type: int + required: false + aliases: ["krbminpwdlife"] + history: + description: Password history size + type: int + required: false + aliases: ["krbpwdhistorylength"] + minclasses: + description: Minimum number of character classes + type: int + required: false + aliases: ["krbpwdmindiffchars"] + minlength: + description: Minimum length of password + type: int + required: false + aliases: ["krbpwdminlength"] + priority: + description: Priority of the policy (higher number means lower priority) + type: int + required: false + aliases: ["cospriority"] + maxfail: + description: Consecutive failures before lockout + type: int + required: false + aliases: ["krbpwdmaxfailure"] + failinterval: + description: Period after which failure count will be reset (seconds) + type: int + required: false + aliases: ["krbpwdfailurecountinterval"] + lockouttime: + description: Period for which lockout is enforced (seconds) + type: int + required: false + aliases: ["krbpwdlockoutduration"] + state: + description: State to ensure + default: present + choices: ["present", "absent"] +author: + - Thomas Woerner +""" + +EXAMPLES = """ +# Ensure pwpolicy is set for ops +- ipapwpolicy: + ipaadmin_password: MyPassword123 + name: ops + minlife: 7 + maxlife: 49 + history: 5 + priority: 1 + lockouttime: 300 + minlength: 8 +""" + +RETURN = """ +""" + +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, compare_args_ipa + + +def find_pwpolicy(module, name): + _args = { + "all": True, + "cn": to_text(name), + } + + _result = api_command(module, "pwpolicy_find", to_text(name), _args) + + if len(_result["result"]) > 1: + module.fail_json( + msg="There is more than one pwpolicy '%s'" % (name)) + elif len(_result["result"]) == 1: + return _result["result"][0] + else: + return None + + +def gen_args(maxlife, minlife, history, minclasses, minlength, priority, + maxfail, failinterval, lockouttime): + _args = {} + if maxlife is not None: + _args["krbmaxpwdlife"] = maxlife + if minlife is not None: + _args["krbminpwdlife"] = minlife + if history is not None: + _args["krbpwdhistorylength"] = history + if minclasses is not None: + _args["krbpwdmindiffchars"] = minclasses + if minlength is not None: + _args["krbpwdminlength"] = minlength + if priority is not None: + _args["cospriority"] = priority + if maxfail is not None: + _args["krbpwdmaxfailure"] = maxfail + if failinterval is not None: + _args["krbpwdfailurecountinterval"] = failinterval + if lockouttime is not None: + _args["krbpwdlockoutduration"] = lockouttime + + return _args + + +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 + + maxlife=dict(type="int", aliases=["krbmaxpwdlife"], default=None), + minlife=dict(type="int", aliases=["krbminpwdlife"], default=None), + history=dict(type="int", aliases=["krbpwdhistorylength"], + default=None), + minclasses=dict(type="int", aliases=["krbpwdmindiffchars"], + default=None), + minlength=dict(type="int", aliases=["krbpwdminlength"], + default=None), + priority=dict(type="int", aliases=["cospriority"], default=None), + maxfail=dict(type="int", aliases=["krbpwdmaxfailure"], + default=None), + failinterval=dict(type="int", + aliases=["krbpwdfailurecountinterval"], + default=None), + lockouttime=dict(type="int", aliases=["krbpwdlockoutduration"], + default=None), + # state + state=dict(type="str", default="present", + choices=["present", "absent"]), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + + # Get parameters + + # general + ipaadmin_principal = ansible_module.params.get("ipaadmin_principal") + ipaadmin_password = ansible_module.params.get("ipaadmin_password") + names = ansible_module.params.get("name") + + # present + maxlife = ansible_module.params.get("maxlife") + minlife = ansible_module.params.get("minlife") + history = ansible_module.params.get("history") + minclasses = ansible_module.params.get("minclasses") + minlength = ansible_module.params.get("minlength") + priority = ansible_module.params.get("priority") + maxfail = ansible_module.params.get("maxfail") + failinterval = ansible_module.params.get("failinterval") + lockouttime = ansible_module.params.get("lockouttime") + + # state + state = ansible_module.params.get("state") + + # Check parameters + + if state == "present": + if len(names) != 1: + ansible_module.fail_json( + msg="Only one pwpolicy can be set at a time.") + + if state == "absent": + if len(names) < 1: + ansible_module.fail_json( + msg="No name given.") + invalid = ["maxlife", "minlife", "history", "minclasses", + "minlength", "priority", "maxfail", "failinterval", + "lockouttime"] + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with state '%s'" % + (x, 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: + # Try to find pwpolicy + res_find = find_pwpolicy(ansible_module, name) + + # Create command + if state == "present": + # Generate args + args = gen_args(maxlife, minlife, history, minclasses, + minlength, priority, maxfail, failinterval, + lockouttime) + + # Found the pwpolicy + 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, "pwpolicy_mod", args]) + else: + commands.append([name, "pwpolicy_add", args]) + + elif state == "absent": + if res_find is not None: + commands.append([name, "pwpolicy_del", {}]) + + else: + ansible_module.fail_json(msg="Unkown state '%s'" % state) + + # Execute commands + + for name, command, args in commands: + try: + api_command(ansible_module, command, to_text(name), args) + changed = True + except Exception as e: + ansible_module.fail_json(msg="%s: %s: %s" % (command, name, + str(e))) + + 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/pwpolicy/test_pwpolicy.yml b/tests/pwpolicy/test_pwpolicy.yml new file mode 100644 index 0000000000000000000000000000000000000000..5c69345cbea330052266c9a691ed3657c4efb040 --- /dev/null +++ b/tests/pwpolicy/test_pwpolicy.yml @@ -0,0 +1,59 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure presence of group ops + ipagroup: + ipaadmin_password: SomeADMINpassword + name: ops + + - name: Ensure presence of pwpolicies for group ops + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + name: ops + minlife: 7 + maxlife: 49 + history: 5 + priority: 1 + lockouttime: 300 + minlength: 8 + minclasses: 5 + maxfail: 3 + failinterval: 5 + register: result + failed_when: not result.changed + + - name: Ensure presence of pwpolicies for group ops again + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + name: ops + minlife: 7 + maxlife: 49 + history: 5 + priority: 1 + lockouttime: 300 + minlength: 8 + minclasses: 5 + maxfail: 3 + failinterval: 5 + register: result + failed_when: result.changed + + - name: Ensure absence of pwpolicies for group ops + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + name: ops + state: absent + register: result + failed_when: not result.changed + + - name: Ensure absence of pwpolicies for group ops + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + name: ops + state: absent + register: result + failed_when: result.changed