Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add aws assume role plugin #15294

Open
wants to merge 11 commits into
base: devel
Choose a base branch
from
104 changes: 104 additions & 0 deletions awx/main/credential_plugins/aws_assumerole.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import boto3
import hashlib
import datetime

from .plugin import CredentialPlugin
from django.utils.translation import gettext_lazy as _

try:
from botocore.exceptions import ClientError
except ImportError:
pass # caught by AnsibleAWSModule

_aws_cred_cache = {}


assume_role_inputs = {
'fields': [
{
'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'),
},
{
'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', 'secret': True, '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_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')
secret_key = kwargs.get('secret_key')
role_arn = kwargs.get('role_arn')
external_id = kwargs.get('external_id')
identifier = kwargs.get('identifier')

# 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.sha256((str(access_key or '') + 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)):

credentials = aws_assumerole_getcreds(access_key, secret_key, role_arn, external_id)

_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)
1 change: 1 addition & 0 deletions awx/main/tests/functional/test_credential.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def test_default_cred_types():
[
'aim',
'aws',
'aws_assumerole',
'aws_secretsmanager_credential',
'azure_kv',
'azure_rm',
Expand Down
58 changes: 58 additions & 0 deletions awx/main/tests/functional/test_credential_plugins.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import pytest
import datetime
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():
Expand Down Expand Up @@ -121,6 +123,62 @@ def test_hashivault_handle_auth_not_enough_args():
hashivault.handle_auth()


def test_aws_assumerole_with_accesssecret():
kwargs = {
'access_key': 'my_access_key',
'secret_key': 'my_secret_key',
'role_arn': 'the_arn',
'identifier': 'access_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(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': 'access_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:
"""
These module have a try-except for ImportError which will allow using the older library
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions docs/docsite/rst/userguide/credential_plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -147,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:

Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading