349 lines
13 KiB
Go
349 lines
13 KiB
Go
package plugin
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"os"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
||
|
|
pluginTesting "github.com/hashicorp/boundary-host-plugin-aws/testing"
|
||
|
|
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
||
|
|
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||
|
|
"github.com/stretchr/testify/require"
|
||
|
|
"google.golang.org/protobuf/types/known/structpb"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestPlugin(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 := pluginTesting.NewTestTerraformer("testdata/basic")
|
||
|
|
require.NoError(err)
|
||
|
|
require.NotNil(tf)
|
||
|
|
|
||
|
|
err = tf.Deploy()
|
||
|
|
require.NoError(err)
|
||
|
|
|
||
|
|
defer func() {
|
||
|
|
if err := tf.Destroy(); err != nil {
|
||
|
|
t.Logf("WARNING: could not run Terraform destroy: %s", err)
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
|
||
|
|
iamUserName, err := tf.GetOutputString("iam_user_name")
|
||
|
|
require.NoError(err)
|
||
|
|
require.NotZero(iamUserName)
|
||
|
|
|
||
|
|
iamUserArn, err := tf.GetOutputString("iam_user_arn")
|
||
|
|
require.NoError(err)
|
||
|
|
require.NotZero(iamUserArn)
|
||
|
|
|
||
|
|
iamAccessKeyId, err := tf.GetOutputString("iam_access_key_id")
|
||
|
|
require.NoError(err)
|
||
|
|
require.NotZero(iamAccessKeyId)
|
||
|
|
|
||
|
|
iamSecretAccessKey, err := tf.GetOutputString("iam_secret_access_key")
|
||
|
|
require.NoError(err)
|
||
|
|
require.NotZero(iamSecretAccessKey)
|
||
|
|
|
||
|
|
ec2InstanceIds, err := tf.GetOutputSlice("instance_ids")
|
||
|
|
require.NoError(err)
|
||
|
|
require.Len(ec2InstanceIds, 5)
|
||
|
|
|
||
|
|
ec2InstanceAddrs, err := tf.GetOutputMap("instance_addrs")
|
||
|
|
require.NoError(err)
|
||
|
|
require.Len(ec2InstanceAddrs, 5)
|
||
|
|
|
||
|
|
ec2InstanceTags, err := tf.GetOutputMap("instance_tags")
|
||
|
|
require.NoError(err)
|
||
|
|
require.Len(ec2InstanceTags, 5)
|
||
|
|
|
||
|
|
// 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(AwsPlugin)
|
||
|
|
ctx := context.Background()
|
||
|
|
|
||
|
|
// ********************
|
||
|
|
// * OnCreateCatalog
|
||
|
|
// ********************
|
||
|
|
//
|
||
|
|
// Test rotation first.
|
||
|
|
iamAccessKeyId, iamSecretAccessKey = testPluginOnCreateCatalog(ctx, t, p, region, iamAccessKeyId, iamSecretAccessKey, true)
|
||
|
|
// Test non-rotation next.
|
||
|
|
iamAccessKeyId, iamSecretAccessKey = testPluginOnCreateCatalog(ctx, t, p, region, iamAccessKeyId, iamSecretAccessKey, false)
|
||
|
|
|
||
|
|
// ********************
|
||
|
|
// * OnUpdateCatalog
|
||
|
|
// ********************
|
||
|
|
//
|
||
|
|
// Test no-op non-rotation.
|
||
|
|
iamAccessKeyId, iamSecretAccessKey = testPluginOnCreateCatalog(ctx, t, p, region, iamAccessKeyId, iamSecretAccessKey, "", "", false, false)
|
||
|
|
// Switch to rotation.
|
||
|
|
iamAccessKeyId, iamSecretAccessKey = testPluginOnCreateCatalog(ctx, t, p, region, iamAccessKeyId, iamSecretAccessKey, "", "", false, true)
|
||
|
|
// Test no-op with rotation.
|
||
|
|
iamAccessKeyId, iamSecretAccessKey = testPluginOnCreateCatalog(ctx, t, p, region, iamAccessKeyId, iamSecretAccessKey, "", "", true, true)
|
||
|
|
// Switch credentials to other user.
|
||
|
|
}
|
||
|
|
|
||
|
|
func testPluginOnCreateCatalog(ctx context.Context, t *testing.T, p *AwsPlugin, region, accessKeyId, secretAccessKey string, rotate bool) (string, string) {
|
||
|
|
t.Helper()
|
||
|
|
require := require.New(t)
|
||
|
|
|
||
|
|
reqAttrs, err := structpb.NewStruct(map[string]interface{}{
|
||
|
|
constRegion: region,
|
||
|
|
constDisableCredentialRotation: !rotate,
|
||
|
|
})
|
||
|
|
require.NoError(err)
|
||
|
|
reqSecrets, err := structpb.NewStruct(map[string]interface{}{
|
||
|
|
constAccessKeyId: accessKeyId,
|
||
|
|
constSecretAccessKey: secretAccessKey,
|
||
|
|
})
|
||
|
|
require.NoError(err)
|
||
|
|
request := &pb.OnCreateCatalogRequest{
|
||
|
|
Catalog: &hostcatalogs.HostCatalog{
|
||
|
|
Attributes: reqAttrs,
|
||
|
|
Secrets: reqSecrets,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
response, err := p.OnCreateCatalog(ctx, request)
|
||
|
|
require.NoError(err)
|
||
|
|
require.NotNil(response)
|
||
|
|
persisted := response.GetPersisted()
|
||
|
|
require.NotNil(persisted)
|
||
|
|
persistedData := persisted.GetData()
|
||
|
|
require.NotNil(persistedData)
|
||
|
|
persistedDataMap := persistedData.AsMap()
|
||
|
|
require.NotNil(persistedDataMap)
|
||
|
|
persistedAccessKeyId, ok := persistedDataMap[constAccessKeyId]
|
||
|
|
require.True(ok)
|
||
|
|
require.NotZero(persistedAccessKeyId)
|
||
|
|
if rotate {
|
||
|
|
require.NotEqual(accessKeyId, persistedAccessKeyId)
|
||
|
|
} else {
|
||
|
|
require.Equal(accessKeyId, persistedAccessKeyId)
|
||
|
|
}
|
||
|
|
|
||
|
|
persistedSecretAccessKey, ok := persistedDataMap[constSecretAccessKey]
|
||
|
|
require.True(ok)
|
||
|
|
require.NotZero(persistedSecretAccessKey)
|
||
|
|
if rotate {
|
||
|
|
require.NotEqual(secretAccessKey, persistedSecretAccessKey)
|
||
|
|
} else {
|
||
|
|
require.Equal(secretAccessKey, persistedSecretAccessKey)
|
||
|
|
}
|
||
|
|
|
||
|
|
persistedCredsLastRotatedTime, ok := persistedDataMap[constCredsLastRotatedTime]
|
||
|
|
require.True(ok)
|
||
|
|
if rotate {
|
||
|
|
require.NotZero(persistedCredsLastRotatedTime)
|
||
|
|
} else {
|
||
|
|
require.Zero(persistedCredsLastRotatedTime)
|
||
|
|
}
|
||
|
|
|
||
|
|
return persistedAccessKeyId.(string), persistedSecretAccessKey.(string)
|
||
|
|
}
|
||
|
|
|
||
|
|
func testPluginOnUpdateCatalog(
|
||
|
|
ctx context.Context, t *testing.T, p *AwsPlugin,
|
||
|
|
region, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey string,
|
||
|
|
rotated, rotate bool,
|
||
|
|
) (string, string) {
|
||
|
|
t.Helper()
|
||
|
|
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.
|
||
|
|
currentLastRotationTime = time.Now()
|
||
|
|
|
||
|
|
reqCurrentAttrs, err := structpb.NewStruct(map[string]interface{}{
|
||
|
|
constRegion: region,
|
||
|
|
constDisableCredentialRotation: !rotated,
|
||
|
|
})
|
||
|
|
reqNewAttrs, err := structpb.NewStruct(map[string]interface{}{
|
||
|
|
constRegion: region,
|
||
|
|
constDisableCredentialRotation: !rotate,
|
||
|
|
})
|
||
|
|
require.NoError(err)
|
||
|
|
var reqSecrets *structpb.Struct
|
||
|
|
if newAccessKeyId != "" && newSecretAccessKey != "" {
|
||
|
|
reqSecrets, err = structpb.NewStruct(map[string]interface{}{
|
||
|
|
constAccessKeyId: newAccessKeyId,
|
||
|
|
constSecretAccessKey: newSecretAccessKey,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
reqPersistedData, err := structpb.NewStruct(map[string]interface{}{
|
||
|
|
constAccessKeyId: currentAccessKeyId,
|
||
|
|
constSecretAccessKey: currentSecretAccessKey,
|
||
|
|
constCredsLastRotatedTime: func() time.Time {
|
||
|
|
if rotated {
|
||
|
|
return currentLastRotationTime
|
||
|
|
}
|
||
|
|
|
||
|
|
return time.Time{}
|
||
|
|
}(),
|
||
|
|
})
|
||
|
|
require.NoError(err)
|
||
|
|
onUpdateCatalogRequest := &pb.OnUpdateCatalogRequest{
|
||
|
|
CurrentCatalog: &hostcatalogs.HostCatalog{
|
||
|
|
Attributes: reqCurrentAttrs,
|
||
|
|
},
|
||
|
|
NewCatalog: &hostcatalogs.HostCatalog{
|
||
|
|
Attributes: reqNewAttrs,
|
||
|
|
Secrets: reqSecrets,
|
||
|
|
},
|
||
|
|
Persisted: &pb.HostCatalogPersisted{
|
||
|
|
Data: reqPersistedData,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
response, err := p.OnCreateCatalog(ctx, request)
|
||
|
|
require.NoError(err)
|
||
|
|
require.NotNil(response)
|
||
|
|
persisted := response.GetPersisted()
|
||
|
|
require.NotNil(persisted)
|
||
|
|
persistedData := persisted.GetData()
|
||
|
|
require.NotNil(persistedData)
|
||
|
|
persistedDataMap := persistedData.AsMap()
|
||
|
|
require.NotNil(persistedDataMap)
|
||
|
|
|
||
|
|
// Complex checks based on the scenarios.
|
||
|
|
persistedAccessKeyId, ok := persistedDataMap[constAccessKeyId]
|
||
|
|
require.True(ok)
|
||
|
|
require.NotZero(persistedAccessKeyId)
|
||
|
|
persistedSecretAccessKey, ok := persistedDataMap[constSecretAccessKey]
|
||
|
|
require.True(ok)
|
||
|
|
require.NotZero(persistedSecretAccessKey)
|
||
|
|
persistedCredsLastRotatedTime, ok := persistedDataMap[constCredsLastRotatedTime]
|
||
|
|
require.True(ok)
|
||
|
|
|
||
|
|
// 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.
|
||
|
|
require.CredentialsInvalid(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.
|
||
|
|
require.CredentialsInvalid(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.
|
||
|
|
require.CredentialsValid(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.
|
||
|
|
require.CredentialsValid(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.
|
||
|
|
require.CredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||
|
|
require.Equal(persistedAccessKeyId, currentAccessKeyId)
|
||
|
|
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
|
||
|
|
require.NotZero(persistedCredsLastRotatedTime)
|
||
|
|
require.Equal(currentCredsLastRotatedTime, persistedCredsLastRotatedTime)
|
||
|
|
|
||
|
|
case newAccessKeyId == "" && rotated && !rotate:
|
||
|
|
// No new credentials have been provided, and we have previously
|
||
|
|
// rotated the credentials. This is actually an error.
|
||
|
|
//
|
||
|
|
// TODO: validate this scenario through unit testing. For now if
|
||
|
|
// we for some reason try to test this with this function, return
|
||
|
|
// an error.
|
||
|
|
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.
|
||
|
|
require.CredentialsInvalid(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.
|
||
|
|
require.CredentialsValid(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.(string), persistedSecretAccessKey.(string)
|
||
|
|
}
|
||
|
|
|
||
|
|
func requireCredentailsInvalid(t *testing.T, accessKeyId, secretAccessKey string) {
|
||
|
|
t.Helper()
|
||
|
|
require := require.New(t)
|
||
|
|
|
||
|
|
err := c.GetCallerIdentity()
|
||
|
|
require.NotNil(err)
|
||
|
|
awsErr, ok := err.(awserr.Error)
|
||
|
|
require.True(ok)
|
||
|
|
require.Equal("InvalidClientTokenId", awsErr.Code())
|
||
|
|
}
|
||
|
|
|
||
|
|
func requireCredentailsvValid(t *testing.T, accessKeyId, secretAccessKey string) {
|
||
|
|
t.Helper()
|
||
|
|
require := require.New(t)
|
||
|
|
|
||
|
|
err := c.GetCallerIdentity()
|
||
|
|
require.NoError(err)
|
||
|
|
}
|