diff --git a/changelogs/fragments/62014-iam_role_session_instanceprofile.yml b/changelogs/fragments/62014-iam_role_session_instanceprofile.yml new file mode 100644 index 00000000000..50e64ac181e --- /dev/null +++ b/changelogs/fragments/62014-iam_role_session_instanceprofile.yml @@ -0,0 +1,3 @@ +minor_changes: +- iam_role - Add support for removing the related instance profile when we delete the role +- iam_role - Add support for managing the maximum session duration diff --git a/hacking/aws_config/testing_policies/security-policy.json b/hacking/aws_config/testing_policies/security-policy.json index 9aee5ab7d4f..67c08e17cc8 100644 --- a/hacking/aws_config/testing_policies/security-policy.json +++ b/hacking/aws_config/testing_policies/security-policy.json @@ -27,14 +27,31 @@ "Effect": "Allow", "Sid": "AllowReadOnlyIAMUse" }, + { + "Action": [ + "iam:CreatePolicy", + "iam:ListPolicyVersions", + "iam:ListEntitiesForPolicy", + "iam:DeletePolicy" + ], + "Resource": "arn:aws:iam::{{ aws_account }}:policy/ansible-test-*", + "Effect": "Allow", + "Sid": "AllowManagementOfSpecificPolicies" + }, { "Action": [ "iam:AttachRolePolicy", "iam:CreateRole", "iam:DeleteRole", + "iam:DeleteRolePolicy", + "iam:DeleteRolePermissionsBoundary", "iam:DetachRolePolicy", "iam:PassRole", + "iam:PutRolePolicy", + "iam:PutRolePermissionsBoundary", "iam:UpdateAssumeRolePolicy", + "iam:UpdateRole", + "iam:UpdateRoleDescription", "sts:AssumeRole" ], "Resource": "arn:aws:iam::{{ aws_account }}:role/ansible-test-*", diff --git a/lib/ansible/modules/cloud/amazon/iam_role.py b/lib/ansible/modules/cloud/amazon/iam_role.py index bdc4a05fd24..ad6957a1dcf 100644 --- a/lib/ansible/modules/cloud/amazon/iam_role.py +++ b/lib/ansible/modules/cloud/amazon/iam_role.py @@ -44,6 +44,11 @@ options: - A list of managed policy ARNs or, since Ansible 2.4, a list of either managed policy ARNs or friendly names. To embed an inline policy, use M(iam_policy). To remove existing policies, use an empty list item. aliases: [ managed_policies ] + max_session_duration: + description: + - The maximum duration (in seconds) of a session when assuming the role. + - Valid values are between 1 and 12 hours (3600 and 43200 seconds). + version_added: "2.10" purge_policies: description: - Detaches any managed policies not listed in the "managed_policy" option. Set to false if you want to attach policies elsewhere. @@ -59,8 +64,16 @@ options: description: - Creates an IAM instance profile along with the role type: bool - default: yes + default: true version_added: "2.5" + delete_instance_profile: + description: + - When deleting a role will also delete the instance profile created with + the same name as the role + - Only applies when C(state=absent) + type: bool + default: false + version_added: "2.10" requirements: [ botocore, boto3 ] extends_documentation_fragment: - aws @@ -206,7 +219,7 @@ def convert_friendly_names_to_arns(connection, module, policy_names): try: return [allpolicies[policy] for policy in policy_names] except KeyError as e: - module.fail_json(msg="Couldn't find policy: " + str(e)) + module.fail_json_aws(e, msg="Couldn't find policy") def remove_policies(connection, module, policies_to_remove, params): @@ -216,11 +229,9 @@ def remove_policies(connection, module, policies_to_remove, params): if not module.check_mode: connection.detach_role_policy(RoleName=params['RoleName'], PolicyArn=policy) except ClientError as e: - module.fail_json(msg="Unable to detach policy {0} from {1}: {2}".format(policy, params['RoleName'], to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) + module.fail_json_aws(e, msg="Unable to detach policy {0} from {1}".format(policy, params['RoleName'])) except BotoCoreError as e: - module.fail_json(msg="Unable to detach policy {0} from {1}: {2}".format(policy, params['RoleName'], to_native(e)), - exception=traceback.format_exc()) + module.fail_json_aws(e, msg="Unable to detach policy {0} from {1}".format(policy, params['RoleName'])) changed = True return changed @@ -230,6 +241,8 @@ def create_or_update_role(connection, module): params['Path'] = module.params.get('path') params['RoleName'] = module.params.get('name') params['AssumeRolePolicyDocument'] = module.params.get('assume_role_policy_document') + if module.params.get('max_session_duration') is not None: + params['MaxSessionDuration'] = module.params.get('max_session_duration') if module.params.get('description') is not None: params['Description'] = module.params.get('description') if module.params.get('boundary') is not None: @@ -257,11 +270,9 @@ def create_or_update_role(connection, module): role['AssumeRolePolicyDocument'] = json.loads(params['AssumeRolePolicyDocument']) changed = True except ClientError as e: - module.fail_json(msg="Unable to create role: {0}".format(to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) + module.fail_json_aws(e, msg="Unable to create role") except BotoCoreError as e: - module.fail_json(msg="Unable to create role: {0}".format(to_native(e)), - exception=traceback.format_exc()) + module.fail_json_aws(e, msg="Unable to create role") else: # Check Assumed Policy document if not compare_assume_role_policy_doc(role['AssumeRolePolicyDocument'], params['AssumeRolePolicyDocument']): @@ -270,11 +281,9 @@ def create_or_update_role(connection, module): connection.update_assume_role_policy(RoleName=params['RoleName'], PolicyDocument=json.dumps(json.loads(params['AssumeRolePolicyDocument']))) changed = True except ClientError as e: - module.fail_json(msg="Unable to update assume role policy for role {0}: {1}".format(params['RoleName'], to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) + module.fail_json_aws(e, msg="Unable to update assume role policy for role {0}".format(params['RoleName'])) except BotoCoreError as e: - module.fail_json(msg="Unable to update assume role policy for role {0}: {1}".format(params['RoleName'], to_native(e)), - exception=traceback.format_exc()) + module.fail_json_aws(e, msg="Unable to update assume role policy for role {0}".format(params['RoleName'])) if managed_policies is not None: # Get list of current attached managed policies @@ -301,11 +310,9 @@ def create_or_update_role(connection, module): if not module.check_mode: connection.attach_role_policy(RoleName=params['RoleName'], PolicyArn=policy_arn) except ClientError as e: - module.fail_json(msg="Unable to attach policy {0} to role {1}: {2}".format(policy_arn, params['RoleName'], to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) + module.fail_json_aws(e, msg="Unable to attach policy {0} to role {1}".format(policy_arn, params['RoleName'])) except BotoCoreError as e: - module.fail_json(msg="Unable to attach policy {0} to role {1}: {2}".format(policy_arn, params['RoleName'], to_native(e)), - exception=traceback.format_exc()) + module.fail_json_aws(e, msg="Unable to attach policy {0} to role {1}".format(policy_arn, params['RoleName'])) changed = True # Instance profile @@ -313,11 +320,9 @@ def create_or_update_role(connection, module): try: instance_profiles = connection.list_instance_profiles_for_role(RoleName=params['RoleName'])['InstanceProfiles'] except ClientError as e: - module.fail_json(msg="Unable to list instance profiles for role {0}: {1}".format(params['RoleName'], to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) + module.fail_json_aws(e, msg="Unable to list instance profiles for role {0}".format(params['RoleName'])) except BotoCoreError as e: - module.fail_json(msg="Unable to list instance profiles for role {0}: {1}".format(params['RoleName'], to_native(e)), - exception=traceback.format_exc()) + module.fail_json_aws(e, msg="Unable to list instance profiles for role {0}".format(params['RoleName'])) if not any(p['InstanceProfileName'] == params['RoleName'] for p in instance_profiles): # Make sure an instance profile is attached try: @@ -329,11 +334,9 @@ def create_or_update_role(connection, module): if e.response['Error']['Code'] == 'EntityAlreadyExists': pass else: - module.fail_json(msg="Unable to create instance profile for role {0}: {1}".format(params['RoleName'], to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) + module.fail_json_aws(e, msg="Unable to create instance profile for role {0}".format(params['RoleName'])) except BotoCoreError as e: - module.fail_json(msg="Unable to create instance profile for role {0}: {1}".format(params['RoleName'], to_native(e)), - exception=traceback.format_exc()) + module.fail_json_aws(e, msg="Unable to create instance profile for role {0}".format(params['RoleName'])) if not module.check_mode: connection.add_role_to_instance_profile(InstanceProfileName=params['RoleName'], RoleName=params['RoleName']) @@ -345,8 +348,17 @@ def create_or_update_role(connection, module): changed = True except (BotoCoreError, ClientError) as e: - module.fail_json(msg="Unable to update description for role {0}: {1}".format(params['RoleName'], to_native(e)), - exception=traceback.format_exc()) + module.fail_json_aws(e, msg="Unable to update description for role {0}".format(params['RoleName'])) + + # Check MaxSessionDuration update + if not role.get('MadeInCheckMode') and params.get('MaxSessionDuration') and role.get('MaxSessionDuration') != params['MaxSessionDuration']: + try: + if not module.check_mode: + connection.update_role(RoleName=params['RoleName'], MaxSessionDuration=params['MaxSessionDuration']) + + changed = True + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Unable to update maximum session duration for role {0}".format(params['RoleName'])) # Check if permission boundary needs update if not role.get('MadeInCheckMode') and ( @@ -362,14 +374,14 @@ def create_or_update_role(connection, module): connection.delete_role_permissions_boundary(RoleName=params['RoleName']) changed = True except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to remove permission boundary for role {0}: {1}".format(params['RoleName'], to_native(e))) + module.fail_json_aws(e, msg="Unable to remove permission boundary for role {0}".format(params['RoleName'])) elif (role.get('PermissionsBoundary') or {}).get('PermissionsBoundaryArn') != params['PermissionsBoundary']: try: if not module.check_mode: connection.put_role_permissions_boundary(RoleName=params['RoleName'], PermissionsBoundary=params['PermissionsBoundary']) changed = True except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to update permission boundary for role {0}: {1}".format(params['RoleName'], to_native(e))) + module.fail_json_aws(e, msg="Unable to update permission boundary for role {0}".format(params['RoleName'])) # Get the role again if not role.get('MadeInCheckMode', False): @@ -392,31 +404,33 @@ def destroy_role(connection, module): try: instance_profiles = connection.list_instance_profiles_for_role(RoleName=params['RoleName'])['InstanceProfiles'] except ClientError as e: - module.fail_json(msg="Unable to list instance profiles for role {0}: {1}".format(params['RoleName'], to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) + module.fail_json_aws(e, msg="Unable to list instance profiles for role {0}".format(params['RoleName'])) except BotoCoreError as e: - module.fail_json(msg="Unable to list instance profiles for role {0}: {1}".format(params['RoleName'], to_native(e)), - exception=traceback.format_exc()) + module.fail_json_aws(e, msg="Unable to list instance profiles for role {0}".format(params['RoleName'])) if role.get('PermissionsBoundary') is not None: try: connection.delete_role_permissions_boundary(RoleName=params['RoleName']) except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Could not delete role permission boundary on role {0}: {1}".format(params['RoleName'], e)) + module.fail_json_aws(e, msg="Could not delete role permission boundary on role {0}".format(params['RoleName'])) # Now remove the role from the instance profile(s) for profile in instance_profiles: try: if not module.check_mode: connection.remove_role_from_instance_profile(InstanceProfileName=profile['InstanceProfileName'], RoleName=params['RoleName']) + if profile['InstanceProfileName'] == params['RoleName']: + if module.params.get("delete_instance_profile"): + try: + connection.delete_instance_profile(InstanceProfileName=profile['InstanceProfileName']) + except ClientError as e: + module.fail_json_aws(e, msg="Unable to remove instance profile {0}".format(profile['InstanceProfileName'])) except ClientError as e: - module.fail_json(msg="Unable to remove role {0} from instance profile {1}: {2}".format( - params['RoleName'], profile['InstanceProfileName'], to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) + module.fail_json_aws(e, msg="Unable to remove role {0} from instance profile {1}".format( + params['RoleName'], profile['InstanceProfileName'])) except BotoCoreError as e: - module.fail_json(msg="Unable to remove role {0} from instance profile {1}: {2}".format( - params['RoleName'], profile['InstanceProfileName'], to_native(e)), - exception=traceback.format_exc()) + module.fail_json_aws(e, msg="Unable to remove role {0} from instance profile {1}".format( + params['RoleName'], profile['InstanceProfileName'])) # Now remove any attached policies otherwise deletion fails try: @@ -424,20 +438,17 @@ def destroy_role(connection, module): if not module.check_mode: connection.detach_role_policy(RoleName=params['RoleName'], PolicyArn=policy['PolicyArn']) except ClientError as e: - module.fail_json(msg="Unable to detach policy {0} from role {1}: {2}".format(policy['PolicyArn'], params['RoleName'], to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) + module.fail_json_aws(e, msg="Unable to detach policy {0} from role {1}".format(policy['PolicyArn'], params['RoleName'])) except BotoCoreError as e: - module.fail_json(msg="Unable to detach policy {0} from role {1}: {2}".format(policy['PolicyArn'], params['RoleName'], to_native(e)), - exception=traceback.format_exc()) + module.fail_json_aws(e, msg="Unable to detach policy {0} from role {1}".format(policy['PolicyArn'], params['RoleName'])) try: if not module.check_mode: connection.delete_role(**params) except ClientError as e: - module.fail_json(msg="Unable to delete role: {0}".format(to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) + module.fail_json_aws(e, msg="Unable to delete role") except BotoCoreError as e: - module.fail_json(msg="Unable to delete role: {0}".format(to_native(e)), exception=traceback.format_exc()) + module.fail_json_aws(e, msg="Unable to delete role") else: module.exit_json(changed=False) @@ -448,7 +459,7 @@ def get_role_with_backoff(connection, module, name): try: return AWSRetry.jittered_backoff(catch_extra_error_codes=['NoSuchEntity'])(connection.get_role)(RoleName=name)['Role'] except (BotoCoreError, ClientError) as e: - module.fail_json(msg="Unable to get role {0}: {1}".format(name, to_native(e)), exception=traceback.format_exc()) + module.fail_json_aws(e, msg="Unable to get role {0}".format(name)) def get_role(connection, module, name): @@ -458,10 +469,9 @@ def get_role(connection, module, name): if e.response['Error']['Code'] == 'NoSuchEntity': return None else: - module.fail_json(msg="Unable to get role {0}: {1}".format(name, to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) + module.fail_json_aws(e, msg="Unable to get role {0}".format(name)) except BotoCoreError as e: - module.fail_json(msg="Unable to get role {0}: {1}".format(name, to_native(e)), exception=traceback.format_exc()) + module.fail_json_aws(e, msg="Unable to get role {0}".format(name)) def get_attached_policy_list(connection, module, name): @@ -472,11 +482,9 @@ def get_attached_policy_list(connection, module, name): if e.response['Error']['Code'] == 'NoSuchEntity': return [] else: - module.fail_json(msg="Unable to list attached policies for role {0}: {1}".format(name, to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) + module.fail_json_aws(e, msg="Unable to list attached policies for role {0}".format(name)) except BotoCoreError as e: - module.fail_json(msg="Unable to list attached policies for role {0}: {1}".format(name, to_native(e)), - exception=traceback.format_exc()) + module.fail_json_aws(e, msg="Unable to list attached policies for role {0}".format(name)) def main(): @@ -486,21 +494,34 @@ def main(): path=dict(type='str', default="/"), assume_role_policy_document=dict(type='json'), managed_policy=dict(type='list', aliases=['managed_policies']), + max_session_duration=dict(type='int'), state=dict(type='str', choices=['present', 'absent'], default='present'), description=dict(type='str'), boundary=dict(type='str', aliases=['boundary_policy_arn']), create_instance_profile=dict(type='bool', default=True), + delete_instance_profile=dict(type='bool', default=False), purge_policies=dict(type='bool', default=True), ) module = AnsibleAWSModule(argument_spec=argument_spec, required_if=[('state', 'present', ['assume_role_policy_document'])], supports_check_mode=True) - if module.params.get('boundary') and module.params.get('create_instance_profile'): - module.fail_json(msg="When using a boundary policy, `create_instance_profile` must be set to `false`.") + if module.params.get('boundary'): + if module.params.get('create_instance_profile'): + module.fail_json(msg="When using a boundary policy, `create_instance_profile` must be set to `false`.") + if not module.params.get('boundary').startswith('arn:aws:iam'): + module.fail_json(msg="Boundary policy must be an ARN") if module.params.get('boundary') is not None and not module.botocore_at_least('1.10.57'): module.fail_json(msg="When using a boundary policy, botocore must be at least v1.10.57. " "Current versions: boto3-{boto3_version} botocore-{botocore_version}".format(**module._gather_versions())) + if module.params.get('max_session_duration'): + max_session_duration = module.params.get('max_session_duration') + if max_session_duration < 3600 or max_session_duration > 43200: + module.fail_json(msg="max_session_duration must be between 1 and 12 hours (3600 and 43200 seconds)") + if module.params.get('path'): + path = module.params.get('path') + if not path.endswith('/') or not path.startswith('/'): + module.fail_json(msg="path must begin and end with /") connection = module.client('iam') diff --git a/test/integration/targets/iam_role/aliases b/test/integration/targets/iam_role/aliases new file mode 100644 index 00000000000..3d7a2c9f14c --- /dev/null +++ b/test/integration/targets/iam_role/aliases @@ -0,0 +1,3 @@ +iam_role_info +unsupported +cloud/aws diff --git a/test/integration/targets/iam_role/defaults/main.yml b/test/integration/targets/iam_role/defaults/main.yml new file mode 100644 index 00000000000..46db605072e --- /dev/null +++ b/test/integration/targets/iam_role/defaults/main.yml @@ -0,0 +1,8 @@ +--- +test_role: '{{ resource_prefix }}-role' +test_path: '/{{ resource_prefix }}/' +safe_managed_policy: 'AWSDenyAll' +custom_policy_name: '{{ resource_prefix }}-denyall' +boundary_policy: 'arn:aws:iam::aws:policy/AWSDenyAll' +paranoid_pauses: no +standard_pauses: no diff --git a/test/integration/targets/iam_role/files/deny-all-a.json b/test/integration/targets/iam_role/files/deny-all-a.json new file mode 100644 index 00000000000..ae62fd1975d --- /dev/null +++ b/test/integration/targets/iam_role/files/deny-all-a.json @@ -0,0 +1,13 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "*" + ], + "Effect": "Deny", + "Resource": "*", + "Sid": "DenyA" + } + ] +} diff --git a/test/integration/targets/iam_role/files/deny-all-b.json b/test/integration/targets/iam_role/files/deny-all-b.json new file mode 100644 index 00000000000..3a4704a46ab --- /dev/null +++ b/test/integration/targets/iam_role/files/deny-all-b.json @@ -0,0 +1,13 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "*" + ], + "Effect": "Deny", + "Resource": "*", + "Sid": "DenyB" + } + ] +} diff --git a/test/integration/targets/iam_role/files/deny-all.json b/test/integration/targets/iam_role/files/deny-all.json new file mode 100644 index 00000000000..3d324b9b9c6 --- /dev/null +++ b/test/integration/targets/iam_role/files/deny-all.json @@ -0,0 +1,12 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "*" + ], + "Effect": "Deny", + "Resource": "*" + } + ] +} diff --git a/test/integration/targets/iam_role/files/deny-assume.json b/test/integration/targets/iam_role/files/deny-assume.json new file mode 100644 index 00000000000..73e87715862 --- /dev/null +++ b/test/integration/targets/iam_role/files/deny-assume.json @@ -0,0 +1,10 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Principal": { "Service": "ec2.amazonaws.com" }, + "Effect": "Deny" + } + ] +} diff --git a/test/integration/targets/iam_role/meta/main.yml b/test/integration/targets/iam_role/meta/main.yml new file mode 100644 index 00000000000..1f64f1169a9 --- /dev/null +++ b/test/integration/targets/iam_role/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - prepare_tests + - setup_ec2 diff --git a/test/integration/targets/iam_role/tasks/main.yml b/test/integration/targets/iam_role/tasks/main.yml new file mode 100644 index 00000000000..fc8be72a3d4 --- /dev/null +++ b/test/integration/targets/iam_role/tasks/main.yml @@ -0,0 +1,1218 @@ +--- +# Tests for iam_role and iam_role_info +# +# Tests: +# - Minimal Role creation +# - Role deletion +# - Fetching a specific role +# - Creating roles w/ and w/o instance profiles +# - Creating roles w/ a path +# - Updating Max Session Duration +# - Updating Description +# - Managing list of managed policies +# - Managing list of inline policies (for testing _info) +# - Managing boundary policy +# +# Notes: +# - Only tests *documented* return values ( RESULT.iam_role ) +# - There are some known timing issues with boto3 returning before actions +# complete in the case of problems with "changed" status it's worth enabling +# the standard_pauses and paranoid_pauses options as a first step in debugging +# +# Possible Bugs: +# - Fails to delete role if inline policies not removed first + +- name: 'Setup AWS connection info' + module_defaults: + group/aws: + aws_access_key: '{{ aws_access_key }}' + aws_secret_key: '{{ aws_secret_key }}' + security_token: '{{ security_token | default(omit) }}' + region: '{{ aws_region }}' + iam_role: + assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}' + block: + # =================================================================== + # Parameter Checks + - name: 'Friendly message when creating an instance profile and adding a boundary profile' + iam_role: + name: '{{ test_role }}' + boundary: '{{ boundary_policy }}' + register: iam_role + ignore_errors: yes + - assert: + that: + - iam_role is failed + - '"boundary policy" in iam_role.msg' + - '"create_instance_profile" in iam_role.msg' + - '"false" in iam_role.msg' + + - name: 'Friendly message when boundary profile is not an ARN' + iam_role: + name: '{{ test_role }}' + boundary: 'AWSDenyAll' + create_instance_profile: no + register: iam_role + ignore_errors: yes + - assert: + that: + - iam_role is failed + - '"Boundary policy" in iam_role.msg' + - '"ARN" in iam_role.msg' + + - name: 'Friendly message when "present" without assume_role_policy_document' + module_defaults: { iam_role: {} } + iam_role: + name: '{{ test_role }}' + register: iam_role + ignore_errors: yes + - assert: + that: + - iam_role is failed + - 'iam_role.msg.startswith("state is present but all of the following are missing")' + - '"assume_role_policy_document" in iam_role.msg' + + - name: 'Maximum Session Duration needs to be between 1 and 12 hours' + iam_role: + name: '{{ test_role }}' + max_session_duration: 3599 + register: iam_role + ignore_errors: yes + - assert: + that: + - iam_role is failed + - '"max_session_duration must be between" in iam_role.msg' + + - name: 'Maximum Session Duration needs to be between 1 and 12 hours' + iam_role: + name: '{{ test_role }}' + max_session_duration: 43201 + register: iam_role + ignore_errors: yes + - assert: + that: + - iam_role is failed + - '"max_session_duration must be between" in iam_role.msg' + + - name: 'Role Paths must start with /' + iam_role: + name: '{{ test_role }}' + path: 'test/' + register: iam_role + ignore_errors: yes + - assert: + that: + - iam_role is failed + - '"path must begin and end with /" in iam_role.msg' + + - name: 'Role Paths must end with /' + iam_role: + name: '{{ test_role }}' + path: '/test' + register: iam_role + ignore_errors: yes + - assert: + that: + - iam_role is failed + - '"path must begin and end with /" in iam_role.msg' + + # =================================================================== + # Supplemental resource pre-creation + - name: 'Create Safe IAM Managed Policy' + iam_managed_policy: + state: present + policy_name: '{{ custom_policy_name }}' + policy_description: "A safe (deny-all) managed policy" + policy: "{{ lookup('file', 'deny-all.json') }}" + register: create_managed_policy + - assert: + that: + - create_managed_policy is succeeded + + # =================================================================== + # Rapid Role Creation and deletion + - name: Try running some rapid fire create/delete tests + # We've previously seen issues with iam_role returning before creation's + # actually complete, if we think the issue's gone, let's try creating and + # deleting things in quick succession + when: not (standard_pauses | bool) + block: + - name: 'Minimal IAM Role without instance profile (rapid)' + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + register: iam_role + - name: 'Minimal IAM Role without instance profile (rapid)' + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + register: iam_role_again + - assert: + that: + - iam_role is changed + - iam_role_again is not changed + - name: 'Remove IAM Role (rapid)' + iam_role: + state: absent + name: '{{ test_role }}' + register: iam_role + - name: 'Remove IAM Role (rapid)' + iam_role: + state: absent + name: '{{ test_role }}' + register: iam_role_again + - assert: + that: + - iam_role is changed + - iam_role_again is not changed + + - name: 'Minimal IAM Role without instance profile (rapid)' + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + register: iam_role + - name: 'Remove IAM Role (rapid)' + iam_role: + state: absent + name: '{{ test_role }}' + register: iam_role_again + - assert: + that: + - iam_role is changed + - iam_role_again is changed + + # =================================================================== + # Role Creation + # (without Instance profile) + - name: 'iam_role_info before Role creation (no args)' + iam_role_info: + register: role_info + - assert: + that: + - role_info is succeeded + + - name: 'iam_role_info before Role creation (search for test role)' + iam_role_info: + name: '{{ test_role }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 0 + + - name: 'Minimal IAM Role (CHECK MODE)' + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + check_mode: yes + register: iam_role + - assert: + that: + - iam_role is changed + # Pause this first time, just in case we actually created something... + - name: Short pause for role creation to finish + pause: + seconds: 10 + when: standard_pauses | bool + + - name: 'iam_role_info after Role creation in check_mode' + iam_role_info: + name: '{{ test_role }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 0 + + - name: 'Minimal IAM Role without instance profile' + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + register: iam_role + - assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - 'iam_role.iam_role.arn.startswith("arn")' + - 'iam_role.iam_role.arn.endswith("role/" + test_role )' + # Would be nice to test the contents... + - '"assume_role_policy_document" in iam_role.iam_role' + - iam_role.iam_role.attached_policies | length == 0 + - iam_role.iam_role.max_session_duration == 3600 + - iam_role.iam_role.path == '/' + - iam_role.iam_role.role_name == test_role + - '"create_date" in iam_role.iam_role' + - '"role_id" in iam_role.iam_role' + - name: Short pause for role creation to finish + pause: + seconds: 10 + when: standard_pauses | bool + + - name: 'Minimal IAM Role without instance profile (no change)' + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + register: iam_role + - assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + + - name: 'iam_role_info after Role creation' + iam_role_info: + name: '{{ test_role }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - '"description" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 0 + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 3600 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + + - name: 'Remove IAM Role' + iam_role: + state: absent + name: '{{ test_role }}' + delete_instance_profile: yes + register: iam_role + - assert: + that: + - iam_role is changed + - name: Short pause for role removal to finish + pause: + seconds: 10 + when: paranoid_pauses | bool + + - name: 'iam_role_info after Role deletion' + iam_role_info: + name: '{{ test_role }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 0 + + # (with path) + - name: 'Minimal IAM Role with path (CHECK MODE)' + iam_role: + name: '{{ test_role }}' + path: '{{ test_path }}' + register: iam_role + check_mode: yes + - assert: + that: + - iam_role is changed + + - name: 'Minimal IAM Role with path' + iam_role: + name: '{{ test_role }}' + path: '{{ test_path }}' + register: iam_role + - assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - 'iam_role.iam_role.arn.startswith("arn")' + - 'iam_role.iam_role.arn.endswith("role" + test_path + test_role )' + # Would be nice to test the contents... + - '"assume_role_policy_document" in iam_role.iam_role' + - iam_role.iam_role.attached_policies | length == 0 + - iam_role.iam_role.max_session_duration == 3600 + - iam_role.iam_role.path == '{{ test_path }}' + - iam_role.iam_role.role_name == test_role + - '"create_date" in iam_role.iam_role' + - '"role_id" in iam_role.iam_role' + - name: Short pause for role creation to finish + pause: + seconds: 10 + when: standard_pauses | bool + + - name: 'Minimal IAM Role with path (no change)' + iam_role: + name: '{{ test_role }}' + path: '{{ test_path }}' + register: iam_role + - assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + + - name: 'iam_role_info after Role creation' + iam_role_info: + name: '{{ test_role }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role" + test_path + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - '"description" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile" + test_path + test_role)' + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 3600 + - role_info.iam_roles[0].path == '{{ test_path }}' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + + - name: 'iam_role_info after Role creation (searching a path)' + iam_role_info: + path_prefix: '{{ test_path }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role" + test_path + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - '"description" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile" + test_path + test_role)' + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 3600 + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].path == '{{ test_path }}' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + + - name: 'Remove IAM Role' + iam_role: + state: absent + name: '{{ test_role }}' + path: '{{ test_path }}' + # If we don't delete the existing profile it'll be reused (with the path) + # by the test below. + delete_instance_profile: yes + register: iam_role + - assert: + that: + - iam_role is changed + - name: Short pause for role removal to finish + pause: + seconds: 10 + when: paranoid_pauses | bool + + - name: 'iam_role_info after Role deletion' + iam_role_info: + name: '{{ test_role }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 0 + + # (with Instance profile) + - name: 'Minimal IAM Role with instance profile' + iam_role: + name: '{{ test_role }}' + create_instance_profile: yes + register: iam_role + - assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - 'iam_role.iam_role.arn.startswith("arn")' + - 'iam_role.iam_role.arn.endswith("role/" + test_role )' + # Would be nice to test the contents... + - '"assume_role_policy_document" in iam_role.iam_role' + - iam_role.iam_role.attached_policies | length == 0 + - iam_role.iam_role.max_session_duration == 3600 + - iam_role.iam_role.path == '/' + - iam_role.iam_role.role_name == test_role + - '"create_date" in iam_role.iam_role' + - '"role_id" in iam_role.iam_role' + - name: Short pause for role creation to finish + pause: + seconds: 10 + when: standard_pauses | bool + + - name: 'Minimal IAM Role wth instance profile (no change)' + iam_role: + name: '{{ test_role }}' + create_instance_profile: yes + register: iam_role + - assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + + - name: 'iam_role_info after Role creation' + iam_role_info: + name: '{{ test_role }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - '"description" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 3600 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + + # =================================================================== + # Max Session Duration Manipulation + + - name: 'Update Max Session Duration (CHECK MODE)' + iam_role: + name: '{{ test_role }}' + max_session_duration: 43200 + check_mode: yes + register: iam_role + - assert: + that: + - iam_role is changed + + - name: 'Update Max Session Duration' + iam_role: + name: '{{ test_role }}' + max_session_duration: 43200 + register: iam_role + - assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.max_session_duration == 43200 + + - name: 'Update Max Session Duration (no change)' + iam_role: + name: '{{ test_role }}' + max_session_duration: 43200 + register: iam_role + - assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + + - name: 'iam_role_info after updating Max Session Duration' + iam_role_info: + name: '{{ test_role }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - '"description" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + + # =================================================================== + # Description Manipulation + + - name: 'Add Description (CHECK MODE)' + iam_role: + name: '{{ test_role }}' + description: 'Ansible Test Role {{ resource_prefix }}' + check_mode: yes + register: iam_role + - assert: + that: + - iam_role is changed + + - name: 'Add Description' + iam_role: + name: '{{ test_role }}' + description: 'Ansible Test Role {{ resource_prefix }}' + register: iam_role + - assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.description == 'Ansible Test Role {{ resource_prefix }}' + + - name: 'Add Description (no change)' + iam_role: + name: '{{ test_role }}' + description: 'Ansible Test Role {{ resource_prefix }}' + register: iam_role + - assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.description == 'Ansible Test Role {{ resource_prefix }}' + + - name: 'iam_role_info after adding Description' + iam_role_info: + name: '{{ test_role }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - 'role_info.iam_roles[0].description == "Ansible Test Role {{ resource_prefix }}"' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + + - name: 'Update Description (CHECK MODE)' + iam_role: + name: '{{ test_role }}' + description: 'Ansible Test Role (updated) {{ resource_prefix }}' + check_mode: yes + register: iam_role + - assert: + that: + - iam_role is changed + + - name: 'Update Description' + iam_role: + name: '{{ test_role }}' + description: 'Ansible Test Role (updated) {{ resource_prefix }}' + register: iam_role + - assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.description == 'Ansible Test Role (updated) {{ resource_prefix }}' + + - name: 'Update Description (no change)' + iam_role: + name: '{{ test_role }}' + description: 'Ansible Test Role (updated) {{ resource_prefix }}' + register: iam_role + - assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.description == 'Ansible Test Role (updated) {{ resource_prefix }}' + + - name: 'iam_role_info after updating Description' + iam_role_info: + name: '{{ test_role }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - 'role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix }}"' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + + + # =================================================================== + # Policy Manipulation + + - name: 'Add Managed Policy (CHECK MODE)' + iam_role: + name: '{{ test_role }}' + purge_policies: no + managed_policy: + - '{{ safe_managed_policy }}' + check_mode: yes + register: iam_role + - assert: + that: + - iam_role is changed + + - name: 'Add Managed Policy' + iam_role: + name: '{{ test_role }}' + purge_policies: no + managed_policy: + - '{{ safe_managed_policy }}' + register: iam_role + - assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + + - name: 'Add Managed Policy (no change)' + iam_role: + name: '{{ test_role }}' + purge_policies: no + managed_policy: + - '{{ safe_managed_policy }}' + register: iam_role + - assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + + - name: 'iam_role_info after adding Managed Policy' + iam_role_info: + name: '{{ test_role }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - 'role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix }}"' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 1 + - safe_managed_policy in ( role_info | json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - custom_policy_name not in ( role_info | json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + + - name: 'Update Managed Policy without purge (CHECK MODE)' + iam_role: + name: '{{ test_role }}' + purge_policies: no + managed_policy: + - '{{ custom_policy_name }}' + check_mode: yes + register: iam_role + - assert: + that: + - iam_role is changed + + - name: 'Update Managed Policy without purge' + iam_role: + name: '{{ test_role }}' + purge_policies: no + managed_policy: + - '{{ custom_policy_name }}' + register: iam_role + - assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + + - name: 'Update Managed Policy without purge (no change)' + iam_role: + name: '{{ test_role }}' + purge_policies: no + managed_policy: + - '{{ custom_policy_name }}' + register: iam_role + - assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + + - name: 'iam_role_info after updating Managed Policy without purge' + iam_role_info: + name: '{{ test_role }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - 'role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix }}"' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 2 + - safe_managed_policy in ( role_info | json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - custom_policy_name in ( role_info | json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + + # Managed Policies are purged by default + - name: 'Update Managed Policy with purge (CHECK MODE)' + iam_role: + name: '{{ test_role }}' + managed_policy: + - '{{ custom_policy_name }}' + check_mode: yes + register: iam_role + - assert: + that: + - iam_role is changed + + - name: 'Update Managed Policy with purge' + iam_role: + name: '{{ test_role }}' + managed_policy: + - '{{ custom_policy_name }}' + register: iam_role + - assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + + - name: 'Update Managed Policy with purge (no change)' + iam_role: + name: '{{ test_role }}' + managed_policy: + - '{{ custom_policy_name }}' + register: iam_role + - assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + + - name: 'iam_role_info after updating Managed Policy with purge' + iam_role_info: + name: '{{ test_role }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - 'role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix }}"' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 1 + - safe_managed_policy not in ( role_info | json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - custom_policy_name in ( role_info | json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + + # =================================================================== + # Inline Policy (test _info behaviour) + + # XXX Not sure if it's a bug in Ansible or a "quirk" of AWS, but these two + # policies need to have at least different Sids or the second doesn't show + # up... + + - name: 'Attach inline policy a' + iam_policy: + state: present + iam_type: 'role' + iam_name: '{{ test_role }}' + policy_name: 'inline-policy-a' + policy_json: '{{ lookup("file", "deny-all-a.json") }}' + + - name: 'Attach inline policy b' + iam_policy: + state: present + iam_type: 'role' + iam_name: '{{ test_role }}' + policy_name: 'inline-policy-b' + policy_json: '{{ lookup("file", "deny-all-b.json") }}' + + - name: 'iam_role_info after attaching inline policies (using iam_policy)' + iam_role_info: + name: '{{ test_role }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - 'role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix }}"' + - role_info.iam_roles[0].inline_policies | length == 2 + - '"inline-policy-a" in role_info.iam_roles[0].inline_policies' + - '"inline-policy-b" in role_info.iam_roles[0].inline_policies' + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 1 + - safe_managed_policy not in ( role_info | json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - custom_policy_name in ( role_info | json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + + # XXX iam_role fails to remove inline policies before deleting the role + - name: 'Detach inline policy a' + iam_policy: + state: absent + iam_type: 'role' + iam_name: '{{ test_role }}' + policy_name: 'inline-policy-a' + + - name: 'Detach inline policy b' + iam_policy: + state: absent + iam_type: 'role' + iam_name: '{{ test_role }}' + policy_name: 'inline-policy-b' + + # =================================================================== + # Role Removal + - name: 'Remove IAM Role (CHECK MODE)' + iam_role: + state: absent + name: '{{ test_role }}' + delete_instance_profile: yes + check_mode: yes + register: iam_role + - assert: + that: + - iam_role is changed + - name: 'Short pause for role removal to finish' + pause: + seconds: 10 + when: paranoid_pauses | bool + + - name: 'iam_role_info after deleting role in check mode' + iam_role_info: + name: '{{ test_role }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + + - name: 'Remove IAM Role' + iam_role: + state: absent + name: '{{ test_role }}' + delete_instance_profile: yes + register: iam_role + - assert: + that: + - iam_role is changed + - name: 'Short pause for role removal to finish' + pause: + seconds: 10 + when: paranoid_pauses | bool + + - name: 'iam_role_info after deleting role' + iam_role_info: + name: '{{ test_role }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 0 + + - name: 'Remove IAM Role (should be gone already)' + iam_role: + state: absent + name: '{{ test_role }}' + delete_instance_profile: yes + register: iam_role + - assert: + that: + - iam_role is not changed + - name: 'Short pause for role removal to finish' + pause: + seconds: 10 + when: paranoid_pauses | bool + + # =================================================================== + # Boundary Policy (requires create_instance_profile: no) + - name: 'Create minimal role with no boundary policy' + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + register: iam_role + - assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + + - name: 'Configure Boundary Policy (CHECK MODE)' + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + boundary: '{{ boundary_policy }}' + check_mode: yes + register: iam_role + - assert: + that: + - iam_role is changed + + - name: 'Configure Boundary Policy' + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + boundary: '{{ boundary_policy }}' + register: iam_role + - assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + + - name: 'Configure Boundary Policy (no change)' + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + boundary: '{{ boundary_policy }}' + register: iam_role + - assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + + - name: 'iam_role_info after adding boundary policy' + iam_role_info: + name: '{{ test_role }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - '"description" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 0 + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 3600 + - role_info.iam_roles[0].path == '/' + - role_info.iam_roles[0].permissions_boundary.permissions_boundary_arn == boundary_policy + - role_info.iam_roles[0].permissions_boundary.permissions_boundary_type == 'Policy' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + + - name: 'Remove IAM Role' + iam_role: + state: absent + name: '{{ test_role }}' + delete_instance_profile: yes + register: iam_role + - assert: + that: + - iam_role is changed + - name: Short pause for role removal to finish + pause: + seconds: 10 + when: paranoid_pauses | bool + + # =================================================================== + # Complex role Creation + - name: 'Complex IAM Role (CHECK MODE)' + iam_role: + name: '{{ test_role }}' + assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}' + boundary: '{{ boundary_policy }}' + create_instance_profile: no + description: 'Ansible Test Role {{ resource_prefix }}' + managed_policy: + - '{{ safe_managed_policy }}' + - '{{ custom_policy_name }}' + max_session_duration: 43200 + path: '{{ test_path }}' + check_mode: yes + register: iam_role + - assert: + that: + - iam_role is changed + - name: Short pause for role creation to finish + pause: + seconds: 10 + when: standard_pauses | bool + + - name: 'iam_role_info after Complex Role creation in check_mode' + iam_role_info: + name: '{{ test_role }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 0 + + - name: 'Complex IAM Role' + iam_role: + name: '{{ test_role }}' + assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}' + boundary: '{{ boundary_policy }}' + create_instance_profile: no + description: 'Ansible Test Role {{ resource_prefix }}' + managed_policy: + - '{{ safe_managed_policy }}' + - '{{ custom_policy_name }}' + max_session_duration: 43200 + path: '{{ test_path }}' + register: iam_role + - assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - 'iam_role.iam_role.arn.startswith("arn")' + - 'iam_role.iam_role.arn.endswith("role" + test_path + test_role )' + # Would be nice to test the contents... + - '"assume_role_policy_document" in iam_role.iam_role' + - iam_role.iam_role.attached_policies | length == 2 + - iam_role.iam_role.max_session_duration == 43200 + - iam_role.iam_role.path == test_path + - iam_role.iam_role.role_name == test_role + - '"create_date" in iam_role.iam_role' + - '"role_id" in iam_role.iam_role' + - name: Short pause for role creation to finish + pause: + seconds: 10 + when: standard_pauses | bool + + - name: 'Complex IAM role (no change)' + iam_role: + name: '{{ test_role }}' + assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}' + boundary: '{{ boundary_policy }}' + create_instance_profile: no + description: 'Ansible Test Role {{ resource_prefix }}' + managed_policy: + - '{{ safe_managed_policy }}' + - '{{ custom_policy_name }}' + max_session_duration: 43200 + path: '{{ test_path }}' + register: iam_role + - assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + + - name: 'iam_role_info after Role creation' + iam_role_info: + name: '{{ test_role }}' + register: role_info + - assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role" + test_path + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - 'role_info.iam_roles[0].description == "Ansible Test Role {{ resource_prefix }}"' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 0 + - role_info.iam_roles[0].managed_policies | length == 2 + - safe_managed_policy in ( role_info | json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - custom_policy_name in ( role_info | json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == test_path + - role_info.iam_roles[0].permissions_boundary.permissions_boundary_arn == boundary_policy + - role_info.iam_roles[0].permissions_boundary.permissions_boundary_type == 'Policy' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + + always: + # =================================================================== + # Cleanup + + # XXX iam_role fails to remove inline policies before deleting the role + - name: 'Detach inline policy a' + iam_policy: + state: absent + iam_type: 'role' + iam_name: '{{ test_role }}' + policy_name: 'inline-policy-a' + ignore_errors: true + + - name: 'Detach inline policy b' + iam_policy: + state: absent + iam_type: 'role' + iam_name: '{{ test_role }}' + policy_name: 'inline-policy-b' + ignore_errors: true + + - name: 'Remove IAM Role' + iam_role: + state: absent + name: '{{ test_role }}' + delete_instance_profile: yes + ignore_errors: true + + - name: 'Remove IAM Role (with path)' + iam_role: + state: absent + name: '{{ test_role }}' + path: '{{ test_path }}' + delete_instance_profile: yes + ignore_errors: true + + - name: 'iam_role_info after Role deletion' + iam_role_info: + name: '{{ test_role }}' + ignore_errors: true + + - name: 'Remove test managed policy' + iam_managed_policy: + state: absent + policy_name: '{{ custom_policy_name }}' diff --git a/test/integration/targets/sts_assume_role/aliases b/test/integration/targets/sts_assume_role/aliases index 859509a0af8..6e3860bee23 100644 --- a/test/integration/targets/sts_assume_role/aliases +++ b/test/integration/targets/sts_assume_role/aliases @@ -1,3 +1,2 @@ cloud/aws shippable/aws/group2 -iam_role