From 74fcb0335e5416abc0663a6b4363c0f4ac63f330 Mon Sep 17 00:00:00 2001 From: adralioh Date: Wed, 6 Jan 2021 16:17:07 +0000 Subject: [PATCH] Add ipa_pwpolicy module (#1147) * Add ipa_pwpolicy module Used for modifying FreeIPA password policies Functions similarly to the existing IPA modules * Add sample return value to ipa_pwpolicy module * Add unit tests for the ipa_pwpolicy module Also moves the `exit_json` call in the main module outside of the try clause because it was stopping the tests from working * Update version added for the ipa_pwpolicy module * Add check_mode note for the ipa_pwpolicy module * Add missing period in ipa_pwpolicy module doc * Fix tense of the ipa_pwpolicy module description * Reword ipa_pwpolicy documentation Improve the wording of the ipa_pwpolicy documentation to make it more clear * Rename ipa_pwpolicy options to use shorter names --- plugins/modules/identity/ipa/ipa_pwpolicy.py | 255 ++++++++ plugins/modules/ipa_pwpolicy.py | 1 + .../unit/plugins/modules/identity/__init__.py | 0 .../plugins/modules/identity/ipa/__init__.py | 0 .../modules/identity/ipa/test_ipa_pwpolicy.py | 613 ++++++++++++++++++ 5 files changed, 869 insertions(+) create mode 100644 plugins/modules/identity/ipa/ipa_pwpolicy.py create mode 120000 plugins/modules/ipa_pwpolicy.py create mode 100644 tests/unit/plugins/modules/identity/__init__.py create mode 100644 tests/unit/plugins/modules/identity/ipa/__init__.py create mode 100644 tests/unit/plugins/modules/identity/ipa/test_ipa_pwpolicy.py diff --git a/plugins/modules/identity/ipa/ipa_pwpolicy.py b/plugins/modules/identity/ipa/ipa_pwpolicy.py new file mode 100644 index 0000000000..7c694f32ee --- /dev/null +++ b/plugins/modules/identity/ipa/ipa_pwpolicy.py @@ -0,0 +1,255 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2020, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +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 +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 +extends_documentation_fragment: +- community.general.ipa.documentation +notes: +- Supports C(check_mode). +''' + +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' + maxfailcount: '4' + failinterval: '600' + lockouttime: '1200' + 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 +from ansible.module_utils._text import to_native + + +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, + lockouttime=None): + pwpolicy = {} + if maxpwdlife is not None: + pwpolicy['krbmaxpwdlife'] = maxpwdlife + if minpwdlife is not None: + pwpolicy['krbminpwdlife'] = minpwdlife + if historylength is not None: + pwpolicy['krbpwdhistorylength'] = historylength + if minclasses is not None: + pwpolicy['krbpwdmindiffchars'] = minclasses + if minlength is not None: + pwpolicy['krbpwdminlength'] = minlength + if priority is not None: + pwpolicy['cospriority'] = priority + if maxfailcount is not None: + pwpolicy['krbpwdmaxfailure'] = maxfailcount + if failinterval is not None: + pwpolicy['krbpwdfailurecountinterval'] = failinterval + if lockouttime is not None: + pwpolicy['krbpwdlockoutduration'] = lockouttime + + 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')) + + 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')) + + 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() diff --git a/plugins/modules/ipa_pwpolicy.py b/plugins/modules/ipa_pwpolicy.py new file mode 120000 index 0000000000..b35fc0fae4 --- /dev/null +++ b/plugins/modules/ipa_pwpolicy.py @@ -0,0 +1 @@ +./identity/ipa/ipa_pwpolicy.py \ No newline at end of file diff --git a/tests/unit/plugins/modules/identity/__init__.py b/tests/unit/plugins/modules/identity/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/plugins/modules/identity/ipa/__init__.py b/tests/unit/plugins/modules/identity/ipa/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/plugins/modules/identity/ipa/test_ipa_pwpolicy.py b/tests/unit/plugins/modules/identity/ipa/test_ipa_pwpolicy.py new file mode 100644 index 0000000000..22353b89f2 --- /dev/null +++ b/tests/unit/plugins/modules/identity/ipa/test_ipa_pwpolicy.py @@ -0,0 +1,613 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from contextlib import contextmanager + +from ansible_collections.community.general.tests.unit.compat import unittest +from ansible_collections.community.general.tests.unit.compat.mock import call, patch +from ansible_collections.community.general.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args + +from ansible_collections.community.general.plugins.modules.identity.ipa import ipa_pwpolicy + + +@contextmanager +def patch_ipa(**kwargs): + """Mock context manager for patching the methods in PwPolicyIPAClient that contact the IPA server + + Patches the `login` and `_post_json` methods + + Keyword arguments are passed to the mock object that patches `_post_json` + + No arguments are passed to the mock object that patches `login` because no tests require it + + Example:: + + with patch_ipa(return_value={}) as (mock_login, mock_post): + ... + """ + obj = ipa_pwpolicy.PwPolicyIPAClient + with patch.object(obj, 'login') as mock_login: + with patch.object(obj, '_post_json', **kwargs) as mock_post: + yield mock_login, mock_post + + +class TestIPAPwPolicy(ModuleTestCase): + def setUp(self): + super(TestIPAPwPolicy, self).setUp() + self.module = ipa_pwpolicy + + def _test_base(self, module_args, return_value, mock_calls, changed): + """Base function that's called by all the other test functions + + module_args (dict): + Arguments passed to the module + + return_value (dict): + Mocked return value of PwPolicyIPAClient.pwpolicy_find, as returned by the IPA API. + This should be set to the current state. It will be changed to the desired state using the above arguments. + (Technically, this is the return value of _post_json, but it's only checked by pwpolicy_find). + An empty dict means that the policy doesn't exist. + + mock_calls (list/tuple of dicts): + List of calls made to PwPolicyIPAClient._post_json, in order. + _post_json is called by all of the pwpolicy_* methods of the class. + Pass an empty list if no calls are expected. + + changed (bool): + Whether or not the module is supposed to be marked as changed + """ + set_module_args(module_args) + + # Run the module + with patch_ipa(return_value=return_value) as (mock_login, mock_post): + with self.assertRaises(AnsibleExitJson) as exec_info: + self.module.main() + + # Verify that the calls to _post_json match what is expected + expected_call_count = len(mock_calls) + if expected_call_count > 1: + # Convert the call dicts to unittest.mock.call instances because `assert_has_calls` only accepts them + converted_calls = [] + for call_dict in mock_calls: + converted_calls.append(call(**call_dict)) + + mock_post.assert_has_calls(converted_calls) + self.assertEqual(len(mock_post.mock_calls), expected_call_count) + elif expected_call_count == 1: + mock_post.assert_called_once_with(**mock_calls[0]) + else: # expected_call_count is 0 + mock_post.assert_not_called() + + # Verify that the module's changed status matches what is expected + self.assertIs(exec_info.exception.args[0]['changed'], changed) + + def test_add(self): + """Add a new policy""" + module_args = { + 'group': 'admins', + 'state': 'present', + 'priority': '10', + 'maxpwdlife': '90', + 'minpwdlife': '1', + 'historylength': '8', + 'minclasses': '3', + 'minlength': '16', + 'maxfailcount': '6', + 'failinterval': '60', + 'lockouttime': '600' + } + return_value = {} + mock_calls = ( + { + 'method': 'pwpolicy_find', + 'name': None, + 'item': { + 'all': True, + 'cn': 'admins' + } + }, + { + 'method': 'pwpolicy_add', + 'name': 'admins', + 'item': { + 'cospriority': '10', + 'krbmaxpwdlife': '90', + 'krbminpwdlife': '1', + 'krbpwdhistorylength': '8', + 'krbpwdmindiffchars': '3', + 'krbpwdminlength': '16', + 'krbpwdmaxfailure': '6', + 'krbpwdfailurecountinterval': '60', + 'krbpwdlockoutduration': '600' + } + } + ) + changed = True + + self._test_base(module_args, return_value, mock_calls, changed) + + def test_aliases(self): + """Same as test_add, but uses the `name` alias for the `group` option""" + module_args = { + 'name': 'admins', + 'state': 'present', + 'priority': '10', + 'maxpwdlife': '90', + 'minpwdlife': '1', + 'historylength': '8', + 'minclasses': '3', + 'minlength': '16', + 'maxfailcount': '6', + 'failinterval': '60', + 'lockouttime': '600' + } + return_value = {} + mock_calls = ( + { + 'method': 'pwpolicy_find', + 'name': None, + 'item': { + 'all': True, + 'cn': 'admins' + } + }, + { + 'method': 'pwpolicy_add', + 'name': 'admins', + 'item': { + 'cospriority': '10', + 'krbmaxpwdlife': '90', + 'krbminpwdlife': '1', + 'krbpwdhistorylength': '8', + 'krbpwdmindiffchars': '3', + 'krbpwdminlength': '16', + 'krbpwdmaxfailure': '6', + 'krbpwdfailurecountinterval': '60', + 'krbpwdlockoutduration': '600' + } + } + ) + changed = True + + self._test_base(module_args, return_value, mock_calls, changed) + + def test_mod_different_args(self): + """Policy exists, but some of the args are different and need to be modified""" + module_args = { + 'group': 'sysops', + 'state': 'present', + 'priority': '10', + 'maxpwdlife': '60', + 'minpwdlife': '24', + 'historylength': '8', + 'minclasses': '3', + 'minlength': '12', + 'maxfailcount': '8', + 'failinterval': '60', + 'lockouttime': '600' + } + return_value = { + 'cn': ['sysops'], + 'cospriority': ['10'], + 'krbmaxpwdlife': ['90'], + 'krbminpwdlife': ['1'], + 'krbpwdhistorylength': ['8'], + 'krbpwdmindiffchars': ['3'], + 'krbpwdminlength': ['16'], + 'krbpwdmaxfailure': ['6'], + 'krbpwdfailurecountinterval': ['60'], + 'krbpwdlockoutduration': ['600'], + 'dn': 'cn=sysops,cn=EXAMPLE.COM,cn=kerberos,dc=example,dc=com', + 'objectclass': ['top', 'nscontainer', 'krbpwdpolicy'] + } + mock_calls = ( + { + 'method': 'pwpolicy_find', + 'name': None, + 'item': { + 'all': True, + 'cn': 'sysops' + } + }, + { + 'method': 'pwpolicy_mod', + 'name': 'sysops', + 'item': { + 'cospriority': '10', + 'krbmaxpwdlife': '60', + 'krbminpwdlife': '24', + 'krbpwdhistorylength': '8', + 'krbpwdmindiffchars': '3', + 'krbpwdminlength': '12', + 'krbpwdmaxfailure': '8', + 'krbpwdfailurecountinterval': '60', + 'krbpwdlockoutduration': '600' + } + } + ) + changed = True + + self._test_base(module_args, return_value, mock_calls, changed) + + def test_mod_missing_args(self): + """Policy exists, but some of the args aren't set, so need to be added""" + module_args = { + 'group': 'sysops', + 'state': 'present', + 'priority': '10', + 'maxpwdlife': '90', + 'minpwdlife': '1', + 'historylength': '8', + 'minclasses': '3', + 'minlength': '16', + 'maxfailcount': '6', + 'failinterval': '60', + 'lockouttime': '600' + } + return_value = { + 'cn': ['sysops'], + 'cospriority': ['10'], + 'krbmaxpwdlife': ['90'], + 'krbpwdhistorylength': ['8'], + 'krbpwdminlength': ['16'], + 'krbpwdmaxfailure': ['6'], + 'dn': 'cn=sysops,cn=EXAMPLE.COM,cn=kerberos,dc=example,dc=com', + 'objectclass': ['top', 'nscontainer', 'krbpwdpolicy'] + } + mock_calls = ( + { + 'method': 'pwpolicy_find', + 'name': None, + 'item': { + 'all': True, + 'cn': 'sysops' + } + }, + { + 'method': 'pwpolicy_mod', + 'name': 'sysops', + 'item': { + 'cospriority': '10', + 'krbmaxpwdlife': '90', + 'krbminpwdlife': '1', + 'krbpwdhistorylength': '8', + 'krbpwdmindiffchars': '3', + 'krbpwdminlength': '16', + 'krbpwdmaxfailure': '6', + 'krbpwdfailurecountinterval': '60', + 'krbpwdlockoutduration': '600' + } + } + ) + changed = True + + self._test_base(module_args, return_value, mock_calls, changed) + + def test_del(self): + """Policy exists, and state is absent. Needs to be deleted""" + module_args = { + 'group': 'sysops', + 'state': 'absent', + # other arguments are ignored when state is `absent` + 'priority': '10', + 'maxpwdlife': '90', + 'historylength': '8', + 'minlength': '16', + 'maxfailcount': '6' + } + return_value = { + 'cn': ['sysops'], + 'cospriority': ['10'], + 'krbmaxpwdlife': ['90'], + 'krbpwdhistorylength': ['8'], + 'krbpwdminlength': ['16'], + 'krbpwdmaxfailure': ['6'], + 'dn': 'cn=sysops,cn=EXAMPLE.COM,cn=kerberos,dc=example,dc=com', + 'objectclass': ['top', 'nscontainer', 'krbpwdpolicy'] + } + mock_calls = ( + { + 'method': 'pwpolicy_find', + 'name': None, + 'item': { + 'all': True, + 'cn': 'sysops' + } + }, + { + 'method': 'pwpolicy_del', + 'name': 'sysops', + } + ) + changed = True + + self._test_base(module_args, return_value, mock_calls, changed) + + def test_no_change(self): + """Policy already exists. No changes needed""" + module_args = { + 'group': 'admins', + 'state': 'present', + 'priority': '10', + 'maxpwdlife': '90', + 'minpwdlife': '1', + 'historylength': '8', + 'minclasses': '3', + 'minlength': '16', + 'maxfailcount': '6', + 'failinterval': '60', + 'lockouttime': '600' + } + return_value = { + 'cn': ['admins'], + 'cospriority': ['10'], + 'krbmaxpwdlife': ['90'], + 'krbminpwdlife': ['1'], + 'krbpwdhistorylength': ['8'], + 'krbpwdmindiffchars': ['3'], + 'krbpwdminlength': ['16'], + 'krbpwdmaxfailure': ['6'], + 'krbpwdfailurecountinterval': ['60'], + 'krbpwdlockoutduration': ['600'], + 'dn': 'cn=admins,cn=EXAMPLE.COM,cn=kerberos,dc=example,dc=com', + 'objectclass': ['top', 'nscontainer', 'krbpwdpolicy'] + } + mock_calls = [ + { + 'method': 'pwpolicy_find', + 'name': None, + 'item': { + 'all': True, + 'cn': 'admins' + } + } + ] + changed = False + + self._test_base(module_args, return_value, mock_calls, changed) + + def test_del_no_change(self): + """Policy doesn't exist, and state is absent. No change needed""" + module_args = { + 'group': 'sysops', + 'state': 'absent', + # other arguments are ignored when state is `absent` + 'priority': '10', + 'maxpwdlife': '90', + 'historylength': '8', + 'minlength': '16', + 'maxfailcount': '6' + } + return_value = {} + mock_calls = [ + { + 'method': 'pwpolicy_find', + 'name': None, + 'item': { + 'all': True, + 'cn': 'sysops' + } + } + ] + changed = False + + self._test_base(module_args, return_value, mock_calls, changed) + + def test_global(self): + """Modify the global policy""" + module_args = { + 'maxpwdlife': '60', + 'minpwdlife': '24', + 'historylength': '8', + 'minclasses': '3', + 'minlength': '12', + 'maxfailcount': '8', + 'failinterval': '60', + 'lockouttime': '600' + } + return_value = { + 'cn': ['global_policy'], + 'krbmaxpwdlife': ['90'], + 'krbminpwdlife': ['1'], + 'krbpwdmindiffchars': ['3'], + 'krbpwdminlength': ['16'], + 'krbpwdmaxfailure': ['6'], + 'krbpwdfailurecountinterval': ['60'], + 'krbpwdlockoutduration': ['600'], + 'dn': 'cn=global_policy,cn=EXAMPLE.COM,cn=kerberos,dc=example,dc=com', + 'objectclass': ['top', 'nscontainer', 'krbpwdpolicy'] + } + mock_calls = ( + { + 'method': 'pwpolicy_find', + 'name': None, + 'item': { + 'all': True, + 'cn': 'global_policy' + } + }, + { + 'method': 'pwpolicy_mod', + 'name': None, + 'item': { + 'krbmaxpwdlife': '60', + 'krbminpwdlife': '24', + 'krbpwdhistorylength': '8', + 'krbpwdmindiffchars': '3', + 'krbpwdminlength': '12', + 'krbpwdmaxfailure': '8', + 'krbpwdfailurecountinterval': '60', + 'krbpwdlockoutduration': '600' + } + } + ) + changed = True + + self._test_base(module_args, return_value, mock_calls, changed) + + def test_global_no_change(self): + """Global policy already matches the given arguments. No change needed""" + module_args = { + 'maxpwdlife': '90', + 'minpwdlife': '1', + 'historylength': '8', + 'minclasses': '3', + 'minlength': '16', + 'maxfailcount': '6', + 'failinterval': '60', + 'lockouttime': '600' + } + return_value = { + 'cn': ['global_policy'], + 'krbmaxpwdlife': ['90'], + 'krbminpwdlife': ['1'], + 'krbpwdhistorylength': ['8'], + 'krbpwdmindiffchars': ['3'], + 'krbpwdminlength': ['16'], + 'krbpwdmaxfailure': ['6'], + 'krbpwdfailurecountinterval': ['60'], + 'krbpwdlockoutduration': ['600'], + 'dn': 'cn=global_policy,cn=EXAMPLE.COM,cn=kerberos,dc=example,dc=com', + 'objectclass': ['top', 'nscontainer', 'krbpwdpolicy'] + } + mock_calls = [ + { + 'method': 'pwpolicy_find', + 'name': None, + 'item': { + 'all': True, + 'cn': 'global_policy' + } + } + ] + changed = False + + self._test_base(module_args, return_value, mock_calls, changed) + + def test_check_add(self): + """Add a new policy in check mode. pwpolicy_add shouldn't be called""" + module_args = { + '_ansible_check_mode': True, + 'group': 'admins', + 'state': 'present', + 'priority': '10', + 'maxpwdlife': '90', + 'minpwdlife': '1', + 'historylength': '8', + 'minclasses': '3', + 'minlength': '16', + 'maxfailcount': '6', + 'failinterval': '60', + 'lockouttime': '600' + } + return_value = {} + mock_calls = [ + { + 'method': 'pwpolicy_find', + 'name': None, + 'item': { + 'all': True, + 'cn': 'admins' + } + } + ] + changed = True + + self._test_base(module_args, return_value, mock_calls, changed) + + def test_check_mod(self): + """Modify a policy in check mode. pwpolicy_mod shouldn't be called""" + module_args = { + '_ansible_check_mode': True, + 'group': 'sysops', + 'state': 'present', + 'priority': '10', + 'maxpwdlife': '60', + 'minpwdlife': '24', + 'historylength': '8', + 'minclasses': '3', + 'minlength': '12', + 'maxfailcount': '8', + 'failinterval': '60', + 'lockouttime': '600' + } + return_value = { + 'cn': ['sysops'], + 'cospriority': ['10'], + 'krbmaxpwdlife': ['90'], + 'krbminpwdlife': ['1'], + 'krbpwdhistorylength': ['8'], + 'krbpwdmindiffchars': ['3'], + 'krbpwdminlength': ['16'], + 'krbpwdmaxfailure': ['6'], + 'krbpwdfailurecountinterval': ['60'], + 'krbpwdlockoutduration': ['600'], + 'dn': 'cn=sysops,cn=EXAMPLE.COM,cn=kerberos,dc=example,dc=com', + 'objectclass': ['top', 'nscontainer', 'krbpwdpolicy'] + } + mock_calls = [ + { + 'method': 'pwpolicy_find', + 'name': None, + 'item': { + 'all': True, + 'cn': 'sysops' + } + } + ] + changed = True + + self._test_base(module_args, return_value, mock_calls, changed) + + def test_check_del(self): + """Delete a policy in check mode. pwpolicy_del shouldn't be called""" + module_args = { + '_ansible_check_mode': True, + 'group': 'sysops', + 'state': 'absent' + } + return_value = { + 'cn': ['sysops'], + 'cospriority': ['10'], + 'krbmaxpwdlife': ['90'], + 'krbpwdhistorylength': ['8'], + 'krbpwdminlength': ['16'], + 'krbpwdmaxfailure': ['6'], + 'dn': 'cn=sysops,cn=EXAMPLE.COM,cn=kerberos,dc=example,dc=com', + 'objectclass': ['top', 'nscontainer', 'krbpwdpolicy'] + } + mock_calls = [ + { + 'method': 'pwpolicy_find', + 'name': None, + 'item': { + 'all': True, + 'cn': 'sysops' + } + } + ] + changed = True + + self._test_base(module_args, return_value, mock_calls, changed) + + def test_fail_post(self): + """Fail due to an exception raised from _post_json""" + set_module_args({ + 'group': 'admins', + 'state': 'absent' + }) + + with patch_ipa(side_effect=Exception('ERROR MESSAGE')) as (mock_login, mock_post): + with self.assertRaises(AnsibleFailJson) as exec_info: + self.module.main() + + self.assertEqual(exec_info.exception.args[0]['msg'], 'ERROR MESSAGE') + + +if __name__ == '__main__': + unittest.main()