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

+11 -5
View File
@@ -42,7 +42,7 @@ Example policy:
```
* `iam:GetUser`, `iam:CreateAccessKey`, and `iam:DeleteAccessKey`, configured to
the IAM user to allow credential rotation. Example policy:
the IAM user to allow static credential rotation. Example policy:
```
{
@@ -66,18 +66,24 @@ Example policy:
The following `attributes` are valid on an AWS storage bucket resource:
- `disable_credential_rotation` (bool): If `true`, credential rotation will not
be performed. See the [Credential Rotation](../../../README.md#credential-rotation) section
above.
be performed.
- `region` (string): The region to configure the storage bucket for.
- `endpoint-url` (string): The endpoint to configure the storage.
- `role_arn` (string): The role arn configured for the assume role provider.
- `role_external_id` (string): The external id configured for the assume role provider.
- `role_session_name` (string): The session name configured for the assume role provider.
- `role_tags` (object): The key-value pair tags configured for the assume role provider.
An example of how to utilize the [endpoint attribute](https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-s3.html)
See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
### Secrets
The following `secrets` are valid on an AWS storage bucket resource:
The following `secrets` are optional on an AWS storage bucket resource:
- `access_key_id` (string): The access key ID for the IAM user to use with this
storage bucket.
- `secret_access_key` (string): The secret access key for the IAM user to use
with this storage bucket.
with this storage bucket.
See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
+8
View File
@@ -44,6 +44,14 @@ func getStorageAttributes(in *structpb.Struct) (*StorageAttributes, error) {
continue
case cred.ConstDisableCredentialRotation:
continue
case cred.ConstRoleArn:
continue
case cred.ConstRoleExternalId:
continue
case cred.ConstRoleSessionName:
continue
case cred.ConstRoleTags:
continue
default:
badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field"
}
+50 -16
View File
@@ -16,29 +16,35 @@ import (
func TestGetStorageAttributes(t *testing.T) {
cases := []struct {
name string
in map[string]any
in *structpb.Struct
expected *StorageAttributes
expectedErrContains string
}{
{
name: "missing region",
in: map[string]any{},
name: "missing region",
in: &structpb.Struct{
Fields: make(map[string]*structpb.Value),
},
expectedErrContains: "missing required value \"region\"",
},
{
name: "unknown fields",
in: map[string]any{
"region": "us-west-2",
"foo": true,
"bar": true,
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"foo": structpb.NewBoolValue(true),
"bar": structpb.NewBoolValue(true),
},
},
expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
},
{
name: "good w/ endpoint",
in: map[string]any{
cred.ConstRegion: "us-west-2",
ConstAwsEndpointUrl: "0.0.0.0",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"endpoint_url": structpb.NewStringValue("0.0.0.0"),
},
},
expected: &StorageAttributes{
CredentialAttributes: &cred.CredentialAttributes{
@@ -49,8 +55,10 @@ func TestGetStorageAttributes(t *testing.T) {
},
{
name: "good w/o endpoint",
in: map[string]any{
cred.ConstRegion: "us-west-2",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
},
},
expected: &StorageAttributes{
CredentialAttributes: &cred.CredentialAttributes{
@@ -58,6 +66,35 @@ func TestGetStorageAttributes(t *testing.T) {
},
},
},
{
name: "credential attributes",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"disable_credential_rotation": structpb.NewBoolValue(true),
"role_arn": structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
"role_external_id": structpb.NewStringValue("1234567890"),
"role_session_name": structpb.NewStringValue("test-session"),
"role_tags": structpb.NewStructValue(&structpb.Struct{
Fields: map[string]*structpb.Value{
"foo": structpb.NewStringValue("bar"),
},
}),
},
},
expected: &StorageAttributes{
CredentialAttributes: &cred.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: true,
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
RoleExternalId: "1234567890",
RoleSessionName: "test-session",
RoleTags: map[string]string{
"foo": "bar",
},
},
},
},
}
for _, tc := range cases {
@@ -65,10 +102,7 @@ func TestGetStorageAttributes(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 := getStorageAttributes(input)
actual, err := getStorageAttributes(tc.in)
if tc.expectedErrContains != "" {
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
+61 -86
View File
@@ -51,11 +51,6 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre
return nil, status.Error(codes.InvalidArgument, "bucketName is required")
}
secrets := bucket.GetSecrets()
if secrets == nil {
return nil, status.Error(codes.InvalidArgument, "secrets is required")
}
attrs := bucket.GetAttributes()
if attrs == nil {
return nil, status.Error(codes.InvalidArgument, "attributes is required")
@@ -66,7 +61,7 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre
return nil, err
}
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
if err != nil {
return nil, err
}
@@ -74,24 +69,24 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre
// Set up the creds in our persisted state.
credState, err := cred.NewAwsCredentialPersistedState(
append([]cred.AwsCredentialPersistedStateOption{
cred.WithAccessKeyId(credConfig.AccessKey),
cred.WithSecretAccessKey(credConfig.SecretKey),
cred.WithRegion(credConfig.Region),
cred.WithCredentialsConfig(credConfig),
}, p.testCredStateOpts...)...,
)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
}
// Try to rotate the credentials if we're not skipping them.
if !storageAttributes.DisableCredentialRotation {
if err := credState.RotateCreds(); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
}
} else {
// Simply validate if we aren't rotating.
if err := credState.ValidateCreds(); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential validation: %s", err)
// Try to rotate static credentials
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
if !storageAttributes.DisableCredentialRotation {
if err := credState.RotateCreds(); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
}
} else {
// Simply validate if we aren't rotating.
if err := credState.ValidateCreds(); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential validation: %s", err)
}
}
}
@@ -160,39 +155,42 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd
// is fine and important, but this may change in the future.
credState, err := cred.AwsCredentialPersistedStateFromProto(
req.GetPersisted().GetData(),
append(p.testCredStateOpts, cred.WithRegion(storageAttributes.Region))...)
storageAttributes.CredentialAttributes,
p.testCredStateOpts...)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
if storageAttributes.DisableCredentialRotation && !updateSecrets {
// This is a validate check to make sure that we aren't disabling
// rotation for credentials currently being managed by rotation.
// This is not allowed.
if !credState.CredsLastRotatedTime.IsZero() {
return nil, status.Error(codes.FailedPrecondition, "cannot disable rotation for already-rotated credentials")
}
}
if updateSecrets {
storageSecrets, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
if err != nil {
return nil, err
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
if storageAttributes.DisableCredentialRotation && !updateSecrets {
// This is a validate check to make sure that we aren't disabling
// rotation for credentials currently being managed by rotation.
// This is not allowed.
if !credState.CredsLastRotatedTime.IsZero() {
return nil, status.Error(codes.FailedPrecondition, "cannot disable rotation for already-rotated credentials")
}
}
// Replace the credentials. This checks the timestamp on the last
// rotation time as well and deletes the credentials if we are
// managing them (ie: if we've rotated them before).
if err := credState.ReplaceCreds(storageSecrets.AccessKey, storageSecrets.SecretKey); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err)
}
}
if updateSecrets {
storageSecrets, err := cred.GetCredentialsConfig(secrets, storageAttributes.CredentialAttributes, false)
if err != nil {
return nil, err
}
if !storageAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
// If we're enabling rotation now but didn't before, or have
// freshly replaced credentials, we can rotate here.
if err := credState.RotateCreds(); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
// Replace the credentials. This checks the timestamp on the last
// rotation time as well and deletes the credentials if we are
// managing them (ie: if we've rotated them before).
if err := credState.ReplaceCreds(storageSecrets); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err)
}
}
if !storageAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
// If we're enabling rotation now but didn't before, or have
// freshly replaced credentials, we can rotate here.
if err := credState.RotateCreds(); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
}
}
}
@@ -245,7 +243,8 @@ func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDel
// may be corrupt/and or legitimately missing.
credState, err := cred.AwsCredentialPersistedStateFromProto(
req.GetPersisted().GetData(),
append(p.testCredStateOpts, cred.WithRegion(storageAttributes.Region))...)
storageAttributes.CredentialAttributes,
p.testCredStateOpts...)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
@@ -259,12 +258,15 @@ func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDel
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
if !credState.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 := credState.DeleteCreds(); err != nil {
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during storage bucket deletion: %s", err)
// try to delete static credentials
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
if !credState.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 := credState.DeleteCreds(); err != nil {
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during storage bucket deletion: %s", err)
}
}
}
@@ -286,11 +288,6 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques
return nil, status.Error(codes.InvalidArgument, "bucketName is required")
}
secrets := bucket.GetSecrets()
if secrets == nil {
return nil, status.Error(codes.InvalidArgument, "secrets is required")
}
attrs := bucket.GetAttributes()
if attrs == nil {
return nil, status.Error(codes.InvalidArgument, "attributes is required")
@@ -301,7 +298,7 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques
return nil, err
}
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
if err != nil {
return nil, err
}
@@ -309,9 +306,7 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques
// Set up the creds in our persisted state.
credState, err := cred.NewAwsCredentialPersistedState(
append([]cred.AwsCredentialPersistedStateOption{
cred.WithAccessKeyId(credConfig.AccessKey),
cred.WithSecretAccessKey(credConfig.SecretKey),
cred.WithRegion(credConfig.Region),
cred.WithCredentialsConfig(credConfig),
}, p.testCredStateOpts...)...,
)
if err != nil {
@@ -364,11 +359,6 @@ func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Validat
return nil, status.Error(codes.InvalidArgument, "bucketName is required")
}
secrets := bucket.GetSecrets()
if secrets == nil {
return nil, status.Error(codes.InvalidArgument, "secrets is required")
}
attrs := bucket.GetAttributes()
if attrs == nil {
return nil, status.Error(codes.InvalidArgument, "attributes is required")
@@ -379,7 +369,7 @@ func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Validat
return nil, err
}
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
if err != nil {
return nil, err
}
@@ -387,9 +377,7 @@ func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Validat
// Set up the creds in our persisted state.
credState, err := cred.NewAwsCredentialPersistedState(
append([]cred.AwsCredentialPersistedStateOption{
cred.WithAccessKeyId(credConfig.AccessKey),
cred.WithSecretAccessKey(credConfig.SecretKey),
cred.WithRegion(credConfig.Region),
cred.WithCredentialsConfig(credConfig),
}, p.testCredStateOpts...)...,
)
if err != nil {
@@ -429,11 +417,6 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu
return status.Error(codes.InvalidArgument, "bucketName is required")
}
secrets := bucket.GetSecrets()
if secrets == nil {
return status.Error(codes.InvalidArgument, "secrets is required")
}
attrs := bucket.GetAttributes()
if attrs == nil {
return status.Error(codes.InvalidArgument, "attributes is required")
@@ -444,7 +427,7 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu
return err
}
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
if err != nil {
return err
}
@@ -452,9 +435,7 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu
// Set up the creds in our persisted state.
credState, err := cred.NewAwsCredentialPersistedState(
append([]cred.AwsCredentialPersistedStateOption{
cred.WithAccessKeyId(credConfig.AccessKey),
cred.WithSecretAccessKey(credConfig.SecretKey),
cred.WithRegion(credConfig.Region),
cred.WithCredentialsConfig(credConfig),
}, p.testCredStateOpts...)...,
)
if err != nil {
@@ -533,10 +514,6 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
if bucket.GetBucketName() == "" {
return nil, status.Error(codes.InvalidArgument, "bucketName is required")
}
secrets := bucket.GetSecrets()
if secrets == nil {
return nil, status.Error(codes.InvalidArgument, "secrets is required")
}
attrs := bucket.GetAttributes()
if attrs == nil {
return nil, status.Error(codes.InvalidArgument, "attributes is required")
@@ -564,7 +541,7 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
return nil, err
}
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
if err != nil {
return nil, err
}
@@ -572,9 +549,7 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
// Set up the creds in our persisted state.
credState, err := cred.NewAwsCredentialPersistedState(
append([]cred.AwsCredentialPersistedStateOption{
cred.WithAccessKeyId(credConfig.AccessKey),
cred.WithSecretAccessKey(credConfig.SecretKey),
cred.WithRegion(credConfig.Region),
cred.WithCredentialsConfig(credConfig),
}, p.testCredStateOpts...)...,
)
if err != nil {
+94 -166
View File
@@ -34,11 +34,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
return &pb.OnCreateStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
}},
Secrets: credential.MockStaticCredentialSecrets(),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
@@ -49,12 +45,13 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
}
}
cases := []struct {
name string
req *pb.OnCreateStorageBucketRequest
credOpts []credential.AwsCredentialPersistedStateOption
storageOpts []awsStoragePersistedStateOption
expectedErrContains string
expectedErrCode codes.Code
name string
req *pb.OnCreateStorageBucketRequest
credOpts []credential.AwsCredentialPersistedStateOption
storageOpts []awsStoragePersistedStateOption
expectedErrContains string
expectedErrCode codes.Code
expectedPersistedData map[string]any
}{
{
name: "nil storage bucket",
@@ -70,22 +67,11 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
expectedErrContains: "bucketName is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil secrets",
req: &pb.OnCreateStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
},
},
expectedErrContains: "secrets is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil attributes",
req: &pb.OnCreateStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
},
},
expectedErrContains: "attributes is required",
@@ -96,39 +82,17 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
req: &pb.OnCreateStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
Attributes: new(structpb.Struct),
},
},
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "error reading secrets",
req: &pb.OnCreateStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
},
expectedErrContains: "missing required value \"access_key_id\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
req: &pb.OnCreateStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
}},
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
@@ -156,11 +120,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
req: &pb.OnCreateStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
}},
Secrets: credential.MockStaticCredentialSecrets(),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
@@ -301,6 +261,67 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
),
),
},
expectedPersistedData: credential.MockStaticCredentialSecrets().AsMap(),
},
{
name: "success rotate static creds",
req: &pb.OnCreateStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: credential.MockStaticCredentialSecrets(),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(false),
},
},
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts(
[]awsutil.Option{
awsutil.WithSTSAPIFunc(
awsutil.NewMockSTS(),
),
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithGetUserOutput(
&iam.GetUserOutput{
User: &iam.User{
Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"),
UserId: aws.String("AIDAJQABLZS4A3QDU576Q"),
UserName: aws.String("JohnDoe"),
},
},
),
awsutil.WithCreateAccessKeyOutput(
&iam.CreateAccessKeyOutput{
AccessKey: &iam.AccessKey{
AccessKeyId: aws.String("one"),
SecretAccessKey: aws.String("two"),
UserName: aws.String("JohnDoe"),
},
},
),
),
),
},
),
},
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
),
),
},
expectedPersistedData: map[string]any{
credential.ConstAccessKeyId: "one",
credential.ConstSecretAccessKey: "two",
},
},
}
@@ -322,12 +343,12 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
require.NotNil(resp)
require.NotNil(resp.GetPersisted())
require.NotNil(resp.GetPersisted().GetData())
require.Equal(
resp.GetPersisted().GetData().AsMap()[credential.ConstAccessKeyId],
tc.req.Bucket.Secrets.AsMap()[credential.ConstAccessKeyId])
require.Equal(
resp.GetPersisted().GetData().AsMap()[credential.ConstSecretAccessKey],
tc.req.Bucket.Secrets.AsMap()[credential.ConstSecretAccessKey])
actualPersistedData := resp.GetPersisted().GetData().AsMap()
if len(actualPersistedData) != 0 {
require.Equal(tc.expectedPersistedData[credential.ConstAccessKeyId], actualPersistedData[credential.ConstAccessKeyId])
require.Equal(tc.expectedPersistedData[credential.ConstSecretAccessKey], actualPersistedData[credential.ConstSecretAccessKey])
require.NotEmpty(actualPersistedData[credential.ConstCredsLastRotatedTime])
}
})
}
}
@@ -345,12 +366,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
Data: credential.MockStaticCredentialSecrets(),
},
}
}
@@ -428,7 +444,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -458,7 +474,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -468,31 +484,6 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
expectedErrContains: "cannot disable rotation for already-rotated credentials",
expectedErrCode: codes.FailedPrecondition,
},
{
name: "error reading secrets",
req: &pb.OnUpdateStorageBucketRequest{
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
},
},
expectedErrContains: "missing required value \"access_key_id\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "updating secrets, replace error",
req: &pb.OnUpdateStorageBucketRequest{
@@ -500,7 +491,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
BucketName: "foo",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("onetwo"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_onetwo"),
credential.ConstSecretAccessKey: structpb.NewStringValue("threefour"),
},
},
@@ -514,7 +505,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -547,7 +538,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
@@ -671,7 +662,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
@@ -696,7 +687,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
awsutil.WithCreateAccessKeyOutput(
&iam.CreateAccessKeyOutput{
AccessKey: &iam.AccessKey{
AccessKeyId: aws.String("one"),
AccessKeyId: aws.String("AKIA_one"),
SecretAccessKey: aws.String("two"),
UserName: aws.String("JohnDoe"),
},
@@ -760,7 +751,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
disableCredentialRotation = value.(bool)
}
if !disableCredentialRotation {
expectedAccessKey = "one"
expectedAccessKey = "AKIA_one"
expectedSecretKey = "two"
}
require.Equal(expectedAccessKey, resp.GetPersisted().GetData().AsMap()[credential.ConstAccessKeyId])
@@ -807,11 +798,7 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) {
req: &pb.OnDeleteStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
}},
Secrets: credential.MockStaticCredentialSecrets(),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
@@ -835,7 +822,7 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) {
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -863,7 +850,7 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) {
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -895,7 +882,7 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) {
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
@@ -924,6 +911,7 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) {
}
resp, err := p.OnDeleteStorageBucket(context.Background(), tc.req)
if tc.expectedErrContains != "" {
require.NotNil(err)
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
return
@@ -940,11 +928,7 @@ func TestStoragePlugin_HeadObject(t *testing.T) {
Key: "/foo/bar/key",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
}},
Secrets: credential.MockStaticCredentialSecrets(),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
@@ -986,17 +970,6 @@ func TestStoragePlugin_HeadObject(t *testing.T) {
expectedErrContains: "bucketName is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil secrets",
req: &pb.HeadObjectRequest{
Key: "/foo/bar/key",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
},
},
expectedErrContains: "secrets is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil attributes",
req: &pb.HeadObjectRequest{
@@ -1140,11 +1113,7 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) {
return &pb.ValidatePermissionsRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
}},
Secrets: credential.MockStaticCredentialSecrets(),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
@@ -1175,16 +1144,6 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) {
expectedErrContains: "bucketName is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil secrets",
req: &pb.ValidatePermissionsRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
},
},
expectedErrContains: "secrets is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil attributes",
req: &pb.ValidatePermissionsRequest{
@@ -1383,11 +1342,7 @@ func TestStoragePlugin_GetObject(t *testing.T) {
Key: "/foo/bar/key",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
}},
Secrets: credential.MockStaticCredentialSecrets(),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
@@ -1428,17 +1383,6 @@ func TestStoragePlugin_GetObject(t *testing.T) {
expectedErrContains: "bucketName is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil secrets",
req: &pb.GetObjectRequest{
Key: "/foo/bar/key",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
},
},
expectedErrContains: "secrets is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil attributes",
req: &pb.GetObjectRequest{
@@ -1616,12 +1560,7 @@ func TestStoragePlugin_PutObject(t *testing.T) {
return &pb.PutObjectRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "external-obj-store",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
Secrets: credential.MockStaticCredentialSecrets(),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
@@ -1671,17 +1610,6 @@ func TestStoragePlugin_PutObject(t *testing.T) {
expectedErrContains: "bucketName is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "missing bucket secrets",
request: &pb.PutObjectRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "external-obj-store",
},
Key: "mock-object",
},
expectedErrContains: "secrets is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "missing attributes",
request: &pb.PutObjectRequest{
+67 -24
View File
@@ -9,35 +9,78 @@ import (
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
"github.com/hashicorp/go-secure-stdlib/awsutil"
"github.com/stretchr/testify/require"
)
func TestAwsStoragePersistedStateS3Client(t *testing.T) {
require := require.New(t)
cred, err := credential.NewAwsCredentialPersistedState(
credential.WithRegion("us-west-2"),
credential.WithAccessKeyId("foobar"),
credential.WithSecretAccessKey("barfoo"),
)
require.NoError(err)
cases := []struct {
name string
creds *awsutil.CredentialsConfig
opts []awsStoragePersistedStateOption
awsOpts []awsutil.Option
s3Opts []s3Option
expectedRegion string
expectedEndpoint string
}{
{
name: "static credentials",
creds: func() *awsutil.CredentialsConfig {
creds, err := awsutil.NewCredentialsConfig(
awsutil.WithRegion("us-west-2"),
awsutil.WithAccessKey("foobar"),
awsutil.WithSecretKey("barfoo"),
)
require.NoError(t, err)
return creds
}(),
awsOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(),
),
},
opts: []awsStoragePersistedStateOption{
withTestS3APIFunc(newTestMockS3(nil)),
},
s3Opts: []s3Option{
WithRegion("us-west-2"),
WithEndpoint("0.0.0.0"),
},
expectedRegion: "us-west-2",
expectedEndpoint: "0.0.0.0",
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
state, err := newAwsStoragePersistedState(
withCredentials(cred),
withTestS3APIFunc(newTestMockS3(nil)),
)
require.NoError(err)
cred, err := credential.NewAwsCredentialPersistedState(
credential.WithCredentialsConfig(tc.creds),
credential.WithStateTestOpts(tc.awsOpts),
)
require.NoError(err)
clientRaw, err := state.S3Client(context.Background(), WithRegion("us-west-2"), WithEndpoint("0.0.0.0"))
require.NoError(err)
require.NotNil(clientRaw)
client, ok := clientRaw.(*testMockS3)
require.True(ok)
require.NotNil(client)
require.Equal("us-west-2", client.Region)
state, err := newAwsStoragePersistedState(
append(tc.opts, withCredentials(cred))...,
)
require.NoError(err)
endpoint, err := client.Endpoint.ResolveEndpoint(s3.ServiceID, client.Region)
require.NoError(err)
require.Equal("0.0.0.0", endpoint.URL)
require.Equal("us-west-2", endpoint.SigningRegion)
require.Equal("aws", endpoint.PartitionID)
clientRaw, err := state.S3Client(context.Background(), tc.s3Opts...)
require.NoError(err)
require.NotNil(clientRaw)
client, ok := clientRaw.(*testMockS3)
require.True(ok)
require.NotNil(client)
require.Equal(tc.expectedRegion, client.Region)
if tc.expectedEndpoint != "" {
endpoint, err := client.Endpoint.ResolveEndpoint(s3.ServiceID, client.Region)
require.NoError(err)
require.Equal(tc.expectedEndpoint, endpoint.URL)
require.Equal(tc.expectedRegion, endpoint.SigningRegion)
require.Equal("aws", endpoint.PartitionID)
}
})
}
}