#!/usr/bin/python # Copyright (c) 2020, Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations DOCUMENTATION = r""" module: ipa_pwpolicy author: Adralioh (@adralioh) short_description: Manage FreeIPA password policies description: - Add, modify, or delete a password policy using the IPA API. version_added: 2.0.0 attributes: check_mode: support: full diff_mode: support: none options: group: description: - Name of the group that the policy applies to. - If omitted, the global policy is used. aliases: ["name"] type: str state: description: State to ensure. default: "present" choices: ["absent", "present"] type: str maxpwdlife: description: Maximum password lifetime (in days). type: str minpwdlife: description: Minimum password lifetime (in hours). type: str historylength: description: - Number of previous passwords that are remembered. - Users cannot reuse remembered passwords. type: str minclasses: description: Minimum number of character classes. type: str minlength: description: Minimum password length. type: str priority: description: - Priority of the policy. - High number means lower priority. - Required when C(cn) is not the global policy. type: str maxfailcount: description: Maximum number of consecutive failures before lockout. type: str failinterval: description: Period (in seconds) after which the number of failed login attempts is reset. type: str lockouttime: description: Period (in seconds) for which users are locked out. type: str gracelimit: description: Maximum number of LDAP logins after password expiration. type: int version_added: 8.2.0 maxrepeat: description: Maximum number of allowed same consecutive characters in the new password. type: int version_added: 8.2.0 maxsequence: description: Maximum length of monotonic character sequences in the new password. An example of a monotonic sequence of length 5 is V(12345). type: int version_added: 8.2.0 dictcheck: description: Check whether the password (with possible modifications) matches a word in a dictionary (using cracklib). type: bool version_added: 8.2.0 usercheck: description: Check whether the password (with possible modifications) contains the user name in some form (if the name has > 3 characters). type: bool version_added: 8.2.0 extends_documentation_fragment: - community.general.ipa.documentation - community.general.ipa.connection_notes - community.general.attributes """ EXAMPLES = r""" - name: Modify the global password policy community.general.ipa_pwpolicy: maxpwdlife: '90' minpwdlife: '1' historylength: '8' minclasses: '3' minlength: '16' maxfailcount: '6' failinterval: '60' lockouttime: '600' ipa_host: ipa.example.com ipa_user: admin ipa_pass: topsecret - name: Ensure the password policy for the group admins is present community.general.ipa_pwpolicy: group: admins state: present maxpwdlife: '60' minpwdlife: '24' historylength: '16' minclasses: '4' priority: '10' minlength: '6' maxfailcount: '4' failinterval: '600' lockouttime: '1200' gracelimit: 3 maxrepeat: 3 maxsequence: 3 dictcheck: true usercheck: true ipa_host: ipa.example.com ipa_user: admin ipa_pass: topsecret - name: Ensure that the group sysops does not have a unique password policy community.general.ipa_pwpolicy: group: sysops state: absent ipa_host: ipa.example.com ipa_user: admin ipa_pass: topsecret """ RETURN = r""" pwpolicy: description: Password policy as returned by IPA API. returned: always type: dict sample: cn: ['admins'] cospriority: ['10'] dn: 'cn=admins,cn=EXAMPLE.COM,cn=kerberos,dc=example,dc=com' krbmaxpwdlife: ['60'] krbminpwdlife: ['24'] krbpwdfailurecountinterval: ['600'] krbpwdhistorylength: ['16'] krbpwdlockoutduration: ['1200'] krbpwdmaxfailure: ['4'] krbpwdmindiffchars: ['4'] objectclass: ['top', 'nscontainer', 'krbpwdpolicy'] """ import traceback from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec class PwPolicyIPAClient(IPAClient): """The global policy will be selected when `name` is `None`""" def __init__(self, module, host, port, protocol): super().__init__(module, host, port, protocol) def pwpolicy_find(self, name): if name is None: # Manually set the cn to the global policy because pwpolicy_find will return a random # different policy if cn is `None` name = "global_policy" return self._post_json(method="pwpolicy_find", name=None, item={"all": True, "cn": name}) def pwpolicy_add(self, name, item): return self._post_json(method="pwpolicy_add", name=name, item=item) def pwpolicy_mod(self, name, item): return self._post_json(method="pwpolicy_mod", name=name, item=item) def pwpolicy_del(self, name): return self._post_json(method="pwpolicy_del", name=name) def get_pwpolicy_dict( maxpwdlife=None, minpwdlife=None, historylength=None, minclasses=None, minlength=None, priority=None, maxfailcount=None, failinterval=None, lockouttime=None, gracelimit=None, maxrepeat=None, maxsequence=None, dictcheck=None, usercheck=None, ): pwpolicy = {} pwpolicy_options = { "krbmaxpwdlife": maxpwdlife, "krbminpwdlife": minpwdlife, "krbpwdhistorylength": historylength, "krbpwdmindiffchars": minclasses, "krbpwdminlength": minlength, "cospriority": priority, "krbpwdmaxfailure": maxfailcount, "krbpwdfailurecountinterval": failinterval, "krbpwdlockoutduration": lockouttime, "passwordgracelimit": gracelimit, "ipapwdmaxrepeat": maxrepeat, "ipapwdmaxsequence": maxsequence, } pwpolicy_boolean_options = { "ipapwddictcheck": dictcheck, "ipapwdusercheck": usercheck, } for option, value in pwpolicy_options.items(): if value is not None: pwpolicy[option] = str(value) for option, value in pwpolicy_boolean_options.items(): if value is not None: pwpolicy[option] = bool(value) return pwpolicy def get_pwpolicy_diff(client, ipa_pwpolicy, module_pwpolicy): return client.get_diff(ipa_data=ipa_pwpolicy, module_data=module_pwpolicy) def ensure(module, client): state = module.params["state"] name = module.params["group"] module_pwpolicy = get_pwpolicy_dict( maxpwdlife=module.params.get("maxpwdlife"), minpwdlife=module.params.get("minpwdlife"), historylength=module.params.get("historylength"), minclasses=module.params.get("minclasses"), minlength=module.params.get("minlength"), priority=module.params.get("priority"), maxfailcount=module.params.get("maxfailcount"), failinterval=module.params.get("failinterval"), lockouttime=module.params.get("lockouttime"), gracelimit=module.params.get("gracelimit"), maxrepeat=module.params.get("maxrepeat"), maxsequence=module.params.get("maxsequence"), dictcheck=module.params.get("dictcheck"), usercheck=module.params.get("usercheck"), ) ipa_pwpolicy = client.pwpolicy_find(name=name) changed = False if state == "present": if not ipa_pwpolicy: changed = True if not module.check_mode: ipa_pwpolicy = client.pwpolicy_add(name=name, item=module_pwpolicy) else: diff = get_pwpolicy_diff(client, ipa_pwpolicy, module_pwpolicy) if len(diff) > 0: changed = True if not module.check_mode: ipa_pwpolicy = client.pwpolicy_mod(name=name, item=module_pwpolicy) else: if ipa_pwpolicy: changed = True if not module.check_mode: client.pwpolicy_del(name=name) return changed, ipa_pwpolicy def main(): argument_spec = ipa_argument_spec() argument_spec.update( group=dict(type="str", aliases=["name"]), state=dict(type="str", default="present", choices=["present", "absent"]), maxpwdlife=dict(type="str"), minpwdlife=dict(type="str"), historylength=dict(type="str"), minclasses=dict(type="str"), minlength=dict(type="str"), priority=dict(type="str"), maxfailcount=dict(type="str"), failinterval=dict(type="str"), lockouttime=dict(type="str"), gracelimit=dict(type="int"), maxrepeat=dict(type="int"), maxsequence=dict(type="int"), dictcheck=dict(type="bool"), usercheck=dict(type="bool"), ) module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) client = PwPolicyIPAClient( module=module, host=module.params["ipa_host"], port=module.params["ipa_port"], protocol=module.params["ipa_prot"], ) try: client.login(username=module.params["ipa_user"], password=module.params["ipa_pass"]) changed, pwpolicy = ensure(module, client) except Exception as e: module.fail_json(msg=f"{e}", exception=traceback.format_exc()) module.exit_json(changed=changed, pwpolicy=pwpolicy) if __name__ == "__main__": main()