refactor: switch from awsutil to awsutilv2 (#34)
* fix(storage): Storage Assume Role Support This commit introduces fixes to enable operators to use AWS dynamic credentials on the storage side of this plugin, namely validation, improved credential lifecycle management to allow for storage buckets to be updated from static to dynamic credentials and vice versa, and a new way to determine credential types. It also introduces new tests for dynamic credential usage for the various functions. * fix(storage): Static rotated creds being deleted on plugin err On Storage Bucket update, when we determine if the credentials are to be updated to new ones, we call `credState.ReplaceCreds`. This call, if the old credentials were managed by us (rotated at create time), will delete the old credentials and set the new ones onto credState. We called this function without validating if the incoming credentials are valid. This could get the bucket in a state where where, while we'd return an update error if the incoming credentials weren't working with S3 (err in the dry run process that happens at the end of the update), we'd also silently delete the old credentials the bucket used, rendering the bucket completely broken. --------- Co-authored-by: Hugo <hugoamvieira@users.noreply.github.com>
This commit is contained in:
1 parent
f263eefbba
commit
fcfe267a3f
18 files changed
+1319
-515
No files matched your search
@@ -83,15 +83,14 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
|
||||
}
|
||||
|
||||
// Try to rotate static credentials
|
||||
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
|
||||
if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS {
|
||||
if !catalogAttributes.DisableCredentialRotation {
|
||||
if err := credState.RotateCreds(); err != nil {
|
||||
if err := credState.RotateCreds(ctx); 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 {
|
||||
if err := credState.ValidateCreds(ctx); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential validation: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -160,7 +159,7 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
|
||||
if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS {
|
||||
if catalogAttributes.DisableCredentialRotation && !updateSecrets {
|
||||
// This is a validate check to make sure that we aren't disabling
|
||||
// rotation for credentials currently being managed by rotation.
|
||||
@@ -179,7 +178,7 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
|
||||
// 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(catalogSecrets); err != nil {
|
||||
if err := credState.ReplaceCreds(ctx, catalogSecrets); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -187,7 +186,7 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
|
||||
if !catalogAttributes.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 {
|
||||
if err := credState.RotateCreds(ctx); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -244,12 +243,12 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo
|
||||
}
|
||||
|
||||
// try to delete static credentials
|
||||
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
|
||||
if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS {
|
||||
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 {
|
||||
if err := credState.DeleteCreds(ctx); err != nil {
|
||||
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during catalog deletion: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -332,11 +331,7 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts := []ec2Option{}
|
||||
if catalogAttributes.Region != "" {
|
||||
opts = append(opts, WithRegion(catalogAttributes.Region))
|
||||
}
|
||||
ec2Client, err := catalogState.EC2Client(ctx, opts...)
|
||||
ec2Client, err := catalogState.EC2Client(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
|
||||
}
|
||||
@@ -409,11 +404,7 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts := []ec2Option{}
|
||||
if catalogAttributes.Region != "" {
|
||||
opts = append(opts, WithRegion(catalogAttributes.Region))
|
||||
}
|
||||
ec2Client, err := catalogState.EC2Client(ctx, opts...)
|
||||
ec2Client, err := catalogState.EC2Client(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
|
||||
}
|
||||
@@ -516,11 +507,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
||||
}
|
||||
}
|
||||
|
||||
opts := []ec2Option{}
|
||||
if catalogAttributes.Region != "" {
|
||||
opts = append(opts, WithRegion(catalogAttributes.Region))
|
||||
}
|
||||
ec2Client, err := catalogState.EC2Client(ctx, opts...)
|
||||
ec2Client, err := catalogState.EC2Client(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
@@ -153,7 +153,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
|
||||
),
|
||||
}),
|
||||
},
|
||||
expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling aws.GetUser: %s", testGetUserErr),
|
||||
expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr),
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
@@ -411,7 +411,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
|
||||
),
|
||||
}),
|
||||
},
|
||||
expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling aws.GetUser: %s", testGetUserErr),
|
||||
expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr),
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
@@ -88,75 +86,16 @@ func (s *awsCatalogPersistedState) toProto() (*pb.HostCatalogPersisted, error) {
|
||||
|
||||
// EC2Client returns a configured EC2 client based on the session
|
||||
// information stored in the state.
|
||||
func (s *awsCatalogPersistedState) EC2Client(ctx context.Context, opt ...ec2Option) (EC2API, error) {
|
||||
opts, err := getOpts(opt...)
|
||||
func (s *awsCatalogPersistedState) EC2Client(ctx context.Context) (EC2API, error) {
|
||||
awsCfg, err := s.GenerateCredentialChain(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing options when fetching EC2 client: %w", err)
|
||||
return nil, fmt.Errorf("error getting AWS configuration when fetching EC2 client: %w", err)
|
||||
}
|
||||
|
||||
awsConfigOpts := []func(*config.LoadOptions) error{}
|
||||
sess, err := s.GetSession()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting AWS session when fetching EC2 client: %w", err)
|
||||
if awsCfg == nil {
|
||||
return nil, fmt.Errorf("nil aws configuration")
|
||||
}
|
||||
creds, err := sess.Config.Credentials.Get()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting AWS session when fetching EC2 client: %w", err)
|
||||
}
|
||||
customCredentials := config.WithCredentialsProvider(
|
||||
credentials.NewStaticCredentialsProvider(creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken),
|
||||
)
|
||||
awsConfigOpts = append(awsConfigOpts, customCredentials)
|
||||
if sess.Config.Region != nil {
|
||||
awsConfigOpts = append(awsConfigOpts, config.WithRegion(*sess.Config.Region))
|
||||
}
|
||||
if opts.withRegion != "" {
|
||||
awsConfigOpts = append(awsConfigOpts, config.WithRegion(opts.withRegion))
|
||||
}
|
||||
cfg, err := config.LoadDefaultConfig(ctx, awsConfigOpts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error loading aws configuration when fetching EC2 client: %w", err)
|
||||
}
|
||||
|
||||
if s.testEC2APIFunc != nil {
|
||||
return s.testEC2APIFunc(cfg)
|
||||
}
|
||||
|
||||
cfg.HTTPClient = customClient
|
||||
|
||||
return ec2.NewFromConfig(cfg), nil
|
||||
}
|
||||
|
||||
// getOpts iterates the inbound ec2Options and returns a struct
|
||||
func getOpts(opt ...ec2Option) (ec2Options, error) {
|
||||
opts := getDefaultEC2Options()
|
||||
for _, o := range opt {
|
||||
if o == nil {
|
||||
continue
|
||||
}
|
||||
if err := o(&opts); err != nil {
|
||||
return ec2Options{}, err
|
||||
}
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// ec2Option - how ec2Options are passed as arguments
|
||||
type ec2Option func(*ec2Options) error
|
||||
|
||||
// options = how options are represented
|
||||
type ec2Options struct {
|
||||
withRegion string
|
||||
}
|
||||
|
||||
func getDefaultEC2Options() ec2Options {
|
||||
return ec2Options{}
|
||||
}
|
||||
|
||||
// WithRegion contains the region to use
|
||||
func WithRegion(with string) ec2Option {
|
||||
return func(o *ec2Options) error {
|
||||
o.withRegion = with
|
||||
return nil
|
||||
return s.testEC2APIFunc(*awsCfg)
|
||||
}
|
||||
return ec2.NewFromConfig(*awsCfg), nil
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestAwsCatalogPersistedStateEC2Client(t *testing.T) {
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
clientRaw, err := state.EC2Client(context.Background(), WithRegion("us-west-2"))
|
||||
clientRaw, err := state.EC2Client(context.Background())
|
||||
require.NoError(err)
|
||||
require.NotNil(clientRaw)
|
||||
client, ok := clientRaw.(*testMockEC2)
|
||||
|
||||
@@ -76,17 +76,10 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %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)
|
||||
}
|
||||
// Try to rotate AWS static credentials
|
||||
if cred.GetCredentialType(credConfig) == cred.StaticAWS && !storageAttributes.DisableCredentialRotation {
|
||||
if err := credState.RotateCreds(ctx); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,76 +112,100 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd
|
||||
// For storage buckets we only need to really validate what is
|
||||
// currently in the new copy of the storage bucket data, so skip
|
||||
// fetching the new copy to keep things a little less messy.
|
||||
bucket := req.GetNewBucket()
|
||||
if bucket == nil {
|
||||
newBucket := req.GetNewBucket()
|
||||
if newBucket == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "new bucket is required")
|
||||
}
|
||||
if bucket.BucketName == "" {
|
||||
if newBucket.BucketName == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "new bucketName is required")
|
||||
}
|
||||
|
||||
var updateSecrets bool
|
||||
secrets := bucket.GetSecrets()
|
||||
if secrets != nil {
|
||||
// We will be updating secrets this run, but what exactly that
|
||||
// means will be determined later.
|
||||
updateSecrets = true
|
||||
currentBucket := req.GetCurrentBucket()
|
||||
if currentBucket == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "current bucket is required")
|
||||
}
|
||||
|
||||
attrs := bucket.GetAttributes()
|
||||
if attrs == nil {
|
||||
oldAttrs := currentBucket.GetAttributes()
|
||||
if oldAttrs == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "current bucket attributes is required")
|
||||
}
|
||||
oldStorageAttributes, err := getStorageAttributes(oldAttrs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newAttrs := newBucket.GetAttributes()
|
||||
if newAttrs == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "new bucket attributes is required")
|
||||
}
|
||||
|
||||
storageAttributes, err := getStorageAttributes(attrs)
|
||||
newStorageAttributes, err := getStorageAttributes(newAttrs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the persisted data.
|
||||
// NOTE: We might need to change this at a later time. I'm not too
|
||||
// sure *exactly* what scenarios we might encounter that would
|
||||
// ultimately mean that we would have to handle an empty or missing
|
||||
// state in update, but what we are ultimately assuming here
|
||||
// (implicitly through awsStoragePersistedStateFromProto) is that
|
||||
// the state will exist and be populated. Personally I think this
|
||||
// is fine and important, but this may change in the future.
|
||||
credState, err := cred.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetData(),
|
||||
storageAttributes.CredentialAttributes,
|
||||
oldStorageAttributes.CredentialAttributes,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", 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.
|
||||
// Verify the incoming credentials are valid and return any errors to the
|
||||
// user if they're not. Note this doesn't validate the credentials against
|
||||
// AWS - it only does logical validation on the fields.
|
||||
updatedCredentials, err := cred.GetCredentialsConfig(newBucket.GetSecrets(), newStorageAttributes.CredentialAttributes, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We will be updating credentials when one of two changes occur:
|
||||
// 1. the new bucket has secrets for static credentials, in which case we need to delete the old static credentials
|
||||
// 2. the new bucket has a different roleARN value for dynamic credentials
|
||||
if newBucket.GetSecrets() != nil || newStorageAttributes.RoleArn != oldStorageAttributes.RoleArn {
|
||||
// Ensure the incoming credentials are valid for interaction with the S3
|
||||
// Bucket before replacing them.
|
||||
newCredState, err := cred.NewAwsCredentialPersistedState(
|
||||
append([]cred.AwsCredentialPersistedStateOption{
|
||||
cred.WithCredentialsConfig(updatedCredentials),
|
||||
}, p.testCredStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error setting up new credential persisted state: %s", err)
|
||||
}
|
||||
newStorageState, err := newAwsStoragePersistedState(
|
||||
append([]awsStoragePersistedStateOption{
|
||||
withCredentials(newCredState),
|
||||
}, p.testStorageStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
if err := dryRunValidation(ctx, newStorageState, newStorageAttributes, newBucket); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "failed to validate incoming credentials: %s", err)
|
||||
}
|
||||
|
||||
// Replace the existing credential state.
|
||||
// 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(ctx, updatedCredentials); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS {
|
||||
// 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 newStorageAttributes.DisableCredentialRotation && newBucket.GetSecrets() == nil {
|
||||
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.CredentialAttributes, false)
|
||||
if err != nil {
|
||||
return nil, 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 {
|
||||
// If we're enabling rotation now but didn't before, or have
|
||||
// freshly replaced credentials, we can rotate here.
|
||||
if !newStorageAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
|
||||
if err := credState.RotateCreds(ctx); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -204,7 +221,7 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd
|
||||
}
|
||||
|
||||
// perform dry run to ensure we can interact with the bucket as expected.
|
||||
if err := dryRunValidation(ctx, storageState, storageAttributes, bucket); err != nil {
|
||||
if err := dryRunValidation(ctx, storageState, newStorageAttributes, newBucket); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -259,12 +276,12 @@ func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDel
|
||||
}
|
||||
|
||||
// try to delete static credentials
|
||||
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
|
||||
if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS {
|
||||
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 {
|
||||
if err := credState.DeleteCreds(ctx); err != nil {
|
||||
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during storage bucket deletion: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -323,9 +340,6 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques
|
||||
}
|
||||
|
||||
opts := []s3Option{}
|
||||
if storageAttributes.Region != "" {
|
||||
opts = append(opts, WithRegion(storageAttributes.Region))
|
||||
}
|
||||
if storageAttributes.EndpointUrl != "" {
|
||||
opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl))
|
||||
}
|
||||
@@ -452,9 +466,6 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu
|
||||
}
|
||||
|
||||
opts := []s3Option{}
|
||||
if storageAttributes.Region != "" {
|
||||
opts = append(opts, WithRegion(storageAttributes.Region))
|
||||
}
|
||||
if storageAttributes.EndpointUrl != "" {
|
||||
opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl))
|
||||
}
|
||||
@@ -566,9 +577,6 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
|
||||
}
|
||||
|
||||
opts := []s3Option{}
|
||||
if storageAttributes.Region != "" {
|
||||
opts = append(opts, WithRegion(storageAttributes.Region))
|
||||
}
|
||||
if storageAttributes.EndpointUrl != "" {
|
||||
opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl))
|
||||
}
|
||||
@@ -632,9 +640,6 @@ func dryRunValidation(ctx context.Context, state *awsStoragePersistedState, attr
|
||||
}
|
||||
|
||||
opts := []s3Option{}
|
||||
if attrs.Region != "" {
|
||||
opts = append(opts, WithRegion(attrs.Region))
|
||||
}
|
||||
if attrs.EndpointUrl != "" {
|
||||
opts = append(opts, WithEndpoint(attrs.EndpointUrl))
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -9,12 +9,12 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
smithyEndpoints "github.com/aws/smithy-go/endpoints"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
@@ -97,55 +97,23 @@ func (s *awsStoragePersistedState) S3Client(ctx context.Context, opt ...s3Option
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing options when fetching S3 client: %w", err)
|
||||
}
|
||||
|
||||
awsConfigOpts := []func(*config.LoadOptions) error{}
|
||||
sess, err := s.GetSession()
|
||||
awsCfg, err := s.GenerateCredentialChain(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting AWS session when fetching S3 client: %w", err)
|
||||
return nil, fmt.Errorf("error getting AWS configuration when fetching S3 client: %w", err)
|
||||
}
|
||||
creds, err := sess.Config.Credentials.Get()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting AWS session when fetching S3 client: %w", err)
|
||||
}
|
||||
customCredentials := config.WithCredentialsProvider(
|
||||
credentials.NewStaticCredentialsProvider(creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken),
|
||||
)
|
||||
awsConfigOpts = append(awsConfigOpts, customCredentials)
|
||||
if sess.Config.Region != nil {
|
||||
awsConfigOpts = append(awsConfigOpts, config.WithRegion(*sess.Config.Region))
|
||||
}
|
||||
if opts.withRegion != "" {
|
||||
awsConfigOpts = append(awsConfigOpts, config.WithRegion(opts.withRegion))
|
||||
if awsCfg == nil {
|
||||
return nil, fmt.Errorf("nil aws configuration")
|
||||
}
|
||||
var s3Opts []func(*s3.Options)
|
||||
if opts.withEndpoint != "" {
|
||||
customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
|
||||
if service == s3.ServiceID {
|
||||
if opts.withRegion != "" {
|
||||
region = opts.withRegion
|
||||
}
|
||||
return aws.Endpoint{
|
||||
PartitionID: "aws",
|
||||
URL: opts.withEndpoint,
|
||||
SigningRegion: region,
|
||||
}, nil
|
||||
}
|
||||
// returning EndpointNotFoundError will allow the service to fallback to it's default resolution
|
||||
return aws.Endpoint{}, &aws.EndpointNotFoundError{}
|
||||
})
|
||||
awsConfigOpts = append(awsConfigOpts, config.WithEndpointResolverWithOptions(customResolver))
|
||||
s3Opts = append(s3Opts, s3.WithEndpointResolverV2(&endpointResolver{
|
||||
endpoint: opts.withEndpoint,
|
||||
}))
|
||||
}
|
||||
cfg, err := config.LoadDefaultConfig(ctx, awsConfigOpts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error loading aws configuration when fetching S3 client: %w", err)
|
||||
}
|
||||
|
||||
if s.testS3APIFunc != nil {
|
||||
return s.testS3APIFunc(cfg)
|
||||
return s.testS3APIFunc(*awsCfg)
|
||||
}
|
||||
|
||||
cfg.HTTPClient = customClient
|
||||
|
||||
return s3.NewFromConfig(cfg), nil
|
||||
return s3.NewFromConfig(*awsCfg, s3Opts...), nil
|
||||
}
|
||||
|
||||
// getOpts iterates the inbound s3Options and returns a struct
|
||||
@@ -167,7 +135,6 @@ type s3Option func(*s3Options) error
|
||||
|
||||
// options = how options are represented
|
||||
type s3Options struct {
|
||||
withRegion string
|
||||
withEndpoint string
|
||||
}
|
||||
|
||||
@@ -175,14 +142,6 @@ func getDefaultS3Options() s3Options {
|
||||
return s3Options{}
|
||||
}
|
||||
|
||||
// WithRegion contains the region to use
|
||||
func WithRegion(with string) s3Option {
|
||||
return func(o *s3Options) error {
|
||||
o.withRegion = with
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithEndpoint contains the endpoint to use
|
||||
func WithEndpoint(with string) s3Option {
|
||||
return func(o *s3Options) error {
|
||||
@@ -190,3 +149,31 @@ func WithEndpoint(with string) s3Option {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type endpointResolver struct {
|
||||
endpoint string
|
||||
}
|
||||
|
||||
func (e *endpointResolver) ResolveEndpoint(ctx context.Context, params s3.EndpointParameters) (resolver smithyEndpoints.Endpoint, err error) {
|
||||
var uri *url.URL
|
||||
resolver, err = s3.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if e.endpoint == "" {
|
||||
// TODO: not sure if we should set an error or ignore the missing endpoint value and continue with the default resolver
|
||||
return
|
||||
}
|
||||
uri, err = url.Parse(e.endpoint)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if uri == nil {
|
||||
// TODO: not sure if we should set an error or ignore the missing uri value and continue with the default resolver
|
||||
return
|
||||
}
|
||||
resolver = smithyEndpoints.Endpoint{
|
||||
URI: *uri,
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -7,9 +7,8 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"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/hashicorp/go-secure-stdlib/awsutil/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -43,7 +42,6 @@ func TestAwsStoragePersistedStateS3Client(t *testing.T) {
|
||||
withTestS3APIFunc(newTestMockS3(nil)),
|
||||
},
|
||||
s3Opts: []s3Option{
|
||||
WithRegion("us-west-2"),
|
||||
WithEndpoint("0.0.0.0"),
|
||||
},
|
||||
expectedRegion: "us-west-2",
|
||||
@@ -73,14 +71,6 @@ func TestAwsStoragePersistedStateS3Client(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,11 @@ import (
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/sts"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
@@ -69,9 +69,8 @@ func (s *testMockS3State) Reset() {
|
||||
type testMockS3 struct {
|
||||
S3API
|
||||
|
||||
State *testMockS3State
|
||||
Region string
|
||||
Endpoint aws.EndpointResolverWithOptions
|
||||
State *testMockS3State
|
||||
Region string
|
||||
|
||||
// mocked responses for getObject
|
||||
GetObjectOutput *s3.GetObjectOutput
|
||||
@@ -147,9 +146,6 @@ func newTestMockS3(state *testMockS3State, opts ...testMockS3Option) s3APIFunc {
|
||||
if cfg.Region != "" {
|
||||
m.Region = cfg.Region
|
||||
}
|
||||
if cfg.EndpointResolverWithOptions != nil {
|
||||
m.Endpoint = cfg.EndpointResolverWithOptions
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
|
||||
Reference in new issue
Block a user