Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46b9666ad3 |
No files matched your search
@@ -1,11 +0,0 @@
|
||||
## PCI review checklist
|
||||
|
||||
<!-- heimdall_github_prtemplate:grc-pci_dss-2024-01-05 -->
|
||||
|
||||
- [ ] I have documented a clear reason for, and description of, the change I am making.
|
||||
|
||||
- [ ] If applicable, I've documented a plan to revert these changes if they require more than reverting the pull request.
|
||||
|
||||
- [ ] If applicable, I've documented the impact of any changes to security controls.
|
||||
|
||||
Examples of changes to security controls include using new access control methods, adding or removing logging pipelines, etc.
|
||||
@@ -4,8 +4,6 @@ on:
|
||||
- workflow_dispatch
|
||||
- push
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -15,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
|
||||
|
||||
@@ -4,12 +4,3 @@ plan.tfplan
|
||||
*.log
|
||||
*.out
|
||||
*.html
|
||||
|
||||
# IntelliJ IDEA project files
|
||||
.idea
|
||||
*.iprg
|
||||
|
||||
.DS_Store
|
||||
|
||||
.env
|
||||
.envrc
|
||||
+2
-1
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright IBM Corp. 2021, 2026
|
||||
Copyright (c) 2021 HashiCorp, Inc.
|
||||
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
@@ -5,32 +5,15 @@ Boundary](https://www.boundaryproject.io/).
|
||||
|
||||
## Credential Rotation
|
||||
|
||||
This is the following priority for the credential chain:
|
||||
static, assume role, environment variables.
|
||||
|
||||
### Static Credentials
|
||||
|
||||
Although static credentials are stored encrypted within Boundary, by default this
|
||||
Although credentials are stored encrypted within Boundary, by default this
|
||||
plugin will attempt to rotate credentials when they are supplied through the
|
||||
`secrets` object. The given credentials will be used to create a new credential,
|
||||
and then the given credential will be revoked. In this way, after rotation,
|
||||
only Boundary knows the client secret in use by this plugin. More information
|
||||
about AWS static credentials can be found [here](https://docs.aws.amazon.com/sdkref/latest/guide/feature-static-credentials.html).
|
||||
only Boundary knows the client secret in use by this plugin.
|
||||
|
||||
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
|
||||
|
||||
This plugin will attempt to assume a role when a `role_arn` is supplied through the
|
||||
`attributes` object. More information about assume an AWS role can be found [here](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html). This feature only works when the plugin is running on a self managed Boundary [worker](https://developer.hashicorp.com/boundary/tutorials/hcp-administration/hcp-manage-workers).
|
||||
|
||||
### Environment Credentials
|
||||
|
||||
This plugin will attempt to retrieve credentials from environment variables. More
|
||||
information about environment variables for AWS credentials can be found [here](https://docs.aws.amazon.com/sdkref/latest/guide/feature-static-credentials.html). This feature only works when the plugin is running on
|
||||
a self managed Boundary [worker](https://developer.hashicorp.com/boundary/tutorials/hcp-administration/hcp-manage-workers).
|
||||
|
||||
## Dynamic Hosts
|
||||
|
||||
This plugin supports dynamically sourcing hosts from Amazon EC2.
|
||||
@@ -48,7 +31,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 +43,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 +59,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)
|
||||
@@ -1,69 +1,64 @@
|
||||
module github.com/hashicorp/boundary-plugin-aws
|
||||
|
||||
go 1.25.7
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.7
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.254.0
|
||||
github.com/aws/aws-sdk-go-v2/service/iam v1.47.6
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5
|
||||
github.com/aws/smithy-go v1.25.1
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/boundary/sdk v0.0.59
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.1.2
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.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.17.7
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.19
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.18
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.31.1
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/hashicorp/boundary/sdk v0.0.33
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6
|
||||
github.com/hashicorp/terraform-json v0.14.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
google.golang.org/grpc v1.79.3
|
||||
google.golang.org/protobuf v1.36.11
|
||||
github.com/stretchr/testify v1.8.2
|
||||
google.golang.org/grpc v1.53.0
|
||||
google.golang.org/protobuf v1.28.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // 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.4.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // 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.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.26 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 // indirect
|
||||
github.com/aws/smithy-go v1.13.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fatih/color v1.18.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.11 // 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.20 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 // 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/kr/text v0.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // 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.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/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/zclconf/go-cty v1.15.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // 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
|
||||
)
|
||||
@@ -1,171 +1,150 @@
|
||||
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.39.1 h1:fWZhGAwVRK/fAN2tmt7ilH4PPAE11rDj7HytrmbZ2FE=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY=
|
||||
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.4.8 h1:6bgAZgRyT4RoFWhxS+aoGMFyE0cD1bSzFnEEi4bFPGI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 h1:HhJYoES3zOz34yWEpGENqJvRVPqpmJyR3+AFg9ybhdY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk=
|
||||
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.4.8 h1:1/bT9kDdLQzfZ1e6J6hpW+SfNDd6xrV8F3M2CuGyUz8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.8/go.mod h1:RbdwTONAIi59ej/+1H+QzZORt5bcyAtbrS7FQb2pvz0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.254.0 h1:fTLR6dLDTGChAjecRPlVrKeznT0rVdzR4yn9Z68MTGk=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.254.0/go.mod h1:V0jbRy1/IPapnkqgXSwVOFB+u5pnCwd9S+R3pKWULC4=
|
||||
github.com/aws/aws-sdk-go-v2/service/iam v1.47.6 h1:EWehQXACWr+6hzfZPwZChlfoVhiUCfLHE0Xh3kAfzWQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/iam v1.47.6/go.mod h1:qRXgEBWPIltrWHQwU+HkyBvwh1QgeigFcaCGCIVrWk0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.8 h1:tIN8MFT1z5STK5kTdOT1TCfMN/bn5fSEnlKsTL8qBOU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.8/go.mod h1:VKS56txtNWjKI8FqD/hliL0BcshyF4ZaLBa1rm2Y+5s=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 h1:ieLCO1JxUWuxTZ1cRd0GAaeX7O6cIxnwk7tc1LsQhC4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15/go.mod h1:e3IzZvQ3kAWNykvE0Tr0RDZCMFInMvhku3qNpcIQXhM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 h1:M6JI2aGFEzYxsF6CXIuRBnkge9Wf9a2xU39rNeXgu10=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8/go.mod h1:Fw+MyTwlwjFsSTE31mH211Np+CUslml8mzc0AFEG09s=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.8 h1:AgYCo1Rb8XChJXA871BXHDNxNWOTAr6V5YdsRIBbgv0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.8/go.mod h1:Au9dvIGm1Hbqnt29d3VakOCQuN9l0WrkDDTRq8biWS4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mwgx5ToK7wot2sBBce/ojzgnPzZXUV445gTSyE8=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.88.2 h1:T7b3qniouutV5Wwa9B1q7gW+Y8s1B3g9RE9qa7zLBIM=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.88.2/go.mod h1:tW9TsLb6t1eaTdBE6LITyJW1m/+DjQPU78Q/jT2FJu8=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 h1:etqBTKY581iwLL/H/S2sVgk3C9lAsTJFeXWFDsDcWOU=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4=
|
||||
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.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0=
|
||||
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
|
||||
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
|
||||
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
|
||||
github.com/aws/aws-sdk-go v1.44.80 h1:jEXGecSgPdvM5KnyDsSgFhZSm7WwaTp4h544Im4SfhI=
|
||||
github.com/aws/aws-sdk-go v1.44.80/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.7 h1:CLSjnhJSTSogvqUGhIC6LqFKATMRexcxLZ0i/Nzk9Eg=
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.7/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.19 h1:AqFK6zFNtq4i1EYu+eC7lcKHYnZagMn6SW171la0bGw=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.19/go.mod h1:XvTmGMY8d52ougvakOv1RpiTLPz9dlG/OQHsKU/cMmY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.18 h1:EQMdtHwz0ILTW1hoP+EwuWhwCG1hD6l3+RWFQABET4c=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.18/go.mod h1:vnwlwjIe+3XJPBYKu1et30ZPABG3VaXJYr8ryohpIyM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 h1:gt57MN3liKiyGopcqgNzJb2+d9MJaKT/q1OksHNXVE4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1/go.mod h1:lfUx8puBRdM5lVVMQlwt2v+ofiG/X6Ms+dy0UkG/kXw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 h1:sJLYcS+eZn5EeNINGHSCRAwUJMFVqklwkH36Vbyai7M=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31/go.mod h1:QT0BqUvX1Bh2ABdTGnjqEjvjzrCfIniM9Sc8zn9Yndo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 h1:1mnRASEKnkqsntcxHaysxwgVoUUp5dkiB+l3llKnqyg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25/go.mod h1:zBHOPwhBc3FlQjQJE/D3IfPWiWaQmT06Vq9aNukDo0k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 h1:p5luUImdIqywn6JpQsW3tq5GNOxKmOnEpybzPx+d1lk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32/go.mod h1:XGhIBZDEgfqmFIugclZ6FU7v75nHhBDtzuB4xB/tEi4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23 h1:DWYZIsyqagnWL00f8M/SOr9fN063OEQWn9LLTbdYXsk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23/go.mod h1:uIiFgURZbACBEQJfqTZPb/jxO7R+9LeoHUFudtIdeQI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.26 h1:CeuSeq/8FnYpPtnuIeLQEEvDv9zUjneuYi8EghMBdwQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.26/go.mod h1:2UqAAwMUXKeRkAHIlDJqvMVgOWkUi/AUXPk/YIe+Dg4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25 h1:5LHn8JQ0qvjD9L9JhMtylnkcw7j05GDZqM9Oin6hpr0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25/go.mod h1:/95IA+0lMnzW6XzqYJRpjjsAbKEORVeO0anQqjd2CNU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.0 h1:e2ooMhpYGhDnBfSvIyusvAwX7KexuZaHbQY2Dyei7VU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.0/go.mod h1:bh2E0CXKZsQN+faiKVqC40vfNMAWheoULBCnEgO9K+8=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.31.1 h1:PJH4I+qYjPXclKRbVCW47iYUvtXEh1u6YmDhn5J8VQE=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.31.1/go.mod h1:ncltU6n4Nof5uJttDtcNQ537uNuwYqsZZQcpkd2/GUQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 h1:5V7DWLBd7wTELVz5bPpwzYy/sikk0gsgZfj40X+l5OI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.6/go.mod h1:Y1VOmit/Fn6Tz1uFAeCO6Q7M2fmfXSCLeL5INVYsLuY=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6 h1:B8cauxOH1W1v7rd8RdI/MWnoR4Ze0wIHWrb90qczxj4=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6/go.mod h1:Lh/bc9XUf8CfOY6Jp5aIkQtN+j1mc+nExc+KXj9jx2s=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 h1:bWNgNdRko2x6gqa0blfATqAZKZokPIeM1vfmQt2pnvM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.18.7/go.mod h1:JuTnSoeePXmMVe9G8NcjjwgOKEfZ4cOjMuT2IBT/2eI=
|
||||
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
|
||||
github.com/aws/smithy-go v1.13.5/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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
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.59 h1:XNniO0rwgihLdF1+4qDHKN3gdta3AU1JteZResCJ8Oc=
|
||||
github.com/hashicorp/boundary/sdk v0.0.59/go.mod h1:VYwVGAEaHTzihRXpzKZL6EhWBhll59Iul3PGLVL63ao=
|
||||
github.com/hashicorp/cli v1.1.7 h1:/fZJ+hNdwfTSfsxMBa9WWMlfjUZbX8/LnUxgAd7lCVU=
|
||||
github.com/hashicorp/cli v1.1.7/go.mod h1:e6Mfpga9OCT1vqzFuoGZiiF/KaG9CbUfO5s3ghU3YgU=
|
||||
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
|
||||
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
|
||||
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
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.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.11 h1:lkK8ARM/DrCeL5deH2yVS6p40337dESi+77p9pIfLqo=
|
||||
github.com/hashicorp/eventlogger v0.2.11/go.mod h1:Rmc3MEopz7jUnLokZVTWAsXaEP5rqd20ObGS+pcau3U=
|
||||
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.8 h1:/GIUjn9GkFXMk/8/irRdbdtmx8CcyeyWdVy/E5LvzyA=
|
||||
github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.8/go.mod h1:JDc9UOD4EVRDIwPVethJcT5Ibi/Nas6eQDPtA60iwP0=
|
||||
github.com/hashicorp/go-kms-wrapping/v2 v2.0.20 h1:afreZ1WJd0WI7v4NsMZ1aL7V/T59sxPuKmQDgGUja20=
|
||||
github.com/hashicorp/go-kms-wrapping/v2 v2.0.20/go.mod h1:NeK2Ul15t1zutp/dZzt28XQrGZHosbxE/QLNNfaWObM=
|
||||
github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
|
||||
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.4 h1:8z+Zi0uLYXONkxwWb/D7O5t1IUucB6Ft3uvSOa950bM=
|
||||
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.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
|
||||
github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.1.2 h1:KlDrZWx/wYpnUcFPB0s9uhPjyNZoeIfhrV2MN/Ly76s=
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.1.2/go.mod h1:6+rVulOPNCQbL3Xv2iLCqM0JmU2WO2wRzP1C6hBKeB8=
|
||||
github.com/hashicorp/go-plugin v1.4.9 h1:ESiK220/qE0aGxWdzKIvRH69iLiuN/PjoLTm69RoWtU=
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6 h1:W9WN8p6moV1fjKLkeqEgkAMu5rauy9QeYDAmIaPuuiA=
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6/go.mod h1:MpCPSPGLDILGb4JMm94/mMi3YysIqsXzGCzkEZjcjXg=
|
||||
github.com/hashicorp/go-secure-stdlib/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.13 h1:TayxZ5drfMP0G6T++WvnLESGLOWeHtdWDWWTAi2e3Qk=
|
||||
github.com/hashicorp/go-secure-stdlib/configutil/v2 v2.0.13/go.mod h1:NDRQ/F3DXTylqjORAP0cA+puH/JFrLlT+NlqxHK2/e8=
|
||||
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.10 h1:2iDz+t0JLl1W0tJhvmhsh/UBgT1JgC8Qxz8HxYMWXQo=
|
||||
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.10/go.mod h1:eZkXE+osawMrAWR4wJRmyKauUwH6mNGbjFuiDujnbPk=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
|
||||
github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.8 h1:pgSicufBI+MvaIG9Keykb3k9B3sWtDNjmsoyrl2/8Qw=
|
||||
github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.8/go.mod h1:sBcjk+paCXCMR9HHLcrYSfPz2FsskD8MQuotKiyM2aA=
|
||||
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.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
|
||||
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
|
||||
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/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
|
||||
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
|
||||
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/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.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
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.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
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/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/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=
|
||||
@@ -174,65 +153,121 @@ 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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
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/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.6.2 h1:aIihoIOHCiLZHxyoNQ+ABL4NKhFTgKLBdMLyEAh98m0=
|
||||
github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||
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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
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=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/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.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=
|
||||
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.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-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
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=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.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/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=
|
||||
mvdan.cc/gofumpt v0.1.1/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48=
|
||||
@@ -1,14 +1,12 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
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"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
@@ -19,48 +17,33 @@ type CredentialAttributes struct {
|
||||
|
||||
// DisableCredentialRotation disables the rotation of aws secrets associated with the plugin
|
||||
DisableCredentialRotation bool
|
||||
|
||||
// RoleArn is the role arn associated with the aws credentials
|
||||
RoleArn string
|
||||
|
||||
// RoleExternalId is the external id associated with the aws credentials
|
||||
RoleExternalId string
|
||||
|
||||
// RoleSessionName is the session name associated with the aws credentials
|
||||
RoleSessionName string
|
||||
|
||||
// RoleTags is the tags associated with the aws credentials
|
||||
RoleTags map[string]string
|
||||
}
|
||||
|
||||
// GetCredentialsConfig parses values out of a protobuf struct 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) {
|
||||
// initialize secrets if it is nil
|
||||
// secrets can be nil because static credentials are optional
|
||||
if secrets == nil {
|
||||
secrets = &structpb.Struct{
|
||||
Fields: make(map[string]*structpb.Value),
|
||||
}
|
||||
}
|
||||
|
||||
unknownFields := values.StructFields(secrets)
|
||||
// GetCredentialsConfig parses values out of a protobuf struct input and returns a
|
||||
// CredentialsConfig used for configuring an AWS session. An error is returned if
|
||||
// any of the following fields are missing from the protobuf struct input or have
|
||||
// invalid value types: access_key_id, secret_access_key. An error is returned if
|
||||
// any unrecognized fields are found in the protobuf struct input.
|
||||
func GetCredentialsConfig(in *structpb.Struct, region string) (*awsutil.CredentialsConfig, error) {
|
||||
unknownFields := values.StructFields(in)
|
||||
badFields := make(map[string]string)
|
||||
|
||||
accessKey, err := values.GetStringValue(secrets, ConstAccessKeyId, false)
|
||||
accessKey, err := values.GetStringValue(in, ConstAccessKeyId, true)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = err.Error()
|
||||
}
|
||||
delete(unknownFields, ConstAccessKeyId)
|
||||
|
||||
secretKey, err := values.GetStringValue(secrets, ConstSecretAccessKey, false)
|
||||
secretKey, err := values.GetStringValue(in, ConstSecretAccessKey, true)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = err.Error()
|
||||
}
|
||||
delete(unknownFields, ConstSecretAccessKey)
|
||||
|
||||
if region == "" {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstRegion)] = err.Error()
|
||||
}
|
||||
|
||||
// the creds_last_rotated_time field will be found in the input struct for
|
||||
// persisted secrets, this value is not needed for creating the CredentialsConfig
|
||||
delete(unknownFields, ConstCredsLastRotatedTime)
|
||||
@@ -68,78 +51,21 @@ func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes,
|
||||
badFields[fmt.Sprintf("secrets.%s", s)] = "unrecognized field"
|
||||
}
|
||||
|
||||
opts := []awsutil.Option{}
|
||||
// logic for parsing the credential type
|
||||
// supported types:
|
||||
// - user static credential
|
||||
// - ec2 assume role dynamic credential
|
||||
// - enviornment variables credential
|
||||
switch {
|
||||
// static credentials and dynamic credentials cannot be used together
|
||||
case accessKey != "" && secretKey != "" && attrs.RoleArn != "":
|
||||
badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = "conflicts with role_arn value"
|
||||
badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = "conflicts with role_arn value"
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstRoleArn)] = "conflicts with access_key_id and secret_access_key values"
|
||||
// static credential is missing it's secret_access_key
|
||||
case accessKey != "" && secretKey == "":
|
||||
badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = "missing required value"
|
||||
// static credential is missing it's access_key_id
|
||||
case accessKey == "" && secretKey != "":
|
||||
badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = "missing required value"
|
||||
// dynamic credentials and credential rotation is not supported
|
||||
case len(attrs.RoleArn) > 0 && !attrs.DisableCredentialRotation:
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstDisableCredentialRotation)] = "disable_credential_rotation attribute is required when providing a role_arn"
|
||||
// add static credentials
|
||||
case accessKey != "" && secretKey != "":
|
||||
opts = append(opts,
|
||||
awsutil.WithAccessKey(accessKey),
|
||||
awsutil.WithSecretKey(secretKey),
|
||||
)
|
||||
// add dynamic credentials
|
||||
case attrs.RoleArn != "":
|
||||
opts = append(opts, awsutil.WithRoleArn(attrs.RoleArn))
|
||||
}
|
||||
|
||||
if len(badFields) > 0 {
|
||||
return nil, errors.InvalidArgumentError("Error in the secrets provided", badFields)
|
||||
}
|
||||
|
||||
if attrs.Region != "" {
|
||||
opts = append(opts, awsutil.WithRegion(attrs.Region))
|
||||
}
|
||||
if attrs.RoleExternalId != "" {
|
||||
opts = append(opts, awsutil.WithRoleExternalId(attrs.RoleExternalId))
|
||||
}
|
||||
if attrs.RoleSessionName != "" {
|
||||
opts = append(opts, awsutil.WithRoleSessionName(attrs.RoleSessionName))
|
||||
}
|
||||
if len(attrs.RoleTags) != 0 {
|
||||
opts = append(opts, awsutil.WithRoleTags(attrs.RoleTags))
|
||||
}
|
||||
|
||||
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 awsutil.NewCredentialsConfig(
|
||||
awsutil.WithAccessKey(accessKey),
|
||||
awsutil.WithSecretKey(secretKey),
|
||||
awsutil.WithRegion(region),
|
||||
)
|
||||
}
|
||||
|
||||
// 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,31 +74,17 @@ 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()
|
||||
}
|
||||
|
||||
roleArn, err := values.GetStringValue(in, ConstRoleArn, false)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstRoleArn)] = err.Error()
|
||||
}
|
||||
|
||||
roleExternalId, err := values.GetStringValue(in, ConstRoleExternalId, false)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstRoleExternalId)] = err.Error()
|
||||
}
|
||||
|
||||
roleSessionName, err := values.GetStringValue(in, ConstRoleSessionName, false)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstRoleSessionName)] = err.Error()
|
||||
}
|
||||
|
||||
roleTags, err := values.GetMapStringString(in, ConstRoleTags, false)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstRoleTags)] = err.Error()
|
||||
}
|
||||
|
||||
if len(badFields) > 0 {
|
||||
return nil, errors.InvalidArgumentError("Error in the attributes provided", badFields)
|
||||
}
|
||||
@@ -180,9 +92,5 @@ func GetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error)
|
||||
return &CredentialAttributes{
|
||||
Region: region,
|
||||
DisableCredentialRotation: disableCredentialRotation,
|
||||
RoleArn: roleArn,
|
||||
RoleExternalId: roleExternalId,
|
||||
RoleSessionName: roleSessionName,
|
||||
RoleTags: roleTags,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
@@ -6,7 +6,7 @@ package credential
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
||||
"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{
|
||||
@@ -54,28 +61,6 @@ func TestGetCredentialAttributes(t *testing.T) {
|
||||
DisableCredentialRotation: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with assume role",
|
||||
in: map[string]any{
|
||||
ConstRegion: "us-west-2",
|
||||
ConstRoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
ConstRoleExternalId: "1234567890",
|
||||
ConstRoleSessionName: "test-session",
|
||||
ConstRoleTags: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
expected: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
DisableCredentialRotation: false,
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@@ -104,147 +89,63 @@ func TestGetCredentialAttributes(t *testing.T) {
|
||||
func TestGetCredentialsConfig(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
secrets *structpb.Struct
|
||||
attrs *CredentialAttributes
|
||||
dualStack bool
|
||||
in map[string]any
|
||||
region string
|
||||
expected *awsutil.CredentialsConfig
|
||||
expectedErrContains string
|
||||
}{
|
||||
{
|
||||
name: "no credentials",
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expected: &awsutil.CredentialsConfig{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
name: "missing access_key_id",
|
||||
in: map[string]any{},
|
||||
region: "us-west-2",
|
||||
expectedErrContains: "missing required value \"access_key_id\"",
|
||||
},
|
||||
{
|
||||
name: "with static credentials",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expected: &awsutil.CredentialsConfig{
|
||||
AccessKey: "AKIAfoobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with dualstack",
|
||||
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",
|
||||
name: "missing secret_access_key",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
},
|
||||
region: "us-west-2",
|
||||
expectedErrContains: "missing required value \"secret_access_key\"",
|
||||
},
|
||||
{
|
||||
name: "unknown fields",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
"foo": structpb.NewBoolValue(true),
|
||||
"bar": structpb.NewBoolValue(true),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
ConstSecretAccessKey: "bazqux",
|
||||
"foo": true,
|
||||
"bar": true,
|
||||
},
|
||||
region: "us-west-2",
|
||||
expectedErrContains: "secrets.bar: unrecognized field, secrets.foo: unrecognized field",
|
||||
},
|
||||
{
|
||||
name: "valid ignore creds_last_rotated_time",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
ConstSecretAccessKey: "bazqux",
|
||||
ConstCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
|
||||
},
|
||||
region: "us-west-2",
|
||||
expected: &awsutil.CredentialsConfig{
|
||||
AccessKey: "AKIAfoobar",
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with assume role",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
DisableCredentialRotation: true,
|
||||
name: "good",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
ConstSecretAccessKey: "bazqux",
|
||||
},
|
||||
region: "us-west-2",
|
||||
expected: &awsutil.CredentialsConfig{
|
||||
Region: "us-west-2",
|
||||
RoleARN: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with static credential & assume role",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
expectedErrContains: "attributes.role_arn: conflicts with access_key_id and secret_access_key values, secrets.access_key_id: conflicts with role_arn value, secrets.secret_access_key: conflicts with role_arn value",
|
||||
},
|
||||
{
|
||||
name: "with dynamic credentials and no disable credential rotation",
|
||||
secrets: &structpb.Struct{Fields: map[string]*structpb.Value{}},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
expectedErrContains: "attributes.disable_credential_rotation: disable_credential_rotation attribute is required when providing a role_arn",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@@ -252,7 +153,10 @@ 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)
|
||||
input, err := structpb.NewStruct(tc.in)
|
||||
require.NoError(err)
|
||||
|
||||
actual, err := GetCredentialsConfig(input, tc.region)
|
||||
if tc.expectedErrContains != "" {
|
||||
require.Error(err)
|
||||
require.Contains(err.Error(), tc.expectedErrContains)
|
||||
@@ -264,17 +168,6 @@ func TestGetCredentialsConfig(t *testing.T) {
|
||||
require.Equal(tc.expected.AccessKey, actual.AccessKey)
|
||||
require.Equal(tc.expected.SecretKey, actual.SecretKey)
|
||||
require.Equal(tc.expected.Region, actual.Region)
|
||||
require.Equal(tc.expected.RoleARN, actual.RoleARN)
|
||||
require.Equal(tc.expected.RoleExternalId, actual.RoleExternalId)
|
||||
require.Equal(tc.expected.RoleSessionName, actual.RoleSessionName)
|
||||
require.Equal(tc.expected.RoleTags, actual.RoleTags)
|
||||
if tc.dualStack {
|
||||
require.NotNil(actual.IAMEndpointResolver)
|
||||
require.NotNil(actual.STSEndpointResolver)
|
||||
} else {
|
||||
require.Nil(actual.IAMEndpointResolver)
|
||||
require.Nil(actual.STSEndpointResolver)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
|
||||
const (
|
||||
@@ -18,16 +15,4 @@ const (
|
||||
|
||||
// ConstCredsLastRotatedTime is the key for the last rotated time in the aws credentials.
|
||||
ConstCredsLastRotatedTime = "creds_last_rotated_time"
|
||||
|
||||
// ConstRoleArn is the key for assuming a IAM role.
|
||||
ConstRoleArn = "role_arn"
|
||||
|
||||
// ConstRoleExternalId is the key for the external id used for assuming a IAM role.
|
||||
ConstRoleExternalId = "role_external_id"
|
||||
|
||||
// ConstRoleSessionName is the key for the session name used for assuming a IAM role.
|
||||
ConstRoleSessionName = "role_session_name"
|
||||
|
||||
// ConstRoleTags is the key for the tags used for assuming a IAM role.
|
||||
ConstRoleTags = "role_tags"
|
||||
)
|
||||
@@ -1,172 +0,0 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
|
||||
// TestGetCredentialsConfigRoleArnFallback documents all credential input
|
||||
// combinations and the CredentialsConfig they produce. The key finding is
|
||||
// in the "role_arn + no static keys" case: GetCredentialsConfig succeeds and
|
||||
// populates CredentialsConfig.RoleARN, but no STS call is ever made during
|
||||
// config construction. At runtime, awsutil.GenerateCredentialChain silently
|
||||
// falls back to the ambient credential chain (env vars / instance profile)
|
||||
// without calling sts:AssumeRole, so a bogus or unreachable ARN is never
|
||||
// detected.
|
||||
//
|
||||
// Run this test to confirm the root cause:
|
||||
//
|
||||
// go test ./internal/credential/... -run TestGetCredentialsConfigRoleArnFallback -v
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
func TestGetCredentialsConfigRoleArnFallback(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
secrets *structpb.Struct
|
||||
attrs *CredentialAttributes
|
||||
expectedAccessKey string
|
||||
expectedSecretKey string
|
||||
expectedRoleARN string
|
||||
expectedType CredentialType
|
||||
expectedErrContains string
|
||||
}{
|
||||
{
|
||||
// NOTE: No role_arn and no static keys. GetCredentialsConfig
|
||||
// succeeds and returns a CredentialsConfig with no credentials set
|
||||
// at all. GetCredentialType returns Unknown because there is no
|
||||
// AccessKey prefix to classify. The AWS SDK will use whatever
|
||||
// ambient credentials it finds at runtime (env vars, instance
|
||||
// profile, etc.).
|
||||
name: "ambient only — no role_arn, no static keys",
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-east-1",
|
||||
DisableCredentialRotation: true,
|
||||
},
|
||||
expectedAccessKey: "",
|
||||
expectedSecretKey: "",
|
||||
expectedRoleARN: "",
|
||||
expectedType: Unknown,
|
||||
},
|
||||
{
|
||||
// NOTE: The normal static-key path. Both keys are provided; no
|
||||
// role_arn. GetCredentialType returns StaticAWS because the
|
||||
// AccessKey starts with "AKIA".
|
||||
name: "static keys only",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-east-1",
|
||||
DisableCredentialRotation: true,
|
||||
},
|
||||
expectedAccessKey: "AKIAfoobar",
|
||||
expectedSecretKey: "bazqux",
|
||||
expectedRoleARN: "",
|
||||
expectedType: StaticAWS,
|
||||
},
|
||||
{
|
||||
// NOTE: THE BUG SCENARIO. role_arn is set (to a clearly bogus
|
||||
// value) and disable_credential_rotation is true, but no static
|
||||
// keys are provided. GetCredentialsConfig succeeds — the ARN is
|
||||
// stored in CredentialsConfig.RoleARN. GetCredentialType returns
|
||||
// DynamicAWS. However, no sts:AssumeRole call is ever made here
|
||||
// or during GenerateCredentialChain: the awsutil library skips the
|
||||
// assume-role path when AccessKey is empty and falls back to the
|
||||
// ambient credential chain instead. The bogus ARN is never
|
||||
// validated or exercised.
|
||||
name: "role_arn + no static keys (THE BUG SCENARIO)",
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-east-1",
|
||||
RoleArn: "arn:aws:sts::000000000000:assumed-role/fake/whoever",
|
||||
DisableCredentialRotation: true,
|
||||
},
|
||||
expectedAccessKey: "",
|
||||
expectedSecretKey: "",
|
||||
expectedRoleARN: "arn:aws:sts::000000000000:assumed-role/fake/whoever",
|
||||
expectedType: DynamicAWS,
|
||||
},
|
||||
{
|
||||
// NOTE: role_arn is set but disable_credential_rotation is false
|
||||
// (the default). This is caught by the validation switch and
|
||||
// returns an explicit error. Validation works correctly here.
|
||||
name: "role_arn without disable_credential_rotation — validation error",
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-east-1",
|
||||
RoleArn: "arn:aws:iam::123456789012:role/MyRole",
|
||||
},
|
||||
expectedErrContains: "disable_credential_rotation attribute is required when providing a role_arn",
|
||||
},
|
||||
{
|
||||
// NOTE: Both role_arn and static keys are provided together. This
|
||||
// is caught by the validation switch and returns an error listing
|
||||
// all three conflicting fields. Validation works correctly here.
|
||||
name: "role_arn + static keys — conflict error",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-east-1",
|
||||
RoleArn: "arn:aws:iam::123456789012:role/MyRole",
|
||||
},
|
||||
expectedErrContains: "conflicts with role_arn value",
|
||||
},
|
||||
{
|
||||
// NOTE: access_key_id is provided but secret_access_key is
|
||||
// missing. The validation switch catches the incomplete pair.
|
||||
name: "static keys — missing secret_access_key",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{Region: "us-east-1"},
|
||||
expectedErrContains: "secrets.secret_access_key: missing required value",
|
||||
},
|
||||
{
|
||||
// NOTE: secret_access_key is provided but access_key_id is
|
||||
// missing. The validation switch catches the incomplete pair.
|
||||
name: "static keys — missing access_key_id",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{Region: "us-east-1"},
|
||||
expectedErrContains: "secrets.access_key_id: missing required value",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
cfg, err := GetCredentialsConfig(tc.secrets, tc.attrs, false)
|
||||
|
||||
if tc.expectedErrContains != "" {
|
||||
require.Error(err)
|
||||
require.Contains(err.Error(), tc.expectedErrContains)
|
||||
require.Equal(codes.InvalidArgument, status.Code(err))
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Equal(tc.expectedAccessKey, cfg.AccessKey)
|
||||
require.Equal(tc.expectedSecretKey, cfg.SecretKey)
|
||||
require.Equal(tc.expectedRoleARN, cfg.RoleARN)
|
||||
require.Equal(tc.expectedType, GetCredentialType(cfg),
|
||||
"credential type mismatch — see NOTE above the test case for why this matters")
|
||||
})
|
||||
}
|
||||
}
|
||||
+147
-209
@@ -1,48 +1,18 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
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/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/iam"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/values"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
||||
"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
|
||||
)
|
||||
|
||||
// rotationWaitTimeout controls the time we wait for credential rotation to
|
||||
// succeed. This is important to ensure that rotated credentials can be used
|
||||
// right away.
|
||||
@@ -52,13 +22,19 @@ 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
|
||||
// AccessKeyId is the access key id for the AWS credential.
|
||||
AccessKeyId string
|
||||
// SecretAccessKey is the secret access key for the AWS credential.
|
||||
SecretAccessKey string
|
||||
// CredsLastRotatedTime is the last rotation of aws secrets for the AWS credential.
|
||||
CredsLastRotatedTime time.Time
|
||||
|
||||
// testOpts are options that should be used for testing only
|
||||
testOpts []awsutil.Option
|
||||
|
||||
// Region is not a part of persisted state but if set is passed to clients
|
||||
// created with this state
|
||||
region string
|
||||
}
|
||||
|
||||
// WithStateTestOpts enables unit testing different edge cases
|
||||
@@ -71,23 +47,35 @@ func WithStateTestOpts(opts []awsutil.Option) AwsCredentialPersistedStateOption
|
||||
}
|
||||
}
|
||||
|
||||
// WithCredentialsConfig sets the value for CredentialsConfig in the credential persisted state.
|
||||
func WithCredentialsConfig(x *awsutil.CredentialsConfig) AwsCredentialPersistedStateOption {
|
||||
// WithAccessKeyId sets the value for AccesskeyId in the storage persisted state.
|
||||
func WithAccessKeyId(x string) AwsCredentialPersistedStateOption {
|
||||
return func(s *AwsCredentialPersistedState) error {
|
||||
if s.CredentialsConfig != nil {
|
||||
return stderr.New("credentials config already set")
|
||||
if s.AccessKeyId != "" {
|
||||
return errors.New("access key id already set")
|
||||
}
|
||||
|
||||
s.CredentialsConfig = x
|
||||
s.AccessKeyId = x
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithCredsLastRotatedTime sets the value for CredsLastRotatedTime in the credential persisted state.
|
||||
// WithSecretAccessKey sets the value for SecretAccessKey in the storage persisted state.
|
||||
func WithSecretAccessKey(x string) AwsCredentialPersistedStateOption {
|
||||
return func(s *AwsCredentialPersistedState) error {
|
||||
if s.SecretAccessKey != "" {
|
||||
return errors.New("secret access key already set")
|
||||
}
|
||||
|
||||
s.SecretAccessKey = x
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithCredsLastRotatedTime sets the value for CredsLastRotatedTime in the storage persisted state.
|
||||
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
|
||||
@@ -95,6 +83,18 @@ func WithCredsLastRotatedTime(t time.Time) AwsCredentialPersistedStateOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithRegion sets the value for region in the storage persisted state.
|
||||
func WithRegion(with string) AwsCredentialPersistedStateOption {
|
||||
return func(s *AwsCredentialPersistedState) error {
|
||||
if s.region != "" {
|
||||
return errors.New("region already set")
|
||||
}
|
||||
|
||||
s.region = with
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewAwsCredentialPersistedState returns a AwsCredentialPersistedState.
|
||||
// Supported options include: WithAccessKeyId, WithSecretAccessKey
|
||||
// WithCredsLastRotatedTime, & WithRegion.
|
||||
@@ -105,21 +105,27 @@ func NewAwsCredentialPersistedState(opts ...AwsCredentialPersistedStateOption) (
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// ValidateCreds takes the credentials configuration from the persisted state
|
||||
// ValidateCreds takes the access key and secret key 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.
|
||||
func (s *AwsCredentialPersistedState) ValidateCreds(ctx context.Context) error {
|
||||
if s.CredentialsConfig == nil {
|
||||
return errors.BadRequestStatus("missing credentials config")
|
||||
// to check that the credentials are valid.
|
||||
func (s *AwsCredentialPersistedState) ValidateCreds() error {
|
||||
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
|
||||
awsutil.WithAccessKey(s.AccessKeyId),
|
||||
awsutil.WithSecretKey(s.SecretAccessKey),
|
||||
awsutil.WithRegion(s.region),
|
||||
}, s.testOpts...)...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading credentials: %w", err)
|
||||
}
|
||||
if _, err := s.CredentialsConfig.GetCallerIdentity(ctx, s.testOpts...); err != nil {
|
||||
st, _ := errors.ParseAWSError("validating credentials", err)
|
||||
return st.Err()
|
||||
|
||||
if _, err := c.GetCallerIdentity(s.testOpts...); err != nil {
|
||||
return fmt.Errorf("error validating credentials: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -127,139 +133,147 @@ 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.
|
||||
func (s *AwsCredentialPersistedState) RotateCreds(ctx context.Context) error {
|
||||
if s.CredentialsConfig == nil {
|
||||
return errors.BadRequestStatus("missing credentials config")
|
||||
// still result in an extraneous access key existing.
|
||||
func (s *AwsCredentialPersistedState) RotateCreds() error {
|
||||
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
|
||||
awsutil.WithAccessKey(s.AccessKeyId),
|
||||
awsutil.WithSecretKey(s.SecretAccessKey),
|
||||
awsutil.WithRegion(s.region),
|
||||
}, s.testOpts...)...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading credentials: %w", err)
|
||||
}
|
||||
if GetCredentialType(s.CredentialsConfig) != StaticAWS {
|
||||
return errors.BadRequestStatus("invalid credential type")
|
||||
}
|
||||
if err := s.CredentialsConfig.RotateKeys(ctx, append([]awsutil.Option{
|
||||
|
||||
if err := c.RotateKeys(append([]awsutil.Option{
|
||||
awsutil.WithValidityCheckTimeout(rotationWaitTimeout),
|
||||
}, s.testOpts...)...); err != nil {
|
||||
st, _ := errors.ParseAWSError("rotating credentials", err)
|
||||
return st.Err()
|
||||
return fmt.Errorf("error rotating credentials: %w", err)
|
||||
}
|
||||
|
||||
s.AccessKeyId = c.AccessKey
|
||||
s.SecretAccessKey = c.SecretKey
|
||||
s.CredsLastRotatedTime = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplaceCreds replaces the access key in the state with a new key.
|
||||
// If the existing key was rotated at any point in time, it is
|
||||
// deleted first, otherwise it's left alone. This method returns a
|
||||
// status error with PluginError details.
|
||||
func (s *AwsCredentialPersistedState) ReplaceCreds(ctx context.Context, credentialsConfig *awsutil.CredentialsConfig) error {
|
||||
if credentialsConfig == nil {
|
||||
return errors.BadRequestStatus("missing new credentials config")
|
||||
}
|
||||
if s.CredentialsConfig == nil {
|
||||
return errors.BadRequestStatus("missing credentials config")
|
||||
// deleted first, otherwise it's left alone.
|
||||
func (s *AwsCredentialPersistedState) ReplaceCreds(accessKeyId, secretAccessKey string) error {
|
||||
if accessKeyId == "" {
|
||||
return errors.New("access key id cannot be empty")
|
||||
}
|
||||
|
||||
// Delete old/existing credentials. This action is only possible for static credential types.
|
||||
// This is done with the same credentials to ensure that it has the proper permissions to do it.
|
||||
ct := GetCredentialType(s.CredentialsConfig)
|
||||
if !s.CredsLastRotatedTime.IsZero() && ct == StaticAWS {
|
||||
if err := s.DeleteCreds(ctx); err != nil {
|
||||
if secretAccessKey == "" {
|
||||
return errors.New("secret access key cannot be empty")
|
||||
}
|
||||
|
||||
if accessKeyId == s.AccessKeyId {
|
||||
return errors.New("attempting to replace access key with the same one")
|
||||
}
|
||||
|
||||
if !s.CredsLastRotatedTime.IsZero() {
|
||||
// Delete old/existing credentials. This is done with the same
|
||||
// credentials to ensure that it has the proper permissions to do
|
||||
// it.
|
||||
if err := s.DeleteCreds(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Set the new attributes and clear the rotated time.
|
||||
s.CredentialsConfig = credentialsConfig
|
||||
s.AccessKeyId = accessKeyId
|
||||
s.SecretAccessKey = secretAccessKey
|
||||
s.CredsLastRotatedTime = time.Time{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (s *AwsCredentialPersistedState) DeleteCreds(ctx context.Context) error {
|
||||
if s.CredentialsConfig == nil {
|
||||
return errors.BadRequestStatus("missing credentials config")
|
||||
}
|
||||
if GetCredentialType(s.CredentialsConfig) != StaticAWS {
|
||||
return errors.BadRequestStatus("invalid credential type")
|
||||
// the state just to ensure that they cannot be re-used after.
|
||||
func (s *AwsCredentialPersistedState) DeleteCreds() error {
|
||||
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
|
||||
awsutil.WithAccessKey(s.AccessKeyId),
|
||||
awsutil.WithSecretKey(s.SecretAccessKey),
|
||||
awsutil.WithRegion(s.region),
|
||||
}, s.testOpts...)...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading credentials: %w", err)
|
||||
}
|
||||
|
||||
if err := s.CredentialsConfig.DeleteAccessKey(ctx, s.CredentialsConfig.AccessKey, s.testOpts...); err != nil {
|
||||
if err := c.DeleteAccessKey(s.AccessKeyId, 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" {
|
||||
s.CredentialsConfig = nil
|
||||
s.CredsLastRotatedTime = time.Time{}
|
||||
return nil
|
||||
var awsErr awserr.Error
|
||||
if errors.As(err, &awsErr) {
|
||||
if awsErr.Code() == iam.ErrCodeNoSuchEntityException {
|
||||
s.AccessKeyId = ""
|
||||
s.SecretAccessKey = ""
|
||||
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
|
||||
s.AccessKeyId = ""
|
||||
s.SecretAccessKey = ""
|
||||
s.CredsLastRotatedTime = time.Time{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateCredentialChain returns a AWS configuration for the credentials in the state.
|
||||
func (s *AwsCredentialPersistedState) GenerateCredentialChain(ctx context.Context) (*aws.Config, error) {
|
||||
return s.CredentialsConfig.GenerateCredentialChain(ctx, s.testOpts...)
|
||||
// GetSession returns a configured AWS session for the credentials in
|
||||
// the state.
|
||||
func (s *AwsCredentialPersistedState) GetSession() (*session.Session, error) {
|
||||
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
|
||||
awsutil.WithAccessKey(s.AccessKeyId),
|
||||
awsutil.WithSecretKey(s.SecretAccessKey),
|
||||
awsutil.WithRegion(s.region),
|
||||
}, s.testOpts...)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.GetSession()
|
||||
}
|
||||
|
||||
// ToMap returns a map of the credentials stored in the persisted state.
|
||||
// ToMap will return an empty map for temporary credentials. ToMap will
|
||||
// return a map for long-term credentials with following keys:
|
||||
// access_key_id, secret_access_key & creds_last_rotated_time
|
||||
// ToMap returns a map of the credentials stored in the persisted state,
|
||||
// which includes the following keys: access_key_id, secret_access_key,
|
||||
// & creds_last_rotated_time
|
||||
func (s *AwsCredentialPersistedState) ToMap() map[string]any {
|
||||
ct := GetCredentialType(s.CredentialsConfig)
|
||||
// Dynamic AWS credentials are temporary credentials that expire within an hour.
|
||||
// ToMap() returns an empty map here so that Boundary does not store temporary
|
||||
// credentials into the database.
|
||||
//
|
||||
// Uknown credentials are not aws compatiable secrets that may have been provided
|
||||
// for a non-aws s3 compatiable service. These credentials will not be returned
|
||||
// because the plugin does not know how to manage non-aws credentials.
|
||||
if ct == DynamicAWS || ct == Unknown {
|
||||
return map[string]any{}
|
||||
}
|
||||
return map[string]any{
|
||||
ConstAccessKeyId: s.CredentialsConfig.AccessKey,
|
||||
ConstSecretAccessKey: s.CredentialsConfig.SecretKey,
|
||||
ConstAccessKeyId: s.AccessKeyId,
|
||||
ConstSecretAccessKey: s.SecretAccessKey,
|
||||
ConstCredsLastRotatedTime: s.CredsLastRotatedTime.Format(time.RFC3339Nano),
|
||||
}
|
||||
}
|
||||
|
||||
// AwsCredentialPersistedStateFromProto parses values out of a protobuf struct input
|
||||
// and returns a AwsCredentialPersistedState used for configuring an AWS session.
|
||||
func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *CredentialAttributes, dualStack bool, opts ...AwsCredentialPersistedStateOption) (*AwsCredentialPersistedState, error) {
|
||||
// initialize secrets if it is nil
|
||||
// secrets can be nil because static credentials are optional
|
||||
if secrets == nil {
|
||||
secrets = &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{},
|
||||
}
|
||||
// An error is returned if any of the following fields are missing from the protobuf
|
||||
// struct input or have invalid value types: access_key_id, secret_access_key, &
|
||||
// creds_last_rotated_time.
|
||||
func AwsCredentialPersistedStateFromProto(in *structpb.Struct, opts ...AwsCredentialPersistedStateOption) (*AwsCredentialPersistedState, error) {
|
||||
if in == nil {
|
||||
return nil, errors.New("missing persisted secrets")
|
||||
}
|
||||
|
||||
if attrs == nil {
|
||||
return nil, fmt.Errorf("missing credential attributes")
|
||||
}
|
||||
|
||||
accessKeyId, err := values.GetStringValue(secrets, ConstAccessKeyId, false)
|
||||
accessKeyId, err := values.GetStringValue(in, ConstAccessKeyId, true)
|
||||
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)
|
||||
secretAccessKey, err := values.GetStringValue(in, ConstSecretAccessKey, true)
|
||||
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)
|
||||
credsLastRotatedTime, err := values.GetTimeValue(in, 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...)
|
||||
@@ -267,85 +281,9 @@ func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *Crede
|
||||
return nil, err
|
||||
}
|
||||
|
||||
awsOpts := append([]awsutil.Option{}, s.testOpts...)
|
||||
if accessKeyId != "" && secretAccessKey != "" {
|
||||
awsOpts = append(awsOpts,
|
||||
awsutil.WithAccessKey(accessKeyId),
|
||||
awsutil.WithSecretKey(secretAccessKey),
|
||||
)
|
||||
}
|
||||
if attrs.Region != "" {
|
||||
awsOpts = append(awsOpts, awsutil.WithRegion(attrs.Region))
|
||||
}
|
||||
if attrs.RoleArn != "" {
|
||||
awsOpts = append(awsOpts, awsutil.WithRoleArn(attrs.RoleArn))
|
||||
}
|
||||
if attrs.RoleExternalId != "" {
|
||||
awsOpts = append(awsOpts, awsutil.WithRoleExternalId(attrs.RoleExternalId))
|
||||
}
|
||||
if attrs.RoleSessionName != "" {
|
||||
awsOpts = append(awsOpts, awsutil.WithRoleSessionName(attrs.RoleSessionName))
|
||||
}
|
||||
if len(attrs.RoleTags) != 0 {
|
||||
awsOpts = append(awsOpts, awsutil.WithRoleTags(attrs.RoleTags))
|
||||
}
|
||||
if dualStack {
|
||||
awsOpts = append(awsOpts, []awsutil.Option{
|
||||
awsutil.WithIamEndpointResolver(&iamEndpointResolver{
|
||||
dualStack: dualStack,
|
||||
}),
|
||||
awsutil.WithStsEndpointResolver(&stsEndpointResolver{
|
||||
dualStack: dualStack,
|
||||
}),
|
||||
}...)
|
||||
}
|
||||
|
||||
credentialsConfig, err := awsutil.NewCredentialsConfig(awsOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.CredentialsConfig = credentialsConfig
|
||||
s.AccessKeyId = accessKeyId
|
||||
s.SecretAccessKey = secretAccessKey
|
||||
s.CredsLastRotatedTime = credsLastRotatedTime
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// GetCredentialType returns the credential type based on the given
|
||||
// AccessKey/RoleARN. See CredentialType definition for more information.
|
||||
func GetCredentialType(cc *awsutil.CredentialsConfig) CredentialType {
|
||||
if cc == nil {
|
||||
return Unknown
|
||||
}
|
||||
|
||||
if len(cc.RoleARN) > 0 || strings.HasPrefix(cc.AccessKey, "ASIA") {
|
||||
return DynamicAWS
|
||||
}
|
||||
if strings.HasPrefix(cc.AccessKey, "AKIA") {
|
||||
return StaticAWS
|
||||
}
|
||||
if len(cc.AccessKey) > 0 {
|
||||
return StaticOther
|
||||
}
|
||||
|
||||
return Unknown
|
||||
}
|
||||
|
||||
type iamEndpointResolver struct {
|
||||
dualStack bool
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
+215
-537
File diff suppressed because it is too large.
Load diff
@@ -1,15 +1,10 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/iam"
|
||||
"github.com/aws/aws-sdk-go/service/iam/iamiface"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -28,61 +23,25 @@ func (s *testMockIAMState) Reset() {
|
||||
}
|
||||
|
||||
type testMockIAM struct {
|
||||
awsutil.IAMClient
|
||||
iamiface.IAMAPI
|
||||
|
||||
State *testMockIAMState
|
||||
}
|
||||
|
||||
func (m *testMockIAM) DeleteAccessKey(ctx context.Context, input *iam.DeleteAccessKeyInput, opts ...func(*iam.Options)) (*iam.DeleteAccessKeyOutput, error) {
|
||||
func (m *testMockIAM) DeleteAccessKey(input *iam.DeleteAccessKeyInput) (*iam.DeleteAccessKeyOutput, error) {
|
||||
m.State.DeleteAccessKeyCalled = true
|
||||
return m.IAMClient.DeleteAccessKey(ctx, input, opts...)
|
||||
return m.IAMAPI.DeleteAccessKey(input)
|
||||
}
|
||||
|
||||
func newTestMockIAM(state *testMockIAMState, opts ...awsutil.MockIAMOption) awsutil.IAMAPIFunc {
|
||||
return func(awsConfig *aws.Config) (awsutil.IAMClient, error) {
|
||||
return func(sess *session.Session) (iamiface.IAMAPI, error) {
|
||||
m := &testMockIAM{
|
||||
State: state,
|
||||
}
|
||||
f := awsutil.NewMockIAM(opts...)
|
||||
var err error
|
||||
|
||||
m.IAMClient, err = f(awsConfig)
|
||||
m.IAMAPI, err = f(sess)
|
||||
return m, err
|
||||
}
|
||||
}
|
||||
|
||||
// MockStaticCredentialSecrets returns a *structpb.Struct that contains two
|
||||
// key pair values: (access_key_id, AKIA_foobar) & (secret_access_key, bazqux)
|
||||
func MockStaticCredentialSecrets() *structpb.Struct {
|
||||
return &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MockAssumeRoleAttributes returns a *structpb.Struct that contains six key pair values:
|
||||
//
|
||||
// (region, region)
|
||||
// (disable_credential_rotation, disableRotate)
|
||||
// (role_arn, arn:aws:iam::123456789012:role/S3Access)
|
||||
// (role_external_id, 1234567890)
|
||||
// (role_session_name, ec2-assume-role-provider)
|
||||
// (role_tags, struct{foo:bar})
|
||||
func MockAssumeRoleAttributes(region string, disableRotate bool) *structpb.Struct {
|
||||
return &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstRegion: structpb.NewStringValue(region),
|
||||
ConstDisableCredentialRotation: structpb.NewBoolValue(disableRotate),
|
||||
ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
|
||||
ConstRoleExternalId: structpb.NewStringValue("1234567890"),
|
||||
ConstRoleSessionName: structpb.NewStringValue("ec2-assume-role-provider"),
|
||||
ConstRoleTags: structpb.NewStructValue(&structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStringValue("bar"),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
+1
-260
@@ -1,71 +1,12 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
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/aws-sdk-go-v2/service/s3/types"
|
||||
"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"
|
||||
|
||||
// NotFound is returned when attempting to interact with an S3 object
|
||||
// whose key does not exist
|
||||
awsErrorNotFound = "NotFound"
|
||||
|
||||
// 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.
|
||||
@@ -78,205 +19,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
|
||||
}
|
||||
|
||||
// match error type against NotFound
|
||||
var notFoundErr *types.NotFound
|
||||
if errors.As(err, ¬FoundErr) {
|
||||
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
|
||||
return status.New(codes.NotFound, statusMsg), nil
|
||||
}
|
||||
|
||||
// match error type against NoSuchkey
|
||||
var noSuchKeyErr *types.NoSuchKey
|
||||
if errors.As(err, &noSuchKeyErr) {
|
||||
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
|
||||
return status.New(codes.NotFound, statusMsg), nil
|
||||
}
|
||||
|
||||
// match error type against InvalidObjectState
|
||||
var invalidObjectStateErr *types.InvalidObjectState
|
||||
if errors.As(err, &invalidObjectStateErr) {
|
||||
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
|
||||
return status.New(codes.NotFound, statusMsg), nil
|
||||
}
|
||||
|
||||
// match error type against NoSuchBucket
|
||||
var noSuchBucketErr *types.NoSuchBucket
|
||||
if errors.As(err, &noSuchBucketErr) {
|
||||
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
|
||||
return status.New(codes.NotFound, statusMsg), &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
ErrorDetails: noSuchBucketErr.ErrorMessage(),
|
||||
CheckedAt: timestamppb.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// match error type against AccessDenied
|
||||
var accessDeniedErr *types.AccessDenied
|
||||
if errors.As(err, &accessDeniedErr) {
|
||||
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: accessDeniedErr.ErrorMessage(),
|
||||
CheckedAt: timestamppb.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to evaluating the smithy APIError code
|
||||
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 awsErrorNotFound:
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to evaluating the http status code
|
||||
var httpErr *awshttp.ResponseError
|
||||
if errors.As(err, &httpErr) {
|
||||
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
|
||||
}
|
||||
|
||||
// BadRequestStatusf returns a status error with an invalid
|
||||
// argument code and a formatted error message.
|
||||
func BadRequestStatusf(format string, args ...any) error {
|
||||
return status.New(codes.InvalidArgument, fmt.Sprintf(format, args...)).Err()
|
||||
}
|
||||
|
||||
// BadRequestStatus returns a status error with an invalid
|
||||
// argument code and the provided message.
|
||||
func BadRequestStatus(msg string) error {
|
||||
return status.New(codes.InvalidArgument, msg).Err()
|
||||
}
|
||||
|
||||
// UnknownStatusf returns a status error with an internal
|
||||
// error code and a formatted error message.
|
||||
func UnknownStatusf(format string, args ...any) error {
|
||||
return status.New(codes.Internal, fmt.Sprintf(format, args...)).Err()
|
||||
}
|
||||
|
||||
// UnknownStatus returns a status error with an internal
|
||||
// error code and the provided message.
|
||||
func UnknownStatus(msg string) error {
|
||||
return status.New(codes.Internal, msg).Err()
|
||||
return status.Error(codes.InvalidArgument, msg)
|
||||
}
|
||||
@@ -1,427 +0,0 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// 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: "wrapped-http-status-code-forbidden",
|
||||
err: fmt.Errorf("wrap: %w", 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: "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(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "aws-s3-typed-not-found",
|
||||
err: TestAwsS3NotFoundError("object was not found"),
|
||||
expectedStatusCode: codes.NotFound,
|
||||
expectedStatusMsg: "aws service unknown: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "aws-s3-typed-no-such-key",
|
||||
err: TestAwsS3NoSuchKeyError("key does not exist"),
|
||||
expectedStatusCode: codes.NotFound,
|
||||
expectedStatusMsg: "aws service unknown: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "aws-s3-typed-invalid-object-state",
|
||||
err: TestAwsS3InvalidObjectStateError("object is archived"),
|
||||
expectedStatusCode: codes.NotFound,
|
||||
expectedStatusMsg: "aws service unknown: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "aws-s3-typed-no-such-bucket",
|
||||
err: TestAwsS3NoSuchBucketError("bucket does not exist"),
|
||||
expectedStatusCode: codes.NotFound,
|
||||
expectedStatusMsg: "aws service unknown: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
ErrorDetails: "bucket does not exist",
|
||||
CheckedAt: timestamppb.Now(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "aws-s3-typed-access-denied",
|
||||
err: TestAwsS3AccessDeniedError("access denied"),
|
||||
expectedStatusCode: codes.PermissionDenied,
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials: test",
|
||||
expectedPermission: &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, 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_BadRequestStatusf(t *testing.T) {
|
||||
require, assert := require.New(t), assert.New(t)
|
||||
err := BadRequestStatusf("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_BadRequestStatus(t *testing.T) {
|
||||
require, assert := require.New(t), assert.New(t)
|
||||
err := BadRequestStatus("test: hello world")
|
||||
assert.ErrorContains(err, "test: hello world")
|
||||
assert.Equal(status.Code(err), codes.InvalidArgument)
|
||||
_, ok := status.FromError(err)
|
||||
require.True(ok)
|
||||
}
|
||||
|
||||
func Test_UnknownStatusf(t *testing.T) {
|
||||
require, assert := require.New(t), assert.New(t)
|
||||
err := UnknownStatusf("test: %s", "hello world")
|
||||
assert.ErrorContains(err, "test: hello world")
|
||||
assert.Equal(status.Code(err), codes.Internal)
|
||||
_, ok := status.FromError(err)
|
||||
require.True(ok)
|
||||
}
|
||||
|
||||
func Test_UnknownStatus(t *testing.T) {
|
||||
require, assert := require.New(t), assert.New(t)
|
||||
err := UnknownStatusf("test: hello world")
|
||||
assert.ErrorContains(err, "test: hello world")
|
||||
assert.Equal(status.Code(err), codes.Internal)
|
||||
_, ok := status.FromError(err)
|
||||
require.True(ok)
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// 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/aws-sdk-go-v2/service/s3/types"
|
||||
"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,
|
||||
}
|
||||
}
|
||||
|
||||
// TestAwsS3NotFoundError returns an S3 NotFound error.
|
||||
func TestAwsS3NotFoundError(msg string) error {
|
||||
return &types.NotFound{Message: &msg}
|
||||
}
|
||||
|
||||
// TestAwsS3NoSuchKeyError returns an S3 NoSuchKey error.
|
||||
func TestAwsS3NoSuchKeyError(msg string) error {
|
||||
return &types.NoSuchKey{Message: &msg}
|
||||
}
|
||||
|
||||
// TestAwsS3InvalidObjectStateError returns an S3 InvalidObjectState error.
|
||||
func TestAwsS3InvalidObjectStateError(msg string) error {
|
||||
return &types.InvalidObjectState{Message: &msg}
|
||||
}
|
||||
|
||||
// TestAwsS3NoSuchBucketError returns an S3 NoSuchBucket error.
|
||||
func TestAwsS3NoSuchBucketError(msg string) error {
|
||||
return &types.NoSuchBucket{Message: &msg}
|
||||
}
|
||||
|
||||
// TestAwsS3AccessDeniedError returns an S3 AccessDenied error.
|
||||
func TestAwsS3AccessDeniedError(msg string) error {
|
||||
return &types.AccessDenied{Message: &msg}
|
||||
}
|
||||
|
||||
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,6 +1,3 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package values
|
||||
|
||||
import (
|
||||
@@ -84,33 +81,6 @@ func GetTimeValue(in *structpb.Struct, k string) (time.Time, error) {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// GetMapStringString returns a map[string]string value and no error if the given key
|
||||
// is found in the provided proto struct input. An error is returned if the key
|
||||
// is not found or the value type is not map[string]string.
|
||||
func GetMapStringString(in *structpb.Struct, k string, required bool) (map[string]string, error) {
|
||||
mv := in.GetFields()
|
||||
v, ok := mv[k]
|
||||
if !ok {
|
||||
if required {
|
||||
return nil, fmt.Errorf("missing required value %q", k)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
result := map[string]string{}
|
||||
m, ok := v.AsInterface().(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type for value %q: want map[string]string, got %T", k, v.AsInterface())
|
||||
}
|
||||
for mapKey, mapValue := range m {
|
||||
e, ok := mapValue.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type for value in map[%q]: want string, got %T", mapKey, mapValue)
|
||||
}
|
||||
result[mapKey] = e
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// StructFields returns a map[string]struct{} of the
|
||||
// proto struct input.
|
||||
func StructFields(s *structpb.Struct) map[string]struct{} {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package values
|
||||
|
||||
import (
|
||||
@@ -208,95 +205,3 @@ func TestGetTimeValue(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMapStringString(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in *structpb.Struct
|
||||
key string
|
||||
required bool
|
||||
expected map[string]string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "required missing",
|
||||
in: &structpb.Struct{},
|
||||
key: "foo",
|
||||
required: true,
|
||||
expectedErr: "missing required value \"foo\"",
|
||||
},
|
||||
{
|
||||
name: "optional missing",
|
||||
in: &structpb.Struct{},
|
||||
key: "foo",
|
||||
},
|
||||
{
|
||||
name: "non-map[string]string value",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewBoolValue(true),
|
||||
},
|
||||
},
|
||||
key: "foo",
|
||||
expectedErr: "unexpected type for value \"foo\": want map[string]string, got bool",
|
||||
},
|
||||
{
|
||||
name: "empty map",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStructValue(&structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{},
|
||||
}),
|
||||
},
|
||||
},
|
||||
key: "foo",
|
||||
expected: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "non-string value in map",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStructValue(&structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"bar": structpb.NewBoolValue(true),
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
key: "foo",
|
||||
expectedErr: "unexpected type for value in map[\"bar\"]: want string, got bool",
|
||||
},
|
||||
{
|
||||
name: "good",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStructValue(&structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"bar": structpb.NewStringValue("zoo"),
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
key: "foo",
|
||||
expected: map[string]string{
|
||||
"bar": "zoo",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
actual, err := GetMapStringString(tc.in, tc.key, tc.required)
|
||||
if tc.expectedErr != "" {
|
||||
require.EqualError(err, tc.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Equal(tc.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
+1
-4
@@ -1,6 +1,3 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
@@ -32,6 +29,6 @@ type AwsPlugin struct {
|
||||
func NewAwsPlugin() *AwsPlugin {
|
||||
return &AwsPlugin{
|
||||
HostPlugin: &host.HostPlugin{},
|
||||
StoragePlugin: storage.New(),
|
||||
StoragePlugin: &storage.StoragePlugin{},
|
||||
}
|
||||
}
|
||||
@@ -105,27 +105,18 @@ a configured IAM user for this provider:
|
||||
The following `attributes` are valid on an AWS host catalog resource:
|
||||
|
||||
- `disable_credential_rotation` (bool): If `true`, credential rotation will not
|
||||
be performed. See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
|
||||
be performed. See the [Credential Rotation](#credential-rotation) section
|
||||
above.
|
||||
- `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:
|
||||
The following `secrets` are valid on an AWS host catalog resource:
|
||||
|
||||
- `access_key_id` (string): The access key ID for the IAM user to use with this
|
||||
host catalog.
|
||||
- `secret_access_key` (string): The secret access key for the IAM user to use
|
||||
with this host catalog.
|
||||
|
||||
See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
|
||||
|
||||
### Host Set
|
||||
|
||||
The following attributes are valid on an AWS host Set resource:
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
@@ -19,14 +16,6 @@ import (
|
||||
// map.
|
||||
type CatalogAttributes struct {
|
||||
*cred.CredentialAttributes
|
||||
|
||||
// DualStack is used for configuring how the aws client will resolve requests.
|
||||
DualStack bool
|
||||
|
||||
// InstanceAddressesOnly specifies whether only the instance addresses (private IPv4/DNS,
|
||||
// public IPv4/DNS, and IPv6) are synced, or all addresses, including those from
|
||||
// secondary ENIs, are synced.
|
||||
InstanceAddressesOnly bool
|
||||
}
|
||||
|
||||
func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
|
||||
@@ -39,18 +28,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)
|
||||
|
||||
instanceAddressesOnly, err := values.GetBoolValue(in, ConstInstanceAddressesOnly, false)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstInstanceAddressesOnly)] = err.Error()
|
||||
}
|
||||
delete(unknownFields, ConstInstanceAddressesOnly)
|
||||
|
||||
for s := range unknownFields {
|
||||
switch s {
|
||||
// Ignore knownFields from CredentialAttributes
|
||||
@@ -58,14 +35,6 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
|
||||
continue
|
||||
case cred.ConstDisableCredentialRotation:
|
||||
continue
|
||||
case cred.ConstRoleArn:
|
||||
continue
|
||||
case cred.ConstRoleExternalId:
|
||||
continue
|
||||
case cred.ConstRoleSessionName:
|
||||
continue
|
||||
case cred.ConstRoleTags:
|
||||
continue
|
||||
default:
|
||||
badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field"
|
||||
}
|
||||
@@ -76,9 +45,7 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
|
||||
}
|
||||
|
||||
return &CatalogAttributes{
|
||||
CredentialAttributes: credAttributes,
|
||||
DualStack: dualStack,
|
||||
InstanceAddressesOnly: instanceAddressesOnly,
|
||||
CredentialAttributes: credAttributes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
@@ -9,7 +6,6 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
cred "github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
@@ -21,66 +17,36 @@ import (
|
||||
func TestGetCatalogAttributes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in *structpb.Struct
|
||||
in map[string]any
|
||||
expected *CatalogAttributes
|
||||
expectedErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing region",
|
||||
in: &structpb.Struct{
|
||||
Fields: make(map[string]*structpb.Value),
|
||||
},
|
||||
name: "missing region",
|
||||
in: map[string]any{},
|
||||
expectedErrContains: "missing required value \"region\"",
|
||||
},
|
||||
{
|
||||
name: "bad value for disable_credential_rotation",
|
||||
in: map[string]any{
|
||||
credential.ConstRegion: "us-west-2",
|
||||
credential.ConstDisableCredentialRotation: "sure",
|
||||
},
|
||||
expectedErrContains: "unexpected type for value \"disable_credential_rotation\": want bool, got string",
|
||||
},
|
||||
{
|
||||
name: "unknown fields",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
"foo": structpb.NewBoolValue(true),
|
||||
"bar": structpb.NewBoolValue(true),
|
||||
},
|
||||
in: map[string]any{
|
||||
credential.ConstRegion: "us-west-2",
|
||||
"foo": true,
|
||||
"bar": true,
|
||||
},
|
||||
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: "withInstanceAddressesOnly",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
"instance_addresses_only": structpb.NewBoolValue(true),
|
||||
},
|
||||
},
|
||||
expected: &CatalogAttributes{
|
||||
CredentialAttributes: &credential.CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
DisableCredentialRotation: false,
|
||||
},
|
||||
InstanceAddressesOnly: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
},
|
||||
in: map[string]any{
|
||||
credential.ConstRegion: "us-west-2",
|
||||
},
|
||||
expected: &CatalogAttributes{
|
||||
CredentialAttributes: &credential.CredentialAttributes{
|
||||
@@ -90,31 +56,15 @@ func TestGetCatalogAttributes(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "credential attributes",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
"disable_credential_rotation": structpb.NewBoolValue(true),
|
||||
"role_arn": structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
|
||||
"role_external_id": structpb.NewStringValue("1234567890"),
|
||||
"role_session_name": structpb.NewStringValue("test-session"),
|
||||
"role_tags": structpb.NewStructValue(&structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStringValue("bar"),
|
||||
},
|
||||
}),
|
||||
},
|
||||
name: "with disable_credential_rotation",
|
||||
in: map[string]any{
|
||||
credential.ConstRegion: "us-west-2",
|
||||
credential.ConstDisableCredentialRotation: true,
|
||||
},
|
||||
expected: &CatalogAttributes{
|
||||
CredentialAttributes: &cred.CredentialAttributes{
|
||||
CredentialAttributes: &credential.CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
DisableCredentialRotation: true,
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -125,7 +75,10 @@ func TestGetCatalogAttributes(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
actual, err := getCatalogAttributes(tc.in)
|
||||
input, err := structpb.NewStruct(tc.in)
|
||||
require.NoError(err)
|
||||
|
||||
actual, err := getCatalogAttributes(input)
|
||||
if tc.expectedErrContains != "" {
|
||||
require.Error(err)
|
||||
require.Contains(err.Error(), tc.expectedErrContains)
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
const (
|
||||
@@ -10,12 +7,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"
|
||||
|
||||
// ConstInstanceAddressesOnly is the key (instance_addresses_only) that specifies whether
|
||||
// only the instance addresses (private IPv4/DNS, public IPv4/DNS, and IPv6) are synced,
|
||||
// or all addresses, including those from secondary ENIs, are synced.
|
||||
ConstInstanceAddressesOnly = "instance_addresses_only"
|
||||
)
|
||||
+236
-264
@@ -1,23 +1,25 @@
|
||||
package host
|
||||
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
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/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
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 +41,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.Region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -59,18 +66,13 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
|
||||
// Set up the creds in our persisted state.
|
||||
credState, err := credential.NewAwsCredentialPersistedState(
|
||||
append([]credential.AwsCredentialPersistedStateOption{
|
||||
credential.WithCredentialsConfig(credConfig),
|
||||
credential.WithAccessKeyId(credConfig.AccessKey),
|
||||
credential.WithSecretAccessKey(credConfig.SecretKey),
|
||||
credential.WithRegion(credConfig.Region),
|
||||
}, p.testCredStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("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 +81,24 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
|
||||
}, p.testCatalogStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("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 the credentials if we're not skipping them.
|
||||
if !catalogAttributes.DisableCredentialRotation {
|
||||
if err := credState.RotateCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||
}
|
||||
} else {
|
||||
// Simply validate if we aren't rotating.
|
||||
if err := credState.ValidateCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential validation: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
persistedProto, err := catalogState.toProto()
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("error converting persisted state to proto: %s", err)
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
return &pb.OnCreateCatalogResponse{
|
||||
@@ -106,105 +111,42 @@ 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,
|
||||
p.testCredStateOpts...)
|
||||
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("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.BadRequestStatusf("error setting up new credential persisted state: %s", err)
|
||||
}
|
||||
newCatalogState, err := newAwsCatalogPersistedState(
|
||||
append([]awsCatalogPersistedStateOption{
|
||||
withCredentials(newCredState),
|
||||
}, p.testCatalogStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("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 +155,44 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
|
||||
}, p.testCatalogStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("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 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.Region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Replace the credentials. This checks the timestamp on the last
|
||||
// rotation time as well and deletes the credentials if we are
|
||||
// managing them (ie: if we've rotated them before).
|
||||
if err := credState.ReplaceCreds(catalogSecrets.AccessKey, catalogSecrets.SecretKey); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !catalogAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
|
||||
// If we're enabling rotation now but didn't before, or have
|
||||
// freshly replaced credentials, we can rotate here.
|
||||
if err := credState.RotateCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// That's it!
|
||||
persistedProto, err := catalogState.toProto()
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("error converting persisted state to proto: %s", err)
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
return &pb.OnUpdateCatalogResponse{
|
||||
@@ -239,12 +204,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)
|
||||
@@ -260,11 +225,9 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo
|
||||
// may be corrupt/and or legitimately missing.
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetSecrets(),
|
||||
catalogAttributes.CredentialAttributes,
|
||||
catalogAttributes.DualStack,
|
||||
p.testCredStateOpts...)
|
||||
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
_, err = newAwsCatalogPersistedState(
|
||||
@@ -273,18 +236,15 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo
|
||||
}, p.testCatalogStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("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 !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
|
||||
}
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
// Delete old/existing credentials. This is done with the same
|
||||
// credentials to ensure that it has the proper permissions to do
|
||||
// it.
|
||||
if err := credState.DeleteCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during catalog deletion: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,12 +282,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)
|
||||
@@ -337,11 +297,9 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
|
||||
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetSecrets(),
|
||||
catalogAttributes.CredentialAttributes,
|
||||
catalogAttributes.DualStack,
|
||||
p.testCredStateOpts...)
|
||||
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
catalogState, err := newAwsCatalogPersistedState(
|
||||
@@ -350,47 +308,55 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
|
||||
}, p.testCatalogStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("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(catalogAttributes.Region)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("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(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
|
||||
|
||||
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "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)
|
||||
@@ -400,11 +366,9 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
|
||||
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetSecrets(),
|
||||
catalogAttributes.CredentialAttributes,
|
||||
catalogAttributes.DualStack,
|
||||
p.testCredStateOpts...)
|
||||
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
catalogState, err := newAwsCatalogPersistedState(
|
||||
@@ -413,38 +377,45 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
|
||||
}, p.testCatalogStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("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(catalogAttributes.Region)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("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(input)
|
||||
if err == nil {
|
||||
return nil, status.Error(codes.FailedPrecondition, "query error: DescribeInstances DryRun should have returned error, but none was found")
|
||||
}
|
||||
|
||||
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "DryRunOperation" {
|
||||
// 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 +429,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)
|
||||
@@ -471,13 +442,9 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetSecrets(),
|
||||
catalogAttributes.CredentialAttributes,
|
||||
catalogAttributes.DualStack,
|
||||
p.testCredStateOpts...)
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(req.GetPersisted().GetSecrets(), p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
catalogState, err := newAwsCatalogPersistedState(
|
||||
@@ -486,12 +453,12 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
||||
}, p.testCatalogStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("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 +473,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.BadRequestStatusf("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 +486,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
||||
|
||||
input, err := buildDescribeInstancesInput(setAttrs, false)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("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,21 +494,17 @@ 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(catalogAttributes.Region)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("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.
|
||||
var maxLen int
|
||||
for i, query := range queries {
|
||||
output, err := ec2Client.DescribeInstances(ctx, query.Input)
|
||||
output, err := ec2Client.DescribeInstances(query.Input)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("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
|
||||
@@ -550,9 +513,9 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
||||
// set of hosts afterwards (possibly removing duplicates).
|
||||
for _, reservation := range output.Reservations {
|
||||
for _, instance := range reservation.Instances {
|
||||
host, err := awsInstanceToHost(instance, catalogAttributes)
|
||||
host, err := awsInstanceToHost(instance)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("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)
|
||||
@@ -593,8 +556,8 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildFilters(attrs *SetAttributes) ([]types.Filter, error) {
|
||||
var filters []types.Filter
|
||||
func buildFilters(attrs *SetAttributes) ([]*ec2.Filter, error) {
|
||||
var filters []*ec2.Filter
|
||||
var foundStateFilter bool
|
||||
for _, filterAttr := range attrs.Filters {
|
||||
// Each filter arrives in "k=v1,v2" format. First, split on equal sign.
|
||||
@@ -613,9 +576,14 @@ func buildFilters(attrs *SetAttributes) ([]types.Filter, error) {
|
||||
foundStateFilter = true
|
||||
}
|
||||
|
||||
filters = append(filters, types.Filter{
|
||||
var filterValues []*string
|
||||
for _, val := range strings.Split(filterValue, ",") {
|
||||
filterValues = append(filterValues, aws.String(val))
|
||||
}
|
||||
|
||||
filters = append(filters, &ec2.Filter{
|
||||
Name: aws.String(filterKey),
|
||||
Values: strings.Split(filterValue, ","),
|
||||
Values: filterValues,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -624,9 +592,9 @@ func buildFilters(attrs *SetAttributes) ([]types.Filter, error) {
|
||||
// instance-state-name = ["running"] to the filter set. This
|
||||
// ensures that we filter on running instances only at the API
|
||||
// side, saving time when processing results.
|
||||
filters = append(filters, types.Filter{
|
||||
filters = append(filters, &ec2.Filter{
|
||||
Name: aws.String("instance-state-name"),
|
||||
Values: []string{string(types.InstanceStateNameRunning)},
|
||||
Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -648,58 +616,97 @@ func buildDescribeInstancesInput(attrs *SetAttributes, dryRun bool) (*ec2.Descri
|
||||
// awsInstanceToHost processes data from an ec2.Instance and returns
|
||||
// a ListHostsResponseHost with the ID and network addresses
|
||||
// populated.
|
||||
func awsInstanceToHost(instance types.Instance, catalogAttributes *CatalogAttributes) (*pb.ListHostsResponseHost, error) {
|
||||
if aws.ToString(instance.InstanceId) == "" {
|
||||
return nil, fmt.Errorf("response integrity error: missing instance id")
|
||||
func awsInstanceToHost(instance *ec2.Instance) (*pb.ListHostsResponseHost, error) {
|
||||
// Integrity check: some fields should always be non-nil. Check
|
||||
// those.
|
||||
if instance == nil {
|
||||
return nil, errors.New("response integrity error: missing instance entry")
|
||||
}
|
||||
if catalogAttributes == nil {
|
||||
return nil, fmt.Errorf("response integrity error: missing catalog attributes")
|
||||
|
||||
if aws.StringValue(instance.InstanceId) == "" {
|
||||
return nil, errors.New("response integrity error: missing instance id")
|
||||
}
|
||||
|
||||
result := new(pb.ListHostsResponseHost)
|
||||
|
||||
// External ID is the instance ID.
|
||||
result.ExternalId = aws.ToString(instance.InstanceId)
|
||||
result.ExternalId = aws.StringValue(instance.InstanceId)
|
||||
|
||||
// External Name is the AWS "Name" tag, if it exists.
|
||||
for _, t := range instance.Tags {
|
||||
if aws.ToString(t.Key) == ConstInstanceNameTagKey {
|
||||
result.ExternalName = aws.ToString(t.Value)
|
||||
if aws.StringValue(t.Key) == ConstInstanceNameTagKey {
|
||||
result.ExternalName = aws.StringValue(t.Value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// First let's get the instance's Private IPv4/DNS, Public IPv4/DNS, and IPv6 addresses
|
||||
result.IpAddresses = appendDistinct(result.IpAddresses, instance.PrivateIpAddress, instance.PublicIpAddress, instance.Ipv6Address)
|
||||
result.DnsNames = appendDistinct(result.DnsNames, instance.PrivateDnsName, instance.PublicDnsName)
|
||||
|
||||
if catalogAttributes.InstanceAddressesOnly {
|
||||
// return early as we already have the instance addresses
|
||||
return result, nil
|
||||
// First IP address/dns name are always the private fields if they
|
||||
// are populated
|
||||
if aws.StringValue(instance.PrivateIpAddress) != "" {
|
||||
result.IpAddresses = append(result.IpAddresses, aws.StringValue(instance.PrivateIpAddress))
|
||||
}
|
||||
if aws.StringValue(instance.PrivateDnsName) != "" {
|
||||
result.DnsNames = append(result.DnsNames, aws.StringValue(instance.PrivateDnsName))
|
||||
}
|
||||
|
||||
// Now go through all of the interfaces and sync the IP address of
|
||||
// Public IP address/dns names are next
|
||||
if aws.StringValue(instance.PublicIpAddress) != "" {
|
||||
result.IpAddresses = append(result.IpAddresses, aws.StringValue(instance.PublicIpAddress))
|
||||
}
|
||||
if aws.StringValue(instance.PublicDnsName) != "" {
|
||||
result.DnsNames = append(result.DnsNames, aws.StringValue(instance.PublicDnsName))
|
||||
}
|
||||
|
||||
// Now go through all of the interfaces and log the IP address of
|
||||
// every interface.
|
||||
for _, iface := range instance.NetworkInterfaces {
|
||||
if iface == nil {
|
||||
// Probably will never happen, but just in case
|
||||
return nil, errors.New("response integrity error: interface entry is nil")
|
||||
}
|
||||
|
||||
// Populate default IP addresses/DNS name similar to how we do
|
||||
// for the entire instance.
|
||||
result.IpAddresses = appendDistinct(result.IpAddresses, iface.PrivateIpAddress)
|
||||
result.DnsNames = appendDistinct(result.DnsNames, iface.PrivateDnsName)
|
||||
if aws.StringValue(iface.PrivateIpAddress) != "" && !stringInSlice(result.IpAddresses, aws.StringValue(iface.PrivateIpAddress)) {
|
||||
result.IpAddresses = append(result.IpAddresses, aws.StringValue(iface.PrivateIpAddress))
|
||||
}
|
||||
if aws.StringValue(iface.PrivateDnsName) != "" && !stringInSlice(result.DnsNames, aws.StringValue(iface.PrivateDnsName)) {
|
||||
result.DnsNames = append(result.DnsNames, aws.StringValue(iface.PrivateDnsName))
|
||||
}
|
||||
|
||||
// Iterate through the private IP addresses and log the
|
||||
// information.
|
||||
for _, addr := range iface.PrivateIpAddresses {
|
||||
result.IpAddresses = appendDistinct(result.IpAddresses, addr.PrivateIpAddress)
|
||||
result.DnsNames = appendDistinct(result.DnsNames, addr.PrivateDnsName)
|
||||
if addr.Association != nil {
|
||||
result.IpAddresses = appendDistinct(result.IpAddresses, addr.Association.PublicIp)
|
||||
result.DnsNames = appendDistinct(result.DnsNames, addr.Association.PublicDnsName)
|
||||
if addr == nil {
|
||||
return nil, errors.New("response integrity error: interface address entry is nil")
|
||||
}
|
||||
|
||||
// Add private address/dns name if they have not been added yet
|
||||
if aws.StringValue(addr.PrivateIpAddress) != "" && !stringInSlice(result.IpAddresses, aws.StringValue(addr.PrivateIpAddress)) {
|
||||
result.IpAddresses = append(result.IpAddresses, aws.StringValue(addr.PrivateIpAddress))
|
||||
}
|
||||
if aws.StringValue(addr.PrivateDnsName) != "" && !stringInSlice(result.DnsNames, aws.StringValue(addr.PrivateDnsName)) {
|
||||
result.DnsNames = append(result.DnsNames, aws.StringValue(addr.PrivateDnsName))
|
||||
}
|
||||
|
||||
// Add public address/dns name if they have not been added yet
|
||||
if addr.Association != nil && aws.StringValue(addr.Association.PublicIp) != "" && !stringInSlice(result.IpAddresses, aws.StringValue(addr.Association.PublicIp)) {
|
||||
result.IpAddresses = append(result.IpAddresses, aws.StringValue(addr.Association.PublicIp))
|
||||
}
|
||||
if addr.Association != nil && aws.StringValue(addr.Association.PublicDnsName) != "" && !stringInSlice(result.DnsNames, aws.StringValue(addr.Association.PublicDnsName)) {
|
||||
result.DnsNames = append(result.DnsNames, aws.StringValue(addr.Association.PublicDnsName))
|
||||
}
|
||||
}
|
||||
|
||||
// Add the IPv6 addresses.
|
||||
for _, addr := range iface.Ipv6Addresses {
|
||||
result.IpAddresses = appendDistinct(result.IpAddresses, addr.Ipv6Address)
|
||||
if addr == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if addr.Ipv6Address != nil && aws.StringValue(addr.Ipv6Address) != "" {
|
||||
result.IpAddresses = append(result.IpAddresses, aws.StringValue(addr.Ipv6Address))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -707,47 +714,12 @@ func awsInstanceToHost(instance types.Instance, catalogAttributes *CatalogAttrib
|
||||
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 {
|
||||
for _, e := range elems {
|
||||
value := aws.ToString(e)
|
||||
if value == "" || stringInSlice(slice, value) {
|
||||
continue
|
||||
}
|
||||
slice = append(slice, value)
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
// stringInSlice returns true if the slice contains the given element.
|
||||
func stringInSlice(s []string, x string) bool {
|
||||
for _, y := range s {
|
||||
if x == y {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,516 +0,0 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
// This file contains integration tests that demonstrate the role_arn silent
|
||||
// fallback bug at the plugin level.
|
||||
//
|
||||
// THE BUG IN ONE SENTENCE:
|
||||
// When a host catalog is configured with role_arn but no static credentials,
|
||||
// the plugin never calls sts:AssumeRole. It silently falls back to the
|
||||
// ambient AWS credential chain (env vars / instance profile), so a bogus or
|
||||
// unreachable ARN produces no error and returns the same hosts as if no ARN
|
||||
// were specified at all.
|
||||
//
|
||||
// HOW TO READ THESE TESTS:
|
||||
// Each test function covers one plugin operation (OnCreateCatalog or
|
||||
// ListHosts). Within each function, the cases are ordered to tell a
|
||||
// progressive story:
|
||||
// 1. Ambient baseline — no role_arn, works fine
|
||||
// 2. Static-key path — explicit keys, works fine
|
||||
// 3. Bug case — bogus role_arn + no keys, silently succeeds
|
||||
// 4. Desired behavior — bogus role_arn + STS error injected, SHOULD fail
|
||||
// 5. Validation errors — combinations that are correctly rejected
|
||||
//
|
||||
// Cases marked "BUG" in the soft-fail block currently pass (no error is
|
||||
// returned). Once the bug is fixed those tests will start asserting correctly
|
||||
// and the t.Fail() path will no longer be reached.
|
||||
//
|
||||
// Run both test functions:
|
||||
//
|
||||
// go test ./plugin/service/host/... -run TestPluginRoleArnFallback -v
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
// ambientInstance is the single fake EC2 instance returned by every mock in
|
||||
// this file. Seeing this instance in the response means the ambient credential
|
||||
// chain was used, regardless of what role_arn was configured.
|
||||
var ambientInstance = ec2types.Instance{
|
||||
InstanceId: aws.String("i-ambientinstance"),
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateDnsName: aws.String("ambient.example.internal"),
|
||||
NetworkInterfaces: []ec2types.InstanceNetworkInterface{
|
||||
{
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateDnsName: aws.String("ambient.example.internal"),
|
||||
PrivateIpAddresses: []ec2types.InstancePrivateIpAddress{
|
||||
{
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateDnsName: aws.String("ambient.example.internal"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ambientInstanceOutput is a DescribeInstancesOutput that returns ambientInstance.
|
||||
// All mock EC2 clients in this file are configured with this output so that
|
||||
// success cases always return the same identifiable host.
|
||||
var ambientInstanceOutput = &ec2.DescribeInstancesOutput{
|
||||
Reservations: []ec2types.Reservation{
|
||||
{Instances: []ec2types.Instance{ambientInstance}},
|
||||
},
|
||||
}
|
||||
|
||||
// catalogAttrsStruct builds a catalog attributes struct for the given region.
|
||||
// disableRotation is always written explicitly — false must be set to prevent
|
||||
// credential rotation from being attempted (which would require IAM mocks).
|
||||
// roleArn is written only when non-empty.
|
||||
func catalogAttrsStruct(region, roleArn string, disableRotation bool) *structpb.Struct {
|
||||
fields := map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue(region),
|
||||
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(disableRotation),
|
||||
}
|
||||
if roleArn != "" {
|
||||
fields[credential.ConstRoleArn] = structpb.NewStringValue(roleArn)
|
||||
}
|
||||
return &structpb.Struct{Fields: fields}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// OnCreateCatalog
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestPluginRoleArnFallback_OnCreateCatalog(t *testing.T) {
|
||||
// bogusARN is the ARN used in bug-scenario cases. It contains a fake
|
||||
// account ID (000000000000) that does not correspond to any real AWS
|
||||
// account. A correct implementation would fail when it attempts to call
|
||||
// sts:AssumeRole with this ARN. Currently it is silently ignored.
|
||||
const bogusARN = "arn:aws:sts::000000000000:assumed-role/fake/whoever"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req *pb.OnCreateCatalogRequest
|
||||
credOpts []credential.AwsCredentialPersistedStateOption
|
||||
catalogOpts []awsCatalogPersistedStateOption
|
||||
// wantErr is set for cases that should return an error.
|
||||
wantErr bool
|
||||
wantErrContains string
|
||||
wantErrCode codes.Code
|
||||
// wantEmptySecrets is true when the persisted secrets should be empty
|
||||
// (dynamic / ambient credentials are not stored in the database).
|
||||
wantEmptySecrets bool
|
||||
}{
|
||||
{
|
||||
// NOTE: No role_arn, no static keys. OnCreateCatalog succeeds using
|
||||
// whatever ambient credentials are available. The persisted secrets
|
||||
// are empty because there are no static keys to store. This is the
|
||||
// baseline — it shows that ambient credentials work on their own.
|
||||
name: "1. ambient only — no role_arn, no static keys (baseline)",
|
||||
req: &pb.OnCreateCatalogRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
Attributes: catalogAttrsStruct("us-east-1", "", true),
|
||||
},
|
||||
},
|
||||
},
|
||||
catalogOpts: []awsCatalogPersistedStateOption{
|
||||
withTestEC2APIFunc(newTestMockEC2(
|
||||
nil,
|
||||
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
|
||||
)),
|
||||
},
|
||||
wantEmptySecrets: true,
|
||||
},
|
||||
{
|
||||
// NOTE: Explicit static keys, rotation disabled. OnCreateCatalog
|
||||
// succeeds and persists the key pair. This is the normal static-key
|
||||
// path — no role assumption involved. No credOpts are needed here:
|
||||
// with disable_credential_rotation=true and StaticAWS keys, the
|
||||
// dry-run only calls the mocked EC2 DescribeInstances and does not
|
||||
// make any real AWS calls (same pattern as the existing
|
||||
// TestPluginOnCreateCatalogSuccess/usingStaticCredentials test).
|
||||
name: "2. static keys only (baseline)",
|
||||
req: &pb.OnCreateCatalogRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
Attributes: catalogAttrsStruct("us-east-1", "", true),
|
||||
},
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAfoo"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
catalogOpts: []awsCatalogPersistedStateOption{
|
||||
withTestEC2APIFunc(newTestMockEC2(
|
||||
nil,
|
||||
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
|
||||
)),
|
||||
},
|
||||
wantEmptySecrets: false, // key pair is stored
|
||||
},
|
||||
{
|
||||
// NOTE: THE BUG — bogus role_arn, no static keys. OnCreateCatalog
|
||||
// succeeds even though the ARN points to a nonexistent role. The
|
||||
// dry-run DescribeInstances call is mocked out, so the bad ARN is
|
||||
// never presented to STS. The persisted secrets are empty (dynamic
|
||||
// credentials are not stored), which is correct — but the absence
|
||||
// of an error is not.
|
||||
name: "3. bogus role_arn + no static keys — BUG: succeeds silently",
|
||||
req: &pb.OnCreateCatalogRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
Attributes: catalogAttrsStruct("us-east-1", bogusARN, true),
|
||||
},
|
||||
},
|
||||
},
|
||||
catalogOpts: []awsCatalogPersistedStateOption{
|
||||
withTestEC2APIFunc(newTestMockEC2(
|
||||
nil,
|
||||
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
|
||||
)),
|
||||
},
|
||||
wantEmptySecrets: true,
|
||||
},
|
||||
{
|
||||
// NOTE: DESIRED BEHAVIOR (currently broken). Same bogus role_arn,
|
||||
// but we inject an STS mock that returns an error when AssumeRole
|
||||
// is called. A correct implementation would propagate that error
|
||||
// and OnCreateCatalog would fail. Currently it succeeds because
|
||||
// sts:AssumeRole is never called — the STS mock is never reached.
|
||||
// The soft-fail below documents this: if err == nil it means the
|
||||
// bug is still present.
|
||||
name: "4. bogus role_arn + STS error injected — DESIRED BEHAVIOR: should fail",
|
||||
req: &pb.OnCreateCatalogRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
Attributes: catalogAttrsStruct("us-east-1", bogusARN, true),
|
||||
},
|
||||
},
|
||||
},
|
||||
credOpts: []credential.AwsCredentialPersistedStateOption{
|
||||
credential.WithStateTestOpts([]awsutil.Option{
|
||||
awsutil.WithSTSAPIFunc(
|
||||
awsutil.NewMockSTS(
|
||||
awsutil.WithAssumeRoleError(
|
||||
errors.New("simulated STS AssumeRole failure: no such role"),
|
||||
),
|
||||
),
|
||||
),
|
||||
}),
|
||||
},
|
||||
catalogOpts: []awsCatalogPersistedStateOption{
|
||||
withTestEC2APIFunc(newTestMockEC2(
|
||||
nil,
|
||||
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
|
||||
)),
|
||||
},
|
||||
// We do NOT set wantErr here — the soft-fail logic below handles
|
||||
// this case explicitly so we can log a clear BUG message.
|
||||
},
|
||||
{
|
||||
// NOTE: Providing both role_arn and static keys is correctly
|
||||
// rejected at validation time. The switch in GetCredentialsConfig
|
||||
// catches this combination before any AWS call is attempted.
|
||||
name: "5. role_arn + static keys — correctly rejected (conflict error)",
|
||||
wantErr: true,
|
||||
req: &pb.OnCreateCatalogRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
Attributes: catalogAttrsStruct("us-east-1", bogusARN, true),
|
||||
},
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAfoo"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErrContains: "conflicts with role_arn value",
|
||||
wantErrCode: codes.InvalidArgument,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &HostPlugin{
|
||||
testCredStateOpts: tt.credOpts,
|
||||
testCatalogStateOpts: tt.catalogOpts,
|
||||
}
|
||||
|
||||
rsp, err := p.OnCreateCatalog(context.Background(), tt.req)
|
||||
|
||||
// Case 4 — desired behavior, soft-fail.
|
||||
if tt.name == "4. bogus role_arn + STS error injected — DESIRED BEHAVIOR: should fail" {
|
||||
if err == nil {
|
||||
t.Log("BUG: expected OnCreateCatalog to fail when sts:AssumeRole returns an error,",
|
||||
"but it succeeded — the role_arn is being silently ignored and sts:AssumeRole",
|
||||
"is never called. This test will pass once the bug is fixed.")
|
||||
t.Fail()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.wantErrContains)
|
||||
require.Equal(t, tt.wantErrCode.String(), status.Code(err).String())
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
secrets := rsp.GetPersisted().GetSecrets().GetFields()
|
||||
// Remove the rotation timestamp before checking emptiness.
|
||||
delete(secrets, credential.ConstCredsLastRotatedTime)
|
||||
|
||||
if tt.wantEmptySecrets {
|
||||
require.Empty(t, secrets,
|
||||
"expected empty persisted secrets (dynamic/ambient credentials are not stored)")
|
||||
} else {
|
||||
require.NotEmpty(t, secrets,
|
||||
"expected persisted secrets to contain the static key pair")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// ListHosts
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// listHostsRequest builds a minimal ListHostsRequest. The persisted secrets
|
||||
// are always an empty struct because the bug scenario uses dynamic credentials
|
||||
// (nothing is persisted for ambient or role-based auth).
|
||||
func listHostsRequest(attrs *structpb.Struct) *pb.ListHostsRequest {
|
||||
return &pb.ListHostsRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
Attributes: attrs,
|
||||
},
|
||||
},
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{},
|
||||
},
|
||||
Sets: []*hostsets.HostSet{
|
||||
{
|
||||
Id: "set-1",
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: &structpb.Struct{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginRoleArnFallback_ListHosts(t *testing.T) {
|
||||
// bogusARN is a clearly invalid ARN: a real AWS account ID has 12 digits,
|
||||
// and "fake/whoever" is not a valid role path. A correct implementation
|
||||
// would call sts:AssumeRole with this ARN and fail. Currently it is ignored.
|
||||
const bogusARN = "arn:aws:sts::000000000000:assumed-role/fake/whoever"
|
||||
|
||||
// ec2WithAmbientInstance is the mock EC2 factory used by all three
|
||||
// narrative cases. It always returns the same ambientInstance regardless
|
||||
// of what credentials were resolved. Seeing "i-ambientinstance" in the
|
||||
// response from case 3 (bogus role_arn) proves the ARN was ignored and
|
||||
// the same ambient identity was used as in case 1.
|
||||
ec2WithAmbientInstance := []awsCatalogPersistedStateOption{
|
||||
withTestEC2APIFunc(newTestMockEC2(
|
||||
nil,
|
||||
testMockEC2WithDescribeInstancesOutput(ambientInstanceOutput),
|
||||
)),
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
req *pb.ListHostsRequest
|
||||
credOpts []credential.AwsCredentialPersistedStateOption
|
||||
catalogOpts []awsCatalogPersistedStateOption
|
||||
// wantHosts is the expected number of hosts in a successful response.
|
||||
wantHosts int
|
||||
wantErr bool
|
||||
wantErrContains string
|
||||
wantErrCode codes.Code
|
||||
}{
|
||||
{
|
||||
// NOTE: No role_arn, no static keys. ListHosts succeeds using
|
||||
// ambient credentials and returns i-ambientinstance (10.0.0.1).
|
||||
// This is the baseline — the "normal" ambient credential flow.
|
||||
name: "1. ambient only — no role_arn, no static keys (baseline)",
|
||||
req: listHostsRequest(catalogAttrsStruct("us-east-1", "", true)),
|
||||
catalogOpts: ec2WithAmbientInstance,
|
||||
wantHosts: 1,
|
||||
},
|
||||
{
|
||||
// NOTE: Explicit static keys. ListHosts succeeds and returns
|
||||
// i-ambientinstance. The same mock EC2 is used here just for
|
||||
// consistency; in a real scenario static keys and role-assumed
|
||||
// keys would return different instances.
|
||||
name: "2. static keys only (baseline)",
|
||||
req: &pb.ListHostsRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
Attributes: catalogAttrsStruct("us-east-1", "", true),
|
||||
},
|
||||
},
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAfoo"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sets: []*hostsets.HostSet{
|
||||
{
|
||||
Id: "set-1",
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: &structpb.Struct{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
catalogOpts: ec2WithAmbientInstance,
|
||||
wantHosts: 1,
|
||||
},
|
||||
{
|
||||
// NOTE: THE BUG — bogus role_arn is set, but ListHosts succeeds
|
||||
// and returns the SAME i-ambientinstance (10.0.0.1) as the ambient
|
||||
// baseline in case 1. The two responses are identical. This proves
|
||||
// the role_arn was completely ignored: the ambient credential chain
|
||||
// was used instead of calling sts:AssumeRole with the bogus ARN.
|
||||
name: "3. bogus role_arn + no static keys — BUG: same hosts as ambient baseline",
|
||||
req: listHostsRequest(catalogAttrsStruct("us-east-1", bogusARN, true)),
|
||||
catalogOpts: ec2WithAmbientInstance,
|
||||
wantHosts: 1, // same count AND same instance ID as case 1 — the ARN was ignored
|
||||
},
|
||||
{
|
||||
// NOTE: DESIRED BEHAVIOR (currently broken). Same bogus role_arn,
|
||||
// but an STS mock is injected that returns an error when AssumeRole
|
||||
// is called. A correct implementation would propagate that STS
|
||||
// error and ListHosts would return an error. Currently ListHosts
|
||||
// succeeds because sts:AssumeRole is never called — the injected
|
||||
// error has no effect. The soft-fail below makes the bug visible
|
||||
// without causing the full test run to abort.
|
||||
name: "4. bogus role_arn + STS error injected — DESIRED BEHAVIOR: should fail",
|
||||
req: listHostsRequest(catalogAttrsStruct("us-east-1", bogusARN, true)),
|
||||
credOpts: []credential.AwsCredentialPersistedStateOption{
|
||||
credential.WithStateTestOpts([]awsutil.Option{
|
||||
awsutil.WithSTSAPIFunc(
|
||||
awsutil.NewMockSTS(
|
||||
awsutil.WithAssumeRoleError(
|
||||
errors.New("simulated STS AssumeRole failure: no such role"),
|
||||
),
|
||||
),
|
||||
),
|
||||
}),
|
||||
},
|
||||
catalogOpts: ec2WithAmbientInstance,
|
||||
// wantErr deliberately not set — handled by soft-fail logic below.
|
||||
},
|
||||
{
|
||||
// NOTE: Providing both role_arn and static keys is correctly
|
||||
// rejected at validation time, before any AWS call is made.
|
||||
// This case is here to show that not all role_arn combinations
|
||||
// are silently ignored — only the "no static keys" path is affected.
|
||||
name: "5. role_arn + static keys — correctly rejected (conflict error)",
|
||||
req: &pb.ListHostsRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
Attributes: catalogAttrsStruct("us-east-1", bogusARN, true),
|
||||
},
|
||||
},
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAfoo"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sets: []*hostsets.HostSet{
|
||||
{
|
||||
Id: "set-1",
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: &structpb.Struct{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// ListHosts calls AwsCredentialPersistedStateFromProto which does
|
||||
// NOT use GetCredentialsConfig — it reconstructs from persisted
|
||||
// state. With both a role_arn in attrs and keys in persisted
|
||||
// secrets, the state is built with both set, which results in the
|
||||
// DynamicAWS path. No conflict error is raised at this layer.
|
||||
// We still include this case for completeness, expecting success
|
||||
// (the conflict is only checked in GetCredentialsConfig, which is
|
||||
// called during OnCreateCatalog, not during ListHosts).
|
||||
catalogOpts: ec2WithAmbientInstance,
|
||||
wantHosts: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p := &HostPlugin{
|
||||
testCredStateOpts: tc.credOpts,
|
||||
testCatalogStateOpts: tc.catalogOpts,
|
||||
}
|
||||
|
||||
rsp, err := p.ListHosts(context.Background(), tc.req)
|
||||
|
||||
// Case 4 — desired behavior, soft-fail.
|
||||
if tc.name == "4. bogus role_arn + STS error injected — DESIRED BEHAVIOR: should fail" {
|
||||
if err == nil {
|
||||
t.Log("BUG: expected ListHosts to fail when sts:AssumeRole returns an error,",
|
||||
"but it succeeded — the role_arn is being silently ignored and sts:AssumeRole",
|
||||
"is never called. Compare the response with case 1 (ambient baseline):",
|
||||
"both return i-ambientinstance, proving the same ambient credentials were",
|
||||
"used in both cases. This test will assert correctly once the bug is fixed.")
|
||||
t.Fail()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if tc.wantErr {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.wantErrContains)
|
||||
require.Equal(t, tc.wantErrCode.String(), status.Code(err).String())
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rsp.GetHosts(), tc.wantHosts,
|
||||
"unexpected number of hosts returned — see case NOTE for what this proves")
|
||||
|
||||
if tc.wantHosts > 0 {
|
||||
require.Equal(t, "i-ambientinstance", rsp.GetHosts()[0].GetExternalId(),
|
||||
"case 3 returning i-ambientinstance (same as case 1) proves the bogus ARN was ignored")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+471
-1237
File diff suppressed because it is too large.
Load diff
@@ -1,25 +1,36 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
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/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
type EC2API interface {
|
||||
DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
|
||||
var customClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).DialContext,
|
||||
},
|
||||
}
|
||||
|
||||
type ec2APIFunc func(*session.Session, ...*aws.Config) (ec2iface.EC2API, error)
|
||||
|
||||
type awsCatalogPersistedStateOption func(s *awsCatalogPersistedState) error
|
||||
|
||||
type awsCatalogPersistedState struct {
|
||||
*credential.AwsCredentialPersistedState
|
||||
|
||||
@@ -27,10 +38,6 @@ type awsCatalogPersistedState struct {
|
||||
testEC2APIFunc ec2APIFunc
|
||||
}
|
||||
|
||||
type ec2APIFunc func(...aws.Config) (EC2API, error)
|
||||
|
||||
type awsCatalogPersistedStateOption func(s *awsCatalogPersistedState) error
|
||||
|
||||
func withTestEC2APIFunc(f ec2APIFunc) awsCatalogPersistedStateOption {
|
||||
return func(s *awsCatalogPersistedState) error {
|
||||
if s.testEC2APIFunc != nil {
|
||||
@@ -73,70 +80,22 @@ 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...)
|
||||
func (s *awsCatalogPersistedState) EC2Client(region string) (ec2iface.EC2API, error) {
|
||||
sess, err := s.GetSession()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing options when fetching EC2 client: %w", err)
|
||||
}
|
||||
awsCfg, err := s.GenerateCredentialChain(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting AWS configuration when fetching EC2 client: %w", err)
|
||||
}
|
||||
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,
|
||||
}))
|
||||
return nil, fmt.Errorf("error getting AWS session when fetching EC2 client: %w", err)
|
||||
}
|
||||
|
||||
if s.testEC2APIFunc != nil {
|
||||
return s.testEC2APIFunc(*awsCfg)
|
||||
return s.testEC2APIFunc(sess, aws.NewConfig().WithRegion(region))
|
||||
}
|
||||
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
|
||||
}
|
||||
cfg := aws.NewConfig()
|
||||
if region != "" {
|
||||
cfg = cfg.WithRegion(region)
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// ec2Option - how ec2Options are passed as arguments
|
||||
type ec2Option func(*ec2Options) error
|
||||
cfg.HTTPClient = customClient
|
||||
|
||||
// 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.New(sess, cfg), nil
|
||||
}
|
||||
@@ -1,25 +1,18 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAwsCatalogPersistedStateEC2Client(t *testing.T) {
|
||||
require := require.New(t)
|
||||
cred, err := credential.NewAwsCredentialPersistedState(
|
||||
credential.WithCredentialsConfig(&awsutil.CredentialsConfig{
|
||||
Region: "us-west-2",
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "barfoo",
|
||||
}),
|
||||
credential.WithRegion("us-west-2"),
|
||||
credential.WithAccessKeyId("foobar"),
|
||||
credential.WithSecretAccessKey("barfoo"),
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
@@ -29,7 +22,7 @@ func TestAwsCatalogPersistedStateEC2Client(t *testing.T) {
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
clientRaw, err := state.EC2Client(context.Background())
|
||||
clientRaw, err := state.EC2Client("us-west-2")
|
||||
require.NoError(err)
|
||||
require.NotNil(clientRaw)
|
||||
client, ok := clientRaw.(*testMockEC2)
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -30,11 +26,10 @@ func (s *testMockEC2State) Reset() {
|
||||
}
|
||||
|
||||
type testMockEC2 struct {
|
||||
EC2API
|
||||
|
||||
State *testMockEC2State
|
||||
Region string
|
||||
ec2iface.EC2API
|
||||
|
||||
State *testMockEC2State
|
||||
Region string
|
||||
DescribeInstancesOutput *ec2.DescribeInstancesOutput
|
||||
DescribeInstancesError error
|
||||
}
|
||||
@@ -56,7 +51,7 @@ func testMockEC2WithDescribeInstancesError(e error) testMockEC2Option {
|
||||
}
|
||||
|
||||
func newTestMockEC2(state *testMockEC2State, opts ...testMockEC2Option) ec2APIFunc {
|
||||
return func(cfgs ...aws.Config) (EC2API, error) {
|
||||
return func(sess *session.Session, cfgs ...*aws.Config) (ec2iface.EC2API, error) {
|
||||
m := &testMockEC2{
|
||||
State: state,
|
||||
}
|
||||
@@ -69,17 +64,14 @@ func newTestMockEC2(state *testMockEC2State, opts ...testMockEC2Option) ec2APIFu
|
||||
|
||||
for _, cfg := range cfgs {
|
||||
// Last region takes precedence
|
||||
if cfg.Region != "" {
|
||||
m.Region = cfg.Region
|
||||
}
|
||||
|
||||
m.Region = *cfg.Region
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *testMockEC2) DescribeInstances(ctx context.Context, input *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
|
||||
func (m *testMockEC2) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) {
|
||||
if m.State != nil {
|
||||
m.State.DescribeInstancesCalled = true
|
||||
m.State.DescribeInstancesInputParams = input
|
||||
@@ -92,7 +84,7 @@ func (m *testMockEC2) DescribeInstances(ctx context.Context, input *ec2.Describe
|
||||
return m.DescribeInstancesOutput, nil
|
||||
}
|
||||
|
||||
type ec2FilterSorter []types.Filter
|
||||
type ec2FilterSorter []*ec2.Filter
|
||||
|
||||
func (s ec2FilterSorter) Len() int { return len(s) }
|
||||
func (s ec2FilterSorter) Less(i, j int) bool { return *s[i].Name < *s[j].Name }
|
||||
|
||||
@@ -42,7 +42,7 @@ Example policy:
|
||||
```
|
||||
|
||||
* `iam:GetUser`, `iam:CreateAccessKey`, and `iam:DeleteAccessKey`, configured to
|
||||
the IAM user to allow static credential rotation. Example policy:
|
||||
the IAM user to allow credential rotation. Example policy:
|
||||
|
||||
```
|
||||
{
|
||||
@@ -66,24 +66,18 @@ Example policy:
|
||||
The following `attributes` are valid on an AWS storage bucket resource:
|
||||
|
||||
- `disable_credential_rotation` (bool): If `true`, credential rotation will not
|
||||
be performed.
|
||||
be performed. See the [Credential Rotation](#credential-rotation) section
|
||||
above.
|
||||
- `region` (string): The region to configure the storage bucket for.
|
||||
- `endpoint_url` (string): The endpoint to configure the storage.
|
||||
- `role_arn` (string): The role arn configured for the assume role provider.
|
||||
- `role_external_id` (string): The external id configured for the assume role provider.
|
||||
- `role_session_name` (string): The session name configured for the assume role provider.
|
||||
- `role_tags` (object): The key-value pair tags configured for the assume role provider.
|
||||
- `endpoint-url` (string): The endpoint to configure the storage.
|
||||
|
||||
An example of how to utilize the [endpoint attribute](https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-s3.html)
|
||||
See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
|
||||
|
||||
### Secrets
|
||||
|
||||
The following `secrets` are optional on an AWS storage bucket resource:
|
||||
The following `secrets` are valid on an AWS storage bucket resource:
|
||||
|
||||
- `access_key_id` (string): The access key ID for the IAM user to use with this
|
||||
storage bucket.
|
||||
- `secret_access_key` (string): The secret access key for the IAM user to use
|
||||
with this storage bucket.
|
||||
|
||||
See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
|
||||
with this storage bucket.
|
||||
@@ -1,6 +1,3 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
@@ -19,9 +16,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 +34,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
|
||||
@@ -53,14 +41,6 @@ func getStorageAttributes(in *structpb.Struct) (*StorageAttributes, error) {
|
||||
continue
|
||||
case cred.ConstDisableCredentialRotation:
|
||||
continue
|
||||
case cred.ConstRoleArn:
|
||||
continue
|
||||
case cred.ConstRoleExternalId:
|
||||
continue
|
||||
case cred.ConstRoleSessionName:
|
||||
continue
|
||||
case cred.ConstRoleTags:
|
||||
continue
|
||||
default:
|
||||
badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field"
|
||||
}
|
||||
@@ -73,6 +53,5 @@ func getStorageAttributes(in *structpb.Struct) (*StorageAttributes, error) {
|
||||
return &StorageAttributes{
|
||||
CredentialAttributes: credAttributes,
|
||||
EndpointUrl: endpointUrl,
|
||||
DualStack: dualStack,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,14 +1,9 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
|
||||
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"
|
||||
@@ -18,51 +13,29 @@ import (
|
||||
func TestGetStorageAttributes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in *structpb.Struct
|
||||
in map[string]any
|
||||
expected *StorageAttributes
|
||||
expectedErrContains string
|
||||
expectedDetails *pb.StorageBucketCredentialState
|
||||
}{
|
||||
{
|
||||
name: "missing region",
|
||||
in: &structpb.Struct{
|
||||
Fields: make(map[string]*structpb.Value),
|
||||
},
|
||||
name: "missing region",
|
||||
in: map[string]any{},
|
||||
expectedErrContains: "missing required value \"region\"",
|
||||
},
|
||||
{
|
||||
name: "unknown fields",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
"foo": structpb.NewBoolValue(true),
|
||||
"bar": structpb.NewBoolValue(true),
|
||||
},
|
||||
in: map[string]any{
|
||||
"region": "us-west-2",
|
||||
"foo": true,
|
||||
"bar": true,
|
||||
},
|
||||
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{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
"endpoint_url": structpb.NewStringValue("0.0.0.0"),
|
||||
},
|
||||
in: map[string]any{
|
||||
cred.ConstRegion: "us-west-2",
|
||||
ConstAwsEndpointUrl: "0.0.0.0",
|
||||
},
|
||||
expected: &StorageAttributes{
|
||||
CredentialAttributes: &cred.CredentialAttributes{
|
||||
@@ -73,10 +46,8 @@ func TestGetStorageAttributes(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "good w/o endpoint",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
},
|
||||
in: map[string]any{
|
||||
cred.ConstRegion: "us-west-2",
|
||||
},
|
||||
expected: &StorageAttributes{
|
||||
CredentialAttributes: &cred.CredentialAttributes{
|
||||
@@ -84,51 +55,21 @@ func TestGetStorageAttributes(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "credential attributes",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
"disable_credential_rotation": structpb.NewBoolValue(true),
|
||||
"role_arn": structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
|
||||
"role_external_id": structpb.NewStringValue("1234567890"),
|
||||
"role_session_name": structpb.NewStringValue("test-session"),
|
||||
"role_tags": structpb.NewStructValue(&structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStringValue("bar"),
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
expected: &StorageAttributes{
|
||||
CredentialAttributes: &cred.CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
DisableCredentialRotation: true,
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
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)
|
||||
input, err := structpb.NewStruct(tc.in)
|
||||
require.NoError(err)
|
||||
|
||||
actual, err := getStorageAttributes(input)
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
|
||||
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 = 1 << 20 // 1 MiB
|
||||
defaultStreamChunkSize = 65536 // 64 KiB
|
||||
)
|
||||
@@ -1,75 +0,0 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// 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
|
||||
case pb.ListObjectsRequest:
|
||||
state.State.Read = permission
|
||||
case *pb.ListObjectsRequest:
|
||||
state.State.Read = 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()
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// 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")
|
||||
})
|
||||
}
|
||||
}
|
||||
+241
-880
File diff suppressed because it is too large.
Load diff
+402
-3199
File diff suppressed because it is too large.
Load diff
+62
-115
@@ -1,21 +1,17 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
|
||||
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/aws/retry"
|
||||
awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
smithyEndpoints "github.com/aws/smithy-go/endpoints"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
@@ -26,22 +22,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)
|
||||
CreateMultipartUpload(ctx context.Context, params *s3.CreateMultipartUploadInput, optFns ...func(*s3.Options)) (*s3.CreateMultipartUploadOutput, error)
|
||||
UploadPart(ctx context.Context, params *s3.UploadPartInput, optFns ...func(*s3.Options)) (*s3.UploadPartOutput, error)
|
||||
CompleteMultipartUpload(ctx context.Context, params *s3.CompleteMultipartUploadInput, optFns ...func(*s3.Options)) (*s3.CompleteMultipartUploadOutput, error)
|
||||
AbortMultipartUpload(ctx context.Context, params *s3.AbortMultipartUploadInput, optFns ...func(*s3.Options)) (*s3.AbortMultipartUploadOutput, 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 {
|
||||
@@ -88,6 +79,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)
|
||||
@@ -95,43 +87,62 @@ 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)
|
||||
}
|
||||
awsCfg, err := s.GenerateCredentialChain(ctx)
|
||||
|
||||
awsConfigOpts := []func(*config.LoadOptions) error{}
|
||||
sess, err := s.GetSession()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting AWS configuration when fetching S3 client: %w", err)
|
||||
return nil, fmt.Errorf("error getting AWS session when fetching S3 client: %w", err)
|
||||
}
|
||||
if awsCfg == nil {
|
||||
return nil, fmt.Errorf("nil aws configuration")
|
||||
creds, err := sess.Config.Credentials.Get()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting AWS session when fetching S3 client: %w", err)
|
||||
}
|
||||
var s3Opts []func(*s3.Options)
|
||||
if opts.withEndpoint != "" || opts.withDualStack {
|
||||
s3Opts = append(s3Opts, s3.WithEndpointResolverV2(&endpointResolver{
|
||||
endpoint: opts.withEndpoint,
|
||||
dualStack: opts.withDualStack,
|
||||
}))
|
||||
customCredentials := config.WithCredentialsProvider(
|
||||
credentials.NewStaticCredentialsProvider(creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken),
|
||||
)
|
||||
awsConfigOpts = append(awsConfigOpts, customCredentials)
|
||||
if sess.Config.Region != nil {
|
||||
awsConfigOpts = append(awsConfigOpts, config.WithRegion(*sess.Config.Region))
|
||||
}
|
||||
if s.testS3APIFunc != nil {
|
||||
return s.testS3APIFunc(*awsCfg)
|
||||
if opts.withRegion != "" {
|
||||
awsConfigOpts = append(awsConfigOpts, config.WithRegion(opts.withRegion))
|
||||
}
|
||||
if opts.withEndpoint != "" {
|
||||
customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
|
||||
if service == s3.ServiceID {
|
||||
if opts.withRegion != "" {
|
||||
region = opts.withRegion
|
||||
}
|
||||
return aws.Endpoint{
|
||||
PartitionID: "aws",
|
||||
URL: opts.withEndpoint,
|
||||
SigningRegion: region,
|
||||
}, nil
|
||||
}
|
||||
// returning EndpointNotFoundError will allow the service to fallback to it's default resolution
|
||||
return aws.Endpoint{}, &aws.EndpointNotFoundError{}
|
||||
})
|
||||
awsConfigOpts = append(awsConfigOpts, config.WithEndpointResolverWithOptions(customResolver))
|
||||
}
|
||||
cfg, err := config.LoadDefaultConfig(ctx, awsConfigOpts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error loading aws configuration when fetching S3 client: %w", err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if s.testS3APIFunc != nil {
|
||||
return s.testS3APIFunc(cfg)
|
||||
}
|
||||
err = newTransport(awsCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to configure new http transport: %v", err)
|
||||
}
|
||||
c := s3.NewFromConfig(*awsCfg, s3Opts...)
|
||||
return &s3Client{Client: c, creds: creds}, nil
|
||||
|
||||
cfg.HTTPClient = customClient
|
||||
|
||||
return s3.NewFromConfig(cfg), nil
|
||||
}
|
||||
|
||||
// getOpts iterates the inbound s3Options and returns a struct
|
||||
@@ -153,19 +164,18 @@ type s3Option func(*s3Options) error
|
||||
|
||||
// options = how options are represented
|
||||
type s3Options struct {
|
||||
withEndpoint string
|
||||
withDualStack bool
|
||||
withCacheRefresh bool
|
||||
withRegion string
|
||||
withEndpoint string
|
||||
}
|
||||
|
||||
func getDefaultS3Options() s3Options {
|
||||
return s3Options{}
|
||||
}
|
||||
|
||||
// WithCacheRefresh controls if the cache should be forced to refresh
|
||||
func WithCacheRefresh(refresh bool) s3Option {
|
||||
// WithRegion contains the region to use
|
||||
func WithRegion(with string) s3Option {
|
||||
return func(o *s3Options) error {
|
||||
o.withCacheRefresh = refresh
|
||||
o.withRegion = with
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -177,66 +187,3 @@ func WithEndpoint(with string) s3Option {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
return
|
||||
}
|
||||
uri, err = url.Parse(e.endpoint)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if uri == nil {
|
||||
return
|
||||
}
|
||||
resolver = smithyEndpoints.Endpoint{
|
||||
URI: *uri,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// newTransport returns an http Transport object. The http transport object utilizes the
|
||||
// default transport created by minio. The following transport fields are updated to provide
|
||||
// more reliable connectivity when handling larger objects:
|
||||
// MaxResponseHeaderBytes is set to 1MiB to prevent header reads from stalling.
|
||||
// WriteBufferSize is set to 1MiB, matching the default buffer size used in Boundary storage.
|
||||
// ReadBufferSize is set to 1MiB, matching the default buffer size used in Boundary storage.
|
||||
// ForceAttemptHTTP2 is set to true, supporting more resilient multiplexing and avoids HOL blocking on retries.
|
||||
// IdleConnTimeout is set to 2 minutes.
|
||||
func newTransport(cfg *aws.Config) error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("missing aws configuration")
|
||||
}
|
||||
cfg.HTTPClient = awshttp.NewBuildableClient().WithTransportOptions(func(transport *http.Transport) {
|
||||
transport.MaxResponseHeaderBytes = 1 << 20 // 1 MiB prevents header read from stalling
|
||||
transport.WriteBufferSize = 1 << 20 // 1 MiB write buffer (default is 4 KiB)
|
||||
transport.ReadBufferSize = 1 << 20 // 1 MiB read buffer (default is 4 KiB)
|
||||
transport.ForceAttemptHTTP2 = true // enables HTTP/2 where supported more resilient multiplexing, avoids HOL blocking on retries
|
||||
transport.IdleConnTimeout = 2 * time.Minute // double the idle connection timeout, default is 1 minute
|
||||
})
|
||||
cfg.Retryer = func() aws.Retryer {
|
||||
return retry.NewStandard(func(o *retry.StandardOptions) {
|
||||
o.MaxAttempts = 20
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,76 +1,40 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAwsStoragePersistedStateS3Client(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
creds *awsutil.CredentialsConfig
|
||||
opts []awsStoragePersistedStateOption
|
||||
awsOpts []awsutil.Option
|
||||
s3Opts []s3Option
|
||||
expectedRegion string
|
||||
expectedEndpoint string
|
||||
}{
|
||||
{
|
||||
name: "static credentials",
|
||||
creds: func() *awsutil.CredentialsConfig {
|
||||
creds, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithRegion("us-west-2"),
|
||||
awsutil.WithAccessKey("foobar"),
|
||||
awsutil.WithSecretKey("barfoo"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
return creds
|
||||
}(),
|
||||
awsOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
awsutil.NewMockIAM(),
|
||||
),
|
||||
},
|
||||
opts: []awsStoragePersistedStateOption{
|
||||
withTestS3APIFunc(newTestMockS3(nil)),
|
||||
},
|
||||
s3Opts: []s3Option{
|
||||
WithEndpoint("0.0.0.0"),
|
||||
},
|
||||
expectedRegion: "us-west-2",
|
||||
expectedEndpoint: "0.0.0.0",
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
require := require.New(t)
|
||||
cred, err := credential.NewAwsCredentialPersistedState(
|
||||
credential.WithRegion("us-west-2"),
|
||||
credential.WithAccessKeyId("foobar"),
|
||||
credential.WithSecretAccessKey("barfoo"),
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
cred, err := credential.NewAwsCredentialPersistedState(
|
||||
credential.WithCredentialsConfig(tc.creds),
|
||||
credential.WithStateTestOpts(tc.awsOpts),
|
||||
)
|
||||
require.NoError(err)
|
||||
state, err := newAwsStoragePersistedState(
|
||||
withCredentials(cred),
|
||||
withTestS3APIFunc(newTestMockS3(nil)),
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
state, err := newAwsStoragePersistedState(
|
||||
append(tc.opts, withCredentials(cred))...,
|
||||
)
|
||||
require.NoError(err)
|
||||
clientRaw, err := state.S3Client(context.Background(), WithRegion("us-west-2"), WithEndpoint("0.0.0.0"))
|
||||
require.NoError(err)
|
||||
require.NotNil(clientRaw)
|
||||
client, ok := clientRaw.(*testMockS3)
|
||||
require.True(ok)
|
||||
require.NotNil(client)
|
||||
require.Equal("us-west-2", client.Region)
|
||||
|
||||
clientRaw, err := state.s3Client(context.Background(), tc.s3Opts...)
|
||||
require.NoError(err)
|
||||
require.NotNil(clientRaw)
|
||||
client, ok := clientRaw.(*testMockS3)
|
||||
require.True(ok)
|
||||
require.NotNil(client)
|
||||
require.Equal(tc.expectedRegion, client.Region)
|
||||
})
|
||||
}
|
||||
endpoint, err := client.Endpoint.ResolveEndpoint(s3.ServiceID, client.Region)
|
||||
require.NoError(err)
|
||||
require.Equal("0.0.0.0", endpoint.URL)
|
||||
require.Equal("us-west-2", endpoint.SigningRegion)
|
||||
require.Equal("aws", endpoint.PartitionID)
|
||||
}
|
||||
@@ -1,13 +1,8 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
@@ -15,12 +10,11 @@ import (
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/aws/smithy-go"
|
||||
"github.com/aws/aws-sdk-go/service/sts"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
@@ -31,8 +25,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,33 +49,8 @@ type testMockS3State struct {
|
||||
PutObjectInputParams *s3.PutObjectInput
|
||||
PutObjectBody []byte
|
||||
|
||||
CreateMultipartUploadCalled bool
|
||||
CreateMultipartUploadInputParams *s3.CreateMultipartUploadInput
|
||||
|
||||
UploadPartCalled bool
|
||||
UploadPartCallCount int
|
||||
UploadPartInputParams []*s3.UploadPartInput
|
||||
// UploadPartBody is the concatenation, in part-number order, of every
|
||||
// part body received by UploadPart. It can be compared against the full
|
||||
// object that was expected to be uploaded.
|
||||
UploadPartBody []byte
|
||||
|
||||
CompleteMultipartUploadCalled bool
|
||||
CompleteMultipartUploadInputParams *s3.CompleteMultipartUploadInput
|
||||
|
||||
AbortMultipartUploadCalled bool
|
||||
AbortMultipartUploadInputParams *s3.AbortMultipartUploadInput
|
||||
|
||||
HeadObjectCalled bool
|
||||
HeadObjectInputParams *s3.HeadObjectInput
|
||||
|
||||
ListObjectsV2Called bool
|
||||
ListObjectsV2InputParams *s3.ListObjectsV2Input
|
||||
|
||||
DeleteObjectCalled bool
|
||||
DeleteObjectInputParams *s3.DeleteObjectInput
|
||||
DeleteObjectsCalled bool
|
||||
DeleteObjectsInputParams *s3.DeleteObjectsInput
|
||||
}
|
||||
|
||||
func (s *testMockS3State) Reset() {
|
||||
@@ -76,31 +59,16 @@ func (s *testMockS3State) Reset() {
|
||||
s.PutObjectCalled = false
|
||||
s.PutObjectInputParams = nil
|
||||
s.PutObjectBody = nil
|
||||
s.CreateMultipartUploadCalled = false
|
||||
s.CreateMultipartUploadInputParams = nil
|
||||
s.UploadPartCalled = false
|
||||
s.UploadPartCallCount = 0
|
||||
s.UploadPartInputParams = nil
|
||||
s.UploadPartBody = nil
|
||||
s.CompleteMultipartUploadCalled = false
|
||||
s.CompleteMultipartUploadInputParams = nil
|
||||
s.AbortMultipartUploadCalled = false
|
||||
s.AbortMultipartUploadInputParams = 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 {
|
||||
S3API
|
||||
|
||||
State *testMockS3State
|
||||
Region string
|
||||
State *testMockS3State
|
||||
Region string
|
||||
Endpoint aws.EndpointResolverWithOptions
|
||||
|
||||
// mocked responses for getObject
|
||||
GetObjectOutput *s3.GetObjectOutput
|
||||
@@ -110,34 +78,9 @@ type testMockS3 struct {
|
||||
PutObjectOutput *s3.PutObjectOutput
|
||||
PutObjectErr error
|
||||
|
||||
// mocked responses for the multipart upload calls
|
||||
CreateMultipartUploadOutput *s3.CreateMultipartUploadOutput
|
||||
CreateMultipartUploadErr error
|
||||
UploadPartOutput *s3.UploadPartOutput
|
||||
UploadPartErr error
|
||||
// UploadPartErrOnPart, when greater than zero, causes UploadPart to return
|
||||
// UploadPartErr (or a generic error) only for the matching part number.
|
||||
UploadPartErrOnPart int32
|
||||
CompleteMultipartUploadOutput *s3.CompleteMultipartUploadOutput
|
||||
CompleteMultipartUploadErr error
|
||||
AbortMultipartUploadOutput *s3.AbortMultipartUploadOutput
|
||||
AbortMultipartUploadErr error
|
||||
|
||||
// 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
|
||||
@@ -156,63 +99,6 @@ func testMockS3WithPutObjectError(e error) testMockS3Option {
|
||||
}
|
||||
}
|
||||
|
||||
func testMockS3WithCreateMultipartUploadOutput(o *s3.CreateMultipartUploadOutput) testMockS3Option {
|
||||
return func(m *testMockS3) error {
|
||||
m.CreateMultipartUploadOutput = o
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testMockS3WithCreateMultipartUploadError(e error) testMockS3Option {
|
||||
return func(m *testMockS3) error {
|
||||
m.CreateMultipartUploadErr = e
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testMockS3WithUploadPartOutput(o *s3.UploadPartOutput) testMockS3Option {
|
||||
return func(m *testMockS3) error {
|
||||
m.UploadPartOutput = o
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testMockS3WithUploadPartError(e error) testMockS3Option {
|
||||
return func(m *testMockS3) error {
|
||||
m.UploadPartErr = e
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testMockS3WithUploadPartErrorOnPart(partNumber int32, e error) testMockS3Option {
|
||||
return func(m *testMockS3) error {
|
||||
m.UploadPartErr = e
|
||||
m.UploadPartErrOnPart = partNumber
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testMockS3WithCompleteMultipartUploadOutput(o *s3.CompleteMultipartUploadOutput) testMockS3Option {
|
||||
return func(m *testMockS3) error {
|
||||
m.CompleteMultipartUploadOutput = o
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testMockS3WithCompleteMultipartUploadError(e error) testMockS3Option {
|
||||
return func(m *testMockS3) error {
|
||||
m.CompleteMultipartUploadErr = e
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testMockS3WithAbortMultipartUploadError(e error) testMockS3Option {
|
||||
return func(m *testMockS3) error {
|
||||
m.AbortMultipartUploadErr = e
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testMockS3WithGetObjectOutput(o *s3.GetObjectOutput) testMockS3Option {
|
||||
return func(m *testMockS3) error {
|
||||
m.GetObjectOutput = o
|
||||
@@ -241,55 +127,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{
|
||||
@@ -307,6 +144,9 @@ func newTestMockS3(state *testMockS3State, opts ...testMockS3Option) s3APIFunc {
|
||||
if cfg.Region != "" {
|
||||
m.Region = cfg.Region
|
||||
}
|
||||
if cfg.EndpointResolverWithOptions != nil {
|
||||
m.Endpoint = cfg.EndpointResolverWithOptions
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
@@ -354,136 +194,8 @@ func (m *testMockS3) PutObject(ctx context.Context, params *s3.PutObjectInput, o
|
||||
return m.PutObjectOutput, nil
|
||||
}
|
||||
|
||||
func (m *testMockS3) CreateMultipartUpload(ctx context.Context, params *s3.CreateMultipartUploadInput, optFns ...func(*s3.Options)) (*s3.CreateMultipartUploadOutput, error) {
|
||||
if m.State != nil {
|
||||
m.State.CreateMultipartUploadCalled = true
|
||||
m.State.CreateMultipartUploadInputParams = params
|
||||
}
|
||||
|
||||
if m.CreateMultipartUploadErr != nil {
|
||||
return nil, m.CreateMultipartUploadErr
|
||||
}
|
||||
|
||||
if m.CreateMultipartUploadOutput != nil {
|
||||
return m.CreateMultipartUploadOutput, nil
|
||||
}
|
||||
return &s3.CreateMultipartUploadOutput{
|
||||
UploadId: aws.String("test-upload-id"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *testMockS3) UploadPart(ctx context.Context, params *s3.UploadPartInput, optFns ...func(*s3.Options)) (*s3.UploadPartOutput, error) {
|
||||
if m.State != nil {
|
||||
m.State.UploadPartCalled = true
|
||||
m.State.UploadPartCallCount++
|
||||
m.State.UploadPartInputParams = append(m.State.UploadPartInputParams, params)
|
||||
}
|
||||
|
||||
// always drain the body so the part data can be asserted on and so the
|
||||
// reader is not left dangling.
|
||||
var data []byte
|
||||
if params.Body != nil {
|
||||
var err error
|
||||
data, err = io.ReadAll(params.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if m.State != nil {
|
||||
m.State.UploadPartBody = append(m.State.UploadPartBody, data...)
|
||||
}
|
||||
}
|
||||
|
||||
if m.UploadPartErr != nil {
|
||||
if m.UploadPartErrOnPart == 0 || (params.PartNumber != nil && *params.PartNumber == m.UploadPartErrOnPart) {
|
||||
return nil, m.UploadPartErr
|
||||
}
|
||||
}
|
||||
|
||||
if m.UploadPartOutput != nil {
|
||||
return m.UploadPartOutput, nil
|
||||
}
|
||||
// echo the supplied part checksum so CompleteMultipartUpload can be built
|
||||
// the way the real service would allow.
|
||||
sum := sha256.Sum256(data)
|
||||
return &s3.UploadPartOutput{
|
||||
ETag: aws.String(fmt.Sprintf("etag-%d", aws.ToInt32(params.PartNumber))),
|
||||
ChecksumSHA256: aws.String(base64.StdEncoding.EncodeToString(sum[:])),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *testMockS3) CompleteMultipartUpload(ctx context.Context, params *s3.CompleteMultipartUploadInput, optFns ...func(*s3.Options)) (*s3.CompleteMultipartUploadOutput, error) {
|
||||
if m.State != nil {
|
||||
m.State.CompleteMultipartUploadCalled = true
|
||||
m.State.CompleteMultipartUploadInputParams = params
|
||||
}
|
||||
|
||||
if m.CompleteMultipartUploadErr != nil {
|
||||
return nil, m.CompleteMultipartUploadErr
|
||||
}
|
||||
|
||||
if m.CompleteMultipartUploadOutput != nil {
|
||||
return m.CompleteMultipartUploadOutput, nil
|
||||
}
|
||||
return &s3.CompleteMultipartUploadOutput{}, nil
|
||||
}
|
||||
|
||||
func (m *testMockS3) AbortMultipartUpload(ctx context.Context, params *s3.AbortMultipartUploadInput, optFns ...func(*s3.Options)) (*s3.AbortMultipartUploadOutput, error) {
|
||||
if m.State != nil {
|
||||
m.State.AbortMultipartUploadCalled = true
|
||||
m.State.AbortMultipartUploadInputParams = params
|
||||
}
|
||||
|
||||
if m.AbortMultipartUploadErr != nil {
|
||||
return nil, m.AbortMultipartUploadErr
|
||||
}
|
||||
|
||||
if m.AbortMultipartUploadOutput != nil {
|
||||
return m.AbortMultipartUploadOutput, nil
|
||||
}
|
||||
return &s3.AbortMultipartUploadOutput{}, 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) {
|
||||
@@ -586,16 +298,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,
|
||||
},
|
||||
}
|
||||
}
|
||||
+201
-105
@@ -1,4 +1,4 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package testing
|
||||
@@ -11,18 +11,26 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/values"
|
||||
"github.com/hashicorp/boundary-plugin-aws/plugin/service/host"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
const expectedEc2InstanceCount = 5
|
||||
const (
|
||||
expectedIamUserCount = 6
|
||||
expectedEc2InstanceCount = 5
|
||||
)
|
||||
|
||||
func TestHostPlugin(t *testing.T) {
|
||||
var expectedTags = []string{"foo", "bar", "baz"}
|
||||
|
||||
func TestPlugin(t *testing.T) {
|
||||
region := os.Getenv("AWS_REGION")
|
||||
if region == "" {
|
||||
t.Skip("set AWS_REGION to use this test")
|
||||
@@ -78,14 +86,6 @@ func TestHostPlugin(t *testing.T) {
|
||||
require.NoError(err)
|
||||
require.Len(ec2InstanceTags, expectedEc2InstanceCount)
|
||||
|
||||
rawExpectedTags, err := tf.GetOutputSlice("instance_tag_keys")
|
||||
require.NoError(err)
|
||||
require.Len(rawExpectedTags, 3)
|
||||
expectedTags := make([]string, 0, len(rawExpectedTags))
|
||||
for _, rawExpectedTag := range rawExpectedTags {
|
||||
expectedTags = append(expectedTags, rawExpectedTag.(string))
|
||||
}
|
||||
|
||||
// Start the workflow now. Set up the host catalog. 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
|
||||
@@ -149,13 +149,13 @@ func TestHostPlugin(t *testing.T) {
|
||||
}
|
||||
|
||||
cases := [][]string{
|
||||
{expectedTags[0]},
|
||||
{expectedTags[1]},
|
||||
{expectedTags[2]},
|
||||
{expectedTags[0], expectedTags[1]},
|
||||
{expectedTags[0], expectedTags[2]},
|
||||
{expectedTags[1], expectedTags[2]},
|
||||
{expectedTags[0], expectedTags[1], expectedTags[2]},
|
||||
{"foo"},
|
||||
{"bar"},
|
||||
{"baz"},
|
||||
{"foo", "bar"},
|
||||
{"foo", "baz"},
|
||||
{"bar", "baz"},
|
||||
{"foo", "bar", "baz"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@@ -165,9 +165,6 @@ func TestHostPlugin(t *testing.T) {
|
||||
testPluginListHosts(ctx, t, p, region, keyid, secret, tc, expectedTagInstancesMap)
|
||||
// TODO: add OnDeleteSet if it needs to be implemented
|
||||
}
|
||||
|
||||
// finally lets test instance_addresses_only catalog attribute correctly syncs only instance addresses
|
||||
testInstanceAddressesOnlyListHosts(ctx, t, p, tf, region, keyid, secret)
|
||||
}
|
||||
|
||||
func testPluginOnCreateCatalog(ctx context.Context, t *testing.T, p *host.HostPlugin, region, accessKeyId, secretAccessKey string, rotate bool) (string, string) {
|
||||
@@ -198,7 +195,36 @@ func testPluginOnCreateCatalog(ctx context.Context, t *testing.T, p *host.HostPl
|
||||
require.NotNil(response)
|
||||
persisted := response.GetPersisted()
|
||||
require.NotNil(persisted)
|
||||
return validatePersistedSecrets(t, persisted.GetSecrets(), accessKeyId, secretAccessKey, rotate)
|
||||
persistedSecrets := persisted.GetSecrets()
|
||||
require.NotNil(persistedSecrets)
|
||||
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedAccessKeyId)
|
||||
if rotate {
|
||||
require.NotEqual(accessKeyId, persistedAccessKeyId)
|
||||
} else {
|
||||
require.Equal(accessKeyId, persistedAccessKeyId)
|
||||
}
|
||||
|
||||
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedSecretAccessKey)
|
||||
if rotate {
|
||||
require.NotEqual(secretAccessKey, persistedSecretAccessKey)
|
||||
} else {
|
||||
require.Equal(secretAccessKey, persistedSecretAccessKey)
|
||||
}
|
||||
|
||||
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
|
||||
require.NoError(err)
|
||||
if rotate {
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
requireCredentialsInvalid(t, accessKeyId, secretAccessKey)
|
||||
} else {
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
}
|
||||
|
||||
return persistedAccessKeyId, persistedSecretAccessKey
|
||||
}
|
||||
|
||||
func testPluginOnUpdateCatalog(
|
||||
@@ -267,7 +293,116 @@ func testPluginOnUpdateCatalog(
|
||||
require.NotNil(response)
|
||||
persisted := response.GetPersisted()
|
||||
require.NotNil(persisted)
|
||||
return validateUpdateSecrets(t, persisted.GetSecrets(), currentCredsLastRotatedTime, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey, rotated, rotate)
|
||||
persistedSecrets := persisted.GetSecrets()
|
||||
require.NotNil(persistedSecrets)
|
||||
|
||||
// Complex checks based on the scenarios.
|
||||
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedAccessKeyId)
|
||||
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedSecretAccessKey)
|
||||
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
|
||||
require.NoError(err)
|
||||
|
||||
// Our test scenarios are complex due the multi-dimensional nature
|
||||
// of criteria, so we lay them out in a switch below.
|
||||
switch {
|
||||
case newAccessKeyId != "" && rotated && rotate:
|
||||
// The new access key ID was provided, we had previously rotated
|
||||
// the credentials before, and the new credential set is to be
|
||||
// rotated as well. In this case, the old credentials should have
|
||||
// been deleted, and the new credentials should have been rotated,
|
||||
// hence, should not match the new credentials initially
|
||||
// provided. Rotation time should be non-zero and updated.
|
||||
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
|
||||
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
|
||||
|
||||
case newAccessKeyId != "" && rotated && !rotate:
|
||||
// The new access key ID was provided, we had previously rotated
|
||||
// the credentials before, and the new credential is *not*
|
||||
// rotated. In this case, the old credentials should have
|
||||
// been deleted, But the new credentials should have not been
|
||||
// rotated, and hence should be the same. Rotation time should be
|
||||
// zero.
|
||||
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, newAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
|
||||
case newAccessKeyId != "" && !rotated && rotate:
|
||||
// The new access key ID was provided, we *have not* previously
|
||||
// rotated the credentials, and the new credential set is to be
|
||||
// rotated. In this case, the old credentials should have been
|
||||
// left alone, and the new credentials should have been rotated,
|
||||
// hence, should not match the new credentials initially
|
||||
// provided. Rotation time should be non-zero, but updated.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
|
||||
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
|
||||
|
||||
case newAccessKeyId != "" && !rotated && !rotate:
|
||||
// The new access key ID was provided, but we have not rotated
|
||||
// the credentials previously and we still don't plan on rotating
|
||||
// them. In this case, the old credentials should still be valid,
|
||||
// and the persisted ones should match the new ones provided.
|
||||
// Rotation time should be zero.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, newAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
|
||||
case newAccessKeyId == "" && rotated && rotate:
|
||||
// No new credentials have been provided, but we have previously
|
||||
// rotated and are still rotating credentials. This is a no-op.
|
||||
// Existing credentials should still be valid and match the ones
|
||||
// persisted to state. Rotation time should be identical since
|
||||
// no new rotation occurred.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, currentAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
require.True(currentCredsLastRotatedTime.Equal(persistedCredsLastRotatedTime))
|
||||
|
||||
case newAccessKeyId == "" && rotated && !rotate:
|
||||
// No new credentials have been provided, and we have previously
|
||||
// rotated the credentials. This is actually an error, but we
|
||||
// don't test it here; it's covered in unit testing (see
|
||||
// plugin_test.go).
|
||||
require.FailNow("testing rotated-to-not-rotated scenario not implemented by this helper")
|
||||
|
||||
case newAccessKeyId == "" && !rotated && rotate:
|
||||
// No new credentials have been provided, and while we did not
|
||||
// rotate before, we want to switch to rotation. In this case,
|
||||
// the existing persisted credentials should have been rotated,
|
||||
// with a new non-zero timestamp.
|
||||
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.NotEqual(persistedAccessKeyId, currentAccessKeyId)
|
||||
require.NotEqual(persistedSecretAccessKey, currentSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
|
||||
case newAccessKeyId == "" && !rotated && !rotate:
|
||||
// No new credentials have been provided and we have not, nor do
|
||||
// not, plan on rotating the credentials. This is a no-op.
|
||||
// Existing credentials should still be valid and match the ones
|
||||
// persisted to state. Rotation time should remain at zero.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, currentAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
|
||||
default:
|
||||
// Scenario was reached that was not covered by this function.
|
||||
require.FailNow("unknown test scenario")
|
||||
}
|
||||
|
||||
return persistedAccessKeyId, persistedSecretAccessKey
|
||||
}
|
||||
|
||||
func testPluginOnDeleteCatalog(ctx context.Context, t *testing.T, p *host.HostPlugin, region, accessKeyId, secretAccessKey string, rotated bool) {
|
||||
@@ -454,91 +589,52 @@ func testPluginListHosts(ctx context.Context, t *testing.T, p *host.HostPlugin,
|
||||
t.Logf("testing ListHosts: success (region=%s, tags=%v, expected/actual=(len=%d, ids=%s))", region, tags, len(actualInstances), actualInstances)
|
||||
}
|
||||
|
||||
func testInstanceAddressesOnlyListHosts(ctx context.Context, t *testing.T, p *host.HostPlugin, tf *TestTerraformer, region, accessKeyId, secretAccessKey string) {
|
||||
func requireCredentialsInvalid(t *testing.T, accessKeyId, secretAccessKey string) {
|
||||
t.Helper()
|
||||
t.Logf("testing testInstanceAddressesOnlyListHosts")
|
||||
require := require.New(t)
|
||||
|
||||
// Get the instance details from tf output
|
||||
instanceID, err := tf.GetOutputString("multi_address_instance_id")
|
||||
require.NoError(t, err)
|
||||
primaryAddresses, err := tf.GetOutputMap("multi_address_primary_addresses")
|
||||
require.NoError(t, err)
|
||||
secondaryAddresses, err := tf.GetOutputMap("multi_address_secondary_addresses")
|
||||
require.NoError(t, err)
|
||||
c, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithAccessKey(accessKeyId),
|
||||
awsutil.WithSecretKey(secretAccessKey),
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
var primary, allAddresses []string
|
||||
for _, addr := range primaryAddresses {
|
||||
primary = append(primary, addr.(string))
|
||||
}
|
||||
allAddresses = append(allAddresses, primary...)
|
||||
for _, addr := range secondaryAddresses {
|
||||
allAddresses = append(allAddresses, addr.(string))
|
||||
// We need to wait for invalidation as while awsutil waits for
|
||||
// credential creation, deletion of the old credentials returns
|
||||
// immediately.
|
||||
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
defer cancel()
|
||||
waitErr:
|
||||
for {
|
||||
_, err = c.GetCallerIdentity()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second):
|
||||
// pass
|
||||
|
||||
case <-timeoutCtx.Done():
|
||||
break waitErr
|
||||
}
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
instanceAddressesOnly bool
|
||||
expectedIPs []string
|
||||
}{
|
||||
{
|
||||
name: "all addresses",
|
||||
expectedIPs: allAddresses,
|
||||
},
|
||||
{
|
||||
name: "instance addresses only",
|
||||
instanceAddressesOnly: true,
|
||||
expectedIPs: primary,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
catalogAttrMap := map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: true,
|
||||
}
|
||||
if tc.instanceAddressesOnly {
|
||||
catalogAttrMap[host.ConstInstanceAddressesOnly] = true
|
||||
}
|
||||
|
||||
catalogAttrs, err := structpb.NewStruct(catalogAttrMap)
|
||||
require.NoError(err)
|
||||
setAttrs, err := structpb.NewStruct(map[string]any{
|
||||
host.ConstDescribeInstancesFilters: []any{fmt.Sprintf("instance-id=%s", instanceID)},
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: accessKeyId,
|
||||
credential.ConstSecretAccessKey: secretAccessKey,
|
||||
credential.ConstCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
|
||||
})
|
||||
require.NoError(err)
|
||||
request := &pb.ListHostsRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
Attributes: catalogAttrs,
|
||||
},
|
||||
},
|
||||
Sets: []*hostsets.HostSet{{
|
||||
Id: "hostset-0",
|
||||
Attrs: &hostsets.HostSet_Attributes{Attributes: setAttrs},
|
||||
}},
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: reqPersistedSecrets,
|
||||
},
|
||||
}
|
||||
|
||||
response, err := p.ListHosts(ctx, request)
|
||||
require.NoError(err)
|
||||
require.Len(response.GetHosts(), 1)
|
||||
hostResp := response.GetHosts()[0]
|
||||
|
||||
require.Equal(instanceID, hostResp.ExternalId)
|
||||
require.ElementsMatch(hostResp.IpAddresses, tc.expectedIPs)
|
||||
})
|
||||
}
|
||||
require.NotNil(err)
|
||||
awsErr, ok := err.(awserr.Error)
|
||||
require.True(ok)
|
||||
require.Equal("InvalidClientTokenId", awsErr.Code())
|
||||
}
|
||||
|
||||
func requireCredentialsValid(t *testing.T, accessKeyId, secretAccessKey string) {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
|
||||
c, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithAccessKey(accessKeyId),
|
||||
awsutil.WithSecretKey(secretAccessKey),
|
||||
)
|
||||
require.NoError(err)
|
||||
_, err = c.GetCallerIdentity()
|
||||
require.NoError(err)
|
||||
}
|
||||
@@ -1,723 +0,0 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package testing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/boundary-plugin-aws/plugin/service/storage"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/stretchr/testify/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 == "" {
|
||||
t.Skip("set AWS_REGION to use this test")
|
||||
}
|
||||
if envAccessKeyId := os.Getenv("AWS_ACCESS_KEY_ID"); envAccessKeyId == "" {
|
||||
t.Skip("set AWS_ACCESS_KEY_ID to use this test")
|
||||
}
|
||||
if envSecretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY"); envSecretAccessKey == "" {
|
||||
t.Skip("set AWS_SECRET_ACCESS_KEY to use this test")
|
||||
}
|
||||
|
||||
require := require.New(t)
|
||||
tf, err := NewTestTerraformer("testdata/storage")
|
||||
require.NoError(err)
|
||||
require.NotNil(tf)
|
||||
|
||||
t.Log("===== deploying test Terraform workspace =====")
|
||||
err = tf.Deploy()
|
||||
require.NoError(err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
t.Log("===== destroying test Terraform workspace =====")
|
||||
if err := tf.Destroy(); err != nil {
|
||||
t.Logf("WARNING: could not run Terraform destroy: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
bucketName, err := tf.GetOutputString("bucket_name")
|
||||
require.NoError(err)
|
||||
require.NotEmpty(bucketName)
|
||||
|
||||
iamAccessKeyIds, err := tf.GetOutputSlice("iam_access_key_ids")
|
||||
require.NoError(err)
|
||||
require.Len(iamAccessKeyIds, expectedIamUserCount)
|
||||
|
||||
iamSecretAccessKeys, err := tf.GetOutputSlice("iam_secret_access_keys")
|
||||
require.NoError(err)
|
||||
require.Len(iamSecretAccessKeys, expectedIamUserCount)
|
||||
|
||||
iamAccessKeyMissingGetObject, err := tf.GetOutputString("iam_access_key_missing_get_obj")
|
||||
require.NoError(err)
|
||||
|
||||
iamSecretAccessKeyMissingGetObject, err := tf.GetOutputString("iam_secret_access_key_missing_get_obj")
|
||||
require.NoError(err)
|
||||
|
||||
iamAccessKeyMissingPutObject, err := tf.GetOutputString("iam_access_key_missing_put_obj")
|
||||
require.NoError(err)
|
||||
|
||||
iamSecretAccessKeyMissingPutObject, err := tf.GetOutputString("iam_secret_access_key_missing_put_obj")
|
||||
require.NoError(err)
|
||||
|
||||
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)
|
||||
|
||||
s3PaginationTestObjectCount, err := tf.GetOutput("s3_pagination_test_object_count")
|
||||
require.NoError(err)
|
||||
objectCountFloat, ok := s3PaginationTestObjectCount.(float64)
|
||||
require.True(ok)
|
||||
objectCount := int(objectCountFloat)
|
||||
|
||||
// Start the workflow now. Set up the storage bucket. Note that this
|
||||
// will cause the state to go out of drift above in the sense that
|
||||
// the access key ID/secret access key will no longer be valid. We
|
||||
// will assert this through the returned state.
|
||||
p := new(storage.StoragePlugin)
|
||||
ctx := context.Background()
|
||||
|
||||
// ********************
|
||||
// * OnCreateStorageBucket
|
||||
// ********************
|
||||
//
|
||||
// Test non-rotation (using primary user).
|
||||
keyid, secret := testPluginOnCreateStorageBucket(ctx, t, p, bucketName, region, iamAccessKeyIds[0].(string), iamSecretAccessKeys[0].(string), false)
|
||||
// Test rotation next.
|
||||
keyid, secret = testPluginOnCreateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, true)
|
||||
|
||||
// ********************
|
||||
// * OnUpdateStorageBucket
|
||||
// ********************
|
||||
//
|
||||
// Test no-op non-rotation.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, "", "", false, false)
|
||||
// Switch to rotation.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, "", "", false, true)
|
||||
// Test no-op with rotation.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, "", "", true, true)
|
||||
// Switch credentials to next user. Don't rotate.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[1].(string), iamSecretAccessKeys[1].(string), true, false)
|
||||
// Switch credentials to next user. Add rotation.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[2].(string), iamSecretAccessKeys[2].(string), false, true)
|
||||
// Switch to next user, with rotation disabled.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[3].(string), iamSecretAccessKeys[3].(string), true, false)
|
||||
// Last case - switch to another user and keep rotation off.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[4].(string), iamSecretAccessKeys[4].(string), false, false)
|
||||
|
||||
// ********************
|
||||
// * OnDeleteStorageBucket
|
||||
// ********************
|
||||
//
|
||||
// Test non-rotated.
|
||||
testPluginOnDeleteStorageBucket(ctx, t, p, bucketName, region, keyid, secret, false)
|
||||
// Test as if we had rotated these credentials (note that this
|
||||
// makes this test set unusable).
|
||||
testPluginOnDeleteStorageBucket(ctx, t, p, bucketName, region, keyid, secret, true)
|
||||
|
||||
// ********************
|
||||
// * Object Methods: PutObject, GetObject, HeadObject, DeleteObjects
|
||||
// ********************
|
||||
//
|
||||
// Reassign the keyid and secret first.
|
||||
keyid, secret = iamAccessKeyIds[5].(string), iamSecretAccessKeys[5].(string)
|
||||
testPluginObjectMethods(ctx, t, p, bucketName, region, keyid, secret)
|
||||
|
||||
// ********************
|
||||
// * ListObjects
|
||||
// ********************
|
||||
testListObjects(ctx, t, p, objectCount, bucketName, region, keyid, secret)
|
||||
|
||||
// ********************
|
||||
// * 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)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
func testPluginOnCreateStorageBucket(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string, rotate bool) (string, string) {
|
||||
t.Helper()
|
||||
t.Logf("testing OnCreateStorageBucket (region=%s, rotate=%t)", region, rotate)
|
||||
require := require.New(t)
|
||||
|
||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: !rotate,
|
||||
})
|
||||
require.NoError(err)
|
||||
reqSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: accessKeyId,
|
||||
credential.ConstSecretAccessKey: secretAccessKey,
|
||||
})
|
||||
require.NoError(err)
|
||||
request := &pb.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Secrets: reqSecrets,
|
||||
Attributes: reqAttrs,
|
||||
},
|
||||
}
|
||||
response, err := p.OnCreateStorageBucket(ctx, request)
|
||||
require.NoError(err)
|
||||
require.NotNil(response)
|
||||
persisted := response.GetPersisted()
|
||||
require.NotNil(persisted)
|
||||
return validatePersistedSecrets(t, persisted.GetData(), accessKeyId, secretAccessKey, rotate)
|
||||
}
|
||||
|
||||
func testPluginOnUpdateStorageBucket(
|
||||
ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName,
|
||||
region, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey string,
|
||||
rotated, rotate bool,
|
||||
) (string, string) {
|
||||
t.Helper()
|
||||
t.Logf("testing OnUpdateStorageBucket (region=%s, newcreds=%t, rotated=%t, rotate=%t)", region, newAccessKeyId != "" && newSecretAccessKey != "", rotated, rotate)
|
||||
require := require.New(t)
|
||||
|
||||
// Take a timestamp of the current time to get a point in time to
|
||||
// reference, ensuring that we are updating credential rotation
|
||||
// timestamps.
|
||||
currentCredsLastRotatedTime := time.Now()
|
||||
|
||||
reqCurrentAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: !rotated,
|
||||
})
|
||||
require.NoError(err)
|
||||
reqNewAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: !rotate,
|
||||
})
|
||||
require.NoError(err)
|
||||
var reqSecrets *structpb.Struct
|
||||
if newAccessKeyId != "" && newSecretAccessKey != "" {
|
||||
reqSecrets, err = structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: newAccessKeyId,
|
||||
credential.ConstSecretAccessKey: newSecretAccessKey,
|
||||
})
|
||||
require.NoError(err)
|
||||
}
|
||||
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: currentAccessKeyId,
|
||||
credential.ConstSecretAccessKey: currentSecretAccessKey,
|
||||
credential.ConstCredsLastRotatedTime: func() string {
|
||||
if rotated {
|
||||
return currentCredsLastRotatedTime.Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
return (time.Time{}).Format(time.RFC3339Nano)
|
||||
}(),
|
||||
})
|
||||
require.NoError(err)
|
||||
require.NotNil(reqPersistedSecrets)
|
||||
request := &pb.OnUpdateStorageBucketRequest{
|
||||
CurrentBucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqCurrentAttrs,
|
||||
},
|
||||
NewBucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqNewAttrs,
|
||||
Secrets: reqSecrets,
|
||||
},
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: reqPersistedSecrets,
|
||||
},
|
||||
}
|
||||
response, err := p.OnUpdateStorageBucket(ctx, request)
|
||||
require.NoError(err)
|
||||
require.NotNil(response)
|
||||
persisted := response.GetPersisted()
|
||||
require.NotNil(persisted)
|
||||
return validateUpdateSecrets(t, persisted.GetData(), currentCredsLastRotatedTime, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey, rotated, rotate)
|
||||
}
|
||||
|
||||
func testPluginOnDeleteStorageBucket(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string, rotated bool) {
|
||||
t.Helper()
|
||||
t.Logf("testing OnDeleteStorageBucket (region=%s, rotated=%t)", region, rotated)
|
||||
require := require.New(t)
|
||||
|
||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: !rotated,
|
||||
})
|
||||
require.NoError(err)
|
||||
reqSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: accessKeyId,
|
||||
credential.ConstSecretAccessKey: secretAccessKey,
|
||||
})
|
||||
require.NoError(err)
|
||||
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: accessKeyId,
|
||||
credential.ConstSecretAccessKey: secretAccessKey,
|
||||
credential.ConstCredsLastRotatedTime: func() string {
|
||||
if rotated {
|
||||
return time.Now().Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
return (time.Time{}).Format(time.RFC3339Nano)
|
||||
}(),
|
||||
})
|
||||
require.NoError(err)
|
||||
request := &pb.OnDeleteStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqAttrs,
|
||||
Secrets: reqSecrets,
|
||||
},
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: reqPersistedSecrets,
|
||||
},
|
||||
}
|
||||
response, err := p.OnDeleteStorageBucket(ctx, request)
|
||||
require.NoError(err)
|
||||
require.NotNil(response)
|
||||
|
||||
// We want to test the validity of the credentials post-deletion.
|
||||
if rotated {
|
||||
// The credentials should no longer be valid.
|
||||
requireCredentialsInvalid(t, accessKeyId, secretAccessKey)
|
||||
} else {
|
||||
// The credentials should still be valid. Sleep 10s first just to
|
||||
// be sure, since we're not rotating.
|
||||
time.Sleep(time.Second * 10)
|
||||
requireCredentialsValid(t, accessKeyId, secretAccessKey)
|
||||
}
|
||||
}
|
||||
|
||||
func testPluginValidatePermissions(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string, expectedState *pb.StorageBucketCredentialState) {
|
||||
t.Helper()
|
||||
t.Logf("testing ValidatePermissions")
|
||||
require, assert := require.New(t), assert.New(t)
|
||||
|
||||
require.NotNil(expectedState)
|
||||
|
||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: true,
|
||||
})
|
||||
require.NoError(err)
|
||||
reqSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: accessKeyId,
|
||||
credential.ConstSecretAccessKey: secretAccessKey,
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
req := &pb.ValidatePermissionsRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqAttrs,
|
||||
Secrets: reqSecrets,
|
||||
},
|
||||
}
|
||||
|
||||
_, err = p.ValidatePermissions(ctx, req)
|
||||
require.Error(err)
|
||||
|
||||
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) {
|
||||
t.Helper()
|
||||
t.Logf("testing PutObject")
|
||||
require := require.New(t)
|
||||
|
||||
testDataPath, err := filepath.Abs("./testdata/storage/test_object_data")
|
||||
require.NoError(err)
|
||||
require.FileExists(testDataPath)
|
||||
|
||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: true,
|
||||
})
|
||||
require.NoError(err)
|
||||
reqSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: accessKeyId,
|
||||
credential.ConstSecretAccessKey: secretAccessKey,
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
objectKey := uuid.New().String()
|
||||
putObjReq := &pb.PutObjectRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqAttrs,
|
||||
Secrets: reqSecrets,
|
||||
},
|
||||
Key: objectKey,
|
||||
Path: testDataPath,
|
||||
}
|
||||
putObjResp, err := p.PutObject(ctx, putObjReq)
|
||||
require.NoError(err)
|
||||
require.NotNil(putObjResp)
|
||||
require.NotEmpty(putObjResp.GetChecksumSha_256())
|
||||
|
||||
t.Logf("testing HeadObject")
|
||||
headObjReq := &pb.HeadObjectRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqAttrs,
|
||||
Secrets: reqSecrets,
|
||||
},
|
||||
Key: objectKey,
|
||||
}
|
||||
headObjResp, err := p.HeadObject(ctx, headObjReq)
|
||||
require.NoError(err)
|
||||
require.NotNil(headObjResp)
|
||||
require.NotEmpty(headObjResp.GetContentLength())
|
||||
require.NotEmpty(headObjResp.GetLastModified())
|
||||
|
||||
// Need to create a GRPC server to test GetObject
|
||||
t.Logf("testing GetObject")
|
||||
lis, err := net.Listen("tcp", "[::1]:2030")
|
||||
require.NoError(err)
|
||||
grpcServer := grpc.NewServer()
|
||||
pb.RegisterStoragePluginServiceServer(grpcServer, &storage.StoragePlugin{})
|
||||
go func() {
|
||||
grpcServer.Serve(lis)
|
||||
}()
|
||||
|
||||
conn, err := grpc.Dial("[::1]:2030", grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
require.NoError(err)
|
||||
defer conn.Close()
|
||||
client := pb.NewStoragePluginServiceClient(conn)
|
||||
|
||||
stream, err := client.GetObject(ctx, &pb.GetObjectRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqAttrs,
|
||||
Secrets: reqSecrets,
|
||||
},
|
||||
Key: objectKey,
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
var actualData []byte
|
||||
expectedData, err := os.ReadFile(testDataPath)
|
||||
require.NoError(err)
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
require.NoError(stream.CloseSend())
|
||||
break
|
||||
} else if err != nil {
|
||||
require.NoError(err)
|
||||
}
|
||||
actualData = append(actualData, resp.GetFileChunk()...)
|
||||
}
|
||||
require.Equal(expectedData, actualData)
|
||||
require.NoError(conn.Close())
|
||||
grpcServer.Stop()
|
||||
}
|
||||
|
||||
// testListObjects verifies top-level and recursive listing by matching the exact keys and types of returned objects.
|
||||
func testListObjects(ctx context.Context, t *testing.T, p *storage.StoragePlugin, objectCount int, bucketName, region, accessKeyId, secretAccessKey string) {
|
||||
t.Helper()
|
||||
t.Logf("testing ListObjects (bucket=%s, region=%s, expectedObjects=%d, creds=%t)",
|
||||
bucketName,
|
||||
region,
|
||||
objectCount,
|
||||
accessKeyId != "" && secretAccessKey != "",
|
||||
)
|
||||
r := require.New(t)
|
||||
|
||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: true,
|
||||
})
|
||||
r.NoError(err)
|
||||
reqSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: accessKeyId,
|
||||
credential.ConstSecretAccessKey: secretAccessKey,
|
||||
})
|
||||
r.NoError(err)
|
||||
|
||||
bucket := &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqAttrs,
|
||||
Secrets: reqSecrets,
|
||||
}
|
||||
|
||||
// Test Scenario 1: Non-Paginated Listing
|
||||
t.Run("NonPaginatedListing", func(t *testing.T) {
|
||||
|
||||
// Subtest for the non-recursive (top-level) option
|
||||
t.Run("TopLevel", func(t *testing.T) {
|
||||
r := require.New(t)
|
||||
t.Logf("testing ListObjects for a small set with recursive=false")
|
||||
req := &pb.ListObjectsRequest{
|
||||
Bucket: bucket,
|
||||
KeyPrefix: "list-objects/",
|
||||
Recursive: false,
|
||||
}
|
||||
resp, err := p.ListObjects(ctx, req)
|
||||
r.NoError(err)
|
||||
r.NotNil(resp)
|
||||
|
||||
expectedObjects := []*pb.Object{
|
||||
{Key: "list-objects/file1.txt", IsDir: false},
|
||||
{Key: "list-objects/data.json", IsDir: false},
|
||||
{Key: "list-objects/nested-dir/", IsDir: true},
|
||||
{Key: "list-objects/empty-nested-dir/", IsDir: true},
|
||||
}
|
||||
r.ElementsMatch(resp.Objects, expectedObjects)
|
||||
})
|
||||
|
||||
// Subtest for the recursive option
|
||||
t.Run("Recursive", func(t *testing.T) {
|
||||
r := require.New(t)
|
||||
t.Logf("testing ListObjects for a small set with recursive=true")
|
||||
req := &pb.ListObjectsRequest{
|
||||
Bucket: bucket,
|
||||
KeyPrefix: "list-objects/",
|
||||
Recursive: true,
|
||||
}
|
||||
resp, err := p.ListObjects(ctx, req)
|
||||
r.NoError(err)
|
||||
r.NotNil(resp)
|
||||
|
||||
expectedObjects := []*pb.Object{
|
||||
{Key: "list-objects/file1.txt", IsDir: false},
|
||||
{Key: "list-objects/data.json", IsDir: false},
|
||||
{Key: "list-objects/nested-dir/file2.txt", IsDir: false},
|
||||
{Key: "list-objects/nested-dir/", IsDir: true},
|
||||
{Key: "list-objects/empty-nested-dir/", IsDir: true},
|
||||
}
|
||||
|
||||
r.ElementsMatch(resp.Objects, expectedObjects)
|
||||
})
|
||||
})
|
||||
|
||||
// Test Scenario 2: Paginated Top-Level Listing
|
||||
t.Run("TopLevelListing", func(t *testing.T) {
|
||||
// Subtest for the root prefix
|
||||
t.Run("AtRoot", func(t *testing.T) {
|
||||
r := require.New(t)
|
||||
t.Logf("testing ListObjects with recursive=false at root prefix")
|
||||
req := &pb.ListObjectsRequest{
|
||||
Bucket: bucket,
|
||||
KeyPrefix: "list-objects-paginated/",
|
||||
Recursive: false,
|
||||
}
|
||||
resp, err := p.ListObjects(ctx, req)
|
||||
r.NoError(err)
|
||||
r.NotNil(resp)
|
||||
|
||||
var expectedObjects []*pb.Object
|
||||
for i := 0; i < objectCount; i++ {
|
||||
key := fmt.Sprintf("list-objects-paginated/root-file-%d.txt", i)
|
||||
expectedObjects = append(expectedObjects, &pb.Object{Key: key, IsDir: false})
|
||||
}
|
||||
for i := 0; i < objectCount; i++ {
|
||||
keyEmpty := fmt.Sprintf("list-objects-paginated/empty-dir-%d/", i)
|
||||
expectedObjects = append(expectedObjects, &pb.Object{Key: keyEmpty, IsDir: true})
|
||||
keyNested := fmt.Sprintf("list-objects-paginated/nested-dir-%d/", i)
|
||||
expectedObjects = append(expectedObjects, &pb.Object{Key: keyNested, IsDir: true})
|
||||
}
|
||||
|
||||
r.ElementsMatch(resp.Objects, expectedObjects)
|
||||
})
|
||||
|
||||
// Subtest for a nested prefix
|
||||
t.Run("AtNestedPrefix", func(t *testing.T) {
|
||||
r := require.New(t)
|
||||
t.Logf("testing ListObjects with recursive=false at a nested prefix")
|
||||
req := &pb.ListObjectsRequest{
|
||||
Bucket: bucket,
|
||||
KeyPrefix: "list-objects-paginated/nested-dir-7/",
|
||||
Recursive: false,
|
||||
}
|
||||
resp, err := p.ListObjects(ctx, req)
|
||||
r.NoError(err)
|
||||
r.NotNil(resp)
|
||||
|
||||
expectedObjects := []*pb.Object{
|
||||
{Key: "list-objects-paginated/nested-dir-7/data-file-7.json", IsDir: false},
|
||||
}
|
||||
|
||||
r.ElementsMatch(resp.Objects, expectedObjects)
|
||||
})
|
||||
})
|
||||
|
||||
// Test Scenario 3: Paginated Recursive Listing
|
||||
t.Run("RecursiveListing", func(t *testing.T) {
|
||||
r := require.New(t)
|
||||
t.Logf("testing ListObjects with recursive=true")
|
||||
req := &pb.ListObjectsRequest{
|
||||
Bucket: bucket,
|
||||
KeyPrefix: "list-objects-paginated/",
|
||||
Recursive: true,
|
||||
}
|
||||
resp, err := p.ListObjects(ctx, req)
|
||||
r.NoError(err)
|
||||
r.NotNil(resp)
|
||||
|
||||
var expectedObjects []*pb.Object
|
||||
for i := 0; i < objectCount; i++ {
|
||||
keyFile := fmt.Sprintf("list-objects-paginated/root-file-%d.txt", i)
|
||||
expectedObjects = append(expectedObjects, &pb.Object{Key: keyFile, IsDir: false})
|
||||
keyNestedFile := fmt.Sprintf("list-objects-paginated/nested-dir-%d/data-file-%d.json", i, i)
|
||||
expectedObjects = append(expectedObjects, &pb.Object{Key: keyNestedFile, IsDir: false})
|
||||
}
|
||||
for i := 0; i < objectCount; i++ {
|
||||
keyNestedDir := fmt.Sprintf("list-objects-paginated/nested-dir-%d/", i)
|
||||
expectedObjects = append(expectedObjects, &pb.Object{Key: keyNestedDir, IsDir: true})
|
||||
keyEmptyDir := fmt.Sprintf("list-objects-paginated/empty-dir-%d/", i)
|
||||
expectedObjects = append(expectedObjects, &pb.Object{Key: keyEmptyDir, IsDir: true})
|
||||
keyNestedEmptyDir := fmt.Sprintf("list-objects-paginated/empty-dir-%d/dir-%d/", i, i)
|
||||
expectedObjects = append(expectedObjects, &pb.Object{Key: keyNestedEmptyDir, IsDir: true})
|
||||
}
|
||||
|
||||
r.ElementsMatch(resp.Objects, expectedObjects)
|
||||
})
|
||||
|
||||
// Test Scenario 4: Edge cases
|
||||
t.Run("EdgeCases", func(t *testing.T) {
|
||||
// Subtest for a prefix that doesn't exist
|
||||
t.Run("NonExistentPrefix", func(t *testing.T) {
|
||||
r := require.New(t)
|
||||
t.Logf("testing ListObjects for a non-existent prefix")
|
||||
req := &pb.ListObjectsRequest{
|
||||
Bucket: bucket,
|
||||
KeyPrefix: "this/prefix/does/not/exist/",
|
||||
Recursive: false,
|
||||
}
|
||||
resp, err := p.ListObjects(ctx, req)
|
||||
r.NoError(err)
|
||||
r.NotNil(resp)
|
||||
r.Empty(resp.Objects)
|
||||
})
|
||||
|
||||
// Subtest for listing inside a known empty directory
|
||||
t.Run("InsideEmptyDirectory", func(t *testing.T) {
|
||||
r := require.New(t)
|
||||
t.Logf("testing ListObjects inside an empty directory")
|
||||
req := &pb.ListObjectsRequest{
|
||||
Bucket: bucket,
|
||||
KeyPrefix: "list-objects-paginated/empty-dirs/dir-10/",
|
||||
Recursive: false,
|
||||
}
|
||||
resp, err := p.ListObjects(ctx, req)
|
||||
r.NoError(err)
|
||||
r.NotNil(resp)
|
||||
r.Empty(resp.Objects)
|
||||
})
|
||||
})
|
||||
}
|
||||
Vendored
+15
-22
@@ -1,20 +1,24 @@
|
||||
# Copyright IBM Corp. 2021, 2026
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
variable "iam_user_prefix" {
|
||||
default = "boundary-plugin-host-aws-test-iam-user"
|
||||
}
|
||||
|
||||
variable "iam_user_count" {
|
||||
default = 6
|
||||
}
|
||||
|
||||
resource "random_id" "aws_iam_user_name" {
|
||||
count = var.iam_user_count
|
||||
prefix = "demo-${local.hashicorp_email}-boundary-iam-user"
|
||||
prefix = "${var.iam_user_prefix}-${count.index}"
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
resource "aws_iam_user" "user" {
|
||||
count = var.iam_user_count
|
||||
name = random_id.aws_iam_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.aws_iam_user_name[count.index].dec
|
||||
force_destroy = true
|
||||
}
|
||||
|
||||
resource "aws_iam_access_key" "user_initial_key" {
|
||||
@@ -22,13 +26,8 @@ resource "aws_iam_access_key" "user_initial_key" {
|
||||
user = aws_iam_user.user[count.index].name
|
||||
}
|
||||
|
||||
resource "random_id" "aws_ec2_policy_name" {
|
||||
prefix = "BoundaryPluginHost"
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
resource "aws_iam_policy" "ec2_describeinstances" {
|
||||
name = random_id.aws_ec2_policy_name.dec
|
||||
name = "BoundaryPluginHostAwsTestDescribeInstancesPolicy"
|
||||
|
||||
policy = <<EOF
|
||||
{
|
||||
@@ -52,15 +51,9 @@ resource "aws_iam_user_policy_attachment" "user_ec2_describeinstances_attachment
|
||||
policy_arn = aws_iam_policy.ec2_describeinstances.arn
|
||||
}
|
||||
|
||||
resource "random_id" "aws_iam_policy_name" {
|
||||
count = var.iam_user_count
|
||||
prefix = "BoundaryPluginCredentials"
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
resource "aws_iam_policy" "user_self_manage_policy" {
|
||||
count = var.iam_user_count
|
||||
name = random_id.aws_iam_policy_name[count.index].dec
|
||||
name = "BoundaryPluginHostAwsTestUserSelfManagePolicy-${count.index}"
|
||||
|
||||
policy = <<EOF
|
||||
{
|
||||
|
||||
Vendored
+2
-2
@@ -1,11 +1,11 @@
|
||||
# Copyright IBM Corp. 2021, 2026
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.0"
|
||||
version = "~> 3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
-49
@@ -1,49 +0,0 @@
|
||||
# Copyright IBM Corp. 2021, 2026
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
|
||||
variable "iam_user_count" {
|
||||
default = 6
|
||||
}
|
||||
|
||||
# We are using a shared account for testing so make sure the stale ec2
|
||||
# machine tags don't match for this test run
|
||||
resource "random_id" "foo_key" {
|
||||
prefix = "foo"
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
resource "random_id" "bar_key" {
|
||||
prefix = "bar"
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
resource "random_id" "baz_key" {
|
||||
prefix = "baz"
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
locals {
|
||||
hashicorp_email = split(":", data.aws_caller_identity.current.user_id)[1]
|
||||
|
||||
instance_tags = [
|
||||
{
|
||||
"${random_id.foo_key.dec}" = "true"
|
||||
},
|
||||
{
|
||||
"${random_id.foo_key.dec}" = "true"
|
||||
"${random_id.bar_key.dec}" = "true"
|
||||
},
|
||||
{
|
||||
"${random_id.bar_key.dec}" = "true"
|
||||
},
|
||||
{
|
||||
"${random_id.bar_key.dec}" = "true"
|
||||
"${random_id.baz_key.dec}" = "true"
|
||||
},
|
||||
{
|
||||
"${random_id.baz_key.dec}" = "true"
|
||||
},
|
||||
]
|
||||
}
|
||||
Vendored
+30
-133
@@ -1,6 +1,28 @@
|
||||
# Copyright IBM Corp. 2021, 2026
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
variable "instance_tags" {
|
||||
default = [
|
||||
{
|
||||
"foo" = "true",
|
||||
},
|
||||
{
|
||||
"foo" = "true",
|
||||
"bar" = "true",
|
||||
},
|
||||
{
|
||||
"bar" = "true",
|
||||
},
|
||||
{
|
||||
"bar" = "true",
|
||||
"baz" = "true",
|
||||
},
|
||||
{
|
||||
"baz" = "true",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
data "aws_availability_zones" "azs" {}
|
||||
|
||||
data "aws_ami" "ubuntu" {
|
||||
@@ -20,117 +42,22 @@ data "aws_ami" "ubuntu" {
|
||||
}
|
||||
|
||||
resource "aws_vpc" "vpc" {
|
||||
cidr_block = "10.0.0.0/16"
|
||||
assign_generated_ipv6_cidr_block = true
|
||||
cidr_block = "10.0.0.0/16"
|
||||
}
|
||||
|
||||
resource "aws_subnet" "subnet" {
|
||||
vpc_id = aws_vpc.vpc.id
|
||||
cidr_block = aws_vpc.vpc.cidr_block
|
||||
ipv6_cidr_block = cidrsubnet(aws_vpc.vpc.ipv6_cidr_block, 8, 0)
|
||||
availability_zone = data.aws_availability_zones.azs.names[0]
|
||||
map_public_ip_on_launch = true
|
||||
assign_ipv6_address_on_creation = true
|
||||
}
|
||||
|
||||
# Need to add a gateway for public IP addr assignments
|
||||
resource "aws_internet_gateway" "igw" {
|
||||
vpc_id = aws_vpc.vpc.id
|
||||
}
|
||||
|
||||
resource "aws_route_table" "public" {
|
||||
vpc_id = aws_vpc.vpc.id
|
||||
|
||||
route {
|
||||
cidr_block = "0.0.0.0/0"
|
||||
gateway_id = aws_internet_gateway.igw.id
|
||||
}
|
||||
|
||||
route {
|
||||
ipv6_cidr_block = "::/0"
|
||||
gateway_id = aws_internet_gateway.igw.id
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_route_table_association" "public" {
|
||||
subnet_id = aws_subnet.subnet.id
|
||||
route_table_id = aws_route_table.public.id
|
||||
vpc_id = aws_vpc.vpc.id
|
||||
cidr_block = aws_vpc.vpc.cidr_block
|
||||
availability_zone = data.aws_availability_zones.azs.names[0]
|
||||
}
|
||||
|
||||
resource "aws_instance" "instances" {
|
||||
count = length(local.instance_tags)
|
||||
count = length(var.instance_tags)
|
||||
ami = data.aws_ami.ubuntu.id
|
||||
instance_type = "t3.nano"
|
||||
subnet_id = aws_subnet.subnet.id
|
||||
|
||||
tags = local.instance_tags[count.index]
|
||||
}
|
||||
|
||||
resource "aws_instance" "multi_address_instance" {
|
||||
ami = data.aws_ami.ubuntu.id
|
||||
instance_type = "t3.nano"
|
||||
subnet_id = aws_subnet.subnet.id
|
||||
secondary_private_ips = ["10.0.10.10"]
|
||||
ipv6_address_count = 1
|
||||
associate_public_ip_address = true
|
||||
|
||||
tags = {
|
||||
Name = "boundary-multi-address-instance"
|
||||
"boundary-demo" = local.hashicorp_email
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_network_interface" "multi_address_secondary_eni" {
|
||||
subnet_id = aws_subnet.subnet.id
|
||||
private_ips = ["10.0.20.10"]
|
||||
ipv6_addresses = [cidrhost(aws_subnet.subnet.ipv6_cidr_block, 32)]
|
||||
|
||||
tags = {
|
||||
Name = "boundary-multi-address-secondary-eni"
|
||||
"boundary-demo" = local.hashicorp_email
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_network_interface_attachment" "multi_address_secondary_eni" {
|
||||
instance_id = aws_instance.multi_address_instance.id
|
||||
network_interface_id = aws_network_interface.multi_address_secondary_eni.id
|
||||
device_index = 1
|
||||
}
|
||||
|
||||
resource "aws_eip" "multi_address_primary" {
|
||||
domain = "vpc"
|
||||
network_interface = aws_instance.multi_address_instance.primary_network_interface_id
|
||||
associate_with_private_ip = aws_instance.multi_address_instance.private_ip
|
||||
depends_on = [aws_internet_gateway.igw, aws_route_table_association.public]
|
||||
|
||||
tags = {
|
||||
Name = "boundary-multi-address-primary-eip"
|
||||
"boundary-demo" = local.hashicorp_email
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_eip" "multi_address_primary_secondary_private" {
|
||||
domain = "vpc"
|
||||
network_interface = aws_instance.multi_address_instance.primary_network_interface_id
|
||||
associate_with_private_ip = "10.0.10.10"
|
||||
depends_on = [aws_internet_gateway.igw, aws_route_table_association.public, aws_instance.multi_address_instance]
|
||||
|
||||
tags = {
|
||||
Name = "boundary-multi-address-primary-secondary-eip"
|
||||
"boundary-demo" = local.hashicorp_email
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_eip" "multi_address_secondary_eni" {
|
||||
domain = "vpc"
|
||||
network_interface = aws_network_interface.multi_address_secondary_eni.id
|
||||
associate_with_private_ip = "10.0.20.10"
|
||||
depends_on = [aws_internet_gateway.igw, aws_route_table_association.public, aws_network_interface_attachment.multi_address_secondary_eni]
|
||||
|
||||
tags = {
|
||||
Name = "boundary-multi-address-secondary-eni-eip"
|
||||
"boundary-demo" = local.hashicorp_email
|
||||
}
|
||||
tags = var.instance_tags[count.index]
|
||||
}
|
||||
|
||||
output "instance_ids" {
|
||||
@@ -147,34 +74,4 @@ output "instance_tags" {
|
||||
value = {
|
||||
for _, r in aws_instance.instances : r.id => r.tags
|
||||
}
|
||||
}
|
||||
|
||||
output "instance_tag_keys" {
|
||||
value = [
|
||||
random_id.foo_key.dec,
|
||||
random_id.bar_key.dec,
|
||||
random_id.baz_key.dec,
|
||||
]
|
||||
}
|
||||
|
||||
output "multi_address_instance_id" {
|
||||
value = aws_instance.multi_address_instance.id
|
||||
}
|
||||
|
||||
output "multi_address_primary_addresses" {
|
||||
value = {
|
||||
private_ip = aws_instance.multi_address_instance.private_ip
|
||||
public_ip = aws_eip.multi_address_primary.public_ip
|
||||
ipv6 = tolist(aws_instance.multi_address_instance.ipv6_addresses)[0]
|
||||
}
|
||||
}
|
||||
|
||||
output "multi_address_secondary_addresses" {
|
||||
value = {
|
||||
primary_eni_private_ip = "10.0.10.10"
|
||||
primary_eni_public_ip = aws_eip.multi_address_primary_secondary_private.public_ip
|
||||
secondary_eni_private_ip = aws_network_interface.multi_address_secondary_eni.private_ip
|
||||
secondary_eni_public_ip = aws_eip.multi_address_secondary_eni.public_ip
|
||||
secondary_eni_ipv6 = tolist(aws_network_interface.multi_address_secondary_eni.ipv6_addresses)[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Copyright IBM Corp. 2021, 2026
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
resource "random_id" "foo" {
|
||||
|
||||
Vendored
-327
@@ -1,327 +0,0 @@
|
||||
# Copyright IBM Corp. 2021, 2026
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
resource "aws_iam_role" "valid" {
|
||||
name = "${random_id.prefix.dec}-valid"
|
||||
tags = local.tags
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Action = "sts:AssumeRole"
|
||||
Effect = "Allow"
|
||||
Sid = ""
|
||||
Principal = {
|
||||
AWS: data.aws_caller_identity.current.user_id
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_policy" "valid" {
|
||||
name = "${random_id.prefix.dec}-valid"
|
||||
tags = local.tags
|
||||
|
||||
policy = <<EOT
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"s3:PutObject",
|
||||
"s3:GetObject",
|
||||
"s3:DeleteObject",
|
||||
"s3:GetObjectAttributes",
|
||||
"s3:ListBucket",
|
||||
"s3:DeleteObject"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_s3_bucket.test.arn}*"
|
||||
},
|
||||
{
|
||||
"Action": [
|
||||
"s3:ListBucket"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_s3_bucket.test.arn}"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
EOT
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "valid" {
|
||||
role = aws_iam_role.valid.name
|
||||
policy_arn = aws_iam_policy.valid.arn
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "missing_put_obj" {
|
||||
name = "${random_id.prefix.dec}-missing-put-obj"
|
||||
tags = local.tags
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Action = "sts:AssumeRole"
|
||||
Effect = "Allow"
|
||||
Sid = ""
|
||||
Principal = {
|
||||
AWS: data.aws_caller_identity.current.user_id
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
resource "aws_iam_policy" "missing_put_obj" {
|
||||
name = "${random_id.prefix.dec}-missing-put-obj"
|
||||
tags = local.tags
|
||||
|
||||
policy = <<EOT
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"s3:GetObject",
|
||||
"s3:GetObjectAttributes",
|
||||
"s3:ListBucket",
|
||||
"s3:DeleteObject"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_s3_bucket.test.arn}*"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
EOT
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "missing_put_obj" {
|
||||
role = aws_iam_role.missing_put_obj.name
|
||||
policy_arn = aws_iam_policy.missing_put_obj.arn
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "missing_get_obj" {
|
||||
name = "${random_id.prefix.dec}-missing-get-obj"
|
||||
tags = local.tags
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Action = "sts:AssumeRole"
|
||||
Effect = "Allow"
|
||||
Sid = ""
|
||||
Principal = {
|
||||
AWS: data.aws_caller_identity.current.user_id
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_policy" "missing_get_obj" {
|
||||
name = "${random_id.prefix.dec}-missing-get-obj"
|
||||
tags = local.tags
|
||||
|
||||
policy = <<EOT
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"s3:PutObject",
|
||||
"s3:DeleteObject"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_s3_bucket.test.arn}*"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
EOT
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "missing_get_obj" {
|
||||
role = aws_iam_role.missing_get_obj.name
|
||||
policy_arn = aws_iam_policy.missing_get_obj.arn
|
||||
}
|
||||
|
||||
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}" # Do Not Remove/Edit. This is a requirement to obtain access in creating a iam user in the dev account.
|
||||
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" # Do Not Remove/Edit. This is a requirement to obtain access in creating a iam user in the dev account.
|
||||
tags = local.tags
|
||||
}
|
||||
|
||||
resource "aws_iam_access_key" "test" {
|
||||
count = var.iam_user_count
|
||||
user = aws_iam_user.test[count.index].name
|
||||
}
|
||||
|
||||
resource "aws_iam_user_policy_attachment" "test_s3" {
|
||||
count = var.iam_user_count
|
||||
user = aws_iam_user.test[count.index].name
|
||||
policy_arn = aws_iam_policy.valid.arn
|
||||
}
|
||||
|
||||
resource "aws_iam_policy" "credentials" {
|
||||
count = var.iam_user_count
|
||||
name = aws_iam_user.test[count.index].name
|
||||
tags = local.tags
|
||||
|
||||
policy = <<EOF
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"iam:DeleteAccessKey",
|
||||
"iam:GetUser",
|
||||
"iam:CreateAccessKey"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_iam_user.test[count.index].arn}"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
resource "aws_iam_user_policy_attachment" "test_credentials" {
|
||||
count = var.iam_user_count
|
||||
user = aws_iam_user.test[count.index].name
|
||||
policy_arn = aws_iam_policy.credentials[count.index].arn
|
||||
}
|
||||
|
||||
# Edge case for missing 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}" # Do Not Remove/Edit. This is a requirement to obtain access in creating a iam user in the dev account.
|
||||
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" # Do Not Remove/Edit. This is a requirement to obtain access in creating a iam user in the dev account.
|
||||
tags = local.tags
|
||||
}
|
||||
|
||||
resource "aws_iam_access_key" "missing_get_obj" {
|
||||
user = aws_iam_user.missing_get_obj.name
|
||||
}
|
||||
|
||||
resource "aws_iam_user_policy_attachment" "missing_get_obj" {
|
||||
user = aws_iam_user.missing_get_obj.name
|
||||
policy_arn = aws_iam_policy.missing_get_obj.arn
|
||||
}
|
||||
|
||||
# Edge case for missing PutObject permission
|
||||
|
||||
resource "random_id" "missing_put_obj" {
|
||||
prefix = "demo-${local.hashicorp_email}" # Do Not Remove/Edit. This is a requirement to obtain access in creating a iam user in the dev account.
|
||||
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" # Do Not Remove/Edit. This is a requirement to obtain access in creating a iam user in the dev account.
|
||||
tags = local.tags
|
||||
}
|
||||
|
||||
resource "aws_iam_access_key" "missing_put_obj" {
|
||||
user = aws_iam_user.missing_put_obj.name
|
||||
}
|
||||
|
||||
resource "aws_iam_user_policy_attachment" "missing_put_obj" {
|
||||
user = aws_iam_user.missing_put_obj.name
|
||||
policy_arn = aws_iam_policy.missing_put_obj.arn
|
||||
}
|
||||
Vendored
-11
@@ -1,11 +0,0 @@
|
||||
# Copyright IBM Corp. 2021, 2026
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
-75
@@ -1,75 +0,0 @@
|
||||
# Copyright IBM Corp. 2021, 2026
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
output "bucket_name" {
|
||||
value = aws_s3_bucket.test.id
|
||||
}
|
||||
|
||||
output "valid_policy_arn" {
|
||||
value = aws_iam_role.valid.arn
|
||||
}
|
||||
|
||||
output "missing_put_obj_policy_arn" {
|
||||
value = aws_iam_role.missing_put_obj.arn
|
||||
}
|
||||
|
||||
output "missing_get_obj_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
|
||||
}
|
||||
|
||||
output "iam_user_arns" {
|
||||
value = aws_iam_user.test.*.arn
|
||||
}
|
||||
|
||||
output "iam_access_key_ids" {
|
||||
value = aws_iam_access_key.test.*.id
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "iam_secret_access_keys" {
|
||||
value = aws_iam_access_key.test.*.secret
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "iam_access_key_missing_put_obj" {
|
||||
value = aws_iam_access_key.missing_put_obj.id
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "iam_secret_access_key_missing_put_obj" {
|
||||
value = aws_iam_access_key.missing_put_obj.secret
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "iam_access_key_missing_get_obj" {
|
||||
value = aws_iam_access_key.missing_get_obj.id
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "iam_secret_access_key_missing_get_obj" {
|
||||
value = aws_iam_access_key.missing_get_obj.secret
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
output "s3_pagination_test_object_count" {
|
||||
description = "The number of S3 objects created for each pagination test scenario."
|
||||
value = local.s3_pagination_test_object_count
|
||||
}
|
||||
Vendored
-25
@@ -1,25 +0,0 @@
|
||||
# Copyright IBM Corp. 2021, 2026
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
resource "aws_s3_bucket" "test" {
|
||||
bucket = random_id.prefix.dec
|
||||
force_destroy = true
|
||||
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
|
||||
}
|
||||
resource "aws_s3_object" "s3_files" {
|
||||
for_each = local.s3_files
|
||||
|
||||
bucket = aws_s3_bucket.test.id
|
||||
key = each.key
|
||||
content = each.value
|
||||
tags = local.tags
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
HELLO_WORLD!
|
||||
Vendored
-62
@@ -1,62 +0,0 @@
|
||||
# Copyright IBM Corp. 2021, 2026
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
|
||||
resource "random_id" "prefix" {
|
||||
prefix = "aws-plugin-e2e-test"
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
variable "iam_user_count" {
|
||||
default = 6
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
# total number of objects of each type to create
|
||||
s3_pagination_test_object_count = 350
|
||||
# Dynamically build a flat map of S3 objects:
|
||||
# - root-level text files
|
||||
# - nested JSON files
|
||||
# - placeholder "empty directories"
|
||||
# - some static files in list-objects directory
|
||||
# This map is consumed directly by an aws_s3_object resource
|
||||
s3_files = merge(
|
||||
# Root-level text files
|
||||
{
|
||||
for i in range(local.s3_pagination_test_object_count) :
|
||||
"list-objects-paginated/root-file-${i}.txt" => "This is a root-level file #${i}."
|
||||
},
|
||||
|
||||
# Nested JSON files inside sub-directories
|
||||
{
|
||||
for i in range(local.s3_pagination_test_object_count) :
|
||||
"list-objects-paginated/nested-dir-${i}/data-file-${i}.json" =>
|
||||
jsonencode({
|
||||
id = i,
|
||||
message = "This is a nested JSON file #${i}."
|
||||
})
|
||||
},
|
||||
|
||||
# "Empty" directories (in S3 these are just placeholder objects ending with `/`)
|
||||
{
|
||||
for i in range(local.s3_pagination_test_object_count) :
|
||||
"list-objects-paginated/empty-dir-${i}/dir-${i}/" => "" # zero-length object to mimic directory
|
||||
},
|
||||
|
||||
# Static example files in list-objects directory
|
||||
{
|
||||
"list-objects/file1.txt" = "This is the content for file 1."
|
||||
"list-objects/data.json" = jsonencode({ key = "value", items = [1, 2, 3] })
|
||||
"list-objects/nested-dir/file2.txt" = "This is a nested file."
|
||||
"list-objects/empty-nested-dir/" = ""
|
||||
}
|
||||
)
|
||||
}
|
||||
+1
-213
@@ -1,29 +1,18 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package testing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/smithy-go"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/values"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
||||
tfjson "github.com/hashicorp/terraform-json"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
const expectedIamUserCount = 6
|
||||
|
||||
// TestTerraformer is an object representing the lifecycle of a
|
||||
// particular Terraform project.
|
||||
//
|
||||
@@ -275,204 +264,3 @@ func (t *TestTerraformer) showJson() ([]byte, error) {
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func requireCredentialsInvalid(t *testing.T, accessKeyId, secretAccessKey string) {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
|
||||
c, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithAccessKey(accessKeyId),
|
||||
awsutil.WithSecretKey(secretAccessKey),
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
// We need to wait for invalidation as while awsutil waits for
|
||||
// credential creation, deletion of the old credentials returns
|
||||
// immediately.
|
||||
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
defer cancel()
|
||||
waitErr:
|
||||
for {
|
||||
_, err = c.GetCallerIdentity(context.Background())
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second):
|
||||
// pass
|
||||
|
||||
case <-timeoutCtx.Done():
|
||||
break waitErr
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(err)
|
||||
var ae smithy.APIError
|
||||
require.True(errors.As(err, &ae))
|
||||
require.Equal("InvalidClientTokenId", ae.ErrorCode())
|
||||
}
|
||||
|
||||
func requireCredentialsValid(t *testing.T, accessKeyId, secretAccessKey string) {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
|
||||
c, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithAccessKey(accessKeyId),
|
||||
awsutil.WithSecretKey(secretAccessKey),
|
||||
)
|
||||
require.NoError(err)
|
||||
_, err = c.GetCallerIdentity(context.Background())
|
||||
require.NoError(err)
|
||||
}
|
||||
|
||||
func validatePersistedSecrets(t *testing.T, persistedSecrets *structpb.Struct, accessKeyId, secretAccessKey string, rotate bool) (string, string) {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
|
||||
require.NotNil(persistedSecrets)
|
||||
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedAccessKeyId)
|
||||
if rotate {
|
||||
require.NotEqual(accessKeyId, persistedAccessKeyId)
|
||||
} else {
|
||||
require.Equal(accessKeyId, persistedAccessKeyId)
|
||||
}
|
||||
|
||||
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedSecretAccessKey)
|
||||
if rotate {
|
||||
require.NotEqual(secretAccessKey, persistedSecretAccessKey)
|
||||
} else {
|
||||
require.Equal(secretAccessKey, persistedSecretAccessKey)
|
||||
}
|
||||
|
||||
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
|
||||
require.NoError(err)
|
||||
if rotate {
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
requireCredentialsInvalid(t, accessKeyId, secretAccessKey)
|
||||
} else {
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
}
|
||||
|
||||
return persistedAccessKeyId, persistedSecretAccessKey
|
||||
}
|
||||
|
||||
func validateUpdateSecrets(
|
||||
t *testing.T, persistedSecrets *structpb.Struct, currentCredsLastRotatedTime time.Time,
|
||||
currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey string,
|
||||
rotated, rotate bool) (string, string) {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
|
||||
// Complex checks based on the scenarios.
|
||||
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedAccessKeyId)
|
||||
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedSecretAccessKey)
|
||||
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
|
||||
require.NoError(err)
|
||||
|
||||
// Our test scenarios are complex due the multi-dimensional nature
|
||||
// of criteria, so we lay them out in a switch below.
|
||||
switch {
|
||||
case newAccessKeyId != "" && rotated && rotate:
|
||||
// The new access key ID was provided, we had previously rotated
|
||||
// the credentials before, and the new credential set is to be
|
||||
// rotated as well. In this case, the old credentials should have
|
||||
// been deleted, and the new credentials should have been rotated,
|
||||
// hence, should not match the new credentials initially
|
||||
// provided. Rotation time should be non-zero and updated.
|
||||
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
|
||||
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
|
||||
|
||||
case newAccessKeyId != "" && rotated && !rotate:
|
||||
// The new access key ID was provided, we had previously rotated
|
||||
// the credentials before, and the new credential is *not*
|
||||
// rotated. In this case, the old credentials should have
|
||||
// been deleted, But the new credentials should have not been
|
||||
// rotated, and hence should be the same. Rotation time should be
|
||||
// zero.
|
||||
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, newAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
|
||||
case newAccessKeyId != "" && !rotated && rotate:
|
||||
// The new access key ID was provided, we *have not* previously
|
||||
// rotated the credentials, and the new credential set is to be
|
||||
// rotated. In this case, the old credentials should have been
|
||||
// left alone, and the new credentials should have been rotated,
|
||||
// hence, should not match the new credentials initially
|
||||
// provided. Rotation time should be non-zero, but updated.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
|
||||
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
|
||||
|
||||
case newAccessKeyId != "" && !rotated && !rotate:
|
||||
// The new access key ID was provided, but we have not rotated
|
||||
// the credentials previously and we still don't plan on rotating
|
||||
// them. In this case, the old credentials should still be valid,
|
||||
// and the persisted ones should match the new ones provided.
|
||||
// Rotation time should be zero.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, newAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
|
||||
case newAccessKeyId == "" && rotated && rotate:
|
||||
// No new credentials have been provided, but we have previously
|
||||
// rotated and are still rotating credentials. This is a no-op.
|
||||
// Existing credentials should still be valid and match the ones
|
||||
// persisted to state. Rotation time should be identical since
|
||||
// no new rotation occurred.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, currentAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
require.True(currentCredsLastRotatedTime.Equal(persistedCredsLastRotatedTime))
|
||||
|
||||
case newAccessKeyId == "" && rotated && !rotate:
|
||||
// No new credentials have been provided, and we have previously
|
||||
// rotated the credentials. This is actually an error, but we
|
||||
// don't test it here; it's covered in unit testing (see
|
||||
// plugin_test.go).
|
||||
require.FailNow("testing rotated-to-not-rotated scenario not implemented by this helper")
|
||||
|
||||
case newAccessKeyId == "" && !rotated && rotate:
|
||||
// No new credentials have been provided, and while we did not
|
||||
// rotate before, we want to switch to rotation. In this case,
|
||||
// the existing persisted credentials should have been rotated,
|
||||
// with a new non-zero timestamp.
|
||||
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.NotEqual(persistedAccessKeyId, currentAccessKeyId)
|
||||
require.NotEqual(persistedSecretAccessKey, currentSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
|
||||
case newAccessKeyId == "" && !rotated && !rotate:
|
||||
// No new credentials have been provided and we have not, nor do
|
||||
// not, plan on rotating the credentials. This is a no-op.
|
||||
// Existing credentials should still be valid and match the ones
|
||||
// persisted to state. Rotation time should remain at zero.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, currentAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
|
||||
default:
|
||||
// Scenario was reached that was not covered by this function.
|
||||
require.FailNow("unknown test scenario")
|
||||
}
|
||||
|
||||
return persistedAccessKeyId, persistedSecretAccessKey
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package testing
|
||||
|
||||
Reference in new issue
Block a user