feat: assume role provider credentials (#28)
* feat: assume role provider credentials This commit updates the awsutil dependency to its latest version, which supports the capability of using dynamic credentials. The storage service was refactored to make static credentials optional. The credentials pkg was refactored to support several attribute fields used for the assume role provider. The host service still enforces the use of static credentials.
This commit is contained in:
1 parent
334732967d
commit
07f015be28
32 files changed
+2351
-998
No files matched your search
@@ -5,15 +5,32 @@ Boundary](https://www.boundaryproject.io/).
|
||||
|
||||
## Credential Rotation
|
||||
|
||||
Although credentials are stored encrypted within Boundary, by default this
|
||||
This is the following priority for the credential chain:
|
||||
static, assume role, environment variables.
|
||||
|
||||
### Static Credentials
|
||||
|
||||
Although static credentials are stored encrypted within Boundary, by default this
|
||||
plugin will attempt to rotate credentials when they are supplied through the
|
||||
`secrets` object. The given credentials will be used to create a new credential,
|
||||
and then the given credential will be revoked. In this way, after rotation,
|
||||
only Boundary knows the client secret in use by this plugin.
|
||||
only Boundary knows the client secret in use by this plugin. More information
|
||||
about AWS static credentials can be found [here](https://docs.aws.amazon.com/sdkref/latest/guide/feature-static-credentials.html).
|
||||
|
||||
Credential rotation can be turned off by setting the
|
||||
`disable_credential_rotation` attribute to `true`.
|
||||
|
||||
### Assume Role Credentials
|
||||
|
||||
This plugin will attempt to assume a role when a `role_arn` is supplied through the
|
||||
`attributes` object. More information about assume an AWS role can be found [here](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html). This feature only works when the plugin is running on a self managed Boundary [worker](https://developer.hashicorp.com/boundary/tutorials/hcp-administration/hcp-manage-workers).
|
||||
|
||||
### Environment Credentials
|
||||
|
||||
This plugin will attempt to retrieve credentials from environment variables. More
|
||||
information about environment variables for AWS credentials can be found [here](https://docs.aws.amazon.com/sdkref/latest/guide/feature-static-credentials.html). This feature only works when the plugin is running on
|
||||
a self managed Boundary [worker](https://developer.hashicorp.com/boundary/tutorials/hcp-administration/hcp-manage-workers).
|
||||
|
||||
## Dynamic Hosts
|
||||
|
||||
This plugin supports dynamically sourcing hosts from Amazon EC2.
|
||||
|
||||
@@ -13,7 +13,7 @@ require (
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/hashicorp/boundary/sdk v0.0.33
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil v0.2.2
|
||||
github.com/hashicorp/terraform-json v0.14.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
|
||||
@@ -4,7 +4,7 @@ github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj
|
||||
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
|
||||
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
||||
github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
|
||||
github.com/aws/aws-sdk-go v1.34.0/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/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
|
||||
@@ -54,7 +54,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
|
||||
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
|
||||
@@ -86,7 +85,6 @@ github.com/hashicorp/eventlogger/filters/encrypt v0.1.8-0.20230428153751-cca4458
|
||||
github.com/hashicorp/eventlogger/filters/encrypt v0.1.8-0.20230428153751-cca445805f24/go.mod h1:EQPLoX6CONA9BSYUovTQBHfPGE91g7wOxv03sO29FzY=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
|
||||
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.4 h1:8z+Zi0uLYXONkxwWb/D7O5t1IUucB6Ft3uvSOa950bM=
|
||||
@@ -95,8 +93,8 @@ github.com/hashicorp/go-kms-wrapping/v2 v2.0.9/go.mod h1:NtMaPhqSlfQ72XWDD2g80o8
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-plugin v1.4.9 h1:ESiK220/qE0aGxWdzKIvRH69iLiuN/PjoLTm69RoWtU=
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6 h1:W9WN8p6moV1fjKLkeqEgkAMu5rauy9QeYDAmIaPuuiA=
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6/go.mod h1:MpCPSPGLDILGb4JMm94/mMi3YysIqsXzGCzkEZjcjXg=
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil v0.2.2 h1:kWg2vyKl7BRXrNxYziqDJ55n+vtOQ1QsGORjzoeB+uM=
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil v0.2.2/go.mod h1:oKHSQs4ivIfZ3fbXGQOop1XuDfdSb8RIsWTGaAanSfg=
|
||||
github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw=
|
||||
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 h1:ET4pqyjiGmY09R5y+rSd70J2w45CtbWDNvGqWp/R3Ng=
|
||||
github.com/hashicorp/go-secure-stdlib/configutil/v2 v2.0.9 h1:YZ01/aiARPneDUryzjiPGvr3pmjCSC+1WBZKI1P7I7k=
|
||||
@@ -134,14 +132,10 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
@@ -175,7 +169,6 @@ github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
@@ -221,12 +214,9 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
@@ -20,33 +20,47 @@ type CredentialAttributes struct {
|
||||
|
||||
// DisableCredentialRotation disables the rotation of aws secrets associated with the plugin
|
||||
DisableCredentialRotation bool
|
||||
|
||||
// RoleArn is the role arn associated with the aws credentials
|
||||
RoleArn string
|
||||
|
||||
// RoleExternalId is the external id associated with the aws credentials
|
||||
RoleExternalId string
|
||||
|
||||
// RoleSessionName is the session name associated with the aws credentials
|
||||
RoleSessionName string
|
||||
|
||||
// RoleTags is the tags associated with the aws credentials
|
||||
RoleTags map[string]string
|
||||
}
|
||||
|
||||
// GetCredentialsConfig parses values out of a protobuf struct input and returns a
|
||||
// GetCredentialsConfig parses values out of a protobuf struct secrets and returns a
|
||||
// CredentialsConfig used for configuring an AWS session. An error is returned if
|
||||
// any of the following fields are missing from the protobuf struct input or have
|
||||
// invalid value types: access_key_id, secret_access_key. An error is returned if
|
||||
// any unrecognized fields are found in the protobuf struct input.
|
||||
func GetCredentialsConfig(in *structpb.Struct, region string) (*awsutil.CredentialsConfig, error) {
|
||||
unknownFields := values.StructFields(in)
|
||||
func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes, required bool) (*awsutil.CredentialsConfig, error) {
|
||||
// initialize secrets if it is nil
|
||||
// secrets can be nil because static credentials are optional
|
||||
if secrets == nil {
|
||||
secrets = &structpb.Struct{
|
||||
Fields: make(map[string]*structpb.Value),
|
||||
}
|
||||
}
|
||||
|
||||
unknownFields := values.StructFields(secrets)
|
||||
badFields := make(map[string]string)
|
||||
|
||||
accessKey, err := values.GetStringValue(in, ConstAccessKeyId, true)
|
||||
accessKey, err := values.GetStringValue(secrets, ConstAccessKeyId, required)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = err.Error()
|
||||
}
|
||||
delete(unknownFields, ConstAccessKeyId)
|
||||
|
||||
secretKey, err := values.GetStringValue(in, ConstSecretAccessKey, true)
|
||||
secretKey, err := values.GetStringValue(secrets, ConstSecretAccessKey, required)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = err.Error()
|
||||
}
|
||||
delete(unknownFields, ConstSecretAccessKey)
|
||||
|
||||
if region == "" {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstRegion)] = err.Error()
|
||||
}
|
||||
|
||||
// the creds_last_rotated_time field will be found in the input struct for
|
||||
// persisted secrets, this value is not needed for creating the CredentialsConfig
|
||||
delete(unknownFields, ConstCredsLastRotatedTime)
|
||||
@@ -58,11 +72,32 @@ func GetCredentialsConfig(in *structpb.Struct, region string) (*awsutil.Credenti
|
||||
return nil, errors.InvalidArgumentError("Error in the secrets provided", badFields)
|
||||
}
|
||||
|
||||
return awsutil.NewCredentialsConfig(
|
||||
awsutil.WithAccessKey(accessKey),
|
||||
awsutil.WithSecretKey(secretKey),
|
||||
awsutil.WithRegion(region),
|
||||
)
|
||||
opts := []awsutil.Option{
|
||||
awsutil.WithEnvironmentCredentials(true),
|
||||
}
|
||||
if accessKey != "" && secretKey != "" {
|
||||
opts = append(opts,
|
||||
awsutil.WithAccessKey(accessKey),
|
||||
awsutil.WithSecretKey(secretKey),
|
||||
)
|
||||
}
|
||||
if attrs.Region != "" {
|
||||
opts = append(opts, awsutil.WithRegion(attrs.Region))
|
||||
}
|
||||
if attrs.RoleArn != "" {
|
||||
opts = append(opts, awsutil.WithRoleArn(attrs.RoleArn))
|
||||
}
|
||||
if attrs.RoleExternalId != "" {
|
||||
opts = append(opts, awsutil.WithRoleExternalId(attrs.RoleExternalId))
|
||||
}
|
||||
if attrs.RoleSessionName != "" {
|
||||
opts = append(opts, awsutil.WithRoleSessionName(attrs.RoleSessionName))
|
||||
}
|
||||
if len(attrs.RoleTags) != 0 {
|
||||
opts = append(opts, awsutil.WithRoleTags(attrs.RoleTags))
|
||||
}
|
||||
|
||||
return awsutil.NewCredentialsConfig(opts...)
|
||||
}
|
||||
|
||||
// GetCredentialAttributes parses values out of a protobuf struct input and returns a
|
||||
@@ -88,6 +123,26 @@ func GetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error)
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstDisableCredentialRotation)] = err.Error()
|
||||
}
|
||||
|
||||
roleArn, err := values.GetStringValue(in, ConstRoleArn, false)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstRoleArn)] = err.Error()
|
||||
}
|
||||
|
||||
roleExternalId, err := values.GetStringValue(in, ConstRoleExternalId, false)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstRoleExternalId)] = err.Error()
|
||||
}
|
||||
|
||||
roleSessionName, err := values.GetStringValue(in, ConstRoleSessionName, false)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstRoleSessionName)] = err.Error()
|
||||
}
|
||||
|
||||
roleTags, err := values.GetMapStringString(in, ConstRoleTags, false)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstRoleTags)] = err.Error()
|
||||
}
|
||||
|
||||
if len(badFields) > 0 {
|
||||
return nil, errors.InvalidArgumentError("Error in the attributes provided", badFields)
|
||||
}
|
||||
@@ -95,5 +150,9 @@ func GetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error)
|
||||
return &CredentialAttributes{
|
||||
Region: region,
|
||||
DisableCredentialRotation: disableCredentialRotation,
|
||||
RoleArn: roleArn,
|
||||
RoleExternalId: roleExternalId,
|
||||
RoleSessionName: roleSessionName,
|
||||
RoleTags: roleTags,
|
||||
}, nil
|
||||
}
|
||||
@@ -61,6 +61,28 @@ func TestGetCredentialAttributes(t *testing.T) {
|
||||
DisableCredentialRotation: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with assume role",
|
||||
in: map[string]any{
|
||||
ConstRegion: "us-west-2",
|
||||
ConstRoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
ConstRoleExternalId: "1234567890",
|
||||
ConstRoleSessionName: "test-session",
|
||||
ConstRoleTags: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
expected: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
DisableCredentialRotation: false,
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@@ -89,44 +111,91 @@ func TestGetCredentialAttributes(t *testing.T) {
|
||||
func TestGetCredentialsConfig(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in map[string]any
|
||||
region string
|
||||
secrets *structpb.Struct
|
||||
required bool
|
||||
attrs *CredentialAttributes
|
||||
expected *awsutil.CredentialsConfig
|
||||
expectedErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing access_key_id",
|
||||
in: map[string]any{},
|
||||
region: "us-west-2",
|
||||
expectedErrContains: "missing required value \"access_key_id\"",
|
||||
name: "no secrets",
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expected: &awsutil.CredentialsConfig{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing secret_access_key",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
name: "with static credentials",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
region: "us-west-2",
|
||||
expectedErrContains: "missing required value \"secret_access_key\"",
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expected: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing access key",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
required: true,
|
||||
expectedErrContains: "secrets.access_key_id: missing required value",
|
||||
},
|
||||
{
|
||||
name: "missing secret key",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expectedErrContains: "secrets.secret_access_key: missing required value",
|
||||
},
|
||||
{
|
||||
name: "unknown fields",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
ConstSecretAccessKey: "bazqux",
|
||||
"foo": true,
|
||||
"bar": true,
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
"foo": structpb.NewBoolValue(true),
|
||||
"bar": structpb.NewBoolValue(true),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
region: "us-west-2",
|
||||
expectedErrContains: "secrets.bar: unrecognized field, secrets.foo: unrecognized field",
|
||||
},
|
||||
{
|
||||
name: "valid ignore creds_last_rotated_time",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
ConstSecretAccessKey: "bazqux",
|
||||
ConstCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
region: "us-west-2",
|
||||
expected: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
@@ -134,16 +203,56 @@ func TestGetCredentialsConfig(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "good",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
ConstSecretAccessKey: "bazqux",
|
||||
name: "with assume role",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
region: "us-west-2",
|
||||
expected: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
Region: "us-west-2",
|
||||
RoleARN: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with static credential & assume role",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
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",
|
||||
},
|
||||
},
|
||||
expected: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
RoleARN: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -153,10 +262,7 @@ func TestGetCredentialsConfig(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
input, err := structpb.NewStruct(tc.in)
|
||||
require.NoError(err)
|
||||
|
||||
actual, err := GetCredentialsConfig(input, tc.region)
|
||||
actual, err := GetCredentialsConfig(tc.secrets, tc.attrs, tc.required)
|
||||
if tc.expectedErrContains != "" {
|
||||
require.Error(err)
|
||||
require.Contains(err.Error(), tc.expectedErrContains)
|
||||
@@ -168,6 +274,10 @@ func TestGetCredentialsConfig(t *testing.T) {
|
||||
require.Equal(tc.expected.AccessKey, actual.AccessKey)
|
||||
require.Equal(tc.expected.SecretKey, actual.SecretKey)
|
||||
require.Equal(tc.expected.Region, actual.Region)
|
||||
require.Equal(tc.expected.RoleARN, actual.RoleARN)
|
||||
require.Equal(tc.expected.RoleExternalId, actual.RoleExternalId)
|
||||
require.Equal(tc.expected.RoleSessionName, actual.RoleSessionName)
|
||||
require.Equal(tc.expected.RoleTags, actual.RoleTags)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -18,4 +18,16 @@ const (
|
||||
|
||||
// ConstCredsLastRotatedTime is the key for the last rotated time in the aws credentials.
|
||||
ConstCredsLastRotatedTime = "creds_last_rotated_time"
|
||||
|
||||
// ConstRoleArn is the key for assuming a IAM role.
|
||||
ConstRoleArn = "role_arn"
|
||||
|
||||
// ConstRoleExternalId is the key for the external id used for assuming a IAM role.
|
||||
ConstRoleExternalId = "role_external_id"
|
||||
|
||||
// ConstRoleSessionName is the key for the session name used for assuming a IAM role.
|
||||
ConstRoleSessionName = "role_session_name"
|
||||
|
||||
// ConstRoleTags is the key for the tags used for assuming a IAM role.
|
||||
ConstRoleTags = "role_tags"
|
||||
)
|
||||
+94
-113
@@ -6,6 +6,7 @@ package credential
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
@@ -25,19 +26,13 @@ type AwsCredentialPersistedStateOption func(s *AwsCredentialPersistedState) erro
|
||||
|
||||
// AwsCredentialPersistedState is the persisted state for the AWS credential.
|
||||
type AwsCredentialPersistedState struct {
|
||||
// AccessKeyId is the access key id for the AWS credential.
|
||||
AccessKeyId string
|
||||
// SecretAccessKey is the secret access key for the AWS credential.
|
||||
SecretAccessKey string
|
||||
// CredentialsConfig is the credential configuration for the AWS credential.
|
||||
CredentialsConfig *awsutil.CredentialsConfig
|
||||
// CredsLastRotatedTime is the last rotation of aws secrets for the AWS credential.
|
||||
CredsLastRotatedTime time.Time
|
||||
|
||||
// testOpts are options that should be used for testing only
|
||||
testOpts []awsutil.Option
|
||||
|
||||
// Region is not a part of persisted state but if set is passed to clients
|
||||
// created with this state
|
||||
region string
|
||||
}
|
||||
|
||||
// WithStateTestOpts enables unit testing different edge cases
|
||||
@@ -50,31 +45,19 @@ func WithStateTestOpts(opts []awsutil.Option) AwsCredentialPersistedStateOption
|
||||
}
|
||||
}
|
||||
|
||||
// WithAccessKeyId sets the value for AccesskeyId in the storage persisted state.
|
||||
func WithAccessKeyId(x string) AwsCredentialPersistedStateOption {
|
||||
// WithCredentialsConfig sets the value for CredentialsConfig in the credential persisted state.
|
||||
func WithCredentialsConfig(x *awsutil.CredentialsConfig) AwsCredentialPersistedStateOption {
|
||||
return func(s *AwsCredentialPersistedState) error {
|
||||
if s.AccessKeyId != "" {
|
||||
return errors.New("access key id already set")
|
||||
if s.CredentialsConfig != nil {
|
||||
return errors.New("credentials config already set")
|
||||
}
|
||||
|
||||
s.AccessKeyId = x
|
||||
s.CredentialsConfig = x
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSecretAccessKey sets the value for SecretAccessKey in the storage persisted state.
|
||||
func WithSecretAccessKey(x string) AwsCredentialPersistedStateOption {
|
||||
return func(s *AwsCredentialPersistedState) error {
|
||||
if s.SecretAccessKey != "" {
|
||||
return errors.New("secret access key already set")
|
||||
}
|
||||
|
||||
s.SecretAccessKey = x
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithCredsLastRotatedTime sets the value for CredsLastRotatedTime in the storage persisted state.
|
||||
// WithCredsLastRotatedTime sets the value for CredsLastRotatedTime in the credential persisted state.
|
||||
func WithCredsLastRotatedTime(t time.Time) AwsCredentialPersistedStateOption {
|
||||
return func(s *AwsCredentialPersistedState) error {
|
||||
if !s.CredsLastRotatedTime.IsZero() {
|
||||
@@ -86,18 +69,6 @@ func WithCredsLastRotatedTime(t time.Time) AwsCredentialPersistedStateOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithRegion sets the value for region in the storage persisted state.
|
||||
func WithRegion(with string) AwsCredentialPersistedStateOption {
|
||||
return func(s *AwsCredentialPersistedState) error {
|
||||
if s.region != "" {
|
||||
return errors.New("region already set")
|
||||
}
|
||||
|
||||
s.region = with
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewAwsCredentialPersistedState returns a AwsCredentialPersistedState.
|
||||
// Supported options include: WithAccessKeyId, WithSecretAccessKey
|
||||
// WithCredsLastRotatedTime, & WithRegion.
|
||||
@@ -108,27 +79,19 @@ func NewAwsCredentialPersistedState(opts ...AwsCredentialPersistedStateOption) (
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// ValidateCreds takes the access key and secret key from the persisted state
|
||||
// ValidateCreds takes the credentials configuration from the persisted state
|
||||
// and runs sts.GetCallerIdentity for the current credentials, which is done
|
||||
// to check that the credentials are valid.
|
||||
func (s *AwsCredentialPersistedState) ValidateCreds() error {
|
||||
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
|
||||
awsutil.WithAccessKey(s.AccessKeyId),
|
||||
awsutil.WithSecretKey(s.SecretAccessKey),
|
||||
awsutil.WithRegion(s.region),
|
||||
}, s.testOpts...)...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading credentials: %w", err)
|
||||
if s.CredentialsConfig == nil {
|
||||
return errors.New("missing credentials config")
|
||||
}
|
||||
|
||||
if _, err := c.GetCallerIdentity(s.testOpts...); err != nil {
|
||||
if _, err := s.CredentialsConfig.GetCallerIdentity(s.testOpts...); err != nil {
|
||||
return fmt.Errorf("error validating credentials: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -138,42 +101,27 @@ func (s *AwsCredentialPersistedState) ValidateCreds() error {
|
||||
// This ensures that any generated new secret key never leaves this function in case of an error, even though it will
|
||||
// still result in an extraneous access key existing.
|
||||
func (s *AwsCredentialPersistedState) RotateCreds() error {
|
||||
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
|
||||
awsutil.WithAccessKey(s.AccessKeyId),
|
||||
awsutil.WithSecretKey(s.SecretAccessKey),
|
||||
awsutil.WithRegion(s.region),
|
||||
}, s.testOpts...)...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading credentials: %w", err)
|
||||
if s.CredentialsConfig == nil {
|
||||
return errors.New("missing credentials config")
|
||||
}
|
||||
|
||||
if err := c.RotateKeys(append([]awsutil.Option{
|
||||
if err := s.CredentialsConfig.RotateKeys(append([]awsutil.Option{
|
||||
awsutil.WithValidityCheckTimeout(rotationWaitTimeout),
|
||||
}, s.testOpts...)...); err != nil {
|
||||
return fmt.Errorf("error rotating credentials: %w", err)
|
||||
}
|
||||
|
||||
s.AccessKeyId = c.AccessKey
|
||||
s.SecretAccessKey = c.SecretKey
|
||||
s.CredsLastRotatedTime = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplaceCreds replaces the access key in the state with a new key.
|
||||
// If the existing key was rotated at any point in time, it is
|
||||
// deleted first, otherwise it's left alone.
|
||||
func (s *AwsCredentialPersistedState) ReplaceCreds(accessKeyId, secretAccessKey string) error {
|
||||
if accessKeyId == "" {
|
||||
return errors.New("access key id cannot be empty")
|
||||
func (s *AwsCredentialPersistedState) ReplaceCreds(credentialsConfig *awsutil.CredentialsConfig) error {
|
||||
if credentialsConfig == nil {
|
||||
return errors.New("missing new credentials config")
|
||||
}
|
||||
|
||||
if secretAccessKey == "" {
|
||||
return errors.New("secret access key cannot be empty")
|
||||
}
|
||||
|
||||
if accessKeyId == s.AccessKeyId {
|
||||
return errors.New("attempting to replace access key with the same one")
|
||||
if s.CredentialsConfig == nil {
|
||||
return errors.New("missing credentials config")
|
||||
}
|
||||
|
||||
if !s.CredsLastRotatedTime.IsZero() {
|
||||
@@ -186,8 +134,7 @@ func (s *AwsCredentialPersistedState) ReplaceCreds(accessKeyId, secretAccessKey
|
||||
}
|
||||
|
||||
// Set the new attributes and clear the rotated time.
|
||||
s.AccessKeyId = accessKeyId
|
||||
s.SecretAccessKey = secretAccessKey
|
||||
s.CredentialsConfig = credentialsConfig
|
||||
s.CredsLastRotatedTime = time.Time{}
|
||||
return nil
|
||||
}
|
||||
@@ -196,23 +143,17 @@ func (s *AwsCredentialPersistedState) ReplaceCreds(accessKeyId, secretAccessKey
|
||||
// ID, secret access key, and rotation time fields are zeroed out in
|
||||
// the state just to ensure that they cannot be re-used after.
|
||||
func (s *AwsCredentialPersistedState) DeleteCreds() error {
|
||||
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
|
||||
awsutil.WithAccessKey(s.AccessKeyId),
|
||||
awsutil.WithSecretKey(s.SecretAccessKey),
|
||||
awsutil.WithRegion(s.region),
|
||||
}, s.testOpts...)...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading credentials: %w", err)
|
||||
if s.CredentialsConfig == nil {
|
||||
return errors.New("missing credentials config")
|
||||
}
|
||||
|
||||
if err := c.DeleteAccessKey(s.AccessKeyId, s.testOpts...); err != nil {
|
||||
if err := s.CredentialsConfig.DeleteAccessKey(s.CredentialsConfig.AccessKey, s.testOpts...); err != nil {
|
||||
// Determine if the deletion error was due to a missing
|
||||
// resource. If it was, just pass it.
|
||||
var awsErr awserr.Error
|
||||
if errors.As(err, &awsErr) {
|
||||
if awsErr.Code() == iam.ErrCodeNoSuchEntityException {
|
||||
s.AccessKeyId = ""
|
||||
s.SecretAccessKey = ""
|
||||
s.CredentialsConfig = nil
|
||||
s.CredsLastRotatedTime = time.Time{}
|
||||
return nil
|
||||
}
|
||||
@@ -222,8 +163,7 @@ func (s *AwsCredentialPersistedState) DeleteCreds() error {
|
||||
return err
|
||||
}
|
||||
|
||||
s.AccessKeyId = ""
|
||||
s.SecretAccessKey = ""
|
||||
s.CredentialsConfig = nil
|
||||
s.CredsLastRotatedTime = time.Time{}
|
||||
return nil
|
||||
}
|
||||
@@ -231,50 +171,50 @@ func (s *AwsCredentialPersistedState) DeleteCreds() error {
|
||||
// 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()
|
||||
return s.CredentialsConfig.GetSession(s.testOpts...)
|
||||
}
|
||||
|
||||
// ToMap returns a map of the credentials stored in the persisted state,
|
||||
// which includes the following keys: access_key_id, secret_access_key,
|
||||
// & creds_last_rotated_time
|
||||
// ToMap returns a map of the credentials stored in the persisted state.
|
||||
// ToMap will return an empty map for temporary credentials. ToMap will
|
||||
// return a map for long-term credentials with following keys:
|
||||
// access_key_id, secret_access_key & creds_last_rotated_time
|
||||
func (s *AwsCredentialPersistedState) ToMap() map[string]any {
|
||||
if HasDynamicCredentials(s.CredentialsConfig.AccessKey) {
|
||||
return map[string]any{}
|
||||
}
|
||||
return map[string]any{
|
||||
ConstAccessKeyId: s.AccessKeyId,
|
||||
ConstSecretAccessKey: s.SecretAccessKey,
|
||||
ConstAccessKeyId: s.CredentialsConfig.AccessKey,
|
||||
ConstSecretAccessKey: s.CredentialsConfig.SecretKey,
|
||||
ConstCredsLastRotatedTime: s.CredsLastRotatedTime.Format(time.RFC3339Nano),
|
||||
}
|
||||
}
|
||||
|
||||
// AwsCredentialPersistedStateFromProto parses values out of a protobuf struct input
|
||||
// and returns a AwsCredentialPersistedState used for configuring an AWS session.
|
||||
// An error is returned if any of the following fields are missing from the protobuf
|
||||
// struct input or have invalid value types: access_key_id, secret_access_key, &
|
||||
// creds_last_rotated_time.
|
||||
func AwsCredentialPersistedStateFromProto(in *structpb.Struct, opts ...AwsCredentialPersistedStateOption) (*AwsCredentialPersistedState, error) {
|
||||
if in == nil {
|
||||
return nil, errors.New("missing persisted secrets")
|
||||
func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *CredentialAttributes, opts ...AwsCredentialPersistedStateOption) (*AwsCredentialPersistedState, error) {
|
||||
// initialize secrets if it is nil
|
||||
// secrets can be nil because static credentials are optional
|
||||
if secrets == nil {
|
||||
secrets = &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{},
|
||||
}
|
||||
}
|
||||
|
||||
accessKeyId, err := values.GetStringValue(in, ConstAccessKeyId, true)
|
||||
if attrs == nil {
|
||||
return nil, fmt.Errorf("missing credential attributes")
|
||||
}
|
||||
|
||||
accessKeyId, err := values.GetStringValue(secrets, ConstAccessKeyId, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||
}
|
||||
|
||||
secretAccessKey, err := values.GetStringValue(in, ConstSecretAccessKey, true)
|
||||
secretAccessKey, err := values.GetStringValue(secrets, ConstSecretAccessKey, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||
}
|
||||
|
||||
credsLastRotatedTime, err := values.GetTimeValue(in, ConstCredsLastRotatedTime)
|
||||
credsLastRotatedTime, err := values.GetTimeValue(secrets, ConstCredsLastRotatedTime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||
}
|
||||
@@ -284,9 +224,50 @@ func AwsCredentialPersistedStateFromProto(in *structpb.Struct, opts ...AwsCreden
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.AccessKeyId = accessKeyId
|
||||
s.SecretAccessKey = secretAccessKey
|
||||
awsOpts := append([]awsutil.Option{}, s.testOpts...)
|
||||
if accessKeyId != "" && secretAccessKey != "" {
|
||||
awsOpts = append(awsOpts,
|
||||
awsutil.WithAccessKey(accessKeyId),
|
||||
awsutil.WithSecretKey(secretAccessKey),
|
||||
)
|
||||
}
|
||||
if attrs.Region != "" {
|
||||
awsOpts = append(awsOpts, awsutil.WithRegion(attrs.Region))
|
||||
}
|
||||
if attrs.RoleArn != "" {
|
||||
awsOpts = append(awsOpts, awsutil.WithRoleArn(attrs.RoleArn))
|
||||
}
|
||||
if attrs.RoleExternalId != "" {
|
||||
awsOpts = append(awsOpts, awsutil.WithRoleExternalId(attrs.RoleExternalId))
|
||||
}
|
||||
if attrs.RoleSessionName != "" {
|
||||
awsOpts = append(awsOpts, awsutil.WithRoleSessionName(attrs.RoleSessionName))
|
||||
}
|
||||
if len(attrs.RoleTags) != 0 {
|
||||
awsOpts = append(awsOpts, awsutil.WithRoleTags(attrs.RoleTags))
|
||||
}
|
||||
credentialsConfig, err := awsutil.NewCredentialsConfig(awsOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.CredentialsConfig = credentialsConfig
|
||||
s.CredsLastRotatedTime = credsLastRotatedTime
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// HasDynamicCredentials returns true if the access key is a dynamic credential type.
|
||||
//
|
||||
// https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html#sec-access-keys-and-secret-access-keys
|
||||
// Access key IDs beginning with ASIA are temporary credentials access keys that you create using AWS STS operations.
|
||||
func HasDynamicCredentials(accessKey string) bool {
|
||||
return strings.HasPrefix(accessKey, "ASIA")
|
||||
}
|
||||
|
||||
// HasStaticCredentials returns true if the access key is a static credential type.
|
||||
//
|
||||
// https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html#sec-access-keys-and-secret-access-keys
|
||||
// Access key IDs beginning with AKIA are long-term access keys for an IAM user or an AWS account root user.
|
||||
func HasStaticCredentials(accessKey string) bool {
|
||||
return strings.HasPrefix(accessKey, "AKIA")
|
||||
}
|
||||
+434
-193
@@ -36,21 +36,22 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
|
||||
expectedErr: testOptionErr,
|
||||
},
|
||||
{
|
||||
name: "access key id",
|
||||
name: "with credentials config",
|
||||
opts: []AwsCredentialPersistedStateOption{
|
||||
WithAccessKeyId("foobar"),
|
||||
WithCredentialsConfig(
|
||||
&awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
),
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "secret access key",
|
||||
opts: []AwsCredentialPersistedStateOption{
|
||||
WithSecretAccessKey("bazqux"),
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -63,29 +64,12 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "region key",
|
||||
name: "double set credentials config",
|
||||
opts: []AwsCredentialPersistedStateOption{
|
||||
WithRegion("foobar"),
|
||||
WithCredentialsConfig(&awsutil.CredentialsConfig{}),
|
||||
WithCredentialsConfig(&awsutil.CredentialsConfig{}),
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
region: "foobar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "double set access key id",
|
||||
opts: []AwsCredentialPersistedStateOption{
|
||||
WithAccessKeyId("foobar"),
|
||||
WithAccessKeyId("onetwo"),
|
||||
},
|
||||
expectedErr: "access key id already set",
|
||||
},
|
||||
{
|
||||
name: "double set secret access key",
|
||||
opts: []AwsCredentialPersistedStateOption{
|
||||
WithSecretAccessKey("bazqux"),
|
||||
WithSecretAccessKey("threefour"),
|
||||
},
|
||||
expectedErr: "secret access key already set",
|
||||
expectedErr: "credentials config already set",
|
||||
},
|
||||
{
|
||||
name: "double set rotation time",
|
||||
@@ -95,14 +79,6 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
|
||||
},
|
||||
expectedErr: "last rotation time already set",
|
||||
},
|
||||
{
|
||||
name: "double set region",
|
||||
opts: []AwsCredentialPersistedStateOption{
|
||||
WithRegion("foobar"),
|
||||
WithRegion("barfoo"),
|
||||
},
|
||||
expectedErr: "region already set",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@@ -116,7 +92,13 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Equal(tc.expected, actual)
|
||||
require.Equal(tc.expected.CredsLastRotatedTime, actual.CredsLastRotatedTime)
|
||||
if tc.expected.CredentialsConfig != nil {
|
||||
require.NotNil(actual.CredentialsConfig)
|
||||
require.Equal(tc.expected.CredentialsConfig.AccessKey, actual.CredentialsConfig.AccessKey)
|
||||
require.Equal(tc.expected.CredentialsConfig.SecretKey, actual.CredentialsConfig.SecretKey)
|
||||
require.Equal(tc.expected.CredentialsConfig.Region, actual.CredentialsConfig.Region)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -124,43 +106,95 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
|
||||
func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in map[string]any
|
||||
secrets *structpb.Struct
|
||||
attrs *CredentialAttributes
|
||||
opts []AwsCredentialPersistedStateOption
|
||||
expected *AwsCredentialPersistedState
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "no secrets",
|
||||
in: nil,
|
||||
expectedErr: "missing persisted secrets",
|
||||
name: "missing credential attributes",
|
||||
expectedErr: "missing credential attributes",
|
||||
},
|
||||
{
|
||||
name: "missing access key ID",
|
||||
in: map[string]any{},
|
||||
expectedErr: "persisted state integrity error: missing required value \"access_key_id\"",
|
||||
},
|
||||
{
|
||||
name: "missing secret access key",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
name: "missing secrets",
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing long-term access key id",
|
||||
secrets: &structpb.Struct{},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing long-term secret access key",
|
||||
secrets: &structpb.Struct{},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with long-term static credentials",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
Region: "us-west-2",
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
},
|
||||
expectedErr: "persisted state integrity error: missing required value \"secret_access_key\"",
|
||||
},
|
||||
{
|
||||
name: "bad last rotated time",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
ConstSecretAccessKey: "bazqux",
|
||||
ConstCredsLastRotatedTime: "notatime",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
ConstCredsLastRotatedTime: structpb.NewStringValue("notatime"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expectedErr: "persisted state integrity error: could not parse time in value \"creds_last_rotated_time\": parsing time \"notatime\" as \"2006-01-02T15:04:05.999999999Z07:00\": cannot parse \"notatime\" as \"2006\"",
|
||||
},
|
||||
{
|
||||
name: "option error",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
ConstSecretAccessKey: "bazqux",
|
||||
ConstCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
opts: []AwsCredentialPersistedStateOption{
|
||||
func(s *AwsCredentialPersistedState) error {
|
||||
@@ -171,14 +205,22 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "good with non-zero timestamp",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
ConstSecretAccessKey: "bazqux",
|
||||
ConstCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
CredsLastRotatedTime: func() time.Time {
|
||||
t, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00")
|
||||
if err != nil {
|
||||
@@ -190,33 +232,79 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "good with zero timestamp",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
ConstSecretAccessKey: "bazqux",
|
||||
ConstCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
ConstCredsLastRotatedTime: structpb.NewStringValue((time.Time{}).Format(time.RFC3339Nano)),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
CredsLastRotatedTime: time.Time{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "good (ignoring non-test options)",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
ConstSecretAccessKey: "bazqux",
|
||||
ConstCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
|
||||
},
|
||||
opts: []AwsCredentialPersistedStateOption{
|
||||
WithAccessKeyId("ignored"),
|
||||
WithRegion("us-west-2"),
|
||||
name: "with assume role",
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
Region: "us-west-2",
|
||||
RoleARN: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with assume role & static credentials",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
ConstCredsLastRotatedTime: structpb.NewStringValue((time.Time{}).Format(time.RFC3339Nano)),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
RoleARN: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
CredsLastRotatedTime: time.Time{},
|
||||
region: "us-west-2",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -226,55 +314,67 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
var (
|
||||
input *structpb.Struct
|
||||
err error
|
||||
)
|
||||
if tc.in != nil {
|
||||
input, err = structpb.NewStruct(tc.in)
|
||||
require.NoError(err)
|
||||
}
|
||||
|
||||
actual, err := AwsCredentialPersistedStateFromProto(input, tc.opts...)
|
||||
actual, err := AwsCredentialPersistedStateFromProto(tc.secrets, tc.attrs, tc.opts...)
|
||||
if tc.expectedErr != "" {
|
||||
require.EqualError(err, tc.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Equal(tc.expected.AccessKeyId, actual.AccessKeyId)
|
||||
require.Equal(tc.expected.SecretAccessKey, actual.SecretAccessKey)
|
||||
require.Equal(tc.expected.region, actual.region)
|
||||
require.NotNil(actual)
|
||||
require.Equal(tc.expected.CredsLastRotatedTime, actual.CredsLastRotatedTime)
|
||||
if tc.expected.CredentialsConfig != nil {
|
||||
require.NotNil(actual.CredentialsConfig)
|
||||
require.Equal(tc.expected.CredentialsConfig.AccessKey, actual.CredentialsConfig.AccessKey)
|
||||
require.Equal(tc.expected.CredentialsConfig.SecretKey, actual.CredentialsConfig.SecretKey)
|
||||
require.Equal(tc.expected.CredentialsConfig.Region, actual.CredentialsConfig.Region)
|
||||
require.Equal(tc.expected.CredentialsConfig.RoleARN, actual.CredentialsConfig.RoleARN)
|
||||
require.Equal(tc.expected.CredentialsConfig.RoleExternalId, actual.CredentialsConfig.RoleExternalId)
|
||||
require.Equal(tc.expected.CredentialsConfig.RoleSessionName, actual.CredentialsConfig.RoleSessionName)
|
||||
require.Equal(tc.expected.CredentialsConfig.RoleTags, actual.CredentialsConfig.RoleTags)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAwsCatalogPersistedStateValidateCreds(t *testing.T) {
|
||||
func TestAwsCatalogPersistedState_ValidateCreds(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in *AwsCredentialPersistedState
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "could not load credentials",
|
||||
in: &AwsCredentialPersistedState{
|
||||
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
|
||||
},
|
||||
expectedErr: fmt.Sprintf("error loading credentials: error reading options in NewCredentialsConfig: %s", testOptionErr),
|
||||
name: "missing credentials config",
|
||||
in: &AwsCredentialPersistedState{},
|
||||
expectedErr: "missing credentials config",
|
||||
},
|
||||
{
|
||||
name: "validation error",
|
||||
in: &AwsCredentialPersistedState{
|
||||
testOpts: []awsutil.Option{awsutil.WithSTSAPIFunc(awsutil.NewMockSTS(awsutil.WithGetCallerIdentityError(errors.New(testGetCallerIdentityErr))))},
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithSTSAPIFunc(
|
||||
awsutil.NewMockSTS(
|
||||
awsutil.WithGetCallerIdentityError(errors.New(testGetCallerIdentityErr)),
|
||||
),
|
||||
),
|
||||
},
|
||||
},
|
||||
expectedErr: fmt.Sprintf("error validating credentials: %s", testGetCallerIdentityErr),
|
||||
},
|
||||
{
|
||||
name: "good",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
testOpts: []awsutil.Option{awsutil.WithSTSAPIFunc(awsutil.NewMockSTS())},
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
testOpts: []awsutil.Option{awsutil.WithSTSAPIFunc(awsutil.NewMockSTS())},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -294,27 +394,27 @@ func TestAwsCatalogPersistedStateValidateCreds(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) {
|
||||
func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in *AwsCredentialPersistedState
|
||||
expectedAccessKeyId string
|
||||
expectedSecretAccessKey string
|
||||
exepected *awsutil.CredentialsConfig
|
||||
expectedNonZeroRotatedTime bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "could not load credentials",
|
||||
in: &AwsCredentialPersistedState{
|
||||
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
|
||||
},
|
||||
expectedErr: fmt.Sprintf("error loading credentials: error reading options in NewCredentialsConfig: %s", testOptionErr),
|
||||
name: "missing credentials config",
|
||||
in: &AwsCredentialPersistedState{},
|
||||
expectedErr: "missing credentials config",
|
||||
},
|
||||
{
|
||||
name: "rotation error",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
awsutil.NewMockIAM(
|
||||
@@ -328,8 +428,11 @@ func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) {
|
||||
{
|
||||
name: "good",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithSTSAPIFunc(
|
||||
awsutil.NewMockSTS(),
|
||||
@@ -358,8 +461,11 @@ func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) {
|
||||
),
|
||||
},
|
||||
},
|
||||
expectedAccessKeyId: "one",
|
||||
expectedSecretAccessKey: "two",
|
||||
exepected: &awsutil.CredentialsConfig{
|
||||
AccessKey: "one",
|
||||
SecretKey: "two",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expectedNonZeroRotatedTime: true,
|
||||
},
|
||||
}
|
||||
@@ -375,52 +481,42 @@ func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) {
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Equal(tc.expectedAccessKeyId, tc.in.AccessKeyId)
|
||||
require.Equal(tc.expectedSecretAccessKey, tc.in.SecretAccessKey)
|
||||
require.Equal(tc.exepected.AccessKey, tc.in.CredentialsConfig.AccessKey)
|
||||
require.Equal(tc.exepected.SecretKey, tc.in.CredentialsConfig.SecretKey)
|
||||
require.Equal(tc.expectedNonZeroRotatedTime, !tc.in.CredsLastRotatedTime.IsZero())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAwsCatalogPersistedStateReplaceCreds(t *testing.T) {
|
||||
func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) {
|
||||
// NOTE: Not safe to run this test in parallel
|
||||
state := new(testMockIAMState)
|
||||
cases := []struct {
|
||||
name string
|
||||
in *AwsCredentialPersistedState
|
||||
accessKeyId string
|
||||
secretAccessKey string
|
||||
expectedAccessKeyId string
|
||||
expectedSecretAccessKey string
|
||||
expectedDeleteCalled bool
|
||||
expectedErr string
|
||||
name string
|
||||
in *AwsCredentialPersistedState
|
||||
credentialConfig *awsutil.CredentialsConfig
|
||||
expected *awsutil.CredentialsConfig
|
||||
expectedDeleteCalled bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "missing access key id",
|
||||
name: "missing new credentials config",
|
||||
in: &AwsCredentialPersistedState{},
|
||||
expectedErr: "access key id cannot be empty",
|
||||
expectedErr: "missing new credentials config",
|
||||
},
|
||||
{
|
||||
name: "missing access key id",
|
||||
in: &AwsCredentialPersistedState{},
|
||||
accessKeyId: "one",
|
||||
expectedErr: "secret access key cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "identical access key id",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
},
|
||||
accessKeyId: "foobar",
|
||||
secretAccessKey: "bazqux",
|
||||
expectedErr: "attempting to replace access key with the same one",
|
||||
name: "missing credentials config",
|
||||
in: &AwsCredentialPersistedState{},
|
||||
credentialConfig: &awsutil.CredentialsConfig{},
|
||||
expectedErr: "missing credentials config",
|
||||
},
|
||||
{
|
||||
name: "deletion error",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
CredsLastRotatedTime: time.Now(),
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
@@ -430,15 +526,16 @@ func TestAwsCatalogPersistedStateReplaceCreds(t *testing.T) {
|
||||
),
|
||||
},
|
||||
},
|
||||
accessKeyId: "one",
|
||||
secretAccessKey: "two",
|
||||
expectedErr: fmt.Sprintf("error deleting old access key: %s", testDeleteAccessKeyErr),
|
||||
credentialConfig: &awsutil.CredentialsConfig{},
|
||||
expectedErr: fmt.Sprintf("error deleting old access key: %s", testDeleteAccessKeyErr),
|
||||
},
|
||||
{
|
||||
name: "good with delete of old rotated",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
CredsLastRotatedTime: time.Now(),
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
@@ -446,28 +543,38 @@ func TestAwsCatalogPersistedStateReplaceCreds(t *testing.T) {
|
||||
),
|
||||
},
|
||||
},
|
||||
accessKeyId: "one",
|
||||
secretAccessKey: "two",
|
||||
expectedAccessKeyId: "one",
|
||||
expectedSecretAccessKey: "two",
|
||||
expectedDeleteCalled: true,
|
||||
credentialConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "one",
|
||||
SecretKey: "two",
|
||||
},
|
||||
expected: &awsutil.CredentialsConfig{
|
||||
AccessKey: "one",
|
||||
SecretKey: "two",
|
||||
},
|
||||
expectedDeleteCalled: true,
|
||||
},
|
||||
{
|
||||
name: "good without delete of old rotated",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
newTestMockIAM(state),
|
||||
),
|
||||
},
|
||||
},
|
||||
accessKeyId: "one",
|
||||
secretAccessKey: "two",
|
||||
expectedAccessKeyId: "one",
|
||||
expectedSecretAccessKey: "two",
|
||||
expectedDeleteCalled: false,
|
||||
credentialConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "one",
|
||||
SecretKey: "two",
|
||||
},
|
||||
expected: &awsutil.CredentialsConfig{
|
||||
AccessKey: "one",
|
||||
SecretKey: "two",
|
||||
},
|
||||
expectedDeleteCalled: false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -476,39 +583,39 @@ func TestAwsCatalogPersistedStateReplaceCreds(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
state.Reset()
|
||||
err := tc.in.ReplaceCreds(tc.accessKeyId, tc.secretAccessKey)
|
||||
err := tc.in.ReplaceCreds(tc.credentialConfig)
|
||||
if tc.expectedErr != "" {
|
||||
require.EqualError(err, tc.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Equal(tc.expectedAccessKeyId, tc.in.AccessKeyId)
|
||||
require.Equal(tc.expectedSecretAccessKey, tc.in.SecretAccessKey)
|
||||
require.Equal(tc.expected.AccessKey, tc.in.CredentialsConfig.AccessKey)
|
||||
require.Equal(tc.expected.SecretKey, tc.in.CredentialsConfig.SecretKey)
|
||||
require.Zero(tc.in.CredsLastRotatedTime)
|
||||
require.Equal(tc.expectedDeleteCalled, state.DeleteAccessKeyCalled)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAwsCatalogPersistedStateDeleteCreds(t *testing.T) {
|
||||
func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in *AwsCredentialPersistedState
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "could not load credentials",
|
||||
in: &AwsCredentialPersistedState{
|
||||
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
|
||||
},
|
||||
expectedErr: fmt.Sprintf("error loading credentials: error reading options in NewCredentialsConfig: %s", testOptionErr),
|
||||
name: "missing credentials config",
|
||||
in: &AwsCredentialPersistedState{},
|
||||
expectedErr: "missing credentials config",
|
||||
},
|
||||
{
|
||||
name: "deletion error",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
awsutil.NewMockIAM(
|
||||
@@ -522,8 +629,10 @@ func TestAwsCatalogPersistedStateDeleteCreds(t *testing.T) {
|
||||
{
|
||||
name: "deletion error, but OK because key was just gone",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
awsutil.NewMockIAM(
|
||||
@@ -538,8 +647,10 @@ func TestAwsCatalogPersistedStateDeleteCreds(t *testing.T) {
|
||||
{
|
||||
name: "good",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
awsutil.NewMockIAM(),
|
||||
@@ -560,19 +671,149 @@ func TestAwsCatalogPersistedStateDeleteCreds(t *testing.T) {
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Empty(tc.in.AccessKeyId)
|
||||
require.Empty(tc.in.SecretAccessKey)
|
||||
require.Nil(tc.in.CredentialsConfig)
|
||||
require.Zero(tc.in.CredsLastRotatedTime)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAwsCatalogPersistedStateGetSessionErr(t *testing.T) {
|
||||
func TestAwsCatalogPersistedState_GetSession(t *testing.T) {
|
||||
require := require.New(t)
|
||||
state := &AwsCredentialPersistedState{
|
||||
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
|
||||
cases := []struct {
|
||||
name string
|
||||
in *AwsCredentialPersistedState
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "error",
|
||||
in: &AwsCredentialPersistedState{
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
|
||||
},
|
||||
expectedErr: fmt.Sprintf("error reading options in GetSession: %s", testOptionErr),
|
||||
},
|
||||
{
|
||||
name: "static credentials",
|
||||
in: &AwsCredentialPersistedState{
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "AKIA_foobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
awsutil.NewMockIAM(),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "assume role",
|
||||
in: &AwsCredentialPersistedState{
|
||||
CredentialsConfig: func() *awsutil.CredentialsConfig {
|
||||
config, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithRegion("us-west-2"),
|
||||
awsutil.WithRoleArn("arn:aws:iam::123456789012:role/S3Access"),
|
||||
awsutil.WithRoleExternalId("1234567890"),
|
||||
awsutil.WithRoleSessionName("assume-role"),
|
||||
awsutil.WithRoleTags(map[string]string{
|
||||
"foo": "bar",
|
||||
}),
|
||||
)
|
||||
require.NoError(err)
|
||||
return config
|
||||
}(),
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
awsutil.NewMockIAM(),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := tc.in.GetSession()
|
||||
if tc.expectedErr != "" {
|
||||
require.EqualError(err, tc.expectedErr)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasStaticCredentials(t *testing.T) {
|
||||
require := require.New(t)
|
||||
cases := []struct {
|
||||
name string
|
||||
accessKey string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "dynamic credential",
|
||||
accessKey: "ASIAEXAMPLE",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "static credential",
|
||||
accessKey: "AKIAEXAMPLE",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "other credential",
|
||||
accessKey: "EXAMPLE",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
accessKey: "",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(tc.expected, HasStaticCredentials(tc.accessKey))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasDynamicCredentials(t *testing.T) {
|
||||
require := require.New(t)
|
||||
cases := []struct {
|
||||
name string
|
||||
accessKey string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "dynamic credential",
|
||||
accessKey: "ASIAEXAMPLE",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "static credential",
|
||||
accessKey: "AKIAEXAMPLE",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "other credential",
|
||||
accessKey: "EXAMPLE",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
accessKey: "",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(tc.expected, HasDynamicCredentials(tc.accessKey))
|
||||
})
|
||||
}
|
||||
|
||||
_, err := state.GetSession()
|
||||
require.EqualError(err, fmt.Sprintf("error reading options in NewCredentialsConfig: %s", testOptionErr))
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go/service/iam"
|
||||
"github.com/aws/aws-sdk-go/service/iam/iamiface"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -48,3 +49,39 @@ func newTestMockIAM(state *testMockIAMState, opts ...awsutil.MockIAMOption) awsu
|
||||
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"),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,33 @@ func GetTimeValue(in *structpb.Struct, k string) (time.Time, error) {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// GetMapStringString returns a map[string]string value and no error if the given key
|
||||
// is found in the provided proto struct input. An error is returned if the key
|
||||
// is not found or the value type is not map[string]string.
|
||||
func GetMapStringString(in *structpb.Struct, k string, required bool) (map[string]string, error) {
|
||||
mv := in.GetFields()
|
||||
v, ok := mv[k]
|
||||
if !ok {
|
||||
if required {
|
||||
return nil, fmt.Errorf("missing required value %q", k)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
result := map[string]string{}
|
||||
m, ok := v.AsInterface().(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type for value %q: want map[string]string, got %T", k, v.AsInterface())
|
||||
}
|
||||
for mapKey, mapValue := range m {
|
||||
e, ok := mapValue.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type for value in map[%q]: want string, got %T", mapKey, mapValue)
|
||||
}
|
||||
result[mapKey] = e
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// StructFields returns a map[string]struct{} of the
|
||||
// proto struct input.
|
||||
func StructFields(s *structpb.Struct) map[string]struct{} {
|
||||
|
||||
@@ -208,3 +208,95 @@ func TestGetTimeValue(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMapStringString(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in *structpb.Struct
|
||||
key string
|
||||
required bool
|
||||
expected map[string]string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "required missing",
|
||||
in: &structpb.Struct{},
|
||||
key: "foo",
|
||||
required: true,
|
||||
expectedErr: "missing required value \"foo\"",
|
||||
},
|
||||
{
|
||||
name: "optional missing",
|
||||
in: &structpb.Struct{},
|
||||
key: "foo",
|
||||
},
|
||||
{
|
||||
name: "non-map[string]string value",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewBoolValue(true),
|
||||
},
|
||||
},
|
||||
key: "foo",
|
||||
expectedErr: "unexpected type for value \"foo\": want map[string]string, got bool",
|
||||
},
|
||||
{
|
||||
name: "empty map",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStructValue(&structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{},
|
||||
}),
|
||||
},
|
||||
},
|
||||
key: "foo",
|
||||
expected: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "non-string value in map",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStructValue(&structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"bar": structpb.NewBoolValue(true),
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
key: "foo",
|
||||
expectedErr: "unexpected type for value in map[\"bar\"]: want string, got bool",
|
||||
},
|
||||
{
|
||||
name: "good",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStructValue(&structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"bar": structpb.NewStringValue("zoo"),
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
key: "foo",
|
||||
expected: map[string]string{
|
||||
"bar": "zoo",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
actual, err := GetMapStringString(tc.in, tc.key, tc.required)
|
||||
if tc.expectedErr != "" {
|
||||
require.EqualError(err, tc.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Equal(tc.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -105,18 +105,19 @@ a configured IAM user for this provider:
|
||||
The following `attributes` are valid on an AWS host catalog resource:
|
||||
|
||||
- `disable_credential_rotation` (bool): If `true`, credential rotation will not
|
||||
be performed. See the [Credential Rotation](../../../README.md#credential-rotation) section
|
||||
above.
|
||||
be performed. See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
|
||||
- `region` (string): The region to configure the host catalog for. All host sets
|
||||
in this catalog will be configured for this region.
|
||||
|
||||
The following `secrets` are valid on an AWS host catalog resource:
|
||||
The following `secrets` are required on an AWS host catalog resource:
|
||||
|
||||
- `access_key_id` (string): The access key ID for the IAM user to use with this
|
||||
host catalog.
|
||||
- `secret_access_key` (string): The secret access key for the IAM user to use
|
||||
with this host catalog.
|
||||
|
||||
See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
|
||||
|
||||
### Host Set
|
||||
|
||||
The following attributes are valid on an AWS host Set resource:
|
||||
|
||||
@@ -38,6 +38,14 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
|
||||
continue
|
||||
case cred.ConstDisableCredentialRotation:
|
||||
continue
|
||||
case cred.ConstRoleArn:
|
||||
continue
|
||||
case cred.ConstRoleExternalId:
|
||||
continue
|
||||
case cred.ConstRoleSessionName:
|
||||
continue
|
||||
case cred.ConstRoleTags:
|
||||
continue
|
||||
default:
|
||||
badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field"
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ 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"
|
||||
@@ -20,36 +21,34 @@ import (
|
||||
func TestGetCatalogAttributes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in map[string]any
|
||||
in *structpb.Struct
|
||||
expected *CatalogAttributes
|
||||
expectedErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing region",
|
||||
in: map[string]any{},
|
||||
name: "missing region",
|
||||
in: &structpb.Struct{
|
||||
Fields: make(map[string]*structpb.Value),
|
||||
},
|
||||
expectedErrContains: "missing required value \"region\"",
|
||||
},
|
||||
{
|
||||
name: "bad value for disable_credential_rotation",
|
||||
in: map[string]any{
|
||||
credential.ConstRegion: "us-west-2",
|
||||
credential.ConstDisableCredentialRotation: "sure",
|
||||
},
|
||||
expectedErrContains: "unexpected type for value \"disable_credential_rotation\": want bool, got string",
|
||||
},
|
||||
{
|
||||
name: "unknown fields",
|
||||
in: map[string]any{
|
||||
credential.ConstRegion: "us-west-2",
|
||||
"foo": true,
|
||||
"bar": true,
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
"foo": structpb.NewBoolValue(true),
|
||||
"bar": structpb.NewBoolValue(true),
|
||||
},
|
||||
},
|
||||
expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
|
||||
},
|
||||
{
|
||||
name: "default",
|
||||
in: map[string]any{
|
||||
credential.ConstRegion: "us-west-2",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
},
|
||||
},
|
||||
expected: &CatalogAttributes{
|
||||
CredentialAttributes: &credential.CredentialAttributes{
|
||||
@@ -59,15 +58,31 @@ func TestGetCatalogAttributes(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with disable_credential_rotation",
|
||||
in: map[string]any{
|
||||
credential.ConstRegion: "us-west-2",
|
||||
credential.ConstDisableCredentialRotation: true,
|
||||
name: "credential attributes",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
"disable_credential_rotation": structpb.NewBoolValue(true),
|
||||
"role_arn": structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
|
||||
"role_external_id": structpb.NewStringValue("1234567890"),
|
||||
"role_session_name": structpb.NewStringValue("test-session"),
|
||||
"role_tags": structpb.NewStructValue(&structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStringValue("bar"),
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
expected: &CatalogAttributes{
|
||||
CredentialAttributes: &credential.CredentialAttributes{
|
||||
CredentialAttributes: &cred.CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
DisableCredentialRotation: true,
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -78,10 +93,7 @@ func TestGetCatalogAttributes(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
input, err := structpb.NewStruct(tc.in)
|
||||
require.NoError(err)
|
||||
|
||||
actual, err := getCatalogAttributes(input)
|
||||
actual, err := getCatalogAttributes(tc.in)
|
||||
if tc.expectedErrContains != "" {
|
||||
require.Error(err)
|
||||
require.Contains(err.Error(), tc.expectedErrContains)
|
||||
|
||||
@@ -59,7 +59,7 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
credConfig, err := cred.GetCredentialsConfig(secrets, catalogAttributes.Region)
|
||||
credConfig, err := cred.GetCredentialsConfig(secrets, catalogAttributes.CredentialAttributes, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -67,9 +67,7 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
|
||||
// Set up the creds in our persisted state.
|
||||
credState, err := credential.NewAwsCredentialPersistedState(
|
||||
append([]credential.AwsCredentialPersistedStateOption{
|
||||
credential.WithAccessKeyId(credConfig.AccessKey),
|
||||
credential.WithSecretAccessKey(credConfig.SecretKey),
|
||||
credential.WithRegion(credConfig.Region),
|
||||
credential.WithCredentialsConfig(credConfig),
|
||||
}, p.testCredStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -85,15 +83,17 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
|
||||
}
|
||||
|
||||
// Try to rotate the credentials if we're not skipping them.
|
||||
if !catalogAttributes.DisableCredentialRotation {
|
||||
if err := credState.RotateCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||
}
|
||||
} else {
|
||||
// Simply validate if we aren't rotating.
|
||||
if err := credState.ValidateCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential validation: %s", err)
|
||||
// Try to rotate static credentials
|
||||
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,8 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
|
||||
// is fine and important, but this may change in the future.
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetSecrets(),
|
||||
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
|
||||
catalogAttributes.CredentialAttributes,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
@@ -159,38 +160,39 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
if catalogAttributes.DisableCredentialRotation && !updateSecrets {
|
||||
// This is a validate check to make sure that we aren't disabling
|
||||
// rotation for credentials currently being managed by rotation.
|
||||
// This is not allowed.
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
return nil, status.Error(codes.FailedPrecondition, "cannot disable rotation for already-rotated credentials")
|
||||
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
|
||||
if catalogAttributes.DisableCredentialRotation && !updateSecrets {
|
||||
// This is a validate check to make sure that we aren't disabling
|
||||
// rotation for credentials currently being managed by rotation.
|
||||
// This is not allowed.
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
return nil, status.Error(codes.FailedPrecondition, "cannot disable rotation for already-rotated credentials")
|
||||
}
|
||||
}
|
||||
|
||||
if updateSecrets {
|
||||
catalogSecrets, err := credential.GetCredentialsConfig(secrets, catalogAttributes.CredentialAttributes, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Replace the credentials. This checks the timestamp on the last
|
||||
// rotation time as well and deletes the credentials if we are
|
||||
// managing them (ie: if we've rotated them before).
|
||||
if err := credState.ReplaceCreds(catalogSecrets); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !catalogAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
|
||||
// If we're enabling rotation now but didn't before, or have
|
||||
// freshly replaced credentials, we can rotate here.
|
||||
if err := credState.RotateCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if updateSecrets {
|
||||
catalogSecrets, err := credential.GetCredentialsConfig(secrets, catalogAttributes.Region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Replace the credentials. This checks the timestamp on the last
|
||||
// rotation time as well and deletes the credentials if we are
|
||||
// managing them (ie: if we've rotated them before).
|
||||
if err := credState.ReplaceCreds(catalogSecrets.AccessKey, catalogSecrets.SecretKey); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !catalogAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
|
||||
// If we're enabling rotation now but didn't before, or have
|
||||
// freshly replaced credentials, we can rotate here.
|
||||
if err := credState.RotateCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// That's it!
|
||||
persistedProto, err := catalogState.toProto()
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
@@ -226,7 +228,8 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo
|
||||
// may be corrupt/and or legitimately missing.
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetSecrets(),
|
||||
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
|
||||
catalogAttributes.CredentialAttributes,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
@@ -240,12 +243,15 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
// Delete old/existing credentials. This is done with the same
|
||||
// credentials to ensure that it has the proper permissions to do
|
||||
// it.
|
||||
if err := credState.DeleteCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during catalog deletion: %s", err)
|
||||
// try to delete static credentials
|
||||
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
// Delete old/existing credentials. This is done with the same
|
||||
// credentials to ensure that it has the proper permissions to do
|
||||
// it.
|
||||
if err := credState.DeleteCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during catalog deletion: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +304,8 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
|
||||
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetSecrets(),
|
||||
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
|
||||
catalogAttributes.CredentialAttributes,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
@@ -371,7 +378,8 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
|
||||
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetSecrets(),
|
||||
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
|
||||
catalogAttributes.CredentialAttributes,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
@@ -451,7 +459,10 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(req.GetPersisted().GetSecrets(), p.testCredStateOpts...)
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetSecrets(),
|
||||
catalogAttributes.CredentialAttributes,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
@@ -39,14 +39,6 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
|
||||
expectedErrContains: "catalog is nil",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil secrets",
|
||||
req: &pb.OnCreateCatalogRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{},
|
||||
},
|
||||
expectedErrContains: "secrets are required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil attributes",
|
||||
req: &pb.OnCreateCatalogRequest{
|
||||
@@ -93,7 +85,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
@@ -114,7 +106,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
@@ -140,7 +132,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
@@ -170,7 +162,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
@@ -281,7 +273,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -312,7 +304,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -338,7 +330,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -354,7 +346,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
|
||||
NewCatalog: &hostcatalogs.HostCatalog{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("onetwo"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_onetwo"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("threefour"),
|
||||
},
|
||||
},
|
||||
@@ -370,7 +362,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -404,7 +396,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
@@ -463,7 +455,7 @@ func TestPluginOnDeleteCatalogErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -493,7 +485,7 @@ func TestPluginOnDeleteCatalogErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -598,7 +590,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -628,7 +620,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -653,7 +645,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -679,7 +671,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -714,7 +706,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -741,7 +733,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
|
||||
awsutil.MockOptionErr(errors.New(testOptionErr)),
|
||||
}),
|
||||
},
|
||||
expectedErrContains: fmt.Sprintf("error getting EC2 client: error getting AWS session when fetching EC2 client: error reading options in NewCredentialsConfig: %s", testOptionErr),
|
||||
expectedErrContains: fmt.Sprintf("error loading persisted state: error reading options in NewCredentialsConfig: %s", testOptionErr),
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
@@ -759,7 +751,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -805,7 +797,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -850,7 +842,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -967,7 +959,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -997,7 +989,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1022,7 +1014,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1048,7 +1040,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1083,7 +1075,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1110,7 +1102,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
|
||||
awsutil.MockOptionErr(errors.New(testOptionErr)),
|
||||
}),
|
||||
},
|
||||
expectedErrContains: fmt.Sprintf("error getting EC2 client: error getting AWS session when fetching EC2 client: error reading options in NewCredentialsConfig: %s", testOptionErr),
|
||||
expectedErrContains: fmt.Sprintf("error loading persisted state: error reading options in NewCredentialsConfig: %s", testOptionErr),
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
@@ -1128,7 +1120,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1174,7 +1166,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1291,7 +1283,7 @@ func TestPluginListHostsErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1321,7 +1313,7 @@ func TestPluginListHostsErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1346,7 +1338,7 @@ func TestPluginListHostsErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1372,7 +1364,7 @@ func TestPluginListHostsErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1402,7 +1394,7 @@ func TestPluginListHostsErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1440,7 +1432,7 @@ func TestPluginListHostsErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1470,7 +1462,7 @@ func TestPluginListHostsErr(t *testing.T) {
|
||||
awsutil.MockOptionErr(errors.New(testOptionErr)),
|
||||
}),
|
||||
},
|
||||
expectedErrContains: fmt.Sprintf("error getting EC2 client: error getting AWS session when fetching EC2 client: error reading options in NewCredentialsConfig: %s", testOptionErr),
|
||||
expectedErrContains: fmt.Sprintf("error loading persisted state: error reading options in NewCredentialsConfig: %s", testOptionErr),
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
@@ -1488,7 +1480,7 @@ func TestPluginListHostsErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1537,7 +1529,7 @@ func TestPluginListHostsErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
|
||||
@@ -8,15 +8,18 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAwsCatalogPersistedStateEC2Client(t *testing.T) {
|
||||
require := require.New(t)
|
||||
cred, err := credential.NewAwsCredentialPersistedState(
|
||||
credential.WithRegion("us-west-2"),
|
||||
credential.WithAccessKeyId("foobar"),
|
||||
credential.WithSecretAccessKey("barfoo"),
|
||||
credential.WithCredentialsConfig(&awsutil.CredentialsConfig{
|
||||
Region: "us-west-2",
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "barfoo",
|
||||
}),
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ Example policy:
|
||||
```
|
||||
|
||||
* `iam:GetUser`, `iam:CreateAccessKey`, and `iam:DeleteAccessKey`, configured to
|
||||
the IAM user to allow credential rotation. Example policy:
|
||||
the IAM user to allow static credential rotation. Example policy:
|
||||
|
||||
```
|
||||
{
|
||||
@@ -66,18 +66,24 @@ Example policy:
|
||||
The following `attributes` are valid on an AWS storage bucket resource:
|
||||
|
||||
- `disable_credential_rotation` (bool): If `true`, credential rotation will not
|
||||
be performed. See the [Credential Rotation](../../../README.md#credential-rotation) section
|
||||
above.
|
||||
be performed.
|
||||
- `region` (string): The region to configure the storage bucket for.
|
||||
- `endpoint-url` (string): The endpoint to configure the storage.
|
||||
- `role_arn` (string): The role arn configured for the assume role provider.
|
||||
- `role_external_id` (string): The external id configured for the assume role provider.
|
||||
- `role_session_name` (string): The session name configured for the assume role provider.
|
||||
- `role_tags` (object): The key-value pair tags configured for the assume role provider.
|
||||
|
||||
An example of how to utilize the [endpoint attribute](https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-s3.html)
|
||||
See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
|
||||
|
||||
### Secrets
|
||||
|
||||
The following `secrets` are valid on an AWS storage bucket resource:
|
||||
The following `secrets` are optional on an AWS storage bucket resource:
|
||||
|
||||
- `access_key_id` (string): The access key ID for the IAM user to use with this
|
||||
storage bucket.
|
||||
- `secret_access_key` (string): The secret access key for the IAM user to use
|
||||
with this storage bucket.
|
||||
with this storage bucket.
|
||||
|
||||
See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
|
||||
@@ -44,6 +44,14 @@ func getStorageAttributes(in *structpb.Struct) (*StorageAttributes, error) {
|
||||
continue
|
||||
case cred.ConstDisableCredentialRotation:
|
||||
continue
|
||||
case cred.ConstRoleArn:
|
||||
continue
|
||||
case cred.ConstRoleExternalId:
|
||||
continue
|
||||
case cred.ConstRoleSessionName:
|
||||
continue
|
||||
case cred.ConstRoleTags:
|
||||
continue
|
||||
default:
|
||||
badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field"
|
||||
}
|
||||
|
||||
@@ -16,29 +16,35 @@ import (
|
||||
func TestGetStorageAttributes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in map[string]any
|
||||
in *structpb.Struct
|
||||
expected *StorageAttributes
|
||||
expectedErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing region",
|
||||
in: map[string]any{},
|
||||
name: "missing region",
|
||||
in: &structpb.Struct{
|
||||
Fields: make(map[string]*structpb.Value),
|
||||
},
|
||||
expectedErrContains: "missing required value \"region\"",
|
||||
},
|
||||
{
|
||||
name: "unknown fields",
|
||||
in: map[string]any{
|
||||
"region": "us-west-2",
|
||||
"foo": true,
|
||||
"bar": true,
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
"foo": structpb.NewBoolValue(true),
|
||||
"bar": structpb.NewBoolValue(true),
|
||||
},
|
||||
},
|
||||
expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
|
||||
},
|
||||
{
|
||||
name: "good w/ endpoint",
|
||||
in: map[string]any{
|
||||
cred.ConstRegion: "us-west-2",
|
||||
ConstAwsEndpointUrl: "0.0.0.0",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
"endpoint_url": structpb.NewStringValue("0.0.0.0"),
|
||||
},
|
||||
},
|
||||
expected: &StorageAttributes{
|
||||
CredentialAttributes: &cred.CredentialAttributes{
|
||||
@@ -49,8 +55,10 @@ func TestGetStorageAttributes(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "good w/o endpoint",
|
||||
in: map[string]any{
|
||||
cred.ConstRegion: "us-west-2",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
},
|
||||
},
|
||||
expected: &StorageAttributes{
|
||||
CredentialAttributes: &cred.CredentialAttributes{
|
||||
@@ -58,6 +66,35 @@ func TestGetStorageAttributes(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "credential attributes",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
"disable_credential_rotation": structpb.NewBoolValue(true),
|
||||
"role_arn": structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
|
||||
"role_external_id": structpb.NewStringValue("1234567890"),
|
||||
"role_session_name": structpb.NewStringValue("test-session"),
|
||||
"role_tags": structpb.NewStructValue(&structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStringValue("bar"),
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
expected: &StorageAttributes{
|
||||
CredentialAttributes: &cred.CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
DisableCredentialRotation: true,
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@@ -65,10 +102,7 @@ func TestGetStorageAttributes(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
input, err := structpb.NewStruct(tc.in)
|
||||
require.NoError(err)
|
||||
|
||||
actual, err := getStorageAttributes(input)
|
||||
actual, err := getStorageAttributes(tc.in)
|
||||
if tc.expectedErrContains != "" {
|
||||
require.Error(err)
|
||||
require.Contains(err.Error(), tc.expectedErrContains)
|
||||
|
||||
@@ -51,11 +51,6 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre
|
||||
return nil, status.Error(codes.InvalidArgument, "bucketName is required")
|
||||
}
|
||||
|
||||
secrets := bucket.GetSecrets()
|
||||
if secrets == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "secrets is required")
|
||||
}
|
||||
|
||||
attrs := bucket.GetAttributes()
|
||||
if attrs == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "attributes is required")
|
||||
@@ -66,7 +61,7 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -74,24 +69,24 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre
|
||||
// Set up the creds in our persisted state.
|
||||
credState, err := cred.NewAwsCredentialPersistedState(
|
||||
append([]cred.AwsCredentialPersistedStateOption{
|
||||
cred.WithAccessKeyId(credConfig.AccessKey),
|
||||
cred.WithSecretAccessKey(credConfig.SecretKey),
|
||||
cred.WithRegion(credConfig.Region),
|
||||
cred.WithCredentialsConfig(credConfig),
|
||||
}, p.testCredStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
|
||||
}
|
||||
|
||||
// Try to rotate the credentials if we're not skipping them.
|
||||
if !storageAttributes.DisableCredentialRotation {
|
||||
if err := credState.RotateCreds(); err != nil {
|
||||
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)
|
||||
// Try to rotate static credentials
|
||||
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
|
||||
if !storageAttributes.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,39 +155,42 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd
|
||||
// is fine and important, but this may change in the future.
|
||||
credState, err := cred.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetData(),
|
||||
append(p.testCredStateOpts, cred.WithRegion(storageAttributes.Region))...)
|
||||
storageAttributes.CredentialAttributes,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
if storageAttributes.DisableCredentialRotation && !updateSecrets {
|
||||
// This is a validate check to make sure that we aren't disabling
|
||||
// rotation for credentials currently being managed by rotation.
|
||||
// This is not allowed.
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
return nil, status.Error(codes.FailedPrecondition, "cannot disable rotation for already-rotated credentials")
|
||||
}
|
||||
}
|
||||
|
||||
if updateSecrets {
|
||||
storageSecrets, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
|
||||
if storageAttributes.DisableCredentialRotation && !updateSecrets {
|
||||
// This is a validate check to make sure that we aren't disabling
|
||||
// rotation for credentials currently being managed by rotation.
|
||||
// This is not allowed.
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
return nil, status.Error(codes.FailedPrecondition, "cannot disable rotation for already-rotated credentials")
|
||||
}
|
||||
}
|
||||
|
||||
// Replace the credentials. This checks the timestamp on the last
|
||||
// rotation time as well and deletes the credentials if we are
|
||||
// managing them (ie: if we've rotated them before).
|
||||
if err := credState.ReplaceCreds(storageSecrets.AccessKey, storageSecrets.SecretKey); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err)
|
||||
}
|
||||
}
|
||||
if updateSecrets {
|
||||
storageSecrets, err := cred.GetCredentialsConfig(secrets, storageAttributes.CredentialAttributes, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !storageAttributes.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)
|
||||
// Replace the credentials. This checks the timestamp on the last
|
||||
// rotation time as well and deletes the credentials if we are
|
||||
// managing them (ie: if we've rotated them before).
|
||||
if err := credState.ReplaceCreds(storageSecrets); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !storageAttributes.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +243,8 @@ func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDel
|
||||
// may be corrupt/and or legitimately missing.
|
||||
credState, err := cred.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetData(),
|
||||
append(p.testCredStateOpts, cred.WithRegion(storageAttributes.Region))...)
|
||||
storageAttributes.CredentialAttributes,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
@@ -259,12 +258,15 @@ func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDel
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
// Delete old/existing credentials. This is done with the same
|
||||
// credentials to ensure that it has the proper permissions to do
|
||||
// it.
|
||||
if err := credState.DeleteCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during storage bucket deletion: %s", err)
|
||||
// try to delete static credentials
|
||||
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
// Delete old/existing credentials. This is done with the same
|
||||
// credentials to ensure that it has the proper permissions to do
|
||||
// it.
|
||||
if err := credState.DeleteCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during storage bucket deletion: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,11 +288,6 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques
|
||||
return nil, status.Error(codes.InvalidArgument, "bucketName is required")
|
||||
}
|
||||
|
||||
secrets := bucket.GetSecrets()
|
||||
if secrets == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "secrets is required")
|
||||
}
|
||||
|
||||
attrs := bucket.GetAttributes()
|
||||
if attrs == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "attributes is required")
|
||||
@@ -301,7 +298,7 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -309,9 +306,7 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques
|
||||
// Set up the creds in our persisted state.
|
||||
credState, err := cred.NewAwsCredentialPersistedState(
|
||||
append([]cred.AwsCredentialPersistedStateOption{
|
||||
cred.WithAccessKeyId(credConfig.AccessKey),
|
||||
cred.WithSecretAccessKey(credConfig.SecretKey),
|
||||
cred.WithRegion(credConfig.Region),
|
||||
cred.WithCredentialsConfig(credConfig),
|
||||
}, p.testCredStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -364,11 +359,6 @@ func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Validat
|
||||
return nil, status.Error(codes.InvalidArgument, "bucketName is required")
|
||||
}
|
||||
|
||||
secrets := bucket.GetSecrets()
|
||||
if secrets == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "secrets is required")
|
||||
}
|
||||
|
||||
attrs := bucket.GetAttributes()
|
||||
if attrs == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "attributes is required")
|
||||
@@ -379,7 +369,7 @@ func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Validat
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -387,9 +377,7 @@ func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Validat
|
||||
// Set up the creds in our persisted state.
|
||||
credState, err := cred.NewAwsCredentialPersistedState(
|
||||
append([]cred.AwsCredentialPersistedStateOption{
|
||||
cred.WithAccessKeyId(credConfig.AccessKey),
|
||||
cred.WithSecretAccessKey(credConfig.SecretKey),
|
||||
cred.WithRegion(credConfig.Region),
|
||||
cred.WithCredentialsConfig(credConfig),
|
||||
}, p.testCredStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -429,11 +417,6 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu
|
||||
return status.Error(codes.InvalidArgument, "bucketName is required")
|
||||
}
|
||||
|
||||
secrets := bucket.GetSecrets()
|
||||
if secrets == nil {
|
||||
return status.Error(codes.InvalidArgument, "secrets is required")
|
||||
}
|
||||
|
||||
attrs := bucket.GetAttributes()
|
||||
if attrs == nil {
|
||||
return status.Error(codes.InvalidArgument, "attributes is required")
|
||||
@@ -444,7 +427,7 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu
|
||||
return err
|
||||
}
|
||||
|
||||
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -452,9 +435,7 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu
|
||||
// Set up the creds in our persisted state.
|
||||
credState, err := cred.NewAwsCredentialPersistedState(
|
||||
append([]cred.AwsCredentialPersistedStateOption{
|
||||
cred.WithAccessKeyId(credConfig.AccessKey),
|
||||
cred.WithSecretAccessKey(credConfig.SecretKey),
|
||||
cred.WithRegion(credConfig.Region),
|
||||
cred.WithCredentialsConfig(credConfig),
|
||||
}, p.testCredStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -533,10 +514,6 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
|
||||
if bucket.GetBucketName() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "bucketName is required")
|
||||
}
|
||||
secrets := bucket.GetSecrets()
|
||||
if secrets == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "secrets is required")
|
||||
}
|
||||
attrs := bucket.GetAttributes()
|
||||
if attrs == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "attributes is required")
|
||||
@@ -564,7 +541,7 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -572,9 +549,7 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
|
||||
// Set up the creds in our persisted state.
|
||||
credState, err := cred.NewAwsCredentialPersistedState(
|
||||
append([]cred.AwsCredentialPersistedStateOption{
|
||||
cred.WithAccessKeyId(credConfig.AccessKey),
|
||||
cred.WithSecretAccessKey(credConfig.SecretKey),
|
||||
cred.WithRegion(credConfig.Region),
|
||||
cred.WithCredentialsConfig(credConfig),
|
||||
}, p.testCredStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -34,11 +34,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
return &pb.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Secrets: credential.MockStaticCredentialSecrets(),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||
@@ -49,12 +45,13 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
}
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
req *pb.OnCreateStorageBucketRequest
|
||||
credOpts []credential.AwsCredentialPersistedStateOption
|
||||
storageOpts []awsStoragePersistedStateOption
|
||||
expectedErrContains string
|
||||
expectedErrCode codes.Code
|
||||
name string
|
||||
req *pb.OnCreateStorageBucketRequest
|
||||
credOpts []credential.AwsCredentialPersistedStateOption
|
||||
storageOpts []awsStoragePersistedStateOption
|
||||
expectedErrContains string
|
||||
expectedErrCode codes.Code
|
||||
expectedPersistedData map[string]any
|
||||
}{
|
||||
{
|
||||
name: "nil storage bucket",
|
||||
@@ -70,22 +67,11 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
expectedErrContains: "bucketName is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil secrets",
|
||||
req: &pb.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
},
|
||||
},
|
||||
expectedErrContains: "secrets is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil attributes",
|
||||
req: &pb.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: new(structpb.Struct),
|
||||
},
|
||||
},
|
||||
expectedErrContains: "attributes is required",
|
||||
@@ -96,39 +82,17 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
req: &pb.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: new(structpb.Struct),
|
||||
Attributes: new(structpb.Struct),
|
||||
},
|
||||
},
|
||||
expectedErrContains: "missing required value \"region\"",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "error reading secrets",
|
||||
req: &pb.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: new(structpb.Struct),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrContains: "missing required value \"access_key_id\"",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "invalid region",
|
||||
req: &pb.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("foobar"),
|
||||
@@ -156,11 +120,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
req: &pb.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Secrets: credential.MockStaticCredentialSecrets(),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||
@@ -301,6 +261,67 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedPersistedData: credential.MockStaticCredentialSecrets().AsMap(),
|
||||
},
|
||||
{
|
||||
name: "success rotate static creds",
|
||||
req: &pb.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: credential.MockStaticCredentialSecrets(),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(false),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
credOpts: []credential.AwsCredentialPersistedStateOption{
|
||||
credential.WithStateTestOpts(
|
||||
[]awsutil.Option{
|
||||
awsutil.WithSTSAPIFunc(
|
||||
awsutil.NewMockSTS(),
|
||||
),
|
||||
awsutil.WithIAMAPIFunc(
|
||||
awsutil.NewMockIAM(
|
||||
awsutil.WithGetUserOutput(
|
||||
&iam.GetUserOutput{
|
||||
User: &iam.User{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"),
|
||||
UserId: aws.String("AIDAJQABLZS4A3QDU576Q"),
|
||||
UserName: aws.String("JohnDoe"),
|
||||
},
|
||||
},
|
||||
),
|
||||
awsutil.WithCreateAccessKeyOutput(
|
||||
&iam.CreateAccessKeyOutput{
|
||||
AccessKey: &iam.AccessKey{
|
||||
AccessKeyId: aws.String("one"),
|
||||
SecretAccessKey: aws.String("two"),
|
||||
UserName: aws.String("JohnDoe"),
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
storageOpts: []awsStoragePersistedStateOption{
|
||||
withTestS3APIFunc(
|
||||
newTestMockS3(
|
||||
nil,
|
||||
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
|
||||
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
|
||||
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedPersistedData: map[string]any{
|
||||
credential.ConstAccessKeyId: "one",
|
||||
credential.ConstSecretAccessKey: "two",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -322,12 +343,12 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
require.NotNil(resp)
|
||||
require.NotNil(resp.GetPersisted())
|
||||
require.NotNil(resp.GetPersisted().GetData())
|
||||
require.Equal(
|
||||
resp.GetPersisted().GetData().AsMap()[credential.ConstAccessKeyId],
|
||||
tc.req.Bucket.Secrets.AsMap()[credential.ConstAccessKeyId])
|
||||
require.Equal(
|
||||
resp.GetPersisted().GetData().AsMap()[credential.ConstSecretAccessKey],
|
||||
tc.req.Bucket.Secrets.AsMap()[credential.ConstSecretAccessKey])
|
||||
actualPersistedData := resp.GetPersisted().GetData().AsMap()
|
||||
if len(actualPersistedData) != 0 {
|
||||
require.Equal(tc.expectedPersistedData[credential.ConstAccessKeyId], actualPersistedData[credential.ConstAccessKeyId])
|
||||
require.Equal(tc.expectedPersistedData[credential.ConstSecretAccessKey], actualPersistedData[credential.ConstSecretAccessKey])
|
||||
require.NotEmpty(actualPersistedData[credential.ConstCredsLastRotatedTime])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -345,12 +366,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
Data: credential.MockStaticCredentialSecrets(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -428,7 +444,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -458,7 +474,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -468,31 +484,6 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
expectedErrContains: "cannot disable rotation for already-rotated credentials",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
},
|
||||
{
|
||||
name: "error reading secrets",
|
||||
req: &pb.OnUpdateStorageBucketRequest{
|
||||
NewBucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: new(structpb.Struct),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrContains: "missing required value \"access_key_id\"",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "updating secrets, replace error",
|
||||
req: &pb.OnUpdateStorageBucketRequest{
|
||||
@@ -500,7 +491,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
BucketName: "foo",
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("onetwo"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_onetwo"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("threefour"),
|
||||
},
|
||||
},
|
||||
@@ -514,7 +505,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -547,7 +538,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
@@ -671,7 +662,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
@@ -696,7 +687,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
awsutil.WithCreateAccessKeyOutput(
|
||||
&iam.CreateAccessKeyOutput{
|
||||
AccessKey: &iam.AccessKey{
|
||||
AccessKeyId: aws.String("one"),
|
||||
AccessKeyId: aws.String("AKIA_one"),
|
||||
SecretAccessKey: aws.String("two"),
|
||||
UserName: aws.String("JohnDoe"),
|
||||
},
|
||||
@@ -760,7 +751,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
disableCredentialRotation = value.(bool)
|
||||
}
|
||||
if !disableCredentialRotation {
|
||||
expectedAccessKey = "one"
|
||||
expectedAccessKey = "AKIA_one"
|
||||
expectedSecretKey = "two"
|
||||
}
|
||||
require.Equal(expectedAccessKey, resp.GetPersisted().GetData().AsMap()[credential.ConstAccessKeyId])
|
||||
@@ -807,11 +798,7 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) {
|
||||
req: &pb.OnDeleteStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Secrets: credential.MockStaticCredentialSecrets(),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("foobar"),
|
||||
@@ -835,7 +822,7 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) {
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -863,7 +850,7 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) {
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -895,7 +882,7 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) {
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -924,6 +911,7 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) {
|
||||
}
|
||||
resp, err := p.OnDeleteStorageBucket(context.Background(), tc.req)
|
||||
if tc.expectedErrContains != "" {
|
||||
require.NotNil(err)
|
||||
require.Contains(err.Error(), tc.expectedErrContains)
|
||||
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
|
||||
return
|
||||
@@ -940,11 +928,7 @@ func TestStoragePlugin_HeadObject(t *testing.T) {
|
||||
Key: "/foo/bar/key",
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Secrets: credential.MockStaticCredentialSecrets(),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||
@@ -986,17 +970,6 @@ func TestStoragePlugin_HeadObject(t *testing.T) {
|
||||
expectedErrContains: "bucketName is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil secrets",
|
||||
req: &pb.HeadObjectRequest{
|
||||
Key: "/foo/bar/key",
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
},
|
||||
},
|
||||
expectedErrContains: "secrets is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil attributes",
|
||||
req: &pb.HeadObjectRequest{
|
||||
@@ -1140,11 +1113,7 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) {
|
||||
return &pb.ValidatePermissionsRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Secrets: credential.MockStaticCredentialSecrets(),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||
@@ -1175,16 +1144,6 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) {
|
||||
expectedErrContains: "bucketName is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil secrets",
|
||||
req: &pb.ValidatePermissionsRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
},
|
||||
},
|
||||
expectedErrContains: "secrets is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil attributes",
|
||||
req: &pb.ValidatePermissionsRequest{
|
||||
@@ -1383,11 +1342,7 @@ func TestStoragePlugin_GetObject(t *testing.T) {
|
||||
Key: "/foo/bar/key",
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Secrets: credential.MockStaticCredentialSecrets(),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||
@@ -1428,17 +1383,6 @@ func TestStoragePlugin_GetObject(t *testing.T) {
|
||||
expectedErrContains: "bucketName is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil secrets",
|
||||
req: &pb.GetObjectRequest{
|
||||
Key: "/foo/bar/key",
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
},
|
||||
},
|
||||
expectedErrContains: "secrets is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil attributes",
|
||||
req: &pb.GetObjectRequest{
|
||||
@@ -1616,12 +1560,7 @@ func TestStoragePlugin_PutObject(t *testing.T) {
|
||||
return &pb.PutObjectRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "external-obj-store",
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
Secrets: credential.MockStaticCredentialSecrets(),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||
@@ -1671,17 +1610,6 @@ func TestStoragePlugin_PutObject(t *testing.T) {
|
||||
expectedErrContains: "bucketName is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "missing bucket secrets",
|
||||
request: &pb.PutObjectRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "external-obj-store",
|
||||
},
|
||||
Key: "mock-object",
|
||||
},
|
||||
expectedErrContains: "secrets is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "missing attributes",
|
||||
request: &pb.PutObjectRequest{
|
||||
|
||||
@@ -9,35 +9,78 @@ import (
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAwsStoragePersistedStateS3Client(t *testing.T) {
|
||||
require := require.New(t)
|
||||
cred, err := credential.NewAwsCredentialPersistedState(
|
||||
credential.WithRegion("us-west-2"),
|
||||
credential.WithAccessKeyId("foobar"),
|
||||
credential.WithSecretAccessKey("barfoo"),
|
||||
)
|
||||
require.NoError(err)
|
||||
cases := []struct {
|
||||
name string
|
||||
creds *awsutil.CredentialsConfig
|
||||
opts []awsStoragePersistedStateOption
|
||||
awsOpts []awsutil.Option
|
||||
s3Opts []s3Option
|
||||
expectedRegion string
|
||||
expectedEndpoint string
|
||||
}{
|
||||
{
|
||||
name: "static credentials",
|
||||
creds: func() *awsutil.CredentialsConfig {
|
||||
creds, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithRegion("us-west-2"),
|
||||
awsutil.WithAccessKey("foobar"),
|
||||
awsutil.WithSecretKey("barfoo"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
return creds
|
||||
}(),
|
||||
awsOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
awsutil.NewMockIAM(),
|
||||
),
|
||||
},
|
||||
opts: []awsStoragePersistedStateOption{
|
||||
withTestS3APIFunc(newTestMockS3(nil)),
|
||||
},
|
||||
s3Opts: []s3Option{
|
||||
WithRegion("us-west-2"),
|
||||
WithEndpoint("0.0.0.0"),
|
||||
},
|
||||
expectedRegion: "us-west-2",
|
||||
expectedEndpoint: "0.0.0.0",
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
state, err := newAwsStoragePersistedState(
|
||||
withCredentials(cred),
|
||||
withTestS3APIFunc(newTestMockS3(nil)),
|
||||
)
|
||||
require.NoError(err)
|
||||
cred, err := credential.NewAwsCredentialPersistedState(
|
||||
credential.WithCredentialsConfig(tc.creds),
|
||||
credential.WithStateTestOpts(tc.awsOpts),
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
clientRaw, err := state.S3Client(context.Background(), WithRegion("us-west-2"), WithEndpoint("0.0.0.0"))
|
||||
require.NoError(err)
|
||||
require.NotNil(clientRaw)
|
||||
client, ok := clientRaw.(*testMockS3)
|
||||
require.True(ok)
|
||||
require.NotNil(client)
|
||||
require.Equal("us-west-2", client.Region)
|
||||
state, err := newAwsStoragePersistedState(
|
||||
append(tc.opts, withCredentials(cred))...,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
endpoint, err := client.Endpoint.ResolveEndpoint(s3.ServiceID, client.Region)
|
||||
require.NoError(err)
|
||||
require.Equal("0.0.0.0", endpoint.URL)
|
||||
require.Equal("us-west-2", endpoint.SigningRegion)
|
||||
require.Equal("aws", endpoint.PartitionID)
|
||||
clientRaw, err := state.S3Client(context.Background(), tc.s3Opts...)
|
||||
require.NoError(err)
|
||||
require.NotNil(clientRaw)
|
||||
client, ok := clientRaw.(*testMockS3)
|
||||
require.True(ok)
|
||||
require.NotNil(client)
|
||||
require.Equal(tc.expectedRegion, client.Region)
|
||||
|
||||
if tc.expectedEndpoint != "" {
|
||||
endpoint, err := client.Endpoint.ResolveEndpoint(s3.ServiceID, client.Region)
|
||||
require.NoError(err)
|
||||
require.Equal(tc.expectedEndpoint, endpoint.URL)
|
||||
require.Equal(tc.expectedRegion, endpoint.SigningRegion)
|
||||
require.Equal("aws", endpoint.PartitionID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+4
-198
@@ -11,26 +11,20 @@ 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 (
|
||||
expectedIamUserCount = 6
|
||||
expectedEc2InstanceCount = 5
|
||||
)
|
||||
const expectedEc2InstanceCount = 5
|
||||
|
||||
var expectedTags = []string{"foo", "bar", "baz"}
|
||||
|
||||
func TestPlugin(t *testing.T) {
|
||||
func TestHostPlugin(t *testing.T) {
|
||||
region := os.Getenv("AWS_REGION")
|
||||
if region == "" {
|
||||
t.Skip("set AWS_REGION to use this test")
|
||||
@@ -195,36 +189,7 @@ func testPluginOnCreateCatalog(ctx context.Context, t *testing.T, p *host.HostPl
|
||||
require.NotNil(response)
|
||||
persisted := response.GetPersisted()
|
||||
require.NotNil(persisted)
|
||||
persistedSecrets := persisted.GetSecrets()
|
||||
require.NotNil(persistedSecrets)
|
||||
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedAccessKeyId)
|
||||
if rotate {
|
||||
require.NotEqual(accessKeyId, persistedAccessKeyId)
|
||||
} else {
|
||||
require.Equal(accessKeyId, persistedAccessKeyId)
|
||||
}
|
||||
|
||||
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedSecretAccessKey)
|
||||
if rotate {
|
||||
require.NotEqual(secretAccessKey, persistedSecretAccessKey)
|
||||
} else {
|
||||
require.Equal(secretAccessKey, persistedSecretAccessKey)
|
||||
}
|
||||
|
||||
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
|
||||
require.NoError(err)
|
||||
if rotate {
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
requireCredentialsInvalid(t, accessKeyId, secretAccessKey)
|
||||
} else {
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
}
|
||||
|
||||
return persistedAccessKeyId, persistedSecretAccessKey
|
||||
return validatePersistedSecrets(t, persisted.GetSecrets(), accessKeyId, secretAccessKey, rotate)
|
||||
}
|
||||
|
||||
func testPluginOnUpdateCatalog(
|
||||
@@ -293,116 +258,7 @@ func testPluginOnUpdateCatalog(
|
||||
require.NotNil(response)
|
||||
persisted := response.GetPersisted()
|
||||
require.NotNil(persisted)
|
||||
persistedSecrets := persisted.GetSecrets()
|
||||
require.NotNil(persistedSecrets)
|
||||
|
||||
// Complex checks based on the scenarios.
|
||||
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedAccessKeyId)
|
||||
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedSecretAccessKey)
|
||||
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
|
||||
require.NoError(err)
|
||||
|
||||
// Our test scenarios are complex due the multi-dimensional nature
|
||||
// of criteria, so we lay them out in a switch below.
|
||||
switch {
|
||||
case newAccessKeyId != "" && rotated && rotate:
|
||||
// The new access key ID was provided, we had previously rotated
|
||||
// the credentials before, and the new credential set is to be
|
||||
// rotated as well. In this case, the old credentials should have
|
||||
// been deleted, and the new credentials should have been rotated,
|
||||
// hence, should not match the new credentials initially
|
||||
// provided. Rotation time should be non-zero and updated.
|
||||
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
|
||||
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
|
||||
|
||||
case newAccessKeyId != "" && rotated && !rotate:
|
||||
// The new access key ID was provided, we had previously rotated
|
||||
// the credentials before, and the new credential is *not*
|
||||
// rotated. In this case, the old credentials should have
|
||||
// been deleted, But the new credentials should have not been
|
||||
// rotated, and hence should be the same. Rotation time should be
|
||||
// zero.
|
||||
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, newAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
|
||||
case newAccessKeyId != "" && !rotated && rotate:
|
||||
// The new access key ID was provided, we *have not* previously
|
||||
// rotated the credentials, and the new credential set is to be
|
||||
// rotated. In this case, the old credentials should have been
|
||||
// left alone, and the new credentials should have been rotated,
|
||||
// hence, should not match the new credentials initially
|
||||
// provided. Rotation time should be non-zero, but updated.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
|
||||
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
|
||||
|
||||
case newAccessKeyId != "" && !rotated && !rotate:
|
||||
// The new access key ID was provided, but we have not rotated
|
||||
// the credentials previously and we still don't plan on rotating
|
||||
// them. In this case, the old credentials should still be valid,
|
||||
// and the persisted ones should match the new ones provided.
|
||||
// Rotation time should be zero.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, newAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
|
||||
case newAccessKeyId == "" && rotated && rotate:
|
||||
// No new credentials have been provided, but we have previously
|
||||
// rotated and are still rotating credentials. This is a no-op.
|
||||
// Existing credentials should still be valid and match the ones
|
||||
// persisted to state. Rotation time should be identical since
|
||||
// no new rotation occurred.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, currentAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
require.True(currentCredsLastRotatedTime.Equal(persistedCredsLastRotatedTime))
|
||||
|
||||
case newAccessKeyId == "" && rotated && !rotate:
|
||||
// No new credentials have been provided, and we have previously
|
||||
// rotated the credentials. This is actually an error, but we
|
||||
// don't test it here; it's covered in unit testing (see
|
||||
// plugin_test.go).
|
||||
require.FailNow("testing rotated-to-not-rotated scenario not implemented by this helper")
|
||||
|
||||
case newAccessKeyId == "" && !rotated && rotate:
|
||||
// No new credentials have been provided, and while we did not
|
||||
// rotate before, we want to switch to rotation. In this case,
|
||||
// the existing persisted credentials should have been rotated,
|
||||
// with a new non-zero timestamp.
|
||||
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.NotEqual(persistedAccessKeyId, currentAccessKeyId)
|
||||
require.NotEqual(persistedSecretAccessKey, currentSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
|
||||
case newAccessKeyId == "" && !rotated && !rotate:
|
||||
// No new credentials have been provided and we have not, nor do
|
||||
// not, plan on rotating the credentials. This is a no-op.
|
||||
// Existing credentials should still be valid and match the ones
|
||||
// persisted to state. Rotation time should remain at zero.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, currentAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
|
||||
default:
|
||||
// Scenario was reached that was not covered by this function.
|
||||
require.FailNow("unknown test scenario")
|
||||
}
|
||||
|
||||
return persistedAccessKeyId, persistedSecretAccessKey
|
||||
return validateUpdateSecrets(t, persisted.GetSecrets(), currentCredsLastRotatedTime, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey, rotated, rotate)
|
||||
}
|
||||
|
||||
func testPluginOnDeleteCatalog(ctx context.Context, t *testing.T, p *host.HostPlugin, region, accessKeyId, secretAccessKey string, rotated bool) {
|
||||
@@ -588,53 +444,3 @@ func testPluginListHosts(ctx context.Context, t *testing.T, p *host.HostPlugin,
|
||||
// Success
|
||||
t.Logf("testing ListHosts: success (region=%s, tags=%v, expected/actual=(len=%d, ids=%s))", region, tags, len(actualInstances), actualInstances)
|
||||
}
|
||||
|
||||
func requireCredentialsInvalid(t *testing.T, accessKeyId, secretAccessKey string) {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
|
||||
c, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithAccessKey(accessKeyId),
|
||||
awsutil.WithSecretKey(secretAccessKey),
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
// We need to wait for invalidation as while awsutil waits for
|
||||
// credential creation, deletion of the old credentials returns
|
||||
// immediately.
|
||||
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
defer cancel()
|
||||
waitErr:
|
||||
for {
|
||||
_, err = c.GetCallerIdentity()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second):
|
||||
// pass
|
||||
|
||||
case <-timeoutCtx.Done():
|
||||
break waitErr
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(err)
|
||||
awsErr, ok := err.(awserr.Error)
|
||||
require.True(ok)
|
||||
require.Equal("InvalidClientTokenId", awsErr.Code())
|
||||
}
|
||||
|
||||
func requireCredentialsValid(t *testing.T, accessKeyId, secretAccessKey string) {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
|
||||
c, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithAccessKey(accessKeyId),
|
||||
awsutil.WithSecretKey(secretAccessKey),
|
||||
)
|
||||
require.NoError(err)
|
||||
_, err = c.GetCallerIdentity()
|
||||
require.NoError(err)
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package testing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/boundary-plugin-aws/plugin/service/storage"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
func TestStoragePlugin(t *testing.T) {
|
||||
region := os.Getenv("AWS_REGION")
|
||||
if region == "" {
|
||||
t.Skip("set AWS_REGION to use this test")
|
||||
}
|
||||
if envAccessKeyId := os.Getenv("AWS_ACCESS_KEY_ID"); envAccessKeyId == "" {
|
||||
t.Skip("set AWS_ACCESS_KEY_ID to use this test")
|
||||
}
|
||||
if envSecretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY"); envSecretAccessKey == "" {
|
||||
t.Skip("set AWS_SECRET_ACCESS_KEY to use this test")
|
||||
}
|
||||
|
||||
require := require.New(t)
|
||||
tf, err := NewTestTerraformer("testdata/storage")
|
||||
require.NoError(err)
|
||||
require.NotNil(tf)
|
||||
|
||||
t.Log("===== deploying test Terraform workspace =====")
|
||||
err = tf.Deploy()
|
||||
require.NoError(err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
t.Log("===== destroying test Terraform workspace =====")
|
||||
if err := tf.Destroy(); err != nil {
|
||||
t.Logf("WARNING: could not run Terraform destroy: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
bucketName, err := tf.GetOutputString("bucket_name")
|
||||
require.NoError(err)
|
||||
require.NotEmpty(bucketName)
|
||||
|
||||
iamAccessKeyIds, err := tf.GetOutputSlice("iam_access_key_ids")
|
||||
require.NoError(err)
|
||||
require.Len(iamAccessKeyIds, expectedIamUserCount)
|
||||
|
||||
iamSecretAccessKeys, err := tf.GetOutputSlice("iam_secret_access_keys")
|
||||
require.NoError(err)
|
||||
require.Len(iamSecretAccessKeys, expectedIamUserCount)
|
||||
|
||||
iamAccessKeyMissingGetObject, err := tf.GetOutputString("iam_access_key_missing_get_obj")
|
||||
require.NoError(err)
|
||||
|
||||
iamSecretAccessKeyMissingGetObject, err := tf.GetOutputString("iam_secret_access_key_missing_get_obj")
|
||||
require.NoError(err)
|
||||
|
||||
iamAccessKeyMissingPutObject, err := tf.GetOutputString("iam_access_key_missing_put_obj")
|
||||
require.NoError(err)
|
||||
|
||||
iamSecretAccessKeyMissingPutObject, err := tf.GetOutputString("iam_secret_access_key_missing_put_obj")
|
||||
require.NoError(err)
|
||||
|
||||
// Start the workflow now. Set up the 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
|
||||
// will assert this through the returned state.
|
||||
p := new(storage.StoragePlugin)
|
||||
ctx := context.Background()
|
||||
|
||||
// ********************
|
||||
// * OnCreateStorageBucket
|
||||
// ********************
|
||||
//
|
||||
// Test non-rotation (using primary user).
|
||||
keyid, secret := testPluginOnCreateStorageBucket(ctx, t, p, bucketName, region, iamAccessKeyIds[0].(string), iamSecretAccessKeys[0].(string), false)
|
||||
// Test rotation next.
|
||||
keyid, secret = testPluginOnCreateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, true)
|
||||
|
||||
// ********************
|
||||
// * OnUpdateStorageBucket
|
||||
// ********************
|
||||
//
|
||||
// Test no-op non-rotation.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, "", "", false, false)
|
||||
// Switch to rotation.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, "", "", false, true)
|
||||
// Test no-op with rotation.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, "", "", true, true)
|
||||
// Switch credentials to next user. Don't rotate.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[1].(string), iamSecretAccessKeys[1].(string), true, false)
|
||||
// Switch credentials to next user. Add rotation.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[2].(string), iamSecretAccessKeys[2].(string), false, true)
|
||||
// Switch to next user, with rotation disabled.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[3].(string), iamSecretAccessKeys[3].(string), true, false)
|
||||
// Last case - switch to another user and keep rotation off.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[4].(string), iamSecretAccessKeys[4].(string), false, false)
|
||||
|
||||
// ********************
|
||||
// * OnDeleteStorageBucket
|
||||
// ********************
|
||||
//
|
||||
// Test non-rotated.
|
||||
testPluginOnDeleteStorageBucket(ctx, t, p, bucketName, region, keyid, secret, false)
|
||||
// Test as if we had rotated these credentials (note that this
|
||||
// makes this test set unusable).
|
||||
testPluginOnDeleteStorageBucket(ctx, t, p, bucketName, region, keyid, secret, true)
|
||||
|
||||
// ********************
|
||||
// * Object Methods: PutObject, GetObject, HeadObject
|
||||
// ********************
|
||||
//
|
||||
// Reassign the keyid and secret first.
|
||||
keyid, secret = iamAccessKeyIds[5].(string), iamSecretAccessKeys[5].(string)
|
||||
testPluginObjectMethods(ctx, t, p, bucketName, region, keyid, secret)
|
||||
|
||||
// ********************
|
||||
// * Validate Permissions
|
||||
// ********************
|
||||
//
|
||||
// Validate error is returned for missing get object permission
|
||||
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingGetObject, iamSecretAccessKeyMissingGetObject)
|
||||
|
||||
// Validate error is returned for missing put object permission
|
||||
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingPutObject, iamSecretAccessKeyMissingPutObject)
|
||||
}
|
||||
|
||||
func testPluginOnCreateStorageBucket(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string, rotate bool) (string, string) {
|
||||
t.Helper()
|
||||
t.Logf("testing OnCreateStorageBucket (region=%s, rotate=%t)", region, rotate)
|
||||
require := require.New(t)
|
||||
|
||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: !rotate,
|
||||
})
|
||||
require.NoError(err)
|
||||
reqSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: accessKeyId,
|
||||
credential.ConstSecretAccessKey: secretAccessKey,
|
||||
})
|
||||
require.NoError(err)
|
||||
request := &pb.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Secrets: reqSecrets,
|
||||
Attributes: reqAttrs,
|
||||
},
|
||||
}
|
||||
response, err := p.OnCreateStorageBucket(ctx, request)
|
||||
require.NoError(err)
|
||||
require.NotNil(response)
|
||||
persisted := response.GetPersisted()
|
||||
require.NotNil(persisted)
|
||||
return validatePersistedSecrets(t, persisted.GetData(), accessKeyId, secretAccessKey, rotate)
|
||||
}
|
||||
|
||||
func testPluginOnUpdateStorageBucket(
|
||||
ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName,
|
||||
region, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey string,
|
||||
rotated, rotate bool,
|
||||
) (string, string) {
|
||||
t.Helper()
|
||||
t.Logf("testing OnUpdateStorageBucket (region=%s, newcreds=%t, rotated=%t, rotate=%t)", region, newAccessKeyId != "" && newSecretAccessKey != "", rotated, rotate)
|
||||
require := require.New(t)
|
||||
|
||||
// Take a timestamp of the current time to get a point in time to
|
||||
// reference, ensuring that we are updating credential rotation
|
||||
// timestamps.
|
||||
currentCredsLastRotatedTime := time.Now()
|
||||
|
||||
reqCurrentAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: !rotated,
|
||||
})
|
||||
require.NoError(err)
|
||||
reqNewAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: !rotate,
|
||||
})
|
||||
require.NoError(err)
|
||||
var reqSecrets *structpb.Struct
|
||||
if newAccessKeyId != "" && newSecretAccessKey != "" {
|
||||
reqSecrets, err = structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: newAccessKeyId,
|
||||
credential.ConstSecretAccessKey: newSecretAccessKey,
|
||||
})
|
||||
require.NoError(err)
|
||||
}
|
||||
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: currentAccessKeyId,
|
||||
credential.ConstSecretAccessKey: currentSecretAccessKey,
|
||||
credential.ConstCredsLastRotatedTime: func() string {
|
||||
if rotated {
|
||||
return currentCredsLastRotatedTime.Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
return (time.Time{}).Format(time.RFC3339Nano)
|
||||
}(),
|
||||
})
|
||||
require.NoError(err)
|
||||
require.NotNil(reqPersistedSecrets)
|
||||
request := &pb.OnUpdateStorageBucketRequest{
|
||||
CurrentBucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqCurrentAttrs,
|
||||
},
|
||||
NewBucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqNewAttrs,
|
||||
Secrets: reqSecrets,
|
||||
},
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: reqPersistedSecrets,
|
||||
},
|
||||
}
|
||||
response, err := p.OnUpdateStorageBucket(ctx, request)
|
||||
require.NoError(err)
|
||||
require.NotNil(response)
|
||||
persisted := response.GetPersisted()
|
||||
require.NotNil(persisted)
|
||||
return validateUpdateSecrets(t, persisted.GetData(), currentCredsLastRotatedTime, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey, rotated, rotate)
|
||||
}
|
||||
|
||||
func testPluginOnDeleteStorageBucket(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string, rotated bool) {
|
||||
t.Helper()
|
||||
t.Logf("testing OnDeleteStorageBucket (region=%s, rotated=%t)", region, rotated)
|
||||
require := require.New(t)
|
||||
|
||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: !rotated,
|
||||
})
|
||||
require.NoError(err)
|
||||
reqSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: accessKeyId,
|
||||
credential.ConstSecretAccessKey: secretAccessKey,
|
||||
})
|
||||
require.NoError(err)
|
||||
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: accessKeyId,
|
||||
credential.ConstSecretAccessKey: secretAccessKey,
|
||||
credential.ConstCredsLastRotatedTime: func() string {
|
||||
if rotated {
|
||||
return time.Now().Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
return (time.Time{}).Format(time.RFC3339Nano)
|
||||
}(),
|
||||
})
|
||||
require.NoError(err)
|
||||
request := &pb.OnDeleteStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqAttrs,
|
||||
Secrets: reqSecrets,
|
||||
},
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: reqPersistedSecrets,
|
||||
},
|
||||
}
|
||||
response, err := p.OnDeleteStorageBucket(ctx, request)
|
||||
require.NoError(err)
|
||||
require.NotNil(response)
|
||||
|
||||
// We want to test the validity of the credentials post-deletion.
|
||||
if rotated {
|
||||
// The credentials should no longer be valid.
|
||||
requireCredentialsInvalid(t, accessKeyId, secretAccessKey)
|
||||
} else {
|
||||
// The credentials should still be valid. Sleep 10s first just to
|
||||
// be sure, since we're not rotating.
|
||||
time.Sleep(time.Second * 10)
|
||||
requireCredentialsValid(t, accessKeyId, secretAccessKey)
|
||||
}
|
||||
}
|
||||
|
||||
func testPluginValidatePermissions(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string) {
|
||||
t.Helper()
|
||||
t.Logf("testing ValidatePermissions")
|
||||
require := require.New(t)
|
||||
|
||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: true,
|
||||
})
|
||||
require.NoError(err)
|
||||
reqSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: accessKeyId,
|
||||
credential.ConstSecretAccessKey: secretAccessKey,
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
req := &pb.ValidatePermissionsRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqAttrs,
|
||||
Secrets: reqSecrets,
|
||||
},
|
||||
}
|
||||
|
||||
_, err = p.ValidatePermissions(ctx, req)
|
||||
require.Error(err)
|
||||
}
|
||||
|
||||
func testPluginObjectMethods(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string) {
|
||||
t.Helper()
|
||||
t.Logf("testing PutObject")
|
||||
require := require.New(t)
|
||||
|
||||
testDataPath, err := filepath.Abs("./testdata/storage/test_object_data")
|
||||
require.NoError(err)
|
||||
require.FileExists(testDataPath)
|
||||
|
||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: true,
|
||||
})
|
||||
require.NoError(err)
|
||||
reqSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: accessKeyId,
|
||||
credential.ConstSecretAccessKey: secretAccessKey,
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
objectKey := uuid.New().String()
|
||||
putObjReq := &pb.PutObjectRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqAttrs,
|
||||
Secrets: reqSecrets,
|
||||
},
|
||||
Key: objectKey,
|
||||
Path: testDataPath,
|
||||
}
|
||||
putObjResp, err := p.PutObject(ctx, putObjReq)
|
||||
require.NoError(err)
|
||||
require.NotNil(putObjResp)
|
||||
require.NotEmpty(putObjResp.GetChecksumSha_256())
|
||||
|
||||
t.Logf("testing HeadObject")
|
||||
headObjReq := &pb.HeadObjectRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqAttrs,
|
||||
Secrets: reqSecrets,
|
||||
},
|
||||
Key: objectKey,
|
||||
}
|
||||
headObjResp, err := p.HeadObject(ctx, headObjReq)
|
||||
require.NoError(err)
|
||||
require.NotNil(headObjResp)
|
||||
require.NotEmpty(headObjResp.GetContentLength())
|
||||
require.NotEmpty(headObjResp.GetLastModified())
|
||||
|
||||
// Need to create a GRPC server to test GetObject
|
||||
t.Logf("testing GetObject")
|
||||
lis, err := net.Listen("tcp", "localhost:2030")
|
||||
require.NoError(err)
|
||||
grpcServer := grpc.NewServer()
|
||||
pb.RegisterStoragePluginServiceServer(grpcServer, &storage.StoragePlugin{})
|
||||
go func() {
|
||||
grpcServer.Serve(lis)
|
||||
}()
|
||||
|
||||
conn, err := grpc.Dial("localhost:2030", grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
require.NoError(err)
|
||||
defer conn.Close()
|
||||
client := pb.NewStoragePluginServiceClient(conn)
|
||||
|
||||
stream, err := client.GetObject(ctx, &pb.GetObjectRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqAttrs,
|
||||
Secrets: reqSecrets,
|
||||
},
|
||||
Key: objectKey,
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
var actualData []byte
|
||||
expectedData, err := os.ReadFile(testDataPath)
|
||||
require.NoError(err)
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
require.NoError(stream.CloseSend())
|
||||
break
|
||||
} else if err != nil {
|
||||
require.NoError(err)
|
||||
}
|
||||
actualData = append(actualData, resp.GetFileChunk()...)
|
||||
}
|
||||
require.Equal(expectedData, actualData)
|
||||
require.NoError(conn.Close())
|
||||
grpcServer.Stop()
|
||||
}
|
||||
Vendored
+240
@@ -0,0 +1,240 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
resource "aws_iam_role" "valid" {
|
||||
name = "${random_id.prefix.dec}-valid"
|
||||
tags = local.tags
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Action = "sts:AssumeRole"
|
||||
Effect = "Allow"
|
||||
Sid = ""
|
||||
Principal = {
|
||||
AWS: data.aws_caller_identity.current.user_id
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_policy" "valid" {
|
||||
name = "${random_id.prefix.dec}-valid"
|
||||
tags = local.tags
|
||||
|
||||
policy = <<EOT
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"s3:PutObject",
|
||||
"s3:GetObject",
|
||||
"s3:GetObjectAttributes"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_s3_bucket.test.arn}*"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
EOT
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "valid" {
|
||||
role = aws_iam_role.valid.name
|
||||
policy_arn = aws_iam_policy.valid.arn
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "missing_put_obj" {
|
||||
name = "${random_id.prefix.dec}-missing-put-obj"
|
||||
tags = local.tags
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Action = "sts:AssumeRole"
|
||||
Effect = "Allow"
|
||||
Sid = ""
|
||||
Principal = {
|
||||
AWS: data.aws_caller_identity.current.user_id
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
resource "aws_iam_policy" "missing_put_obj" {
|
||||
name = "${random_id.prefix.dec}-missing-put-obj"
|
||||
tags = local.tags
|
||||
|
||||
policy = <<EOT
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"s3:GetObject",
|
||||
"s3:GetObjectAttributes"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_s3_bucket.test.arn}*"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
EOT
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "missing_put_obj" {
|
||||
role = aws_iam_role.missing_put_obj.name
|
||||
policy_arn = aws_iam_policy.missing_put_obj.arn
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "missing_get_obj" {
|
||||
name = "${random_id.prefix.dec}-missing-get-obj"
|
||||
tags = local.tags
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Action = "sts:AssumeRole"
|
||||
Effect = "Allow"
|
||||
Sid = ""
|
||||
Principal = {
|
||||
AWS: data.aws_caller_identity.current.user_id
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_policy" "missing_get_obj" {
|
||||
name = "${random_id.prefix.dec}-missing-get-obj"
|
||||
tags = local.tags
|
||||
|
||||
policy = <<EOT
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"s3:PutObject",
|
||||
"s3:GetObjectAttributes"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_s3_bucket.test.arn}*"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
EOT
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "missing_get_obj" {
|
||||
role = aws_iam_role.missing_get_obj.name
|
||||
policy_arn = aws_iam_policy.missing_get_obj.arn
|
||||
}
|
||||
|
||||
# Static Credential Testing
|
||||
resource "random_id" "user_name" {
|
||||
count = var.iam_user_count
|
||||
prefix = random_id.prefix.dec
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
resource "aws_iam_user" "test" {
|
||||
count = var.iam_user_count
|
||||
name = random_id.user_name[count.index].dec
|
||||
force_destroy = true
|
||||
tags = local.tags
|
||||
}
|
||||
|
||||
resource "aws_iam_access_key" "test" {
|
||||
count = var.iam_user_count
|
||||
user = aws_iam_user.test[count.index].name
|
||||
}
|
||||
|
||||
resource "aws_iam_user_policy_attachment" "test_s3" {
|
||||
count = var.iam_user_count
|
||||
user = aws_iam_user.test[count.index].name
|
||||
policy_arn = aws_iam_policy.valid.arn
|
||||
}
|
||||
|
||||
resource "aws_iam_policy" "credentials" {
|
||||
count = var.iam_user_count
|
||||
name = aws_iam_user.test[count.index].name
|
||||
tags = local.tags
|
||||
|
||||
policy = <<EOF
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"iam:DeleteAccessKey",
|
||||
"iam:GetUser",
|
||||
"iam:CreateAccessKey"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_iam_user.test[count.index].arn}"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
resource "aws_iam_user_policy_attachment" "test_credentials" {
|
||||
count = var.iam_user_count
|
||||
user = aws_iam_user.test[count.index].name
|
||||
policy_arn = aws_iam_policy.credentials[count.index].arn
|
||||
}
|
||||
|
||||
# Edge case for missing GetObject permission
|
||||
|
||||
resource "random_id" "missing_get_obj" {
|
||||
prefix = random_id.prefix.dec
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
resource "aws_iam_user" "missing_get_obj" {
|
||||
name = random_id.missing_get_obj.dec
|
||||
force_destroy = true
|
||||
tags = local.tags
|
||||
}
|
||||
|
||||
resource "aws_iam_access_key" "missing_get_obj" {
|
||||
user = aws_iam_user.missing_get_obj.name
|
||||
}
|
||||
|
||||
resource "aws_iam_user_policy_attachment" "missing_get_obj" {
|
||||
user = aws_iam_user.missing_get_obj.name
|
||||
policy_arn = aws_iam_policy.missing_get_obj.arn
|
||||
}
|
||||
|
||||
# Edge case for missing PutObject permission
|
||||
|
||||
resource "random_id" "missing_put_obj" {
|
||||
prefix = random_id.prefix.dec
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
resource "aws_iam_user" "missing_put_obj" {
|
||||
name = random_id.missing_put_obj.dec
|
||||
force_destroy = true
|
||||
tags = local.tags
|
||||
}
|
||||
|
||||
resource "aws_iam_access_key" "missing_put_obj" {
|
||||
user = aws_iam_user.missing_put_obj.name
|
||||
}
|
||||
|
||||
resource "aws_iam_user_policy_attachment" "missing_put_obj" {
|
||||
user = aws_iam_user.missing_put_obj.name
|
||||
policy_arn = aws_iam_policy.missing_put_obj.arn
|
||||
}
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+56
@@ -0,0 +1,56 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
output "bucket_name" {
|
||||
value = aws_s3_bucket.test.id
|
||||
}
|
||||
|
||||
output "valid_policy_arn" {
|
||||
value = aws_iam_role.valid.arn
|
||||
}
|
||||
|
||||
output "missing_put_obj_policy_arn" {
|
||||
value = aws_iam_role.missing_put_obj.arn
|
||||
}
|
||||
|
||||
output "missing_put_get_policy_arn" {
|
||||
value = aws_iam_role.missing_get_obj.arn
|
||||
}
|
||||
|
||||
output "iam_user_names" {
|
||||
value = aws_iam_user.test.*.name
|
||||
}
|
||||
|
||||
output "iam_user_arns" {
|
||||
value = aws_iam_user.test.*.arn
|
||||
}
|
||||
|
||||
output "iam_access_key_ids" {
|
||||
value = aws_iam_access_key.test.*.id
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "iam_secret_access_keys" {
|
||||
value = aws_iam_access_key.test.*.secret
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "iam_access_key_missing_put_obj" {
|
||||
value = aws_iam_access_key.missing_put_obj.id
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "iam_secret_access_key_missing_put_obj" {
|
||||
value = aws_iam_access_key.missing_put_obj.secret
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "iam_access_key_missing_get_obj" {
|
||||
value = aws_iam_access_key.missing_get_obj.id
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "iam_secret_access_key_missing_get_obj" {
|
||||
value = aws_iam_access_key.missing_get_obj.secret
|
||||
sensitive = true
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
resource "aws_s3_bucket" "test" {
|
||||
bucket = random_id.prefix.dec
|
||||
force_destroy = true
|
||||
acl = "private"
|
||||
tags = local.tags
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
HELLO_WORLD!
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
|
||||
resource "random_id" "prefix" {
|
||||
prefix = "storage-e2e-test"
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
variable "iam_user_count" {
|
||||
default = 6
|
||||
}
|
||||
|
||||
locals {
|
||||
tags = {
|
||||
user_id = split(":", data.aws_caller_identity.current.user_id)[1]
|
||||
repo = "boundary-plugin-aws"
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,26 @@
|
||||
package testing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"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/go-secure-stdlib/awsutil"
|
||||
tfjson "github.com/hashicorp/terraform-json"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
const expectedIamUserCount = 6
|
||||
|
||||
// TestTerraformer is an object representing the lifecycle of a
|
||||
// particular Terraform project.
|
||||
//
|
||||
@@ -264,3 +275,204 @@ func (t *TestTerraformer) showJson() ([]byte, error) {
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func requireCredentialsInvalid(t *testing.T, accessKeyId, secretAccessKey string) {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
|
||||
c, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithAccessKey(accessKeyId),
|
||||
awsutil.WithSecretKey(secretAccessKey),
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
// We need to wait for invalidation as while awsutil waits for
|
||||
// credential creation, deletion of the old credentials returns
|
||||
// immediately.
|
||||
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
defer cancel()
|
||||
waitErr:
|
||||
for {
|
||||
_, err = c.GetCallerIdentity()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second):
|
||||
// pass
|
||||
|
||||
case <-timeoutCtx.Done():
|
||||
break waitErr
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(err)
|
||||
awsErr, ok := err.(awserr.Error)
|
||||
require.True(ok)
|
||||
require.Equal("InvalidClientTokenId", awsErr.Code())
|
||||
}
|
||||
|
||||
func requireCredentialsValid(t *testing.T, accessKeyId, secretAccessKey string) {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
|
||||
c, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithAccessKey(accessKeyId),
|
||||
awsutil.WithSecretKey(secretAccessKey),
|
||||
)
|
||||
require.NoError(err)
|
||||
_, err = c.GetCallerIdentity()
|
||||
require.NoError(err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user