Compare commits

..
Author SHA1 Message Date
Hugo 7fa6b7e7a3 wip: Dynamic Creds tests 2023-08-23 12:23:36 +01:00
Damian Debkowski d970294995 refactor: switch from awsutil to awsutilv2 2023-08-08 23:14:11 -07:00
40 changed files with 1689 additions and 5998 deletions

No files matched your search

+2 -2
View File
@@ -13,9 +13,9 @@ jobs:
id-token: write
contents: read
steps:
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- name: Set up Go
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
with:
go-version-file: 'go.mod'
cache: true
-6
View File
@@ -4,9 +4,3 @@ plan.tfplan
*.log
*.out
*.html
# IntelliJ IDEA project files
.idea
*.iprg
.DS_Store
+2 -1
View File
@@ -1,4 +1,5 @@
# These owners will be the default owners for everything in
# the repo, unless a later match takes precedence.
* @hashicorp/boundary
@hashicorp/boundary
+4 -4
View File
@@ -17,7 +17,7 @@ and then the given credential will be revoked. In this way, after rotation,
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
Credential rotation can be turned off by setting the
`disable_credential_rotation` attribute to `true`.
### Assume Role Credentials
@@ -48,7 +48,7 @@ The plugin fetches hosts through the
[DescribeInstances](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html)
call.
[Getting Started](plugin/service/host/README.md)
[Getting Started](https://github.com/hashicorp/boundary-plugin-aws/plugin/service/host/README.md)
## Storage Bucket
@@ -60,7 +60,7 @@ these storage bucket resources can in turn be associated to targets within
Boundary.
At creation, update or deletion of a storage bucket of this type, configuration of the
plugin is performed via the attribute/secret values passed to the create, update, or
plugin is performed via the attribute/secret values passed to the create, update, or
delete calls actions. The values passed in to the plugin here are the attributes set
on on a storage bucket in boundary.
@@ -76,4 +76,4 @@ The plugin fetches metadata about the files through the
[HeadObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html)
call.
[Getting Started](plugin/service/storage/README.md)
[Getting Started](https://github.com/hashicorp/boundary-plugin-aws/plugin/service/storage/README.md)
+48 -45
View File
@@ -1,64 +1,67 @@
module github.com/hashicorp/boundary-plugin-aws
go 1.23.3
go 1.20
require (
github.com/aws/aws-sdk-go-v2 v1.32.5
github.com/aws/aws-sdk-go-v2/service/ec2 v1.193.0
github.com/aws/aws-sdk-go-v2/service/iam v1.38.1
github.com/aws/aws-sdk-go-v2/service/s3 v1.69.0
github.com/aws/aws-sdk-go-v2/service/sts v1.33.1
github.com/aws/smithy-go v1.22.1
github.com/google/go-cmp v0.6.0
github.com/google/uuid v1.6.0
github.com/hashicorp/boundary/sdk v0.0.49
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.1.0
github.com/hashicorp/terraform-json v0.23.0
github.com/aws/aws-sdk-go v1.44.80
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.2.2
github.com/hashicorp/terraform-json v0.14.0
github.com/mitchellh/mapstructure v1.5.0
github.com/stretchr/testify v1.10.0
google.golang.org/grpc v1.68.0
google.golang.org/protobuf v1.35.2
github.com/stretchr/testify v1.8.4
google.golang.org/grpc v1.53.0
google.golang.org/protobuf v1.28.1
)
replace github.com/hashicorp/go-secure-stdlib/awsutil v0.2.2 => github.com/hashicorp/go-secure-stdlib/awsutilv2 v0.0.0-20230809054404-9029211590fd
require (
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect
github.com/aws/aws-sdk-go-v2/config v1.28.5 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.46 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.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.15.0 // indirect
github.com/fatih/color v1.14.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/eventlogger v0.2.6-0.20231025104552-802587e608f0 // indirect
github.com/hashicorp/eventlogger/filters/encrypt v0.1.8-0.20231025104552-802587e608f0 // indirect
github.com/hashicorp/eventlogger v0.1.2-0.20230428153751-cca445805f24 // indirect
github.com/hashicorp/eventlogger/filters/encrypt v0.1.8-0.20230428153751-cca445805f24 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-kms-wrapping/v2 v2.0.14 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
github.com/hashicorp/go-kms-wrapping/v2 v2.0.9 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/go-version v1.5.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
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/pmezard/go-difflib v1.0.0 // indirect
github.com/zclconf/go-cty v1.15.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
github.com/zclconf/go-cty v1.10.0 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+141 -129
View File
@@ -1,140 +1,136 @@
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
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/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go-v2 v1.32.5 h1:U8vdWJuY7ruAkzaOdD7guwJjD06YSKmnKCJs7s3IkIo=
github.com/aws/aws-sdk-go-v2 v1.32.5/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc=
github.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0=
github.com/aws/aws-sdk-go-v2/config v1.28.5/go.mod h1:4VsPbHP8JdcdUDmbTVgNL/8w9SqOkM5jyY8ljIxLO3o=
github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg=
github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 h1:4usbeaes3yJnCFC7kfeyhkdkPtoRYPa/hTmCqMpKpLI=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24/go.mod h1:5CI1JemjVwde8m2WG3cz23qHKPOxbpkq0HaoreEgLIY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 h1:N1zsICrQglfzaBnrfM0Ys00860C+QFwu6u/5+LomP+o=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24/go.mod h1:dCn9HbJ8+K31i8IQ8EWmWj0EiIk0+vKiHNMxTTYveAg=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24 h1:JX70yGKLj25+lMC5Yyh8wBtvB01GDilyRuJvXJ4piD0=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24/go.mod h1:+Ln60j9SUTD0LEwnhEB0Xhg61DHqplBrbZpLgyjoEHg=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.193.0 h1:RhSoBFT5/8tTmIseJUXM6INTXTQDF8+0oyxWBnozIms=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.193.0/go.mod h1:mzj8EEjIHSN2oZRXiw1Dd+uB4HZTl7hC8nBzX9IZMWw=
github.com/aws/aws-sdk-go-v2/service/iam v1.38.1 h1:hfkzDZHBp9jAT4zcd5mtqckpU4E3Ax0LQaEWWk1VgN8=
github.com/aws/aws-sdk-go-v2/service/iam v1.38.1/go.mod h1:u36ahDtZcQHGmVm/r+0L1sfKX4fzLEMdCqiKRKkUMVM=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5 h1:gvZOjQKPxFXy1ft3QnEyXmT+IqneM9QAUWlM3r0mfqw=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5/go.mod h1:DLWnfvIcm9IET/mmjdxeXbBKmTCm0ZB8p1za9BVteM8=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 h1:wtpJ4zcwrSbwhECWQoI/g6WM9zqCcSpHDJIWSbMLOu4=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5/go.mod h1:qu/W9HXQbbQ4+1+JcZp0ZNPV31ym537ZJN+fiS7Ti8E=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5 h1:P1doBzv5VEg1ONxnJss1Kh5ZG/ewoIE4MQtKKc6Crgg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5/go.mod h1:NOP+euMW7W3Ukt28tAxPuoWao4rhhqJD3QEBk7oCg7w=
github.com/aws/aws-sdk-go-v2/service/s3 v1.69.0 h1:Q2ax8S21clKOnHhhr933xm3JxdJebql+R7aNo7p7GBQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.69.0/go.mod h1:ralv4XawHjEMaHOWnTFushl0WRqim/gQWesAMF6hTow=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 h1:3zu537oLmsPfDMyjnUS2g+F2vITgy5pB74tHI+JBNoM=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.6/go.mod h1:WJSZH2ZvepM6t6jwu4w/Z45Eoi75lPN7DcydSRtJg6Y=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 h1:K0OQAsDywb0ltlFrZm0JHPY3yZp/S9OaoLU33S7vPS8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5/go.mod h1:ORITg+fyuMoeiQFiVGoqB3OydVTLkClw/ljbblMq6Cc=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 h1:6SZUVRQNvExYlMLbHdlKB48x0fLbc2iVROyaNEwBHbU=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyNSPqPLQJvmo8J0DWBzp9mtg=
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
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.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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
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.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
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-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.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/boundary/sdk v0.0.49 h1:XOb6mSKyrU/wI20+5xTYBHQUP7eIeKcLxKSCpCs4yzM=
github.com/hashicorp/boundary/sdk v0.0.49/go.mod h1:IHP79to8aIi22FiY58pgBqJL96/U9D8ZAUhS2DdC+Us=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/boundary/sdk v0.0.33 h1:kHaVZ3b9j1QvYlG+vhAgrbXh8/+Rq2qlUIs8fKMQH7k=
github.com/hashicorp/boundary/sdk v0.0.33/go.mod h1:jn9j5mM8v2pOk8aLeJNIszdm7WBf4gRSP68iy8Iu5Q0=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/eventlogger v0.2.6-0.20231025104552-802587e608f0 h1:f9oX8/3zxiQrfrWnBeyjDm4S02GAU02OBtCRoZOUwlo=
github.com/hashicorp/eventlogger v0.2.6-0.20231025104552-802587e608f0/go.mod h1://CHt6/j+Q2lc0NlUB5af4aS2M0c0aVBg9/JfcpAyhM=
github.com/hashicorp/eventlogger/filters/encrypt v0.1.8-0.20231025104552-802587e608f0 h1:iAb287bq0TaWTnhDYuN/zVqdD2EwanQg9ncVelC60Xc=
github.com/hashicorp/eventlogger/filters/encrypt v0.1.8-0.20231025104552-802587e608f0/go.mod h1:tMywUTIvdB/FXhwm6HMTt61C8/eODY6gitCHhXtyojg=
github.com/hashicorp/eventlogger v0.1.2-0.20230428153751-cca445805f24 h1:vtCJgcxwDMFrpUgVMfjtwXY1YqvHbj8pNA7GsXYEoLg=
github.com/hashicorp/eventlogger v0.1.2-0.20230428153751-cca445805f24/go.mod h1://CHt6/j+Q2lc0NlUB5af4aS2M0c0aVBg9/JfcpAyhM=
github.com/hashicorp/eventlogger/filters/encrypt v0.1.8-0.20230428153751-cca445805f24 h1:mopKPTxSaVr24uSysyRwywTDs85L5b05qqZvEkWa0qc=
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 v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.5 h1:jrnDfQm2hCQ0/hEselgqzV4fK16gpZoY0OWGZpVPNHM=
github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.5/go.mod h1:psh1qKep5ukvuNobFY/hCybuudlkkACpmazOsCgX5Rg=
github.com/hashicorp/go-kms-wrapping/v2 v2.0.14 h1:1ZuhfnZgRnLK8S0KovJkoTCRIQId5pv3sDR7pG5VQBw=
github.com/hashicorp/go-kms-wrapping/v2 v2.0.14/go.mod h1:0dWtzl2ilqKpavgM3id/kFK9L3tjo6fS4OhbVPSYpnQ=
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=
github.com/hashicorp/go-kms-wrapping/v2 v2.0.9 h1:JpCvi97NMA+saNqO8ovQcGoRbBq6P5ZZlJqvOsW5ick=
github.com/hashicorp/go-kms-wrapping/v2 v2.0.9/go.mod h1:NtMaPhqSlfQ72XWDD2g80o8HI8RKkowIB8/WZHMyPY4=
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.5.2 h1:aWv8eimFqWlsEiMrYZdPYl+FdHaBJSN4AWwGWfT1G2Y=
github.com/hashicorp/go-plugin v1.5.2/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4=
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.1.0 h1:V3TJFolOHYSDqQLbTUBygXtbX4jKXyBcDoU+KNZE1Ak=
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.1.0/go.mod h1:OeRwM2eWNW62L1Z+8GvoZM5nQJMRWBewHSoo77qmb4Y=
github.com/hashicorp/go-plugin v1.4.9 h1:ESiK220/qE0aGxWdzKIvRH69iLiuN/PjoLTm69RoWtU=
github.com/hashicorp/go-secure-stdlib/awsutilv2 v0.0.0-20230809054404-9029211590fd h1:46A3lfywoivyHBOXlXioGDSDo3UJj2aW2ncnSCSJ7bs=
github.com/hashicorp/go-secure-stdlib/awsutilv2 v0.0.0-20230809054404-9029211590fd/go.mod h1:XnoCjGsUi+UKk0+0P5TIS2rGKdw7O9ehSJy+kIuffBY=
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/base62 v0.1.2/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw=
github.com/hashicorp/go-secure-stdlib/configutil/v2 v2.0.11 h1:uPW2Wn0YlmI9RGSkZpcIplnVRwJ7BCiGpk1vnF2TMw4=
github.com/hashicorp/go-secure-stdlib/configutil/v2 v2.0.11/go.mod h1:uis9dCmOzXuOaRyXq+1Foh31kcvXKoWogjNnhfjHfW8=
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.9 h1:0S0ctJ7Ra8O7ap+/3fZUnzJ3VzJyirWS/WnNCuOYtZY=
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.9/go.mod h1:TNNdgtjLgVDbrgFcyCKrlAicIl3dZF94swJltyGUX2M=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 h1:iBt4Ew4XEGLfh6/bPk4rSYmuZJGizr6/x/AEizP0CQc=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8/go.mod h1:aiJI+PIApBRQG7FZTEBx5GiiX+HbOHilUdNxUZi4eV0=
github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.6 h1:ZYv2XA+tEfFXIToR2jmBgVqQU9gERt0APbWqmUoNGnY=
github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.6/go.mod h1:ggFN8dlaLWS2R1gymBbCrvXM/bkZP7hEAa4seqDwhyg=
github.com/hashicorp/go-secure-stdlib/configutil/v2 v2.0.9 h1:YZ01/aiARPneDUryzjiPGvr3pmjCSC+1WBZKI1P7I7k=
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.6 h1:rOmnqOSUBn5opme1Kh9OXO8qWd0KztO+f02vvCwCc/w=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs=
github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.4 h1:IU2iGcvthrJ53rPbQU6B4+iVs/cLPO89ti5dd2XEj3k=
github.com/hashicorp/go-secure-stdlib/reloadutil v0.1.1 h1:SMGUnbpAcat8rIKHkBPjfv81yC46a8eCNZ2hsR2l1EI=
github.com/hashicorp/go-secure-stdlib/reloadutil v0.1.1/go.mod h1:Ch/bf00Qnx77MZd49JRgHYqHQjtEmTgGU2faufpVZb0=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.3 h1:xbrxd0U9XQW8qL1BAz2XrAjAF/P2vcqUTAues9c24B8=
github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.3/go.mod h1:LWq2Sy8UoKKuK4lFuCNWSjJj57MhNNf2zzBWMtkAIX4=
github.com/hashicorp/go-sockaddr v1.0.5 h1:dvk7TIXCZpmfOlM+9mlcrWmWjw/wlKT+VDq2wMvfPJU=
github.com/hashicorp/go-sockaddr v1.0.5/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI=
github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.2 h1:phcbL8urUzF/kxA/Oj6awENaRwfWsjP59GW7u2qlDyY=
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.5.0 h1:O293SZ2Eg+AAYijkVK3jR786Am1bhDEh2GHT0tIVE5E=
github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/terraform-json v0.23.0 h1:sniCkExU4iKtTADReHzACkk8fnpQXrdD2xoR+lppBkI=
github.com/hashicorp/terraform-json v0.23.0/go.mod h1:MHdXbBAbSg0GvzuWazEGKAn/cyNfIB7mN6y7KJN6y2c=
github.com/hashicorp/terraform-json v0.14.0 h1:sh9iZ1Y8IFJLx+xQiKHGud6/TSUCM0N8e17dKDpqV7s=
github.com/hashicorp/terraform-json v0.14.0/go.mod h1:5A9HIWPkk4e5aeeXIBbkcOvaZbIYnAIkEyqP2pNSckM=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f h1:E87tDTVS5W65euzixn7clSzK66puSt1H4I5SC0EmHH4=
github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f/go.mod h1:3J2qVK16Lq8V+wfiL2lPeDZ7UWMxk5LemerHa1p6N00=
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=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
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.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=
@@ -142,14 +138,12 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
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=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng=
github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
@@ -158,48 +152,56 @@ 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/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
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/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ=
github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/zclconf/go-cty v1.10.0 h1:mp9ZXQeIcN8kAwuqorjH+Q+njbJKjLrvB2yIh4q7U+0=
github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
go.uber.org/goleak v1.0.0 h1:qsup4IcBdlmsnGfqyLl4Ntn3C2XCCuKAE7DwHpScyUo=
go.uber.org/goleak v1.0.0/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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-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.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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=
@@ -207,41 +209,51 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
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-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=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488 h1:QQF+HdiI4iocoxUjjpLgvTYDHKm99C/VtTBFnfiCJos=
google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA=
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+30 -39
View File
@@ -6,9 +6,10 @@ package credential
import (
"fmt"
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/hashicorp/boundary-plugin-aws/internal/errors"
"github.com/hashicorp/boundary-plugin-aws/internal/values"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil"
"google.golang.org/protobuf/types/known/structpb"
)
@@ -34,10 +35,9 @@ type CredentialAttributes struct {
}
// GetCredentialsConfig parses values out of a protobuf struct secrets and returns a
// CredentialsConfig used for configuring an AWS session. An status error is returned
// with an InvalidArgument code if any unrecognized fields are found in the protobuf
// struct input.
func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes, dualStack bool) (*awsutil.CredentialsConfig, error) {
// CredentialsConfig used for configuring an AWS session. An error is returned if
// any unrecognized fields are found in the protobuf struct input.
func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes, required bool) (*awsutilv2.CredentialsConfig, error) {
// initialize secrets if it is nil
// secrets can be nil because static credentials are optional
if secrets == nil {
@@ -49,13 +49,17 @@ func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes,
unknownFields := values.StructFields(secrets)
badFields := make(map[string]string)
accessKey, err := values.GetStringValue(secrets, ConstAccessKeyId, false)
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(secrets, ConstSecretAccessKey, false)
if accessKey != "" && GetCredentialType(accessKey) != Static {
badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = "requires long-term access key beginning with \"AKIA\" prefix"
}
secretKey, err := values.GetStringValue(secrets, ConstSecretAccessKey, required)
if err != nil {
badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = err.Error()
}
@@ -68,7 +72,7 @@ func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes,
badFields[fmt.Sprintf("secrets.%s", s)] = "unrecognized field"
}
opts := []awsutil.Option{}
opts := []awsutilv2.Option{}
// logic for parsing the credential type
// supported types:
// - user static credential
@@ -79,25 +83,22 @@ func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes,
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"
badFields[fmt.Sprintf("secrets.%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),
awsutilv2.WithAccessKey(accessKey),
awsutilv2.WithSecretKey(secretKey),
)
// add dynamic credentials
case attrs.RoleArn != "":
opts = append(opts, awsutil.WithRoleArn(attrs.RoleArn))
opts = append(opts, awsutilv2.WithRoleArn(attrs.RoleArn))
}
if len(badFields) > 0 {
@@ -105,41 +106,25 @@ func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes,
}
if attrs.Region != "" {
opts = append(opts, awsutil.WithRegion(attrs.Region))
opts = append(opts, awsutilv2.WithRegion(attrs.Region))
}
if attrs.RoleExternalId != "" {
opts = append(opts, awsutil.WithRoleExternalId(attrs.RoleExternalId))
opts = append(opts, awsutilv2.WithRoleExternalId(attrs.RoleExternalId))
}
if attrs.RoleSessionName != "" {
opts = append(opts, awsutil.WithRoleSessionName(attrs.RoleSessionName))
opts = append(opts, awsutilv2.WithRoleSessionName(attrs.RoleSessionName))
}
if len(attrs.RoleTags) != 0 {
opts = append(opts, awsutil.WithRoleTags(attrs.RoleTags))
opts = append(opts, awsutilv2.WithRoleTags(attrs.RoleTags))
}
if dualStack {
opts = append(opts, []awsutil.Option{
awsutil.WithIamEndpointResolver(&iamEndpointResolver{
dualStack: dualStack,
}),
awsutil.WithStsEndpointResolver(&stsEndpointResolver{
dualStack: dualStack,
}),
}...)
}
cfg, err := awsutil.NewCredentialsConfig(opts...)
if err != nil {
return nil, errors.BadRequestStatus(err.Error())
}
return cfg, nil
return awsutilv2.NewCredentialsConfig(opts...)
}
// GetCredentialAttributes parses values out of a protobuf struct input and returns a
// CredentialAttributes used for configuring an AWS session. An status error is returned
// with an InvalidArgument code if any of the following fields are missing from the protobuf
// struct input or have invalid value types: region, disableCredentialRotation
// CredentialAttributes 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: region, disableCredentialRotation
func GetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error) {
badFields := make(map[string]string)
@@ -148,6 +133,12 @@ func GetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error)
badFields[fmt.Sprintf("attributes.%s", ConstRegion)] = err.Error()
}
if region != "" {
if _, found := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), region); !found {
badFields[fmt.Sprintf("attributes.%s", ConstRegion)] = fmt.Sprintf("not a valid region: %s", region)
}
}
disableCredentialRotation, err := values.GetBoolValue(in, ConstDisableCredentialRotation, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstDisableCredentialRotation)] = err.Error()
+44 -38
View File
@@ -6,7 +6,7 @@ package credential
import (
"testing"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -25,6 +25,13 @@ func TestGetCredentialAttributes(t *testing.T) {
in: map[string]any{},
expectedErrContains: "missing required value \"region\"",
},
{
name: "invalid aws region value",
in: map[string]any{
ConstRegion: "dne",
},
expectedErrContains: "not a valid region",
},
{
name: "bad value for disable_credential_rotation",
in: map[string]any{
@@ -105,9 +112,9 @@ func TestGetCredentialsConfig(t *testing.T) {
cases := []struct {
name string
secrets *structpb.Struct
required bool
attrs *CredentialAttributes
dualStack bool
expected *awsutil.CredentialsConfig
expected *awsutilv2.CredentialsConfig
expectedErrContains string
}{
{
@@ -115,10 +122,23 @@ func TestGetCredentialsConfig(t *testing.T) {
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &awsutil.CredentialsConfig{
expected: &awsutilv2.CredentialsConfig{
Region: "us-west-2",
},
},
{
name: "with invalid static credentials",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("ASIAfoobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expectedErrContains: `secrets.access_key_id: requires long-term access key beginning with "AKIA" prefix`,
},
{
name: "with static credentials",
secrets: &structpb.Struct{
@@ -130,29 +150,37 @@ func TestGetCredentialsConfig(t *testing.T) {
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &awsutil.CredentialsConfig{
expected: &awsutilv2.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
},
{
name: "with dualstack",
name: "missing access key",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
dualStack: true,
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &awsutil.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
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",
@@ -181,7 +209,7 @@ func TestGetCredentialsConfig(t *testing.T) {
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &awsutil.CredentialsConfig{
expected: &awsutilv2.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
Region: "us-west-2",
@@ -200,9 +228,8 @@ func TestGetCredentialsConfig(t *testing.T) {
RoleTags: map[string]string{
"foo": "bar",
},
DisableCredentialRotation: true,
},
expected: &awsutil.CredentialsConfig{
expected: &awsutilv2.CredentialsConfig{
Region: "us-west-2",
RoleARN: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
@@ -229,21 +256,7 @@ func TestGetCredentialsConfig(t *testing.T) {
"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",
expectedErrContains: "secrets.access_key_id: conflicts with role_arn value, secrets.role_arn: conflicts with access_key_id and secret_access_key values, secrets.secret_access_key: conflicts with role_arn value",
},
}
@@ -252,7 +265,7 @@ func TestGetCredentialsConfig(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
actual, err := GetCredentialsConfig(tc.secrets, tc.attrs, tc.dualStack)
actual, err := GetCredentialsConfig(tc.secrets, tc.attrs, tc.required)
if tc.expectedErrContains != "" {
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
@@ -268,13 +281,6 @@ func TestGetCredentialsConfig(t *testing.T) {
require.Equal(tc.expected.RoleExternalId, actual.RoleExternalId)
require.Equal(tc.expected.RoleSessionName, actual.RoleSessionName)
require.Equal(tc.expected.RoleTags, actual.RoleTags)
if tc.dualStack {
require.NotNil(actual.IAMEndpointResolver)
require.NotNil(actual.STSEndpointResolver)
} else {
require.Nil(actual.IAMEndpointResolver)
require.Nil(actual.STSEndpointResolver)
}
})
}
}
+68 -128
View File
@@ -5,42 +5,24 @@ package credential
import (
"context"
stderr "errors"
"errors"
"fmt"
"strings"
"time"
"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/aws/aws-sdk-go-v2/service/sts"
smithyEndpoints "github.com/aws/smithy-go/endpoints"
"github.com/hashicorp/boundary-plugin-aws/internal/errors"
"github.com/hashicorp/boundary-plugin-aws/internal/values"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil"
"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
Unknown CredentialType = iota
Static
Dynamic
)
// rotationWaitTimeout controls the time we wait for credential rotation to
@@ -53,18 +35,18 @@ type AwsCredentialPersistedStateOption func(s *AwsCredentialPersistedState) erro
// AwsCredentialPersistedState is the persisted state for the AWS credential.
type AwsCredentialPersistedState struct {
// CredentialsConfig is the credential configuration for the AWS credential.
CredentialsConfig *awsutil.CredentialsConfig
CredentialsConfig *awsutilv2.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
testOpts []awsutilv2.Option
}
// WithStateTestOpts enables unit testing different edge cases
// when using CredentialsConfig. This should never be used in
// production code. This should only be used in unit tests.
func WithStateTestOpts(opts []awsutil.Option) AwsCredentialPersistedStateOption {
func WithStateTestOpts(opts []awsutilv2.Option) AwsCredentialPersistedStateOption {
return func(s *AwsCredentialPersistedState) error {
s.testOpts = opts
return nil
@@ -72,10 +54,10 @@ func WithStateTestOpts(opts []awsutil.Option) AwsCredentialPersistedStateOption
}
// WithCredentialsConfig sets the value for CredentialsConfig in the credential persisted state.
func WithCredentialsConfig(x *awsutil.CredentialsConfig) AwsCredentialPersistedStateOption {
func WithCredentialsConfig(x *awsutilv2.CredentialsConfig) AwsCredentialPersistedStateOption {
return func(s *AwsCredentialPersistedState) error {
if s.CredentialsConfig != nil {
return stderr.New("credentials config already set")
return errors.New("credentials config already set")
}
s.CredentialsConfig = x
@@ -87,7 +69,7 @@ func WithCredentialsConfig(x *awsutil.CredentialsConfig) AwsCredentialPersistedS
func WithCredsLastRotatedTime(t time.Time) AwsCredentialPersistedStateOption {
return func(s *AwsCredentialPersistedState) error {
if !s.CredsLastRotatedTime.IsZero() {
return stderr.New("last rotation time already set")
return errors.New("last rotation time already set")
}
s.CredsLastRotatedTime = t
@@ -110,15 +92,13 @@ func NewAwsCredentialPersistedState(opts ...AwsCredentialPersistedStateOption) (
// 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. This method returns a status error
// with PluginError details.
// to check that the credentials are valid.
func (s *AwsCredentialPersistedState) ValidateCreds(ctx context.Context) error {
if s.CredentialsConfig == nil {
return errors.BadRequestStatus("missing credentials config")
return errors.New("missing credentials config")
}
if _, err := s.CredentialsConfig.GetCallerIdentity(ctx, s.testOpts...); err != nil {
st, _ := errors.ParseAWSError("validating credentials", err)
return st.Err()
return fmt.Errorf("error validating credentials: %w", err)
}
return nil
}
@@ -127,19 +107,18 @@ func (s *AwsCredentialPersistedState) ValidateCreds(ctx context.Context) error {
// then deletes the old access key. If deletion of the old access key is successful, the new access key/secret key are
// 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. This method returns a status error with PluginError details.
// still result in an extraneous access key existing.
func (s *AwsCredentialPersistedState) RotateCreds(ctx context.Context) error {
if s.CredentialsConfig == nil {
return errors.BadRequestStatus("missing credentials config")
return errors.New("missing credentials config")
}
if GetCredentialType(s.CredentialsConfig) != StaticAWS {
return errors.BadRequestStatus("invalid credential type")
if GetCredentialType(s.CredentialsConfig.AccessKey) != Static {
return errors.New("invalid credential type")
}
if err := s.CredentialsConfig.RotateKeys(ctx, append([]awsutil.Option{
awsutil.WithValidityCheckTimeout(rotationWaitTimeout),
if err := s.CredentialsConfig.RotateKeys(ctx, append([]awsutilv2.Option{
awsutilv2.WithValidityCheckTimeout(rotationWaitTimeout),
}, s.testOpts...)...); err != nil {
st, _ := errors.ParseAWSError("rotating credentials", err)
return st.Err()
return fmt.Errorf("error rotating credentials: %w", err)
}
s.CredsLastRotatedTime = time.Now()
return nil
@@ -147,20 +126,21 @@ func (s *AwsCredentialPersistedState) RotateCreds(ctx context.Context) error {
// 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. This method returns a
// status error with PluginError details.
func (s *AwsCredentialPersistedState) ReplaceCreds(ctx context.Context, credentialsConfig *awsutil.CredentialsConfig) error {
// deleted first, otherwise it's left alone.
func (s *AwsCredentialPersistedState) ReplaceCreds(ctx context.Context, credentialsConfig *awsutilv2.CredentialsConfig) error {
if credentialsConfig == nil {
return errors.BadRequestStatus("missing new credentials config")
return errors.New("missing new credentials config")
}
if s.CredentialsConfig == nil {
return errors.BadRequestStatus("missing credentials config")
return errors.New("missing credentials config")
}
if GetCredentialType(s.CredentialsConfig.AccessKey) != Static {
return errors.New("invalid credential type")
}
// 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 !s.CredsLastRotatedTime.IsZero() {
if err := s.DeleteCreds(ctx); err != nil {
return err
}
@@ -174,29 +154,27 @@ func (s *AwsCredentialPersistedState) ReplaceCreds(ctx context.Context, credenti
// 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. This
// method returns a status error with PluginError details.
// the state just to ensure that they cannot be re-used after.
func (s *AwsCredentialPersistedState) DeleteCreds(ctx context.Context) error {
if s.CredentialsConfig == nil {
return errors.BadRequestStatus("missing credentials config")
return errors.New("missing credentials config")
}
if GetCredentialType(s.CredentialsConfig) != StaticAWS {
return errors.BadRequestStatus("invalid credential type")
if GetCredentialType(s.CredentialsConfig.AccessKey) != Static {
return errors.New("invalid credential type")
}
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 *iamTypes.NoSuchEntityException
if stderr.As(err, &awsErr) && awsErr.ErrorCode() == "NoSuchEntity" {
if errors.As(err, &awsErr) && awsErr.ErrorCode() == "NoSuchEntity" {
s.CredentialsConfig = nil
s.CredsLastRotatedTime = time.Time{}
return nil
}
// Otherwise treat it as an actual error.
st, _ := errors.ParseAWSError("deleting credentials", err)
return st.Err()
return err
}
s.CredentialsConfig = nil
@@ -206,13 +184,7 @@ func (s *AwsCredentialPersistedState) DeleteCreds(ctx context.Context) error {
// GenerateCredentialChain returns a AWS configuration for the credentials in the state.
func (s *AwsCredentialPersistedState) GenerateCredentialChain(ctx context.Context) (*aws.Config, error) {
// Default `WithSharedCredentials` is `true` - This disables the ability to
// read credentials from environment variables because we set the default
// profile options, so we need to set it to false (see
// awsutil@v2.(*CredentialsConfig).generateAwsConfigOptions) for extra
// details.
opts := append(s.testOpts, awsutil.WithSharedCredentials(false))
return s.CredentialsConfig.GenerateCredentialChain(ctx, opts...)
return s.CredentialsConfig.GenerateCredentialChain(ctx, s.testOpts...)
}
// ToMap returns a map of the credentials stored in the persisted state.
@@ -220,15 +192,7 @@ func (s *AwsCredentialPersistedState) GenerateCredentialChain(ctx context.Contex
// 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 {
if HasDynamicCredentials(s.CredentialsConfig.AccessKey) {
return map[string]any{}
}
return map[string]any{
@@ -240,7 +204,7 @@ func (s *AwsCredentialPersistedState) ToMap() map[string]any {
// AwsCredentialPersistedStateFromProto parses values out of a protobuf struct input
// and returns a AwsCredentialPersistedState used for configuring an AWS session.
func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *CredentialAttributes, dualStack bool, opts ...AwsCredentialPersistedStateOption) (*AwsCredentialPersistedState, error) {
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 {
@@ -255,17 +219,17 @@ func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *Crede
accessKeyId, err := values.GetStringValue(secrets, ConstAccessKeyId, false)
if err != nil {
return nil, fmt.Errorf("persisted state integrity: %w", err)
return nil, fmt.Errorf("persisted state integrity error: %w", err)
}
secretAccessKey, err := values.GetStringValue(secrets, ConstSecretAccessKey, false)
if err != nil {
return nil, fmt.Errorf("persisted state integrity: %w", err)
return nil, fmt.Errorf("persisted state integrity error: %w", err)
}
credsLastRotatedTime, err := values.GetTimeValue(secrets, ConstCredsLastRotatedTime)
if err != nil {
return nil, fmt.Errorf("persisted state integrity: %w", err)
return nil, fmt.Errorf("persisted state integrity error: %w", err)
}
s, err := NewAwsCredentialPersistedState(opts...)
@@ -273,40 +237,29 @@ func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *Crede
return nil, err
}
awsOpts := append([]awsutil.Option{}, s.testOpts...)
awsOpts := append([]awsutilv2.Option{}, s.testOpts...)
if accessKeyId != "" && secretAccessKey != "" {
awsOpts = append(awsOpts,
awsutil.WithAccessKey(accessKeyId),
awsutil.WithSecretKey(secretAccessKey),
awsutilv2.WithAccessKey(accessKeyId),
awsutilv2.WithSecretKey(secretAccessKey),
)
}
if attrs.Region != "" {
awsOpts = append(awsOpts, awsutil.WithRegion(attrs.Region))
awsOpts = append(awsOpts, awsutilv2.WithRegion(attrs.Region))
}
if attrs.RoleArn != "" {
awsOpts = append(awsOpts, awsutil.WithRoleArn(attrs.RoleArn))
awsOpts = append(awsOpts, awsutilv2.WithRoleArn(attrs.RoleArn))
}
if attrs.RoleExternalId != "" {
awsOpts = append(awsOpts, awsutil.WithRoleExternalId(attrs.RoleExternalId))
awsOpts = append(awsOpts, awsutilv2.WithRoleExternalId(attrs.RoleExternalId))
}
if attrs.RoleSessionName != "" {
awsOpts = append(awsOpts, awsutil.WithRoleSessionName(attrs.RoleSessionName))
awsOpts = append(awsOpts, awsutilv2.WithRoleSessionName(attrs.RoleSessionName))
}
if len(attrs.RoleTags) != 0 {
awsOpts = append(awsOpts, awsutil.WithRoleTags(attrs.RoleTags))
awsOpts = append(awsOpts, awsutilv2.WithRoleTags(attrs.RoleTags))
}
if dualStack {
awsOpts = append(awsOpts, []awsutil.Option{
awsutil.WithIamEndpointResolver(&iamEndpointResolver{
dualStack: dualStack,
}),
awsutil.WithStsEndpointResolver(&stsEndpointResolver{
dualStack: dualStack,
}),
}...)
}
credentialsConfig, err := awsutil.NewCredentialsConfig(awsOpts...)
credentialsConfig, err := awsutilv2.NewCredentialsConfig(awsOpts...)
if err != nil {
return nil, err
}
@@ -316,42 +269,29 @@ func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *Crede
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
// GetCredentialType returns the credential type based on the given AWS AccessKey.
//
// https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html#sec-access-keys-and-secret-access-keys
// Access key IDs beginning with ASIA are temporary credentials access keys that you create using AWS STS operations.
//
// https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html#sec-access-keys-and-secret-access-keys
// Access key IDs beginning with AKIA are long-term access keys for an IAM user or an AWS account root user.
func GetCredentialType(accessKey string) CredentialType {
if strings.HasPrefix(accessKey, "ASIA") {
return Dynamic
}
if len(cc.RoleARN) > 0 || strings.HasPrefix(cc.AccessKey, "ASIA") {
return DynamicAWS
if strings.HasPrefix(accessKey, "AKIA") {
return Static
}
if strings.HasPrefix(cc.AccessKey, "AKIA") {
return StaticAWS
}
if len(cc.AccessKey) > 0 {
return StaticOther
}
return Unknown
}
type iamEndpointResolver struct {
dualStack bool
// HasDynamicCredentials returns true if the access key is a dynamic credential type.
func HasDynamicCredentials(accessKey string) bool {
return strings.HasPrefix(accessKey, "ASIA")
}
func (e *iamEndpointResolver) ResolveEndpoint(ctx context.Context, params iam.EndpointParameters) (resolver smithyEndpoints.Endpoint, err error) {
params.UseDualStack = aws.Bool(e.dualStack)
resolver, err = iam.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params)
return
}
type stsEndpointResolver struct {
dualStack bool
}
func (e *stsEndpointResolver) ResolveEndpoint(ctx context.Context, params sts.EndpointParameters) (resolver smithyEndpoints.Endpoint, err error) {
params.UseDualStack = aws.Bool(e.dualStack)
resolver, err = sts.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params)
return
// HasStaticCredentials returns true if the access key is a static credential type.
func HasStaticCredentials(accessKey string) bool {
return strings.HasPrefix(accessKey, "AKIA")
}
+208 -217
View File
@@ -13,11 +13,8 @@ import (
"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/assert"
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb"
)
@@ -43,7 +40,7 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
name: "with credentials config",
opts: []AwsCredentialPersistedStateOption{
WithCredentialsConfig(
&awsutil.CredentialsConfig{
&awsutilv2.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
@@ -51,7 +48,7 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
),
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
@@ -70,8 +67,8 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
{
name: "double set credentials config",
opts: []AwsCredentialPersistedStateOption{
WithCredentialsConfig(&awsutil.CredentialsConfig{}),
WithCredentialsConfig(&awsutil.CredentialsConfig{}),
WithCredentialsConfig(&awsutilv2.CredentialsConfig{}),
WithCredentialsConfig(&awsutilv2.CredentialsConfig{}),
},
expectedErr: "credentials config already set",
},
@@ -112,7 +109,6 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
name string
secrets *structpb.Struct
attrs *CredentialAttributes
dualStack bool
opts []AwsCredentialPersistedStateOption
expected *AwsCredentialPersistedState
expectedErr string
@@ -127,7 +123,7 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
Region: "us-west-2",
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
Region: "us-west-2",
},
},
@@ -139,7 +135,7 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
Region: "us-west-2",
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
Region: "us-west-2",
},
},
@@ -151,7 +147,7 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
Region: "us-west-2",
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
Region: "us-west-2",
},
},
@@ -168,27 +164,7 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
Region: "us-west-2",
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
Region: "us-west-2",
AccessKey: "foobar",
SecretKey: "bazqux",
},
},
},
{
name: "with dualstack static credentials",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
dualStack: true,
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
Region: "us-west-2",
AccessKey: "foobar",
SecretKey: "bazqux",
@@ -207,7 +183,7 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expectedErr: "persisted state integrity: 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\"",
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",
@@ -241,7 +217,7 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
Region: "us-west-2",
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
@@ -268,7 +244,7 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
Region: "us-west-2",
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
@@ -288,7 +264,7 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
},
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
Region: "us-west-2",
RoleARN: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
@@ -318,7 +294,7 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
},
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
@@ -339,7 +315,7 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
actual, err := AwsCredentialPersistedStateFromProto(tc.secrets, tc.attrs, tc.dualStack, tc.opts...)
actual, err := AwsCredentialPersistedStateFromProto(tc.secrets, tc.attrs, tc.opts...)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
@@ -357,13 +333,6 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
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)
if tc.dualStack {
require.NotNil(actual.CredentialsConfig.STSEndpointResolver)
require.NotNil(actual.CredentialsConfig.IAMEndpointResolver)
} else {
require.Nil(actual.CredentialsConfig.STSEndpointResolver)
require.Nil(actual.CredentialsConfig.IAMEndpointResolver)
}
}
})
}
@@ -371,45 +340,42 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
func TestAwsCatalogPersistedState_ValidateCreds(t *testing.T) {
cases := []struct {
name string
in *AwsCredentialPersistedState
expectedStatusCode codes.Code
expectedErr string
name string
in *AwsCredentialPersistedState
expectedErr string
}{
{
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
expectedStatusCode: codes.InvalidArgument,
expectedErr: "missing credentials config",
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing credentials config",
},
{
name: "validation error",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
testOpts: []awsutil.Option{
awsutil.WithSTSAPIFunc(
awsutil.NewMockSTS(
awsutil.WithGetCallerIdentityError(errors.New(testGetCallerIdentityErr)),
testOpts: []awsutilv2.Option{
awsutilv2.WithSTSAPIFunc(
awsutilv2.NewMockSTS(
awsutilv2.WithGetCallerIdentityError(errors.New(testGetCallerIdentityErr)),
),
),
},
},
expectedStatusCode: codes.Unknown,
expectedErr: "aws service unknown: unknown error: validating credentials",
expectedErr: fmt.Sprintf("error validating credentials: %s", testGetCallerIdentityErr),
},
{
name: "good",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
testOpts: []awsutil.Option{awsutil.WithSTSAPIFunc(awsutil.NewMockSTS())},
testOpts: []awsutilv2.Option{awsutilv2.WithSTSAPIFunc(awsutilv2.NewMockSTS())},
},
},
}
@@ -417,12 +383,10 @@ func TestAwsCatalogPersistedState_ValidateCreds(t *testing.T) {
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require, assert := require.New(t), assert.New(t)
require := require.New(t)
err := tc.in.ValidateCreds(context.Background())
if tc.expectedErr != "" {
require.Error(err)
assert.ErrorContains(err, tc.expectedErr)
assert.Equal(tc.expectedStatusCode, status.Code(err))
require.EqualError(err, tc.expectedErr)
return
}
@@ -435,75 +399,70 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) {
cases := []struct {
name string
in *AwsCredentialPersistedState
exepected *awsutil.CredentialsConfig
exepected *awsutilv2.CredentialsConfig
expectedNonZeroRotatedTime bool
expectedErr string
expectedStatusCode codes.Code
}{
{
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing credentials config",
expectedStatusCode: codes.InvalidArgument,
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing credentials config",
},
{
name: "cannot delete dynamic credential type",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "ASIAfoobar",
SecretKey: "bazqux",
},
CredsLastRotatedTime: time.Now(),
},
expectedErr: "invalid credential type",
expectedStatusCode: codes.InvalidArgument,
expectedErr: "invalid credential type",
},
{
name: "cannot delete unknown credential type",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "DNEfoobar",
SecretKey: "bazqux",
},
CredsLastRotatedTime: time.Now(),
},
expectedErr: "invalid credential type",
expectedStatusCode: codes.InvalidArgument,
expectedErr: "invalid credential type",
},
{
name: "rotation error",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithGetUserError(errors.New(testGetUserErr)),
testOpts: []awsutilv2.Option{
awsutilv2.WithIAMAPIFunc(
awsutilv2.NewMockIAM(
awsutilv2.WithGetUserError(errors.New(testGetUserErr)),
),
),
},
},
expectedErr: "aws service unknown: unknown error: rotating credentials",
expectedStatusCode: codes.Unknown,
expectedErr: fmt.Sprintf("error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr),
},
{
name: "good",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
testOpts: []awsutil.Option{
awsutil.WithSTSAPIFunc(
awsutil.NewMockSTS(),
testOpts: []awsutilv2.Option{
awsutilv2.WithSTSAPIFunc(
awsutilv2.NewMockSTS(),
),
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithGetUserOutput(
awsutilv2.WithIAMAPIFunc(
awsutilv2.NewMockIAM(
awsutilv2.WithGetUserOutput(
&iam.GetUserOutput{
User: &iamTypes.User{
Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"),
@@ -512,7 +471,7 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) {
},
},
),
awsutil.WithCreateAccessKeyOutput(
awsutilv2.WithCreateAccessKeyOutput(
&iam.CreateAccessKeyOutput{
AccessKey: &iamTypes.AccessKey{
AccessKeyId: aws.String("one"),
@@ -525,7 +484,7 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) {
),
},
},
exepected: &awsutil.CredentialsConfig{
exepected: &awsutilv2.CredentialsConfig{
AccessKey: "one",
SecretKey: "two",
Region: "us-west-2",
@@ -537,12 +496,10 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) {
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require, assert := require.New(t), assert.New(t)
require := require.New(t)
err := tc.in.RotateCreds(context.Background())
if tc.expectedErr != "" {
require.Error(err)
assert.ErrorContains(err, tc.expectedErr)
assert.Equal(tc.expectedStatusCode, status.Code(err))
require.EqualError(err, tc.expectedErr)
return
}
@@ -560,64 +517,84 @@ func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) {
cases := []struct {
name string
in *AwsCredentialPersistedState
credentialConfig *awsutil.CredentialsConfig
expected *awsutil.CredentialsConfig
credentialConfig *awsutilv2.CredentialsConfig
expected *awsutilv2.CredentialsConfig
expectedDeleteCalled bool
expectedErr string
expectedStatusCode codes.Code
}{
{
name: "missing new credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing new credentials config",
expectedStatusCode: codes.InvalidArgument,
name: "missing new credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing new credentials config",
},
{
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
credentialConfig: &awsutil.CredentialsConfig{},
expectedErr: "missing credentials config",
expectedStatusCode: codes.InvalidArgument,
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
credentialConfig: &awsutilv2.CredentialsConfig{},
expectedErr: "missing credentials config",
},
{
name: "cannot delete dynamic credential type",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "ASIAfoobar",
SecretKey: "bazqux",
},
CredsLastRotatedTime: time.Now(),
},
credentialConfig: &awsutilv2.CredentialsConfig{},
expectedErr: "invalid credential type",
},
{
name: "cannot delete unknown credential type",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "DNEfoobar",
SecretKey: "bazqux",
},
CredsLastRotatedTime: time.Now(),
},
credentialConfig: &awsutilv2.CredentialsConfig{},
expectedErr: "invalid credential type",
},
{
name: "deletion error",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
},
CredsLastRotatedTime: time.Now(),
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithDeleteAccessKeyError(errors.New(testDeleteAccessKeyErr)),
testOpts: []awsutilv2.Option{
awsutilv2.WithIAMAPIFunc(
awsutilv2.NewMockIAM(
awsutilv2.WithDeleteAccessKeyError(errors.New(testDeleteAccessKeyErr)),
),
),
},
},
credentialConfig: &awsutil.CredentialsConfig{},
expectedErr: "aws service unknown: unknown error: deleting credentials",
expectedStatusCode: codes.Unknown,
credentialConfig: &awsutilv2.CredentialsConfig{},
expectedErr: fmt.Sprintf("error deleting old access key: %s", testDeleteAccessKeyErr),
},
{
name: "good with delete of old rotated",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
},
CredsLastRotatedTime: time.Now(),
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
testOpts: []awsutilv2.Option{
awsutilv2.WithIAMAPIFunc(
newTestMockIAM(state),
),
},
},
credentialConfig: &awsutil.CredentialsConfig{
credentialConfig: &awsutilv2.CredentialsConfig{
AccessKey: "one",
SecretKey: "two",
},
expected: &awsutil.CredentialsConfig{
expected: &awsutilv2.CredentialsConfig{
AccessKey: "one",
SecretKey: "two",
},
@@ -626,21 +603,21 @@ func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) {
{
name: "good without delete of old rotated",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
},
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
testOpts: []awsutilv2.Option{
awsutilv2.WithIAMAPIFunc(
newTestMockIAM(state),
),
},
},
credentialConfig: &awsutil.CredentialsConfig{
credentialConfig: &awsutilv2.CredentialsConfig{
AccessKey: "one",
SecretKey: "two",
},
expected: &awsutil.CredentialsConfig{
expected: &awsutilv2.CredentialsConfig{
AccessKey: "one",
SecretKey: "two",
},
@@ -651,12 +628,11 @@ func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) {
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require, assert := require.New(t), assert.New(t)
require := require.New(t)
state.Reset()
err := tc.in.ReplaceCreds(context.Background(), tc.credentialConfig)
if tc.expectedErr != "" {
require.Error(err)
assert.ErrorContains(err, tc.expectedErr)
require.EqualError(err, tc.expectedErr)
return
}
@@ -671,70 +647,65 @@ func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) {
func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) {
cases := []struct {
name string
in *AwsCredentialPersistedState
expectedErr string
expectedStatusCode codes.Code
name string
in *AwsCredentialPersistedState
expectedErr string
}{
{
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing credentials config",
expectedStatusCode: codes.InvalidArgument,
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing credentials config",
},
{
name: "cannot delete dynamic credential type",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "ASIAfoobar",
SecretKey: "bazqux",
},
CredsLastRotatedTime: time.Now(),
},
expectedErr: "invalid credential type",
expectedStatusCode: codes.InvalidArgument,
expectedErr: "invalid credential type",
},
{
name: "cannot delete unknown credential type",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "DNEfoobar",
SecretKey: "bazqux",
},
CredsLastRotatedTime: time.Now(),
},
expectedErr: "invalid credential type",
expectedStatusCode: codes.InvalidArgument,
expectedErr: "invalid credential type",
},
{
name: "deletion error",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
},
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithDeleteAccessKeyError(errors.New(testDeleteAccessKeyErr)),
testOpts: []awsutilv2.Option{
awsutilv2.WithIAMAPIFunc(
awsutilv2.NewMockIAM(
awsutilv2.WithDeleteAccessKeyError(errors.New(testDeleteAccessKeyErr)),
),
),
},
},
expectedErr: "aws service unknown: unknown error: deleting credentials",
expectedStatusCode: codes.Unknown,
expectedErr: fmt.Sprintf("error deleting old access key: %s", testDeleteAccessKeyErr),
},
{
name: "deletion error, but OK because key was just gone",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
},
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithDeleteAccessKeyError(
testOpts: []awsutilv2.Option{
awsutilv2.WithIAMAPIFunc(
awsutilv2.NewMockIAM(
awsutilv2.WithDeleteAccessKeyError(
&iamTypes.NoSuchEntityException{},
),
),
@@ -745,13 +716,13 @@ func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) {
{
name: "good",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "AKIAfoobar",
SecretKey: "bazqux",
},
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(),
testOpts: []awsutilv2.Option{
awsutilv2.WithIAMAPIFunc(
awsutilv2.NewMockIAM(),
),
},
},
@@ -761,11 +732,10 @@ func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) {
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require, assert := require.New(t), assert.New(t)
require := require.New(t)
err := tc.in.DeleteCreds(context.Background())
if tc.expectedErr != "" {
require.Error(err)
assert.ErrorContains(err, tc.expectedErr)
require.EqualError(err, tc.expectedErr)
return
}
@@ -786,24 +756,24 @@ func TestAwsCatalogPersistedState_GenerateCredentialChain(t *testing.T) {
{
name: "error",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
},
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
testOpts: []awsutilv2.Option{awsutilv2.MockOptionErr(errors.New(testOptionErr))},
},
expectedErr: fmt.Sprintf("error reading options in GenerateCredentialChain: %s", testOptionErr),
},
{
name: "static credentials",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
CredentialsConfig: &awsutilv2.CredentialsConfig{
AccessKey: "AKIA_foobar",
SecretKey: "bazqux",
},
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(),
testOpts: []awsutilv2.Option{
awsutilv2.WithIAMAPIFunc(
awsutilv2.NewMockIAM(),
),
},
},
@@ -811,22 +781,22 @@ func TestAwsCatalogPersistedState_GenerateCredentialChain(t *testing.T) {
{
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{
CredentialsConfig: func() *awsutilv2.CredentialsConfig {
config, err := awsutilv2.NewCredentialsConfig(
awsutilv2.WithRegion("us-west-2"),
awsutilv2.WithRoleArn("arn:aws:iam::123456789012:role/S3Access"),
awsutilv2.WithRoleExternalId("1234567890"),
awsutilv2.WithRoleSessionName("assume-role"),
awsutilv2.WithRoleTags(map[string]string{
"foo": "bar",
}),
)
require.NoError(err)
return config
}(),
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(),
testOpts: []awsutilv2.Option{
awsutilv2.WithIAMAPIFunc(
awsutilv2.NewMockIAM(),
),
},
},
@@ -845,53 +815,74 @@ func TestAwsCatalogPersistedState_GenerateCredentialChain(t *testing.T) {
}
}
func TestGetCredentialType(t *testing.T) {
tests := []struct {
name string
credConfig *awsutil.CredentialsConfig
expectedCredType CredentialType
func TestHasStaticCredentials(t *testing.T) {
require := require.New(t)
cases := []struct {
name string
accessKey string
expected bool
}{
{
name: "nil credential config",
credConfig: nil,
expectedCredType: Unknown,
name: "dynamic credential",
accessKey: "ASIAEXAMPLE",
expected: false,
},
{
name: "empty credential config",
credConfig: &awsutil.CredentialsConfig{},
expectedCredType: Unknown,
name: "static credential",
accessKey: "AKIAEXAMPLE",
expected: true,
},
{
name: "static aws",
credConfig: &awsutil.CredentialsConfig{AccessKey: "AKIAfoobar"},
expectedCredType: StaticAWS,
name: "other credential",
accessKey: "EXAMPLE",
expected: false,
},
{
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,
name: "empty",
accessKey: "",
expected: false,
},
}
for _, tc := range tests {
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
ct := GetCredentialType(tc.credConfig)
require.Equal(t, tc.expectedCredType, ct)
require.Equal(tc.expected, HasStaticCredentials(tc.accessKey))
})
}
}
func TestHasDynamicCredentials(t *testing.T) {
require := require.New(t)
cases := []struct {
name string
accessKey string
expected bool
}{
{
name: "dynamic credential",
accessKey: "ASIAEXAMPLE",
expected: true,
},
{
name: "static credential",
accessKey: "AKIAEXAMPLE",
expected: false,
},
{
name: "other credential",
accessKey: "EXAMPLE",
expected: false,
},
{
name: "empty",
accessKey: "",
expected: false,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require.Equal(tc.expected, HasDynamicCredentials(tc.accessKey))
})
}
}
+5 -5
View File
@@ -8,7 +8,7 @@ import (
"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"
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil"
"google.golang.org/protobuf/types/known/structpb"
)
@@ -28,7 +28,7 @@ func (s *testMockIAMState) Reset() {
}
type testMockIAM struct {
awsutil.IAMClient
awsutilv2.IAMClient
State *testMockIAMState
}
@@ -38,12 +38,12 @@ func (m *testMockIAM) DeleteAccessKey(ctx context.Context, input *iam.DeleteAcce
return m.IAMClient.DeleteAccessKey(ctx, input, opts...)
}
func newTestMockIAM(state *testMockIAMState, opts ...awsutil.MockIAMOption) awsutil.IAMAPIFunc {
return func(awsConfig *aws.Config) (awsutil.IAMClient, error) {
func newTestMockIAM(state *testMockIAMState, opts ...awsutilv2.MockIAMOption) awsutilv2.IAMAPIFunc {
return func(awsConfig *aws.Config) (awsutilv2.IAMClient, error) {
m := &testMockIAM{
State: state,
}
f := awsutil.NewMockIAM(opts...)
f := awsutilv2.NewMockIAM(opts...)
var err error
m.IAMClient, err = f(awsConfig)
+1 -194
View File
@@ -4,63 +4,12 @@
package errors
import (
"bytes"
"errors"
"fmt"
"net/http"
"sort"
"strings"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/aws/aws-sdk-go-v2/aws/retry"
awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
"github.com/aws/smithy-go"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"google.golang.org/protobuf/types/known/timestamppb"
)
const (
// awsErrorAccessDenied is returned when the credentials do not have
// the required permissions to do a specific action on a resource.
// This error will persist until the IAM Role Policy is updated by
// the owner. This error cannot be resolved by retrying the request.
awsErrorAccessDenied = "AccessDenied"
// awsErrorInvalidAccessKeyId is returned when the credential does
// not exist. This error will persist until the credentials attached
// to the storage bucket is updated. This error can occur when the
// given credentials are deleted, deactivated, or rotated. This
// error cannot be resolved by retrying the request.
awsErrorInvalidAccessKeyId = "InvalidAccessKeyId"
// awsErrorExpiredToken is returned when the provided credential is
// expired. This error can only be returned by a dynamic credential
// type. This error can resolve itself by retrying the request.
awsErrorExpiredToken = "ExpiredToken"
// NoSuchBucket is returned when the specified S3 storage bucket does
// not exist
awsErrorNoSuchBucket = "NoSuchBucket"
// BadDigest is returned when an S3 PUT OBJECT request's calculated
// checksum does not match the checksum which was sent by the client
awsErrorBadDigest = "BadDigest"
// NoSuchKey is returned when attempting to interact with an S3 object
// whose key does not exist
awsErrorNoSuchKey = "NoSuchKey"
// The InvalidObjectState error is returned when trying to
// access an object that was moved into cold storage.
awsErrorInvalidObjectState = "InvalidObjectState"
// RequestTimeout is returned when an http request takes longer than allowed
awsErrorRequestTimeout = "RequestTimeout"
// RequestTimeoutException is returned when an http request takes longer than allowed
awsErrorRequestTimeoutException = "RequestTimeoutException"
)
// InvalidArgumentError returns an grpc invalid argument status error.
@@ -73,147 +22,5 @@ func InvalidArgumentError(msg string, f map[string]string) error {
sort.Strings(fieldMsgs)
msg = fmt.Sprintf("%s: [%s]", msg, strings.Join(fieldMsgs, ", "))
}
return BadRequestStatus(msg)
}
// ParseAWSError converts an aws service error into a RPC status. This method
// will handle parsing the following aws error codes: throttling, connections,
// and credentials. This method will fallback to parsing the http status code
// when it cannot match an aws error code. This method does not handle specific
// service type errors such as S3 or EC2.
func ParseAWSError(op string, err error) (st *status.Status, permission *pb.Permission) {
if err == nil {
return nil, nil
}
msg := fmt.Sprintf("%s: %v", op, err)
// find the service name of the aws api
serviceName := "unknown"
var oe *smithy.OperationError
if errors.As(err, &oe) {
serviceName = oe.Service()
}
// The unknown plugin error code will be used
// when the aws service returns an error that
// has failed to be parsed. By default, this
// error code is retryable.
defer func() {
if st == nil {
statusMsg := fmt.Sprintf("aws service %s: unknown error: %s", serviceName, msg)
st = status.New(codes.Unknown, statusMsg)
}
if permission == nil {
permission = &pb.Permission{State: pb.StateType_STATE_TYPE_UNKNOWN, CheckedAt: timestamppb.Now()}
}
}()
// evaluate against aws throttling error codes
throttleErr := retry.ThrottleErrorCode{
Codes: retry.DefaultThrottleErrorCodes,
}
if throttleErr.IsErrorThrottle(err).Bool() {
statusMsg := fmt.Sprintf("aws service %s: throttling: %s", serviceName, msg)
return status.New(codes.Unavailable, statusMsg), nil
}
// parse some specific aws error codes that we have special cred states for
var apiErr smithy.APIError
if errors.As(err, &apiErr) {
switch apiErr.ErrorCode() {
case awsErrorAccessDenied:
fallthrough
case awsErrorInvalidAccessKeyId:
fallthrough
case awsErrorExpiredToken:
statusMsg := fmt.Sprintf("aws service %s: invalid credentials: %s", serviceName, msg)
return status.New(codes.PermissionDenied, statusMsg), &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: apiErr.ErrorMessage(),
CheckedAt: timestamppb.Now(),
}
case awsErrorNoSuchBucket:
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
return status.New(codes.NotFound, statusMsg), &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: apiErr.ErrorMessage(),
CheckedAt: timestamppb.Now(),
}
case awsErrorBadDigest:
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
return status.New(codes.Aborted, statusMsg), nil
case awsErrorNoSuchKey:
fallthrough
case awsErrorInvalidObjectState:
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
return status.New(codes.NotFound, statusMsg), nil
case awsErrorRequestTimeout:
fallthrough
case awsErrorRequestTimeoutException:
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
return status.New(codes.DeadlineExceeded, statusMsg), nil
}
}
// default to evaluating the http status code
if httpErr, ok := err.(*awshttp.ResponseError); ok {
buf := new(bytes.Buffer)
_, err = buf.ReadFrom(httpErr.Response.Body)
if err != nil {
return status.New(codes.Internal, err.Error()), nil
}
defer httpErr.Response.Body.Close()
switch httpErr.HTTPStatusCode() {
case http.StatusBadRequest:
statusMsg := fmt.Sprintf("aws service %s: bad request: %s", serviceName, msg)
return status.New(codes.InvalidArgument, statusMsg), nil
case http.StatusUnauthorized:
statusMsg := fmt.Sprintf("aws service %s: invalid credentials: %s", serviceName, msg)
return status.New(codes.PermissionDenied, statusMsg), &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: buf.String(),
CheckedAt: timestamppb.Now(),
}
case http.StatusForbidden:
statusMsg := fmt.Sprintf("aws service %s: invalid credentials: %s", serviceName, msg)
return status.New(codes.PermissionDenied, statusMsg), &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: buf.String(),
CheckedAt: timestamppb.Now(),
}
case http.StatusNotFound:
statusMsg := fmt.Sprintf("aws service %s: resource not found: %s", serviceName, msg)
return status.New(codes.NotFound, statusMsg), nil
case http.StatusTooManyRequests:
statusMsg := fmt.Sprintf("aws service %s: throttling: %s", serviceName, msg)
return status.New(codes.Unavailable, statusMsg), nil
case http.StatusInternalServerError:
fallthrough
case http.StatusBadGateway:
fallthrough
case http.StatusServiceUnavailable:
statusMsg := fmt.Sprintf("aws service %s: connectivity: %s", serviceName, msg)
return status.New(codes.Unavailable, statusMsg), nil
case http.StatusRequestTimeout:
fallthrough
case http.StatusGatewayTimeout:
statusMsg := fmt.Sprintf("aws service %s: timeout: %s", serviceName, msg)
return status.New(codes.DeadlineExceeded, statusMsg), nil
}
}
return st, nil
}
// BadRequestStatus returns a status error with an invalid
// argument code
func BadRequestStatus(format string, args ...any) error {
return status.New(codes.InvalidArgument, fmt.Sprintf(format, args...)).Err()
}
// UnknownStatus returns a status error with an internal
// error code
func UnknownStatus(format string, args ...any) error {
return status.New(codes.Internal, fmt.Sprintf(format, args...)).Err()
return status.Error(codes.InvalidArgument, msg)
}
-349
View File
@@ -1,349 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package errors
import (
"fmt"
"testing"
"net/http"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
)
func Test_ParseAWSError(t *testing.T) {
t.Parallel()
cases := []struct {
name string
err error
expectedStatusCode codes.Code
expectedStatusMsg string
expectedPermission *pb.Permission
}{
{
name: "nil-error",
expectedStatusCode: codes.OK,
},
{
name: "default-to-unknwon",
err: fmt.Errorf("what can this error be?"),
expectedStatusCode: codes.Unknown,
expectedStatusMsg: "aws service unknown: unknown error: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "http-status-code-bad-request",
err: TestAwsHttpResponseError(http.StatusBadRequest, "bad request"),
expectedStatusCode: codes.InvalidArgument,
expectedStatusMsg: "aws service unknown: bad request: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "http-status-code-unauthorized",
err: TestAwsHttpResponseError(http.StatusUnauthorized, "unauthorized request"),
expectedStatusCode: codes.PermissionDenied,
expectedStatusMsg: "aws service unknown: invalid credentials: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: "unauthorized request",
CheckedAt: timestamppb.Now(),
},
},
{
name: "http-status-code-forbidden",
err: TestAwsHttpResponseError(http.StatusForbidden, "forbidden request"),
expectedStatusCode: codes.PermissionDenied,
expectedStatusMsg: "aws service unknown: invalid credentials: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: "forbidden request",
CheckedAt: timestamppb.Now(),
},
},
{
name: "http-status-code-not-found",
err: TestAwsHttpResponseError(http.StatusNotFound, "not found request"),
expectedStatusCode: codes.NotFound,
expectedStatusMsg: "aws service unknown: resource not found: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "http-status-code-too-many-requests",
err: TestAwsHttpResponseError(http.StatusTooManyRequests, "too many requests"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "http-status-code-request-timeout",
err: TestAwsHttpResponseError(http.StatusRequestTimeout, "request timeout"),
expectedStatusCode: codes.DeadlineExceeded,
expectedStatusMsg: "aws service unknown: timeout: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "http-status-code-internal-server-error",
err: TestAwsHttpResponseError(http.StatusInternalServerError, "internal server error"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: connectivity: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "http-status-code-bad-gateway",
err: TestAwsHttpResponseError(http.StatusBadGateway, "bad gateway"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: connectivity: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "http-status-code-service-unavailable",
err: TestAwsHttpResponseError(http.StatusServiceUnavailable, "service unavailable"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: connectivity: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "http-status-code-gateway-timeout",
err: TestAwsHttpResponseError(http.StatusGatewayTimeout, "gateway timeout"),
expectedStatusCode: codes.DeadlineExceeded,
expectedStatusMsg: "aws service unknown: timeout: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-throttling",
err: TestAwsError("Throttling", "throttling error"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-throttling-exception",
err: TestAwsError("ThrottlingException", "throttling exception"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-throttled-exception",
err: TestAwsError("ThrottledException", "throttled exception"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-request-throttled-exception",
err: TestAwsError("RequestThrottledException", "request throttled exception"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-request-too-many-requests-exception",
err: TestAwsError("TooManyRequestsException", "too many requests exception"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-request-provisioned-throughput-exceeded-exception",
err: TestAwsError("ProvisionedThroughputExceededException", "provisioned throughput exceeded exception"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-request-transaction-in-progress-exception",
err: TestAwsError("TransactionInProgressException", "transaction in progress exception"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-request-limit-exceeded",
err: TestAwsError("RequestLimitExceeded", "request limit exceeded"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-request-bandwidth-limit-exceeded",
err: TestAwsError("BandwidthLimitExceeded", "bandwidth limit exceeded"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-request-limit-exceeded-exception",
err: TestAwsError("LimitExceededException", "limit exceeded exception"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-request-throttled",
err: TestAwsError("RequestThrottled", "request throttled"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-request-slow-down",
err: TestAwsError("SlowDown", "slow down"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-request-prior-request-not-complete",
err: TestAwsError("PriorRequestNotComplete", "prior request not complete"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-request-ec2-throttled-exception",
err: TestAwsError("EC2ThrottledException", "ec2 throttled exception"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-request-timeout",
err: TestAwsError("RequestTimeout", "request timeout"),
expectedStatusCode: codes.DeadlineExceeded,
expectedStatusMsg: "aws service unknown: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-request-timeout-exception",
err: TestAwsError("RequestTimeoutException", "request timeout exception"),
expectedStatusCode: codes.DeadlineExceeded,
expectedStatusMsg: "aws service unknown: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-request-access-denied",
err: TestAwsError(awsErrorAccessDenied, "not authorized to perform action"),
expectedStatusCode: codes.PermissionDenied,
expectedStatusMsg: "aws service unknown: invalid credentials: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: "not authorized to perform action",
CheckedAt: timestamppb.Now(),
},
},
{
name: "aws-request-invalid-access-key-id",
err: TestAwsError(awsErrorInvalidAccessKeyId, "The AWS Access Key Id you provided does not exist in our records."),
expectedStatusCode: codes.PermissionDenied,
expectedStatusMsg: "aws service unknown: invalid credentials: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: "The AWS Access Key Id you provided does not exist in our records.",
CheckedAt: timestamppb.Now(),
},
},
{
name: "aws-request-expired-token",
err: TestAwsError(awsErrorExpiredToken, "The security token included in the request is expired."),
expectedStatusCode: codes.PermissionDenied,
expectedStatusMsg: "aws service unknown: invalid credentials: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: "The security token included in the request is expired.",
CheckedAt: timestamppb.Now(),
},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
require, assert := require.New(t), assert.New(t)
actualStatus, permission := ParseAWSError("test", tt.err)
if tt.expectedStatusCode == codes.OK {
require.Nil(actualStatus)
return
}
require.Equal(tt.expectedStatusCode, actualStatus.Code())
require.Contains(actualStatus.Message(), tt.expectedStatusMsg)
require.Len(actualStatus.Details(), 0)
CheckSimilarPermission(assert, tt.expectedPermission, permission, true)
})
}
}
func Test_BadRequestStatus(t *testing.T) {
require, assert := require.New(t), assert.New(t)
err := BadRequestStatus("test: %s", "hello world")
assert.ErrorContains(err, "test: hello world")
assert.Equal(status.Code(err), codes.InvalidArgument)
_, ok := status.FromError(err)
require.True(ok)
}
func Test_UnknownStatus(t *testing.T) {
require, assert := require.New(t), assert.New(t)
err := UnknownStatus("test: %s", "hello world")
assert.ErrorContains(err, "test: hello world")
assert.Equal(status.Code(err), codes.Internal)
_, ok := status.FromError(err)
require.True(ok)
}
-60
View File
@@ -1,60 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package errors
import (
"io"
"net/http"
"strings"
awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
"github.com/aws/smithy-go"
smithyhttp "github.com/aws/smithy-go/transport/http"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/timestamppb"
)
// TestAwsHttpResponseError returns an aws http response error
func TestAwsHttpResponseError(code int, msg string) error {
return &awshttp.ResponseError{
ResponseError: &smithyhttp.ResponseError{
Response: &smithyhttp.Response{
Response: &http.Response{
StatusCode: code,
Body: io.NopCloser(strings.NewReader(msg)),
},
},
},
}
}
// TestAwsError returns an generic api error
func TestAwsError(code, msg string) error {
return &smithy.GenericAPIError{
Code: code,
Message: msg,
Fault: smithy.FaultServer,
}
}
func CheckSimilarPermission(assert *assert.Assertions, expected, actual *pb.Permission, substituteNilWithDefaultOk bool) {
if expected == nil && substituteNilWithDefaultOk {
expected = &pb.Permission{
State: pb.StateType_STATE_TYPE_OK,
CheckedAt: timestamppb.Now(),
}
}
if expected != nil {
assert.NotNil(actual)
if actual != nil {
assert.Equal(expected.State, actual.State, "StateType mismatch")
assert.Equal(expected.ErrorDetails, actual.ErrorDetails)
assert.NotNil(actual.CheckedAt)
}
} else {
assert.Equal(expected, actual)
}
}
+1 -1
View File
@@ -32,6 +32,6 @@ type AwsPlugin struct {
func NewAwsPlugin() *AwsPlugin {
return &AwsPlugin{
HostPlugin: &host.HostPlugin{},
StoragePlugin: storage.New(),
StoragePlugin: &storage.StoragePlugin{},
}
}
-8
View File
@@ -108,14 +108,6 @@ The following `attributes` are valid on an AWS host catalog resource:
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.
- `role_arn` (string): The AWS Role ARN used for `AssumeRole` authentication. If
provided, `disable_credential_rotation` must be also set to `true.
- `role_external_id` (string): The external id configured for the `AssumeRole`
provider.
- `role_session_name` (string): The session name configured for the `AssumeRole`
provider.
- `role_tags` (object): The key-value pair tags configured for the `AssumeRole`
provider.
The following `secrets` are required on an AWS host catalog resource:
-10
View File
@@ -19,9 +19,6 @@ import (
// map.
type CatalogAttributes struct {
*cred.CredentialAttributes
// DualStack is used for configuring how the aws client will resolve requests.
DualStack bool
}
func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
@@ -34,12 +31,6 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
return nil, err
}
dualStack, err := values.GetBoolValue(in, ConstAwsDualStack, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstAwsDualStack)] = err.Error()
}
delete(unknownFields, ConstAwsDualStack)
for s := range unknownFields {
switch s {
// Ignore knownFields from CredentialAttributes
@@ -66,7 +57,6 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
return &CatalogAttributes{
CredentialAttributes: credAttributes,
DualStack: dualStack,
}, nil
}
-16
View File
@@ -43,22 +43,6 @@ func TestGetCatalogAttributes(t *testing.T) {
},
expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
},
{
name: "with dual stack",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"dual_stack": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
DualStack: true,
},
},
{
name: "default",
in: &structpb.Struct{
-3
View File
@@ -10,7 +10,4 @@ const (
// ConstInstanceNameTagKey denotes the special AWS tag key that contains an EC2
// instance's name. This field is to be matched literally.
ConstInstanceNameTagKey = "Name"
// ConstAwsDualStack is the key for the dualstack flag in the aws s3 client.
ConstAwsDualStack = "dual_stack"
)
+151 -196
View File
@@ -5,19 +5,22 @@ package host
import (
"context"
"errors"
"fmt"
"strings"
"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-plugin-aws/internal/errors"
"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-aws/internal/credential"
cred "github.com/hashicorp/boundary-plugin-aws/internal/credential"
)
// HostPlugin implements the HostPluginServiceServer interface for the
@@ -39,19 +42,24 @@ var _ pb.HostPluginServiceServer = (*HostPlugin)(nil)
func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalogRequest) (*pb.OnCreateCatalogResponse, error) {
catalog := req.GetCatalog()
if catalog == nil {
return nil, errors.BadRequestStatus("catalog is required")
return nil, status.Error(codes.InvalidArgument, "catalog is nil")
}
secrets := catalog.GetSecrets()
if secrets == nil {
return nil, status.Error(codes.InvalidArgument, "secrets are required")
}
attrs := catalog.GetAttributes()
if attrs == nil {
return nil, errors.BadRequestStatus("attributes are required")
return nil, status.Error(codes.InvalidArgument, "attributes are required")
}
catalogAttributes, err := getCatalogAttributes(attrs)
if err != nil {
return nil, err
}
credConfig, err := credential.GetCredentialsConfig(catalog.GetSecrets(), catalogAttributes.CredentialAttributes, catalogAttributes.DualStack)
credConfig, err := cred.GetCredentialsConfig(secrets, catalogAttributes.CredentialAttributes, true)
if err != nil {
return nil, err
}
@@ -63,14 +71,7 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
}, p.testCredStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error setting up persisted state: %s", err)
}
// Try to rotate AWS static credentials
if credential.GetCredentialType(credState.CredentialsConfig) == credential.StaticAWS && !catalogAttributes.DisableCredentialRotation {
if err := credState.RotateCreds(ctx); err != nil {
return nil, err
}
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
}
catalogState, err := newAwsCatalogPersistedState(
@@ -79,21 +80,26 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
}, p.testCatalogStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error setting up persisted state: %s", err)
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
}
// perform dry run to ensure we can interact with AWS as expected.
opts := []ec2Option{}
if catalogAttributes.DualStack {
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
}
if st := dryRunValidation(ctx, catalogState, opts); st != nil {
return nil, st.Err()
// Try to rotate static credentials
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
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)
}
}
}
persistedProto, err := catalogState.toProto()
if err != nil {
return nil, errors.BadRequestStatus(err.Error())
return nil, status.Error(codes.InvalidArgument, err.Error())
}
return &pb.OnCreateCatalogResponse{
@@ -106,105 +112,43 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
// For host catalogs we only need to really validate what is
// currently in the new copy of the host catalog data, so skip
// fetching the new copy to keep things a little less messy.
currentCatalog := req.GetCurrentCatalog()
if currentCatalog == nil {
return nil, errors.BadRequestStatus("current catalog is required")
}
newCatalog := req.GetNewCatalog()
if newCatalog == nil {
return nil, errors.BadRequestStatus("new catalog is required")
catalog := req.GetNewCatalog()
if catalog == nil {
return nil, status.Error(codes.InvalidArgument, "new catalog is nil")
}
oldAttrs := currentCatalog.GetAttributes()
if oldAttrs == nil {
return nil, errors.BadRequestStatus("current catalog attributes are required")
var updateSecrets bool
secrets := catalog.GetSecrets()
if secrets != nil {
// We will be updating secrets this run, but what exactly that
// means will be determined later.
updateSecrets = true
}
oldCatalogAttributes, err := getCatalogAttributes(oldAttrs)
if err != nil {
return nil, err
attrs := catalog.GetAttributes()
if attrs == nil {
return nil, status.Error(codes.InvalidArgument, "new catalog missing attributes")
}
newAttrs := newCatalog.GetAttributes()
if newAttrs == nil {
return nil, errors.BadRequestStatus("new catalog attributes are required")
}
newCatalogAttributes, err := getCatalogAttributes(newAttrs)
catalogAttributes, err := getCatalogAttributes(attrs)
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 awsCatalogPersistedStateFromProto) 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 := credential.AwsCredentialPersistedStateFromProto(
req.GetPersisted().GetSecrets(),
oldCatalogAttributes.CredentialAttributes,
oldCatalogAttributes.DualStack,
catalogAttributes.CredentialAttributes,
p.testCredStateOpts...)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
}
// 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 := credential.GetCredentialsConfig(newCatalog.GetSecrets(), newCatalogAttributes.CredentialAttributes, newCatalogAttributes.DualStack)
if err != nil {
return nil, err
}
// We will be updating credentials when one of two changes occur:
// 1. the new catalog has secrets for static credentials, in which case we need to delete the old static credentials
// 2. the new catalog has a different roleARN value for dynamic credentials
if newCatalog.GetSecrets() != nil || newCatalogAttributes.RoleArn != oldCatalogAttributes.RoleArn {
// Ensure the incoming credentials are valid for interaction with the S3
// Bucket before replacing them.
newCredState, err := credential.NewAwsCredentialPersistedState(
append([]credential.AwsCredentialPersistedStateOption{
credential.WithCredentialsConfig(updatedCredentials),
}, p.testCredStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error setting up new credential persisted state: %s", err)
}
newCatalogState, err := newAwsCatalogPersistedState(
append([]awsCatalogPersistedStateOption{
withCredentials(newCredState),
}, p.testCatalogStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
}
opts := []ec2Option{}
if newCatalogAttributes.DualStack {
opts = append(opts, WithDualStack(newCatalogAttributes.DualStack))
}
if st := dryRunValidation(ctx, newCatalogState, opts); st != nil {
return nil, st.Err()
}
// 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, err
}
}
if credential.GetCredentialType(credState.CredentialsConfig) == credential.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 newCatalogAttributes.DisableCredentialRotation && newCatalog.GetSecrets() == nil {
if !credState.CredsLastRotatedTime.IsZero() {
return nil, errors.BadRequestStatus("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 !newCatalogAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
if err := credState.RotateCreds(ctx); err != nil {
return nil, err
}
}
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
catalogState, err := newAwsCatalogPersistedState(
@@ -213,21 +157,45 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
}, p.testCatalogStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
// perform dry run to ensure we can interact with AWS as expected.
opts := []ec2Option{}
if newCatalogAttributes.DualStack {
opts = append(opts, WithDualStack(newCatalogAttributes.DualStack))
}
if st := dryRunValidation(ctx, catalogState, opts); st != nil {
return nil, st.Err()
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
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)
}
}
}
persistedProto, err := catalogState.toProto()
if err != nil {
return nil, errors.BadRequestStatus(err.Error())
return nil, status.Error(codes.InvalidArgument, err.Error())
}
return &pb.OnUpdateCatalogResponse{
@@ -239,12 +207,12 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalogRequest) (*pb.OnDeleteCatalogResponse, error) {
catalog := req.GetCatalog()
if catalog == nil {
return nil, errors.BadRequestStatus("catalog is required")
return nil, status.Error(codes.InvalidArgument, "new catalog is nil")
}
attrs := catalog.GetAttributes()
if attrs == nil {
return nil, errors.BadRequestStatus("catalog attributes are required")
return nil, status.Error(codes.InvalidArgument, "new catalog missing attributes")
}
catalogAttributes, err := getCatalogAttributes(attrs)
@@ -261,10 +229,9 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo
credState, err := credential.AwsCredentialPersistedStateFromProto(
req.GetPersisted().GetSecrets(),
catalogAttributes.CredentialAttributes,
catalogAttributes.DualStack,
p.testCredStateOpts...)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
_, err = newAwsCatalogPersistedState(
@@ -273,17 +240,17 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo
}, p.testCatalogStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
// try to delete static credentials
if credential.GetCredentialType(credState.CredentialsConfig) == credential.StaticAWS {
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
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, err
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during catalog deletion: %s", err)
}
}
}
@@ -322,12 +289,12 @@ func (p *HostPlugin) NormalizeSetData(ctx context.Context, req *pb.NormalizeSetD
func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest) (*pb.OnCreateSetResponse, error) {
catalog := req.GetCatalog()
if catalog == nil {
return nil, errors.BadRequestStatus("catalog is required")
return nil, status.Error(codes.InvalidArgument, "catalog is nil")
}
catalogAttrsRaw := catalog.GetAttributes()
if catalogAttrsRaw == nil {
return nil, errors.BadRequestStatus("catalog attributes are required")
return nil, status.Error(codes.InvalidArgument, "catalog missing attributes")
}
catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw)
@@ -338,10 +305,9 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
credState, err := credential.AwsCredentialPersistedStateFromProto(
req.GetPersisted().GetSecrets(),
catalogAttributes.CredentialAttributes,
catalogAttributes.DualStack,
p.testCredStateOpts...)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
catalogState, err := newAwsCatalogPersistedState(
@@ -350,47 +316,56 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
}, p.testCatalogStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
set := req.GetSet()
if set == nil {
return nil, errors.BadRequestStatus("set is required")
return nil, status.Error(codes.InvalidArgument, "set is nil")
}
if set.GetAttributes() == nil {
return nil, errors.BadRequestStatus("set attributes are required")
return nil, status.Error(codes.InvalidArgument, "set missing attributes")
}
setAttrs, err := getSetAttributes(set.GetAttributes())
if err != nil {
return nil, err
}
describeInstanceFilters, err := buildFilters(setAttrs)
ec2Client, err := catalogState.EC2Client(ctx)
if err != nil {
return nil, errors.BadRequestStatus("error building set filters: %s", err)
return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
}
opts := []ec2Option{}
if catalogAttributes.DualStack {
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
input, err := buildDescribeInstancesInput(setAttrs, true)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error building DescribeInstances parameters: %s", err)
}
if st := dryRunValidation(ctx, catalogState, opts, describeInstanceFilters...); st != nil {
return nil, st.Err()
_, 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")
}
return &pb.OnCreateSetResponse{}, nil
var apiErr smithy.APIError
if errors.As(err, &apiErr) && apiErr.ErrorCode() == "DryRunOperation" {
// Success
return &pb.OnCreateSetResponse{}, nil
}
return nil, status.Errorf(codes.InvalidArgument, "error performing dry run of DescribeInstances: %s", err)
}
// OnUpdateSet is called when a dynamic host set is updated.
func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest) (*pb.OnUpdateSetResponse, error) {
catalog := req.GetCatalog()
if catalog == nil {
return nil, errors.BadRequestStatus("catalog is required")
return nil, status.Error(codes.InvalidArgument, "catalog is nil")
}
catalogAttrsRaw := catalog.GetAttributes()
if catalogAttrsRaw == nil {
return nil, errors.BadRequestStatus("catalog attributes are required")
return nil, status.Error(codes.InvalidArgument, "catalog missing attributes")
}
catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw)
@@ -401,10 +376,9 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
credState, err := credential.AwsCredentialPersistedStateFromProto(
req.GetPersisted().GetSecrets(),
catalogAttributes.CredentialAttributes,
catalogAttributes.DualStack,
p.testCredStateOpts...)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
catalogState, err := newAwsCatalogPersistedState(
@@ -413,38 +387,46 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
}, p.testCatalogStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
// As with catalog, we don't need to really look at the old host
// set here, just need to work off of/validate the new set config
set := req.GetNewSet()
if set == nil {
return nil, errors.BadRequestStatus("new set is required")
return nil, status.Error(codes.InvalidArgument, "new set is nil")
}
if set.GetAttributes() == nil {
return nil, errors.BadRequestStatus("new set attributes are required")
return nil, status.Error(codes.InvalidArgument, "new set missing attributes")
}
setAttrs, err := getSetAttributes(set.GetAttributes())
if err != nil {
return nil, err
}
describeInstanceFilters, err := buildFilters(setAttrs)
ec2Client, err := catalogState.EC2Client(ctx)
if err != nil {
return nil, errors.BadRequestStatus("error building set filters: %s", err)
return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
}
opts := []ec2Option{}
if catalogAttributes.DualStack {
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
}
if st := dryRunValidation(ctx, catalogState, opts, describeInstanceFilters...); st != nil {
return nil, st.Err()
input, err := buildDescribeInstancesInput(setAttrs, true)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error building DescribeInstances parameters: %s", err)
}
return &pb.OnUpdateSetResponse{}, nil
_, 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")
}
var apiErr smithy.APIError
if errors.As(err, &apiErr) && apiErr.ErrorCode() == "DryRunOperation" {
// Success
return &pb.OnUpdateSetResponse{}, nil
}
return nil, status.Errorf(codes.InvalidArgument, "error performing dry run of DescribeInstances: %s", err)
}
// OnDeleteSet is called when a dynamic host set is deleted.
@@ -458,12 +440,12 @@ func (p *HostPlugin) OnDeleteSet(ctx context.Context, req *pb.OnDeleteSetRequest
func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*pb.ListHostsResponse, error) {
catalog := req.GetCatalog()
if catalog == nil {
return nil, errors.BadRequestStatus("catalog is required")
return nil, status.Error(codes.InvalidArgument, "catalog is nil")
}
catalogAttrsRaw := catalog.GetAttributes()
if catalogAttrsRaw == nil {
return nil, errors.BadRequestStatus("catalog attributes are required")
return nil, status.Error(codes.InvalidArgument, "catalog missing attributes")
}
catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw)
@@ -474,10 +456,9 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
credState, err := credential.AwsCredentialPersistedStateFromProto(
req.GetPersisted().GetSecrets(),
catalogAttributes.CredentialAttributes,
catalogAttributes.DualStack,
p.testCredStateOpts...)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
catalogState, err := newAwsCatalogPersistedState(
@@ -486,12 +467,12 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
}, p.testCatalogStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
sets := req.GetSets()
if sets == nil {
return nil, errors.BadRequestStatus("sets are required")
return nil, status.Error(codes.InvalidArgument, "sets is nil")
}
// Build all the queries in advance.
@@ -506,11 +487,11 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
for i, set := range sets {
// Validate Id since we use it in output
if set.GetId() == "" {
return nil, errors.BadRequestStatus("set id is required")
return nil, status.Error(codes.InvalidArgument, "set missing id")
}
if set.GetAttributes() == nil {
return nil, errors.BadRequestStatus("set %s attributes are required", set.GetId())
return nil, status.Error(codes.InvalidArgument, "set missing attributes")
}
setAttrs, err := getSetAttributes(set.GetAttributes())
if err != nil {
@@ -519,7 +500,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
input, err := buildDescribeInstancesInput(setAttrs, false)
if err != nil {
return nil, errors.BadRequestStatus("error building DescribeInstances parameters for host set id %q: %s", set.GetId(), err)
return nil, status.Errorf(codes.InvalidArgument, "error building DescribeInstances parameters for host set id %q: %s", set.GetId(), err)
}
queries[i] = hostSetQuery{
Id: set.GetId(),
@@ -527,13 +508,9 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
}
}
opts := []ec2Option{}
if catalogAttributes.DualStack {
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
}
ec2Client, err := catalogState.EC2Client(ctx, opts...)
ec2Client, err := catalogState.EC2Client(ctx)
if err != nil {
return nil, errors.BadRequestStatus("error getting EC2 client: %s", err)
return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
}
// Run all queries now and assemble output.
@@ -541,7 +518,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
for i, query := range queries {
output, err := ec2Client.DescribeInstances(ctx, query.Input)
if err != nil {
return nil, errors.BadRequestStatus("error running DescribeInstances for host set id %q: %s", query.Id, err)
return nil, status.Errorf(codes.InvalidArgument, "error running DescribeInstances for host set id %q: %s", query.Id, err)
}
queries[i].Output = output
@@ -552,7 +529,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
for _, instance := range reservation.Instances {
host, err := awsInstanceToHost(instance)
if err != nil {
return nil, errors.BadRequestStatus("error processing host results for host set id %q: %s", query.Id, err)
return nil, status.Errorf(codes.InvalidArgument, "error processing host results for host set id %q: %s", query.Id, err)
}
queries[i].OutputHosts = append(queries[i].OutputHosts, host)
@@ -650,7 +627,7 @@ func buildDescribeInstancesInput(attrs *SetAttributes, dryRun bool) (*ec2.Descri
// populated.
func awsInstanceToHost(instance types.Instance) (*pb.ListHostsResponseHost, error) {
if aws.ToString(instance.InstanceId) == "" {
return nil, fmt.Errorf("response integrity error: missing instance id")
return nil, errors.New("response integrity error: missing instance id")
}
result := new(pb.ListHostsResponseHost)
@@ -698,28 +675,6 @@ func awsInstanceToHost(instance types.Instance) (*pb.ListHostsResponseHost, erro
return result, nil
}
// dryRunValidation performs an AWS DescribeInstances call to verify the state's
// credentials, the host listing functionality as well as the filters, if any
// are passed in. This function can therefore be used for both host catalog and
// host set validation.
func dryRunValidation(ctx context.Context, state *awsCatalogPersistedState, ec2Opts []ec2Option, filters ...types.Filter) *status.Status {
if state == nil {
return status.New(codes.InvalidArgument, "persisted state is required")
}
ec2Client, err := state.EC2Client(ctx, ec2Opts...)
if err != nil {
return status.New(codes.InvalidArgument, fmt.Sprintf("error getting EC2 client: %s", err))
}
_, err = ec2Client.DescribeInstances(ctx, &ec2.DescribeInstancesInput{Filters: filters})
if err != nil {
return status.New(codes.FailedPrecondition, fmt.Sprintf("aws describe instances failed: %s", err))
}
return 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 {
File diff suppressed because it is too large. Load diff
+16 -57
View File
@@ -7,10 +7,12 @@ import (
"context"
"errors"
"fmt"
"net"
"net/http"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
smithyEndpoints "github.com/aws/smithy-go/endpoints"
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"google.golang.org/protobuf/types/known/structpb"
@@ -20,6 +22,17 @@ type EC2API interface {
DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
}
var customClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
},
}
type awsCatalogPersistedState struct {
*credential.AwsCredentialPersistedState
@@ -73,11 +86,7 @@ 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(ctx context.Context, opt ...ec2Option) (EC2API, error) {
opts, err := getOpts(opt...)
if err != nil {
return nil, fmt.Errorf("error parsing options when fetching EC2 client: %w", err)
}
func (s *awsCatalogPersistedState) EC2Client(ctx context.Context) (EC2API, error) {
awsCfg, err := s.GenerateCredentialChain(ctx)
if err != nil {
return nil, fmt.Errorf("error getting AWS configuration when fetching EC2 client: %w", err)
@@ -85,58 +94,8 @@ func (s *awsCatalogPersistedState) EC2Client(ctx context.Context, opt ...ec2Opti
if awsCfg == nil {
return nil, fmt.Errorf("nil aws configuration")
}
var ec2Opts []func(*ec2.Options)
if opts.withDualStack {
ec2Opts = append(ec2Opts, ec2.WithEndpointResolverV2(&endpointResolver{
dualStack: opts.withDualStack,
}))
}
if s.testEC2APIFunc != nil {
return s.testEC2APIFunc(*awsCfg)
}
return ec2.NewFromConfig(*awsCfg, ec2Opts...), nil
}
// getOpts iterates the inbound ec2Options and returns a struct
func getOpts(opt ...ec2Option) (ec2Options, error) {
opts := getDefaultEc2Options()
for _, o := range opt {
if o == nil {
continue
}
if err := o(&opts); err != nil {
return ec2Options{}, err
}
}
return opts, nil
}
// ec2Option - how ec2Options are passed as arguments
type ec2Option func(*ec2Options) error
// options = how options are represented
type ec2Options struct {
withDualStack bool
}
func getDefaultEc2Options() ec2Options {
return ec2Options{}
}
// WithDualStack sets the dual stack resolver
func WithDualStack(with bool) ec2Option {
return func(o *ec2Options) error {
o.withDualStack = with
return nil
}
}
type endpointResolver struct {
dualStack bool
}
func (e *endpointResolver) ResolveEndpoint(ctx context.Context, params ec2.EndpointParameters) (resolver smithyEndpoints.Endpoint, err error) {
params.UseDualStack = aws.Bool(e.dualStack)
resolver, err = ec2.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params)
return
return ec2.NewFromConfig(*awsCfg), nil
}
+2 -2
View File
@@ -8,14 +8,14 @@ import (
"testing"
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil"
"github.com/stretchr/testify/require"
)
func TestAwsCatalogPersistedStateEC2Client(t *testing.T) {
require := require.New(t)
cred, err := credential.NewAwsCredentialPersistedState(
credential.WithCredentialsConfig(&awsutil.CredentialsConfig{
credential.WithCredentialsConfig(&awsutilv2.CredentialsConfig{
Region: "us-west-2",
AccessKey: "foobar",
SecretKey: "barfoo",
+1 -1
View File
@@ -68,7 +68,7 @@ The following `attributes` are valid on an AWS storage bucket resource:
- `disable_credential_rotation` (bool): If `true`, credential rotation will not
be performed.
- `region` (string): The region to configure the storage bucket for.
- `endpoint_url` (string): The endpoint to configure the storage.
- `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.
-10
View File
@@ -19,9 +19,6 @@ type StorageAttributes struct {
// EndpointUrl is used for configuring how the aws client will resolve requests.
EndpointUrl string
// DualStack is used for configuring how the aws client will resolve requests.
DualStack bool
}
func getStorageAttributes(in *structpb.Struct) (*StorageAttributes, error) {
@@ -40,12 +37,6 @@ func getStorageAttributes(in *structpb.Struct) (*StorageAttributes, error) {
}
delete(unknownFields, ConstAwsEndpointUrl)
dualStack, err := values.GetBoolValue(in, ConstAwsDualStack, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstAwsDualStack)] = err.Error()
}
delete(unknownFields, ConstAwsDualStack)
for s := range unknownFields {
switch s {
// Ignore knownFields from CredentialAttributes
@@ -73,6 +64,5 @@ func getStorageAttributes(in *structpb.Struct) (*StorageAttributes, error) {
return &StorageAttributes{
CredentialAttributes: credAttributes,
EndpointUrl: endpointUrl,
DualStack: dualStack,
}, nil
}
+2 -24
View File
@@ -7,8 +7,6 @@ import (
"testing"
cred "github.com/hashicorp/boundary-plugin-aws/internal/credential"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -21,7 +19,6 @@ func TestGetStorageAttributes(t *testing.T) {
in *structpb.Struct
expected *StorageAttributes
expectedErrContains string
expectedDetails *pb.StorageBucketCredentialState
}{
{
name: "missing region",
@@ -41,21 +38,6 @@ func TestGetStorageAttributes(t *testing.T) {
},
expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
},
{
name: "good w/ dualstack",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"dual_stack": structpb.NewBoolValue(true),
},
},
expected: &StorageAttributes{
CredentialAttributes: &cred.CredentialAttributes{
Region: "us-west-2",
},
DualStack: true,
},
},
{
name: "good w/ endpoint",
in: &structpb.Struct{
@@ -118,17 +100,13 @@ func TestGetStorageAttributes(t *testing.T) {
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require, assert := require.New(t), assert.New(t)
require := require.New(t)
actual, err := getStorageAttributes(tc.in)
if tc.expectedErrContains != "" {
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(codes.InvalidArgument, status.Code(err))
st, ok := status.FromError(err)
require.True(ok)
assert.Len(st.Details(), 0)
require.Equal(status.Code(err), codes.InvalidArgument)
return
}
-3
View File
@@ -7,9 +7,6 @@ const (
// ConstAwsEndpointUrl is the key for the endpoint url in the aws s3 client.
ConstAwsEndpointUrl = "endpoint_url"
// ConstAwsDualStack is the key for the dualstack flag in the aws s3 client.
ConstAwsDualStack = "dual_stack"
// defaultStreamChunkSize is the recommened chunk size for sending data through a stream
defaultStreamChunkSize = 65536 // 64 KiB
)
-71
View File
@@ -1,71 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package storage
import (
"github.com/hashicorp/boundary-plugin-aws/internal/errors"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
s3StatusErrMsg = "aws s3 error: %s"
// s3ErrorBadDigest is returned when there is a checksum
// mismatch between the request and the s3 service. This
// error code is returned for the PutObject request.
s3ErrorBadDigest = "BadDigest"
)
func setStatePermission(req any, permission *pb.Permission) *pb.StorageBucketCredentialState {
state := &pb.StorageBucketCredentialState{
State: &pb.Permissions{},
}
switch req.(type) {
case pb.HeadObjectRequest:
state.State.Read = permission
case *pb.HeadObjectRequest:
state.State.Read = permission
case pb.GetObjectRequest:
state.State.Read = permission
case *pb.GetObjectRequest:
state.State.Read = permission
case pb.PutObjectRequest:
state.State.Write = permission
case *pb.PutObjectRequest:
state.State.Write = permission
case pb.DeleteObjectsRequest:
state.State.Delete = permission
case *pb.DeleteObjectsRequest:
state.State.Delete = permission
}
return state
}
// parseS3Error will convert an S3 api error into a RPC status. If the
// error does not match against an s3 error type, it will fallback to
// evaluating against generic aws service errors. This includes throttling,
// credentials, and connectivity error types.
//
// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html
func parseS3Error(op string, err error, req any) *status.Status {
if err == nil {
return nil
}
st, p := errors.ParseAWSError(op, err)
if p != nil {
state := setStatePermission(req, p)
if st, err = st.WithDetails(state); err != nil {
st = status.New(codes.Internal, err.Error())
}
}
return st
}
func checksumMistmatchStatus() error {
return status.New(codes.Aborted, "mismatched checksum").Err()
}
-183
View File
@@ -1,183 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package storage
import (
"testing"
"github.com/hashicorp/boundary-plugin-aws/internal/errors"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/protobuf/types/known/timestamppb"
)
func Test_ParseS3Error(t *testing.T) {
t.Parallel()
cases := []struct {
name string
req any
err error
expectedStatusCode codes.Code
expectedStatusMsg string
expectedDetails *pb.StorageBucketCredentialState
}{
{
name: "nil-error",
expectedStatusCode: codes.OK,
},
{
name: "no-such-bucket",
req: &pb.GetObjectRequest{},
err: TestAwsS3Error("NoSuchBucket", "GetObject", "no such bucket"),
expectedStatusCode: codes.NotFound,
expectedStatusMsg: "aws service s3: test:",
expectedDetails: &pb.StorageBucketCredentialState{
State: &pb.Permissions{
Read: &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: "no such bucket",
CheckedAt: timestamppb.Now(),
},
},
},
},
{
name: "no-such-key",
req: &pb.GetObjectRequest{},
err: TestAwsS3Error("NoSuchKey", "GetObject", "no such key"),
expectedStatusCode: codes.NotFound,
expectedStatusMsg: "aws service s3: test:",
expectedDetails: &pb.StorageBucketCredentialState{
State: &pb.Permissions{
Read: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
CheckedAt: timestamppb.Now(),
},
},
},
},
{
name: "not-found",
req: &pb.GetObjectRequest{},
err: errors.TestAwsHttpResponseError(404, "not found"),
expectedStatusCode: codes.NotFound,
// cannot embed services within mock http errors, therefore unknown service
expectedStatusMsg: "aws service unknown: resource not found: test:",
expectedDetails: &pb.StorageBucketCredentialState{
State: &pb.Permissions{
Read: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
CheckedAt: timestamppb.Now(),
},
},
},
},
{
name: "invalid-object-state",
req: &pb.GetObjectRequest{},
err: TestAwsS3Error("InvalidObjectState", "GetObject", "invalid object state"),
expectedStatusCode: codes.NotFound,
expectedStatusMsg: "aws service s3: test:",
expectedDetails: &pb.StorageBucketCredentialState{
State: &pb.Permissions{
Read: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
CheckedAt: timestamppb.Now(),
},
},
},
},
{
name: "bad-digest",
req: &pb.PutObjectRequest{},
err: TestAwsS3Error(s3ErrorBadDigest, "PutObject", "checksum mismatch"),
expectedStatusCode: codes.Aborted,
expectedStatusMsg: "aws service s3: test:",
expectedDetails: &pb.StorageBucketCredentialState{
State: &pb.Permissions{
Write: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
CheckedAt: timestamppb.Now(),
},
},
},
},
{
name: "fallback-throttle-error",
req: &pb.GetObjectRequest{},
err: TestAwsS3Error("Throttling", "action", "throttling exception"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service s3: throttling: test:",
expectedDetails: &pb.StorageBucketCredentialState{
State: &pb.Permissions{
Read: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
CheckedAt: timestamppb.Now(),
},
},
},
},
{
name: "fallback-connectivity-error",
req: &pb.GetObjectRequest{},
err: TestAwsS3Error("RequestTimeout", "action", "request timeout"),
expectedStatusCode: codes.DeadlineExceeded,
expectedStatusMsg: "aws service s3: test:",
expectedDetails: &pb.StorageBucketCredentialState{
State: &pb.Permissions{
Read: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
CheckedAt: timestamppb.Now(),
},
},
},
},
{
name: "fallback-credentials-error",
req: &pb.GetObjectRequest{},
err: TestAwsS3Error("AccessDenied", "action", "access denied"),
expectedStatusCode: codes.PermissionDenied,
expectedStatusMsg: "aws service s3: invalid credentials: test:",
expectedDetails: &pb.StorageBucketCredentialState{
State: &pb.Permissions{
Read: &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: "access denied",
CheckedAt: timestamppb.Now(),
},
},
},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
require, assert := require.New(t), assert.New(t)
actualStatus := parseS3Error("test", tt.err, tt.req)
if tt.expectedStatusCode == codes.OK {
require.Nil(actualStatus)
return
}
require.Equal(tt.expectedStatusCode, actualStatus.Code())
require.Contains(actualStatus.Message(), tt.expectedStatusMsg)
require.Len(actualStatus.Details(), 1)
foundDetail := false
for _, detail := range actualStatus.Details() {
switch errDetail := detail.(type) {
case *pb.StorageBucketCredentialState:
foundDetail = true
errors.CheckSimilarPermission(assert, tt.expectedDetails.State.Read, errDetail.State.Read, false)
errors.CheckSimilarPermission(assert, tt.expectedDetails.State.Write, errDetail.State.Write, false)
errors.CheckSimilarPermission(assert, tt.expectedDetails.State.Delete, errDetail.State.Delete, false)
}
}
require.True(foundDetail, "did not find error details with status")
})
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+22 -47
View File
@@ -7,7 +7,10 @@ import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
@@ -22,18 +25,17 @@ type S3API interface {
GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
HeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options)) (*s3.HeadObjectOutput, error)
PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
DeleteObjects(ctx context.Context, params *s3.DeleteObjectsInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectsOutput, error)
ListObjectsV2(ctx context.Context, params *s3.ListObjectsV2Input, optFns ...func(*s3.Options)) (*s3.ListObjectsV2Output, error)
Credentials() aws.Credentials
}
type s3Client struct {
*s3.Client
creds aws.Credentials
}
func (c *s3Client) Credentials() aws.Credentials {
return c.creds
var customClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
},
}
type awsStoragePersistedState struct {
@@ -80,6 +82,7 @@ func newAwsStoragePersistedState(opts ...awsStoragePersistedStateOption) (*awsSt
}
func (s *awsStoragePersistedState) toProto() (*storagebuckets.StorageBucketPersisted, error) {
data, err := structpb.NewStruct(s.ToMap())
if err != nil {
return nil, fmt.Errorf("error converting state to structpb.Struct: %w", err)
@@ -87,9 +90,9 @@ func (s *awsStoragePersistedState) toProto() (*storagebuckets.StorageBucketPersi
return &storagebuckets.StorageBucketPersisted{Data: data}, nil
}
// s3Client returns a configured S3 client based on the session
// S3Client returns a configured S3 client based on the session
// information stored in the state.
func (s *awsStoragePersistedState) s3Client(ctx context.Context, opt ...s3Option) (S3API, error) {
func (s *awsStoragePersistedState) S3Client(ctx context.Context, opt ...s3Option) (S3API, error) {
opts, err := getOpts(opt...)
if err != nil {
return nil, fmt.Errorf("error parsing options when fetching S3 client: %w", err)
@@ -102,25 +105,15 @@ func (s *awsStoragePersistedState) s3Client(ctx context.Context, opt ...s3Option
return nil, fmt.Errorf("nil aws configuration")
}
var s3Opts []func(*s3.Options)
if opts.withEndpoint != "" || opts.withDualStack {
if opts.withEndpoint != "" {
s3Opts = append(s3Opts, s3.WithEndpointResolverV2(&endpointResolver{
endpoint: opts.withEndpoint,
dualStack: opts.withDualStack,
endpoint: opts.withEndpoint,
}))
}
if s.testS3APIFunc != nil {
return s.testS3APIFunc(*awsCfg)
}
// Retrieve the credentials provider from the client
credsProvider := awsCfg.Credentials
creds, err := credsProvider.Retrieve(ctx)
if err != nil {
return nil, fmt.Errorf("failed to retrieve credentials: %v\n", err)
}
c := s3.NewFromConfig(*awsCfg, s3Opts...)
return &s3Client{Client: c, creds: creds}, nil
return s3.NewFromConfig(*awsCfg, s3Opts...), nil
}
// getOpts iterates the inbound s3Options and returns a struct
@@ -142,23 +135,13 @@ type s3Option func(*s3Options) error
// options = how options are represented
type s3Options struct {
withEndpoint string
withDualStack bool
withCacheRefresh bool
withEndpoint string
}
func getDefaultS3Options() s3Options {
return s3Options{}
}
// WithCacheRefresh controls if the cache should be forced to refresh
func WithCacheRefresh(refresh bool) s3Option {
return func(o *s3Options) error {
o.withCacheRefresh = refresh
return nil
}
}
// WithEndpoint contains the endpoint to use
func WithEndpoint(with string) s3Option {
return func(o *s3Options) error {
@@ -167,27 +150,18 @@ func WithEndpoint(with string) s3Option {
}
}
// WithDualStack sets the dual stack resolver
func WithDualStack(with bool) s3Option {
return func(o *s3Options) error {
o.withDualStack = with
return nil
}
}
type endpointResolver struct {
endpoint string
dualStack bool
endpoint string
}
func (e *endpointResolver) ResolveEndpoint(ctx context.Context, params s3.EndpointParameters) (resolver smithyEndpoints.Endpoint, err error) {
var uri *url.URL
params.UseDualStack = aws.Bool(e.dualStack)
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)
@@ -195,6 +169,7 @@ func (e *endpointResolver) ResolveEndpoint(ctx context.Context, params s3.Endpoi
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{
+12 -12
View File
@@ -8,34 +8,34 @@ import (
"testing"
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil"
"github.com/stretchr/testify/require"
)
func TestAwsStoragePersistedStateS3Client(t *testing.T) {
cases := []struct {
name string
creds *awsutil.CredentialsConfig
creds *awsutilv2.CredentialsConfig
opts []awsStoragePersistedStateOption
awsOpts []awsutil.Option
awsOpts []awsutilv2.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"),
creds: func() *awsutilv2.CredentialsConfig {
creds, err := awsutilv2.NewCredentialsConfig(
awsutilv2.WithRegion("us-west-2"),
awsutilv2.WithAccessKey("foobar"),
awsutilv2.WithSecretKey("barfoo"),
)
require.NoError(t, err)
return creds
}(),
awsOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(),
awsOpts: []awsutilv2.Option{
awsutilv2.WithIAMAPIFunc(
awsutilv2.NewMockIAM(),
),
},
opts: []awsStoragePersistedStateOption{
@@ -64,7 +64,7 @@ func TestAwsStoragePersistedStateS3Client(t *testing.T) {
)
require.NoError(err)
clientRaw, err := state.s3Client(context.Background(), tc.s3Opts...)
clientRaw, err := state.S3Client(context.Background(), tc.s3Opts...)
require.NoError(err)
require.NotNil(clientRaw)
client, ok := clientRaw.(*testMockS3)
+23 -134
View File
@@ -14,11 +14,10 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/aws/smithy-go"
"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/v2"
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/structpb"
@@ -29,8 +28,22 @@ const (
testGetUserErr = "test error for GetUser"
testGetCallerIdentityErr = "test error for GetCallerIdentity"
testDeleteAccessKeyErr = "test error for DeleteAccessKey"
testGetObjectErr = "test error for GetObject"
testPutObjectErr = "test error for PutObject"
testHeadObjectErr = "test error for HeadObject"
)
// throttleErr is a mocked error used for testing the aws s3 client
type throttleErr struct{}
func (e *throttleErr) ErrorCode() string {
return "ThrottlingException"
}
func (e *throttleErr) Error() string {
return "ThrottlingException"
}
type testMockS3State struct {
GetObjectCalled bool
GetObjectInputParams *s3.GetObjectInput
@@ -41,14 +54,6 @@ type testMockS3State struct {
HeadObjectCalled bool
HeadObjectInputParams *s3.HeadObjectInput
ListObjectsV2Called bool
ListObjectsV2InputParams *s3.ListObjectsV2Input
DeleteObjectCalled bool
DeleteObjectInputParams *s3.DeleteObjectInput
DeleteObjectsCalled bool
DeleteObjectsInputParams *s3.DeleteObjectsInput
}
func (s *testMockS3State) Reset() {
@@ -59,12 +64,6 @@ func (s *testMockS3State) Reset() {
s.PutObjectBody = nil
s.HeadObjectCalled = false
s.HeadObjectInputParams = nil
s.ListObjectsV2Called = false
s.ListObjectsV2InputParams = nil
s.DeleteObjectCalled = false
s.DeleteObjectInputParams = nil
s.DeleteObjectsCalled = false
s.DeleteObjectsInputParams = nil
}
type testMockS3 struct {
@@ -84,18 +83,6 @@ type testMockS3 struct {
// mocked responses for headObject
HeadObjectOutput *s3.HeadObjectOutput
HeadObjectErr error
// mocked responses for ListObjectsV2 (needed for delete)
ListObjectsV2Output *s3.ListObjectsV2Output
ListObjectsV2Err error
// sometimes we need to create a wildcard output
ListObjectsV2OutputFunc func(*s3.ListObjectsV2Input) *s3.ListObjectsV2Output
// mocked responses for DeleteObject(s)
DeleteObjectOutput *s3.DeleteObjectOutput
DeleteObjectErr error
DeleteObjectsOutput *s3.DeleteObjectsOutput
DeleteObjectsErr error
}
type testMockS3Option func(m *testMockS3) error
@@ -142,55 +129,6 @@ func testMockS3WithHeadObjectError(e error) testMockS3Option {
}
}
func testMockS3WithListObjectsV2Output(o *s3.ListObjectsV2Output) testMockS3Option {
return func(m *testMockS3) error {
m.ListObjectsV2Output = o
return nil
}
}
func testMockS3WithListObjectsV2Error(e error) testMockS3Option {
return func(m *testMockS3) error {
m.ListObjectsV2Err = e
return nil
}
}
func testMockS3WithListObjectsV2OutputFunc(o func(*s3.ListObjectsV2Input) *s3.ListObjectsV2Output) testMockS3Option {
return func(m *testMockS3) error {
m.ListObjectsV2OutputFunc = o
return nil
}
}
func testMockS3WithDeleteObjectOutput(o *s3.DeleteObjectOutput) testMockS3Option {
return func(m *testMockS3) error {
m.DeleteObjectOutput = o
return nil
}
}
func testMockS3WithDeleteObjectError(e error) testMockS3Option {
return func(m *testMockS3) error {
m.DeleteObjectErr = e
return nil
}
}
func testMockS3WithDeleteObjectsOutput(o *s3.DeleteObjectsOutput) testMockS3Option {
return func(m *testMockS3) error {
m.DeleteObjectsOutput = o
return nil
}
}
func testMockS3WithDeleteObjectsError(e error) testMockS3Option {
return func(m *testMockS3) error {
m.DeleteObjectsErr = e
return nil
}
}
func newTestMockS3(state *testMockS3State, opts ...testMockS3Option) s3APIFunc {
return func(cfgs ...aws.Config) (S3API, error) {
m := &testMockS3{
@@ -208,6 +146,9 @@ func newTestMockS3(state *testMockS3State, opts ...testMockS3Option) s3APIFunc {
if cfg.Region != "" {
m.Region = cfg.Region
}
// if cfg. != nil {
// m.Endpoint = cfg.EndpointResolverWithOptions
// }
}
return m, nil
@@ -255,47 +196,8 @@ func (m *testMockS3) PutObject(ctx context.Context, params *s3.PutObjectInput, o
return m.PutObjectOutput, nil
}
func (m *testMockS3) ListObjectsV2(ctx context.Context, params *s3.ListObjectsV2Input, optFns ...func(*s3.Options)) (*s3.ListObjectsV2Output, error) {
if m.State != nil {
m.State.ListObjectsV2Called = true
m.State.ListObjectsV2InputParams = params
}
if m.ListObjectsV2Err != nil {
return nil, m.ListObjectsV2Err
}
if m.ListObjectsV2OutputFunc != nil {
return m.ListObjectsV2OutputFunc(params), nil
}
return m.ListObjectsV2Output, nil
}
func (m *testMockS3) DeleteObject(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error) {
if m.State != nil {
m.State.DeleteObjectCalled = true
m.State.DeleteObjectInputParams = params
}
if m.DeleteObjectErr != nil {
return nil, m.DeleteObjectErr
}
return m.DeleteObjectOutput, nil
}
func (m *testMockS3) DeleteObjects(ctx context.Context, params *s3.DeleteObjectsInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectsOutput, error) {
if m.State != nil {
m.State.DeleteObjectsCalled = true
m.State.DeleteObjectsInputParams = params
}
if m.DeleteObjectsErr != nil {
return nil, m.DeleteObjectsErr
}
return m.DeleteObjectsOutput, nil
return &s3.DeleteObjectOutput{}, nil
}
func (m *testMockS3) HeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options)) (*s3.HeadObjectOutput, error) {
@@ -385,10 +287,10 @@ func deepCopyPutObjectRequest(v *pb.PutObjectRequest) *pb.PutObjectRequest {
func validSTSMock() []credential.AwsCredentialPersistedStateOption {
return []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithSTSAPIFunc(
awsutil.NewMockSTS(
awsutil.WithGetCallerIdentityOutput(&sts.GetCallerIdentityOutput{
credential.WithStateTestOpts([]awsutilv2.Option{
awsutilv2.WithSTSAPIFunc(
awsutilv2.NewMockSTS(
awsutilv2.WithGetCallerIdentityOutput(&sts.GetCallerIdentityOutput{
Account: aws.String("0123456789"),
Arn: aws.String("arn:aws:iam::0123456789:user/test"),
UserId: aws.String("test"),
@@ -398,16 +300,3 @@ func validSTSMock() []credential.AwsCredentialPersistedStateOption {
}),
}
}
// TestAwsS3Error returns an s3 api error
func TestAwsS3Error(code, op, msg string) error {
return &smithy.OperationError{
ServiceID: "s3",
OperationName: op,
Err: &smithy.GenericAPIError{
Code: code,
Message: msg,
Fault: smithy.FaultServer,
},
}
}
+7 -112
View File
@@ -17,16 +17,12 @@ import (
"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/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb"
)
// NOTE: this test does not vaildate against dynamic credentials because
// this credential type can only be tested on an EC2 instance.
func TestStoragePlugin(t *testing.T) {
region := os.Getenv("AWS_REGION")
if region == "" {
@@ -79,12 +75,6 @@ func TestStoragePlugin(t *testing.T) {
iamSecretAccessKeyMissingPutObject, err := tf.GetOutputString("iam_secret_access_key_missing_put_obj")
require.NoError(err)
iamAccessKeyMissingDeleteObject, err := tf.GetOutputString("iam_access_key_missing_delete_obj")
require.NoError(err)
iamSecretAccessKeyMissingDeleteObject, err := tf.GetOutputString("iam_secret_access_key_missing_delete_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
@@ -131,7 +121,7 @@ func TestStoragePlugin(t *testing.T) {
testPluginOnDeleteStorageBucket(ctx, t, p, bucketName, region, keyid, secret, true)
// ********************
// * Object Methods: PutObject, GetObject, HeadObject, DeleteObjects
// * Object Methods: PutObject, GetObject, HeadObject
// ********************
//
// Reassign the keyid and secret first.
@@ -142,59 +132,11 @@ func TestStoragePlugin(t *testing.T) {
// * Validate Permissions
// ********************
//
var expectedSBCState *pb.StorageBucketCredentialState
// Validate error is returned for missing get object permission
expectedSBCState = &pb.StorageBucketCredentialState{
State: &pb.Permissions{
Write: &pb.Permission{
State: pb.StateType_STATE_TYPE_OK,
},
Read: &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
},
Delete: &pb.Permission{
State: pb.StateType_STATE_TYPE_OK,
},
},
}
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingGetObject, iamSecretAccessKeyMissingGetObject, expectedSBCState)
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingGetObject, iamSecretAccessKeyMissingGetObject)
// Validate error is returned for missing put object permission
expectedSBCState = &pb.StorageBucketCredentialState{
State: &pb.Permissions{
Write: &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
},
Read: &pb.Permission{
// Since we cannot write an object to S3,
// we cannot test against the ability to read from S3.
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
Delete: &pb.Permission{
// We can still try deleting an object that does
// not exist in the S3 bucket to validate access.
State: pb.StateType_STATE_TYPE_OK,
},
},
}
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingPutObject, iamSecretAccessKeyMissingPutObject, expectedSBCState)
// Validate error is returned for missing delete object permission
expectedSBCState = &pb.StorageBucketCredentialState{
State: &pb.Permissions{
Write: &pb.Permission{
State: pb.StateType_STATE_TYPE_OK,
},
Read: &pb.Permission{
State: pb.StateType_STATE_TYPE_OK,
},
Delete: &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
},
},
}
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingDeleteObject, iamSecretAccessKeyMissingDeleteObject, expectedSBCState)
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) {
@@ -347,12 +289,10 @@ func testPluginOnDeleteStorageBucket(ctx context.Context, t *testing.T, p *stora
}
}
func testPluginValidatePermissions(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string, expectedState *pb.StorageBucketCredentialState) {
func testPluginValidatePermissions(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string) {
t.Helper()
t.Logf("testing ValidatePermissions")
require, assert := require.New(t), assert.New(t)
require.NotNil(expectedState)
require := require.New(t)
reqAttrs, err := structpb.NewStruct(map[string]any{
credential.ConstRegion: region,
@@ -375,51 +315,6 @@ func testPluginValidatePermissions(ctx context.Context, t *testing.T, p *storage
_, err = p.ValidatePermissions(ctx, req)
require.Error(err)
var actualSBCState *pb.StorageBucketCredentialState
if st, ok := status.FromError(err); ok {
for _, detail := range st.Details() {
if statusDetail, ok := detail.(*pb.StorageBucketCredentialState); ok {
actualSBCState = statusDetail
break
}
}
}
require.NotNil(actualSBCState)
require.NotNil(actualSBCState.GetState())
expectedReadState := expectedState.GetState().GetRead()
if expectedReadState != nil {
actualReadState := actualSBCState.GetState().GetRead()
require.NotNil(actualReadState)
assert.Equal(expectedReadState.GetState(), actualReadState.GetState())
assert.NotEmpty(actualReadState.GetCheckedAt())
if expectedReadState.GetState() == pb.StateType_STATE_TYPE_ERROR {
assert.NotEmpty(actualReadState.GetErrorDetails())
}
}
expectedWriteState := expectedState.GetState().GetWrite()
if expectedWriteState != nil {
actualWriteState := actualSBCState.GetState().GetWrite()
require.NotNil(actualWriteState)
assert.Equal(expectedWriteState.GetState(), actualWriteState.GetState())
assert.NotEmpty(actualWriteState.GetCheckedAt())
if expectedWriteState.GetState() == pb.StateType_STATE_TYPE_ERROR {
assert.NotEmpty(actualWriteState.GetErrorDetails())
}
}
expectedDeleteState := expectedState.GetState().GetDelete()
if expectedDeleteState != nil {
actualDeleteState := actualSBCState.GetState().GetDelete()
require.NotNil(actualDeleteState)
assert.Equal(expectedDeleteState.GetState(), actualDeleteState.GetState())
assert.NotEmpty(actualDeleteState.GetCheckedAt())
if expectedDeleteState.GetState() == pb.StateType_STATE_TYPE_ERROR {
assert.NotEmpty(actualDeleteState.GetErrorDetails())
}
}
}
func testPluginObjectMethods(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string) {
@@ -474,7 +369,7 @@ func testPluginObjectMethods(ctx context.Context, t *testing.T, p *storage.Stora
// Need to create a GRPC server to test GetObject
t.Logf("testing GetObject")
lis, err := net.Listen("tcp", "[::1]:2030")
lis, err := net.Listen("tcp", "localhost:2030")
require.NoError(err)
grpcServer := grpc.NewServer()
pb.RegisterStoragePluginServiceServer(grpcServer, &storage.StoragePlugin{})
@@ -482,7 +377,7 @@ func testPluginObjectMethods(ctx context.Context, t *testing.T, p *storage.Stora
grpcServer.Serve(lis)
}()
conn, err := grpc.Dial("[::1]:2030", grpc.WithTransportCredentials(insecure.NewCredentials()))
conn, err := grpc.Dial("localhost:2030", grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(err)
defer conn.Close()
client := pb.NewStoragePluginServiceClient(conn)
+12 -97
View File
@@ -32,9 +32,7 @@ resource "aws_iam_policy" "valid" {
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:GetObjectAttributes",
"s3:ListBucket"
"s3:GetObjectAttributes"
],
"Effect": "Allow",
"Resource": "${aws_s3_bucket.test.arn}*"
@@ -81,9 +79,7 @@ resource "aws_iam_policy" "missing_put_obj" {
{
"Action": [
"s3:GetObject",
"s3:GetObjectAttributes",
"s3:ListBucket",
"s3:DeleteObject"
"s3:GetObjectAttributes"
],
"Effect": "Allow",
"Resource": "${aws_s3_bucket.test.arn}*"
@@ -129,7 +125,7 @@ resource "aws_iam_policy" "missing_get_obj" {
{
"Action": [
"s3:PutObject",
"s3:DeleteObject"
"s3:GetObjectAttributes"
],
"Effect": "Allow",
"Resource": "${aws_s3_bucket.test.arn}*"
@@ -145,69 +141,18 @@ resource "aws_iam_role_policy_attachment" "missing_get_obj" {
policy_arn = aws_iam_policy.missing_get_obj.arn
}
resource "aws_iam_role" "missing_delete_obj" {
name = "${random_id.prefix.dec}-missing-delete-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_delete_obj" {
name = "${random_id.prefix.dec}-missing-delete-obj"
tags = local.tags
policy = <<EOT
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:GetObjectAttributes",
"s3:ListBucket"
],
"Effect": "Allow",
"Resource": "${aws_s3_bucket.test.arn}*"
}
]
}
EOT
}
resource "aws_iam_role_policy_attachment" "missing_delete_obj" {
role = aws_iam_role.missing_delete_obj.name
policy_arn = aws_iam_policy.missing_delete_obj.arn
}
# Static Credential Testing
resource "random_id" "user_name" {
count = var.iam_user_count
prefix = "demo-${local.hashicorp_email}-boundary-iam-user"
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
permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/DemoUser"
tags = {
"boundary-demo" = local.hashicorp_email
}
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" {
@@ -250,44 +195,17 @@ resource "aws_iam_user_policy_attachment" "test_credentials" {
policy_arn = aws_iam_policy.credentials[count.index].arn
}
# Edge case for missing DeleteObject permission
resource "random_id" "missing_delete_obj" {
prefix = "demo-${local.hashicorp_email}-boundary-iam-user"
byte_length = 4
}
resource "aws_iam_user" "missing_delete_obj" {
name = random_id.missing_delete_obj.dec
force_destroy = true
permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/DemoUser"
tags = {
"boundary-demo" = local.hashicorp_email
}
}
resource "aws_iam_access_key" "missing_delete_obj" {
user = aws_iam_user.missing_delete_obj.name
}
resource "aws_iam_user_policy_attachment" "missing_delete_obj" {
user = aws_iam_user.missing_delete_obj.name
policy_arn = aws_iam_policy.missing_delete_obj.arn
}
# Edge case for missing GetObject permission
resource "random_id" "missing_get_obj" {
prefix = "demo-${local.hashicorp_email}-boundary-iam-user"
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
permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/DemoUser"
tags = {
"boundary-demo" = local.hashicorp_email
}
tags = local.tags
}
resource "aws_iam_access_key" "missing_get_obj" {
@@ -302,17 +220,14 @@ resource "aws_iam_user_policy_attachment" "missing_get_obj" {
# Edge case for missing PutObject permission
resource "random_id" "missing_put_obj" {
prefix = "demo-${local.hashicorp_email}-boundary-iam-user"
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
permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/DemoUser"
tags = {
"boundary-demo" = local.hashicorp_email
}
tags = local.tags
}
resource "aws_iam_access_key" "missing_put_obj" {
+1 -15
View File
@@ -13,14 +13,10 @@ output "missing_put_obj_policy_arn" {
value = aws_iam_role.missing_put_obj.arn
}
output "missing_get_obj_policy_arn" {
output "missing_put_get_policy_arn" {
value = aws_iam_role.missing_get_obj.arn
}
output "missing_delete_obj_policy_arn" {
value = aws_iam_role.missing_delete_obj.arn
}
output "iam_user_names" {
value = aws_iam_user.test.*.name
}
@@ -57,14 +53,4 @@ output "iam_access_key_missing_get_obj" {
output "iam_secret_access_key_missing_get_obj" {
value = aws_iam_access_key.missing_get_obj.secret
sensitive = true
}
output "iam_access_key_missing_delete_obj" {
value = aws_iam_access_key.missing_delete_obj.id
sensitive = true
}
output "iam_secret_access_key_missing_delete_obj" {
value = aws_iam_access_key.missing_delete_obj.secret
sensitive = true
}
+1 -9
View File
@@ -4,14 +4,6 @@
resource "aws_s3_bucket" "test" {
bucket = random_id.prefix.dec
force_destroy = true
acl = "private"
tags = local.tags
}
resource "aws_s3_bucket_public_access_block" "secure_access" {
bucket = aws_s3_bucket.test.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
+4 -6
View File
@@ -4,7 +4,7 @@
data "aws_caller_identity" "current" {}
resource "random_id" "prefix" {
prefix = "aws-plugin-e2e-test"
prefix = "storage-e2e-test"
byte_length = 4
}
@@ -13,10 +13,8 @@ variable "iam_user_count" {
}
locals {
hashicorp_email = split(":", data.aws_caller_identity.current.user_id)[1]
tags = {
User = split(":", data.aws_caller_identity.current.user_id)[1]
Repo = "boundary-plugin-aws"
Environment = "Demo"
user_id = split(":", data.aws_caller_identity.current.user_id)[1]
repo = "boundary-plugin-aws"
}
}
}
+7 -7
View File
@@ -16,7 +16,7 @@ import (
"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"
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil"
tfjson "github.com/hashicorp/terraform-json"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
@@ -280,9 +280,9 @@ func requireCredentialsInvalid(t *testing.T, accessKeyId, secretAccessKey string
t.Helper()
require := require.New(t)
c, err := awsutil.NewCredentialsConfig(
awsutil.WithAccessKey(accessKeyId),
awsutil.WithSecretKey(secretAccessKey),
c, err := awsutilv2.NewCredentialsConfig(
awsutilv2.WithAccessKey(accessKeyId),
awsutilv2.WithSecretKey(secretAccessKey),
)
require.NoError(err)
@@ -317,9 +317,9 @@ func requireCredentialsValid(t *testing.T, accessKeyId, secretAccessKey string)
t.Helper()
require := require.New(t)
c, err := awsutil.NewCredentialsConfig(
awsutil.WithAccessKey(accessKeyId),
awsutil.WithSecretKey(secretAccessKey),
c, err := awsutilv2.NewCredentialsConfig(
awsutilv2.WithAccessKey(accessKeyId),
awsutilv2.WithSecretKey(secretAccessKey),
)
require.NoError(err)
_, err = c.GetCallerIdentity(context.Background())