From 9c08ff7a94e6f584a16f81b28abf31b68d523fb0 Mon Sep 17 00:00:00 2001 From: Aaron Date: Wed, 17 Oct 2018 12:56:13 -0500 Subject: [PATCH] [aws] New module: iam_password_policy (#36200) * Adding iam_password_policy module * fixing various issues -- error handling, bugs * fixing various issues based on tests * renaming dummy var * fixing type reference in documentation * adding int tests and other updates * removing typo * fixing auth for int tests * removing int tests for now * readding integration tests w/ unsupported designation * removing conflicting group * Update aliases * Fix unused variable --- .../cloud/amazon/iam_password_policy.py | 190 ++++++++++++++++++ .../targets/iam_password_policy/aliases | 2 + .../iam_password_policy/tasks/main.yaml | 90 +++++++++ .../cloud/amazon/test_iam_password_policy.py | 25 +++ 4 files changed, 307 insertions(+) create mode 100644 lib/ansible/modules/cloud/amazon/iam_password_policy.py create mode 100644 test/integration/targets/iam_password_policy/aliases create mode 100644 test/integration/targets/iam_password_policy/tasks/main.yaml create mode 100644 test/units/modules/cloud/amazon/test_iam_password_policy.py diff --git a/lib/ansible/modules/cloud/amazon/iam_password_policy.py b/lib/ansible/modules/cloud/amazon/iam_password_policy.py new file mode 100644 index 0000000000..435cf08605 --- /dev/null +++ b/lib/ansible/modules/cloud/amazon/iam_password_policy.py @@ -0,0 +1,190 @@ +#!/usr/bin/python + +# Copyright: (c) 2018, Aaron Smith +# 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 + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: iam_password_policy +short_description: Update an IAM Password Policy +description: + - Module updates an IAM Password Policy on a given AWS account +version_added: "2.8" +requirements: [ 'botocore', 'boto3' ] +author: + - "Aaron Smith (@slapula)" +options: + state: + description: + - Specifies the overall state of the password policy. + required: true + choices: ['present', 'absent'] + min_pw_length: + description: + - Minimum password length. + default: 6 + aliases: [minimum_password_length] + require_symbols: + description: + - Require symbols in password. + default: false + type: bool + require_numbers: + description: + - Require numbers in password. + default: false + type: bool + require_uppercase: + description: + - Require uppercase letters in password. + default: false + type: bool + require_lowercase: + description: + - Require lowercase letters in password. + default: false + type: bool + allow_pw_change: + description: + - Allow users to change their password. + default: false + type: bool + aliases: [allow_password_change] + pw_max_age: + description: + - Maximum age for a password in days. + default: 0 + aliases: [password_max_age] + pw_reuse_prevent: + description: + - Prevent re-use of passwords. + default: 0 + aliases: [password_reuse_prevent, prevent_reuse] + pw_expire: + description: + - Prevents users from change an expired password. + default: false + type: bool + aliases: [password_expire, expire] +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +- name: Password policy for AWS account + iam_password_policy: + state: present + min_pw_length: 8 + require_symbols: false + require_numbers: true + require_uppercase: true + require_lowercase: true + allow_pw_change: true + pw_max_age: 60 + pw_reuse_prevent: 5 + pw_expire: false +''' + +RETURN = ''' # ''' + +try: + import botocore +except ImportError: + pass # caught by AnsibleAWSModule + +from ansible.module_utils.aws.core import AnsibleAWSModule +from ansible.module_utils.ec2 import boto3_conn, get_aws_connection_info, AWSRetry +from ansible.module_utils.ec2 import camel_dict_to_snake_dict, boto3_tag_list_to_ansible_dict + + +class IAMConnection(object): + + def __init__(self, module): + try: + self.connection = module.resource('iam') + self.module = module + except Exception as e: + module.fail_json(msg="Failed to connect to AWS: %s" % str(e)) + + def update_password_policy(self, module, policy): + min_pw_length = module.params.get('min_pw_length') + require_symbols = module.params.get('require_symbols') + require_numbers = module.params.get('require_numbers') + require_uppercase = module.params.get('require_uppercase') + require_lowercase = module.params.get('require_lowercase') + allow_pw_change = module.params.get('allow_pw_change') + pw_max_age = module.params.get('pw_max_age') + pw_reuse_prevent = module.params.get('pw_reuse_prevent') + pw_expire = module.params.get('pw_expire') + + try: + results = policy.update( + MinimumPasswordLength=min_pw_length, + RequireSymbols=require_symbols, + RequireNumbers=require_numbers, + RequireUppercaseCharacters=require_uppercase, + RequireLowercaseCharacters=require_lowercase, + AllowUsersToChangePassword=allow_pw_change, + MaxPasswordAge=pw_max_age, + PasswordReusePrevention=pw_reuse_prevent, + HardExpiry=pw_expire + ) + policy.reload() + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + self.module.fail_json_aws(e, msg="Couldn't update IAM Password Policy") + return camel_dict_to_snake_dict(results) + + def delete_password_policy(self, policy): + try: + results = policy.delete() + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + if e.response['Error']['Code'] == 'NoSuchEntity': + self.module.exit_json(changed=False, task_status={'IAM': "Couldn't find IAM Password Policy"}) + else: + self.module.fail_json_aws(e, msg="Couldn't delete IAM Password Policy") + return camel_dict_to_snake_dict(results) + + +def main(): + module = AnsibleAWSModule( + argument_spec={ + 'state': dict(choices=['present', 'absent'], required=True), + 'min_pw_length': dict(type='int', aliases=['minimum_password_length'], default=6), + 'require_symbols': dict(type='bool', default=False), + 'require_numbers': dict(type='bool', default=False), + 'require_uppercase': dict(type='bool', default=False), + 'require_lowercase': dict(type='bool', default=False), + 'allow_pw_change': dict(type='bool', aliases=['allow_password_change'], default=False), + 'pw_max_age': dict(type='int', aliases=['password_max_age'], default=0), + 'pw_reuse_prevent': dict(type='int', aliases=['password_reuse_prevent', 'prevent_reuse'], default=0), + 'pw_expire': dict(type='bool', aliases=['password_expire', 'expire'], default=False), + }, + supports_check_mode=True, + ) + + resource = IAMConnection(module) + policy = resource.connection.AccountPasswordPolicy() + + state = module.params.get('state') + + if state == 'present': + update_result = resource.update_password_policy(module, policy) + module.exit_json(changed=True, task_status={'IAM': update_result}) + + if state == 'absent': + delete_result = resource.delete_password_policy(policy) + module.exit_json(changed=True, task_status={'IAM': delete_result}) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/iam_password_policy/aliases b/test/integration/targets/iam_password_policy/aliases new file mode 100644 index 0000000000..5692719518 --- /dev/null +++ b/test/integration/targets/iam_password_policy/aliases @@ -0,0 +1,2 @@ +cloud/aws +unsupported diff --git a/test/integration/targets/iam_password_policy/tasks/main.yaml b/test/integration/targets/iam_password_policy/tasks/main.yaml new file mode 100644 index 0000000000..09f6afa956 --- /dev/null +++ b/test/integration/targets/iam_password_policy/tasks/main.yaml @@ -0,0 +1,90 @@ +- name: set connection information for all tasks + set_fact: + aws_connection_info: &aws_connection_info + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + security_token: "{{ security_token }}" + region: "{{ aws_region }}" + no_log: true + +- name: set iam password policy + iam_password_policy: + <<: *aws_connection_info + state: present + min_pw_length: 8 + require_symbols: false + require_numbers: true + require_uppercase: true + require_lowercase: true + allow_pw_change: true + pw_max_age: 60 + pw_reuse_prevent: 5 + pw_expire: false + register: result + +- name: assert that changes were made + assert: + that: + - result.changed + +- name: verify iam password policy has been created + iam_password_policy: + <<: *aws_connection_info + state: present + min_pw_length: 8 + require_symbols: false + require_numbers: true + require_uppercase: true + require_lowercase: true + allow_pw_change: true + pw_max_age: 60 + pw_reuse_prevent: 5 + pw_expire: false + register: result + +- name: assert that no changes were made + assert: + that: + - not result.changed + +- name: update iam password policy + iam_password_policy: + <<: *aws_connection_info + state: present + min_pw_length: 15 + require_symbols: true + require_numbers: true + require_uppercase: true + require_lowercase: true + allow_pw_change: true + pw_max_age: 30 + pw_reuse_prevent: 10 + pw_expire: true + register: result + +- name: assert that updates were made + assert: + that: + - result.changed + +- name: remove iam password policy + iam_password_policy: + <<: *aws_connection_info + state: absent + register: result + +- name: assert password policy has been removed + assert: + that: + - result.changed + +- name: verify password policy has been removed + iam_password_policy: + <<: *aws_connection_info + state: absent + register: result + +- name: assert no changes were made + assert: + that: + - not result.changed diff --git a/test/units/modules/cloud/amazon/test_iam_password_policy.py b/test/units/modules/cloud/amazon/test_iam_password_policy.py new file mode 100644 index 0000000000..f7076a4c86 --- /dev/null +++ b/test/units/modules/cloud/amazon/test_iam_password_policy.py @@ -0,0 +1,25 @@ +import boto3 +import pytest + +from units.modules.utils import set_module_args +from ansible.module_utils.ec2 import HAS_BOTO3 +from ansible.modules.cloud.amazon import iam_password_policy + +if not HAS_BOTO3: + pytestmark = pytest.mark.skip("iam_password_policy.py requires the `boto3` and `botocore` modules") + + +def test_warn_if_state_not_specified(): + set_module_args({ + "min_pw_length": "8", + "require_symbols": "false", + "require_numbers": "true", + "require_uppercase": "true", + "require_lowercase": "true", + "allow_pw_change": "true", + "pw_max_age": "60", + "pw_reuse_prevent": "5", + "pw_expire": "false" + }) + with pytest.raises(SystemExit): + print(iam_password_policy.main())