2021-09-30 17:05:27 -07:00
|
|
|
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
|
2021-10-05 16:36:53 -07:00
|
|
|
|
|
|
|
|
// testOpts are options that should be used for testing only
|
|
|
|
|
testOpts []awsutil.Option
|
2021-09-30 17:05:27 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2021-10-05 16:36:53 -07:00
|
|
|
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
|
2021-09-30 17:05:27 -07:00
|
|
|
awsutil.WithAccessKey(s.AccessKeyId),
|
|
|
|
|
awsutil.WithSecretKey(s.SecretAccessKey),
|
2021-10-05 16:36:53 -07:00
|
|
|
}, s.testOpts...)...)
|
2021-09-30 17:05:27 -07:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error loading credentials: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-05 16:36:53 -07:00
|
|
|
if _, err := c.GetCallerIdentity(s.testOpts...); err != nil {
|
2021-09-30 17:05:27 -07:00
|
|
|
return fmt.Errorf("error validating credentials: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *awsCatalogPersistedState) RotateCreds() error {
|
2021-10-05 16:36:53 -07:00
|
|
|
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
|
2021-09-30 17:05:27 -07:00
|
|
|
awsutil.WithAccessKey(s.AccessKeyId),
|
|
|
|
|
awsutil.WithSecretKey(s.SecretAccessKey),
|
2021-10-05 16:36:53 -07:00
|
|
|
}, s.testOpts...)...)
|
2021-09-30 17:05:27 -07:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error loading credentials: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-05 16:36:53 -07:00
|
|
|
if err := c.RotateKeys(append([]awsutil.Option{
|
|
|
|
|
awsutil.WithValidityCheckTimeout(rotationWaitTimeout),
|
|
|
|
|
}, s.testOpts...)...); err != nil {
|
2021-09-30 17:05:27 -07:00
|
|
|
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 {
|
2021-10-05 16:36:53 -07:00
|
|
|
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
|
2021-09-30 17:05:27 -07:00
|
|
|
awsutil.WithAccessKey(s.AccessKeyId),
|
|
|
|
|
awsutil.WithSecretKey(s.SecretAccessKey),
|
2021-10-05 16:36:53 -07:00
|
|
|
}, s.testOpts...)...)
|
2021-09-30 17:05:27 -07:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error loading credentials: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-05 16:36:53 -07:00
|
|
|
if err := c.DeleteAccessKey(s.AccessKeyId, s.testOpts...); err != nil {
|
2021-09-30 17:05:27 -07:00
|
|
|
// Determine if the deletion error was due to a missing
|
|
|
|
|
// resource. If it was, just pass it.
|
2021-10-05 16:36:53 -07:00
|
|
|
var awsErr awserr.Error
|
|
|
|
|
if errors.As(err, &awsErr) {
|
|
|
|
|
if awsErr.Code() == iam.ErrCodeNoSuchEntityException {
|
|
|
|
|
s.AccessKeyId = ""
|
|
|
|
|
s.SecretAccessKey = ""
|
|
|
|
|
s.CredsLastRotatedTime = time.Time{}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2021-09-30 17:05:27 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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) {
|
2021-10-05 16:36:53 -07:00
|
|
|
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
|
2021-09-30 17:05:27 -07:00
|
|
|
awsutil.WithAccessKey(s.AccessKeyId),
|
|
|
|
|
awsutil.WithSecretKey(s.SecretAccessKey),
|
2021-10-05 16:36:53 -07:00
|
|
|
}, s.testOpts...)...)
|
2021-09-30 17:05:27 -07:00
|
|
|
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
|
|
|
|
|
}
|