Compare commits

...
Author SHA1 Message Date
Damian Debkowski 2e4cfa41e9 chore: add comment for ToMap method 2023-09-22 11:55:20 -07:00
Damian Debkowski cc86ec92e8 chore: change awsutilv2 reference to awsutil 2023-09-22 11:42:46 -07:00
Hugo a0f070e5ac fix(storage): Static rotated creds being deleted on plugin err
On Storage Bucket update, when we determine if the credentials are to be
updated to new ones, we call `credState.ReplaceCreds`.

This call, if the old credentials were managed by us (rotated at create
time), will delete the old credentials and set the new ones onto
credState.

We called this function without validating if the incoming credentials
are valid. This could get the bucket in a state where where, while we'd
return an update error if the incoming credentials weren't working with
S3 (err in the dry run process that happens at the end of the update),
we'd also silently delete the old credentials the bucket used, rendering
the bucket completely broken.
2023-09-21 18:45:35 +01:00
Hugo 2568eb83b8 fix(storage): Storage Assume Role Support
This commit introduces fixes to enable operators to use AWS dynamic
credentials on the storage side of this plugin, namely validation,
improved credential lifecycle management to allow for storage buckets to
be updated from static to dynamic credentials and vice versa, and a new
way to determine credential types.

It also introduces new tests for dynamic credential usage for the
various functions.
2023-09-21 18:45:35 +01:00
Damian Debkowski d970294995 refactor: switch from awsutil to awsutilv2 2023-08-08 23:14:11 -07:00
Damian Debkowski 03e5368b71 fix(host): catch dry run validation error (#31) 2023-07-19 10:56:30 -07:00
Damian Debkowski 07f015be28 feat: assume role provider credentials (#28)
* feat: assume role provider credentials

This commit updates the awsutil dependency to its latest version, which
supports the capability of using dynamic credentials. The storage service
was refactored to make static credentials optional. The credentials pkg
was refactored to support several attribute fields used for the assume
role provider. The host service still enforces the use of static
credentials.
2023-07-18 16:33:24 -07:00
Damian Debkowski 334732967d chore: host plugin import aws-sdk-go-v2 (#27) 2023-07-04 11:03:24 -07:00
Damian Debkowski 7bf9fe2ecf chore: fix credential rotation reference in readme (#25) 2023-06-05 09:53:19 -07:00
hashicorp-copywrite[bot] c6687840d5 [COMPLIANCE] Add Copyright and License Headers (#26)
Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2023-06-05 09:52:48 -07:00
Louis Ruch 62e885a802 chore: Rename module to drop host element (#24) 2023-06-02 08:55:14 -07:00
Stan Ryzhov 42629b729e feat(ci): Update GitHub Actions Runners (#23) 2023-06-02 11:02:40 -04:00
41 changed files with 3755 additions and 1452 deletions

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@ on:
jobs:
test:
runs-on: [self-hosted, ondemand, linux, small]
runs-on: ubuntu-latest
env:
GO111MODULE: on
permissions:
+19 -2
View File
@@ -5,15 +5,32 @@ Boundary](https://www.boundaryproject.io/).
## Credential Rotation
Although credentials are stored encrypted within Boundary, by default this
This is the following priority for the credential chain:
static, assume role, environment variables.
### Static Credentials
Although static credentials are stored encrypted within Boundary, by default this
plugin will attempt to rotate credentials when they are supplied through the
`secrets` object. The given credentials will be used to create a new credential,
and then the given credential will be revoked. In this way, after rotation,
only Boundary knows the client secret in use by this plugin.
only Boundary knows the client secret in use by this plugin. More information
about AWS static credentials can be found [here](https://docs.aws.amazon.com/sdkref/latest/guide/feature-static-credentials.html).
Credential rotation can be turned off by setting the
`disable_credential_rotation` attribute to `true`.
### Assume Role Credentials
This plugin will attempt to assume a role when a `role_arn` is supplied through the
`attributes` object. More information about assume an AWS role can be found [here](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html). This feature only works when the plugin is running on a self managed Boundary [worker](https://developer.hashicorp.com/boundary/tutorials/hcp-administration/hcp-manage-workers).
### Environment Credentials
This plugin will attempt to retrieve credentials from environment variables. More
information about environment variables for AWS credentials can be found [here](https://docs.aws.amazon.com/sdkref/latest/guide/feature-static-credentials.html). This feature only works when the plugin is running on
a self managed Boundary [worker](https://developer.hashicorp.com/boundary/tutorials/hcp-administration/hcp-manage-workers).
## Dynamic Hosts
This plugin supports dynamically sourcing hosts from Amazon EC2.
+23 -22
View File
@@ -1,39 +1,41 @@
module github.com/hashicorp/boundary-plugin-host-aws
module github.com/hashicorp/boundary-plugin-aws
go 1.20
require (
github.com/aws/aws-sdk-go v1.44.80
github.com/aws/aws-sdk-go-v2 v1.17.7
github.com/aws/aws-sdk-go-v2/config v1.18.19
github.com/aws/aws-sdk-go-v2/credentials v1.13.18
github.com/aws/aws-sdk-go-v2/service/s3 v1.31.1
github.com/aws/aws-sdk-go-v2 v1.20.1
github.com/aws/aws-sdk-go-v2/service/ec2 v1.99.0
github.com/aws/aws-sdk-go-v2/service/iam v1.22.2
github.com/aws/aws-sdk-go-v2/service/s3 v1.38.2
github.com/aws/aws-sdk-go-v2/service/sts v1.21.2
github.com/aws/smithy-go v1.14.1
github.com/google/go-cmp v0.5.9
github.com/google/uuid v1.3.0
github.com/hashicorp/boundary/sdk v0.0.33
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.0.0
github.com/hashicorp/terraform-json v0.14.0
github.com/mitchellh/mapstructure v1.5.0
github.com/stretchr/testify v1.8.2
github.com/stretchr/testify v1.8.4
google.golang.org/grpc v1.53.0
google.golang.org/protobuf v1.28.1
)
require (
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.26 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.12 // indirect
github.com/aws/aws-sdk-go-v2/config v1.18.33 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.32 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.8 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.38 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.32 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.39 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.33 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.32 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.13.2 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.14.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
@@ -52,7 +54,6 @@ require (
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/pointerstructure v1.2.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/zclconf/go-cty v1.10.0 // indirect
golang.org/x/crypto v0.8.0 // indirect
+48 -61
View File
@@ -4,55 +4,61 @@ github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/aws/aws-sdk-go v1.44.80 h1:jEXGecSgPdvM5KnyDsSgFhZSm7WwaTp4h544Im4SfhI=
github.com/aws/aws-sdk-go v1.44.80/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go-v2 v1.17.7 h1:CLSjnhJSTSogvqUGhIC6LqFKATMRexcxLZ0i/Nzk9Eg=
github.com/aws/aws-sdk-go-v2 v1.17.7/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
github.com/aws/aws-sdk-go-v2/config v1.18.19 h1:AqFK6zFNtq4i1EYu+eC7lcKHYnZagMn6SW171la0bGw=
github.com/aws/aws-sdk-go-v2/config v1.18.19/go.mod h1:XvTmGMY8d52ougvakOv1RpiTLPz9dlG/OQHsKU/cMmY=
github.com/aws/aws-sdk-go-v2/credentials v1.13.18 h1:EQMdtHwz0ILTW1hoP+EwuWhwCG1hD6l3+RWFQABET4c=
github.com/aws/aws-sdk-go-v2/credentials v1.13.18/go.mod h1:vnwlwjIe+3XJPBYKu1et30ZPABG3VaXJYr8ryohpIyM=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 h1:gt57MN3liKiyGopcqgNzJb2+d9MJaKT/q1OksHNXVE4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1/go.mod h1:lfUx8puBRdM5lVVMQlwt2v+ofiG/X6Ms+dy0UkG/kXw=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 h1:sJLYcS+eZn5EeNINGHSCRAwUJMFVqklwkH36Vbyai7M=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31/go.mod h1:QT0BqUvX1Bh2ABdTGnjqEjvjzrCfIniM9Sc8zn9Yndo=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 h1:1mnRASEKnkqsntcxHaysxwgVoUUp5dkiB+l3llKnqyg=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25/go.mod h1:zBHOPwhBc3FlQjQJE/D3IfPWiWaQmT06Vq9aNukDo0k=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 h1:p5luUImdIqywn6JpQsW3tq5GNOxKmOnEpybzPx+d1lk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32/go.mod h1:XGhIBZDEgfqmFIugclZ6FU7v75nHhBDtzuB4xB/tEi4=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23 h1:DWYZIsyqagnWL00f8M/SOr9fN063OEQWn9LLTbdYXsk=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23/go.mod h1:uIiFgURZbACBEQJfqTZPb/jxO7R+9LeoHUFudtIdeQI=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.26 h1:CeuSeq/8FnYpPtnuIeLQEEvDv9zUjneuYi8EghMBdwQ=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.26/go.mod h1:2UqAAwMUXKeRkAHIlDJqvMVgOWkUi/AUXPk/YIe+Dg4=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25 h1:5LHn8JQ0qvjD9L9JhMtylnkcw7j05GDZqM9Oin6hpr0=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25/go.mod h1:/95IA+0lMnzW6XzqYJRpjjsAbKEORVeO0anQqjd2CNU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.0 h1:e2ooMhpYGhDnBfSvIyusvAwX7KexuZaHbQY2Dyei7VU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.0/go.mod h1:bh2E0CXKZsQN+faiKVqC40vfNMAWheoULBCnEgO9K+8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.31.1 h1:PJH4I+qYjPXclKRbVCW47iYUvtXEh1u6YmDhn5J8VQE=
github.com/aws/aws-sdk-go-v2/service/s3 v1.31.1/go.mod h1:ncltU6n4Nof5uJttDtcNQ537uNuwYqsZZQcpkd2/GUQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 h1:5V7DWLBd7wTELVz5bPpwzYy/sikk0gsgZfj40X+l5OI=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.6/go.mod h1:Y1VOmit/Fn6Tz1uFAeCO6Q7M2fmfXSCLeL5INVYsLuY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6 h1:B8cauxOH1W1v7rd8RdI/MWnoR4Ze0wIHWrb90qczxj4=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6/go.mod h1:Lh/bc9XUf8CfOY6Jp5aIkQtN+j1mc+nExc+KXj9jx2s=
github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 h1:bWNgNdRko2x6gqa0blfATqAZKZokPIeM1vfmQt2pnvM=
github.com/aws/aws-sdk-go-v2/service/sts v1.18.7/go.mod h1:JuTnSoeePXmMVe9G8NcjjwgOKEfZ4cOjMuT2IBT/2eI=
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2 v1.20.1 h1:rZBf5DWr7YGrnlTK4kgDQGn1ltqOg5orCYb/UhOFZkg=
github.com/aws/aws-sdk-go-v2 v1.20.1/go.mod h1:NU06lETsFm8fUC6ZjhgDpVBcGZTFQ6XM+LZWZxMI4ac=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.12 h1:lN6L3LrYHeZ6xCxaIYtoWCx4GMLk4nRknsh29OMSqHY=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.12/go.mod h1:TDCkEAkMTXxTs0oLBGBKpBZbk3NLh8EvAfF0Q3x8/0c=
github.com/aws/aws-sdk-go-v2/config v1.18.33 h1:JKcw5SFxFW/rpM4mOPjv0VQ11E2kxW13F3exWOy7VZU=
github.com/aws/aws-sdk-go-v2/config v1.18.33/go.mod h1:hXO/l9pgY3K5oZJldamP0pbZHdPqqk+4/maa7DSD3cA=
github.com/aws/aws-sdk-go-v2/credentials v1.13.32 h1:lIH1eKPcCY1ylR4B6PkBGRWMHO3aVenOKJHWiS4/G2w=
github.com/aws/aws-sdk-go-v2/credentials v1.13.32/go.mod h1:lL8U3v/Y79YRG69WlAho0OHIKUXCyFvSXaIvfo81sls=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.8 h1:DK/9C+UN/X+1+Wm8pqaDksQr2tSLzq+8X1/rI/ZxKEQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.8/go.mod h1:ce7BgLQfYr5hQFdy67oX2svto3ufGtm6oBvmsHScI1Q=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.38 h1:c8ed/T9T2K5I+h/JzmF5tpI46+OODQ74dzmdo+QnaMg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.38/go.mod h1:qggunOChCMu9ZF/UkAfhTz25+U2rLVb3ya0Ua6TTfCA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.32 h1:hNeAAymUY5gu11WrrmFb3CVIp9Dar9hbo44yzzcQpzA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.32/go.mod h1:0ZXSqrty4FtQ7p8TEuRde/SZm9X05KT18LAUlR40Ln0=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.39 h1:fc0ukRAiP1syoSGZYu+DaE+FulSYhTiJ8WpVu5jElU4=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.39/go.mod h1:WLAW8PT7+JhjZfLSWe7WEJaJu0GNo0cKc2Zyo003RBs=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.1 h1:vUh7dBFNS3oFCtVv6CiYKh5hP9ls8+kIpKLeFruIBLk=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.1/go.mod h1:sFMeinkhj/SZKQM8BxtvNtSPjJEo0Xrz+w3g2e4FSKI=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.99.0 h1:NXi4pNJWjAaiI56P1Rl8DC9A4jMNRE00WNBsDua5WRg=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.99.0/go.mod h1:L3ZT0N/vBsw77mOAawXmRnREpEjcHd2v5Hzf7AkIH8M=
github.com/aws/aws-sdk-go-v2/service/iam v1.22.2 h1:DPFxx/6Zwes/MiadlDteVqDKov7yQ5v9vuwfhZuJm1s=
github.com/aws/aws-sdk-go-v2/service/iam v1.22.2/go.mod h1:cQTMNdo/Z5t1DDRsUnx0a2j6cPnytMBidUYZw2zks28=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.13 h1:iV/W5OMBys+66OeXJi/7xIRrKZNsu0ylsLGu+6nbmQE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.13/go.mod h1:ReJb6xYmtGyu9KoFtRreWegbN9dZqvZIIv4vWnhcsyI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.33 h1:QviNkc+vGSuEHx8P+pVNKOdWLXBPIwMFv7p0fphgE4U=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.33/go.mod h1:fABTUmOrAgAalG2i9WJpjBvlnk7UK8YmnYaxN+Q2CwE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.32 h1:dGAseBFEYxth10V23b5e2mAS+tX7oVbfYHD6dnDdAsg=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.32/go.mod h1:4jwAWKEkCR0anWk5+1RbfSg1R5Gzld7NLiuaq5bTR/Y=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.1 h1:PT6PBCycRwhpEW5hJnRiceCeoWJ+r3bdgXtV+VKG7Pk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.1/go.mod h1:TqoxCLwT2nrxrBGA+z7t6OWM7LBkgRckK3gOjYE+7JA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.38.2 h1:v346f1h8sUBKXnEbrv43L37MTBlFHyKXQPIZHNAaghA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.38.2/go.mod h1:cwCATiyNrXK9P2FsWdZ89g9mpsYv2rhk0UA/KByl5fY=
github.com/aws/aws-sdk-go-v2/service/sso v1.13.2 h1:A2RlEMo4SJSwbNoUUgkxTAEMduAy/8wG3eB2b2lP4gY=
github.com/aws/aws-sdk-go-v2/service/sso v1.13.2/go.mod h1:ju+nNXUunfIFamXUIZQiICjnO/TPlOmWcYhZcSy7xaE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.2 h1:OJELEgyaT2kmaBGZ+myyZbTTLobfe3ox3FSh5eYK9Qs=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.2/go.mod h1:ubDBBaDFs1GHijSOTi8ljppML15GLG0HxhILtbjNNYQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.21.2 h1:ympg1+Lnq33XLhcK/xTG4yZHPs1Oyxu+6DEWbl7qOzA=
github.com/aws/aws-sdk-go-v2/service/sts v1.21.2/go.mod h1:FQ/DQcOfESELfJi5ED+IPPAjI5xC6nxtSolVVB773jM=
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/aws/smithy-go v1.14.1 h1:EFKMUmH/iHMqLiwoEDx2rRjRQpI1YCn5jTysoaDujFs=
github.com/aws/smithy-go v1.14.1/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -80,7 +86,6 @@ github.com/hashicorp/eventlogger/filters/encrypt v0.1.8-0.20230428153751-cca4458
github.com/hashicorp/eventlogger/filters/encrypt v0.1.8-0.20230428153751-cca445805f24/go.mod h1:EQPLoX6CONA9BSYUovTQBHfPGE91g7wOxv03sO29FzY=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.4 h1:8z+Zi0uLYXONkxwWb/D7O5t1IUucB6Ft3uvSOa950bM=
@@ -89,8 +94,8 @@ github.com/hashicorp/go-kms-wrapping/v2 v2.0.9/go.mod h1:NtMaPhqSlfQ72XWDD2g80o8
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.4.9 h1:ESiK220/qE0aGxWdzKIvRH69iLiuN/PjoLTm69RoWtU=
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6 h1:W9WN8p6moV1fjKLkeqEgkAMu5rauy9QeYDAmIaPuuiA=
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6/go.mod h1:MpCPSPGLDILGb4JMm94/mMi3YysIqsXzGCzkEZjcjXg=
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.0.0 h1:ca5TSI4AgaOncPpyzLDtCGjVEtKukONpeM95vFxXCOQ=
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.0.0/go.mod h1:7CUvZtfTp2U0CYQCLzMtS2ngckjAZePSfwrE2aeDP1M=
github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw=
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 h1:ET4pqyjiGmY09R5y+rSd70J2w45CtbWDNvGqWp/R3Ng=
github.com/hashicorp/go-secure-stdlib/configutil/v2 v2.0.9 h1:YZ01/aiARPneDUryzjiPGvr3pmjCSC+1WBZKI1P7I7k=
@@ -114,7 +119,6 @@ github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f h1:E87tDTVS5W65euzixn7clSzK66puSt1H4I5SC0EmHH4=
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
@@ -122,20 +126,15 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -153,12 +152,10 @@ github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8oh
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.6.2 h1:aIihoIOHCiLZHxyoNQ+ABL4NKhFTgKLBdMLyEAh98m0=
github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
@@ -167,18 +164,12 @@ github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdk
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
@@ -205,7 +196,6 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
@@ -215,12 +205,9 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+101 -19
View File
@@ -1,12 +1,15 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package credential
import (
"fmt"
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/hashicorp/boundary-plugin-host-aws/internal/errors"
"github.com/hashicorp/boundary-plugin-host-aws/internal/values"
"github.com/hashicorp/go-secure-stdlib/awsutil"
"github.com/hashicorp/boundary-plugin-aws/internal/errors"
"github.com/hashicorp/boundary-plugin-aws/internal/values"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
"google.golang.org/protobuf/types/known/structpb"
)
@@ -17,33 +20,47 @@ type CredentialAttributes struct {
// DisableCredentialRotation disables the rotation of aws secrets associated with the plugin
DisableCredentialRotation bool
// RoleArn is the role arn associated with the aws credentials
RoleArn string
// RoleExternalId is the external id associated with the aws credentials
RoleExternalId string
// RoleSessionName is the session name associated with the aws credentials
RoleSessionName string
// RoleTags is the tags associated with the aws credentials
RoleTags map[string]string
}
// GetCredentialsConfig parses values out of a protobuf struct input and returns a
// GetCredentialsConfig parses values out of a protobuf struct secrets and returns a
// CredentialsConfig used for configuring an AWS session. An error is returned if
// any of the following fields are missing from the protobuf struct input or have
// invalid value types: access_key_id, secret_access_key. An error is returned if
// any unrecognized fields are found in the protobuf struct input.
func GetCredentialsConfig(in *structpb.Struct, region string) (*awsutil.CredentialsConfig, error) {
unknownFields := values.StructFields(in)
func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes, required bool) (*awsutil.CredentialsConfig, error) {
// initialize secrets if it is nil
// secrets can be nil because static credentials are optional
if secrets == nil {
secrets = &structpb.Struct{
Fields: make(map[string]*structpb.Value),
}
}
unknownFields := values.StructFields(secrets)
badFields := make(map[string]string)
accessKey, err := values.GetStringValue(in, ConstAccessKeyId, true)
accessKey, err := values.GetStringValue(secrets, ConstAccessKeyId, required)
if err != nil {
badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = err.Error()
}
delete(unknownFields, ConstAccessKeyId)
secretKey, err := values.GetStringValue(in, ConstSecretAccessKey, true)
secretKey, err := values.GetStringValue(secrets, ConstSecretAccessKey, required)
if err != nil {
badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = err.Error()
}
delete(unknownFields, ConstSecretAccessKey)
if region == "" {
badFields[fmt.Sprintf("attributes.%s", ConstRegion)] = err.Error()
}
// the creds_last_rotated_time field will be found in the input struct for
// persisted secrets, this value is not needed for creating the CredentialsConfig
delete(unknownFields, ConstCredsLastRotatedTime)
@@ -51,15 +68,56 @@ func GetCredentialsConfig(in *structpb.Struct, region string) (*awsutil.Credenti
badFields[fmt.Sprintf("secrets.%s", s)] = "unrecognized field"
}
opts := []awsutil.Option{}
// logic for parsing the credential type
// supported types:
// - user static credential
// - ec2 assume role dynamic credential
// - enviornment variables credential
switch {
// static credentials and dynamic credentials cannot be used together
case accessKey != "" && secretKey != "" && attrs.RoleArn != "":
badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = "conflicts with role_arn value"
badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = "conflicts with role_arn value"
badFields[fmt.Sprintf("attributes.%s", ConstRoleArn)] = "conflicts with access_key_id and secret_access_key values"
// static credential is missing it's secret_access_key
case accessKey != "" && secretKey == "":
badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = "missing required value"
// static credential is missing it's access_key_id
case accessKey == "" && secretKey != "":
badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = "missing required value"
// dynamic credentials and credential rotation is not supported
case len(attrs.RoleArn) > 0 && !attrs.DisableCredentialRotation:
badFields[fmt.Sprintf("attributes.%s", ConstDisableCredentialRotation)] = "disable_credential_rotation attribute is required when providing a role_arn"
// add static credentials
case accessKey != "" && secretKey != "":
opts = append(opts,
awsutil.WithAccessKey(accessKey),
awsutil.WithSecretKey(secretKey),
)
// add dynamic credentials
case attrs.RoleArn != "":
opts = append(opts, awsutil.WithRoleArn(attrs.RoleArn))
}
if len(badFields) > 0 {
return nil, errors.InvalidArgumentError("Error in the secrets provided", badFields)
}
return awsutil.NewCredentialsConfig(
awsutil.WithAccessKey(accessKey),
awsutil.WithSecretKey(secretKey),
awsutil.WithRegion(region),
)
if attrs.Region != "" {
opts = append(opts, awsutil.WithRegion(attrs.Region))
}
if attrs.RoleExternalId != "" {
opts = append(opts, awsutil.WithRoleExternalId(attrs.RoleExternalId))
}
if attrs.RoleSessionName != "" {
opts = append(opts, awsutil.WithRoleSessionName(attrs.RoleSessionName))
}
if len(attrs.RoleTags) != 0 {
opts = append(opts, awsutil.WithRoleTags(attrs.RoleTags))
}
return awsutil.NewCredentialsConfig(opts...)
}
// GetCredentialAttributes parses values out of a protobuf struct input and returns a
@@ -85,6 +143,26 @@ func GetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error)
badFields[fmt.Sprintf("attributes.%s", ConstDisableCredentialRotation)] = err.Error()
}
roleArn, err := values.GetStringValue(in, ConstRoleArn, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstRoleArn)] = err.Error()
}
roleExternalId, err := values.GetStringValue(in, ConstRoleExternalId, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstRoleExternalId)] = err.Error()
}
roleSessionName, err := values.GetStringValue(in, ConstRoleSessionName, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstRoleSessionName)] = err.Error()
}
roleTags, err := values.GetMapStringString(in, ConstRoleTags, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstRoleTags)] = err.Error()
}
if len(badFields) > 0 {
return nil, errors.InvalidArgumentError("Error in the attributes provided", badFields)
}
@@ -92,5 +170,9 @@ func GetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error)
return &CredentialAttributes{
Region: region,
DisableCredentialRotation: disableCredentialRotation,
RoleArn: roleArn,
RoleExternalId: roleExternalId,
RoleSessionName: roleSessionName,
RoleTags: roleTags,
}, nil
}
+151 -36
View File
@@ -6,7 +6,7 @@ package credential
import (
"testing"
"github.com/hashicorp/go-secure-stdlib/awsutil"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -61,6 +61,28 @@ func TestGetCredentialAttributes(t *testing.T) {
DisableCredentialRotation: true,
},
},
{
name: "with assume role",
in: map[string]any{
ConstRegion: "us-west-2",
ConstRoleArn: "arn:aws:iam::123456789012:role/S3Access",
ConstRoleExternalId: "1234567890",
ConstRoleSessionName: "test-session",
ConstRoleTags: map[string]interface{}{
"foo": "bar",
},
},
expected: &CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
},
}
for _, tc := range cases {
@@ -89,63 +111,155 @@ func TestGetCredentialAttributes(t *testing.T) {
func TestGetCredentialsConfig(t *testing.T) {
cases := []struct {
name string
in map[string]any
region string
secrets *structpb.Struct
required bool
attrs *CredentialAttributes
expected *awsutil.CredentialsConfig
expectedErrContains string
}{
{
name: "missing access_key_id",
in: map[string]any{},
region: "us-west-2",
expectedErrContains: "missing required value \"access_key_id\"",
name: "no credentials",
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &awsutil.CredentialsConfig{
Region: "us-west-2",
},
},
{
name: "missing secret_access_key",
in: map[string]any{
ConstAccessKeyId: "foobar",
name: "with static credentials",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
region: "us-west-2",
expectedErrContains: "missing required value \"secret_access_key\"",
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &awsutil.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
},
{
name: "missing access key",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
required: true,
expectedErrContains: "secrets.access_key_id: missing required value",
},
{
name: "missing secret key",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
},
},
required: true,
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expectedErrContains: "secrets.secret_access_key: missing required value",
},
{
name: "unknown fields",
in: map[string]any{
ConstAccessKeyId: "foobar",
ConstSecretAccessKey: "bazqux",
"foo": true,
"bar": true,
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
"foo": structpb.NewBoolValue(true),
"bar": structpb.NewBoolValue(true),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
region: "us-west-2",
expectedErrContains: "secrets.bar: unrecognized field, secrets.foo: unrecognized field",
},
{
name: "valid ignore creds_last_rotated_time",
in: map[string]any{
ConstAccessKeyId: "foobar",
ConstSecretAccessKey: "bazqux",
ConstCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
region: "us-west-2",
expected: &awsutil.CredentialsConfig{
AccessKey: "foobar",
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
},
{
name: "good",
in: map[string]any{
ConstAccessKeyId: "foobar",
ConstSecretAccessKey: "bazqux",
name: "with assume role",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
DisableCredentialRotation: true,
},
region: "us-west-2",
expected: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
Region: "us-west-2",
RoleARN: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
},
{
name: "with static credential & assume role",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
expectedErrContains: "attributes.role_arn: conflicts with access_key_id and secret_access_key values, secrets.access_key_id: conflicts with role_arn value, secrets.secret_access_key: conflicts with role_arn value",
},
{
name: "with dynamic credentials and no disable credential rotation",
secrets: &structpb.Struct{Fields: map[string]*structpb.Value{}},
attrs: &CredentialAttributes{
Region: "us-west-2",
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
expectedErrContains: "attributes.disable_credential_rotation: disable_credential_rotation attribute is required when providing a role_arn",
},
}
for _, tc := range cases {
@@ -153,10 +267,7 @@ func TestGetCredentialsConfig(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
input, err := structpb.NewStruct(tc.in)
require.NoError(err)
actual, err := GetCredentialsConfig(input, tc.region)
actual, err := GetCredentialsConfig(tc.secrets, tc.attrs, tc.required)
if tc.expectedErrContains != "" {
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
@@ -168,6 +279,10 @@ func TestGetCredentialsConfig(t *testing.T) {
require.Equal(tc.expected.AccessKey, actual.AccessKey)
require.Equal(tc.expected.SecretKey, actual.SecretKey)
require.Equal(tc.expected.Region, actual.Region)
require.Equal(tc.expected.RoleARN, actual.RoleARN)
require.Equal(tc.expected.RoleExternalId, actual.RoleExternalId)
require.Equal(tc.expected.RoleSessionName, actual.RoleSessionName)
require.Equal(tc.expected.RoleTags, actual.RoleTags)
})
}
}
+15
View File
@@ -1,3 +1,6 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package credential
const (
@@ -15,4 +18,16 @@ const (
// ConstCredsLastRotatedTime is the key for the last rotated time in the aws credentials.
ConstCredsLastRotatedTime = "creds_last_rotated_time"
// ConstRoleArn is the key for assuming a IAM role.
ConstRoleArn = "role_arn"
// ConstRoleExternalId is the key for the external id used for assuming a IAM role.
ConstRoleExternalId = "role_external_id"
// ConstRoleSessionName is the key for the session name used for assuming a IAM role.
ConstRoleSessionName = "role_session_name"
// ConstRoleTags is the key for the tags used for assuming a IAM role.
ConstRoleTags = "role_tags"
)
+157 -136
View File
@@ -1,18 +1,44 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package credential
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/boundary-plugin-host-aws/internal/values"
"github.com/hashicorp/go-secure-stdlib/awsutil"
"github.com/aws/aws-sdk-go-v2/aws"
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/hashicorp/boundary-plugin-aws/internal/values"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
"google.golang.org/protobuf/types/known/structpb"
)
type CredentialType int
const (
// StaticAWS denotes an Access Key Id that begins with "AKIA". These are
// long-term access keys, provided by AWS, for an IAM user or an AWS account
// root user.
// https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html#sec-access-keys-and-secret-access-keys
StaticAWS CredentialType = iota
// DynamicAWS denotes the presence of a RoleARN, or an Access Key Id that
// begins with "ASIA". The latter are temporary credentials access keys that
// are created using AWS STS operations.
DynamicAWS
// StaticOther denotes the presence of an Access Key Id that does not follow
// the AKIA/ASIA convention outlined by AWS.
StaticOther
// Unknown is a catch-all for everything else.
Unknown
)
// rotationWaitTimeout controls the time we wait for credential rotation to
// succeed. This is important to ensure that rotated credentials can be used
// right away.
@@ -22,19 +48,13 @@ type AwsCredentialPersistedStateOption func(s *AwsCredentialPersistedState) erro
// AwsCredentialPersistedState is the persisted state for the AWS credential.
type AwsCredentialPersistedState struct {
// AccessKeyId is the access key id for the AWS credential.
AccessKeyId string
// SecretAccessKey is the secret access key for the AWS credential.
SecretAccessKey string
// CredentialsConfig is the credential configuration for the AWS credential.
CredentialsConfig *awsutil.CredentialsConfig
// CredsLastRotatedTime is the last rotation of aws secrets for the AWS credential.
CredsLastRotatedTime time.Time
// testOpts are options that should be used for testing only
testOpts []awsutil.Option
// Region is not a part of persisted state but if set is passed to clients
// created with this state
region string
}
// WithStateTestOpts enables unit testing different edge cases
@@ -47,31 +67,19 @@ func WithStateTestOpts(opts []awsutil.Option) AwsCredentialPersistedStateOption
}
}
// WithAccessKeyId sets the value for AccesskeyId in the storage persisted state.
func WithAccessKeyId(x string) AwsCredentialPersistedStateOption {
// WithCredentialsConfig sets the value for CredentialsConfig in the credential persisted state.
func WithCredentialsConfig(x *awsutil.CredentialsConfig) AwsCredentialPersistedStateOption {
return func(s *AwsCredentialPersistedState) error {
if s.AccessKeyId != "" {
return errors.New("access key id already set")
if s.CredentialsConfig != nil {
return errors.New("credentials config already set")
}
s.AccessKeyId = x
s.CredentialsConfig = x
return nil
}
}
// WithSecretAccessKey sets the value for SecretAccessKey in the storage persisted state.
func WithSecretAccessKey(x string) AwsCredentialPersistedStateOption {
return func(s *AwsCredentialPersistedState) error {
if s.SecretAccessKey != "" {
return errors.New("secret access key already set")
}
s.SecretAccessKey = x
return nil
}
}
// WithCredsLastRotatedTime sets the value for CredsLastRotatedTime in the storage persisted state.
// WithCredsLastRotatedTime sets the value for CredsLastRotatedTime in the credential persisted state.
func WithCredsLastRotatedTime(t time.Time) AwsCredentialPersistedStateOption {
return func(s *AwsCredentialPersistedState) error {
if !s.CredsLastRotatedTime.IsZero() {
@@ -83,18 +91,6 @@ func WithCredsLastRotatedTime(t time.Time) AwsCredentialPersistedStateOption {
}
}
// WithRegion sets the value for region in the storage persisted state.
func WithRegion(with string) AwsCredentialPersistedStateOption {
return func(s *AwsCredentialPersistedState) error {
if s.region != "" {
return errors.New("region already set")
}
s.region = with
return nil
}
}
// NewAwsCredentialPersistedState returns a AwsCredentialPersistedState.
// Supported options include: WithAccessKeyId, WithSecretAccessKey
// WithCredsLastRotatedTime, & WithRegion.
@@ -105,27 +101,19 @@ func NewAwsCredentialPersistedState(opts ...AwsCredentialPersistedStateOption) (
return nil, err
}
}
return s, nil
}
// ValidateCreds takes the access key and secret key from the persisted state
// ValidateCreds takes the credentials configuration from the persisted state
// and runs sts.GetCallerIdentity for the current credentials, which is done
// to check that the credentials are valid.
func (s *AwsCredentialPersistedState) ValidateCreds() error {
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
awsutil.WithAccessKey(s.AccessKeyId),
awsutil.WithSecretKey(s.SecretAccessKey),
awsutil.WithRegion(s.region),
}, s.testOpts...)...)
if err != nil {
return fmt.Errorf("error loading credentials: %w", err)
func (s *AwsCredentialPersistedState) ValidateCreds(ctx context.Context) error {
if s.CredentialsConfig == nil {
return errors.New("missing credentials config")
}
if _, err := c.GetCallerIdentity(s.testOpts...); err != nil {
if _, err := s.CredentialsConfig.GetCallerIdentity(ctx, s.testOpts...); err != nil {
return fmt.Errorf("error validating credentials: %w", err)
}
return nil
}
@@ -134,57 +122,44 @@ func (s *AwsCredentialPersistedState) ValidateCreds() error {
// written into the credentials config and the persisted state. On any error, the old credentials are not overwritten.
// This ensures that any generated new secret key never leaves this function in case of an error, even though it will
// still result in an extraneous access key existing.
func (s *AwsCredentialPersistedState) RotateCreds() error {
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
awsutil.WithAccessKey(s.AccessKeyId),
awsutil.WithSecretKey(s.SecretAccessKey),
awsutil.WithRegion(s.region),
}, s.testOpts...)...)
if err != nil {
return fmt.Errorf("error loading credentials: %w", err)
func (s *AwsCredentialPersistedState) RotateCreds(ctx context.Context) error {
if s.CredentialsConfig == nil {
return errors.New("missing credentials config")
}
if err := c.RotateKeys(append([]awsutil.Option{
if GetCredentialType(s.CredentialsConfig) != StaticAWS {
return errors.New("invalid credential type")
}
if err := s.CredentialsConfig.RotateKeys(ctx, append([]awsutil.Option{
awsutil.WithValidityCheckTimeout(rotationWaitTimeout),
}, s.testOpts...)...); err != nil {
return fmt.Errorf("error rotating credentials: %w", err)
}
s.AccessKeyId = c.AccessKey
s.SecretAccessKey = c.SecretKey
s.CredsLastRotatedTime = time.Now()
return nil
}
// ReplaceCreds replaces the access key in the state with a new key.
// If the existing key was rotated at any point in time, it is
// deleted first, otherwise it's left alone.
func (s *AwsCredentialPersistedState) ReplaceCreds(accessKeyId, secretAccessKey string) error {
if accessKeyId == "" {
return errors.New("access key id cannot be empty")
func (s *AwsCredentialPersistedState) ReplaceCreds(ctx context.Context, credentialsConfig *awsutil.CredentialsConfig) error {
if credentialsConfig == nil {
return errors.New("missing new credentials config")
}
if s.CredentialsConfig == nil {
return errors.New("missing credentials config")
}
if secretAccessKey == "" {
return errors.New("secret access key cannot be empty")
}
if accessKeyId == s.AccessKeyId {
return errors.New("attempting to replace access key with the same one")
}
if !s.CredsLastRotatedTime.IsZero() {
// Delete old/existing credentials. This is done with the same
// credentials to ensure that it has the proper permissions to do
// it.
if err := s.DeleteCreds(); err != nil {
// Delete old/existing credentials. This action is only possible for static credential types.
// This is done with the same credentials to ensure that it has the proper permissions to do it.
ct := GetCredentialType(s.CredentialsConfig)
if !s.CredsLastRotatedTime.IsZero() && ct == StaticAWS {
if err := s.DeleteCreds(ctx); err != nil {
return err
}
}
// Set the new attributes and clear the rotated time.
s.AccessKeyId = accessKeyId
s.SecretAccessKey = secretAccessKey
s.CredentialsConfig = credentialsConfig
s.CredsLastRotatedTime = time.Time{}
return nil
}
@@ -192,86 +167,87 @@ func (s *AwsCredentialPersistedState) ReplaceCreds(accessKeyId, secretAccessKey
// DeleteCreds deletes the credentials in the state. The access key
// ID, secret access key, and rotation time fields are zeroed out in
// the state just to ensure that they cannot be re-used after.
func (s *AwsCredentialPersistedState) DeleteCreds() error {
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
awsutil.WithAccessKey(s.AccessKeyId),
awsutil.WithSecretKey(s.SecretAccessKey),
awsutil.WithRegion(s.region),
}, s.testOpts...)...)
if err != nil {
return fmt.Errorf("error loading credentials: %w", err)
func (s *AwsCredentialPersistedState) DeleteCreds(ctx context.Context) error {
if s.CredentialsConfig == nil {
return errors.New("missing credentials config")
}
if GetCredentialType(s.CredentialsConfig) != StaticAWS {
return errors.New("invalid credential type")
}
if err := c.DeleteAccessKey(s.AccessKeyId, s.testOpts...); err != nil {
if err := s.CredentialsConfig.DeleteAccessKey(ctx, s.CredentialsConfig.AccessKey, s.testOpts...); err != nil {
// Determine if the deletion error was due to a missing
// resource. If it was, just pass it.
var awsErr awserr.Error
if errors.As(err, &awsErr) {
if awsErr.Code() == iam.ErrCodeNoSuchEntityException {
s.AccessKeyId = ""
s.SecretAccessKey = ""
s.CredsLastRotatedTime = time.Time{}
return nil
}
var awsErr *iamTypes.NoSuchEntityException
if errors.As(err, &awsErr) && awsErr.ErrorCode() == "NoSuchEntity" {
s.CredentialsConfig = nil
s.CredsLastRotatedTime = time.Time{}
return nil
}
// Otherwise treat it as an actual error.
return err
}
s.AccessKeyId = ""
s.SecretAccessKey = ""
s.CredentialsConfig = nil
s.CredsLastRotatedTime = time.Time{}
return nil
}
// GetSession returns a configured AWS session for the credentials in
// the state.
func (s *AwsCredentialPersistedState) GetSession() (*session.Session, error) {
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
awsutil.WithAccessKey(s.AccessKeyId),
awsutil.WithSecretKey(s.SecretAccessKey),
awsutil.WithRegion(s.region),
}, s.testOpts...)...)
if err != nil {
return nil, err
}
return c.GetSession()
// GenerateCredentialChain returns a AWS configuration for the credentials in the state.
func (s *AwsCredentialPersistedState) GenerateCredentialChain(ctx context.Context) (*aws.Config, error) {
return s.CredentialsConfig.GenerateCredentialChain(ctx, s.testOpts...)
}
// ToMap returns a map of the credentials stored in the persisted state,
// which includes the following keys: access_key_id, secret_access_key,
// & creds_last_rotated_time
// ToMap returns a map of the credentials stored in the persisted state.
// ToMap will return an empty map for temporary credentials. ToMap will
// return a map for long-term credentials with following keys:
// access_key_id, secret_access_key & creds_last_rotated_time
func (s *AwsCredentialPersistedState) ToMap() map[string]any {
ct := GetCredentialType(s.CredentialsConfig)
// Dynamic AWS credentials are temporary credentials that expire within an hour.
// ToMap() returns an empty map here so that Boundary does not store temporary
// credentials into the database.
//
// Uknown credentials are not aws compatiable secrets that may have been provided
// for a non-aws s3 compatiable service. These credentials will not be returned
// because the plugin does not know how to manage non-aws credentials.
if ct == DynamicAWS || ct == Unknown {
return map[string]any{}
}
return map[string]any{
ConstAccessKeyId: s.AccessKeyId,
ConstSecretAccessKey: s.SecretAccessKey,
ConstAccessKeyId: s.CredentialsConfig.AccessKey,
ConstSecretAccessKey: s.CredentialsConfig.SecretKey,
ConstCredsLastRotatedTime: s.CredsLastRotatedTime.Format(time.RFC3339Nano),
}
}
// AwsCredentialPersistedStateFromProto parses values out of a protobuf struct input
// and returns a AwsCredentialPersistedState used for configuring an AWS session.
// An error is returned if any of the following fields are missing from the protobuf
// struct input or have invalid value types: access_key_id, secret_access_key, &
// creds_last_rotated_time.
func AwsCredentialPersistedStateFromProto(in *structpb.Struct, opts ...AwsCredentialPersistedStateOption) (*AwsCredentialPersistedState, error) {
if in == nil {
return nil, errors.New("missing persisted secrets")
func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *CredentialAttributes, opts ...AwsCredentialPersistedStateOption) (*AwsCredentialPersistedState, error) {
// initialize secrets if it is nil
// secrets can be nil because static credentials are optional
if secrets == nil {
secrets = &structpb.Struct{
Fields: map[string]*structpb.Value{},
}
}
accessKeyId, err := values.GetStringValue(in, ConstAccessKeyId, true)
if attrs == nil {
return nil, fmt.Errorf("missing credential attributes")
}
accessKeyId, err := values.GetStringValue(secrets, ConstAccessKeyId, false)
if err != nil {
return nil, fmt.Errorf("persisted state integrity error: %w", err)
}
secretAccessKey, err := values.GetStringValue(in, ConstSecretAccessKey, true)
secretAccessKey, err := values.GetStringValue(secrets, ConstSecretAccessKey, false)
if err != nil {
return nil, fmt.Errorf("persisted state integrity error: %w", err)
}
credsLastRotatedTime, err := values.GetTimeValue(in, ConstCredsLastRotatedTime)
credsLastRotatedTime, err := values.GetTimeValue(secrets, ConstCredsLastRotatedTime)
if err != nil {
return nil, fmt.Errorf("persisted state integrity error: %w", err)
}
@@ -281,9 +257,54 @@ func AwsCredentialPersistedStateFromProto(in *structpb.Struct, opts ...AwsCreden
return nil, err
}
s.AccessKeyId = accessKeyId
s.SecretAccessKey = secretAccessKey
awsOpts := append([]awsutil.Option{}, s.testOpts...)
if accessKeyId != "" && secretAccessKey != "" {
awsOpts = append(awsOpts,
awsutil.WithAccessKey(accessKeyId),
awsutil.WithSecretKey(secretAccessKey),
)
}
if attrs.Region != "" {
awsOpts = append(awsOpts, awsutil.WithRegion(attrs.Region))
}
if attrs.RoleArn != "" {
awsOpts = append(awsOpts, awsutil.WithRoleArn(attrs.RoleArn))
}
if attrs.RoleExternalId != "" {
awsOpts = append(awsOpts, awsutil.WithRoleExternalId(attrs.RoleExternalId))
}
if attrs.RoleSessionName != "" {
awsOpts = append(awsOpts, awsutil.WithRoleSessionName(attrs.RoleSessionName))
}
if len(attrs.RoleTags) != 0 {
awsOpts = append(awsOpts, awsutil.WithRoleTags(attrs.RoleTags))
}
credentialsConfig, err := awsutil.NewCredentialsConfig(awsOpts...)
if err != nil {
return nil, err
}
s.CredentialsConfig = credentialsConfig
s.CredsLastRotatedTime = credsLastRotatedTime
return s, nil
}
// GetCredentialType returns the credential type based on the given
// AccessKey/RoleARN. See CredentialType definition for more information.
func GetCredentialType(cc *awsutil.CredentialsConfig) CredentialType {
if cc == nil {
return Unknown
}
if len(cc.RoleARN) > 0 || strings.HasPrefix(cc.AccessKey, "ASIA") {
return DynamicAWS
}
if strings.HasPrefix(cc.AccessKey, "AKIA") {
return StaticAWS
}
if len(cc.AccessKey) > 0 {
return StaticOther
}
return Unknown
}
+467 -199
View File
@@ -1,15 +1,19 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package credential
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/go-secure-stdlib/awsutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/iam"
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
)
@@ -33,21 +37,22 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
expectedErr: testOptionErr,
},
{
name: "access key id",
name: "with credentials config",
opts: []AwsCredentialPersistedStateOption{
WithAccessKeyId("foobar"),
WithCredentialsConfig(
&awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
),
},
expected: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
},
},
{
name: "secret access key",
opts: []AwsCredentialPersistedStateOption{
WithSecretAccessKey("bazqux"),
},
expected: &AwsCredentialPersistedState{
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
},
},
{
@@ -60,29 +65,12 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
},
},
{
name: "region key",
name: "double set credentials config",
opts: []AwsCredentialPersistedStateOption{
WithRegion("foobar"),
WithCredentialsConfig(&awsutil.CredentialsConfig{}),
WithCredentialsConfig(&awsutil.CredentialsConfig{}),
},
expected: &AwsCredentialPersistedState{
region: "foobar",
},
},
{
name: "double set access key id",
opts: []AwsCredentialPersistedStateOption{
WithAccessKeyId("foobar"),
WithAccessKeyId("onetwo"),
},
expectedErr: "access key id already set",
},
{
name: "double set secret access key",
opts: []AwsCredentialPersistedStateOption{
WithSecretAccessKey("bazqux"),
WithSecretAccessKey("threefour"),
},
expectedErr: "secret access key already set",
expectedErr: "credentials config already set",
},
{
name: "double set rotation time",
@@ -92,14 +80,6 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
},
expectedErr: "last rotation time already set",
},
{
name: "double set region",
opts: []AwsCredentialPersistedStateOption{
WithRegion("foobar"),
WithRegion("barfoo"),
},
expectedErr: "region already set",
},
}
for _, tc := range cases {
@@ -113,7 +93,13 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
}
require.NoError(err)
require.Equal(tc.expected, actual)
require.Equal(tc.expected.CredsLastRotatedTime, actual.CredsLastRotatedTime)
if tc.expected.CredentialsConfig != nil {
require.NotNil(actual.CredentialsConfig)
require.Equal(tc.expected.CredentialsConfig.AccessKey, actual.CredentialsConfig.AccessKey)
require.Equal(tc.expected.CredentialsConfig.SecretKey, actual.CredentialsConfig.SecretKey)
require.Equal(tc.expected.CredentialsConfig.Region, actual.CredentialsConfig.Region)
}
})
}
}
@@ -121,43 +107,95 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
cases := []struct {
name string
in map[string]any
secrets *structpb.Struct
attrs *CredentialAttributes
opts []AwsCredentialPersistedStateOption
expected *AwsCredentialPersistedState
expectedErr string
}{
{
name: "no secrets",
in: nil,
expectedErr: "missing persisted secrets",
name: "missing credential attributes",
expectedErr: "missing credential attributes",
},
{
name: "missing access key ID",
in: map[string]any{},
expectedErr: "persisted state integrity error: missing required value \"access_key_id\"",
},
{
name: "missing secret access key",
in: map[string]any{
ConstAccessKeyId: "foobar",
name: "missing secrets",
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
Region: "us-west-2",
},
},
},
{
name: "missing long-term access key id",
secrets: &structpb.Struct{},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
Region: "us-west-2",
},
},
},
{
name: "missing long-term secret access key",
secrets: &structpb.Struct{},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
Region: "us-west-2",
},
},
},
{
name: "with long-term static credentials",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
Region: "us-west-2",
AccessKey: "foobar",
SecretKey: "bazqux",
},
},
expectedErr: "persisted state integrity error: missing required value \"secret_access_key\"",
},
{
name: "bad last rotated time",
in: map[string]any{
ConstAccessKeyId: "foobar",
ConstSecretAccessKey: "bazqux",
ConstCredsLastRotatedTime: "notatime",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
ConstCredsLastRotatedTime: structpb.NewStringValue("notatime"),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expectedErr: "persisted state integrity error: could not parse time in value \"creds_last_rotated_time\": parsing time \"notatime\" as \"2006-01-02T15:04:05.999999999Z07:00\": cannot parse \"notatime\" as \"2006\"",
},
{
name: "option error",
in: map[string]any{
ConstAccessKeyId: "foobar",
ConstSecretAccessKey: "bazqux",
ConstCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
opts: []AwsCredentialPersistedStateOption{
func(s *AwsCredentialPersistedState) error {
@@ -168,14 +206,22 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
},
{
name: "good with non-zero timestamp",
in: map[string]any{
ConstAccessKeyId: "foobar",
ConstSecretAccessKey: "bazqux",
ConstCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
CredsLastRotatedTime: func() time.Time {
t, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00")
if err != nil {
@@ -187,33 +233,79 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
},
{
name: "good with zero timestamp",
in: map[string]any{
ConstAccessKeyId: "foobar",
ConstSecretAccessKey: "bazqux",
ConstCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
ConstCredsLastRotatedTime: structpb.NewStringValue((time.Time{}).Format(time.RFC3339Nano)),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
CredsLastRotatedTime: time.Time{},
},
},
{
name: "good (ignoring non-test options)",
in: map[string]any{
ConstAccessKeyId: "foobar",
ConstSecretAccessKey: "bazqux",
ConstCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
},
opts: []AwsCredentialPersistedStateOption{
WithAccessKeyId("ignored"),
WithRegion("us-west-2"),
name: "with assume role",
attrs: &CredentialAttributes{
Region: "us-west-2",
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
expected: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
Region: "us-west-2",
RoleARN: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
},
},
{
name: "with assume role & static credentials",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
ConstCredsLastRotatedTime: structpb.NewStringValue((time.Time{}).Format(time.RFC3339Nano)),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
RoleARN: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
CredsLastRotatedTime: time.Time{},
region: "us-west-2",
},
},
}
@@ -223,55 +315,67 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
var (
input *structpb.Struct
err error
)
if tc.in != nil {
input, err = structpb.NewStruct(tc.in)
require.NoError(err)
}
actual, err := AwsCredentialPersistedStateFromProto(input, tc.opts...)
actual, err := AwsCredentialPersistedStateFromProto(tc.secrets, tc.attrs, tc.opts...)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected.AccessKeyId, actual.AccessKeyId)
require.Equal(tc.expected.SecretAccessKey, actual.SecretAccessKey)
require.Equal(tc.expected.region, actual.region)
require.NotNil(actual)
require.Equal(tc.expected.CredsLastRotatedTime, actual.CredsLastRotatedTime)
if tc.expected.CredentialsConfig != nil {
require.NotNil(actual.CredentialsConfig)
require.Equal(tc.expected.CredentialsConfig.AccessKey, actual.CredentialsConfig.AccessKey)
require.Equal(tc.expected.CredentialsConfig.SecretKey, actual.CredentialsConfig.SecretKey)
require.Equal(tc.expected.CredentialsConfig.Region, actual.CredentialsConfig.Region)
require.Equal(tc.expected.CredentialsConfig.RoleARN, actual.CredentialsConfig.RoleARN)
require.Equal(tc.expected.CredentialsConfig.RoleExternalId, actual.CredentialsConfig.RoleExternalId)
require.Equal(tc.expected.CredentialsConfig.RoleSessionName, actual.CredentialsConfig.RoleSessionName)
require.Equal(tc.expected.CredentialsConfig.RoleTags, actual.CredentialsConfig.RoleTags)
}
})
}
}
func TestAwsCatalogPersistedStateValidateCreds(t *testing.T) {
func TestAwsCatalogPersistedState_ValidateCreds(t *testing.T) {
cases := []struct {
name string
in *AwsCredentialPersistedState
expectedErr string
}{
{
name: "could not load credentials",
in: &AwsCredentialPersistedState{
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
},
expectedErr: fmt.Sprintf("error loading credentials: error reading options in NewCredentialsConfig: %s", testOptionErr),
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing credentials config",
},
{
name: "validation error",
in: &AwsCredentialPersistedState{
testOpts: []awsutil.Option{awsutil.WithSTSAPIFunc(awsutil.NewMockSTS(awsutil.WithGetCallerIdentityError(errors.New(testGetCallerIdentityErr))))},
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
testOpts: []awsutil.Option{
awsutil.WithSTSAPIFunc(
awsutil.NewMockSTS(
awsutil.WithGetCallerIdentityError(errors.New(testGetCallerIdentityErr)),
),
),
},
},
expectedErr: fmt.Sprintf("error validating credentials: %s", testGetCallerIdentityErr),
},
{
name: "good",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
testOpts: []awsutil.Option{awsutil.WithSTSAPIFunc(awsutil.NewMockSTS())},
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
testOpts: []awsutil.Option{awsutil.WithSTSAPIFunc(awsutil.NewMockSTS())},
},
},
}
@@ -280,7 +384,7 @@ func TestAwsCatalogPersistedStateValidateCreds(t *testing.T) {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
err := tc.in.ValidateCreds()
err := tc.in.ValidateCreds(context.Background())
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
@@ -291,27 +395,49 @@ func TestAwsCatalogPersistedStateValidateCreds(t *testing.T) {
}
}
func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) {
func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) {
cases := []struct {
name string
in *AwsCredentialPersistedState
expectedAccessKeyId string
expectedSecretAccessKey string
exepected *awsutil.CredentialsConfig
expectedNonZeroRotatedTime bool
expectedErr string
}{
{
name: "could not load credentials",
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing credentials config",
},
{
name: "cannot delete dynamic credential type",
in: &AwsCredentialPersistedState{
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "ASIAfoobar",
SecretKey: "bazqux",
},
CredsLastRotatedTime: time.Now(),
},
expectedErr: fmt.Sprintf("error loading credentials: error reading options in NewCredentialsConfig: %s", testOptionErr),
expectedErr: "invalid credential type",
},
{
name: "cannot delete unknown credential type",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "DNEfoobar",
SecretKey: "bazqux",
},
CredsLastRotatedTime: time.Now(),
},
expectedErr: "invalid credential type",
},
{
name: "rotation error",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
@@ -320,13 +446,16 @@ func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) {
),
},
},
expectedErr: fmt.Sprintf("error rotating credentials: error calling CreateAccessKey: error calling aws.GetUser: %s", testGetUserErr),
expectedErr: fmt.Sprintf("error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr),
},
{
name: "good",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
testOpts: []awsutil.Option{
awsutil.WithSTSAPIFunc(
awsutil.NewMockSTS(),
@@ -335,7 +464,7 @@ func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) {
awsutil.NewMockIAM(
awsutil.WithGetUserOutput(
&iam.GetUserOutput{
User: &iam.User{
User: &iamTypes.User{
Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"),
UserId: aws.String("AIDAJQABLZS4A3QDU576Q"),
UserName: aws.String("JohnDoe"),
@@ -344,7 +473,7 @@ func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) {
),
awsutil.WithCreateAccessKeyOutput(
&iam.CreateAccessKeyOutput{
AccessKey: &iam.AccessKey{
AccessKey: &iamTypes.AccessKey{
AccessKeyId: aws.String("one"),
SecretAccessKey: aws.String("two"),
UserName: aws.String("JohnDoe"),
@@ -355,8 +484,11 @@ func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) {
),
},
},
expectedAccessKeyId: "one",
expectedSecretAccessKey: "two",
exepected: &awsutil.CredentialsConfig{
AccessKey: "one",
SecretKey: "two",
Region: "us-west-2",
},
expectedNonZeroRotatedTime: true,
},
}
@@ -365,59 +497,49 @@ func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
err := tc.in.RotateCreds()
err := tc.in.RotateCreds(context.Background())
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expectedAccessKeyId, tc.in.AccessKeyId)
require.Equal(tc.expectedSecretAccessKey, tc.in.SecretAccessKey)
require.Equal(tc.exepected.AccessKey, tc.in.CredentialsConfig.AccessKey)
require.Equal(tc.exepected.SecretKey, tc.in.CredentialsConfig.SecretKey)
require.Equal(tc.expectedNonZeroRotatedTime, !tc.in.CredsLastRotatedTime.IsZero())
})
}
}
func TestAwsCatalogPersistedStateReplaceCreds(t *testing.T) {
func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) {
// NOTE: Not safe to run this test in parallel
state := new(testMockIAMState)
cases := []struct {
name string
in *AwsCredentialPersistedState
accessKeyId string
secretAccessKey string
expectedAccessKeyId string
expectedSecretAccessKey string
expectedDeleteCalled bool
expectedErr string
name string
in *AwsCredentialPersistedState
credentialConfig *awsutil.CredentialsConfig
expected *awsutil.CredentialsConfig
expectedDeleteCalled bool
expectedErr string
}{
{
name: "missing access key id",
name: "missing new credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "access key id cannot be empty",
expectedErr: "missing new credentials config",
},
{
name: "missing access key id",
in: &AwsCredentialPersistedState{},
accessKeyId: "one",
expectedErr: "secret access key cannot be empty",
},
{
name: "identical access key id",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
},
accessKeyId: "foobar",
secretAccessKey: "bazqux",
expectedErr: "attempting to replace access key with the same one",
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
credentialConfig: &awsutil.CredentialsConfig{},
expectedErr: "missing credentials config",
},
{
name: "deletion error",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
},
CredsLastRotatedTime: time.Now(),
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
@@ -427,15 +549,16 @@ func TestAwsCatalogPersistedStateReplaceCreds(t *testing.T) {
),
},
},
accessKeyId: "one",
secretAccessKey: "two",
expectedErr: fmt.Sprintf("error deleting old access key: %s", testDeleteAccessKeyErr),
credentialConfig: &awsutil.CredentialsConfig{},
expectedErr: fmt.Sprintf("error deleting old access key: %s", testDeleteAccessKeyErr),
},
{
name: "good with delete of old rotated",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
},
CredsLastRotatedTime: time.Now(),
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
@@ -443,28 +566,38 @@ func TestAwsCatalogPersistedStateReplaceCreds(t *testing.T) {
),
},
},
accessKeyId: "one",
secretAccessKey: "two",
expectedAccessKeyId: "one",
expectedSecretAccessKey: "two",
expectedDeleteCalled: true,
credentialConfig: &awsutil.CredentialsConfig{
AccessKey: "one",
SecretKey: "two",
},
expected: &awsutil.CredentialsConfig{
AccessKey: "one",
SecretKey: "two",
},
expectedDeleteCalled: true,
},
{
name: "good without delete of old rotated",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
},
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
newTestMockIAM(state),
),
},
},
accessKeyId: "one",
secretAccessKey: "two",
expectedAccessKeyId: "one",
expectedSecretAccessKey: "two",
expectedDeleteCalled: false,
credentialConfig: &awsutil.CredentialsConfig{
AccessKey: "one",
SecretKey: "two",
},
expected: &awsutil.CredentialsConfig{
AccessKey: "one",
SecretKey: "two",
},
expectedDeleteCalled: false,
},
}
@@ -473,39 +606,61 @@ func TestAwsCatalogPersistedStateReplaceCreds(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
state.Reset()
err := tc.in.ReplaceCreds(tc.accessKeyId, tc.secretAccessKey)
err := tc.in.ReplaceCreds(context.Background(), tc.credentialConfig)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expectedAccessKeyId, tc.in.AccessKeyId)
require.Equal(tc.expectedSecretAccessKey, tc.in.SecretAccessKey)
require.Equal(tc.expected.AccessKey, tc.in.CredentialsConfig.AccessKey)
require.Equal(tc.expected.SecretKey, tc.in.CredentialsConfig.SecretKey)
require.Zero(tc.in.CredsLastRotatedTime)
require.Equal(tc.expectedDeleteCalled, state.DeleteAccessKeyCalled)
})
}
}
func TestAwsCatalogPersistedStateDeleteCreds(t *testing.T) {
func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) {
cases := []struct {
name string
in *AwsCredentialPersistedState
expectedErr string
}{
{
name: "could not load credentials",
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing credentials config",
},
{
name: "cannot delete dynamic credential type",
in: &AwsCredentialPersistedState{
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "ASIAfoobar",
SecretKey: "bazqux",
},
CredsLastRotatedTime: time.Now(),
},
expectedErr: fmt.Sprintf("error loading credentials: error reading options in NewCredentialsConfig: %s", testOptionErr),
expectedErr: "invalid credential type",
},
{
name: "cannot delete unknown credential type",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "DNEfoobar",
SecretKey: "bazqux",
},
CredsLastRotatedTime: time.Now(),
},
expectedErr: "invalid credential type",
},
{
name: "deletion error",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
},
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
@@ -519,13 +674,15 @@ func TestAwsCatalogPersistedStateDeleteCreds(t *testing.T) {
{
name: "deletion error, but OK because key was just gone",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
},
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithDeleteAccessKeyError(
awserr.New(iam.ErrCodeNoSuchEntityException, "", nil),
&iamTypes.NoSuchEntityException{},
),
),
),
@@ -535,8 +692,10 @@ func TestAwsCatalogPersistedStateDeleteCreds(t *testing.T) {
{
name: "good",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
},
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(),
@@ -550,26 +709,135 @@ func TestAwsCatalogPersistedStateDeleteCreds(t *testing.T) {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
err := tc.in.DeleteCreds()
err := tc.in.DeleteCreds(context.Background())
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Empty(tc.in.AccessKeyId)
require.Empty(tc.in.SecretAccessKey)
require.Nil(tc.in.CredentialsConfig)
require.Zero(tc.in.CredsLastRotatedTime)
})
}
}
func TestAwsCatalogPersistedStateGetSessionErr(t *testing.T) {
func TestAwsCatalogPersistedState_GenerateCredentialChain(t *testing.T) {
require := require.New(t)
state := &AwsCredentialPersistedState{
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
cases := []struct {
name string
in *AwsCredentialPersistedState
expectedErr string
}{
{
name: "error",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
},
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
},
expectedErr: fmt.Sprintf("error reading options in GenerateCredentialChain: %s", testOptionErr),
},
{
name: "static credentials",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "AKIA_foobar",
SecretKey: "bazqux",
},
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(),
),
},
},
},
{
name: "assume role",
in: &AwsCredentialPersistedState{
CredentialsConfig: func() *awsutil.CredentialsConfig {
config, err := awsutil.NewCredentialsConfig(
awsutil.WithRegion("us-west-2"),
awsutil.WithRoleArn("arn:aws:iam::123456789012:role/S3Access"),
awsutil.WithRoleExternalId("1234567890"),
awsutil.WithRoleSessionName("assume-role"),
awsutil.WithRoleTags(map[string]string{
"foo": "bar",
}),
)
require.NoError(err)
return config
}(),
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(),
),
},
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
_, err := tc.in.GenerateCredentialChain(context.Background())
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
})
}
}
func TestGetCredentialType(t *testing.T) {
tests := []struct {
name string
credConfig *awsutil.CredentialsConfig
expectedCredType CredentialType
}{
{
name: "nil credential config",
credConfig: nil,
expectedCredType: Unknown,
},
{
name: "empty credential config",
credConfig: &awsutil.CredentialsConfig{},
expectedCredType: Unknown,
},
{
name: "static aws",
credConfig: &awsutil.CredentialsConfig{AccessKey: "AKIAfoobar"},
expectedCredType: StaticAWS,
},
{
name: "static other",
credConfig: &awsutil.CredentialsConfig{AccessKey: "cK3kNFa24foobar"},
expectedCredType: StaticOther,
},
{
name: "dynamic aws - role arn",
credConfig: &awsutil.CredentialsConfig{RoleARN: "arn:aws:iam::123456789012:role/S3Access"},
expectedCredType: DynamicAWS,
},
{
name: "dynamic aws - access key",
credConfig: &awsutil.CredentialsConfig{AccessKey: "ASIAfoobar"},
expectedCredType: DynamicAWS,
},
{
name: "dynamic aws - role arn and access key",
credConfig: &awsutil.CredentialsConfig{AccessKey: "ASIAfoobar", RoleARN: "arn:aws:iam::123456789012:role/S3Access"},
expectedCredType: DynamicAWS,
},
}
_, err := state.GetSession()
require.EqualError(err, fmt.Sprintf("error reading options in NewCredentialsConfig: %s", testOptionErr))
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ct := GetCredentialType(tc.credConfig)
require.Equal(t, tc.expectedCredType, ct)
})
}
}
+50 -9
View File
@@ -1,10 +1,15 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package credential
import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/iam/iamiface"
"github.com/hashicorp/go-secure-stdlib/awsutil"
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
"google.golang.org/protobuf/types/known/structpb"
)
const (
@@ -23,25 +28,61 @@ func (s *testMockIAMState) Reset() {
}
type testMockIAM struct {
iamiface.IAMAPI
awsutil.IAMClient
State *testMockIAMState
}
func (m *testMockIAM) DeleteAccessKey(input *iam.DeleteAccessKeyInput) (*iam.DeleteAccessKeyOutput, error) {
func (m *testMockIAM) DeleteAccessKey(ctx context.Context, input *iam.DeleteAccessKeyInput, opts ...func(*iam.Options)) (*iam.DeleteAccessKeyOutput, error) {
m.State.DeleteAccessKeyCalled = true
return m.IAMAPI.DeleteAccessKey(input)
return m.IAMClient.DeleteAccessKey(ctx, input, opts...)
}
func newTestMockIAM(state *testMockIAMState, opts ...awsutil.MockIAMOption) awsutil.IAMAPIFunc {
return func(sess *session.Session) (iamiface.IAMAPI, error) {
return func(awsConfig *aws.Config) (awsutil.IAMClient, error) {
m := &testMockIAM{
State: state,
}
f := awsutil.NewMockIAM(opts...)
var err error
m.IAMAPI, err = f(sess)
m.IAMClient, err = f(awsConfig)
return m, err
}
}
// MockStaticCredentialSecrets returns a *structpb.Struct that contains two
// key pair values: (access_key_id, AKIA_foobar) & (secret_access_key, bazqux)
func MockStaticCredentialSecrets() *structpb.Struct {
return &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
}
}
// MockAssumeRoleAttributes returns a *structpb.Struct that contains six key pair values:
//
// (region, region)
// (disable_credential_rotation, disableRotate)
// (role_arn, arn:aws:iam::123456789012:role/S3Access)
// (role_external_id, 1234567890)
// (role_session_name, ec2-assume-role-provider)
// (role_tags, struct{foo:bar})
func MockAssumeRoleAttributes(region string, disableRotate bool) *structpb.Struct {
return &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstRegion: structpb.NewStringValue(region),
ConstDisableCredentialRotation: structpb.NewBoolValue(disableRotate),
ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
ConstRoleExternalId: structpb.NewStringValue("1234567890"),
ConstRoleSessionName: structpb.NewStringValue("ec2-assume-role-provider"),
ConstRoleTags: structpb.NewStructValue(&structpb.Struct{
Fields: map[string]*structpb.Value{
"foo": structpb.NewStringValue("bar"),
},
}),
},
}
}
+3
View File
@@ -1,3 +1,6 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package errors
import (
+30
View File
@@ -1,3 +1,6 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package values
import (
@@ -81,6 +84,33 @@ func GetTimeValue(in *structpb.Struct, k string) (time.Time, error) {
return t, nil
}
// GetMapStringString returns a map[string]string value and no error if the given key
// is found in the provided proto struct input. An error is returned if the key
// is not found or the value type is not map[string]string.
func GetMapStringString(in *structpb.Struct, k string, required bool) (map[string]string, error) {
mv := in.GetFields()
v, ok := mv[k]
if !ok {
if required {
return nil, fmt.Errorf("missing required value %q", k)
}
return nil, nil
}
result := map[string]string{}
m, ok := v.AsInterface().(map[string]any)
if !ok {
return nil, fmt.Errorf("unexpected type for value %q: want map[string]string, got %T", k, v.AsInterface())
}
for mapKey, mapValue := range m {
e, ok := mapValue.(string)
if !ok {
return nil, fmt.Errorf("unexpected type for value in map[%q]: want string, got %T", mapKey, mapValue)
}
result[mapKey] = e
}
return result, nil
}
// StructFields returns a map[string]struct{} of the
// proto struct input.
func StructFields(s *structpb.Struct) map[string]struct{} {
+95
View File
@@ -1,3 +1,6 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package values
import (
@@ -205,3 +208,95 @@ func TestGetTimeValue(t *testing.T) {
})
}
}
func TestGetMapStringString(t *testing.T) {
cases := []struct {
name string
in *structpb.Struct
key string
required bool
expected map[string]string
expectedErr string
}{
{
name: "required missing",
in: &structpb.Struct{},
key: "foo",
required: true,
expectedErr: "missing required value \"foo\"",
},
{
name: "optional missing",
in: &structpb.Struct{},
key: "foo",
},
{
name: "non-map[string]string value",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"foo": structpb.NewBoolValue(true),
},
},
key: "foo",
expectedErr: "unexpected type for value \"foo\": want map[string]string, got bool",
},
{
name: "empty map",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"foo": structpb.NewStructValue(&structpb.Struct{
Fields: map[string]*structpb.Value{},
}),
},
},
key: "foo",
expected: map[string]string{},
},
{
name: "non-string value in map",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"foo": structpb.NewStructValue(&structpb.Struct{
Fields: map[string]*structpb.Value{
"bar": structpb.NewBoolValue(true),
},
}),
},
},
key: "foo",
expectedErr: "unexpected type for value in map[\"bar\"]: want string, got bool",
},
{
name: "good",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"foo": structpb.NewStructValue(&structpb.Struct{
Fields: map[string]*structpb.Value{
"bar": structpb.NewStringValue("zoo"),
},
}),
},
},
key: "foo",
expected: map[string]string{
"bar": "zoo",
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
actual, err := GetMapStringString(tc.in, tc.key, tc.required)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})
}
}
+5 -2
View File
@@ -1,8 +1,11 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package plugin
import (
"github.com/hashicorp/boundary-plugin-host-aws/plugin/service/host"
"github.com/hashicorp/boundary-plugin-host-aws/plugin/service/storage"
"github.com/hashicorp/boundary-plugin-aws/plugin/service/host"
"github.com/hashicorp/boundary-plugin-aws/plugin/service/storage"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
)
+4 -3
View File
@@ -105,18 +105,19 @@ a configured IAM user for this provider:
The following `attributes` are valid on an AWS host catalog resource:
- `disable_credential_rotation` (bool): If `true`, credential rotation will not
be performed. See the [Credential Rotation](#credential-rotation) section
above.
be performed. See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
- `region` (string): The region to configure the host catalog for. All host sets
in this catalog will be configured for this region.
The following `secrets` are valid on an AWS host catalog resource:
The following `secrets` are required on an AWS host catalog resource:
- `access_key_id` (string): The access key ID for the IAM user to use with this
host catalog.
- `secret_access_key` (string): The secret access key for the IAM user to use
with this host catalog.
See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
### Host Set
The following attributes are valid on an AWS host Set resource:
+14 -3
View File
@@ -1,11 +1,14 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package host
import (
"fmt"
cred "github.com/hashicorp/boundary-plugin-host-aws/internal/credential"
"github.com/hashicorp/boundary-plugin-host-aws/internal/errors"
"github.com/hashicorp/boundary-plugin-host-aws/internal/values"
cred "github.com/hashicorp/boundary-plugin-aws/internal/credential"
"github.com/hashicorp/boundary-plugin-aws/internal/errors"
"github.com/hashicorp/boundary-plugin-aws/internal/values"
"github.com/mitchellh/mapstructure"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -35,6 +38,14 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
continue
case cred.ConstDisableCredentialRotation:
continue
case cred.ConstRoleArn:
continue
case cred.ConstRoleExternalId:
continue
case cred.ConstRoleSessionName:
continue
case cred.ConstRoleTags:
continue
default:
badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field"
}
+42 -27
View File
@@ -1,3 +1,6 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package host
import (
@@ -5,7 +8,8 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/boundary-plugin-host-aws/internal/credential"
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
cred "github.com/hashicorp/boundary-plugin-aws/internal/credential"
"github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
@@ -17,36 +21,34 @@ import (
func TestGetCatalogAttributes(t *testing.T) {
cases := []struct {
name string
in map[string]any
in *structpb.Struct
expected *CatalogAttributes
expectedErrContains string
}{
{
name: "missing region",
in: map[string]any{},
name: "missing region",
in: &structpb.Struct{
Fields: make(map[string]*structpb.Value),
},
expectedErrContains: "missing required value \"region\"",
},
{
name: "bad value for disable_credential_rotation",
in: map[string]any{
credential.ConstRegion: "us-west-2",
credential.ConstDisableCredentialRotation: "sure",
},
expectedErrContains: "unexpected type for value \"disable_credential_rotation\": want bool, got string",
},
{
name: "unknown fields",
in: map[string]any{
credential.ConstRegion: "us-west-2",
"foo": true,
"bar": true,
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"foo": structpb.NewBoolValue(true),
"bar": structpb.NewBoolValue(true),
},
},
expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
},
{
name: "default",
in: map[string]any{
credential.ConstRegion: "us-west-2",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
@@ -56,15 +58,31 @@ func TestGetCatalogAttributes(t *testing.T) {
},
},
{
name: "with disable_credential_rotation",
in: map[string]any{
credential.ConstRegion: "us-west-2",
credential.ConstDisableCredentialRotation: true,
name: "credential attributes",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"disable_credential_rotation": structpb.NewBoolValue(true),
"role_arn": structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
"role_external_id": structpb.NewStringValue("1234567890"),
"role_session_name": structpb.NewStringValue("test-session"),
"role_tags": structpb.NewStructValue(&structpb.Struct{
Fields: map[string]*structpb.Value{
"foo": structpb.NewStringValue("bar"),
},
}),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
CredentialAttributes: &cred.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: true,
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
},
},
@@ -75,10 +93,7 @@ func TestGetCatalogAttributes(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
input, err := structpb.NewStruct(tc.in)
require.NoError(err)
actual, err := getCatalogAttributes(input)
actual, err := getCatalogAttributes(tc.in)
if tc.expectedErrContains != "" {
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
+3
View File
@@ -1,3 +1,6 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package host
const (
+114 -141
View File
@@ -9,17 +9,18 @@ import (
"fmt"
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/smithy-go"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
"github.com/hashicorp/boundary-plugin-host-aws/internal/credential"
cred "github.com/hashicorp/boundary-plugin-host-aws/internal/credential"
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
cred "github.com/hashicorp/boundary-plugin-aws/internal/credential"
)
// HostPlugin implements the HostPluginServiceServer interface for the
@@ -58,7 +59,7 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
if err != nil {
return nil, err
}
credConfig, err := cred.GetCredentialsConfig(secrets, catalogAttributes.Region)
credConfig, err := cred.GetCredentialsConfig(secrets, catalogAttributes.CredentialAttributes, true)
if err != nil {
return nil, err
}
@@ -66,9 +67,7 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
// Set up the creds in our persisted state.
credState, err := credential.NewAwsCredentialPersistedState(
append([]credential.AwsCredentialPersistedStateOption{
credential.WithAccessKeyId(credConfig.AccessKey),
credential.WithSecretAccessKey(credConfig.SecretKey),
credential.WithRegion(credConfig.Region),
credential.WithCredentialsConfig(credConfig),
}, p.testCredStateOpts...)...,
)
if err != nil {
@@ -84,15 +83,16 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
}
// Try to rotate the credentials if we're not skipping them.
if !catalogAttributes.DisableCredentialRotation {
if err := credState.RotateCreds(); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
}
} else {
// Simply validate if we aren't rotating.
if err := credState.ValidateCreds(); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential validation: %s", err)
if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS {
if !catalogAttributes.DisableCredentialRotation {
if err := credState.RotateCreds(ctx); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
}
} else {
// Simply validate if we aren't rotating.
if err := credState.ValidateCreds(ctx); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential validation: %s", err)
}
}
}
@@ -144,7 +144,8 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
// is fine and important, but this may change in the future.
credState, err := credential.AwsCredentialPersistedStateFromProto(
req.GetPersisted().GetSecrets(),
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
catalogAttributes.CredentialAttributes,
p.testCredStateOpts...)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
@@ -158,38 +159,39 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
if catalogAttributes.DisableCredentialRotation && !updateSecrets {
// This is a validate check to make sure that we aren't disabling
// rotation for credentials currently being managed by rotation.
// This is not allowed.
if !credState.CredsLastRotatedTime.IsZero() {
return nil, status.Error(codes.FailedPrecondition, "cannot disable rotation for already-rotated credentials")
if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS {
if catalogAttributes.DisableCredentialRotation && !updateSecrets {
// This is a validate check to make sure that we aren't disabling
// rotation for credentials currently being managed by rotation.
// This is not allowed.
if !credState.CredsLastRotatedTime.IsZero() {
return nil, status.Error(codes.FailedPrecondition, "cannot disable rotation for already-rotated credentials")
}
}
if updateSecrets {
catalogSecrets, err := credential.GetCredentialsConfig(secrets, catalogAttributes.CredentialAttributes, true)
if err != nil {
return nil, err
}
// Replace the credentials. This checks the timestamp on the last
// rotation time as well and deletes the credentials if we are
// managing them (ie: if we've rotated them before).
if err := credState.ReplaceCreds(ctx, catalogSecrets); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err)
}
}
if !catalogAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
// If we're enabling rotation now but didn't before, or have
// freshly replaced credentials, we can rotate here.
if err := credState.RotateCreds(ctx); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
}
}
}
if updateSecrets {
catalogSecrets, err := credential.GetCredentialsConfig(secrets, catalogAttributes.Region)
if err != nil {
return nil, err
}
// Replace the credentials. This checks the timestamp on the last
// rotation time as well and deletes the credentials if we are
// managing them (ie: if we've rotated them before).
if err := credState.ReplaceCreds(catalogSecrets.AccessKey, catalogSecrets.SecretKey); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err)
}
}
if !catalogAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
// If we're enabling rotation now but didn't before, or have
// freshly replaced credentials, we can rotate here.
if err := credState.RotateCreds(); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
}
}
// That's it!
persistedProto, err := catalogState.toProto()
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
@@ -225,7 +227,8 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo
// may be corrupt/and or legitimately missing.
credState, err := credential.AwsCredentialPersistedStateFromProto(
req.GetPersisted().GetSecrets(),
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
catalogAttributes.CredentialAttributes,
p.testCredStateOpts...)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
@@ -239,12 +242,15 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
if !credState.CredsLastRotatedTime.IsZero() {
// Delete old/existing credentials. This is done with the same
// credentials to ensure that it has the proper permissions to do
// it.
if err := credState.DeleteCreds(); err != nil {
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during catalog deletion: %s", err)
// try to delete static credentials
if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS {
if !credState.CredsLastRotatedTime.IsZero() {
// Delete old/existing credentials. This is done with the same
// credentials to ensure that it has the proper permissions to do
// it.
if err := credState.DeleteCreds(ctx); err != nil {
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during catalog deletion: %s", err)
}
}
}
@@ -297,7 +303,8 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
credState, err := credential.AwsCredentialPersistedStateFromProto(
req.GetPersisted().GetSecrets(),
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
catalogAttributes.CredentialAttributes,
p.testCredStateOpts...)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
@@ -324,7 +331,7 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
return nil, err
}
ec2Client, err := catalogState.EC2Client(catalogAttributes.Region)
ec2Client, err := catalogState.EC2Client(ctx)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
}
@@ -334,12 +341,13 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
return nil, status.Errorf(codes.InvalidArgument, "error building DescribeInstances parameters: %s", err)
}
_, err = ec2Client.DescribeInstances(input)
_, err = ec2Client.DescribeInstances(ctx, input)
if err == nil {
return nil, status.Error(codes.FailedPrecondition, "query error: DescribeInstances DryRun should have returned error, but none was found")
}
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "DryRunOperation" {
var apiErr smithy.APIError
if errors.As(err, &apiErr) && apiErr.ErrorCode() == "DryRunOperation" {
// Success
return &pb.OnCreateSetResponse{}, nil
}
@@ -366,7 +374,8 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
credState, err := credential.AwsCredentialPersistedStateFromProto(
req.GetPersisted().GetSecrets(),
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
catalogAttributes.CredentialAttributes,
p.testCredStateOpts...)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
@@ -395,7 +404,7 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
return nil, err
}
ec2Client, err := catalogState.EC2Client(catalogAttributes.Region)
ec2Client, err := catalogState.EC2Client(ctx)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
}
@@ -405,12 +414,13 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
return nil, status.Errorf(codes.InvalidArgument, "error building DescribeInstances parameters: %s", err)
}
_, err = ec2Client.DescribeInstances(input)
_, err = ec2Client.DescribeInstances(ctx, input)
if err == nil {
return nil, status.Error(codes.FailedPrecondition, "query error: DescribeInstances DryRun should have returned error, but none was found")
}
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "DryRunOperation" {
var apiErr smithy.APIError
if errors.As(err, &apiErr) && apiErr.ErrorCode() == "DryRunOperation" {
// Success
return &pb.OnUpdateSetResponse{}, nil
}
@@ -442,7 +452,10 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
return nil, err
}
credState, err := credential.AwsCredentialPersistedStateFromProto(req.GetPersisted().GetSecrets(), p.testCredStateOpts...)
credState, err := credential.AwsCredentialPersistedStateFromProto(
req.GetPersisted().GetSecrets(),
catalogAttributes.CredentialAttributes,
p.testCredStateOpts...)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
@@ -494,7 +507,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
}
}
ec2Client, err := catalogState.EC2Client(catalogAttributes.Region)
ec2Client, err := catalogState.EC2Client(ctx)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
}
@@ -502,7 +515,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
// Run all queries now and assemble output.
var maxLen int
for i, query := range queries {
output, err := ec2Client.DescribeInstances(query.Input)
output, err := ec2Client.DescribeInstances(ctx, query.Input)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error running DescribeInstances for host set id %q: %s", query.Id, err)
}
@@ -556,8 +569,8 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
}, nil
}
func buildFilters(attrs *SetAttributes) ([]*ec2.Filter, error) {
var filters []*ec2.Filter
func buildFilters(attrs *SetAttributes) ([]types.Filter, error) {
var filters []types.Filter
var foundStateFilter bool
for _, filterAttr := range attrs.Filters {
// Each filter arrives in "k=v1,v2" format. First, split on equal sign.
@@ -576,14 +589,9 @@ func buildFilters(attrs *SetAttributes) ([]*ec2.Filter, error) {
foundStateFilter = true
}
var filterValues []*string
for _, val := range strings.Split(filterValue, ",") {
filterValues = append(filterValues, aws.String(val))
}
filters = append(filters, &ec2.Filter{
filters = append(filters, types.Filter{
Name: aws.String(filterKey),
Values: filterValues,
Values: strings.Split(filterValue, ","),
})
}
@@ -592,9 +600,9 @@ func buildFilters(attrs *SetAttributes) ([]*ec2.Filter, error) {
// instance-state-name = ["running"] to the filter set. This
// ensures that we filter on running instances only at the API
// side, saving time when processing results.
filters = append(filters, &ec2.Filter{
filters = append(filters, types.Filter{
Name: aws.String("instance-state-name"),
Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}),
Values: []string{string(types.InstanceStateNameRunning)},
})
}
@@ -616,97 +624,49 @@ func buildDescribeInstancesInput(attrs *SetAttributes, dryRun bool) (*ec2.Descri
// awsInstanceToHost processes data from an ec2.Instance and returns
// a ListHostsResponseHost with the ID and network addresses
// populated.
func awsInstanceToHost(instance *ec2.Instance) (*pb.ListHostsResponseHost, error) {
// Integrity check: some fields should always be non-nil. Check
// those.
if instance == nil {
return nil, errors.New("response integrity error: missing instance entry")
}
if aws.StringValue(instance.InstanceId) == "" {
func awsInstanceToHost(instance types.Instance) (*pb.ListHostsResponseHost, error) {
if aws.ToString(instance.InstanceId) == "" {
return nil, errors.New("response integrity error: missing instance id")
}
result := new(pb.ListHostsResponseHost)
// External ID is the instance ID.
result.ExternalId = aws.StringValue(instance.InstanceId)
result.ExternalId = aws.ToString(instance.InstanceId)
// External Name is the AWS "Name" tag, if it exists.
for _, t := range instance.Tags {
if aws.StringValue(t.Key) == ConstInstanceNameTagKey {
result.ExternalName = aws.StringValue(t.Value)
if aws.ToString(t.Key) == ConstInstanceNameTagKey {
result.ExternalName = aws.ToString(t.Value)
break
}
}
// First IP address/dns name are always the private fields if they
// are populated
if aws.StringValue(instance.PrivateIpAddress) != "" {
result.IpAddresses = append(result.IpAddresses, aws.StringValue(instance.PrivateIpAddress))
}
if aws.StringValue(instance.PrivateDnsName) != "" {
result.DnsNames = append(result.DnsNames, aws.StringValue(instance.PrivateDnsName))
}
// Public IP address/dns names are next
if aws.StringValue(instance.PublicIpAddress) != "" {
result.IpAddresses = append(result.IpAddresses, aws.StringValue(instance.PublicIpAddress))
}
if aws.StringValue(instance.PublicDnsName) != "" {
result.DnsNames = append(result.DnsNames, aws.StringValue(instance.PublicDnsName))
}
result.IpAddresses = appendDistinct(result.IpAddresses, instance.PrivateIpAddress, instance.PublicIpAddress)
result.DnsNames = appendDistinct(result.DnsNames, instance.PrivateDnsName, instance.PublicDnsName)
// Now go through all of the interfaces and log the IP address of
// every interface.
for _, iface := range instance.NetworkInterfaces {
if iface == nil {
// Probably will never happen, but just in case
return nil, errors.New("response integrity error: interface entry is nil")
}
// Populate default IP addresses/DNS name similar to how we do
// for the entire instance.
if aws.StringValue(iface.PrivateIpAddress) != "" && !stringInSlice(result.IpAddresses, aws.StringValue(iface.PrivateIpAddress)) {
result.IpAddresses = append(result.IpAddresses, aws.StringValue(iface.PrivateIpAddress))
}
if aws.StringValue(iface.PrivateDnsName) != "" && !stringInSlice(result.DnsNames, aws.StringValue(iface.PrivateDnsName)) {
result.DnsNames = append(result.DnsNames, aws.StringValue(iface.PrivateDnsName))
}
result.IpAddresses = appendDistinct(result.IpAddresses, iface.PrivateIpAddress)
result.DnsNames = appendDistinct(result.DnsNames, iface.PrivateDnsName)
// Iterate through the private IP addresses and log the
// information.
for _, addr := range iface.PrivateIpAddresses {
if addr == nil {
return nil, errors.New("response integrity error: interface address entry is nil")
}
// Add private address/dns name if they have not been added yet
if aws.StringValue(addr.PrivateIpAddress) != "" && !stringInSlice(result.IpAddresses, aws.StringValue(addr.PrivateIpAddress)) {
result.IpAddresses = append(result.IpAddresses, aws.StringValue(addr.PrivateIpAddress))
}
if aws.StringValue(addr.PrivateDnsName) != "" && !stringInSlice(result.DnsNames, aws.StringValue(addr.PrivateDnsName)) {
result.DnsNames = append(result.DnsNames, aws.StringValue(addr.PrivateDnsName))
}
// Add public address/dns name if they have not been added yet
if addr.Association != nil && aws.StringValue(addr.Association.PublicIp) != "" && !stringInSlice(result.IpAddresses, aws.StringValue(addr.Association.PublicIp)) {
result.IpAddresses = append(result.IpAddresses, aws.StringValue(addr.Association.PublicIp))
}
if addr.Association != nil && aws.StringValue(addr.Association.PublicDnsName) != "" && !stringInSlice(result.DnsNames, aws.StringValue(addr.Association.PublicDnsName)) {
result.DnsNames = append(result.DnsNames, aws.StringValue(addr.Association.PublicDnsName))
result.IpAddresses = appendDistinct(result.IpAddresses, addr.PrivateIpAddress)
result.DnsNames = appendDistinct(result.DnsNames, addr.PrivateDnsName)
if addr.Association != nil {
result.IpAddresses = appendDistinct(result.IpAddresses, addr.Association.PublicIp)
result.DnsNames = appendDistinct(result.DnsNames, addr.Association.PublicDnsName)
}
}
// Add the IPv6 addresses.
for _, addr := range iface.Ipv6Addresses {
if addr == nil {
continue
}
if addr.Ipv6Address != nil && aws.StringValue(addr.Ipv6Address) != "" {
result.IpAddresses = append(result.IpAddresses, aws.StringValue(addr.Ipv6Address))
}
result.IpAddresses = appendDistinct(result.IpAddresses, addr.Ipv6Address)
}
}
@@ -714,12 +674,25 @@ func awsInstanceToHost(instance *ec2.Instance) (*pb.ListHostsResponseHost, error
return result, nil
}
// appendDistinct will append the elements to the slice
// if an element is not nil, empty, and does not exist in slice.
func appendDistinct(slice []string, elems ...*string) []string {
for _, e := range elems {
value := aws.ToString(e)
if value == "" || stringInSlice(slice, value) {
continue
}
slice = append(slice, value)
}
return slice
}
// stringInSlice returns true if the slice contains the given element.
func stringInSlice(s []string, x string) bool {
for _, y := range s {
if x == y {
return true
}
}
return false
}
+160 -137
View File
@@ -1,3 +1,6 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package host
import (
@@ -7,13 +10,14 @@ import (
"sort"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/boundary-plugin-host-aws/internal/credential"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/hashicorp/go-secure-stdlib/awsutil"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -35,14 +39,6 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
expectedErrContains: "catalog is nil",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil secrets",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{},
},
expectedErrContains: "secrets are required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil attributes",
req: &pb.OnCreateCatalogRequest{
@@ -89,7 +85,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
Catalog: &hostcatalogs.HostCatalog{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
}},
Attrs: &hostcatalogs.HostCatalog_Attributes{
@@ -110,7 +106,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
Catalog: &hostcatalogs.HostCatalog{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
}},
Attrs: &hostcatalogs.HostCatalog_Attributes{
@@ -136,7 +132,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
Catalog: &hostcatalogs.HostCatalog{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
}},
Attrs: &hostcatalogs.HostCatalog_Attributes{
@@ -157,7 +153,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
),
}),
},
expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling aws.GetUser: %s", testGetUserErr),
expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr),
expectedErrCode: codes.InvalidArgument,
},
{
@@ -166,7 +162,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
Catalog: &hostcatalogs.HostCatalog{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
}},
Attrs: &hostcatalogs.HostCatalog_Attributes{
@@ -277,7 +273,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -308,7 +304,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -334,7 +330,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -350,7 +346,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
NewCatalog: &hostcatalogs.HostCatalog{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("onetwo"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_onetwo"),
credential.ConstSecretAccessKey: structpb.NewStringValue("threefour"),
},
},
@@ -366,7 +362,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -400,7 +396,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
@@ -415,7 +411,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
),
}),
},
expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling aws.GetUser: %s", testGetUserErr),
expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr),
expectedErrCode: codes.InvalidArgument,
},
}
@@ -459,7 +455,7 @@ func TestPluginOnDeleteCatalogErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -489,7 +485,7 @@ func TestPluginOnDeleteCatalogErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -594,7 +590,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -624,7 +620,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -649,7 +645,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -675,7 +671,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -710,7 +706,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -737,7 +733,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
awsutil.MockOptionErr(errors.New(testOptionErr)),
}),
},
expectedErrContains: fmt.Sprintf("error getting EC2 client: error getting AWS session when fetching EC2 client: error reading options in NewCredentialsConfig: %s", testOptionErr),
expectedErrContains: fmt.Sprintf("error loading persisted state: error reading options in NewCredentialsConfig: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
},
{
@@ -755,7 +751,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -801,7 +797,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -846,7 +842,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -963,7 +959,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -993,7 +989,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -1018,7 +1014,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -1044,7 +1040,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -1079,7 +1075,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -1106,7 +1102,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
awsutil.MockOptionErr(errors.New(testOptionErr)),
}),
},
expectedErrContains: fmt.Sprintf("error getting EC2 client: error getting AWS session when fetching EC2 client: error reading options in NewCredentialsConfig: %s", testOptionErr),
expectedErrContains: fmt.Sprintf("error loading persisted state: error reading options in NewCredentialsConfig: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
},
{
@@ -1124,7 +1120,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -1170,7 +1166,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -1287,7 +1283,7 @@ func TestPluginListHostsErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -1317,7 +1313,7 @@ func TestPluginListHostsErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -1342,7 +1338,7 @@ func TestPluginListHostsErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -1368,7 +1364,7 @@ func TestPluginListHostsErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -1398,7 +1394,7 @@ func TestPluginListHostsErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -1436,7 +1432,7 @@ func TestPluginListHostsErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -1466,7 +1462,7 @@ func TestPluginListHostsErr(t *testing.T) {
awsutil.MockOptionErr(errors.New(testOptionErr)),
}),
},
expectedErrContains: fmt.Sprintf("error getting EC2 client: error getting AWS session when fetching EC2 client: error reading options in NewCredentialsConfig: %s", testOptionErr),
expectedErrContains: fmt.Sprintf("error loading persisted state: error reading options in NewCredentialsConfig: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
},
{
@@ -1484,7 +1480,7 @@ func TestPluginListHostsErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -1533,7 +1529,7 @@ func TestPluginListHostsErr(t *testing.T) {
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -1563,9 +1559,9 @@ func TestPluginListHostsErr(t *testing.T) {
nil,
testMockEC2WithDescribeInstancesOutput(
&ec2.DescribeInstancesOutput{
Reservations: []*ec2.Reservation{
Reservations: []types.Reservation{
{
Instances: []*ec2.Instance{
Instances: []types.Instance{
{
// Blank so we error out
},
@@ -1600,7 +1596,7 @@ func TestBuildFilters(t *testing.T) {
cases := []struct {
name string
in map[string]any
expected []*ec2.Filter
expected []types.Filter
expectedErrContains string
}{
{
@@ -1608,14 +1604,14 @@ func TestBuildFilters(t *testing.T) {
in: map[string]any{
ConstDescribeInstancesFilters: []any{"foo=bar"},
},
expected: []*ec2.Filter{
expected: []types.Filter{
{
Name: aws.String("foo"),
Values: aws.StringSlice([]string{"bar"}),
Values: []string{"bar"},
},
{
Name: aws.String("instance-state-name"),
Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}),
Values: []string{string(types.InstanceStateNameRunning)},
},
},
},
@@ -1627,14 +1623,14 @@ func TestBuildFilters(t *testing.T) {
"instance-state-name=static",
},
},
expected: []*ec2.Filter{
expected: []types.Filter{
{
Name: aws.String("foo"),
Values: aws.StringSlice([]string{"bar"}),
Values: []string{"bar"},
},
{
Name: aws.String("instance-state-name"),
Values: aws.StringSlice([]string{"static"}),
Values: []string{"static"},
},
},
},
@@ -1645,14 +1641,14 @@ func TestBuildFilters(t *testing.T) {
"foo=bar,baz",
},
},
expected: []*ec2.Filter{
expected: []types.Filter{
{
Name: aws.String("foo"),
Values: aws.StringSlice([]string{"bar", "baz"}),
Values: []string{"bar", "baz"},
},
{
Name: aws.String("instance-state-name"),
Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}),
Values: []string{string(types.InstanceStateNameRunning)},
},
},
},
@@ -1661,10 +1657,10 @@ func TestBuildFilters(t *testing.T) {
in: map[string]any{
ConstDescribeInstancesFilters: []any{},
},
expected: []*ec2.Filter{
expected: []types.Filter{
{
Name: aws.String("instance-state-name"),
Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}),
Values: []string{string(types.InstanceStateNameRunning)},
},
},
},
@@ -1711,14 +1707,14 @@ func TestBuildDescribeInstancesInput(t *testing.T) {
dryRun: true,
expected: &ec2.DescribeInstancesInput{
DryRun: aws.Bool(true),
Filters: []*ec2.Filter{
Filters: []types.Filter{
{
Name: aws.String("foo"),
Values: aws.StringSlice([]string{"bar"}),
Values: []string{"bar"},
},
{
Name: aws.String("instance-state-name"),
Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}),
Values: []string{string(types.InstanceStateNameRunning)},
},
},
},
@@ -1733,14 +1729,14 @@ func TestBuildDescribeInstancesInput(t *testing.T) {
dryRun: false,
expected: &ec2.DescribeInstancesInput{
DryRun: aws.Bool(false),
Filters: []*ec2.Filter{
Filters: []types.Filter{
{
Name: aws.String("foo"),
Values: aws.StringSlice([]string{"bar"}),
Values: []string{"bar"},
},
{
Name: aws.String("instance-state-name"),
Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}),
Values: []string{string(types.InstanceStateNameRunning)},
},
},
},
@@ -1772,55 +1768,30 @@ func TestBuildDescribeInstancesInput(t *testing.T) {
func TestAwsInstanceToHost(t *testing.T) {
cases := []struct {
name string
instance *ec2.Instance
instance types.Instance
expected *pb.ListHostsResponseHost
expectedErr string
}{
{
name: "nil instance",
instance: nil,
expectedErr: "response integrity error: missing instance entry",
},
{
name: "missing instance id",
instance: &ec2.Instance{},
instance: types.Instance{},
expectedErr: "response integrity error: missing instance id",
},
{
name: "nil interface entry",
instance: &ec2.Instance{
InstanceId: aws.String("foobar"),
NetworkInterfaces: []*ec2.InstanceNetworkInterface{nil},
},
expectedErr: "response integrity error: interface entry is nil",
},
{
name: "nil interface address entry",
instance: &ec2.Instance{
InstanceId: aws.String("foobar"),
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
{
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{nil},
},
},
},
expectedErr: "response integrity error: interface address entry is nil",
},
{
name: "good, single IP w/public addr",
instance: &ec2.Instance{
instance: types.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PublicIpAddress: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &ec2.InstanceNetworkInterfaceAssociation{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
},
@@ -1839,15 +1810,15 @@ func TestAwsInstanceToHost(t *testing.T) {
},
{
name: "good, private",
instance: &ec2.Instance{
instance: types.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
@@ -1864,17 +1835,17 @@ func TestAwsInstanceToHost(t *testing.T) {
},
{
name: "good, multiple interfaces",
instance: &ec2.Instance{
instance: types.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PublicIpAddress: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.2"),
PrivateDnsName: aws.String("test2.example.internal"),
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
PrivateIpAddress: aws.String("10.0.0.2"),
PrivateDnsName: aws.String("test2.example.internal"),
@@ -1884,9 +1855,9 @@ func TestAwsInstanceToHost(t *testing.T) {
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &ec2.InstanceNetworkInterfaceAssociation{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
},
@@ -1905,19 +1876,19 @@ func TestAwsInstanceToHost(t *testing.T) {
},
{
name: "good, multiple public interfaces",
instance: &ec2.Instance{
instance: types.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PublicIpAddress: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.2"),
PrivateDnsName: aws.String("test2.example.internal"),
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &ec2.InstanceNetworkInterfaceAssociation{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.1.1.2"),
PublicDnsName: aws.String("test2.example.com"),
},
@@ -1929,9 +1900,9 @@ func TestAwsInstanceToHost(t *testing.T) {
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &ec2.InstanceNetworkInterfaceAssociation{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
},
@@ -1950,19 +1921,19 @@ func TestAwsInstanceToHost(t *testing.T) {
},
{
name: "good, multiple addresses on single interface",
instance: &ec2.Instance{
instance: types.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PublicIpAddress: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &ec2.InstanceNetworkInterfaceAssociation{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
},
@@ -1985,25 +1956,25 @@ func TestAwsInstanceToHost(t *testing.T) {
},
{
name: "good, single IP w/public addr and IPv6",
instance: &ec2.Instance{
instance: types.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PublicIpAddress: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &ec2.InstanceNetworkInterfaceAssociation{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.1.1.1"),
},
PrivateIpAddress: aws.String("10.0.0.1"),
},
},
Ipv6Addresses: []*ec2.InstanceIpv6Address{
nil, // Just coverage for nil assertion which is skipped
Ipv6Addresses: []types.InstanceIpv6Address{
{Ipv6Address: nil}, // Just coverage for nil assertion which is skipped
{Ipv6Address: aws.String("some::fake::address")},
},
},
@@ -2017,24 +1988,24 @@ func TestAwsInstanceToHost(t *testing.T) {
},
{
name: "good, single IP w/public addr and external name",
instance: &ec2.Instance{
instance: types.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PublicIpAddress: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
Tags: []*ec2.Tag{
Tags: []types.Tag{
{Key: aws.String("Name"), Value: aws.String("test-instance-actual-name")}, // The tag name is "Name", not "name".
{Key: aws.String("name"), Value: aws.String("test-instance-fake-name")},
{Key: aws.String("contains-Name"), Value: aws.String("test-instance-contains-name")},
},
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &ec2.InstanceNetworkInterfaceAssociation{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
},
@@ -2054,24 +2025,24 @@ func TestAwsInstanceToHost(t *testing.T) {
},
{
name: "good, single IP w/public addr and external name",
instance: &ec2.Instance{
instance: types.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PublicIpAddress: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
Tags: []*ec2.Tag{
Tags: []types.Tag{
{Key: aws.String("Name"), Value: aws.String("test-instance-actual-name")}, // The tag name is "Name", not "name".
{Key: aws.String("name"), Value: aws.String("test-instance-fake-name")},
{Key: aws.String("contains-Name"), Value: aws.String("test-instance-contains-name")},
},
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &ec2.InstanceNetworkInterfaceAssociation{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
},
@@ -2106,3 +2077,55 @@ func TestAwsInstanceToHost(t *testing.T) {
})
}
}
func TestAppendDistinct(t *testing.T) {
cases := []struct {
name string
slice []string
elems []*string
expected []string
}{
{
name: "empty slice, empty elems",
slice: []string{},
elems: []*string{},
expected: []string{},
},
{
name: "empty elems",
slice: []string{"bar"},
elems: []*string{},
expected: []string{"bar"},
},
{
name: "skip nil elem",
slice: []string{"bar"},
elems: []*string{nil},
expected: []string{"bar"},
},
{
name: "skip empty elem",
slice: []string{"bar"},
elems: []*string{aws.String("")},
expected: []string{"bar"},
},
{
name: "skip duplicate elem",
slice: []string{"bar"},
elems: []*string{
aws.String("bar"),
aws.String("foo"),
aws.String("foo"),
},
expected: []string{"bar", "foo"},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
actual := appendDistinct(tc.slice, tc.elems...)
require.ElementsMatch(actual, tc.expected)
})
}
}
+23 -23
View File
@@ -1,21 +1,27 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package host
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
"github.com/hashicorp/boundary-plugin-host-aws/internal/credential"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"google.golang.org/protobuf/types/known/structpb"
)
type EC2API interface {
DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
}
var customClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
@@ -27,10 +33,6 @@ var customClient = &http.Client{
},
}
type ec2APIFunc func(*session.Session, ...*aws.Config) (ec2iface.EC2API, error)
type awsCatalogPersistedStateOption func(s *awsCatalogPersistedState) error
type awsCatalogPersistedState struct {
*credential.AwsCredentialPersistedState
@@ -38,6 +40,10 @@ type awsCatalogPersistedState struct {
testEC2APIFunc ec2APIFunc
}
type ec2APIFunc func(...aws.Config) (EC2API, error)
type awsCatalogPersistedStateOption func(s *awsCatalogPersistedState) error
func withTestEC2APIFunc(f ec2APIFunc) awsCatalogPersistedStateOption {
return func(s *awsCatalogPersistedState) error {
if s.testEC2APIFunc != nil {
@@ -80,22 +86,16 @@ func (s *awsCatalogPersistedState) toProto() (*pb.HostCatalogPersisted, error) {
// EC2Client returns a configured EC2 client based on the session
// information stored in the state.
func (s *awsCatalogPersistedState) EC2Client(region string) (ec2iface.EC2API, error) {
sess, err := s.GetSession()
func (s *awsCatalogPersistedState) EC2Client(ctx context.Context) (EC2API, error) {
awsCfg, err := s.GenerateCredentialChain(ctx)
if err != nil {
return nil, fmt.Errorf("error getting AWS session when fetching EC2 client: %w", err)
return nil, fmt.Errorf("error getting AWS configuration when fetching EC2 client: %w", err)
}
if awsCfg == nil {
return nil, fmt.Errorf("nil aws configuration")
}
if s.testEC2APIFunc != nil {
return s.testEC2APIFunc(sess, aws.NewConfig().WithRegion(region))
return s.testEC2APIFunc(*awsCfg)
}
cfg := aws.NewConfig()
if region != "" {
cfg = cfg.WithRegion(region)
}
cfg.HTTPClient = customClient
return ec2.New(sess, cfg), nil
return ec2.NewFromConfig(*awsCfg), nil
}
+12 -5
View File
@@ -1,18 +1,25 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package host
import (
"context"
"testing"
"github.com/hashicorp/boundary-plugin-host-aws/internal/credential"
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
"github.com/stretchr/testify/require"
)
func TestAwsCatalogPersistedStateEC2Client(t *testing.T) {
require := require.New(t)
cred, err := credential.NewAwsCredentialPersistedState(
credential.WithRegion("us-west-2"),
credential.WithAccessKeyId("foobar"),
credential.WithSecretAccessKey("barfoo"),
credential.WithCredentialsConfig(&awsutil.CredentialsConfig{
Region: "us-west-2",
AccessKey: "foobar",
SecretKey: "barfoo",
}),
)
require.NoError(err)
@@ -22,7 +29,7 @@ func TestAwsCatalogPersistedStateEC2Client(t *testing.T) {
)
require.NoError(err)
clientRaw, err := state.EC2Client("us-west-2")
clientRaw, err := state.EC2Client(context.Background())
require.NoError(err)
require.NotNil(clientRaw)
client, ok := clientRaw.(*testMockEC2)
+19 -11
View File
@@ -1,10 +1,14 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package host
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
)
const (
@@ -26,10 +30,11 @@ func (s *testMockEC2State) Reset() {
}
type testMockEC2 struct {
ec2iface.EC2API
EC2API
State *testMockEC2State
Region string
State *testMockEC2State
Region string
DescribeInstancesOutput *ec2.DescribeInstancesOutput
DescribeInstancesError error
}
@@ -51,7 +56,7 @@ func testMockEC2WithDescribeInstancesError(e error) testMockEC2Option {
}
func newTestMockEC2(state *testMockEC2State, opts ...testMockEC2Option) ec2APIFunc {
return func(sess *session.Session, cfgs ...*aws.Config) (ec2iface.EC2API, error) {
return func(cfgs ...aws.Config) (EC2API, error) {
m := &testMockEC2{
State: state,
}
@@ -64,14 +69,17 @@ func newTestMockEC2(state *testMockEC2State, opts ...testMockEC2Option) ec2APIFu
for _, cfg := range cfgs {
// Last region takes precedence
m.Region = *cfg.Region
if cfg.Region != "" {
m.Region = cfg.Region
}
}
return m, nil
}
}
func (m *testMockEC2) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) {
func (m *testMockEC2) DescribeInstances(ctx context.Context, input *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
if m.State != nil {
m.State.DescribeInstancesCalled = true
m.State.DescribeInstancesInputParams = input
@@ -84,7 +92,7 @@ func (m *testMockEC2) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2
return m.DescribeInstancesOutput, nil
}
type ec2FilterSorter []*ec2.Filter
type ec2FilterSorter []types.Filter
func (s ec2FilterSorter) Len() int { return len(s) }
func (s ec2FilterSorter) Less(i, j int) bool { return *s[i].Name < *s[j].Name }
+11 -5
View File
@@ -42,7 +42,7 @@ Example policy:
```
* `iam:GetUser`, `iam:CreateAccessKey`, and `iam:DeleteAccessKey`, configured to
the IAM user to allow credential rotation. Example policy:
the IAM user to allow static credential rotation. Example policy:
```
{
@@ -66,18 +66,24 @@ Example policy:
The following `attributes` are valid on an AWS storage bucket resource:
- `disable_credential_rotation` (bool): If `true`, credential rotation will not
be performed. See the [Credential Rotation](#credential-rotation) section
above.
be performed.
- `region` (string): The region to configure the storage bucket for.
- `endpoint-url` (string): The endpoint to configure the storage.
- `role_arn` (string): The role arn configured for the assume role provider.
- `role_external_id` (string): The external id configured for the assume role provider.
- `role_session_name` (string): The session name configured for the assume role provider.
- `role_tags` (object): The key-value pair tags configured for the assume role provider.
An example of how to utilize the [endpoint attribute](https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-s3.html)
See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
### Secrets
The following `secrets` are valid on an AWS storage bucket resource:
The following `secrets` are optional on an AWS storage bucket resource:
- `access_key_id` (string): The access key ID for the IAM user to use with this
storage bucket.
- `secret_access_key` (string): The secret access key for the IAM user to use
with this storage bucket.
with this storage bucket.
See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
+14 -3
View File
@@ -1,11 +1,14 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package storage
import (
"fmt"
cred "github.com/hashicorp/boundary-plugin-host-aws/internal/credential"
"github.com/hashicorp/boundary-plugin-host-aws/internal/errors"
"github.com/hashicorp/boundary-plugin-host-aws/internal/values"
cred "github.com/hashicorp/boundary-plugin-aws/internal/credential"
"github.com/hashicorp/boundary-plugin-aws/internal/errors"
"github.com/hashicorp/boundary-plugin-aws/internal/values"
"google.golang.org/protobuf/types/known/structpb"
)
@@ -41,6 +44,14 @@ func getStorageAttributes(in *structpb.Struct) (*StorageAttributes, error) {
continue
case cred.ConstDisableCredentialRotation:
continue
case cred.ConstRoleArn:
continue
case cred.ConstRoleExternalId:
continue
case cred.ConstRoleSessionName:
continue
case cred.ConstRoleTags:
continue
default:
badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field"
}
+54 -17
View File
@@ -1,9 +1,12 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package storage
import (
"testing"
cred "github.com/hashicorp/boundary-plugin-host-aws/internal/credential"
cred "github.com/hashicorp/boundary-plugin-aws/internal/credential"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -13,29 +16,35 @@ import (
func TestGetStorageAttributes(t *testing.T) {
cases := []struct {
name string
in map[string]any
in *structpb.Struct
expected *StorageAttributes
expectedErrContains string
}{
{
name: "missing region",
in: map[string]any{},
name: "missing region",
in: &structpb.Struct{
Fields: make(map[string]*structpb.Value),
},
expectedErrContains: "missing required value \"region\"",
},
{
name: "unknown fields",
in: map[string]any{
"region": "us-west-2",
"foo": true,
"bar": true,
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"foo": structpb.NewBoolValue(true),
"bar": structpb.NewBoolValue(true),
},
},
expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
},
{
name: "good w/ endpoint",
in: map[string]any{
cred.ConstRegion: "us-west-2",
ConstAwsEndpointUrl: "0.0.0.0",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"endpoint_url": structpb.NewStringValue("0.0.0.0"),
},
},
expected: &StorageAttributes{
CredentialAttributes: &cred.CredentialAttributes{
@@ -46,8 +55,10 @@ func TestGetStorageAttributes(t *testing.T) {
},
{
name: "good w/o endpoint",
in: map[string]any{
cred.ConstRegion: "us-west-2",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
},
},
expected: &StorageAttributes{
CredentialAttributes: &cred.CredentialAttributes{
@@ -55,6 +66,35 @@ func TestGetStorageAttributes(t *testing.T) {
},
},
},
{
name: "credential attributes",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"disable_credential_rotation": structpb.NewBoolValue(true),
"role_arn": structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
"role_external_id": structpb.NewStringValue("1234567890"),
"role_session_name": structpb.NewStringValue("test-session"),
"role_tags": structpb.NewStructValue(&structpb.Struct{
Fields: map[string]*structpb.Value{
"foo": structpb.NewStringValue("bar"),
},
}),
},
},
expected: &StorageAttributes{
CredentialAttributes: &cred.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: true,
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
},
},
}
for _, tc := range cases {
@@ -62,10 +102,7 @@ func TestGetStorageAttributes(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
input, err := structpb.NewStruct(tc.in)
require.NoError(err)
actual, err := getStorageAttributes(input)
actual, err := getStorageAttributes(tc.in)
if tc.expectedErrContains != "" {
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
+3
View File
@@ -1,3 +1,6 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package storage
const (
+96 -113
View File
@@ -1,3 +1,6 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package storage
import (
@@ -15,7 +18,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/google/uuid"
cred "github.com/hashicorp/boundary-plugin-host-aws/internal/credential"
cred "github.com/hashicorp/boundary-plugin-aws/internal/credential"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"google.golang.org/grpc/codes"
@@ -48,11 +51,6 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre
return nil, status.Error(codes.InvalidArgument, "bucketName is required")
}
secrets := bucket.GetSecrets()
if secrets == nil {
return nil, status.Error(codes.InvalidArgument, "secrets is required")
}
attrs := bucket.GetAttributes()
if attrs == nil {
return nil, status.Error(codes.InvalidArgument, "attributes is required")
@@ -63,7 +61,7 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre
return nil, err
}
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
if err != nil {
return nil, err
}
@@ -71,25 +69,18 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre
// Set up the creds in our persisted state.
credState, err := cred.NewAwsCredentialPersistedState(
append([]cred.AwsCredentialPersistedStateOption{
cred.WithAccessKeyId(credConfig.AccessKey),
cred.WithSecretAccessKey(credConfig.SecretKey),
cred.WithRegion(credConfig.Region),
cred.WithCredentialsConfig(credConfig),
}, p.testCredStateOpts...)...,
)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
}
// Try to rotate the credentials if we're not skipping them.
if !storageAttributes.DisableCredentialRotation {
if err := credState.RotateCreds(); err != nil {
// Try to rotate AWS static credentials
if cred.GetCredentialType(credConfig) == cred.StaticAWS && !storageAttributes.DisableCredentialRotation {
if err := credState.RotateCreds(ctx); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
}
} else {
// Simply validate if we aren't rotating.
if err := credState.ValidateCreds(); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential validation: %s", err)
}
}
storageState, err := newAwsStoragePersistedState(
@@ -121,75 +112,102 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd
// For storage buckets we only need to really validate what is
// currently in the new copy of the storage bucket data, so skip
// fetching the new copy to keep things a little less messy.
bucket := req.GetNewBucket()
if bucket == nil {
newBucket := req.GetNewBucket()
if newBucket == nil {
return nil, status.Error(codes.InvalidArgument, "new bucket is required")
}
if bucket.BucketName == "" {
if newBucket.BucketName == "" {
return nil, status.Error(codes.InvalidArgument, "new bucketName is required")
}
var updateSecrets bool
secrets := bucket.GetSecrets()
if secrets != nil {
// We will be updating secrets this run, but what exactly that
// means will be determined later.
updateSecrets = true
currentBucket := req.GetCurrentBucket()
if currentBucket == nil {
return nil, status.Error(codes.InvalidArgument, "current bucket is required")
}
attrs := bucket.GetAttributes()
if attrs == nil {
oldAttrs := currentBucket.GetAttributes()
if oldAttrs == nil {
return nil, status.Error(codes.InvalidArgument, "current bucket attributes is required")
}
oldStorageAttributes, err := getStorageAttributes(oldAttrs)
if err != nil {
return nil, err
}
newAttrs := newBucket.GetAttributes()
if newAttrs == nil {
return nil, status.Error(codes.InvalidArgument, "new bucket attributes is required")
}
storageAttributes, err := getStorageAttributes(attrs)
newStorageAttributes, err := getStorageAttributes(newAttrs)
if err != nil {
return nil, err
}
// Get the persisted data.
// NOTE: We might need to change this at a later time. I'm not too
// sure *exactly* what scenarios we might encounter that would
// ultimately mean that we would have to handle an empty or missing
// state in update, but what we are ultimately assuming here
// (implicitly through awsStoragePersistedStateFromProto) is that
// the state will exist and be populated. Personally I think this
// is fine and important, but this may change in the future.
credState, err := cred.AwsCredentialPersistedStateFromProto(
req.GetPersisted().GetData(),
append(p.testCredStateOpts, cred.WithRegion(storageAttributes.Region))...)
oldStorageAttributes.CredentialAttributes,
p.testCredStateOpts...)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
if storageAttributes.DisableCredentialRotation && !updateSecrets {
// This is a validate check to make sure that we aren't disabling
// rotation for credentials currently being managed by rotation.
// This is not allowed.
if !credState.CredsLastRotatedTime.IsZero() {
return nil, status.Error(codes.FailedPrecondition, "cannot disable rotation for already-rotated credentials")
}
// Verify the incoming credentials are valid and return any errors to the
// user if they're not. Note this doesn't validate the credentials against
// AWS - it only does logical validation on the fields.
updatedCredentials, err := cred.GetCredentialsConfig(newBucket.GetSecrets(), newStorageAttributes.CredentialAttributes, false)
if err != nil {
return nil, err
}
if updateSecrets {
storageSecrets, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
// We will be updating credentials when one of two changes occur:
// 1. the new bucket has secrets for static credentials, in which case we need to delete the old static credentials
// 2. the new bucket has a different roleARN value for dynamic credentials
if newBucket.GetSecrets() != nil || newStorageAttributes.RoleArn != oldStorageAttributes.RoleArn {
// Ensure the incoming credentials are valid for interaction with the S3
// Bucket before replacing them.
newCredState, err := cred.NewAwsCredentialPersistedState(
append([]cred.AwsCredentialPersistedStateOption{
cred.WithCredentialsConfig(updatedCredentials),
}, p.testCredStateOpts...)...,
)
if err != nil {
return nil, err
return nil, status.Errorf(codes.InvalidArgument, "error setting up new credential persisted state: %s", err)
}
newStorageState, err := newAwsStoragePersistedState(
append([]awsStoragePersistedStateOption{
withCredentials(newCredState),
}, p.testStorageStateOpts...)...,
)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
if err := dryRunValidation(ctx, newStorageState, newStorageAttributes, newBucket); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to validate incoming credentials: %s", err)
}
// Replace the credentials. This checks the timestamp on the last
// rotation time as well and deletes the credentials if we are
// managing them (ie: if we've rotated them before).
if err := credState.ReplaceCreds(storageSecrets.AccessKey, storageSecrets.SecretKey); err != nil {
// Replace the existing credential state.
// This checks the timestamp on the last rotation time as well
// and deletes the credentials if we are managing them
// (ie: if we've rotated them before).
if err := credState.ReplaceCreds(ctx, updatedCredentials); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err)
}
}
if !storageAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS {
// This is a validate check to make sure that we aren't disabling
// rotation for credentials currently being managed by rotation.
// This is not allowed.
if newStorageAttributes.DisableCredentialRotation && newBucket.GetSecrets() == nil {
if !credState.CredsLastRotatedTime.IsZero() {
return nil, status.Error(codes.FailedPrecondition, "cannot disable rotation for already-rotated credentials")
}
}
// If we're enabling rotation now but didn't before, or have
// freshly replaced credentials, we can rotate here.
if err := credState.RotateCreds(); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
if !newStorageAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
if err := credState.RotateCreds(ctx); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
}
}
}
@@ -203,7 +221,7 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd
}
// perform dry run to ensure we can interact with the bucket as expected.
if err := dryRunValidation(ctx, storageState, storageAttributes, bucket); err != nil {
if err := dryRunValidation(ctx, storageState, newStorageAttributes, newBucket); err != nil {
return nil, err
}
@@ -242,7 +260,8 @@ func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDel
// may be corrupt/and or legitimately missing.
credState, err := cred.AwsCredentialPersistedStateFromProto(
req.GetPersisted().GetData(),
append(p.testCredStateOpts, cred.WithRegion(storageAttributes.Region))...)
storageAttributes.CredentialAttributes,
p.testCredStateOpts...)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
@@ -256,12 +275,15 @@ func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDel
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
if !credState.CredsLastRotatedTime.IsZero() {
// Delete old/existing credentials. This is done with the same
// credentials to ensure that it has the proper permissions to do
// it.
if err := credState.DeleteCreds(); err != nil {
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during storage bucket deletion: %s", err)
// try to delete static credentials
if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS {
if !credState.CredsLastRotatedTime.IsZero() {
// Delete old/existing credentials. This is done with the same
// credentials to ensure that it has the proper permissions to do
// it.
if err := credState.DeleteCreds(ctx); err != nil {
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during storage bucket deletion: %s", err)
}
}
}
@@ -283,11 +305,6 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques
return nil, status.Error(codes.InvalidArgument, "bucketName is required")
}
secrets := bucket.GetSecrets()
if secrets == nil {
return nil, status.Error(codes.InvalidArgument, "secrets is required")
}
attrs := bucket.GetAttributes()
if attrs == nil {
return nil, status.Error(codes.InvalidArgument, "attributes is required")
@@ -298,7 +315,7 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques
return nil, err
}
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
if err != nil {
return nil, err
}
@@ -306,9 +323,7 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques
// Set up the creds in our persisted state.
credState, err := cred.NewAwsCredentialPersistedState(
append([]cred.AwsCredentialPersistedStateOption{
cred.WithAccessKeyId(credConfig.AccessKey),
cred.WithSecretAccessKey(credConfig.SecretKey),
cred.WithRegion(credConfig.Region),
cred.WithCredentialsConfig(credConfig),
}, p.testCredStateOpts...)...,
)
if err != nil {
@@ -325,9 +340,6 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques
}
opts := []s3Option{}
if storageAttributes.Region != "" {
opts = append(opts, WithRegion(storageAttributes.Region))
}
if storageAttributes.EndpointUrl != "" {
opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl))
}
@@ -361,11 +373,6 @@ func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Validat
return nil, status.Error(codes.InvalidArgument, "bucketName is required")
}
secrets := bucket.GetSecrets()
if secrets == nil {
return nil, status.Error(codes.InvalidArgument, "secrets is required")
}
attrs := bucket.GetAttributes()
if attrs == nil {
return nil, status.Error(codes.InvalidArgument, "attributes is required")
@@ -376,7 +383,7 @@ func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Validat
return nil, err
}
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
if err != nil {
return nil, err
}
@@ -384,9 +391,7 @@ func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Validat
// Set up the creds in our persisted state.
credState, err := cred.NewAwsCredentialPersistedState(
append([]cred.AwsCredentialPersistedStateOption{
cred.WithAccessKeyId(credConfig.AccessKey),
cred.WithSecretAccessKey(credConfig.SecretKey),
cred.WithRegion(credConfig.Region),
cred.WithCredentialsConfig(credConfig),
}, p.testCredStateOpts...)...,
)
if err != nil {
@@ -426,11 +431,6 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu
return status.Error(codes.InvalidArgument, "bucketName is required")
}
secrets := bucket.GetSecrets()
if secrets == nil {
return status.Error(codes.InvalidArgument, "secrets is required")
}
attrs := bucket.GetAttributes()
if attrs == nil {
return status.Error(codes.InvalidArgument, "attributes is required")
@@ -441,7 +441,7 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu
return err
}
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
if err != nil {
return err
}
@@ -449,9 +449,7 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu
// Set up the creds in our persisted state.
credState, err := cred.NewAwsCredentialPersistedState(
append([]cred.AwsCredentialPersistedStateOption{
cred.WithAccessKeyId(credConfig.AccessKey),
cred.WithSecretAccessKey(credConfig.SecretKey),
cred.WithRegion(credConfig.Region),
cred.WithCredentialsConfig(credConfig),
}, p.testCredStateOpts...)...,
)
if err != nil {
@@ -468,9 +466,6 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu
}
opts := []s3Option{}
if storageAttributes.Region != "" {
opts = append(opts, WithRegion(storageAttributes.Region))
}
if storageAttributes.EndpointUrl != "" {
opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl))
}
@@ -530,10 +525,6 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
if bucket.GetBucketName() == "" {
return nil, status.Error(codes.InvalidArgument, "bucketName is required")
}
secrets := bucket.GetSecrets()
if secrets == nil {
return nil, status.Error(codes.InvalidArgument, "secrets is required")
}
attrs := bucket.GetAttributes()
if attrs == nil {
return nil, status.Error(codes.InvalidArgument, "attributes is required")
@@ -561,7 +552,7 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
return nil, err
}
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
if err != nil {
return nil, err
}
@@ -569,9 +560,7 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
// Set up the creds in our persisted state.
credState, err := cred.NewAwsCredentialPersistedState(
append([]cred.AwsCredentialPersistedStateOption{
cred.WithAccessKeyId(credConfig.AccessKey),
cred.WithSecretAccessKey(credConfig.SecretKey),
cred.WithRegion(credConfig.Region),
cred.WithCredentialsConfig(credConfig),
}, p.testCredStateOpts...)...,
)
if err != nil {
@@ -588,9 +577,6 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
}
opts := []s3Option{}
if storageAttributes.Region != "" {
opts = append(opts, WithRegion(storageAttributes.Region))
}
if storageAttributes.EndpointUrl != "" {
opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl))
}
@@ -654,9 +640,6 @@ func dryRunValidation(ctx context.Context, state *awsStoragePersistedState, attr
}
opts := []s3Option{}
if attrs.Region != "" {
opts = append(opts, WithRegion(attrs.Region))
}
if attrs.EndpointUrl != "" {
opts = append(opts, WithEndpoint(attrs.EndpointUrl))
}
File diff suppressed because it is too large. Load diff
+44 -54
View File
@@ -1,3 +1,6 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package storage
import (
@@ -6,13 +9,13 @@ import (
"fmt"
"net"
"net/http"
"net/url"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/hashicorp/boundary-plugin-host-aws/internal/credential"
smithyEndpoints "github.com/aws/smithy-go/endpoints"
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
"google.golang.org/protobuf/types/known/structpb"
)
@@ -94,55 +97,23 @@ func (s *awsStoragePersistedState) S3Client(ctx context.Context, opt ...s3Option
if err != nil {
return nil, fmt.Errorf("error parsing options when fetching S3 client: %w", err)
}
awsConfigOpts := []func(*config.LoadOptions) error{}
sess, err := s.GetSession()
awsCfg, err := s.GenerateCredentialChain(ctx)
if err != nil {
return nil, fmt.Errorf("error getting AWS session when fetching S3 client: %w", err)
return nil, fmt.Errorf("error getting AWS configuration when fetching S3 client: %w", err)
}
creds, err := sess.Config.Credentials.Get()
if err != nil {
return nil, fmt.Errorf("error getting AWS session when fetching S3 client: %w", err)
}
customCredentials := config.WithCredentialsProvider(
credentials.NewStaticCredentialsProvider(creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken),
)
awsConfigOpts = append(awsConfigOpts, customCredentials)
if sess.Config.Region != nil {
awsConfigOpts = append(awsConfigOpts, config.WithRegion(*sess.Config.Region))
}
if opts.withRegion != "" {
awsConfigOpts = append(awsConfigOpts, config.WithRegion(opts.withRegion))
if awsCfg == nil {
return nil, fmt.Errorf("nil aws configuration")
}
var s3Opts []func(*s3.Options)
if opts.withEndpoint != "" {
customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
if service == s3.ServiceID {
if opts.withRegion != "" {
region = opts.withRegion
}
return aws.Endpoint{
PartitionID: "aws",
URL: opts.withEndpoint,
SigningRegion: region,
}, nil
}
// returning EndpointNotFoundError will allow the service to fallback to it's default resolution
return aws.Endpoint{}, &aws.EndpointNotFoundError{}
})
awsConfigOpts = append(awsConfigOpts, config.WithEndpointResolverWithOptions(customResolver))
s3Opts = append(s3Opts, s3.WithEndpointResolverV2(&endpointResolver{
endpoint: opts.withEndpoint,
}))
}
cfg, err := config.LoadDefaultConfig(ctx, awsConfigOpts...)
if err != nil {
return nil, fmt.Errorf("error loading aws configuration when fetching S3 client: %w", err)
}
if s.testS3APIFunc != nil {
return s.testS3APIFunc(cfg)
return s.testS3APIFunc(*awsCfg)
}
cfg.HTTPClient = customClient
return s3.NewFromConfig(cfg), nil
return s3.NewFromConfig(*awsCfg, s3Opts...), nil
}
// getOpts iterates the inbound s3Options and returns a struct
@@ -164,7 +135,6 @@ type s3Option func(*s3Options) error
// options = how options are represented
type s3Options struct {
withRegion string
withEndpoint string
}
@@ -172,14 +142,6 @@ func getDefaultS3Options() s3Options {
return s3Options{}
}
// WithRegion contains the region to use
func WithRegion(with string) s3Option {
return func(o *s3Options) error {
o.withRegion = with
return nil
}
}
// WithEndpoint contains the endpoint to use
func WithEndpoint(with string) s3Option {
return func(o *s3Options) error {
@@ -187,3 +149,31 @@ func WithEndpoint(with string) s3Option {
return nil
}
}
type endpointResolver struct {
endpoint string
}
func (e *endpointResolver) ResolveEndpoint(ctx context.Context, params s3.EndpointParameters) (resolver smithyEndpoints.Endpoint, err error) {
var uri *url.URL
resolver, err = s3.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params)
if err != nil {
return
}
if e.endpoint == "" {
// TODO: not sure if we should set an error or ignore the missing endpoint value and continue with the default resolver
return
}
uri, err = url.Parse(e.endpoint)
if err != nil {
return
}
if uri == nil {
// TODO: not sure if we should set an error or ignore the missing uri value and continue with the default resolver
return
}
resolver = smithyEndpoints.Endpoint{
URI: *uri,
}
return
}
+62 -26
View File
@@ -1,40 +1,76 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package storage
import (
"context"
"testing"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/hashicorp/boundary-plugin-host-aws/internal/credential"
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
"github.com/stretchr/testify/require"
)
func TestAwsStoragePersistedStateS3Client(t *testing.T) {
require := require.New(t)
cred, err := credential.NewAwsCredentialPersistedState(
credential.WithRegion("us-west-2"),
credential.WithAccessKeyId("foobar"),
credential.WithSecretAccessKey("barfoo"),
)
require.NoError(err)
cases := []struct {
name string
creds *awsutil.CredentialsConfig
opts []awsStoragePersistedStateOption
awsOpts []awsutil.Option
s3Opts []s3Option
expectedRegion string
expectedEndpoint string
}{
{
name: "static credentials",
creds: func() *awsutil.CredentialsConfig {
creds, err := awsutil.NewCredentialsConfig(
awsutil.WithRegion("us-west-2"),
awsutil.WithAccessKey("foobar"),
awsutil.WithSecretKey("barfoo"),
)
require.NoError(t, err)
return creds
}(),
awsOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(),
),
},
opts: []awsStoragePersistedStateOption{
withTestS3APIFunc(newTestMockS3(nil)),
},
s3Opts: []s3Option{
WithEndpoint("0.0.0.0"),
},
expectedRegion: "us-west-2",
expectedEndpoint: "0.0.0.0",
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
state, err := newAwsStoragePersistedState(
withCredentials(cred),
withTestS3APIFunc(newTestMockS3(nil)),
)
require.NoError(err)
cred, err := credential.NewAwsCredentialPersistedState(
credential.WithCredentialsConfig(tc.creds),
credential.WithStateTestOpts(tc.awsOpts),
)
require.NoError(err)
clientRaw, err := state.S3Client(context.Background(), WithRegion("us-west-2"), WithEndpoint("0.0.0.0"))
require.NoError(err)
require.NotNil(clientRaw)
client, ok := clientRaw.(*testMockS3)
require.True(ok)
require.NotNil(client)
require.Equal("us-west-2", client.Region)
state, err := newAwsStoragePersistedState(
append(tc.opts, withCredentials(cred))...,
)
require.NoError(err)
endpoint, err := client.Endpoint.ResolveEndpoint(s3.ServiceID, client.Region)
require.NoError(err)
require.Equal("0.0.0.0", endpoint.URL)
require.Equal("us-west-2", endpoint.SigningRegion)
require.Equal("aws", endpoint.PartitionID)
clientRaw, err := state.S3Client(context.Background(), tc.s3Opts...)
require.NoError(err)
require.NotNil(clientRaw)
client, ok := clientRaw.(*testMockS3)
require.True(ok)
require.NotNil(client)
require.Equal(tc.expectedRegion, client.Region)
})
}
}
+8 -9
View File
@@ -1,3 +1,6 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package storage
import (
@@ -10,11 +13,11 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/hashicorp/boundary-plugin-host-aws/internal/credential"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/hashicorp/go-secure-stdlib/awsutil"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/structpb"
@@ -66,9 +69,8 @@ func (s *testMockS3State) Reset() {
type testMockS3 struct {
S3API
State *testMockS3State
Region string
Endpoint aws.EndpointResolverWithOptions
State *testMockS3State
Region string
// mocked responses for getObject
GetObjectOutput *s3.GetObjectOutput
@@ -144,9 +146,6 @@ func newTestMockS3(state *testMockS3State, opts ...testMockS3Option) s3APIFunc {
if cfg.Region != "" {
m.Region = cfg.Region
}
if cfg.EndpointResolverWithOptions != nil {
m.Endpoint = cfg.EndpointResolverWithOptions
}
}
return m, nil
+6 -200
View File
@@ -11,26 +11,20 @@ import (
"testing"
"time"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/hashicorp/boundary-plugin-host-aws/internal/credential"
"github.com/hashicorp/boundary-plugin-host-aws/internal/values"
"github.com/hashicorp/boundary-plugin-host-aws/plugin/service/host"
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
"github.com/hashicorp/boundary-plugin-aws/plugin/service/host"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/hashicorp/go-secure-stdlib/awsutil"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
)
const (
expectedIamUserCount = 6
expectedEc2InstanceCount = 5
)
const expectedEc2InstanceCount = 5
var expectedTags = []string{"foo", "bar", "baz"}
func TestPlugin(t *testing.T) {
func TestHostPlugin(t *testing.T) {
region := os.Getenv("AWS_REGION")
if region == "" {
t.Skip("set AWS_REGION to use this test")
@@ -195,36 +189,7 @@ func testPluginOnCreateCatalog(ctx context.Context, t *testing.T, p *host.HostPl
require.NotNil(response)
persisted := response.GetPersisted()
require.NotNil(persisted)
persistedSecrets := persisted.GetSecrets()
require.NotNil(persistedSecrets)
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
require.NoError(err)
require.NotZero(persistedAccessKeyId)
if rotate {
require.NotEqual(accessKeyId, persistedAccessKeyId)
} else {
require.Equal(accessKeyId, persistedAccessKeyId)
}
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
require.NoError(err)
require.NotZero(persistedSecretAccessKey)
if rotate {
require.NotEqual(secretAccessKey, persistedSecretAccessKey)
} else {
require.Equal(secretAccessKey, persistedSecretAccessKey)
}
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
require.NoError(err)
if rotate {
require.NotZero(persistedCredsLastRotatedTime)
requireCredentialsInvalid(t, accessKeyId, secretAccessKey)
} else {
require.Zero(persistedCredsLastRotatedTime)
}
return persistedAccessKeyId, persistedSecretAccessKey
return validatePersistedSecrets(t, persisted.GetSecrets(), accessKeyId, secretAccessKey, rotate)
}
func testPluginOnUpdateCatalog(
@@ -293,116 +258,7 @@ func testPluginOnUpdateCatalog(
require.NotNil(response)
persisted := response.GetPersisted()
require.NotNil(persisted)
persistedSecrets := persisted.GetSecrets()
require.NotNil(persistedSecrets)
// Complex checks based on the scenarios.
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
require.NoError(err)
require.NotZero(persistedAccessKeyId)
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
require.NoError(err)
require.NotZero(persistedSecretAccessKey)
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
require.NoError(err)
// Our test scenarios are complex due the multi-dimensional nature
// of criteria, so we lay them out in a switch below.
switch {
case newAccessKeyId != "" && rotated && rotate:
// The new access key ID was provided, we had previously rotated
// the credentials before, and the new credential set is to be
// rotated as well. In this case, the old credentials should have
// been deleted, and the new credentials should have been rotated,
// hence, should not match the new credentials initially
// provided. Rotation time should be non-zero and updated.
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
require.NotZero(persistedCredsLastRotatedTime)
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
case newAccessKeyId != "" && rotated && !rotate:
// The new access key ID was provided, we had previously rotated
// the credentials before, and the new credential is *not*
// rotated. In this case, the old credentials should have
// been deleted, But the new credentials should have not been
// rotated, and hence should be the same. Rotation time should be
// zero.
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
require.Equal(persistedAccessKeyId, newAccessKeyId)
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
require.Zero(persistedCredsLastRotatedTime)
case newAccessKeyId != "" && !rotated && rotate:
// The new access key ID was provided, we *have not* previously
// rotated the credentials, and the new credential set is to be
// rotated. In this case, the old credentials should have been
// left alone, and the new credentials should have been rotated,
// hence, should not match the new credentials initially
// provided. Rotation time should be non-zero, but updated.
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
require.NotZero(persistedCredsLastRotatedTime)
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
case newAccessKeyId != "" && !rotated && !rotate:
// The new access key ID was provided, but we have not rotated
// the credentials previously and we still don't plan on rotating
// them. In this case, the old credentials should still be valid,
// and the persisted ones should match the new ones provided.
// Rotation time should be zero.
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
require.Equal(persistedAccessKeyId, newAccessKeyId)
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
require.Zero(persistedCredsLastRotatedTime)
case newAccessKeyId == "" && rotated && rotate:
// No new credentials have been provided, but we have previously
// rotated and are still rotating credentials. This is a no-op.
// Existing credentials should still be valid and match the ones
// persisted to state. Rotation time should be identical since
// no new rotation occurred.
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
require.Equal(persistedAccessKeyId, currentAccessKeyId)
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
require.NotZero(persistedCredsLastRotatedTime)
require.True(currentCredsLastRotatedTime.Equal(persistedCredsLastRotatedTime))
case newAccessKeyId == "" && rotated && !rotate:
// No new credentials have been provided, and we have previously
// rotated the credentials. This is actually an error, but we
// don't test it here; it's covered in unit testing (see
// plugin_test.go).
require.FailNow("testing rotated-to-not-rotated scenario not implemented by this helper")
case newAccessKeyId == "" && !rotated && rotate:
// No new credentials have been provided, and while we did not
// rotate before, we want to switch to rotation. In this case,
// the existing persisted credentials should have been rotated,
// with a new non-zero timestamp.
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
require.NotEqual(persistedAccessKeyId, currentAccessKeyId)
require.NotEqual(persistedSecretAccessKey, currentSecretAccessKey)
require.NotZero(persistedCredsLastRotatedTime)
case newAccessKeyId == "" && !rotated && !rotate:
// No new credentials have been provided and we have not, nor do
// not, plan on rotating the credentials. This is a no-op.
// Existing credentials should still be valid and match the ones
// persisted to state. Rotation time should remain at zero.
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
require.Equal(persistedAccessKeyId, currentAccessKeyId)
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
require.Zero(persistedCredsLastRotatedTime)
default:
// Scenario was reached that was not covered by this function.
require.FailNow("unknown test scenario")
}
return persistedAccessKeyId, persistedSecretAccessKey
return validateUpdateSecrets(t, persisted.GetSecrets(), currentCredsLastRotatedTime, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey, rotated, rotate)
}
func testPluginOnDeleteCatalog(ctx context.Context, t *testing.T, p *host.HostPlugin, region, accessKeyId, secretAccessKey string, rotated bool) {
@@ -588,53 +444,3 @@ func testPluginListHosts(ctx context.Context, t *testing.T, p *host.HostPlugin,
// Success
t.Logf("testing ListHosts: success (region=%s, tags=%v, expected/actual=(len=%d, ids=%s))", region, tags, len(actualInstances), actualInstances)
}
func requireCredentialsInvalid(t *testing.T, accessKeyId, secretAccessKey string) {
t.Helper()
require := require.New(t)
c, err := awsutil.NewCredentialsConfig(
awsutil.WithAccessKey(accessKeyId),
awsutil.WithSecretKey(secretAccessKey),
)
require.NoError(err)
// We need to wait for invalidation as while awsutil waits for
// credential creation, deletion of the old credentials returns
// immediately.
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
waitErr:
for {
_, err = c.GetCallerIdentity()
if err != nil {
break
}
select {
case <-time.After(time.Second):
// pass
case <-timeoutCtx.Done():
break waitErr
}
}
require.NotNil(err)
awsErr, ok := err.(awserr.Error)
require.True(ok)
require.Equal("InvalidClientTokenId", awsErr.Code())
}
func requireCredentialsValid(t *testing.T, accessKeyId, secretAccessKey string) {
t.Helper()
require := require.New(t)
c, err := awsutil.NewCredentialsConfig(
awsutil.WithAccessKey(accessKeyId),
awsutil.WithSecretKey(secretAccessKey),
)
require.NoError(err)
_, err = c.GetCallerIdentity()
require.NoError(err)
}
+411
View File
@@ -0,0 +1,411 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package testing
import (
"context"
"io"
"net"
"os"
"path/filepath"
"testing"
"time"
"github.com/google/uuid"
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
"github.com/hashicorp/boundary-plugin-aws/plugin/service/storage"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/types/known/structpb"
)
func TestStoragePlugin(t *testing.T) {
region := os.Getenv("AWS_REGION")
if region == "" {
t.Skip("set AWS_REGION to use this test")
}
if envAccessKeyId := os.Getenv("AWS_ACCESS_KEY_ID"); envAccessKeyId == "" {
t.Skip("set AWS_ACCESS_KEY_ID to use this test")
}
if envSecretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY"); envSecretAccessKey == "" {
t.Skip("set AWS_SECRET_ACCESS_KEY to use this test")
}
require := require.New(t)
tf, err := NewTestTerraformer("testdata/storage")
require.NoError(err)
require.NotNil(tf)
t.Log("===== deploying test Terraform workspace =====")
err = tf.Deploy()
require.NoError(err)
t.Cleanup(func() {
t.Log("===== destroying test Terraform workspace =====")
if err := tf.Destroy(); err != nil {
t.Logf("WARNING: could not run Terraform destroy: %s", err)
}
})
bucketName, err := tf.GetOutputString("bucket_name")
require.NoError(err)
require.NotEmpty(bucketName)
iamAccessKeyIds, err := tf.GetOutputSlice("iam_access_key_ids")
require.NoError(err)
require.Len(iamAccessKeyIds, expectedIamUserCount)
iamSecretAccessKeys, err := tf.GetOutputSlice("iam_secret_access_keys")
require.NoError(err)
require.Len(iamSecretAccessKeys, expectedIamUserCount)
iamAccessKeyMissingGetObject, err := tf.GetOutputString("iam_access_key_missing_get_obj")
require.NoError(err)
iamSecretAccessKeyMissingGetObject, err := tf.GetOutputString("iam_secret_access_key_missing_get_obj")
require.NoError(err)
iamAccessKeyMissingPutObject, err := tf.GetOutputString("iam_access_key_missing_put_obj")
require.NoError(err)
iamSecretAccessKeyMissingPutObject, err := tf.GetOutputString("iam_secret_access_key_missing_put_obj")
require.NoError(err)
// Start the workflow now. Set up the storage bucket. Note that this
// will cause the state to go out of drift above in the sense that
// the access key ID/secret access key will no longer be valid. We
// will assert this through the returned state.
p := new(storage.StoragePlugin)
ctx := context.Background()
// ********************
// * OnCreateStorageBucket
// ********************
//
// Test non-rotation (using primary user).
keyid, secret := testPluginOnCreateStorageBucket(ctx, t, p, bucketName, region, iamAccessKeyIds[0].(string), iamSecretAccessKeys[0].(string), false)
// Test rotation next.
keyid, secret = testPluginOnCreateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, true)
// ********************
// * OnUpdateStorageBucket
// ********************
//
// Test no-op non-rotation.
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, "", "", false, false)
// Switch to rotation.
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, "", "", false, true)
// Test no-op with rotation.
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, "", "", true, true)
// Switch credentials to next user. Don't rotate.
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[1].(string), iamSecretAccessKeys[1].(string), true, false)
// Switch credentials to next user. Add rotation.
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[2].(string), iamSecretAccessKeys[2].(string), false, true)
// Switch to next user, with rotation disabled.
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[3].(string), iamSecretAccessKeys[3].(string), true, false)
// Last case - switch to another user and keep rotation off.
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[4].(string), iamSecretAccessKeys[4].(string), false, false)
// ********************
// * OnDeleteStorageBucket
// ********************
//
// Test non-rotated.
testPluginOnDeleteStorageBucket(ctx, t, p, bucketName, region, keyid, secret, false)
// Test as if we had rotated these credentials (note that this
// makes this test set unusable).
testPluginOnDeleteStorageBucket(ctx, t, p, bucketName, region, keyid, secret, true)
// ********************
// * Object Methods: PutObject, GetObject, HeadObject
// ********************
//
// Reassign the keyid and secret first.
keyid, secret = iamAccessKeyIds[5].(string), iamSecretAccessKeys[5].(string)
testPluginObjectMethods(ctx, t, p, bucketName, region, keyid, secret)
// ********************
// * Validate Permissions
// ********************
//
// Validate error is returned for missing get object permission
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingGetObject, iamSecretAccessKeyMissingGetObject)
// Validate error is returned for missing put object permission
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingPutObject, iamSecretAccessKeyMissingPutObject)
}
func testPluginOnCreateStorageBucket(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string, rotate bool) (string, string) {
t.Helper()
t.Logf("testing OnCreateStorageBucket (region=%s, rotate=%t)", region, rotate)
require := require.New(t)
reqAttrs, err := structpb.NewStruct(map[string]any{
credential.ConstRegion: region,
credential.ConstDisableCredentialRotation: !rotate,
})
require.NoError(err)
reqSecrets, err := structpb.NewStruct(map[string]any{
credential.ConstAccessKeyId: accessKeyId,
credential.ConstSecretAccessKey: secretAccessKey,
})
require.NoError(err)
request := &pb.OnCreateStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: bucketName,
Secrets: reqSecrets,
Attributes: reqAttrs,
},
}
response, err := p.OnCreateStorageBucket(ctx, request)
require.NoError(err)
require.NotNil(response)
persisted := response.GetPersisted()
require.NotNil(persisted)
return validatePersistedSecrets(t, persisted.GetData(), accessKeyId, secretAccessKey, rotate)
}
func testPluginOnUpdateStorageBucket(
ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName,
region, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey string,
rotated, rotate bool,
) (string, string) {
t.Helper()
t.Logf("testing OnUpdateStorageBucket (region=%s, newcreds=%t, rotated=%t, rotate=%t)", region, newAccessKeyId != "" && newSecretAccessKey != "", rotated, rotate)
require := require.New(t)
// Take a timestamp of the current time to get a point in time to
// reference, ensuring that we are updating credential rotation
// timestamps.
currentCredsLastRotatedTime := time.Now()
reqCurrentAttrs, err := structpb.NewStruct(map[string]any{
credential.ConstRegion: region,
credential.ConstDisableCredentialRotation: !rotated,
})
require.NoError(err)
reqNewAttrs, err := structpb.NewStruct(map[string]any{
credential.ConstRegion: region,
credential.ConstDisableCredentialRotation: !rotate,
})
require.NoError(err)
var reqSecrets *structpb.Struct
if newAccessKeyId != "" && newSecretAccessKey != "" {
reqSecrets, err = structpb.NewStruct(map[string]any{
credential.ConstAccessKeyId: newAccessKeyId,
credential.ConstSecretAccessKey: newSecretAccessKey,
})
require.NoError(err)
}
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
credential.ConstAccessKeyId: currentAccessKeyId,
credential.ConstSecretAccessKey: currentSecretAccessKey,
credential.ConstCredsLastRotatedTime: func() string {
if rotated {
return currentCredsLastRotatedTime.Format(time.RFC3339Nano)
}
return (time.Time{}).Format(time.RFC3339Nano)
}(),
})
require.NoError(err)
require.NotNil(reqPersistedSecrets)
request := &pb.OnUpdateStorageBucketRequest{
CurrentBucket: &storagebuckets.StorageBucket{
BucketName: bucketName,
Attributes: reqCurrentAttrs,
},
NewBucket: &storagebuckets.StorageBucket{
BucketName: bucketName,
Attributes: reqNewAttrs,
Secrets: reqSecrets,
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: reqPersistedSecrets,
},
}
response, err := p.OnUpdateStorageBucket(ctx, request)
require.NoError(err)
require.NotNil(response)
persisted := response.GetPersisted()
require.NotNil(persisted)
return validateUpdateSecrets(t, persisted.GetData(), currentCredsLastRotatedTime, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey, rotated, rotate)
}
func testPluginOnDeleteStorageBucket(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string, rotated bool) {
t.Helper()
t.Logf("testing OnDeleteStorageBucket (region=%s, rotated=%t)", region, rotated)
require := require.New(t)
reqAttrs, err := structpb.NewStruct(map[string]any{
credential.ConstRegion: region,
credential.ConstDisableCredentialRotation: !rotated,
})
require.NoError(err)
reqSecrets, err := structpb.NewStruct(map[string]any{
credential.ConstAccessKeyId: accessKeyId,
credential.ConstSecretAccessKey: secretAccessKey,
})
require.NoError(err)
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
credential.ConstAccessKeyId: accessKeyId,
credential.ConstSecretAccessKey: secretAccessKey,
credential.ConstCredsLastRotatedTime: func() string {
if rotated {
return time.Now().Format(time.RFC3339Nano)
}
return (time.Time{}).Format(time.RFC3339Nano)
}(),
})
require.NoError(err)
request := &pb.OnDeleteStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: bucketName,
Attributes: reqAttrs,
Secrets: reqSecrets,
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: reqPersistedSecrets,
},
}
response, err := p.OnDeleteStorageBucket(ctx, request)
require.NoError(err)
require.NotNil(response)
// We want to test the validity of the credentials post-deletion.
if rotated {
// The credentials should no longer be valid.
requireCredentialsInvalid(t, accessKeyId, secretAccessKey)
} else {
// The credentials should still be valid. Sleep 10s first just to
// be sure, since we're not rotating.
time.Sleep(time.Second * 10)
requireCredentialsValid(t, accessKeyId, secretAccessKey)
}
}
func testPluginValidatePermissions(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string) {
t.Helper()
t.Logf("testing ValidatePermissions")
require := require.New(t)
reqAttrs, err := structpb.NewStruct(map[string]any{
credential.ConstRegion: region,
credential.ConstDisableCredentialRotation: true,
})
require.NoError(err)
reqSecrets, err := structpb.NewStruct(map[string]any{
credential.ConstAccessKeyId: accessKeyId,
credential.ConstSecretAccessKey: secretAccessKey,
})
require.NoError(err)
req := &pb.ValidatePermissionsRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: bucketName,
Attributes: reqAttrs,
Secrets: reqSecrets,
},
}
_, err = p.ValidatePermissions(ctx, req)
require.Error(err)
}
func testPluginObjectMethods(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string) {
t.Helper()
t.Logf("testing PutObject")
require := require.New(t)
testDataPath, err := filepath.Abs("./testdata/storage/test_object_data")
require.NoError(err)
require.FileExists(testDataPath)
reqAttrs, err := structpb.NewStruct(map[string]any{
credential.ConstRegion: region,
credential.ConstDisableCredentialRotation: true,
})
require.NoError(err)
reqSecrets, err := structpb.NewStruct(map[string]any{
credential.ConstAccessKeyId: accessKeyId,
credential.ConstSecretAccessKey: secretAccessKey,
})
require.NoError(err)
objectKey := uuid.New().String()
putObjReq := &pb.PutObjectRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: bucketName,
Attributes: reqAttrs,
Secrets: reqSecrets,
},
Key: objectKey,
Path: testDataPath,
}
putObjResp, err := p.PutObject(ctx, putObjReq)
require.NoError(err)
require.NotNil(putObjResp)
require.NotEmpty(putObjResp.GetChecksumSha_256())
t.Logf("testing HeadObject")
headObjReq := &pb.HeadObjectRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: bucketName,
Attributes: reqAttrs,
Secrets: reqSecrets,
},
Key: objectKey,
}
headObjResp, err := p.HeadObject(ctx, headObjReq)
require.NoError(err)
require.NotNil(headObjResp)
require.NotEmpty(headObjResp.GetContentLength())
require.NotEmpty(headObjResp.GetLastModified())
// Need to create a GRPC server to test GetObject
t.Logf("testing GetObject")
lis, err := net.Listen("tcp", "localhost:2030")
require.NoError(err)
grpcServer := grpc.NewServer()
pb.RegisterStoragePluginServiceServer(grpcServer, &storage.StoragePlugin{})
go func() {
grpcServer.Serve(lis)
}()
conn, err := grpc.Dial("localhost:2030", grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(err)
defer conn.Close()
client := pb.NewStoragePluginServiceClient(conn)
stream, err := client.GetObject(ctx, &pb.GetObjectRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: bucketName,
Attributes: reqAttrs,
Secrets: reqSecrets,
},
Key: objectKey,
})
require.NoError(err)
var actualData []byte
expectedData, err := os.ReadFile(testDataPath)
require.NoError(err)
for {
resp, err := stream.Recv()
if err == io.EOF {
require.NoError(stream.CloseSend())
break
} else if err != nil {
require.NoError(err)
}
actualData = append(actualData, resp.GetFileChunk()...)
}
require.Equal(expectedData, actualData)
require.NoError(conn.Close())
grpcServer.Stop()
}
+240
View File
@@ -0,0 +1,240 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
resource "aws_iam_role" "valid" {
name = "${random_id.prefix.dec}-valid"
tags = local.tags
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Sid = ""
Principal = {
AWS: data.aws_caller_identity.current.user_id
}
},
]
})
}
resource "aws_iam_policy" "valid" {
name = "${random_id.prefix.dec}-valid"
tags = local.tags
policy = <<EOT
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:GetObjectAttributes"
],
"Effect": "Allow",
"Resource": "${aws_s3_bucket.test.arn}*"
}
]
}
EOT
}
resource "aws_iam_role_policy_attachment" "valid" {
role = aws_iam_role.valid.name
policy_arn = aws_iam_policy.valid.arn
}
resource "aws_iam_role" "missing_put_obj" {
name = "${random_id.prefix.dec}-missing-put-obj"
tags = local.tags
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Sid = ""
Principal = {
AWS: data.aws_caller_identity.current.user_id
}
},
]
})
}
resource "aws_iam_policy" "missing_put_obj" {
name = "${random_id.prefix.dec}-missing-put-obj"
tags = local.tags
policy = <<EOT
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:GetObject",
"s3:GetObjectAttributes"
],
"Effect": "Allow",
"Resource": "${aws_s3_bucket.test.arn}*"
}
]
}
EOT
}
resource "aws_iam_role_policy_attachment" "missing_put_obj" {
role = aws_iam_role.missing_put_obj.name
policy_arn = aws_iam_policy.missing_put_obj.arn
}
resource "aws_iam_role" "missing_get_obj" {
name = "${random_id.prefix.dec}-missing-get-obj"
tags = local.tags
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Sid = ""
Principal = {
AWS: data.aws_caller_identity.current.user_id
}
},
]
})
}
resource "aws_iam_policy" "missing_get_obj" {
name = "${random_id.prefix.dec}-missing-get-obj"
tags = local.tags
policy = <<EOT
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:PutObject",
"s3:GetObjectAttributes"
],
"Effect": "Allow",
"Resource": "${aws_s3_bucket.test.arn}*"
}
]
}
EOT
}
resource "aws_iam_role_policy_attachment" "missing_get_obj" {
role = aws_iam_role.missing_get_obj.name
policy_arn = aws_iam_policy.missing_get_obj.arn
}
# Static Credential Testing
resource "random_id" "user_name" {
count = var.iam_user_count
prefix = random_id.prefix.dec
byte_length = 4
}
resource "aws_iam_user" "test" {
count = var.iam_user_count
name = random_id.user_name[count.index].dec
force_destroy = true
tags = local.tags
}
resource "aws_iam_access_key" "test" {
count = var.iam_user_count
user = aws_iam_user.test[count.index].name
}
resource "aws_iam_user_policy_attachment" "test_s3" {
count = var.iam_user_count
user = aws_iam_user.test[count.index].name
policy_arn = aws_iam_policy.valid.arn
}
resource "aws_iam_policy" "credentials" {
count = var.iam_user_count
name = aws_iam_user.test[count.index].name
tags = local.tags
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"iam:DeleteAccessKey",
"iam:GetUser",
"iam:CreateAccessKey"
],
"Effect": "Allow",
"Resource": "${aws_iam_user.test[count.index].arn}"
}
]
}
EOF
}
resource "aws_iam_user_policy_attachment" "test_credentials" {
count = var.iam_user_count
user = aws_iam_user.test[count.index].name
policy_arn = aws_iam_policy.credentials[count.index].arn
}
# Edge case for missing GetObject permission
resource "random_id" "missing_get_obj" {
prefix = random_id.prefix.dec
byte_length = 4
}
resource "aws_iam_user" "missing_get_obj" {
name = random_id.missing_get_obj.dec
force_destroy = true
tags = local.tags
}
resource "aws_iam_access_key" "missing_get_obj" {
user = aws_iam_user.missing_get_obj.name
}
resource "aws_iam_user_policy_attachment" "missing_get_obj" {
user = aws_iam_user.missing_get_obj.name
policy_arn = aws_iam_policy.missing_get_obj.arn
}
# Edge case for missing PutObject permission
resource "random_id" "missing_put_obj" {
prefix = random_id.prefix.dec
byte_length = 4
}
resource "aws_iam_user" "missing_put_obj" {
name = random_id.missing_put_obj.dec
force_destroy = true
tags = local.tags
}
resource "aws_iam_access_key" "missing_put_obj" {
user = aws_iam_user.missing_put_obj.name
}
resource "aws_iam_user_policy_attachment" "missing_put_obj" {
user = aws_iam_user.missing_put_obj.name
policy_arn = aws_iam_policy.missing_put_obj.arn
}
+11
View File
@@ -0,0 +1,11 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
+56
View File
@@ -0,0 +1,56 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
output "bucket_name" {
value = aws_s3_bucket.test.id
}
output "valid_policy_arn" {
value = aws_iam_role.valid.arn
}
output "missing_put_obj_policy_arn" {
value = aws_iam_role.missing_put_obj.arn
}
output "missing_put_get_policy_arn" {
value = aws_iam_role.missing_get_obj.arn
}
output "iam_user_names" {
value = aws_iam_user.test.*.name
}
output "iam_user_arns" {
value = aws_iam_user.test.*.arn
}
output "iam_access_key_ids" {
value = aws_iam_access_key.test.*.id
sensitive = true
}
output "iam_secret_access_keys" {
value = aws_iam_access_key.test.*.secret
sensitive = true
}
output "iam_access_key_missing_put_obj" {
value = aws_iam_access_key.missing_put_obj.id
sensitive = true
}
output "iam_secret_access_key_missing_put_obj" {
value = aws_iam_access_key.missing_put_obj.secret
sensitive = true
}
output "iam_access_key_missing_get_obj" {
value = aws_iam_access_key.missing_get_obj.id
sensitive = true
}
output "iam_secret_access_key_missing_get_obj" {
value = aws_iam_access_key.missing_get_obj.secret
sensitive = true
}
+9
View File
@@ -0,0 +1,9 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
resource "aws_s3_bucket" "test" {
bucket = random_id.prefix.dec
force_destroy = true
acl = "private"
tags = local.tags
}
+1
View File
@@ -0,0 +1 @@
HELLO_WORLD!
+20
View File
@@ -0,0 +1,20 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
data "aws_caller_identity" "current" {}
resource "random_id" "prefix" {
prefix = "storage-e2e-test"
byte_length = 4
}
variable "iam_user_count" {
default = 6
}
locals {
tags = {
user_id = split(":", data.aws_caller_identity.current.user_id)[1]
repo = "boundary-plugin-aws"
}
}
+212
View File
@@ -4,15 +4,26 @@
package testing
import (
"context"
"encoding/json"
"errors"
"fmt"
"os/exec"
"sync"
"testing"
"time"
"github.com/aws/smithy-go"
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
"github.com/hashicorp/boundary-plugin-aws/internal/values"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
tfjson "github.com/hashicorp/terraform-json"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
)
const expectedIamUserCount = 6
// TestTerraformer is an object representing the lifecycle of a
// particular Terraform project.
//
@@ -264,3 +275,204 @@ func (t *TestTerraformer) showJson() ([]byte, error) {
return output, nil
}
func requireCredentialsInvalid(t *testing.T, accessKeyId, secretAccessKey string) {
t.Helper()
require := require.New(t)
c, err := awsutil.NewCredentialsConfig(
awsutil.WithAccessKey(accessKeyId),
awsutil.WithSecretKey(secretAccessKey),
)
require.NoError(err)
// We need to wait for invalidation as while awsutil waits for
// credential creation, deletion of the old credentials returns
// immediately.
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
waitErr:
for {
_, err = c.GetCallerIdentity(context.Background())
if err != nil {
break
}
select {
case <-time.After(time.Second):
// pass
case <-timeoutCtx.Done():
break waitErr
}
}
require.NotNil(err)
var ae smithy.APIError
require.True(errors.As(err, &ae))
require.Equal("InvalidClientTokenId", ae.ErrorCode())
}
func requireCredentialsValid(t *testing.T, accessKeyId, secretAccessKey string) {
t.Helper()
require := require.New(t)
c, err := awsutil.NewCredentialsConfig(
awsutil.WithAccessKey(accessKeyId),
awsutil.WithSecretKey(secretAccessKey),
)
require.NoError(err)
_, err = c.GetCallerIdentity(context.Background())
require.NoError(err)
}
func validatePersistedSecrets(t *testing.T, persistedSecrets *structpb.Struct, accessKeyId, secretAccessKey string, rotate bool) (string, string) {
t.Helper()
require := require.New(t)
require.NotNil(persistedSecrets)
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
require.NoError(err)
require.NotZero(persistedAccessKeyId)
if rotate {
require.NotEqual(accessKeyId, persistedAccessKeyId)
} else {
require.Equal(accessKeyId, persistedAccessKeyId)
}
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
require.NoError(err)
require.NotZero(persistedSecretAccessKey)
if rotate {
require.NotEqual(secretAccessKey, persistedSecretAccessKey)
} else {
require.Equal(secretAccessKey, persistedSecretAccessKey)
}
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
require.NoError(err)
if rotate {
require.NotZero(persistedCredsLastRotatedTime)
requireCredentialsInvalid(t, accessKeyId, secretAccessKey)
} else {
require.Zero(persistedCredsLastRotatedTime)
}
return persistedAccessKeyId, persistedSecretAccessKey
}
func validateUpdateSecrets(
t *testing.T, persistedSecrets *structpb.Struct, currentCredsLastRotatedTime time.Time,
currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey string,
rotated, rotate bool) (string, string) {
t.Helper()
require := require.New(t)
// Complex checks based on the scenarios.
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
require.NoError(err)
require.NotZero(persistedAccessKeyId)
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
require.NoError(err)
require.NotZero(persistedSecretAccessKey)
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
require.NoError(err)
// Our test scenarios are complex due the multi-dimensional nature
// of criteria, so we lay them out in a switch below.
switch {
case newAccessKeyId != "" && rotated && rotate:
// The new access key ID was provided, we had previously rotated
// the credentials before, and the new credential set is to be
// rotated as well. In this case, the old credentials should have
// been deleted, and the new credentials should have been rotated,
// hence, should not match the new credentials initially
// provided. Rotation time should be non-zero and updated.
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
require.NotZero(persistedCredsLastRotatedTime)
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
case newAccessKeyId != "" && rotated && !rotate:
// The new access key ID was provided, we had previously rotated
// the credentials before, and the new credential is *not*
// rotated. In this case, the old credentials should have
// been deleted, But the new credentials should have not been
// rotated, and hence should be the same. Rotation time should be
// zero.
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
require.Equal(persistedAccessKeyId, newAccessKeyId)
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
require.Zero(persistedCredsLastRotatedTime)
case newAccessKeyId != "" && !rotated && rotate:
// The new access key ID was provided, we *have not* previously
// rotated the credentials, and the new credential set is to be
// rotated. In this case, the old credentials should have been
// left alone, and the new credentials should have been rotated,
// hence, should not match the new credentials initially
// provided. Rotation time should be non-zero, but updated.
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
require.NotZero(persistedCredsLastRotatedTime)
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
case newAccessKeyId != "" && !rotated && !rotate:
// The new access key ID was provided, but we have not rotated
// the credentials previously and we still don't plan on rotating
// them. In this case, the old credentials should still be valid,
// and the persisted ones should match the new ones provided.
// Rotation time should be zero.
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
require.Equal(persistedAccessKeyId, newAccessKeyId)
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
require.Zero(persistedCredsLastRotatedTime)
case newAccessKeyId == "" && rotated && rotate:
// No new credentials have been provided, but we have previously
// rotated and are still rotating credentials. This is a no-op.
// Existing credentials should still be valid and match the ones
// persisted to state. Rotation time should be identical since
// no new rotation occurred.
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
require.Equal(persistedAccessKeyId, currentAccessKeyId)
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
require.NotZero(persistedCredsLastRotatedTime)
require.True(currentCredsLastRotatedTime.Equal(persistedCredsLastRotatedTime))
case newAccessKeyId == "" && rotated && !rotate:
// No new credentials have been provided, and we have previously
// rotated the credentials. This is actually an error, but we
// don't test it here; it's covered in unit testing (see
// plugin_test.go).
require.FailNow("testing rotated-to-not-rotated scenario not implemented by this helper")
case newAccessKeyId == "" && !rotated && rotate:
// No new credentials have been provided, and while we did not
// rotate before, we want to switch to rotation. In this case,
// the existing persisted credentials should have been rotated,
// with a new non-zero timestamp.
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
require.NotEqual(persistedAccessKeyId, currentAccessKeyId)
require.NotEqual(persistedSecretAccessKey, currentSecretAccessKey)
require.NotZero(persistedCredsLastRotatedTime)
case newAccessKeyId == "" && !rotated && !rotate:
// No new credentials have been provided and we have not, nor do
// not, plan on rotating the credentials. This is a no-op.
// Existing credentials should still be valid and match the ones
// persisted to state. Rotation time should remain at zero.
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
require.Equal(persistedAccessKeyId, currentAccessKeyId)
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
require.Zero(persistedCredsLastRotatedTime)
default:
// Scenario was reached that was not covered by this function.
require.FailNow("unknown test scenario")
}
return persistedAccessKeyId, persistedSecretAccessKey
}