-
Notifications
You must be signed in to change notification settings - Fork 34
Add assumerole #24
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
base: devel
Are you sure you want to change the base?
Add assumerole #24
Changes from all commits
764bb69
5d19ff1
cf6d481
24ce59c
b50e003
35e5dcb
656ecb7
847d081
1807b76
648e8f7
4dd176a
c6a99ae
fb2a28c
5268832
d9435eb
836b5c9
e949dd2
2d48e82
ef334bc
814e586
93a28bd
1345871
a0737ef
29aee1e
86eef44
ea9d0e7
f1e4035
4e7cc43
499c494
87cd182
5d346ff
3f4340c
3e44826
b308b67
a132eeb
d441571
9883c3d
b565e0f
e876ff0
bf1ec34
753b6af
d5f99d6
b1a2129
9430333
e93e917
4ada473
0b1e5ef
fb85b7e
444d121
74b9206
c4dd94d
50fe83e
803c659
83ab18e
3f8bb9e
669ba01
881976f
53fb4a2
f7db82c
9842a9b
c75d33c
ff886aa
0dcc430
0eb6e86
164b54a
db9e869
cbeea1c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
thedoubl3j marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,182 @@ | ||||||||||||||
| """This module provides integration with AWS AssumeRole functionality.""" | ||||||||||||||
|
|
||||||||||||||
| import hashlib | ||||||||||||||
| import typing | ||||||||||||||
| from datetime import datetime | ||||||||||||||
|
|
||||||||||||||
| from awx_plugins.interfaces._temporary_private_django_api import ( # noqa: WPS436 | ||||||||||||||
| gettext_noop as _, | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| import boto3 | ||||||||||||||
| from botocore.exceptions import ClientError | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| if typing.TYPE_CHECKING: | ||||||||||||||
| from mypy_boto3_sts.client import STSClient | ||||||||||||||
| from mypy_boto3_sts.type_defs import ( | ||||||||||||||
| AssumeRoleResponseTypeDef, | ||||||||||||||
| CredentialsTypeDef, | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| from .plugin import CredentialPlugin | ||||||||||||||
thedoubl3j marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| _aws_cred_cache: dict[ | ||||||||||||||
| str, | ||||||||||||||
| 'CredentialsTypeDef | dict[typing.Never, typing.Never]', | ||||||||||||||
| ] | dict[typing.Never, typing.Never] = {} | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| 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.', | ||||||||||||||
| ), | ||||||||||||||
| }, | ||||||||||||||
| ], | ||||||||||||||
| '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: str | None, | ||||||||||||||
| secret_key: str | None, | ||||||||||||||
| role_arn: str, | ||||||||||||||
| external_id: int, | ||||||||||||||
| ) -> 'CredentialsTypeDef | dict[typing.Never, typing.Never]': | ||||||||||||||
| """Return the credentials for use. | ||||||||||||||
|
|
||||||||||||||
| :param access_key: The AWS access key ID. | ||||||||||||||
| :type access_key: str | ||||||||||||||
| :param secret_key: The AWS secret access key. | ||||||||||||||
| :type secret_key: str | ||||||||||||||
| :param role_arn: The ARN received from AWS. | ||||||||||||||
| :type role_arn: str | ||||||||||||||
| :param external_id: The external ID received from AWS. | ||||||||||||||
| :type external_id: int | ||||||||||||||
| :returns: The credentials received from AWS. | ||||||||||||||
| :rtype: dict | ||||||||||||||
| :raises ValueError: If the client response is bad. | ||||||||||||||
| """ | ||||||||||||||
| connection: 'STSClient' = boto3.client( | ||||||||||||||
| service_name='sts', | ||||||||||||||
| # The following EE creds are read from the env if they are not passed: | ||||||||||||||
| aws_access_key_id=access_key, # defaults to `None` in the lib | ||||||||||||||
| aws_secret_access_key=secret_key, # defaults to `None` in the lib | ||||||||||||||
| ) | ||||||||||||||
| try: | ||||||||||||||
| response: 'AssumeRoleResponseTypeDef' = connection.assume_role( | ||||||||||||||
| RoleArn=role_arn, | ||||||||||||||
| RoleSessionName='AAP_AWS_Role_Session1', | ||||||||||||||
| ExternalId=external_id, | ||||||||||||||
| ) | ||||||||||||||
| except ClientError as client_err: | ||||||||||||||
| raise ValueError( | ||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Or perhaps a
Suggested change
It might also be a
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think any of these make sense and agree value isn't the correct thing in place. Only one I can see kinda not fitting is connection since errors could or could not be connection related. IMO lookup makes the most since we the actual function call is going and "lookin " something up but I am not dead set on that.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep. That's what I often use. And the calling code would need to handle it and turn into whatever awx interface currently wants, which seems to be a |
||||||||||||||
| f'Got a bad client response from AWS: {client_err.message}.', | ||||||||||||||
| ) from client_err | ||||||||||||||
|
|
||||||||||||||
| return response.get('Credentials', {}) | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def aws_assumerole_backend( | ||||||||||||||
| access_key: str | None, | ||||||||||||||
| secret_key: str | None, | ||||||||||||||
| role_arn: str, | ||||||||||||||
| external_id: int, | ||||||||||||||
| identifier: str, | ||||||||||||||
| ) -> dict: | ||||||||||||||
| """Contact AWS to assume a given role for the user. | ||||||||||||||
|
|
||||||||||||||
| :param access_key: The AWS access key ID. | ||||||||||||||
| :type access_key: str | ||||||||||||||
| :param secret_key: The AWS secret access key. | ||||||||||||||
| :type secret_key: str | ||||||||||||||
| :param role_arn: The ARN received from AWS. | ||||||||||||||
| :type role_arn: str | ||||||||||||||
| :param external_id: The external ID received from AWS. | ||||||||||||||
| :type external_id: int | ||||||||||||||
| :param identifier: The identifier to fetch from the assumed role. | ||||||||||||||
| :type identifier: str | ||||||||||||||
| :raises ValueError: If the identifier is not found. | ||||||||||||||
| :returns: The identifier fetched from the assumed role. | ||||||||||||||
| :rtype: dict | ||||||||||||||
| """ | ||||||||||||||
| # 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, {}) | ||||||||||||||
|
|
||||||||||||||
| # 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. | ||||||||||||||
| creds_expired = ( | ||||||||||||||
| (creds_expire_at := credentials.get('Expiration')) and | ||||||||||||||
| creds_expire_at < datetime.now(credentials['Expiration'].tzinfo) | ||||||||||||||
| ) | ||||||||||||||
| if creds_expired: | ||||||||||||||
|
|
||||||||||||||
| 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, {}) | ||||||||||||||
|
|
||||||||||||||
| try: | ||||||||||||||
| return credentials[identifier] | ||||||||||||||
| except KeyError as key_err: | ||||||||||||||
| raise ValueError( | ||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The same concern goes for this exception. Although I imagine that awx expects a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that is my thought as well, looking at the actual error message makes it seem like this should be dev focused since we are the ones going and getting this actual value with the user inputs but could be helpful for debugging if someone hits this. kinda leaves a good flag as where things could have fallen apart?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically, all the exceptions are targeting the devs, so are the tracebacks. But if awx uses this somehow, we can't change it to a |
||||||||||||||
| f'Could not find a value for {identifier}.', | ||||||||||||||
| ) from key_err | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| aws_assumerole_plugin = CredentialPlugin( | ||||||||||||||
| 'AWS Assume Role Plugin', | ||||||||||||||
| inputs=assume_role_inputs, | ||||||||||||||
| backend=aws_assumerole_backend, | ||||||||||||||
| ) | ||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.