WIP - more unit testing
This commit is contained in:
4 files changed
+299
-175
No files matched your search
@@ -1,3 +1,4 @@
|
||||
.terraform*
|
||||
terraform.tfstate*
|
||||
plan.tfplan
|
||||
*.log
|
||||
@@ -9,11 +9,8 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/endpoints"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/iam"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
@@ -455,178 +452,6 @@ func (p *AwsPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*p
|
||||
}, nil
|
||||
}
|
||||
|
||||
type awsCatalogPersistedState struct {
|
||||
AccessKeyId string
|
||||
SecretAccessKey string
|
||||
CredsLastRotatedTime time.Time
|
||||
}
|
||||
|
||||
func awsCatalogPersistedStateFromProto(in *pb.HostCatalogPersisted) (*awsCatalogPersistedState, error) {
|
||||
data := in.GetSecrets()
|
||||
if data == nil {
|
||||
return nil, errors.New("missing persisted secrets")
|
||||
}
|
||||
|
||||
accessKeyId, err := getStringValue(data, constAccessKeyId, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||
}
|
||||
|
||||
secretAccessKey, err := getStringValue(data, constSecretAccessKey, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||
}
|
||||
|
||||
credsLastRotatedTime, err := getTimeValue(data, constCredsLastRotatedTime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||
}
|
||||
|
||||
return &awsCatalogPersistedState{
|
||||
AccessKeyId: accessKeyId,
|
||||
SecretAccessKey: secretAccessKey,
|
||||
CredsLastRotatedTime: credsLastRotatedTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *awsCatalogPersistedState) ToProto() (*pb.HostCatalogPersisted, error) {
|
||||
data, err := structpb.NewStruct(map[string]interface{}{
|
||||
constAccessKeyId: s.AccessKeyId,
|
||||
constSecretAccessKey: s.SecretAccessKey,
|
||||
constCredsLastRotatedTime: s.CredsLastRotatedTime.Format(time.RFC3339Nano),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting state to structpb.Struct: %w", err)
|
||||
}
|
||||
|
||||
return &pb.HostCatalogPersisted{Secrets: 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),
|
||||
awsutil.WithSecretKey(s.SecretAccessKey),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading credentials: %w", err)
|
||||
}
|
||||
|
||||
if err := c.RotateKeys(awsutil.WithValidityCheckTimeout(rotationWaitTimeout)); 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 *awsCatalogPersistedState) ReplaceCreds(accessKeyId, secretAccessKey string) error {
|
||||
if accessKeyId == "" {
|
||||
return errors.New("access key id cannot be empty")
|
||||
}
|
||||
|
||||
if secretAccessKey == "" {
|
||||
return errors.New("secret access key cannot be empty")
|
||||
}
|
||||
|
||||
if accessKeyId == s.AccessKeyId {
|
||||
return errors.New("attempting to replace access key with the same one")
|
||||
}
|
||||
|
||||
if !s.CredsLastRotatedTime.IsZero() {
|
||||
// Delete old/existing credentials. This is done with the same
|
||||
// credentials to ensure that it has the proper permissions to do
|
||||
// it.
|
||||
if err := s.DeleteCreds(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Set the new attributes and clear the rotated time.
|
||||
s.AccessKeyId = accessKeyId
|
||||
s.SecretAccessKey = secretAccessKey
|
||||
s.CredsLastRotatedTime = time.Time{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteCreds deletes the credentials in the state. The access key
|
||||
// ID, secret access key, and rotation time fields are zeroed out in
|
||||
// the state just to ensure that they cannot be re-used after.
|
||||
func (s *awsCatalogPersistedState) DeleteCreds() 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.DeleteAccessKey(s.AccessKeyId); err != nil {
|
||||
// Determine if the deletion error was due to a missing
|
||||
// resource. If it was, just pass it.
|
||||
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == iam.ErrCodeNoSuchEntityException {
|
||||
s.AccessKeyId = ""
|
||||
s.SecretAccessKey = ""
|
||||
s.CredsLastRotatedTime = time.Time{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Otherwise treat it as an actual error.
|
||||
return fmt.Errorf("error deleting old access key: %w", err)
|
||||
}
|
||||
|
||||
s.AccessKeyId = ""
|
||||
s.SecretAccessKey = ""
|
||||
s.CredsLastRotatedTime = time.Time{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSession returns a configured AWS session for the credentials in
|
||||
// the state.
|
||||
func (s *awsCatalogPersistedState) GetSession() (*session.Session, error) {
|
||||
c, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithAccessKey(s.AccessKeyId),
|
||||
awsutil.WithSecretKey(s.SecretAccessKey),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.GetSession()
|
||||
}
|
||||
|
||||
// EC2Client returns a configured EC2 client based on the session
|
||||
// information stored in the state.
|
||||
func (s *awsCatalogPersistedState) EC2Client(region string) (*ec2.EC2, error) {
|
||||
sess, err := s.GetSession()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting AWS session: %w", err)
|
||||
}
|
||||
|
||||
return ec2.New(sess, aws.NewConfig().WithRegion(region)), nil
|
||||
}
|
||||
|
||||
func getStringValue(in *structpb.Struct, k string, required bool) (string, error) {
|
||||
mv := in.AsMap()
|
||||
v, ok := mv[k]
|
||||
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/iam"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
type awsCatalogPersistedState struct {
|
||||
AccessKeyId string
|
||||
SecretAccessKey string
|
||||
CredsLastRotatedTime time.Time
|
||||
}
|
||||
|
||||
func awsCatalogPersistedStateFromProto(in *pb.HostCatalogPersisted) (*awsCatalogPersistedState, error) {
|
||||
data := in.GetSecrets()
|
||||
if data == nil {
|
||||
return nil, errors.New("missing persisted secrets")
|
||||
}
|
||||
|
||||
accessKeyId, err := getStringValue(data, constAccessKeyId, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||
}
|
||||
|
||||
secretAccessKey, err := getStringValue(data, constSecretAccessKey, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||
}
|
||||
|
||||
credsLastRotatedTime, err := getTimeValue(data, constCredsLastRotatedTime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||
}
|
||||
|
||||
return &awsCatalogPersistedState{
|
||||
AccessKeyId: accessKeyId,
|
||||
SecretAccessKey: secretAccessKey,
|
||||
CredsLastRotatedTime: credsLastRotatedTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *awsCatalogPersistedState) ToProto() (*pb.HostCatalogPersisted, error) {
|
||||
data, err := structpb.NewStruct(map[string]interface{}{
|
||||
constAccessKeyId: s.AccessKeyId,
|
||||
constSecretAccessKey: s.SecretAccessKey,
|
||||
constCredsLastRotatedTime: s.CredsLastRotatedTime.Format(time.RFC3339Nano),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting state to structpb.Struct: %w", err)
|
||||
}
|
||||
|
||||
return &pb.HostCatalogPersisted{Secrets: 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),
|
||||
awsutil.WithSecretKey(s.SecretAccessKey),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading credentials: %w", err)
|
||||
}
|
||||
|
||||
if err := c.RotateKeys(awsutil.WithValidityCheckTimeout(rotationWaitTimeout)); 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 *awsCatalogPersistedState) ReplaceCreds(accessKeyId, secretAccessKey string) error {
|
||||
if accessKeyId == "" {
|
||||
return errors.New("access key id cannot be empty")
|
||||
}
|
||||
|
||||
if secretAccessKey == "" {
|
||||
return errors.New("secret access key cannot be empty")
|
||||
}
|
||||
|
||||
if accessKeyId == s.AccessKeyId {
|
||||
return errors.New("attempting to replace access key with the same one")
|
||||
}
|
||||
|
||||
if !s.CredsLastRotatedTime.IsZero() {
|
||||
// Delete old/existing credentials. This is done with the same
|
||||
// credentials to ensure that it has the proper permissions to do
|
||||
// it.
|
||||
if err := s.DeleteCreds(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Set the new attributes and clear the rotated time.
|
||||
s.AccessKeyId = accessKeyId
|
||||
s.SecretAccessKey = secretAccessKey
|
||||
s.CredsLastRotatedTime = time.Time{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteCreds deletes the credentials in the state. The access key
|
||||
// ID, secret access key, and rotation time fields are zeroed out in
|
||||
// the state just to ensure that they cannot be re-used after.
|
||||
func (s *awsCatalogPersistedState) DeleteCreds() 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.DeleteAccessKey(s.AccessKeyId); err != nil {
|
||||
// Determine if the deletion error was due to a missing
|
||||
// resource. If it was, just pass it.
|
||||
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == iam.ErrCodeNoSuchEntityException {
|
||||
s.AccessKeyId = ""
|
||||
s.SecretAccessKey = ""
|
||||
s.CredsLastRotatedTime = time.Time{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Otherwise treat it as an actual error.
|
||||
return fmt.Errorf("error deleting old access key: %w", err)
|
||||
}
|
||||
|
||||
s.AccessKeyId = ""
|
||||
s.SecretAccessKey = ""
|
||||
s.CredsLastRotatedTime = time.Time{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSession returns a configured AWS session for the credentials in
|
||||
// the state.
|
||||
func (s *awsCatalogPersistedState) GetSession() (*session.Session, error) {
|
||||
c, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithAccessKey(s.AccessKeyId),
|
||||
awsutil.WithSecretKey(s.SecretAccessKey),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.GetSession()
|
||||
}
|
||||
|
||||
// EC2Client returns a configured EC2 client based on the session
|
||||
// information stored in the state.
|
||||
func (s *awsCatalogPersistedState) EC2Client(region string) (*ec2.EC2, error) {
|
||||
sess, err := s.GetSession()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting AWS session: %w", err)
|
||||
}
|
||||
|
||||
return ec2.New(sess, aws.NewConfig().WithRegion(region)), nil
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAwsCatalogPersistedStateFromProto(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in *pb.HostCatalogPersisted
|
||||
expected *awsCatalogPersistedState
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "no secrets",
|
||||
in: &pb.HostCatalogPersisted{
|
||||
Secrets: nil,
|
||||
},
|
||||
expectedErr: "missing persisted secrets",
|
||||
},
|
||||
{
|
||||
name: "missing access key ID",
|
||||
in: &pb.HostCatalogPersisted{
|
||||
// Nil should be the same as empty map here
|
||||
Secrets: mustStruct(nil),
|
||||
},
|
||||
expectedErr: "persisted state integrity error: missing required value \"access_key_id\"",
|
||||
},
|
||||
{
|
||||
name: "missing secret access key",
|
||||
in: &pb.HostCatalogPersisted{
|
||||
// Nil should be the same as empty map here
|
||||
Secrets: mustStruct(map[string]interface{}{
|
||||
constAccessKeyId: "foobar",
|
||||
}),
|
||||
},
|
||||
expectedErr: "persisted state integrity error: missing required value \"secret_access_key\"",
|
||||
},
|
||||
{
|
||||
name: "bad last rotated time",
|
||||
in: &pb.HostCatalogPersisted{
|
||||
// Nil should be the same as empty map here
|
||||
Secrets: mustStruct(map[string]interface{}{
|
||||
constAccessKeyId: "foobar",
|
||||
constSecretAccessKey: "bazqux",
|
||||
constCredsLastRotatedTime: "notatime",
|
||||
}),
|
||||
},
|
||||
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: "good with non-zero timestamp",
|
||||
in: &pb.HostCatalogPersisted{
|
||||
// Nil should be the same as empty map here
|
||||
Secrets: mustStruct(map[string]interface{}{
|
||||
constAccessKeyId: "foobar",
|
||||
constSecretAccessKey: "bazqux",
|
||||
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
|
||||
}),
|
||||
},
|
||||
expected: &awsCatalogPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredsLastRotatedTime: func() time.Time {
|
||||
t, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return t
|
||||
}(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "good with zero timestamp",
|
||||
in: &pb.HostCatalogPersisted{
|
||||
// Nil should be the same as empty map here
|
||||
Secrets: mustStruct(map[string]interface{}{
|
||||
constAccessKeyId: "foobar",
|
||||
constSecretAccessKey: "bazqux",
|
||||
constCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
|
||||
}),
|
||||
},
|
||||
expected: &awsCatalogPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredsLastRotatedTime: time.Time{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
actual, err := awsCatalogPersistedStateFromProto(tc.in)
|
||||
if tc.expectedErr != "" {
|
||||
require.EqualError(err, tc.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Equal(tc.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user