more WIP: lots of fun testing

This commit is contained in:
Chris Marchesi committed 2021-09-22 17:00:37 -07:00
1 parent 01855d28f7
commit b61d0ba244
7 files changed
+463 -17

No files matched your search

+27 -1
View File
@@ -17,6 +17,11 @@ import (
"google.golang.org/protobuf/types/known/structpb"
)
// rotationWaitTimeout controls the time we wait for credential rotation to
// succeed. This is important to ensure that rotated credentials can be used
// right away.
const rotationWaitTimeout = time.Second * 30
// AwsPlugin implements the HostPluginServiceServer interface for the
// AWS plugin.
type AwsPlugin struct {
@@ -74,6 +79,11 @@ func (p *AwsPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalog
if err := state.RotateCreds(); err != nil {
return nil, fmt.Errorf("error during credential rotation: %w", err)
}
} else {
// Simply validate if we aren't rotating.
if err := state.ValidateCreds(); err != nil {
return nil, fmt.Errorf("error during credential validation: %w", err)
}
}
persistedProto, err := state.ToProto()
@@ -456,6 +466,22 @@ func (s *awsCatalogPersistedState) ToProto() (*pb.HostCatalogPersisted, error) {
return &pb.HostCatalogPersisted{Data: data}, nil
}
func (s *awsCatalogPersistedState) ValidateCreds() error {
c, err := awsutil.NewCredentialsConfig(
awsutil.WithAccessKey(s.AccessKeyId),
awsutil.WithSecretKey(s.SecretAccessKey),
)
if err != nil {
return fmt.Errorf("error loading credentials: %w", err)
}
if _, err := c.GetCallerIdentity(); err != nil {
return fmt.Errorf("error validating credentials: %w", err)
}
return nil
}
func (s *awsCatalogPersistedState) RotateCreds() error {
c, err := awsutil.NewCredentialsConfig(
awsutil.WithAccessKey(s.AccessKeyId),
@@ -465,7 +491,7 @@ func (s *awsCatalogPersistedState) RotateCreds() error {
return fmt.Errorf("error loading credentials: %w", err)
}
if err := c.RotateKeys(); err != nil {
if err := c.RotateKeys(awsutil.WithValidityCheckTimeout(rotationWaitTimeout)); err != nil {
return fmt.Errorf("error rotating credentials: %w", err)
}
+349
View File
@@ -0,0 +1,349 @@
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)
}
+26 -15
View File
@@ -2,19 +2,27 @@ variable "iam_user_prefix" {
default = "boundary-plugin-host-aws-test-iam-user"
}
variable "iam_user_count" {
default = 2
}
resource "random_id" "aws_iam_user_name" {
prefix = var.iam_user_prefix
count = var.iam_user_count
prefix = "${var.iam_user_prefix}-${count.index}"
byte_length = 4
}
resource "aws_iam_user" "user" {
name = random_id.aws_iam_user_name.dec
count = var.iam_user_count
name = random_id.aws_iam_user_name[count.index].dec
force_destroy = true
tags = var.project_path_tags
}
resource "aws_iam_access_key" "user_initial_key" {
user = aws_iam_user.user.name
count = var.iam_user_count
user = aws_iam_user.user[count.index].name
}
resource "aws_iam_policy" "ec2_describeinstances" {
@@ -39,12 +47,14 @@ EOF
}
resource "aws_iam_user_policy_attachment" "user_ec2_describeinstances_attachment" {
user = aws_iam_user.user.name
count = var.iam_user_count
user = aws_iam_user.user[count.index].name
policy_arn = aws_iam_policy.ec2_describeinstances.arn
}
resource "aws_iam_policy" "user_self_manage_policy" {
name = "BoundaryPluginHostAwsTestUserSelfManagePolicy"
count = var.iam_user_count
name = "BoundaryPluginHostAwsTestUserSelfManagePolicy-${count.index}"
policy = <<EOF
{
@@ -57,7 +67,7 @@ resource "aws_iam_policy" "user_self_manage_policy" {
"iam:CreateAccessKey"
],
"Effect": "Allow",
"Resource": "${aws_iam_user.user.arn}"
"Resource": "${aws_iam_user.user[count.index].arn}"
}
]
}
@@ -67,24 +77,25 @@ EOF
}
resource "aws_iam_user_policy_attachment" "user_self_manage_policy_attachment" {
user = aws_iam_user.user.name
policy_arn = aws_iam_policy.user_self_manage_policy.arn
count = var.iam_user_count
user = aws_iam_user.user[count.index].name
policy_arn = aws_iam_policy.user_self_manage_policy[count.index].arn
}
output "iam_user_name" {
value = aws_iam_user.user.name
output "iam_user_names" {
value = aws_iam_user.user.*.name
}
output "iam_user_arn" {
value = aws_iam_user.user.arn
output "iam_user_arns" {
value = aws_iam_user.user.*.arn
}
output "iam_access_key_id" {
value = aws_iam_access_key.user_initial_key.id
output "iam_access_key_ids" {
value = aws_iam_access_key.user_initial_key.*.id
sensitive = true
}
output "iam_secret_access_key" {
value = aws_iam_access_key.user_initial_key.secret
value = aws_iam_access_key.user_initial_key.*.secret
sensitive = true
}
+10
View File
@@ -60,6 +60,16 @@ resource "aws_instance" "instances" {
tags = merge(var.instance_tags[count.index], var.project_path_tags)
}
output "instance_ids" {
value = aws_instance.instances.*.id
}
output "instance_addrs" {
value = {
for _, r in aws_instance.instances : r.id => r.private_ip
}
}
output "instance_tags" {
value = {
for _, r in aws_instance.instances : r.id => r.tags