#!/usr/bin/python
# This file is part of Ansible
# 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 = '''
---
module: iam_policy
version_added: 1.0.0
short_description: Manage inline IAM policies for users, groups, and roles
description:
    - Allows uploading or removing inline IAM policies for IAM users, groups or roles.
    - To administer managed policies please see M(community.aws.iam_user), M(community.aws.iam_role),
      M(community.aws.iam_group) and M(community.aws.iam_managed_policy)
options:
  iam_type:
    description:
      - Type of IAM resource.
    required: true
    choices: [ "user", "group", "role"]
    type: str
  iam_name:
    description:
      - Name of IAM resource you wish to target for policy actions. In other words, the user name, group name or role name.
    required: true
    type: str
  policy_name:
    description:
      - The name label for the policy to create or remove.
    required: true
    type: str
  policy_document:
    description:
      - The path to the properly json formatted policy file.
      - Mutually exclusive with I(policy_json).
      - This option has been deprecated and will be removed in 2.14.  The existing behavior can be
        reproduced by using the I(policy_json) option and reading the file using the lookup plugin.
    type: str
  policy_json:
    description:
      - A properly json formatted policy as string.
      - Mutually exclusive with I(policy_document).
      - See U(https://github.com/ansible/ansible/issues/7005#issuecomment-42894813) on how to use it properly.
    type: json
  state:
    description:
      - Whether to create or delete the IAM policy.
    choices: [ "present", "absent"]
    default: present
    type: str
  skip_duplicates:
    description:
      - When I(skip_duplicates=true) the module looks for any policies that match the document you pass in.  If there is a match it will not make
        a new policy object with the same rules.
      - The current default is C(true).  However, this behavior can be confusing and as such the default will change to C(false) in 2.14.  To maintain
        the existing behavior explicitly set I(skip_duplicates=true).
    type: bool

author:
  - "Jonathan I. Davila (@defionscode)"
  - "Dennis Podkovyrin (@sbj-ss)"
extends_documentation_fragment:
- amazon.aws.aws
- amazon.aws.ec2

'''

EXAMPLES = '''
# Create a policy with the name of 'Admin' to the group 'administrators'
- name: Assign a policy called Admin to the administrators group
  community.aws.iam_policy:
    iam_type: group
    iam_name: administrators
    policy_name: Admin
    state: present
    policy_document: admin_policy.json

# Advanced example, create two new groups and add a READ-ONLY policy to both
# groups.
- name: Create Two Groups, Mario and Luigi
  community.aws.iam:
    iam_type: group
    name: "{{ item }}"
    state: present
  loop:
     - Mario
     - Luigi
  register: new_groups

- name: Apply READ-ONLY policy to new groups that have been recently created
  community.aws.iam_policy:
    iam_type: group
    iam_name: "{{ item.created_group.group_name }}"
    policy_name: "READ-ONLY"
    policy_document: readonlypolicy.json
    state: present
  loop: "{{ new_groups.results }}"

# Create a new S3 policy with prefix per user
- name: Create S3 policy from template
  community.aws.iam_policy:
    iam_type: user
    iam_name: "{{ item.user }}"
    policy_name: "s3_limited_access_{{ item.prefix }}"
    state: present
    policy_json: " {{ lookup( 'template', 's3_policy.json.j2') }} "
    loop:
      - user: s3_user
        prefix: s3_user_prefix

'''
import json

try:
    from botocore.exceptions import BotoCoreError, ClientError
except ImportError:
    pass

from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import compare_policies, AWSRetry
from ansible.module_utils.six import string_types


class PolicyError(Exception):
    pass


class Policy:

    def __init__(self, client, name, policy_name, policy_document, policy_json, skip_duplicates, state, check_mode):
        self.client = client
        self.name = name
        self.policy_name = policy_name
        self.policy_document = policy_document
        self.policy_json = policy_json
        self.skip_duplicates = skip_duplicates
        self.state = state
        self.check_mode = check_mode
        self.changed = False

    @staticmethod
    def _iam_type():
        return ''

    def _list(self, name):
        return {}

    def list(self):
        return self._list(self.name).get('PolicyNames', [])

    def _get(self, name, policy_name):
        return '{}'

    def get(self, policy_name):
        return self._get(self.name, policy_name)['PolicyDocument']

    def _put(self, name, policy_name, policy_doc):
        pass

    def put(self, policy_doc):
        if not self.check_mode:
            self._put(self.name, self.policy_name, json.dumps(policy_doc, sort_keys=True))
        self.changed = True

    def _delete(self, name, policy_name):
        pass

    def delete(self):
        if self.policy_name not in self.list():
            self.changed = False
            return

        self.changed = True
        if not self.check_mode:
            self._delete(self.name, self.policy_name)

    def get_policy_text(self):
        try:
            if self.policy_document is not None:
                return self.get_policy_from_document()
            if self.policy_json is not None:
                return self.get_policy_from_json()
        except json.JSONDecodeError as e:
            raise PolicyError('Failed to decode the policy as valid JSON: %s' % str(e))
        return None

    def get_policy_from_document(self):
        try:
            with open(self.policy_document, 'r') as json_data:
                pdoc = json.load(json_data)
                json_data.close()
        except IOError as e:
            if e.errno == 2:
                raise PolicyError('policy_document {0:!r} does not exist'.format(self.policy_document))
            raise
        return pdoc

    def get_policy_from_json(self):
        if isinstance(self.policy_json, string_types):
            pdoc = json.loads(self.policy_json)
        else:
            pdoc = self.policy_json
        return pdoc

    def create(self):
        matching_policies = []
        policy_doc = self.get_policy_text()
        policy_match = False
        for pol in self.list():
            if not compare_policies(self.get(pol), policy_doc):
                matching_policies.append(pol)
                policy_match = True

        if (self.policy_name not in matching_policies) and not (self.skip_duplicates and policy_match):
            self.put(policy_doc)

    def run(self):
        if self.state == 'present':
            self.create()
        elif self.state == 'absent':
            self.delete()
        return {
            'changed': self.changed,
            self._iam_type() + '_name': self.name,
            'policies': self.list()
        }


class UserPolicy(Policy):

    @staticmethod
    def _iam_type():
        return 'user'

    def _list(self, name):
        return self.client.list_user_policies(aws_retry=True, UserName=name)

    def _get(self, name, policy_name):
        return self.client.get_user_policy(aws_retry=True, UserName=name, PolicyName=policy_name)

    def _put(self, name, policy_name, policy_doc):
        return self.client.put_user_policy(aws_retry=True, UserName=name, PolicyName=policy_name, PolicyDocument=policy_doc)

    def _delete(self, name, policy_name):
        return self.client.delete_user_policy(aws_retry=True, UserName=name, PolicyName=policy_name)


class RolePolicy(Policy):

    @staticmethod
    def _iam_type():
        return 'role'

    def _list(self, name):
        return self.client.list_role_policies(aws_retry=True, RoleName=name)

    def _get(self, name, policy_name):
        return self.client.get_role_policy(aws_retry=True, RoleName=name, PolicyName=policy_name)

    def _put(self, name, policy_name, policy_doc):
        return self.client.put_role_policy(aws_retry=True, RoleName=name, PolicyName=policy_name, PolicyDocument=policy_doc)

    def _delete(self, name, policy_name):
        return self.client.delete_role_policy(aws_retry=True, RoleName=name, PolicyName=policy_name)


class GroupPolicy(Policy):

    @staticmethod
    def _iam_type():
        return 'group'

    def _list(self, name):
        return self.client.list_group_policies(aws_retry=True, GroupName=name)

    def _get(self, name, policy_name):
        return self.client.get_group_policy(aws_retry=True, GroupName=name, PolicyName=policy_name)

    def _put(self, name, policy_name, policy_doc):
        return self.client.put_group_policy(aws_retry=True, GroupName=name, PolicyName=policy_name, PolicyDocument=policy_doc)

    def _delete(self, name, policy_name):
        return self.client.delete_group_policy(aws_retry=True, GroupName=name, PolicyName=policy_name)


def main():
    argument_spec = dict(
        iam_type=dict(required=True, choices=['user', 'group', 'role']),
        state=dict(default='present', choices=['present', 'absent']),
        iam_name=dict(required=True),
        policy_name=dict(required=True),
        policy_document=dict(default=None, required=False),
        policy_json=dict(type='json', default=None, required=False),
        skip_duplicates=dict(type='bool', default=None, required=False)
    )
    mutually_exclusive = [['policy_document', 'policy_json']]

    module = AnsibleAWSModule(argument_spec=argument_spec, mutually_exclusive=mutually_exclusive, supports_check_mode=True)

    skip_duplicates = module.params.get('skip_duplicates')

    if (skip_duplicates is None):
        module.deprecate('The skip_duplicates behaviour has caused confusion and'
                         ' will be disabled by default in Ansible 2.14',
                         date='2022-06-01', collection_name='community.aws')
        skip_duplicates = True

    if module.params.get('policy_document'):
        module.deprecate('The policy_document option has been deprecated and'
                         ' will be removed in Ansible 2.14',
                         date='2022-06-01', collection_name='community.aws')

    args = dict(
        client=module.client('iam', retry_decorator=AWSRetry.jittered_backoff()),
        name=module.params.get('iam_name'),
        policy_name=module.params.get('policy_name'),
        policy_document=module.params.get('policy_document'),
        policy_json=module.params.get('policy_json'),
        skip_duplicates=skip_duplicates,
        state=module.params.get('state'),
        check_mode=module.check_mode,
    )
    iam_type = module.params.get('iam_type')

    try:
        if iam_type == 'user':
            policy = UserPolicy(**args)
        elif iam_type == 'role':
            policy = RolePolicy(**args)
        elif iam_type == 'group':
            policy = GroupPolicy(**args)

        module.exit_json(**(policy.run()))
    except (BotoCoreError, ClientError) as e:
        module.fail_json_aws(e)
    except PolicyError as e:
        module.fail_json(msg=str(e))


if __name__ == '__main__':
    main()
