From 43c508b3c3b227500e74fbc7450743cfdb450eb0 Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Mon, 30 Jul 2018 15:06:23 -0400 Subject: [PATCH] [AWS] iam_role->AnsibleAWSModule and support IAM permission boundaries (#43270) * Convert to AnsibleAWSModule and support IAM permission boundaries * Handle adding boundary to existing role that lacks one * Properly clean up role boundary associations on delete * Handle case when policy boundary is `""` but does not exist --- lib/ansible/modules/cloud/amazon/iam_role.py | 90 ++++++++++++++------ 1 file changed, 64 insertions(+), 26 deletions(-) diff --git a/lib/ansible/modules/cloud/amazon/iam_role.py b/lib/ansible/modules/cloud/amazon/iam_role.py index aba7b86a28..ab4d2b6031 100644 --- a/lib/ansible/modules/cloud/amazon/iam_role.py +++ b/lib/ansible/modules/cloud/amazon/iam_role.py @@ -27,6 +27,14 @@ options: description: - Provide a description of the new role version_added: "2.5" + boundary: + description: + - Add the ARN of an IAM managed policy to restrict the permissions this role can pass on to IAM roles/users that it creates. + - Boundaries cannot be set on Instance Profiles, so if this option is specified then C(create_instance_profile) must be false. + - This is intended for roles/users that have permissions to create new IAM objects. + - For more information on boundaries, see U(https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) + aliases: [boundary_policy_arn] + version_added: "2.7" assume_role_policy_document: description: - The trust relationship policy document that grants an entity permission to assume the role. @@ -150,9 +158,9 @@ iam_role: ''' from ansible.module_utils._text import to_native -from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.aws.core import AnsibleAWSModule from ansible.module_utils.ec2 import camel_dict_to_snake_dict, ec2_argument_spec, get_aws_connection_info, boto3_conn, sort_json_policy_dict -from ansible.module_utils.ec2 import HAS_BOTO3, AWSRetry +from ansible.module_utils.ec2 import AWSRetry import json import traceback @@ -160,7 +168,7 @@ import traceback try: from botocore.exceptions import ClientError, BotoCoreError except ImportError: - pass # caught by imported HAS_BOTO3 + pass # caught by AnsibleAWSModule def compare_assume_role_policy_doc(current_policy_doc, new_policy_doc): @@ -218,13 +226,14 @@ def remove_policies(connection, module, policies_to_remove, params): def create_or_update_role(connection, module): - params = dict() 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('description') is not None: params['Description'] = module.params.get('description') + if module.params.get('boundary') is not None: + params['PermissionsBoundary'] = module.params.get('boundary') managed_policies = module.params.get('managed_policy') create_instance_profile = module.params.get('create_instance_profile') if managed_policies: @@ -339,6 +348,29 @@ def create_or_update_role(connection, module): module.fail_json(msg="Unable to update description for role {0}: {1}".format(params['RoleName'], to_native(e)), exception=traceback.format_exc()) + # Check if permission boundary needs update + if not role.get('MadeInCheckMode') and ( + (role.get('PermissionsBoundary') or {}).get('PermissionsBoundaryArn') or + params.get('PermissionsBoundary') is not None): + # the existing role has a boundary + if module.params.get('boundary') is None: + pass + elif module.params.get('boundary') == '': + if (role.get('PermissionsBoundary') or {}).get('PermissionsBoundaryArn'): + try: + if not module.check_mode: + 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))) + 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))) + # Get the role again if not role.get('MadeInCheckMode', False): role = get_role(connection, module, params['RoleName']) @@ -352,7 +384,9 @@ def destroy_role(connection, module): params = dict() params['RoleName'] = module.params.get('name') - if get_role(connection, module, params['RoleName']): + role = get_role(connection, module, params['RoleName']) + + if role: # We need to remove any instance profiles from the role before we delete it try: @@ -364,6 +398,12 @@ def destroy_role(connection, module): module.fail_json(msg="Unable to list instance profiles for role {0}: {1}".format(params['RoleName'], to_native(e)), exception=traceback.format_exc()) + 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)) + # Now remove the role from the instance profile(s) for profile in instance_profiles: try: @@ -441,30 +481,28 @@ def get_attached_policy_list(connection, module, name): def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - name=dict(type='str', required=True), - path=dict(type='str', default="/"), - assume_role_policy_document=dict(type='json'), - managed_policy=dict(type='list', aliases=['managed_policies']), - state=dict(type='str', choices=['present', 'absent'], default='present'), - description=dict(type='str'), - create_instance_profile=dict(type='bool', default=True), - purge_policies=dict(type='bool', default=True), - ) + argument_spec = dict( + name=dict(type='str', required=True), + path=dict(type='str', default="/"), + assume_role_policy_document=dict(type='json'), + managed_policy=dict(type='list', aliases=['managed_policies']), + 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), + 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) - module = AnsibleModule(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') 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 not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - - connection = boto3_conn(module, conn_type='client', resource='iam', region=region, endpoint=ec2_url, **aws_connect_params) + connection = module.client('iam') state = module.params.get("state")