diff --git a/changelogs/fragments/migrate_sts_assume_role.yml b/changelogs/fragments/migrate_sts_assume_role.yml new file mode 100644 index 00000000000..f054230de9b --- /dev/null +++ b/changelogs/fragments/migrate_sts_assume_role.yml @@ -0,0 +1,4 @@ +major_changes: +- sts_assume_role - The module has been migrated from the ``community.aws`` collection. + Playbooks using the Fully Qualified Collection Name for this module should be updated + to use ``amazon.aws.sts_assume_role``. diff --git a/meta/runtime.yml b/meta/runtime.yml index 62050ae54b0..14faa7da240 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -109,6 +109,7 @@ action_groups: - s3_bucket - s3_object - s3_object_info + - sts_assume_role plugin_routing: action: aws_s3: diff --git a/plugins/modules/sts_assume_role.py b/plugins/modules/sts_assume_role.py new file mode 100644 index 00000000000..96abfd20136 --- /dev/null +++ b/plugins/modules/sts_assume_role.py @@ -0,0 +1,173 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r""" +--- +module: sts_assume_role +version_added: 1.0.0 +version_added_collection: community.aws +short_description: Assume a role using AWS Security Token Service and obtain temporary credentials +description: + - Assume a role using AWS Security Token Service and obtain temporary credentials. +author: + - Boris Ekelchik (@bekelchik) + - Marek Piatek (@piontas) +options: + role_arn: + description: + - The Amazon Resource Name (ARN) of the role that the caller is + assuming U(https://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html#Identifiers_ARNs). + required: true + type: str + role_session_name: + description: + - Name of the role's session - will be used by CloudTrail. + required: true + type: str + policy: + description: + - Supplemental policy to use in addition to assumed role's policies. + type: str + duration_seconds: + description: + - The duration, in seconds, of the role session. The value can range from 900 seconds (15 minutes) to 43200 seconds (12 hours). + - The max depends on the IAM role's sessions duration setting. + - By default, the value is set to 3600 seconds. + type: int + external_id: + description: + - A unique identifier that is used by third parties to assume a role in their customers' accounts. + type: str + mfa_serial_number: + description: + - The identification number of the MFA device that is associated with the user who is making the AssumeRole call. + type: str + mfa_token: + description: + - The value provided by the MFA device, if the trust policy of the role being assumed requires MFA. + type: str +notes: + - In order to use the assumed role in a following playbook task you must pass the I(access_key), + I(secret_key) and I(session_token) parameters to modules that should use the assumed credentials. +extends_documentation_fragment: + - amazon.aws.common.modules + - amazon.aws.region.modules + - amazon.aws.boto3 +""" + +RETURN = r""" +sts_creds: + description: The temporary security credentials, which include an access key ID, a secret access key, and a security (or session) token + returned: always + type: dict + sample: + access_key: XXXXXXXXXXXXXXXXXXXX + expiration: '2017-11-11T11:11:11+00:00' + secret_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + session_token: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +sts_user: + description: The Amazon Resource Name (ARN) and the assumed role ID + returned: always + type: dict + sample: + assumed_role_id: arn:aws:sts::123456789012:assumed-role/demo/Bob + arn: ARO123EXAMPLE123:Bob +changed: + description: True if obtaining the credentials succeeds + type: bool + returned: always +""" + +EXAMPLES = r""" +# Assume an existing role (more details: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html) +- amazon.aws.sts_assume_role: + access_key: AKIA1EXAMPLE1EXAMPLE + secret_key: 123456789abcdefghijklmnopqrstuvwxyzABCDE + role_arn: "arn:aws:iam::123456789012:role/someRole" + role_session_name: "someRoleSession" + register: assumed_role + +# Use the assumed role above to tag an instance in account 123456789012 +- amazon.aws.ec2_tag: + access_key: "{{ assumed_role.sts_creds.access_key }}" + secret_key: "{{ assumed_role.sts_creds.secret_key }}" + session_token: "{{ assumed_role.sts_creds.session_token }}" + resource: i-xyzxyz01 + state: present + tags: + MyNewTag: value + +""" + +try: + from botocore.exceptions import ClientError + from botocore.exceptions import ParamValidationError +except ImportError: + pass # caught by AnsibleAWSModule + +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict + +from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule + + +def _parse_response(response): + credentials = response.get("Credentials", {}) + user = response.get("AssumedRoleUser", {}) + + sts_cred = { + "access_key": credentials.get("AccessKeyId"), + "secret_key": credentials.get("SecretAccessKey"), + "session_token": credentials.get("SessionToken"), + "expiration": credentials.get("Expiration"), + } + sts_user = camel_dict_to_snake_dict(user) + return sts_cred, sts_user + + +def assume_role_policy(connection, module): + params = { + "RoleArn": module.params.get("role_arn"), + "RoleSessionName": module.params.get("role_session_name"), + "Policy": module.params.get("policy"), + "DurationSeconds": module.params.get("duration_seconds"), + "ExternalId": module.params.get("external_id"), + "SerialNumber": module.params.get("mfa_serial_number"), + "TokenCode": module.params.get("mfa_token"), + } + changed = False + + kwargs = dict((k, v) for k, v in params.items() if v is not None) + + try: + response = connection.assume_role(**kwargs) + changed = True + except (ClientError, ParamValidationError) as e: + module.fail_json_aws(e) + + sts_cred, sts_user = _parse_response(response) + module.exit_json(changed=changed, sts_creds=sts_cred, sts_user=sts_user) + + +def main(): + argument_spec = dict( + role_arn=dict(required=True), + role_session_name=dict(required=True), + duration_seconds=dict(required=False, default=None, type="int"), + external_id=dict(required=False, default=None), + policy=dict(required=False, default=None), + mfa_serial_number=dict(required=False, default=None), + mfa_token=dict(required=False, default=None, no_log=True), + ) + + module = AnsibleAWSModule(argument_spec=argument_spec) + + connection = module.client("sts") + + assume_role_policy(connection, module) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/cloudtrail/tasks/main.yml b/tests/integration/targets/cloudtrail/tasks/main.yml index 60d1dad95c0..86fc7bdc605 100644 --- a/tests/integration/targets/cloudtrail/tasks/main.yml +++ b/tests/integration/targets/cloudtrail/tasks/main.yml @@ -1336,7 +1336,7 @@ # Assume role to a role with Denied access to KMS - - community.aws.sts_assume_role: + - amazon.aws.sts_assume_role: role_arn: '{{ output_cloudwatch_no_kms_role.arn }}' role_session_name: "cloudtrailNoKms" region: '{{ aws_region }}' diff --git a/tests/integration/targets/sts_assume_role/aliases b/tests/integration/targets/sts_assume_role/aliases new file mode 100644 index 00000000000..4ef4b2067d0 --- /dev/null +++ b/tests/integration/targets/sts_assume_role/aliases @@ -0,0 +1 @@ +cloud/aws diff --git a/tests/integration/targets/sts_assume_role/defaults/main.yml b/tests/integration/targets/sts_assume_role/defaults/main.yml new file mode 100644 index 00000000000..17072d6a4fd --- /dev/null +++ b/tests/integration/targets/sts_assume_role/defaults/main.yml @@ -0,0 +1 @@ +iam_role_name: "ansible-test-{{ tiny_prefix }}" diff --git a/tests/integration/targets/sts_assume_role/meta/main.yml b/tests/integration/targets/sts_assume_role/meta/main.yml new file mode 100644 index 00000000000..32cf5dda7ed --- /dev/null +++ b/tests/integration/targets/sts_assume_role/meta/main.yml @@ -0,0 +1 @@ +dependencies: [] diff --git a/tests/integration/targets/sts_assume_role/tasks/main.yml b/tests/integration/targets/sts_assume_role/tasks/main.yml new file mode 100644 index 00000000000..23e0dba7843 --- /dev/null +++ b/tests/integration/targets/sts_assume_role/tasks/main.yml @@ -0,0 +1,305 @@ +--- +# tasks file for sts_assume_role + +- module_defaults: + group/aws: + region: "{{ aws_region }}" + access_key: "{{ aws_access_key }}" + secret_key: "{{ aws_secret_key }}" + session_token: "{{ security_token | default(omit) }}" + collections: + - amazon.aws + block: + # Get some information about who we are before starting our tests + # we'll need this as soon as we start working on the policies + - name: get ARN of calling user + aws_caller_info: + register: aws_caller_info + + - name: register account id + set_fact: + aws_account: "{{ aws_caller_info.account }}" + + # ============================================================ + - name: create test iam role + iam_role: + name: "{{ iam_role_name }}" + assume_role_policy_document: "{{ lookup('template','policy.json.j2') }}" + create_instance_profile: False + managed_policy: + - arn:aws:iam::aws:policy/IAMReadOnlyAccess + state: present + register: test_role + + # ============================================================ + - name: pause to ensure role exists before using + pause: + seconds: 30 + + # ============================================================ + - name: test with no parameters + sts_assume_role: + access_key: '{{ omit }}' + secret_key: '{{ omit }}' + session_token: '{{ omit }}' + register: result + ignore_errors: true + + - name: assert with no parameters + assert: + that: + - 'result.failed' + - "'missing required arguments:' in result.msg" + + # ============================================================ + - name: test with only 'role_arn' parameter + sts_assume_role: + role_arn: "{{ test_role.iam_role.arn }}" + register: result + ignore_errors: true + + - name: assert with only 'role_arn' parameter + assert: + that: + - 'result.failed' + - "'missing required arguments: role_session_name' in result.msg" + + # ============================================================ + - name: test with only 'role_session_name' parameter + sts_assume_role: + role_session_name: "AnsibleTest" + register: result + ignore_errors: true + + - name: assert with only 'role_session_name' parameter + assert: + that: + - 'result.failed' + - "'missing required arguments: role_arn' in result.msg" + + # ============================================================ + - name: test assume role with invalid policy + sts_assume_role: + role_arn: "{{ test_role.iam_role.arn }}" + role_session_name: "AnsibleTest" + policy: "invalid policy" + register: result + ignore_errors: true + + - name: assert assume role with invalid policy + assert: + that: + - 'result.failed' + - "'The policy is not in the valid JSON format.' in result.msg" + when: result.module_stderr is not defined + + - name: assert assume role with invalid policy + assert: + that: + - 'result.failed' + - "'The policy is not in the valid JSON format.' in result.module_stderr" + when: result.module_stderr is defined + + # ============================================================ + - name: test assume role with invalid duration seconds + sts_assume_role: + role_arn: "{{ test_role.iam_role.arn }}" + role_session_name: AnsibleTest + duration_seconds: invalid duration + register: result + ignore_errors: true + + - name: assert assume role with invalid duration seconds + assert: + that: + - result is failed + - "'duration_seconds' in result.msg" + - "'cannot be converted to an int' in result.msg" + + # ============================================================ + - name: test assume role with invalid external id + sts_assume_role: + role_arn: "{{ test_role.iam_role.arn }}" + role_session_name: AnsibleTest + external_id: invalid external id + register: result + ignore_errors: true + + - name: assert assume role with invalid external id + assert: + that: + - 'result.failed' + - "'Member must satisfy regular expression pattern:' in result.msg" + when: result.module_stderr is not defined + + - name: assert assume role with invalid external id + assert: + that: + - 'result.failed' + - "'Member must satisfy regular expression pattern:' in result.module_stderr" + when: result.module_stderr is defined + + # ============================================================ + - name: test assume role with invalid mfa serial number + sts_assume_role: + role_arn: "{{ test_role.iam_role.arn }}" + role_session_name: AnsibleTest + mfa_serial_number: invalid serial number + register: result + ignore_errors: true + + - name: assert assume role with invalid mfa serial number + assert: + that: + - 'result.failed' + - "'Member must satisfy regular expression pattern:' in result.msg" + when: result.module_stderr is not defined + + - name: assert assume role with invalid mfa serial number + assert: + that: + - 'result.failed' + - "'Member must satisfy regular expression pattern:' in result.module_stderr" + when: result.module_stderr is defined + + # ============================================================ + - name: test assume role with invalid mfa token code + sts_assume_role: + role_arn: "{{ test_role.iam_role.arn }}" + role_session_name: AnsibleTest + mfa_token: invalid token code + register: result + ignore_errors: true + + - name: assert assume role with invalid mfa token code + assert: + that: + - 'result.failed' + - "'Member must satisfy regular expression pattern:' in result.msg" + when: result.module_stderr is not defined + + - name: assert assume role with invalid mfa token code + assert: + that: + - 'result.failed' + - "'Member must satisfy regular expression pattern:' in result.module_stderr" + when: result.module_stderr is defined + + # ============================================================ + - name: test assume role with invalid role_arn + sts_assume_role: + role_arn: invalid role arn + role_session_name: AnsibleTest + register: result + ignore_errors: true + + - name: assert assume role with invalid role_arn + assert: + that: + - result.failed + - "'Invalid length for parameter RoleArn' in result.msg" + when: result.module_stderr is not defined + + - name: assert assume role with invalid role_arn + assert: + that: + - 'result.failed' + - "'Member must have length greater than or equal to 20' in result.module_stderr" + when: result.module_stderr is defined + + # ============================================================ + - name: test assume not existing sts role + sts_assume_role: + role_arn: "arn:aws:iam::123456789:role/non-existing-role" + role_session_name: "AnsibleTest" + register: result + ignore_errors: true + + - name: assert assume not existing sts role + assert: + that: + - 'result.failed' + - "'is not authorized to perform: sts:AssumeRole' in result.msg" + when: result.module_stderr is not defined + + - name: assert assume not existing sts role + assert: + that: + - 'result.failed' + - "'is not authorized to perform: sts:AssumeRole' in result.msg" + when: result.module_stderr is defined + + # ============================================================ + - name: test assume role + sts_assume_role: + role_arn: "{{ test_role.iam_role.arn }}" + role_session_name: AnsibleTest + register: assumed_role + + - name: assert assume role + assert: + that: + - 'not assumed_role.failed' + - "'sts_creds' in assumed_role" + - "'access_key' in assumed_role.sts_creds" + - "'secret_key' in assumed_role.sts_creds" + - "'session_token' in assumed_role.sts_creds" + + # ============================================================ + - name: test that assumed credentials have IAM read-only access + iam_role: + access_key: "{{ assumed_role.sts_creds.access_key }}" + secret_key: "{{ assumed_role.sts_creds.secret_key }}" + session_token: "{{ assumed_role.sts_creds.session_token }}" + name: "{{ iam_role_name }}" + assume_role_policy_document: "{{ lookup('template','policy.json.j2') }}" + create_instance_profile: False + state: present + register: result + + - name: assert assumed role with privileged action (expect changed=false) + assert: + that: + - 'not result.failed' + - 'not result.changed' + - "'iam_role' in result" + + # ============================================================ + - name: test assumed role with unprivileged action + iam_role: + access_key: "{{ assumed_role.sts_creds.access_key }}" + secret_key: "{{ assumed_role.sts_creds.secret_key }}" + session_token: "{{ assumed_role.sts_creds.session_token }}" + name: "{{ iam_role_name }}-new" + assume_role_policy_document: "{{ lookup('template','policy.json.j2') }}" + state: present + register: result + ignore_errors: true + + - name: assert assumed role with unprivileged action (expect changed=false) + assert: + that: + - 'result.failed' + - "'is not authorized to perform: iam:CreateRole' in result.msg" + # runs on Python2 + when: result.module_stderr is not defined + + - name: assert assumed role with unprivileged action (expect changed=false) + assert: + that: + - 'result.failed' + - "'is not authorized to perform: iam:CreateRole' in result.module_stderr" + # runs on Python3 + when: result.module_stderr is defined + + # ============================================================ + always: + + - name: delete test iam role + iam_role: + name: "{{ iam_role_name }}" + assume_role_policy_document: "{{ lookup('template','policy.json.j2') }}" + delete_instance_profile: True + managed_policy: + - arn:aws:iam::aws:policy/IAMReadOnlyAccess + state: absent diff --git a/tests/integration/targets/sts_assume_role/templates/policy.json.j2 b/tests/integration/targets/sts_assume_role/templates/policy.json.j2 new file mode 100644 index 00000000000..559562fd91d --- /dev/null +++ b/tests/integration/targets/sts_assume_role/templates/policy.json.j2 @@ -0,0 +1,12 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::{{ aws_account }}:root" + }, + "Action": "sts:AssumeRole" + } + ] +} \ No newline at end of file