[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
This commit is contained in:
Ryan Brown 2018-07-30 15:06:23 -04:00 committed by GitHub
commit 43c508b3c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -27,6 +27,14 @@ options:
description: description:
- Provide a description of the new role - Provide a description of the new role
version_added: "2.5" 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: assume_role_policy_document:
description: description:
- The trust relationship policy document that grants an entity permission to assume the role. - 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._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 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 json
import traceback import traceback
@ -160,7 +168,7 @@ import traceback
try: try:
from botocore.exceptions import ClientError, BotoCoreError from botocore.exceptions import ClientError, BotoCoreError
except ImportError: except ImportError:
pass # caught by imported HAS_BOTO3 pass # caught by AnsibleAWSModule
def compare_assume_role_policy_doc(current_policy_doc, new_policy_doc): 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): def create_or_update_role(connection, module):
params = dict() params = dict()
params['Path'] = module.params.get('path') params['Path'] = module.params.get('path')
params['RoleName'] = module.params.get('name') params['RoleName'] = module.params.get('name')
params['AssumeRolePolicyDocument'] = module.params.get('assume_role_policy_document') params['AssumeRolePolicyDocument'] = module.params.get('assume_role_policy_document')
if module.params.get('description') is not None: if module.params.get('description') is not None:
params['Description'] = module.params.get('description') 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') managed_policies = module.params.get('managed_policy')
create_instance_profile = module.params.get('create_instance_profile') create_instance_profile = module.params.get('create_instance_profile')
if managed_policies: 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)), module.fail_json(msg="Unable to update description for role {0}: {1}".format(params['RoleName'], to_native(e)),
exception=traceback.format_exc()) 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 # Get the role again
if not role.get('MadeInCheckMode', False): if not role.get('MadeInCheckMode', False):
role = get_role(connection, module, params['RoleName']) role = get_role(connection, module, params['RoleName'])
@ -352,7 +384,9 @@ def destroy_role(connection, module):
params = dict() params = dict()
params['RoleName'] = module.params.get('name') 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 # We need to remove any instance profiles from the role before we delete it
try: 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)), module.fail_json(msg="Unable to list instance profiles for role {0}: {1}".format(params['RoleName'], to_native(e)),
exception=traceback.format_exc()) 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) # Now remove the role from the instance profile(s)
for profile in instance_profiles: for profile in instance_profiles:
try: try:
@ -441,30 +481,28 @@ def get_attached_policy_list(connection, module, name):
def main(): def main():
argument_spec = ec2_argument_spec() argument_spec = dict(
argument_spec.update( name=dict(type='str', required=True),
dict( path=dict(type='str', default="/"),
name=dict(type='str', required=True), assume_role_policy_document=dict(type='json'),
path=dict(type='str', default="/"), managed_policy=dict(type='list', aliases=['managed_policies']),
assume_role_policy_document=dict(type='json'), state=dict(type='str', choices=['present', 'absent'], default='present'),
managed_policy=dict(type='list', aliases=['managed_policies']), description=dict(type='str'),
state=dict(type='str', choices=['present', 'absent'], default='present'), boundary=dict(type='str', aliases=['boundary_policy_arn']),
description=dict(type='str'), create_instance_profile=dict(type='bool', default=True),
create_instance_profile=dict(type='bool', default=True), purge_policies=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, if module.params.get('boundary') and module.params.get('create_instance_profile'):
required_if=[('state', 'present', ['assume_role_policy_document'])], module.fail_json(msg="When using a boundary policy, `create_instance_profile` must be set to `false`.")
supports_check_mode=True) 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: connection = module.client('iam')
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)
state = module.params.get("state") state = module.params.get("state")