From 0e24c57c08b57676c36170e54557e2b94c7884e2 Mon Sep 17 00:00:00 2001 From: Derek Date: Mon, 24 Jun 2024 23:29:35 +1000 Subject: [PATCH 1/8] Add a credential plugin to implement AWS AssumeRole functionality Signed-off-by: Derek --- awx/main/credential_plugins/aws_assumerole.py | 100 ++++++++++++++++++ .../functional/test_credential_plugins.py | 27 +++++ setup.cfg | 1 + 3 files changed, 128 insertions(+) create mode 100644 awx/main/credential_plugins/aws_assumerole.py diff --git a/awx/main/credential_plugins/aws_assumerole.py b/awx/main/credential_plugins/aws_assumerole.py new file mode 100644 index 000000000000..0efb8dcb93e2 --- /dev/null +++ b/awx/main/credential_plugins/aws_assumerole.py @@ -0,0 +1,100 @@ +import boto3 +import collections +import hashlib +import datetime +from botocore.exceptions import ClientError + +from .plugin import CredentialPlugin +from django.utils.translation import gettext_lazy as _ + +try: + from botocore.exceptions import ClientError + from botocore.exceptions import ParamValidationError +except ImportError: + pass # caught by AnsibleAWSModule + +_aws_cred_cache = {} + + +assume_role_inputs = { + 'fields': [ + { + 'id': 'access_key', + 'label': _('AWS Access Key'), + 'type': 'string', + 'help_text': _('The optional AWS access key for the user who will assume the role'), + }, + { + 'id': 'secret_key', + 'label': 'AWS Secret Key', + 'type': 'string', + 'secret': True, + 'help_text': _('The optional AWS secret key for the user who will assume the role'), + }, + { + 'id': 'external_id', + 'label': 'External ID', + 'type': 'string', + 'help_text': _('The optional External ID which will be provided to the assume role API'), + }, + {'id': 'role_arn', 'label': 'AWS ARN Role Name', 'type': 'string', 'help_text': _('The ARN Role Name to be assumed in AWS')}, + ], + 'metadata': [ + { + 'id': 'identifier', + 'label': 'Identifier', + 'type': 'string', + 'help_text': _('The name of the key in the assumed AWS role to fetch [AccessKeyId | SecretAccessKey | SessionToken].'), + }, + ], + 'required': ['role_arn'], +} + + +def aws_assumerole_backend(**kwargs): + """This backend function actually contacts AWS to assume a given role for the specified user""" + access_key = kwargs.get('access_key') + secret_key = kwargs.get('secret_key') + role_arn = kwargs.get('role_arn') + external_id = kwargs.get('external_id') + identifier = kwargs.get('identifier') + + # Generate a hash unique MD5 for combo of user access key and ARN + # This should allow two users requesting the same ARN role to have + # separate credentials, and should allow the same user to request + # multiple roles. + # + credential_key_hash = hashlib.md5((access_key + role_arn).encode('utf-8')) + credential_key = credential_key_hash.hexdigest() + + credentials = _aws_cred_cache.get(credential_key, None) + + # If there are no credentials for this user/ARN *or* the credentials + # we have in the cache have expired, then we need to contact AWS again. + # + if (credentials is None) or (credentials['Expiration'] < datetime.datetime.now(credentials['Expiration'].tzinfo)): + + if (access_key is None or len(access_key) == 0) and (secret_key is None or len(secret_key) == 0): + # Connect using credentials in the EE + connection = boto3.client(service_name="sts") + else: + # Connect to AWS using provided credentials + connection = boto3.client(service_name="sts", aws_access_key_id=access_key, aws_secret_access_key=secret_key) + try: + response = connection.assume_role(RoleArn=role_arn, RoleSessionName='AAP_AWS_Role_Session1', ExternalId=external_id) + except ClientError as ce: + raise ValueError(f'Got a bad client response from AWS: {ce.msg}.') + + credentials = response.get("Credentials", {}) + + _aws_cred_cache[credential_key] = credentials + + credentials = _aws_cred_cache.get(credential_key, None) + + if identifier in credentials: + return credentials[identifier] + + raise ValueError(f'Could not find a value for {identifier}.') + + +aws_assumerole_plugin = CredentialPlugin('AWS Assume Role Plugin', inputs=assume_role_inputs, backend=aws_assumerole_backend) diff --git a/awx/main/tests/functional/test_credential_plugins.py b/awx/main/tests/functional/test_credential_plugins.py index 3ee29e9ce33a..198db21a2689 100644 --- a/awx/main/tests/functional/test_credential_plugins.py +++ b/awx/main/tests/functional/test_credential_plugins.py @@ -1,6 +1,7 @@ import pytest from unittest import mock from awx.main.credential_plugins import hashivault +from awx.main.credential_plugins import aws_assumerole def test_imported_azure_cloud_sdk_vars(): @@ -121,6 +122,32 @@ def test_hashivault_handle_auth_not_enough_args(): hashivault.handle_auth() +def test_aws_assumerole_with_accesssecret(): + kwargs = { + 'access_key': 'the_access_key', + 'secret_key': 'the_secret_key', + 'role_arn': 'the_arn', + 'identifier': 'the_session_token', + } + with mock.patch.object(aws_assumerole, 'aws_assumerole_backend') as method_mock: + method_mock.return_value = 'the_session_token' + token = aws_assumerole.aws_assumerole_backend(**kwargs) + method_mock.assert_called_with(**kwargs, auth_param=kwargs) + assert token == 'the_session_token' + + +def test_aws_assumerole_with_arnonly(): + kwargs = { + 'role_arn': 'the_arn', + 'identifier': 'the_session_token', + } + with mock.patch.object(aws_assumerole, 'aws_assumerole_backend') as method_mock: + method_mock.return_value = 'the_session_token' + token = aws_assumerole.aws_assumerole_backend(**kwargs) + method_mock.assert_called_with(**kwargs, auth_param=kwargs) + assert token == 'the_session_token' + + class TestDelineaImports: """ These module have a try-except for ImportError which will allow using the older library diff --git a/setup.cfg b/setup.cfg index c861d3ae92cf..fc0c74928556 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,4 +22,5 @@ awx.credential_plugins = centrify_vault_kv = awx.main.credential_plugins.centrify_vault:centrify_plugin thycotic_dsv = awx.main.credential_plugins.dsv:dsv_plugin thycotic_tss = awx.main.credential_plugins.tss:tss_plugin + aws_assumerole = awx.main.credential_plugins.aws_assumerole:aws_assumerole_plugin aws_secretsmanager_credential = awx.main.credential_plugins.aws_secretsmanager:aws_secretmanager_plugin From 9abcd82c87ddad610620774a3e84e8aa067d81a8 Mon Sep 17 00:00:00 2001 From: Derek Date: Mon, 24 Jun 2024 23:44:39 +1000 Subject: [PATCH 2/8] Add documentation for AWS AssumeRole Plugin lookup fields Signed-off-by: Derek --- docs/docsite/rst/userguide/credential_plugins.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/docsite/rst/userguide/credential_plugins.rst b/docs/docsite/rst/userguide/credential_plugins.rst index 6ce53b862f7b..3d42bf8b9c93 100644 --- a/docs/docsite/rst/userguide/credential_plugins.rst +++ b/docs/docsite/rst/userguide/credential_plugins.rst @@ -66,6 +66,9 @@ Use the AWX User Interface to configure and use each of the supported 3-party se * - - AWS Secret Name (Required) - Specify the AWS secret name that was generated by the AWS access key. + * - *AWS Assume Role Plugin* + - Identifier (required) + - Specifies the name of the property to return (``AccessKeyId``, ``SecretAccessKey`` or ``SessionToken``). * - *Centrify Vault Credential Provider Lookup* - Account Name (Required) - Name of the system account or domain associated with Centrify Vault. From 1a3ac780699e4f87f00eb8b0dcb3ba7a7f1f115c Mon Sep 17 00:00:00 2001 From: Derek Waters Date: Fri, 2 Aug 2024 19:17:22 +1000 Subject: [PATCH 3/8] Apply linting suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) --- awx/main/credential_plugins/aws_assumerole.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/awx/main/credential_plugins/aws_assumerole.py b/awx/main/credential_plugins/aws_assumerole.py index 0efb8dcb93e2..e883efde8f24 100644 --- a/awx/main/credential_plugins/aws_assumerole.py +++ b/awx/main/credential_plugins/aws_assumerole.py @@ -1,15 +1,12 @@ import boto3 -import collections import hashlib import datetime -from botocore.exceptions import ClientError from .plugin import CredentialPlugin from django.utils.translation import gettext_lazy as _ try: from botocore.exceptions import ClientError - from botocore.exceptions import ParamValidationError except ImportError: pass # caught by AnsibleAWSModule From 57595826274ccbf72c424b0ffb7a5a71697e6c42 Mon Sep 17 00:00:00 2001 From: Derek Date: Thu, 8 Aug 2024 21:50:14 +1000 Subject: [PATCH 4/8] Fix errors in unit test code Signed-off-by: Derek --- awx/main/tests/functional/test_credential.py | 1 + awx/main/tests/functional/test_credential_plugins.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/awx/main/tests/functional/test_credential.py b/awx/main/tests/functional/test_credential.py index 97cf2beb2d81..b2d896952fdb 100644 --- a/awx/main/tests/functional/test_credential.py +++ b/awx/main/tests/functional/test_credential.py @@ -78,6 +78,7 @@ def test_default_cred_types(): [ 'aim', 'aws', + 'aws_assumerole', 'aws_secretsmanager_credential', 'azure_kv', 'azure_rm', diff --git a/awx/main/tests/functional/test_credential_plugins.py b/awx/main/tests/functional/test_credential_plugins.py index 198db21a2689..3344d3894fef 100644 --- a/awx/main/tests/functional/test_credential_plugins.py +++ b/awx/main/tests/functional/test_credential_plugins.py @@ -131,7 +131,7 @@ def test_aws_assumerole_with_accesssecret(): } with mock.patch.object(aws_assumerole, 'aws_assumerole_backend') as method_mock: method_mock.return_value = 'the_session_token' - token = aws_assumerole.aws_assumerole_backend(**kwargs) + token = aws_assumerole.backend(**kwargs) method_mock.assert_called_with(**kwargs, auth_param=kwargs) assert token == 'the_session_token' @@ -143,7 +143,7 @@ def test_aws_assumerole_with_arnonly(): } with mock.patch.object(aws_assumerole, 'aws_assumerole_backend') as method_mock: method_mock.return_value = 'the_session_token' - token = aws_assumerole.aws_assumerole_backend(**kwargs) + token = aws_assumerole.backend(**kwargs) method_mock.assert_called_with(**kwargs, auth_param=kwargs) assert token == 'the_session_token' From 3f001d72c1a2189773c5ab2c5847fea3bbdb07dd Mon Sep 17 00:00:00 2001 From: Derek Date: Thu, 8 Aug 2024 22:34:17 +1000 Subject: [PATCH 5/8] Replace MD5 hashing with SHA256 --- awx/main/credential_plugins/aws_assumerole.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/main/credential_plugins/aws_assumerole.py b/awx/main/credential_plugins/aws_assumerole.py index e883efde8f24..cba74dcedb5f 100644 --- a/awx/main/credential_plugins/aws_assumerole.py +++ b/awx/main/credential_plugins/aws_assumerole.py @@ -56,12 +56,12 @@ def aws_assumerole_backend(**kwargs): external_id = kwargs.get('external_id') identifier = kwargs.get('identifier') - # Generate a hash unique MD5 for combo of user access key and ARN + # Generate a unique SHA256 hash for combo of user access key and ARN # This should allow two users requesting the same ARN role to have # separate credentials, and should allow the same user to request # multiple roles. # - credential_key_hash = hashlib.md5((access_key + role_arn).encode('utf-8')) + credential_key_hash = hashlib.sha256((access_key + role_arn).encode('utf-8')) credential_key = credential_key_hash.hexdigest() credentials = _aws_cred_cache.get(credential_key, None) From 5e4a58775e65ee617fbb943b6a6393e57be29e17 Mon Sep 17 00:00:00 2001 From: Derek Date: Sat, 10 Aug 2024 17:11:42 +1000 Subject: [PATCH 6/8] Refactor for unit testing --- awx/main/credential_plugins/aws_assumerole.py | 35 ++++++----- .../functional/test_credential_plugins.py | 60 ++++++++++++++----- 2 files changed, 67 insertions(+), 28 deletions(-) diff --git a/awx/main/credential_plugins/aws_assumerole.py b/awx/main/credential_plugins/aws_assumerole.py index cba74dcedb5f..cba2a5443484 100644 --- a/awx/main/credential_plugins/aws_assumerole.py +++ b/awx/main/credential_plugins/aws_assumerole.py @@ -19,6 +19,7 @@ 'id': 'access_key', 'label': _('AWS Access Key'), 'type': 'string', + 'secret': True, 'help_text': _('The optional AWS access key for the user who will assume the role'), }, { @@ -34,7 +35,7 @@ 'type': 'string', 'help_text': _('The optional External ID which will be provided to the assume role API'), }, - {'id': 'role_arn', 'label': 'AWS ARN Role Name', 'type': 'string', 'help_text': _('The ARN Role Name to be assumed in AWS')}, + {'id': 'role_arn', 'label': 'AWS ARN Role Name', 'type': 'string', 'secret': True, 'help_text': _('The ARN Role Name to be assumed in AWS')}, ], 'metadata': [ { @@ -48,6 +49,23 @@ } +def aws_assumerole_getcreds(access_key, secret_key, role_arn, external_id): + if (access_key is None or len(access_key) == 0) and (secret_key is None or len(secret_key) == 0): + # Connect using credentials in the EE + connection = boto3.client(service_name="sts") + else: + # Connect to AWS using provided credentials + connection = boto3.client(service_name="sts", aws_access_key_id=access_key, aws_secret_access_key=secret_key) + try: + response = connection.assume_role(RoleArn=role_arn, RoleSessionName='AAP_AWS_Role_Session1', ExternalId=external_id) + except ClientError as ce: + raise ValueError(f'Got a bad client response from AWS: {ce.msg}.') + + credentials = response.get("Credentials", {}) + + return credentials + + def aws_assumerole_backend(**kwargs): """This backend function actually contacts AWS to assume a given role for the specified user""" access_key = kwargs.get('access_key') @@ -61,7 +79,7 @@ def aws_assumerole_backend(**kwargs): # separate credentials, and should allow the same user to request # multiple roles. # - credential_key_hash = hashlib.sha256((access_key + role_arn).encode('utf-8')) + credential_key_hash = hashlib.sha256((str(access_key or '') + role_arn).encode('utf-8')) credential_key = credential_key_hash.hexdigest() credentials = _aws_cred_cache.get(credential_key, None) @@ -71,18 +89,7 @@ def aws_assumerole_backend(**kwargs): # if (credentials is None) or (credentials['Expiration'] < datetime.datetime.now(credentials['Expiration'].tzinfo)): - if (access_key is None or len(access_key) == 0) and (secret_key is None or len(secret_key) == 0): - # Connect using credentials in the EE - connection = boto3.client(service_name="sts") - else: - # Connect to AWS using provided credentials - connection = boto3.client(service_name="sts", aws_access_key_id=access_key, aws_secret_access_key=secret_key) - try: - response = connection.assume_role(RoleArn=role_arn, RoleSessionName='AAP_AWS_Role_Session1', ExternalId=external_id) - except ClientError as ce: - raise ValueError(f'Got a bad client response from AWS: {ce.msg}.') - - credentials = response.get("Credentials", {}) + credentials = aws_assumerole_getcreds(access_key, secret_key, role_arn, external_id) _aws_cred_cache[credential_key] = credentials diff --git a/awx/main/tests/functional/test_credential_plugins.py b/awx/main/tests/functional/test_credential_plugins.py index 3344d3894fef..61e9cda67179 100644 --- a/awx/main/tests/functional/test_credential_plugins.py +++ b/awx/main/tests/functional/test_credential_plugins.py @@ -1,4 +1,5 @@ import pytest +import datetime from unittest import mock from awx.main.credential_plugins import hashivault from awx.main.credential_plugins import aws_assumerole @@ -124,28 +125,59 @@ def test_hashivault_handle_auth_not_enough_args(): def test_aws_assumerole_with_accesssecret(): kwargs = { - 'access_key': 'the_access_key', - 'secret_key': 'the_secret_key', + 'access_key': 'my_access_key', + 'secret_key': 'my_secret_key', 'role_arn': 'the_arn', - 'identifier': 'the_session_token', + 'identifier': 'access_token', } - with mock.patch.object(aws_assumerole, 'aws_assumerole_backend') as method_mock: - method_mock.return_value = 'the_session_token' - token = aws_assumerole.backend(**kwargs) - method_mock.assert_called_with(**kwargs, auth_param=kwargs) - assert token == 'the_session_token' + method_call_with = [kwargs.get('access_key'), kwargs.get('secret_key'), kwargs.get('role_arn'), None] + with mock.patch.object(aws_assumerole, 'aws_assumerole_getcreds') as method_mock: + method_mock.return_value = { + 'access_key': 'the_access_key', + 'secret_key': 'the_secret_key', + 'access_token': 'the_access_token', + 'Expiration': datetime.datetime.today() + datetime.timedelta(days=1), + } + token = aws_assumerole.aws_assumerole_backend(**kwargs) + method_mock.assert_called_with(kwargs.get('access_key'), kwargs.get('secret_key'), kwargs.get('role_arn'), None) + assert token == 'the_access_token' + kwargs['identifier'] = 'secret_key' + method_mock.reset_mock() + token = aws_assumerole.aws_assumerole_backend(**kwargs) + method_mock.assert_not_called() + assert token == 'the_secret_key' + kwargs['identifier'] = 'access_key' + method_mock.reset_mock() + token = aws_assumerole.aws_assumerole_backend(**kwargs) + method_mock.assert_not_called() + assert token == 'the_access_key' def test_aws_assumerole_with_arnonly(): kwargs = { 'role_arn': 'the_arn', - 'identifier': 'the_session_token', + 'identifier': 'access_token', } - with mock.patch.object(aws_assumerole, 'aws_assumerole_backend') as method_mock: - method_mock.return_value = 'the_session_token' - token = aws_assumerole.backend(**kwargs) - method_mock.assert_called_with(**kwargs, auth_param=kwargs) - assert token == 'the_session_token' + with mock.patch.object(aws_assumerole, 'aws_assumerole_getcreds') as method_mock: + method_mock.return_value = { + 'access_key': 'the_access_key', + 'secret_key': 'the_secret_key', + 'access_token': 'the_access_token', + 'Expiration': datetime.datetime.today() + datetime.timedelta(days=1), + } + token = aws_assumerole.aws_assumerole_backend(**kwargs) + method_mock.assert_called_with(None, None, kwargs.get('role_arn'), None) + assert token == 'the_access_token' + kwargs['identifier'] = 'secret_key' + method_mock.reset_mock() + token = aws_assumerole.aws_assumerole_backend(**kwargs) + method_mock.assert_not_called() + assert token == 'the_secret_key' + kwargs['identifier'] = 'access_key' + method_mock.reset_mock() + token = aws_assumerole.aws_assumerole_backend(**kwargs) + method_mock.assert_not_called() + assert token == 'the_access_key' class TestDelineaImports: From 88509cadc0de717f91f4964c96c24c28dc02a759 Mon Sep 17 00:00:00 2001 From: Derek Date: Sat, 10 Aug 2024 18:21:18 +1000 Subject: [PATCH 7/8] Update documentation for AWS Assume Role plugin --- ...ials-create-aws-assume-role-credential.png | Bin 0 -> 41682 bytes .../rst/userguide/credential_plugins.rst | 22 ++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 docs/docsite/rst/common/images/credentials-create-aws-assume-role-credential.png diff --git a/docs/docsite/rst/common/images/credentials-create-aws-assume-role-credential.png b/docs/docsite/rst/common/images/credentials-create-aws-assume-role-credential.png new file mode 100644 index 0000000000000000000000000000000000000000..5ddc9d5fa5823c9cd2b704de668237c6aa788b61 GIT binary patch literal 41682 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 literal 0 HcmV?d00001 diff --git a/docs/docsite/rst/userguide/credential_plugins.rst b/docs/docsite/rst/userguide/credential_plugins.rst index 3d42bf8b9c93..be84f5a9ca00 100644 --- a/docs/docsite/rst/userguide/credential_plugins.rst +++ b/docs/docsite/rst/userguide/credential_plugins.rst @@ -150,6 +150,28 @@ This example shows the Metadata prompt for HashiVault Secret Lookup. 8. Click **Save** when done. +.. _ug_credentials_aws_assume_role: + +AWS Assume Role Lookup +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. index:: + pair: credential types; AWS + +This plugin allows AWS credential details to assume an AWS IAM role to be used as a credential source. + +When **AWS Assume Role lookup** is selected for **Credential Type**, provide the following attributes to properly configure your lookup: + +- **AWS Access Key** : provide the access key used for communicating with AWS' IAM role assumption API +- **AWS Secret Key** : provide the secret key used for communicating with AWS' IAM role assumption API +- **External ID** : provide an optional app-specific identifier used for auditing and securing the IAM role assumption +- **AWS ARN Role Name** (required): provide the ARN of the IAM role that should be assumed + +Below shows an example of a configured AWS Assume Role credential. + +.. image:: ../common/images/credentials-create-aws-assume-role-credential.png + :width: 1400px + :alt: Example new AWS Assume Role credential lookup dialog + .. _ug_credentials_aws_lookup: From 0fd02b724c8de6b639fddee5ffe67f7afd43ddac Mon Sep 17 00:00:00 2001 From: Derek Date: Tue, 13 Aug 2024 06:39:03 +1000 Subject: [PATCH 8/8] Remove unused test variable --- awx/main/tests/functional/test_credential_plugins.py | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/main/tests/functional/test_credential_plugins.py b/awx/main/tests/functional/test_credential_plugins.py index 61e9cda67179..3a9360ad5ee7 100644 --- a/awx/main/tests/functional/test_credential_plugins.py +++ b/awx/main/tests/functional/test_credential_plugins.py @@ -130,7 +130,6 @@ def test_aws_assumerole_with_accesssecret(): 'role_arn': 'the_arn', 'identifier': 'access_token', } - method_call_with = [kwargs.get('access_key'), kwargs.get('secret_key'), kwargs.get('role_arn'), None] with mock.patch.object(aws_assumerole, 'aws_assumerole_getcreds') as method_mock: method_mock.return_value = { 'access_key': 'the_access_key',