-
Notifications
You must be signed in to change notification settings - Fork 14
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 session token support #23
Conversation
This commit will add a pytest fixture to configure the S3 bucket name. A configurable S3 bucket name enables contributors to run tests on their forks and in their AWS accounts, by setting a GitHub Secrets variable `${{ secrets.TEST_AWS_S3_BUCKET_NAME }}`. Defaults to `aioaws-testing`. The function is written with an if expression instead of with `os.getenv('TEST_AWS_S3_BUCKET_NAME', 'aioaws-testing')` because of how the environment variable value is passed in from GitHub Actions. If `${{ secrets.TEST_AWS_S3_BUCKET_NAME }}` is not set, then the value of the environment variable `TEST_AWS_S3_BUCKET_NAME` will be an empty string, and `os.getenv('TEST_AWS_S3_BUCKET_NAME', 'aioaws-testing')` will return an empty string.
As with the S3 bucket name, enabling configuration of the email address allowscontributors to run tests on their forks and in their AWS accounts by setting `${{ secrets.TEST_AWS_SES_ADDRESS }}` in GitHub Secrets. Defaults to `testing@scolvin.com`.
This is a "resource-based policy" that allows an IAM role to be assumed. https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_identity-vs-resource.html
- Use aws-actions/configure-aws-credentials to obtain temporary security credentials - Pass both the static and temporary credentials to pytest
Parametrize the pytest fixture so that it is generated for each item in params, and each test that uses the fixture runs for each param https://docs.pytest.org/en/latest/how-to/fixtures.html
Session tokens are used when AWS resources obtain temporary credentials. - Add `aws_session_token: str` model fields - `aioaws._types.S3ConfigProtocol` - `aioaws.s3.S3Config` - Add `aioaws.core.AwsClient.aws_session_token` attribute - Update `aioaws.core.AwsClient._auth_headers` to add session token into signature - For S3 downloads, add `X-Amz-Security-Token` param to `aioaws.core.AwsClient.add_signed_download_params` - For S3 uploads, allow `X-Amz-Security-Token` as an extra condition in `aioaws.core.AwsClient.upload_extra_conditions`, then add to upload signature using `aioaws.core.AwsClient.signed_upload_fields`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looking good, but we need to get tests to work before I can review properly.
@@ -7,6 +7,7 @@ on: | |||
tags: | |||
- '**' | |||
pull_request: {} | |||
workflow_dispatch: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is making the yml invalid and meaning tests can't be run.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it can be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The workflow YAML is valid. See here for an example run on my fork.
I'm okay with removing it though, if you prefer.
You'll need to grant approval for the workflow to run - is this what you're referring to?
aioaws/core.py
Outdated
@@ -32,6 +32,7 @@ def __init__(self, client: AsyncClient, config: 'BaseConfigProtocol', service: L | |||
self.client = client | |||
self.aws_access_key = get_config_attr(config, 'aws_access_key') | |||
self.aws_secret_key = get_config_attr(config, 'aws_secret_key') | |||
self.aws_session_token = getattr(config, 'aws_session_token') if hasattr(config, 'aws_session_token') else '' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.aws_session_token = getattr(config, 'aws_session_token') if hasattr(config, 'aws_session_token') else '' | |
self.aws_session_token = getattr(config, 'aws_session_token', None) |
Was there a reason to use an empty string as a default instead of None
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The syntax in your suggestion is clearer, thank you. -> 7b7ba5f
It is possible to use None
as a default here, but I used an empty string to keep the type signature consistent with S3Config.aws_session_token
and S3ConfigProtocol.aws_session_token
.
aioaws/core.py
Outdated
params.update({'X-Amz-Security-Token': self.aws_session_token}) | ||
params.update({'X-Amz-SignedHeaders': 'host'}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
params.update({'X-Amz-Security-Token': self.aws_session_token}) | |
params.update({'X-Amz-SignedHeaders': 'host'}) | |
params['X-Amz-Security-Token'] = self.aws_session_token | |
params['X-Amz-SignedHeaders'] = 'host' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason to put host
after X-Amz-Security-Token
?
Same question to the cases below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, to keep the fields in alphabetical order. I get errors when the fields are out of order. You had a similar note for headers here:
Line 146 in 9e93843
# WARNING! order is important here, headers need to be in alphabetical order |
aws_region: str | ||
aws_s3_bucket: str | ||
aws_session_token: str = field(repr=False, default='') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
again, is there a reason to use ''
instead of None
as default?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, I used an empty string as a default to keep the type signature consistent with S3ConfigProtocol.aws_session_token
. Otherwise the type signature would be Optional[str]
.
A default of None
could also impact use of _utils.get_config_attr
, because this method expects all config attributes to be strings.
Line 25 in 9e93843
raise TypeError(f'config.{name} must be a string not {s.__class__.__name__}') |
will be an empty string, and `os.getenv('TEST_AWS_S3_BUCKET_NAME', 'aioaws-testing')` | ||
will return an empty string. | ||
""" | ||
return value if (value := os.getenv('TEST_AWS_S3_BUCKET_NAME')) else 'aioaws-testing' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return value if (value := os.getenv('TEST_AWS_S3_BUCKET_NAME')) else 'aioaws-testing' | |
return os.getenv('TEST_AWS_S3_BUCKET_NAME', 'aioaws-testing') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This syntax is a simple way to differentiate between:
- an environment variable set to an empty string
- an environment variable not set at all
This is mostly for GitHub Actions. If ${{ secrets.TEST_AWS_S3_BUCKET_NAME }}
is not set:
- GitHub Actions will pass in
TEST_AWS_S3_BUCKET_NAME=''
os.getenv('TEST_AWS_S3_BUCKET_NAME', 'aioaws-testing')
will return''
, rather than'aioaws-testing'
.
>>> import os
>>> os.getenv('TEST_AWS_S3_BUCKET_NAME')
>>> os.environ['TEST_AWS_S3_BUCKET_NAME'] = ''
>>> os.getenv('TEST_AWS_S3_BUCKET_NAME')
''
>>> os.getenv('TEST_AWS_S3_BUCKET_NAME', 'aioaws-testing')
''
>>> value if (value := os.getenv('TEST_AWS_S3_BUCKET_NAME')) else 'aioaws-testing'
'aioaws-testing'
contributors to run tests on their forks and in their AWS accounts, by setting | ||
`${{ secrets.TEST_AWS_SES_ADDRESS }}` on GitHub. Defaults to `testing@scolvin.com`. | ||
""" | ||
return value if (value := os.getenv('TEST_AWS_SES_ADDRESS')) else 'testing@scolvin.com' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return value if (value := os.getenv('TEST_AWS_SES_ADDRESS')) else 'testing@scolvin.com' | |
return os.getenv('TEST_AWS_SES_ADDRESS', 'testing@scolvin.com') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as for TEST_AWS_S3_BUCKET_NAME
: #23 (comment)
Thanks for your review! I would welcome any further comments or suggestions. |
really not sure why tests didn't run for your two pull requests, I just got a friend to try and PRs from external contributors are working #26 |
Tests appear to be running after merging the master branch. You'll need to set up a role in order to test session tokens. See the "Maintainer TODOs" section above. Once you get it set up, runs should succeed like this. AWS CLI steps would look like this: cd aioaws
# copy your account number (replace fx with jq or other JSON parser as needed)
AWS_ACCOUNT_NUMBER=$(aws sts get-caller-identity | fx .Account)
# add account number and username to role trust policy (this is GNU sed syntax)
gsed -Ei "s|<AWS_ACCOUNT_NUMBER>|$AWS_ACCOUNT_NUMBER|g" aws_test_role_permissions.json
gsed -Ei "s|<IAM_USERNAME>|aioaws|g" aws_test_role_permissions.json
# create IAM role using the role trust policy document
aws iam create-role --role-name aioaws-testing-role --assume-role-policy-document file://aws_test_role_permissions.json
# create a customer-managed identity-based policy if you haven't already
POLICY_ARN=$(aws iam create-policy --policy-name aioaws-testing --policy-document file://aws_test_key_permissions.json | fx .Policy.Arn)
# attach the policy to the role
aws iam attach-role-policy --policy-arn $POLICY_ARN --role-name aioaws-testing-role |
It's not I'll look into the role changes next week. |
This reverts commit 4f0142d.
can you remove the changes to |
Again, you need to create a role in your AWS account for the updated tests to work. |
Sorry, I'll do that. |
Closing off this PR because it hasn't moved forward in about a year, and I no longer have a use case for this package. |
Sorry I didn't get around to completing this, and thanks for the contribution. 🙏 |
Description
Resolves #16
Thanks for this project. It’s refreshing to see an alternative to the other "bloated, opaque" AWS tools out there.
This PR will add basic AWS session token support to aioaws. AWS session tokens are used when an AWS resource obtains temporary security credentials.
The authorization flow for temporary security credentials commonly works like this:
AssumeRole
API.AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
AWS_SESSION_TOKEN
Changes
Source code
aws_session_token: str
model fieldsaioaws._types.S3ConfigProtocol
aioaws.s3.S3Config
aioaws.core.AwsClient.aws_session_token
attributeaioaws.core.AwsClient._auth_headers
to add session token into signatureX-Amz-Security-Token
param toaioaws.core.AwsClient.add_signed_download_params
X-Amz-Security-Token
as an extra condition inaioaws.core.AwsClient.upload_extra_conditions
, then add to upload signature usingaioaws.core.AwsClient.signed_upload_fields
Tests
${{ secrets.TEST_AWS_S3_BUCKET_NAME }}
in GitHub Secrets to use the bucket name in GitHub Actions. Defaults to the previous hard-coded value (aioaws-testing
).${{ secrets.TEST_AWS_SES_ADDRESS }}
in GitHub Secrets. Defaults to the previous hard-coded value (testing@scolvin.com
).workflow_dispatch
trigger to GitHub Actions workflow: Enables workflow to be run manually, from any branch. Useful for testing code changes on feature branches.IAM policies
aws_test_role_permissions.json
sts:AssumeRole
allows GitHub Actions to obtain temporary security credentialssts:TagSession
allows GitHub Actions to tag the temporary security credentials with metadata as described here and hereUsage
AWS_SESSION_TOKEN
can be added with theS3Config.aws_session_token
attribute.Maintainer TODOs
There are a few steps needed to enable
AssumeRole
for testing. This may be confusing, so I'm happy to help with setup as needed. We can also do this with OpenID Connect if you prefer.aws_test_role_permissions.json
with:<AWS_ACCOUNT_NUMBER>
: Your account number<IAM_USERNAME>
: The name of the IAM user that owns the access key used for testingaws_test_role_permissions.json
as the role trust policyaws_test_key_permissions.json
, if you haven't alreadyaws_test_key_permissions.json
to the roleAWS CLI steps look like this:
The end result is that the AWS access key can be used to assume the role and obtain temporary credentials. The temporary credentials have the same IAM policy as the regular access key, so tests can be parametrized accordingly.
Notes
Session token expiration
aioaws clients do not automatically rotate temporary credentials. Developers are responsible for updating client attributes or instantiating new clients when temporary credentials expire.
Token expiration should be taken into account when generating S3 presigned URLs. As explained in the docs, "If you created a presigned URL using a temporary token, then the URL expires when the token expires, even if the URL was created with a later expiration time."
Other credential sources
There are several other ways to source credentials (see the AWS IAM docs, AWS CLI docs, and Boto3 docs). This project only handles AWS access keys and session tokens.
For CI/CD, GitHub Actions supports OpenID Connect ("OIDC"), which allows workflows to authenticate with AWS without having to store access keys in GitHub Secrets.