290 lines
9.3 KiB
Go
290 lines
9.3 KiB
Go
package credential
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
|
"github.com/aws/aws-sdk-go/aws/session"
|
|
"github.com/aws/aws-sdk-go/service/iam"
|
|
"github.com/hashicorp/boundary-plugin-aws/internal/values"
|
|
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
|
"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
|
|
|
|
type AwsCredentialPersistedStateOption func(s *AwsCredentialPersistedState) error
|
|
|
|
// 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
|
|
// 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
|
|
// when using CredentialsConfig. This should never be used in
|
|
// production code. This should only be used in unit tests.
|
|
func WithStateTestOpts(opts []awsutil.Option) AwsCredentialPersistedStateOption {
|
|
return func(s *AwsCredentialPersistedState) error {
|
|
s.testOpts = opts
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithAccessKeyId sets the value for AccesskeyId in the storage persisted state.
|
|
func WithAccessKeyId(x string) AwsCredentialPersistedStateOption {
|
|
return func(s *AwsCredentialPersistedState) error {
|
|
if s.AccessKeyId != "" {
|
|
return errors.New("access key id already set")
|
|
}
|
|
|
|
s.AccessKeyId = 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.
|
|
func WithCredsLastRotatedTime(t time.Time) AwsCredentialPersistedStateOption {
|
|
return func(s *AwsCredentialPersistedState) error {
|
|
if !s.CredsLastRotatedTime.IsZero() {
|
|
return errors.New("last rotation time already set")
|
|
}
|
|
|
|
s.CredsLastRotatedTime = t
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
func NewAwsCredentialPersistedState(opts ...AwsCredentialPersistedStateOption) (*AwsCredentialPersistedState, error) {
|
|
s := new(AwsCredentialPersistedState)
|
|
for _, opt := range opts {
|
|
if err := opt(s); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// ValidateCreds takes the access key and secret key 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 _, err := c.GetCallerIdentity(s.testOpts...); err != nil {
|
|
return fmt.Errorf("error validating credentials: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RotateCreds takes the access key and secret key from the persisted state and creates a new access/secret key,
|
|
// then deletes the old access key. If deletion of the old access key is successful, the new access key/secret key are
|
|
// written into the credentials config and the persisted state. On any error, the old credentials are not overwritten.
|
|
// 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 err := c.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")
|
|
}
|
|
|
|
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 *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 err := c.DeleteAccessKey(s.AccessKeyId, 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.CredsLastRotatedTime = time.Time{}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Otherwise treat it as an actual error.
|
|
return 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 *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()
|
|
}
|
|
|
|
// 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
|
|
func (s *AwsCredentialPersistedState) ToMap() map[string]any {
|
|
return map[string]any{
|
|
ConstAccessKeyId: s.AccessKeyId,
|
|
ConstSecretAccessKey: s.SecretAccessKey,
|
|
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")
|
|
}
|
|
|
|
accessKeyId, err := values.GetStringValue(in, ConstAccessKeyId, true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
|
}
|
|
|
|
secretAccessKey, err := values.GetStringValue(in, ConstSecretAccessKey, true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
|
}
|
|
|
|
credsLastRotatedTime, err := values.GetTimeValue(in, ConstCredsLastRotatedTime)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
|
}
|
|
|
|
s, err := NewAwsCredentialPersistedState(opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s.AccessKeyId = accessKeyId
|
|
s.SecretAccessKey = secretAccessKey
|
|
s.CredsLastRotatedTime = credsLastRotatedTime
|
|
|
|
return s, nil
|
|
}
|