From 58725364c14e6ba261efc2ab17010645af3f53c0 Mon Sep 17 00:00:00 2001 From: Rafael Guterres Jeffman <rjeffman@redhat.com> Date: Fri, 4 Nov 2022 22:36:43 -0300 Subject: [PATCH] pwpolicy: Add support for password check and grace limit. On recent versions of FreeIPA option to verify passwords and for controlling a password grace period have been added to IPA API. This patch adds support for the parameters maxrepeat, maxsequence, dictcheck and usercheck, available since FreeIPA, 4.9 and gracelimit, available since FreeIPA 4.9.10. Test playbooks for the module have been updated with the new supported parameters. New example playbooks can be found at: playbooks/pwpolicy/pwpolicy_grace_limit.yml playbooks/pwpolicy/pwpolicy_password_check.yml --- README-pwpolicy.md | 35 +++++ playbooks/pwpolicy/pwpolicy_grace_limit.yml | 11 ++ .../pwpolicy/pwpolicy_password_check.yml | 14 ++ plugins/modules/ipapwpolicy.py | 108 ++++++++++++- tests/pwpolicy/test_pwpolicy.yml | 145 ++++++++++++++++++ 5 files changed, 310 insertions(+), 3 deletions(-) create mode 100644 playbooks/pwpolicy/pwpolicy_grace_limit.yml create mode 100644 playbooks/pwpolicy/pwpolicy_password_check.yml diff --git a/README-pwpolicy.md b/README-pwpolicy.md index 7e1eb898..7fd05164 100644 --- a/README-pwpolicy.md +++ b/README-pwpolicy.md @@ -87,6 +87,36 @@ Example playbook to ensure maxlife is set to 49 in global policy: maxlife: 49 ``` +Example playbook to ensure password grace period is set to 3 in global policy: + +```yaml +--- +- name: Playbook to handle pwpolicies + hosts: ipaserver + become: true + + tasks: + # Ensure maxlife is set to 49 in global policy + - ipapwpolicy: + ipaadmin_password: SomeADMINpassword + gracelimit: 3 +``` + +Example playbook to ensure password grace period is set to unlimited in global policy: + +```yaml +--- +- name: Playbook to handle pwpolicies + hosts: ipaserver + become: true + + tasks: + # Ensure maxlife is set to 49 in global policy + - ipapwpolicy: + ipaadmin_password: SomeADMINpassword + gracelimit: -1 +``` + Variables ========= @@ -107,6 +137,11 @@ Variable | Description | Required `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 +`maxrepeat` \| `ipapwdmaxrepeat` | Maximum number of same consecutive characters. Requires IPA 4.9+ (int) | no +`maxsequence` \| `ipapwdmaxsequence` | The maximum length of monotonic character sequences (abcd). Requires IPA 4.9+ (int) | no +`dictcheck` \| `ipapwdictcheck` | Check if the password is a dictionary word. Requires IPA 4.9+ (int) | no +`usercheck` \| `ipapwdusercheck` | Check if the password contains the username. Requires IPA 4.9+ (int) | no +`gracelimit` \| `passwordgracelimit` | Number of LDAP authentications allowed after expiration. Requires IPA 4.9.10 (int) | no `state` | The state to ensure. It can be one of `present` or `absent`, default: `present`. | yes diff --git a/playbooks/pwpolicy/pwpolicy_grace_limit.yml b/playbooks/pwpolicy/pwpolicy_grace_limit.yml new file mode 100644 index 00000000..e1ed3076 --- /dev/null +++ b/playbooks/pwpolicy/pwpolicy_grace_limit.yml @@ -0,0 +1,11 @@ +--- +- name: Playbook to manage password policy + hosts: ipaserver + become: no + gather_facts: no + + tasks: + - name: Set password policy grace limit. + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + gracelimit: 3 diff --git a/playbooks/pwpolicy/pwpolicy_password_check.yml b/playbooks/pwpolicy/pwpolicy_password_check.yml new file mode 100644 index 00000000..6ae237e0 --- /dev/null +++ b/playbooks/pwpolicy/pwpolicy_password_check.yml @@ -0,0 +1,14 @@ +--- +- name: Playbook to manage password policy + hosts: ipaserver + become: no + gather_facts: no + + tasks: + - name: Set password checking parameters. + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + maxrepeat: 2 + maxsequence: 3 + dictcheck: yes + usercheck: yes diff --git a/plugins/modules/ipapwpolicy.py b/plugins/modules/ipapwpolicy.py index c29bb27b..4398c4e8 100644 --- a/plugins/modules/ipapwpolicy.py +++ b/plugins/modules/ipapwpolicy.py @@ -2,6 +2,7 @@ # Authors: # Thomas Woerner <twoerner@redhat.com> +# Rafael Guterres Jeffman <rjeffman@redhat.com> # # Copyright (C) 2019-2022 Red Hat # see file 'COPYING' for use and warranty information @@ -88,6 +89,41 @@ options: type: int required: false aliases: ["krbpwdlockoutduration"] + maxrepeat: + description: > + Maximum number of same consecutive characters. + Requires IPA 4.9+ + type: int + required: false + aliases: ["ipapwdmaxrepeat"] + maxsequence: + description: > + The maximum length of monotonic character sequences (abcd). + Requires IPA 4.9+ + type: int + required: false + aliases: ["ipapwdmaxsequence"] + dictcheck: + description: > + Check if the password is a dictionary word. + Requires IPA 4.9+ + type: bool + required: false + aliases: ["ipapwdictcheck"] + usercheck: + description: > + Check if the password contains the username. + Requires IPA 4.9+ + type: bool + required: false + aliases: ["ipapwdusercheck"] + gracelimit: + description: > + Number of LDAP authentications allowed after expiration. + Requires IPA 4.10.1+ + type: int + required: false + aliases: ["passwordgracelimit"] state: description: State to ensure type: str @@ -95,6 +131,7 @@ options: choices: ["present", "absent"] author: - Thomas Woerner (@t-woerner) + - Rafael Guterres Jeffman (@rjeffman) """ EXAMPLES = """ @@ -135,7 +172,8 @@ def find_pwpolicy(module, name): def gen_args(maxlife, minlife, history, minclasses, minlength, priority, - maxfail, failinterval, lockouttime): + maxfail, failinterval, lockouttime, maxrepeat, maxsequence, + dictcheck, usercheck, gracelimit): _args = {} if maxlife is not None: _args["krbmaxpwdlife"] = maxlife @@ -155,10 +193,47 @@ def gen_args(maxlife, minlife, history, minclasses, minlength, priority, _args["krbpwdfailurecountinterval"] = failinterval if lockouttime is not None: _args["krbpwdlockoutduration"] = lockouttime + if maxrepeat is not None: + _args["ipapwdmaxrepeat"] = maxrepeat + if maxsequence is not None: + _args["ipapwdmaxrsequence"] = maxsequence + if dictcheck is not None: + _args["ipapwddictcheck"] = dictcheck + if usercheck is not None: + _args["ipapwdusercheck"] = usercheck + if gracelimit is not None: + _args["passwordgracelimit"] = gracelimit return _args +def check_supported_params( + module, maxrepeat, maxsequence, dictcheck, usercheck, gracelimit +): + # All password checking parameters were added by the same commit, + # so we only need to test one of them. + has_password_check = module.ipa_command_param_exists( + "pwpolicy_add", "ipapwdmaxrepeat") + # check if gracelimit is supported + has_gracelimit = module.ipa_command_param_exists( + "pwpolicy_add", "passwordgracelimit") + + # If needed, report unsupported password checking paramteres + if not has_password_check: + check_password_params = [maxrepeat, maxsequence, dictcheck, usercheck] + unsupported = [ + x for x in check_password_params if x is not None + ] + if unsupported: + module.fail_json( + msg="Your IPA version does not support arguments: " + "maxrepeat, maxsequence, dictcheck, usercheck.") + + if gracelimit is not None and not has_gracelimit: + module.fail_json( + msg="Your IPA version does not support 'gracelimit'.") + + def main(): ansible_module = IPAAnsibleModule( argument_spec=dict( @@ -183,6 +258,16 @@ def main(): default=None), lockouttime=dict(type="int", aliases=["krbpwdlockoutduration"], default=None), + maxrepeat=dict(type="int", aliases=["ipapwdmaxrepeat"], + default=None), + maxsequence=dict(type="int", aliases=["ipapwdmaxsequence"], + default=None), + dictcheck=dict(type="bool", aliases=["ipapwdictcheck"], + default=None), + usercheck=dict(type="bool", aliases=["ipapwusercheck"], + default=None), + gracelimit=dict(type="int", aliases=["passwordgracelimit"], + default=None), # state state=dict(type="str", default="present", choices=["present", "absent"]), @@ -207,6 +292,11 @@ def main(): maxfail = ansible_module.params_get("maxfail") failinterval = ansible_module.params_get("failinterval") lockouttime = ansible_module.params_get("lockouttime") + maxrepeat = ansible_module.params_get("maxrepeat") + maxsequence = ansible_module.params_get("maxsequence") + dictcheck = ansible_module.params_get("dictcheck") + usercheck = ansible_module.params_get("usercheck") + gracelimit = ansible_module.params_get("gracelimit") # state state = ansible_module.params_get("state") @@ -230,10 +320,16 @@ def main(): msg="'global_policy' can not be made absent.") invalid = ["maxlife", "minlife", "history", "minclasses", "minlength", "priority", "maxfail", "failinterval", - "lockouttime"] + "lockouttime", "maxrepeat", "maxsequence", "dictcheck", + "usercheck", "gracelimit"] ansible_module.params_fail_used_invalid(invalid, state) + if gracelimit is not None: + if gracelimit < -1: + ansible_module.fail_json( + msg="'gracelimit' must be no less than -1") + # Init changed = False @@ -241,6 +337,11 @@ def main(): with ansible_module.ipa_connect(): + check_supported_params( + ansible_module, maxrepeat, maxsequence, dictcheck, usercheck, + gracelimit + ) + commands = [] for name in names: @@ -252,7 +353,8 @@ def main(): # Generate args args = gen_args(maxlife, minlife, history, minclasses, minlength, priority, maxfail, failinterval, - lockouttime) + lockouttime, maxrepeat, maxsequence, dictcheck, + usercheck, gracelimit) # Found the pwpolicy if res_find is not None: diff --git a/tests/pwpolicy/test_pwpolicy.yml b/tests/pwpolicy/test_pwpolicy.yml index 5ac18d76..e9868934 100644 --- a/tests/pwpolicy/test_pwpolicy.yml +++ b/tests/pwpolicy/test_pwpolicy.yml @@ -5,6 +5,9 @@ gather_facts: false tasks: + - name: Setup FreeIPA test facts. + import_tasks: ../env_freeipa_facts.yml + - name: Ensure maxlife of 90 for global_policy ipapwpolicy: ipaadmin_password: SomeADMINpassword @@ -117,3 +120,145 @@ state: absent register: result failed_when: result.changed or result.failed + + - block: + - name: Ensure maxrepeat of 2 for global_policy + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + maxrepeat: 2 + register: result + failed_when: not result.changed or result.failed + + - name: Ensure maxrepeat of 2 for global_policy, again + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + maxrepeat: 2 + register: result + failed_when: result.changed or result.failed + + - name: Ensure maxrepeat of 0 for global_policy + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + maxrepeat: 0 + register: result + failed_when: not result.changed or result.failed + + - name: Ensure maxsequence of 4 for global_policy + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + maxrepeat: 4 + register: result + failed_when: not result.changed or result.failed + + - name: Ensure maxsequence of 4 for global_policy, again + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + maxrepeat: 4 + register: result + failed_when: result.changed or result.failed + + - name: Ensure maxsequence of 0 for global_policy + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + maxrepeat: 0 + register: result + failed_when: not result.changed or result.failed + + - name: Ensure dictcheck is set for global_policy + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + dictcheck: yes + register: result + failed_when: not result.changed or result.failed + + - name: Ensure dictcheck is set for global_policy, again + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + dictcheck: yes + register: result + failed_when: result.changed or result.failed + + - name: Ensure dictcheck is not set for global_policy + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + dictcheck: no + register: result + failed_when: not result.changed or result.failed + + - name: Ensure usercheck is set for global_policy + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + usercheck: yes + register: result + failed_when: not result.changed or result.failed + + - name: Ensure usercheck is set for global_policy, again + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + usercheck: yes + register: result + failed_when: result.changed or result.failed + + - name: Ensure usercheck is not set for global_policy + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + usercheck: no + register: result + failed_when: not result.changed or result.failed + + when: ipa_version is version("4.9", ">=") + + - block: + - name: Ensure grace limit is set to 10 for global_policy + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + gracelimit: 10 + register: result + failed_when: not result.changed or result.failed + + - name: Ensure grace limit is set to 0 for global_policy + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + gracelimit: 0 + register: result + failed_when: not result.changed or result.failed + + - name: Ensure grace limit is set to 0 for global_policy + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + gracelimit: 0 + register: result + failed_when: result.changed or result.failed + + - name: Ensure grace limit is set to 0 for global_policy + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + gracelimit: -1 + register: result + failed_when: not result.changed or result.failed + + - name: Ensure grace limit is not set to -2 for global_policy + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + gracelimit: -2 + register: result + failed_when: not result.failed and "must be at least -1" not in result.msg + + when: ipa_version is version("4.9.10", ">=") -- GitLab