2021-01-06 17:17:07 +01:00
|
|
|
#!/usr/bin/python
|
|
|
|
# -*- coding: utf-8 -*-
|
2022-08-05 12:28:29 +02:00
|
|
|
# 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
|
2021-01-06 17:17:07 +01:00
|
|
|
|
|
|
|
from __future__ import absolute_import, division, print_function
|
|
|
|
__metaclass__ = type
|
|
|
|
|
|
|
|
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
|
2023-02-24 09:25:31 +01:00
|
|
|
attributes:
|
|
|
|
check_mode:
|
|
|
|
support: full
|
|
|
|
diff_mode:
|
|
|
|
support: none
|
2021-01-06 17:17:07 +01:00
|
|
|
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
|
2023-12-31 15:43:52 +01:00
|
|
|
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
|
2021-01-06 17:17:07 +01:00
|
|
|
extends_documentation_fragment:
|
2023-02-24 09:25:31 +01:00
|
|
|
- community.general.ipa.documentation
|
|
|
|
- community.general.attributes
|
2021-01-06 17:17:07 +01:00
|
|
|
'''
|
|
|
|
|
|
|
|
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'
|
2023-12-31 15:43:52 +01:00
|
|
|
minlength: '6'
|
2021-01-06 17:17:07 +01:00
|
|
|
maxfailcount: '4'
|
|
|
|
failinterval: '600'
|
|
|
|
lockouttime: '1200'
|
2023-12-31 15:43:52 +01:00
|
|
|
gracelimit: 3
|
|
|
|
maxrepeat: 3
|
|
|
|
maxsequence: 3
|
|
|
|
dictcheck: true
|
|
|
|
usercheck: true
|
2021-01-06 17:17:07 +01:00
|
|
|
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
|
2021-06-26 23:59:11 +02:00
|
|
|
from ansible.module_utils.common.text.converters import to_native
|
2021-01-06 17:17:07 +01:00
|
|
|
|
|
|
|
|
|
|
|
class PwPolicyIPAClient(IPAClient):
|
|
|
|
'''The global policy will be selected when `name` is `None`'''
|
|
|
|
def __init__(self, module, host, port, protocol):
|
|
|
|
super(PwPolicyIPAClient, self).__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,
|
2023-12-31 15:43:52 +01:00
|
|
|
lockouttime=None, gracelimit=None, maxrepeat=None, maxsequence=None, dictcheck=None, usercheck=None):
|
2021-01-06 17:17:07 +01:00
|
|
|
pwpolicy = {}
|
2023-12-31 15:43:52 +01:00
|
|
|
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] = to_native(value)
|
|
|
|
|
|
|
|
for option, value in pwpolicy_boolean_options.items():
|
|
|
|
if value is not None:
|
|
|
|
pwpolicy[option] = bool(value)
|
2021-01-06 17:17:07 +01:00
|
|
|
|
|
|
|
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'),
|
2023-12-31 15:43:52 +01:00
|
|
|
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'),
|
|
|
|
)
|
2021-01-06 17:17:07 +01:00
|
|
|
|
|
|
|
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'),
|
2023-12-31 15:43:52 +01:00
|
|
|
lockouttime=dict(type='str'),
|
|
|
|
gracelimit=dict(type='int'),
|
|
|
|
maxrepeat=dict(type='int'),
|
|
|
|
maxsequence=dict(type='int'),
|
|
|
|
dictcheck=dict(type='bool'),
|
|
|
|
usercheck=dict(type='bool'),
|
|
|
|
)
|
2021-01-06 17:17:07 +01:00
|
|
|
|
|
|
|
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=to_native(e), exception=traceback.format_exc())
|
|
|
|
|
|
|
|
module.exit_json(changed=changed, pwpolicy=pwpolicy)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|