diff --git a/README-pwpolicy.md b/README-pwpolicy.md index 7e1eb8982766be7bba90e92d367ebf020ab69dd8..7fd051643b6514be4ae07fa7d3c72e7160f9bb5f 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 0000000000000000000000000000000000000000..e1ed3076f2add621e8f7dca74acf20791a8b2a5a --- /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 0000000000000000000000000000000000000000..6ae237e06b7629938d33b824807c7b3c6e0ae2d7 --- /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 c29bb27b770047baec7e88aae0cad15a7ce6131b..4398c4e8f96d309ca8004f8bf43f75e7be9130d5 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 5ac18d769cf66d3f8b5a65ce60331a16b81eb495..e98689349aa55dfa1ce28d1e605edbf211d26f8a 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", ">=")