feat: assume role provider credentials (#28)

* feat: assume role provider credentials

This commit updates the awsutil dependency to its latest version, which
supports the capability of using dynamic credentials. The storage service
was refactored to make static credentials optional. The credentials pkg
was refactored to support several attribute fields used for the assume
role provider. The host service still enforces the use of static
credentials.
This commit is contained in:
Damian Debkowski authored and GitHub committed 2023-07-18 16:33:24 -07:00
1 parent 334732967d
commit 07f015be28
32 files changed
+2351 -998

No files matched your search

+75 -16
View File
@@ -20,33 +20,47 @@ type CredentialAttributes struct {
// DisableCredentialRotation disables the rotation of aws secrets associated with the plugin
DisableCredentialRotation bool
// RoleArn is the role arn associated with the aws credentials
RoleArn string
// RoleExternalId is the external id associated with the aws credentials
RoleExternalId string
// RoleSessionName is the session name associated with the aws credentials
RoleSessionName string
// RoleTags is the tags associated with the aws credentials
RoleTags map[string]string
}
// GetCredentialsConfig parses values out of a protobuf struct input and returns a
// GetCredentialsConfig parses values out of a protobuf struct secrets and returns a
// CredentialsConfig 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. An error is returned if
// any unrecognized fields are found in the protobuf struct input.
func GetCredentialsConfig(in *structpb.Struct, region string) (*awsutil.CredentialsConfig, error) {
unknownFields := values.StructFields(in)
func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes, required bool) (*awsutil.CredentialsConfig, error) {
// initialize secrets if it is nil
// secrets can be nil because static credentials are optional
if secrets == nil {
secrets = &structpb.Struct{
Fields: make(map[string]*structpb.Value),
}
}
unknownFields := values.StructFields(secrets)
badFields := make(map[string]string)
accessKey, err := values.GetStringValue(in, ConstAccessKeyId, true)
accessKey, err := values.GetStringValue(secrets, ConstAccessKeyId, required)
if err != nil {
badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = err.Error()
}
delete(unknownFields, ConstAccessKeyId)
secretKey, err := values.GetStringValue(in, ConstSecretAccessKey, true)
secretKey, err := values.GetStringValue(secrets, ConstSecretAccessKey, required)
if err != nil {
badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = err.Error()
}
delete(unknownFields, ConstSecretAccessKey)
if region == "" {
badFields[fmt.Sprintf("attributes.%s", ConstRegion)] = err.Error()
}
// the creds_last_rotated_time field will be found in the input struct for
// persisted secrets, this value is not needed for creating the CredentialsConfig
delete(unknownFields, ConstCredsLastRotatedTime)
@@ -58,11 +72,32 @@ func GetCredentialsConfig(in *structpb.Struct, region string) (*awsutil.Credenti
return nil, errors.InvalidArgumentError("Error in the secrets provided", badFields)
}
return awsutil.NewCredentialsConfig(
awsutil.WithAccessKey(accessKey),
awsutil.WithSecretKey(secretKey),
awsutil.WithRegion(region),
)
opts := []awsutil.Option{
awsutil.WithEnvironmentCredentials(true),
}
if accessKey != "" && secretKey != "" {
opts = append(opts,
awsutil.WithAccessKey(accessKey),
awsutil.WithSecretKey(secretKey),
)
}
if attrs.Region != "" {
opts = append(opts, awsutil.WithRegion(attrs.Region))
}
if attrs.RoleArn != "" {
opts = append(opts, awsutil.WithRoleArn(attrs.RoleArn))
}
if attrs.RoleExternalId != "" {
opts = append(opts, awsutil.WithRoleExternalId(attrs.RoleExternalId))
}
if attrs.RoleSessionName != "" {
opts = append(opts, awsutil.WithRoleSessionName(attrs.RoleSessionName))
}
if len(attrs.RoleTags) != 0 {
opts = append(opts, awsutil.WithRoleTags(attrs.RoleTags))
}
return awsutil.NewCredentialsConfig(opts...)
}
// GetCredentialAttributes parses values out of a protobuf struct input and returns a
@@ -88,6 +123,26 @@ func GetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error)
badFields[fmt.Sprintf("attributes.%s", ConstDisableCredentialRotation)] = err.Error()
}
roleArn, err := values.GetStringValue(in, ConstRoleArn, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstRoleArn)] = err.Error()
}
roleExternalId, err := values.GetStringValue(in, ConstRoleExternalId, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstRoleExternalId)] = err.Error()
}
roleSessionName, err := values.GetStringValue(in, ConstRoleSessionName, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstRoleSessionName)] = err.Error()
}
roleTags, err := values.GetMapStringString(in, ConstRoleTags, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstRoleTags)] = err.Error()
}
if len(badFields) > 0 {
return nil, errors.InvalidArgumentError("Error in the attributes provided", badFields)
}
@@ -95,5 +150,9 @@ func GetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error)
return &CredentialAttributes{
Region: region,
DisableCredentialRotation: disableCredentialRotation,
RoleArn: roleArn,
RoleExternalId: roleExternalId,
RoleSessionName: roleSessionName,
RoleTags: roleTags,
}, nil
}
+144 -34
View File
@@ -61,6 +61,28 @@ func TestGetCredentialAttributes(t *testing.T) {
DisableCredentialRotation: true,
},
},
{
name: "with assume role",
in: map[string]any{
ConstRegion: "us-west-2",
ConstRoleArn: "arn:aws:iam::123456789012:role/S3Access",
ConstRoleExternalId: "1234567890",
ConstRoleSessionName: "test-session",
ConstRoleTags: map[string]interface{}{
"foo": "bar",
},
},
expected: &CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
},
}
for _, tc := range cases {
@@ -89,44 +111,91 @@ func TestGetCredentialAttributes(t *testing.T) {
func TestGetCredentialsConfig(t *testing.T) {
cases := []struct {
name string
in map[string]any
region string
secrets *structpb.Struct
required bool
attrs *CredentialAttributes
expected *awsutil.CredentialsConfig
expectedErrContains string
}{
{
name: "missing access_key_id",
in: map[string]any{},
region: "us-west-2",
expectedErrContains: "missing required value \"access_key_id\"",
name: "no secrets",
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &awsutil.CredentialsConfig{
Region: "us-west-2",
},
},
{
name: "missing secret_access_key",
in: map[string]any{
ConstAccessKeyId: "foobar",
name: "with static credentials",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
region: "us-west-2",
expectedErrContains: "missing required value \"secret_access_key\"",
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
},
{
name: "missing access key",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
required: true,
expectedErrContains: "secrets.access_key_id: missing required value",
},
{
name: "missing secret key",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foobar"),
},
},
required: true,
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expectedErrContains: "secrets.secret_access_key: missing required value",
},
{
name: "unknown fields",
in: map[string]any{
ConstAccessKeyId: "foobar",
ConstSecretAccessKey: "bazqux",
"foo": true,
"bar": true,
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
"foo": structpb.NewBoolValue(true),
"bar": structpb.NewBoolValue(true),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
region: "us-west-2",
expectedErrContains: "secrets.bar: unrecognized field, secrets.foo: unrecognized field",
},
{
name: "valid ignore creds_last_rotated_time",
in: map[string]any{
ConstAccessKeyId: "foobar",
ConstSecretAccessKey: "bazqux",
ConstCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
region: "us-west-2",
expected: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
@@ -134,16 +203,56 @@ func TestGetCredentialsConfig(t *testing.T) {
},
},
{
name: "good",
in: map[string]any{
ConstAccessKeyId: "foobar",
ConstSecretAccessKey: "bazqux",
name: "with assume role",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
region: "us-west-2",
expected: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
Region: "us-west-2",
RoleARN: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
},
{
name: "with static credential & assume role",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
expected: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
RoleARN: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
},
}
@@ -153,10 +262,7 @@ func TestGetCredentialsConfig(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
input, err := structpb.NewStruct(tc.in)
require.NoError(err)
actual, err := GetCredentialsConfig(input, tc.region)
actual, err := GetCredentialsConfig(tc.secrets, tc.attrs, tc.required)
if tc.expectedErrContains != "" {
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
@@ -168,6 +274,10 @@ func TestGetCredentialsConfig(t *testing.T) {
require.Equal(tc.expected.AccessKey, actual.AccessKey)
require.Equal(tc.expected.SecretKey, actual.SecretKey)
require.Equal(tc.expected.Region, actual.Region)
require.Equal(tc.expected.RoleARN, actual.RoleARN)
require.Equal(tc.expected.RoleExternalId, actual.RoleExternalId)
require.Equal(tc.expected.RoleSessionName, actual.RoleSessionName)
require.Equal(tc.expected.RoleTags, actual.RoleTags)
})
}
}
+12
View File
@@ -18,4 +18,16 @@ const (
// ConstCredsLastRotatedTime is the key for the last rotated time in the aws credentials.
ConstCredsLastRotatedTime = "creds_last_rotated_time"
// ConstRoleArn is the key for assuming a IAM role.
ConstRoleArn = "role_arn"
// ConstRoleExternalId is the key for the external id used for assuming a IAM role.
ConstRoleExternalId = "role_external_id"
// ConstRoleSessionName is the key for the session name used for assuming a IAM role.
ConstRoleSessionName = "role_session_name"
// ConstRoleTags is the key for the tags used for assuming a IAM role.
ConstRoleTags = "role_tags"
)
+94 -113
View File
@@ -6,6 +6,7 @@ package credential
import (
"errors"
"fmt"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws/awserr"
@@ -25,19 +26,13 @@ type AwsCredentialPersistedStateOption func(s *AwsCredentialPersistedState) erro
// 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
// CredentialsConfig is the credential configuration for the AWS credential.
CredentialsConfig *awsutil.CredentialsConfig
// 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
@@ -50,31 +45,19 @@ func WithStateTestOpts(opts []awsutil.Option) AwsCredentialPersistedStateOption
}
}
// WithAccessKeyId sets the value for AccesskeyId in the storage persisted state.
func WithAccessKeyId(x string) AwsCredentialPersistedStateOption {
// WithCredentialsConfig sets the value for CredentialsConfig in the credential persisted state.
func WithCredentialsConfig(x *awsutil.CredentialsConfig) AwsCredentialPersistedStateOption {
return func(s *AwsCredentialPersistedState) error {
if s.AccessKeyId != "" {
return errors.New("access key id already set")
if s.CredentialsConfig != nil {
return errors.New("credentials config already set")
}
s.AccessKeyId = x
s.CredentialsConfig = 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.
// WithCredsLastRotatedTime sets the value for CredsLastRotatedTime in the credential persisted state.
func WithCredsLastRotatedTime(t time.Time) AwsCredentialPersistedStateOption {
return func(s *AwsCredentialPersistedState) error {
if !s.CredsLastRotatedTime.IsZero() {
@@ -86,18 +69,6 @@ func WithCredsLastRotatedTime(t time.Time) AwsCredentialPersistedStateOption {
}
}
// 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.
@@ -108,27 +79,19 @@ func NewAwsCredentialPersistedState(opts ...AwsCredentialPersistedStateOption) (
return nil, err
}
}
return s, nil
}
// ValidateCreds takes the access key and secret key from the persisted state
// ValidateCreds takes the credentials configuration 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 s.CredentialsConfig == nil {
return errors.New("missing credentials config")
}
if _, err := c.GetCallerIdentity(s.testOpts...); err != nil {
if _, err := s.CredentialsConfig.GetCallerIdentity(s.testOpts...); err != nil {
return fmt.Errorf("error validating credentials: %w", err)
}
return nil
}
@@ -138,42 +101,27 @@ func (s *AwsCredentialPersistedState) ValidateCreds() error {
// 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 s.CredentialsConfig == nil {
return errors.New("missing credentials config")
}
if err := c.RotateKeys(append([]awsutil.Option{
if err := s.CredentialsConfig.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")
func (s *AwsCredentialPersistedState) ReplaceCreds(credentialsConfig *awsutil.CredentialsConfig) error {
if credentialsConfig == nil {
return errors.New("missing new credentials config")
}
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.CredentialsConfig == nil {
return errors.New("missing credentials config")
}
if !s.CredsLastRotatedTime.IsZero() {
@@ -186,8 +134,7 @@ func (s *AwsCredentialPersistedState) ReplaceCreds(accessKeyId, secretAccessKey
}
// Set the new attributes and clear the rotated time.
s.AccessKeyId = accessKeyId
s.SecretAccessKey = secretAccessKey
s.CredentialsConfig = credentialsConfig
s.CredsLastRotatedTime = time.Time{}
return nil
}
@@ -196,23 +143,17 @@ func (s *AwsCredentialPersistedState) ReplaceCreds(accessKeyId, secretAccessKey
// 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 s.CredentialsConfig == nil {
return errors.New("missing credentials config")
}
if err := c.DeleteAccessKey(s.AccessKeyId, s.testOpts...); err != nil {
if err := s.CredentialsConfig.DeleteAccessKey(s.CredentialsConfig.AccessKey, 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.CredentialsConfig = nil
s.CredsLastRotatedTime = time.Time{}
return nil
}
@@ -222,8 +163,7 @@ func (s *AwsCredentialPersistedState) DeleteCreds() error {
return err
}
s.AccessKeyId = ""
s.SecretAccessKey = ""
s.CredentialsConfig = nil
s.CredsLastRotatedTime = time.Time{}
return nil
}
@@ -231,50 +171,50 @@ func (s *AwsCredentialPersistedState) DeleteCreds() error {
// 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()
return s.CredentialsConfig.GetSession(s.testOpts...)
}
// 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
// ToMap returns a map of the credentials stored in the persisted state.
// ToMap will return an empty map for temporary credentials. ToMap will
// return a map for long-term credentials with following keys:
// access_key_id, secret_access_key & creds_last_rotated_time
func (s *AwsCredentialPersistedState) ToMap() map[string]any {
if HasDynamicCredentials(s.CredentialsConfig.AccessKey) {
return map[string]any{}
}
return map[string]any{
ConstAccessKeyId: s.AccessKeyId,
ConstSecretAccessKey: s.SecretAccessKey,
ConstAccessKeyId: s.CredentialsConfig.AccessKey,
ConstSecretAccessKey: s.CredentialsConfig.SecretKey,
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")
func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *CredentialAttributes, opts ...AwsCredentialPersistedStateOption) (*AwsCredentialPersistedState, error) {
// initialize secrets if it is nil
// secrets can be nil because static credentials are optional
if secrets == nil {
secrets = &structpb.Struct{
Fields: map[string]*structpb.Value{},
}
}
accessKeyId, err := values.GetStringValue(in, ConstAccessKeyId, true)
if attrs == nil {
return nil, fmt.Errorf("missing credential attributes")
}
accessKeyId, err := values.GetStringValue(secrets, ConstAccessKeyId, false)
if err != nil {
return nil, fmt.Errorf("persisted state integrity error: %w", err)
}
secretAccessKey, err := values.GetStringValue(in, ConstSecretAccessKey, true)
secretAccessKey, err := values.GetStringValue(secrets, ConstSecretAccessKey, false)
if err != nil {
return nil, fmt.Errorf("persisted state integrity error: %w", err)
}
credsLastRotatedTime, err := values.GetTimeValue(in, ConstCredsLastRotatedTime)
credsLastRotatedTime, err := values.GetTimeValue(secrets, ConstCredsLastRotatedTime)
if err != nil {
return nil, fmt.Errorf("persisted state integrity error: %w", err)
}
@@ -284,9 +224,50 @@ func AwsCredentialPersistedStateFromProto(in *structpb.Struct, opts ...AwsCreden
return nil, err
}
s.AccessKeyId = accessKeyId
s.SecretAccessKey = secretAccessKey
awsOpts := append([]awsutil.Option{}, s.testOpts...)
if accessKeyId != "" && secretAccessKey != "" {
awsOpts = append(awsOpts,
awsutil.WithAccessKey(accessKeyId),
awsutil.WithSecretKey(secretAccessKey),
)
}
if attrs.Region != "" {
awsOpts = append(awsOpts, awsutil.WithRegion(attrs.Region))
}
if attrs.RoleArn != "" {
awsOpts = append(awsOpts, awsutil.WithRoleArn(attrs.RoleArn))
}
if attrs.RoleExternalId != "" {
awsOpts = append(awsOpts, awsutil.WithRoleExternalId(attrs.RoleExternalId))
}
if attrs.RoleSessionName != "" {
awsOpts = append(awsOpts, awsutil.WithRoleSessionName(attrs.RoleSessionName))
}
if len(attrs.RoleTags) != 0 {
awsOpts = append(awsOpts, awsutil.WithRoleTags(attrs.RoleTags))
}
credentialsConfig, err := awsutil.NewCredentialsConfig(awsOpts...)
if err != nil {
return nil, err
}
s.CredentialsConfig = credentialsConfig
s.CredsLastRotatedTime = credsLastRotatedTime
return s, nil
}
// HasDynamicCredentials returns true if the access key is a dynamic credential type.
//
// https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html#sec-access-keys-and-secret-access-keys
// Access key IDs beginning with ASIA are temporary credentials access keys that you create using AWS STS operations.
func HasDynamicCredentials(accessKey string) bool {
return strings.HasPrefix(accessKey, "ASIA")
}
// HasStaticCredentials returns true if the access key is a static credential type.
//
// https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html#sec-access-keys-and-secret-access-keys
// Access key IDs beginning with AKIA are long-term access keys for an IAM user or an AWS account root user.
func HasStaticCredentials(accessKey string) bool {
return strings.HasPrefix(accessKey, "AKIA")
}
+434 -193
View File
@@ -36,21 +36,22 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
expectedErr: testOptionErr,
},
{
name: "access key id",
name: "with credentials config",
opts: []AwsCredentialPersistedStateOption{
WithAccessKeyId("foobar"),
WithCredentialsConfig(
&awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
),
},
expected: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
},
},
{
name: "secret access key",
opts: []AwsCredentialPersistedStateOption{
WithSecretAccessKey("bazqux"),
},
expected: &AwsCredentialPersistedState{
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
},
},
{
@@ -63,29 +64,12 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
},
},
{
name: "region key",
name: "double set credentials config",
opts: []AwsCredentialPersistedStateOption{
WithRegion("foobar"),
WithCredentialsConfig(&awsutil.CredentialsConfig{}),
WithCredentialsConfig(&awsutil.CredentialsConfig{}),
},
expected: &AwsCredentialPersistedState{
region: "foobar",
},
},
{
name: "double set access key id",
opts: []AwsCredentialPersistedStateOption{
WithAccessKeyId("foobar"),
WithAccessKeyId("onetwo"),
},
expectedErr: "access key id already set",
},
{
name: "double set secret access key",
opts: []AwsCredentialPersistedStateOption{
WithSecretAccessKey("bazqux"),
WithSecretAccessKey("threefour"),
},
expectedErr: "secret access key already set",
expectedErr: "credentials config already set",
},
{
name: "double set rotation time",
@@ -95,14 +79,6 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
},
expectedErr: "last rotation time already set",
},
{
name: "double set region",
opts: []AwsCredentialPersistedStateOption{
WithRegion("foobar"),
WithRegion("barfoo"),
},
expectedErr: "region already set",
},
}
for _, tc := range cases {
@@ -116,7 +92,13 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
}
require.NoError(err)
require.Equal(tc.expected, actual)
require.Equal(tc.expected.CredsLastRotatedTime, actual.CredsLastRotatedTime)
if tc.expected.CredentialsConfig != nil {
require.NotNil(actual.CredentialsConfig)
require.Equal(tc.expected.CredentialsConfig.AccessKey, actual.CredentialsConfig.AccessKey)
require.Equal(tc.expected.CredentialsConfig.SecretKey, actual.CredentialsConfig.SecretKey)
require.Equal(tc.expected.CredentialsConfig.Region, actual.CredentialsConfig.Region)
}
})
}
}
@@ -124,43 +106,95 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
cases := []struct {
name string
in map[string]any
secrets *structpb.Struct
attrs *CredentialAttributes
opts []AwsCredentialPersistedStateOption
expected *AwsCredentialPersistedState
expectedErr string
}{
{
name: "no secrets",
in: nil,
expectedErr: "missing persisted secrets",
name: "missing credential attributes",
expectedErr: "missing credential attributes",
},
{
name: "missing access key ID",
in: map[string]any{},
expectedErr: "persisted state integrity error: missing required value \"access_key_id\"",
},
{
name: "missing secret access key",
in: map[string]any{
ConstAccessKeyId: "foobar",
name: "missing secrets",
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
Region: "us-west-2",
},
},
},
{
name: "missing long-term access key id",
secrets: &structpb.Struct{},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
Region: "us-west-2",
},
},
},
{
name: "missing long-term secret access key",
secrets: &structpb.Struct{},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
Region: "us-west-2",
},
},
},
{
name: "with long-term static credentials",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
Region: "us-west-2",
AccessKey: "foobar",
SecretKey: "bazqux",
},
},
expectedErr: "persisted state integrity error: missing required value \"secret_access_key\"",
},
{
name: "bad last rotated time",
in: map[string]any{
ConstAccessKeyId: "foobar",
ConstSecretAccessKey: "bazqux",
ConstCredsLastRotatedTime: "notatime",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
ConstCredsLastRotatedTime: structpb.NewStringValue("notatime"),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
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: "option error",
in: map[string]any{
ConstAccessKeyId: "foobar",
ConstSecretAccessKey: "bazqux",
ConstCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
opts: []AwsCredentialPersistedStateOption{
func(s *AwsCredentialPersistedState) error {
@@ -171,14 +205,22 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
},
{
name: "good with non-zero timestamp",
in: map[string]any{
ConstAccessKeyId: "foobar",
ConstSecretAccessKey: "bazqux",
ConstCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
CredsLastRotatedTime: func() time.Time {
t, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00")
if err != nil {
@@ -190,33 +232,79 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
},
{
name: "good with zero timestamp",
in: map[string]any{
ConstAccessKeyId: "foobar",
ConstSecretAccessKey: "bazqux",
ConstCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
ConstCredsLastRotatedTime: structpb.NewStringValue((time.Time{}).Format(time.RFC3339Nano)),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
},
expected: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
CredsLastRotatedTime: time.Time{},
},
},
{
name: "good (ignoring non-test options)",
in: map[string]any{
ConstAccessKeyId: "foobar",
ConstSecretAccessKey: "bazqux",
ConstCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
},
opts: []AwsCredentialPersistedStateOption{
WithAccessKeyId("ignored"),
WithRegion("us-west-2"),
name: "with assume role",
attrs: &CredentialAttributes{
Region: "us-west-2",
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
expected: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
Region: "us-west-2",
RoleARN: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
},
},
{
name: "with assume role & static credentials",
secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
ConstCredsLastRotatedTime: structpb.NewStringValue((time.Time{}).Format(time.RFC3339Nano)),
},
},
attrs: &CredentialAttributes{
Region: "us-west-2",
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
expected: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
RoleARN: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
CredsLastRotatedTime: time.Time{},
region: "us-west-2",
},
},
}
@@ -226,55 +314,67 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
var (
input *structpb.Struct
err error
)
if tc.in != nil {
input, err = structpb.NewStruct(tc.in)
require.NoError(err)
}
actual, err := AwsCredentialPersistedStateFromProto(input, tc.opts...)
actual, err := AwsCredentialPersistedStateFromProto(tc.secrets, tc.attrs, tc.opts...)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected.AccessKeyId, actual.AccessKeyId)
require.Equal(tc.expected.SecretAccessKey, actual.SecretAccessKey)
require.Equal(tc.expected.region, actual.region)
require.NotNil(actual)
require.Equal(tc.expected.CredsLastRotatedTime, actual.CredsLastRotatedTime)
if tc.expected.CredentialsConfig != nil {
require.NotNil(actual.CredentialsConfig)
require.Equal(tc.expected.CredentialsConfig.AccessKey, actual.CredentialsConfig.AccessKey)
require.Equal(tc.expected.CredentialsConfig.SecretKey, actual.CredentialsConfig.SecretKey)
require.Equal(tc.expected.CredentialsConfig.Region, actual.CredentialsConfig.Region)
require.Equal(tc.expected.CredentialsConfig.RoleARN, actual.CredentialsConfig.RoleARN)
require.Equal(tc.expected.CredentialsConfig.RoleExternalId, actual.CredentialsConfig.RoleExternalId)
require.Equal(tc.expected.CredentialsConfig.RoleSessionName, actual.CredentialsConfig.RoleSessionName)
require.Equal(tc.expected.CredentialsConfig.RoleTags, actual.CredentialsConfig.RoleTags)
}
})
}
}
func TestAwsCatalogPersistedStateValidateCreds(t *testing.T) {
func TestAwsCatalogPersistedState_ValidateCreds(t *testing.T) {
cases := []struct {
name string
in *AwsCredentialPersistedState
expectedErr string
}{
{
name: "could not load credentials",
in: &AwsCredentialPersistedState{
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
},
expectedErr: fmt.Sprintf("error loading credentials: error reading options in NewCredentialsConfig: %s", testOptionErr),
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing credentials config",
},
{
name: "validation error",
in: &AwsCredentialPersistedState{
testOpts: []awsutil.Option{awsutil.WithSTSAPIFunc(awsutil.NewMockSTS(awsutil.WithGetCallerIdentityError(errors.New(testGetCallerIdentityErr))))},
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
testOpts: []awsutil.Option{
awsutil.WithSTSAPIFunc(
awsutil.NewMockSTS(
awsutil.WithGetCallerIdentityError(errors.New(testGetCallerIdentityErr)),
),
),
},
},
expectedErr: fmt.Sprintf("error validating credentials: %s", testGetCallerIdentityErr),
},
{
name: "good",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
testOpts: []awsutil.Option{awsutil.WithSTSAPIFunc(awsutil.NewMockSTS())},
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
testOpts: []awsutil.Option{awsutil.WithSTSAPIFunc(awsutil.NewMockSTS())},
},
},
}
@@ -294,27 +394,27 @@ func TestAwsCatalogPersistedStateValidateCreds(t *testing.T) {
}
}
func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) {
func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) {
cases := []struct {
name string
in *AwsCredentialPersistedState
expectedAccessKeyId string
expectedSecretAccessKey string
exepected *awsutil.CredentialsConfig
expectedNonZeroRotatedTime bool
expectedErr string
}{
{
name: "could not load credentials",
in: &AwsCredentialPersistedState{
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
},
expectedErr: fmt.Sprintf("error loading credentials: error reading options in NewCredentialsConfig: %s", testOptionErr),
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing credentials config",
},
{
name: "rotation error",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
@@ -328,8 +428,11 @@ func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) {
{
name: "good",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
Region: "us-west-2",
},
testOpts: []awsutil.Option{
awsutil.WithSTSAPIFunc(
awsutil.NewMockSTS(),
@@ -358,8 +461,11 @@ func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) {
),
},
},
expectedAccessKeyId: "one",
expectedSecretAccessKey: "two",
exepected: &awsutil.CredentialsConfig{
AccessKey: "one",
SecretKey: "two",
Region: "us-west-2",
},
expectedNonZeroRotatedTime: true,
},
}
@@ -375,52 +481,42 @@ func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) {
}
require.NoError(err)
require.Equal(tc.expectedAccessKeyId, tc.in.AccessKeyId)
require.Equal(tc.expectedSecretAccessKey, tc.in.SecretAccessKey)
require.Equal(tc.exepected.AccessKey, tc.in.CredentialsConfig.AccessKey)
require.Equal(tc.exepected.SecretKey, tc.in.CredentialsConfig.SecretKey)
require.Equal(tc.expectedNonZeroRotatedTime, !tc.in.CredsLastRotatedTime.IsZero())
})
}
}
func TestAwsCatalogPersistedStateReplaceCreds(t *testing.T) {
func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) {
// NOTE: Not safe to run this test in parallel
state := new(testMockIAMState)
cases := []struct {
name string
in *AwsCredentialPersistedState
accessKeyId string
secretAccessKey string
expectedAccessKeyId string
expectedSecretAccessKey string
expectedDeleteCalled bool
expectedErr string
name string
in *AwsCredentialPersistedState
credentialConfig *awsutil.CredentialsConfig
expected *awsutil.CredentialsConfig
expectedDeleteCalled bool
expectedErr string
}{
{
name: "missing access key id",
name: "missing new credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "access key id cannot be empty",
expectedErr: "missing new credentials config",
},
{
name: "missing access key id",
in: &AwsCredentialPersistedState{},
accessKeyId: "one",
expectedErr: "secret access key cannot be empty",
},
{
name: "identical access key id",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
},
accessKeyId: "foobar",
secretAccessKey: "bazqux",
expectedErr: "attempting to replace access key with the same one",
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
credentialConfig: &awsutil.CredentialsConfig{},
expectedErr: "missing credentials config",
},
{
name: "deletion error",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
},
CredsLastRotatedTime: time.Now(),
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
@@ -430,15 +526,16 @@ func TestAwsCatalogPersistedStateReplaceCreds(t *testing.T) {
),
},
},
accessKeyId: "one",
secretAccessKey: "two",
expectedErr: fmt.Sprintf("error deleting old access key: %s", testDeleteAccessKeyErr),
credentialConfig: &awsutil.CredentialsConfig{},
expectedErr: fmt.Sprintf("error deleting old access key: %s", testDeleteAccessKeyErr),
},
{
name: "good with delete of old rotated",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
},
CredsLastRotatedTime: time.Now(),
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
@@ -446,28 +543,38 @@ func TestAwsCatalogPersistedStateReplaceCreds(t *testing.T) {
),
},
},
accessKeyId: "one",
secretAccessKey: "two",
expectedAccessKeyId: "one",
expectedSecretAccessKey: "two",
expectedDeleteCalled: true,
credentialConfig: &awsutil.CredentialsConfig{
AccessKey: "one",
SecretKey: "two",
},
expected: &awsutil.CredentialsConfig{
AccessKey: "one",
SecretKey: "two",
},
expectedDeleteCalled: true,
},
{
name: "good without delete of old rotated",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
},
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
newTestMockIAM(state),
),
},
},
accessKeyId: "one",
secretAccessKey: "two",
expectedAccessKeyId: "one",
expectedSecretAccessKey: "two",
expectedDeleteCalled: false,
credentialConfig: &awsutil.CredentialsConfig{
AccessKey: "one",
SecretKey: "two",
},
expected: &awsutil.CredentialsConfig{
AccessKey: "one",
SecretKey: "two",
},
expectedDeleteCalled: false,
},
}
@@ -476,39 +583,39 @@ func TestAwsCatalogPersistedStateReplaceCreds(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
state.Reset()
err := tc.in.ReplaceCreds(tc.accessKeyId, tc.secretAccessKey)
err := tc.in.ReplaceCreds(tc.credentialConfig)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expectedAccessKeyId, tc.in.AccessKeyId)
require.Equal(tc.expectedSecretAccessKey, tc.in.SecretAccessKey)
require.Equal(tc.expected.AccessKey, tc.in.CredentialsConfig.AccessKey)
require.Equal(tc.expected.SecretKey, tc.in.CredentialsConfig.SecretKey)
require.Zero(tc.in.CredsLastRotatedTime)
require.Equal(tc.expectedDeleteCalled, state.DeleteAccessKeyCalled)
})
}
}
func TestAwsCatalogPersistedStateDeleteCreds(t *testing.T) {
func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) {
cases := []struct {
name string
in *AwsCredentialPersistedState
expectedErr string
}{
{
name: "could not load credentials",
in: &AwsCredentialPersistedState{
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
},
expectedErr: fmt.Sprintf("error loading credentials: error reading options in NewCredentialsConfig: %s", testOptionErr),
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing credentials config",
},
{
name: "deletion error",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
},
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
@@ -522,8 +629,10 @@ func TestAwsCatalogPersistedStateDeleteCreds(t *testing.T) {
{
name: "deletion error, but OK because key was just gone",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
},
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
@@ -538,8 +647,10 @@ func TestAwsCatalogPersistedStateDeleteCreds(t *testing.T) {
{
name: "good",
in: &AwsCredentialPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
},
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(),
@@ -560,19 +671,149 @@ func TestAwsCatalogPersistedStateDeleteCreds(t *testing.T) {
}
require.NoError(err)
require.Empty(tc.in.AccessKeyId)
require.Empty(tc.in.SecretAccessKey)
require.Nil(tc.in.CredentialsConfig)
require.Zero(tc.in.CredsLastRotatedTime)
})
}
}
func TestAwsCatalogPersistedStateGetSessionErr(t *testing.T) {
func TestAwsCatalogPersistedState_GetSession(t *testing.T) {
require := require.New(t)
state := &AwsCredentialPersistedState{
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
cases := []struct {
name string
in *AwsCredentialPersistedState
expectedErr string
}{
{
name: "error",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "foobar",
SecretKey: "bazqux",
},
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
},
expectedErr: fmt.Sprintf("error reading options in GetSession: %s", testOptionErr),
},
{
name: "static credentials",
in: &AwsCredentialPersistedState{
CredentialsConfig: &awsutil.CredentialsConfig{
AccessKey: "AKIA_foobar",
SecretKey: "bazqux",
},
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(),
),
},
},
},
{
name: "assume role",
in: &AwsCredentialPersistedState{
CredentialsConfig: func() *awsutil.CredentialsConfig {
config, err := awsutil.NewCredentialsConfig(
awsutil.WithRegion("us-west-2"),
awsutil.WithRoleArn("arn:aws:iam::123456789012:role/S3Access"),
awsutil.WithRoleExternalId("1234567890"),
awsutil.WithRoleSessionName("assume-role"),
awsutil.WithRoleTags(map[string]string{
"foo": "bar",
}),
)
require.NoError(err)
return config
}(),
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(),
),
},
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
_, err := tc.in.GetSession()
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
})
}
}
func TestHasStaticCredentials(t *testing.T) {
require := require.New(t)
cases := []struct {
name string
accessKey string
expected bool
}{
{
name: "dynamic credential",
accessKey: "ASIAEXAMPLE",
expected: false,
},
{
name: "static credential",
accessKey: "AKIAEXAMPLE",
expected: true,
},
{
name: "other credential",
accessKey: "EXAMPLE",
expected: false,
},
{
name: "empty",
accessKey: "",
expected: false,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require.Equal(tc.expected, HasStaticCredentials(tc.accessKey))
})
}
}
func TestHasDynamicCredentials(t *testing.T) {
require := require.New(t)
cases := []struct {
name string
accessKey string
expected bool
}{
{
name: "dynamic credential",
accessKey: "ASIAEXAMPLE",
expected: true,
},
{
name: "static credential",
accessKey: "AKIAEXAMPLE",
expected: false,
},
{
name: "other credential",
accessKey: "EXAMPLE",
expected: false,
},
{
name: "empty",
accessKey: "",
expected: false,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require.Equal(tc.expected, HasDynamicCredentials(tc.accessKey))
})
}
_, err := state.GetSession()
require.EqualError(err, fmt.Sprintf("error reading options in NewCredentialsConfig: %s", testOptionErr))
}
+37
View File
@@ -8,6 +8,7 @@ import (
"github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/iam/iamiface"
"github.com/hashicorp/go-secure-stdlib/awsutil"
"google.golang.org/protobuf/types/known/structpb"
)
const (
@@ -48,3 +49,39 @@ func newTestMockIAM(state *testMockIAMState, opts ...awsutil.MockIAMOption) awsu
return m, err
}
}
// MockStaticCredentialSecrets returns a *structpb.Struct that contains two
// key pair values: (access_key_id, AKIA_foobar) & (secret_access_key, bazqux)
func MockStaticCredentialSecrets() *structpb.Struct {
return &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
}
}
// MockAssumeRoleAttributes returns a *structpb.Struct that contains six key pair values:
//
// (region, region)
// (disable_credential_rotation, disableRotate)
// (role_arn, arn:aws:iam::123456789012:role/S3Access)
// (role_external_id, 1234567890)
// (role_session_name, ec2-assume-role-provider)
// (role_tags, struct{foo:bar})
func MockAssumeRoleAttributes(region string, disableRotate bool) *structpb.Struct {
return &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstRegion: structpb.NewStringValue(region),
ConstDisableCredentialRotation: structpb.NewBoolValue(disableRotate),
ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
ConstRoleExternalId: structpb.NewStringValue("1234567890"),
ConstRoleSessionName: structpb.NewStringValue("ec2-assume-role-provider"),
ConstRoleTags: structpb.NewStructValue(&structpb.Struct{
Fields: map[string]*structpb.Value{
"foo": structpb.NewStringValue("bar"),
},
}),
},
}
}
+27
View File
@@ -84,6 +84,33 @@ func GetTimeValue(in *structpb.Struct, k string) (time.Time, error) {
return t, nil
}
// GetMapStringString returns a map[string]string value and no error if the given key
// is found in the provided proto struct input. An error is returned if the key
// is not found or the value type is not map[string]string.
func GetMapStringString(in *structpb.Struct, k string, required bool) (map[string]string, error) {
mv := in.GetFields()
v, ok := mv[k]
if !ok {
if required {
return nil, fmt.Errorf("missing required value %q", k)
}
return nil, nil
}
result := map[string]string{}
m, ok := v.AsInterface().(map[string]any)
if !ok {
return nil, fmt.Errorf("unexpected type for value %q: want map[string]string, got %T", k, v.AsInterface())
}
for mapKey, mapValue := range m {
e, ok := mapValue.(string)
if !ok {
return nil, fmt.Errorf("unexpected type for value in map[%q]: want string, got %T", mapKey, mapValue)
}
result[mapKey] = e
}
return result, nil
}
// StructFields returns a map[string]struct{} of the
// proto struct input.
func StructFields(s *structpb.Struct) map[string]struct{} {
+92
View File
@@ -208,3 +208,95 @@ func TestGetTimeValue(t *testing.T) {
})
}
}
func TestGetMapStringString(t *testing.T) {
cases := []struct {
name string
in *structpb.Struct
key string
required bool
expected map[string]string
expectedErr string
}{
{
name: "required missing",
in: &structpb.Struct{},
key: "foo",
required: true,
expectedErr: "missing required value \"foo\"",
},
{
name: "optional missing",
in: &structpb.Struct{},
key: "foo",
},
{
name: "non-map[string]string value",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"foo": structpb.NewBoolValue(true),
},
},
key: "foo",
expectedErr: "unexpected type for value \"foo\": want map[string]string, got bool",
},
{
name: "empty map",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"foo": structpb.NewStructValue(&structpb.Struct{
Fields: map[string]*structpb.Value{},
}),
},
},
key: "foo",
expected: map[string]string{},
},
{
name: "non-string value in map",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"foo": structpb.NewStructValue(&structpb.Struct{
Fields: map[string]*structpb.Value{
"bar": structpb.NewBoolValue(true),
},
}),
},
},
key: "foo",
expectedErr: "unexpected type for value in map[\"bar\"]: want string, got bool",
},
{
name: "good",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"foo": structpb.NewStructValue(&structpb.Struct{
Fields: map[string]*structpb.Value{
"bar": structpb.NewStringValue("zoo"),
},
}),
},
},
key: "foo",
expected: map[string]string{
"bar": "zoo",
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
actual, err := GetMapStringString(tc.in, tc.key, tc.required)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})
}
}