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 session token support #23

Closed
wants to merge 14 commits into from
Closed

Add AWS session token support #23

wants to merge 14 commits into from

Conversation

br3ndonland
Copy link

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:

  • IAM roles, such as service-linked roles or Lambda execution roles, are set up and linked to infrastructure resources. These roles can have two kinds of IAM policies attached: resource-based and identity-based policies.
    • Identity-based policies define interactions with other resources on AWS.
    • A resource-based policy called a "role trust policy" defines how the role can be assumed.
  • The AWS runtime (Fargate, Lambda, etc) requests authorization to use the role by calling the STS AssumeRole API.
  • If the requesting entity has permissions to assume the role, STS responds with temporary security credentials that have permissions based on the identity-based policies associated with the IAM role.
  • The AWS runtime stores the temporary security credentials, typically by setting environment variables:
    • AWS_ACCESS_KEY_ID
    • AWS_SECRET_ACCESS_KEY
    • AWS_SESSION_TOKEN
  • AWS API calls with temporary credentials must include the session token, as mentioned in AWS_SESSION_TOKEN support #16.
  • The AWS runtime will typically rotate the temporary security credentials before they expire.

Changes

Source code

  • 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

Tests

  • Make S3 bucket name configurable in tests: S3 bucket names are globally unique, so other contributors will need to create buckets with different names. They can then set ${{ 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).
  • Make SES email address configurable in tests: As with S3 bucket names, enabling configuration of the email address allows contributors to run tests on their forks and in their AWS accounts, by setting ${{ secrets.TEST_AWS_SES_ADDRESS }} in GitHub Secrets. Defaults to the previous hard-coded value (testing@scolvin.com).
  • Add 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

  • Add role trust policy document aws_test_role_permissions.json

Usage

AWS_SESSION_TOKEN can be added with the S3Config.aws_session_token attribute.

S3Config('<access key>', '<secret key>', '<region>', 'my_bucket_name.com', os.getenv('AWS_SESSION_TOKEN', ''))

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.

  • Update the new role trust policy file 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 testing
  • Create an IAM role for the user to assume (same user that owns the access key used for testing), using aws_test_role_permissions.json as the role trust policy
  • Create a customer-managed identity-based policy using aws_test_key_permissions.json, if you haven't already
  • Attach the managed policy from aws_test_key_permissions.json to the role

AWS CLI steps 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

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.

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`.
- 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`
Copy link
Owner

@samuelcolvin samuelcolvin left a 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:
Copy link
Owner

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.

Copy link
Owner

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.

Copy link
Author

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 ''
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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?

Copy link
Author

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
Comment on lines 115 to 116
params.update({'X-Amz-Security-Token': self.aws_session_token})
params.update({'X-Amz-SignedHeaders': 'host'})
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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'

Copy link
Owner

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.

Copy link
Author

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:

# WARNING! order is important here, headers need to be in alphabetical order

aioaws/core.py Outdated Show resolved Hide resolved
aioaws/core.py Outdated Show resolved Hide resolved
aws_region: str
aws_s3_bucket: str
aws_session_token: str = field(repr=False, default='')
Copy link
Owner

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?

Copy link
Author

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.

raise TypeError(f'config.{name} must be a string not {s.__class__.__name__}')

tests/conftest.py Outdated Show resolved Hide resolved
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'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return value if (value := os.getenv('TEST_AWS_S3_BUCKET_NAME')) else 'aioaws-testing'
return os.getenv('TEST_AWS_S3_BUCKET_NAME', 'aioaws-testing')

Copy link
Author

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'

tests/conftest.py Outdated Show resolved Hide resolved
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'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return value if (value := os.getenv('TEST_AWS_SES_ADDRESS')) else 'testing@scolvin.com'
return os.getenv('TEST_AWS_SES_ADDRESS', 'testing@scolvin.com')

Copy link
Author

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)

@br3ndonland
Copy link
Author

looking good, but we need to get tests to work before I can review properly.

Thanks for your review! I would welcome any further comments or suggestions.

@samuelcolvin
Copy link
Owner

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

@br3ndonland
Copy link
Author

br3ndonland commented Jan 22, 2022

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

@samuelcolvin
Copy link
Owner

It's not pull_request: {} since #26 ran tests.

I'll look into the role changes next week.

@samuelcolvin
Copy link
Owner

can you remove the changes to .github/workflows/ci.yml they seem to be causing the PR to fail.

@br3ndonland
Copy link
Author

can you remove the changes to .github/workflows/ci.yml they seem to be causing the PR to fail.

Again, you need to create a role in your AWS account for the updated tests to work.

@samuelcolvin
Copy link
Owner

Sorry, I'll do that.

@br3ndonland
Copy link
Author

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.

@samuelcolvin
Copy link
Owner

Sorry I didn't get around to completing this, and thanks for the contribution. 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

AWS_SESSION_TOKEN support
2 participants