#!/usr/bin/python
# -*- coding: utf-8 -*-
# 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 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
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.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
from ansible.module_utils.common.text.converters 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, 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] = to_native(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=to_native(e), exception=traceback.format_exc())

    module.exit_json(changed=changed, pwpolicy=pwpolicy)


if __name__ == '__main__':
    main()
