Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
719 lines
23 KiB
Go
719 lines
23 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package storage
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/aws/retry"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
"github.com/google/uuid"
|
|
cred "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"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
// Ensure that we are implementing StoragePluginServiceServer
|
|
var _ pb.StoragePluginServiceServer = (*StoragePlugin)(nil)
|
|
|
|
// StoragePlugin implements the StoragePluginServiceServer interface for the
|
|
// AWS storage service plugin.
|
|
type StoragePlugin struct {
|
|
pb.UnimplementedStoragePluginServiceServer
|
|
|
|
// testCredStateOpts are passed in to the stored state to control test behavior
|
|
testCredStateOpts []cred.AwsCredentialPersistedStateOption
|
|
|
|
// testStorageStateOpts are passed in to the stored state to control test behavior
|
|
testStorageStateOpts []awsStoragePersistedStateOption
|
|
}
|
|
|
|
// OnCreateStorageBucket is called when a storage bucket is created.
|
|
func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCreateStorageBucketRequest) (*pb.OnCreateStorageBucketResponse, error) {
|
|
bucket := req.GetBucket()
|
|
if bucket == nil {
|
|
return nil, status.Error(codes.InvalidArgument, "bucket is required")
|
|
}
|
|
if bucket.BucketName == "" {
|
|
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")
|
|
}
|
|
|
|
storageAttributes, err := getStorageAttributes(attrs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 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),
|
|
}, 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)
|
|
}
|
|
}
|
|
|
|
storageState, err := newAwsStoragePersistedState(
|
|
append([]awsStoragePersistedStateOption{
|
|
withCredentials(credState),
|
|
}, p.testStorageStateOpts...)...,
|
|
)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
|
|
}
|
|
|
|
// perform dry run to ensure we can interact with the bucket as expected.
|
|
if err := dryRunValidation(ctx, storageState, storageAttributes, bucket); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
persistedProto, err := storageState.toProto()
|
|
if err != nil {
|
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
|
}
|
|
|
|
return &pb.OnCreateStorageBucketResponse{
|
|
Persisted: persistedProto,
|
|
}, nil
|
|
}
|
|
|
|
// OnUpdateStorageBucket is called when a storage bucket is updated.
|
|
func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpdateStorageBucketRequest) (*pb.OnUpdateStorageBucketResponse, error) {
|
|
// 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 {
|
|
return nil, status.Error(codes.InvalidArgument, "new bucket is required")
|
|
}
|
|
if bucket.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
|
|
}
|
|
|
|
attrs := bucket.GetAttributes()
|
|
if attrs == nil {
|
|
return nil, status.Error(codes.InvalidArgument, "new bucket attributes is required")
|
|
}
|
|
|
|
storageAttributes, err := getStorageAttributes(attrs)
|
|
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(),
|
|
append(p.testCredStateOpts, cred.WithRegion(storageAttributes.Region))...)
|
|
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
|
|
}
|
|
|
|
// 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 !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)
|
|
}
|
|
}
|
|
|
|
storageState, err := newAwsStoragePersistedState(
|
|
append([]awsStoragePersistedStateOption{
|
|
withCredentials(credState),
|
|
}, p.testStorageStateOpts...)...,
|
|
)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
|
}
|
|
|
|
// perform dry run to ensure we can interact with the bucket as expected.
|
|
if err := dryRunValidation(ctx, storageState, storageAttributes, bucket); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
persistedProto, err := storageState.toProto()
|
|
if err != nil {
|
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
|
}
|
|
|
|
return &pb.OnUpdateStorageBucketResponse{
|
|
Persisted: persistedProto,
|
|
}, nil
|
|
}
|
|
|
|
// OnDeleteStorageBucket is called when a storage bucket is deleted.
|
|
func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDeleteStorageBucketRequest) (*pb.OnDeleteStorageBucketResponse, error) {
|
|
bucket := req.GetBucket()
|
|
if bucket == nil {
|
|
return nil, status.Error(codes.InvalidArgument, "bucket is required")
|
|
}
|
|
|
|
attrs := bucket.GetAttributes()
|
|
if attrs == nil {
|
|
return nil, status.Error(codes.InvalidArgument, "attributes is required")
|
|
}
|
|
|
|
storageAttributes, err := getStorageAttributes(attrs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get the persisted data.
|
|
// NOTE: We return on error here, blocking the delete. This may or
|
|
// may not be an overzealous approach to maintaining database/state
|
|
// integrity. May need to be changed at later time if there are
|
|
// scenarios where we might be deleting things and any secret state
|
|
// may be corrupt/and or legitimately missing.
|
|
credState, err := cred.AwsCredentialPersistedStateFromProto(
|
|
req.GetPersisted().GetData(),
|
|
append(p.testCredStateOpts, cred.WithRegion(storageAttributes.Region))...)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
|
}
|
|
|
|
_, err = newAwsStoragePersistedState(
|
|
append([]awsStoragePersistedStateOption{
|
|
withCredentials(credState),
|
|
}, p.testStorageStateOpts...)...,
|
|
)
|
|
if err != nil {
|
|
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)
|
|
}
|
|
}
|
|
|
|
return &pb.OnDeleteStorageBucketResponse{}, nil
|
|
}
|
|
|
|
// HeadObject is called to get the metadata of an object.
|
|
func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectRequest) (*pb.HeadObjectResponse, error) {
|
|
if req.GetKey() == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "key is required")
|
|
}
|
|
|
|
bucket := req.GetBucket()
|
|
if bucket == nil {
|
|
return nil, status.Error(codes.InvalidArgument, "bucket is required")
|
|
}
|
|
|
|
if bucket.BucketName == "" {
|
|
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")
|
|
}
|
|
|
|
storageAttributes, err := getStorageAttributes(attrs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 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),
|
|
}, p.testCredStateOpts...)...,
|
|
)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
|
|
}
|
|
|
|
storageState, err := newAwsStoragePersistedState(
|
|
append([]awsStoragePersistedStateOption{
|
|
withCredentials(credState),
|
|
}, p.testStorageStateOpts...)...,
|
|
)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
|
|
}
|
|
|
|
opts := []s3Option{}
|
|
if storageAttributes.Region != "" {
|
|
opts = append(opts, WithRegion(storageAttributes.Region))
|
|
}
|
|
if storageAttributes.EndpointUrl != "" {
|
|
opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl))
|
|
}
|
|
s3Client, err := storageState.S3Client(ctx, opts...)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "error getting S3 client: %s", err)
|
|
}
|
|
|
|
objectKey := path.Join(bucket.GetBucketPrefix(), req.GetKey())
|
|
resp, err := s3Client.HeadObject(ctx, &s3.HeadObjectInput{
|
|
Bucket: aws.String(bucket.GetBucketName()),
|
|
Key: aws.String(objectKey),
|
|
})
|
|
if err != nil {
|
|
return nil, parseAWSErrCode("error getting head object from s3", err)
|
|
}
|
|
return &pb.HeadObjectResponse{
|
|
ContentLength: resp.ContentLength,
|
|
LastModified: timestamppb.New(*resp.LastModified),
|
|
}, nil
|
|
}
|
|
|
|
// ValidatePermissions is called to validate the secrets associated with the storage bucket.
|
|
func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.ValidatePermissionsRequest) (*pb.ValidatePermissionsResponse, error) {
|
|
bucket := req.GetBucket()
|
|
if bucket == nil {
|
|
return nil, status.Error(codes.InvalidArgument, "bucket is required")
|
|
}
|
|
|
|
if bucket.BucketName == "" {
|
|
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")
|
|
}
|
|
|
|
storageAttributes, err := getStorageAttributes(attrs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 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),
|
|
}, p.testCredStateOpts...)...,
|
|
)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
|
|
}
|
|
|
|
storageState, err := newAwsStoragePersistedState(
|
|
append([]awsStoragePersistedStateOption{
|
|
withCredentials(credState),
|
|
}, p.testStorageStateOpts...)...,
|
|
)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
|
|
}
|
|
|
|
// perform dry run to ensure we can interact with the bucket as expected.
|
|
if err := dryRunValidation(ctx, storageState, storageAttributes, bucket); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &pb.ValidatePermissionsResponse{}, nil
|
|
}
|
|
|
|
// GetObject is called when retrieving objects from an s3 bucket.
|
|
// GetObject is a blocking call until the stream has been recieved in full.
|
|
func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePluginService_GetObjectServer) error {
|
|
if req.GetKey() == "" {
|
|
return status.Error(codes.InvalidArgument, "key is required")
|
|
}
|
|
|
|
bucket := req.GetBucket()
|
|
if bucket == nil {
|
|
return status.Error(codes.InvalidArgument, "bucket is required")
|
|
}
|
|
|
|
if bucket.GetBucketName() == "" {
|
|
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")
|
|
}
|
|
|
|
storageAttributes, err := getStorageAttributes(attrs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 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),
|
|
}, p.testCredStateOpts...)...,
|
|
)
|
|
if err != nil {
|
|
return status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
|
|
}
|
|
|
|
storageState, err := newAwsStoragePersistedState(
|
|
append([]awsStoragePersistedStateOption{
|
|
withCredentials(credState),
|
|
}, p.testStorageStateOpts...)...,
|
|
)
|
|
if err != nil {
|
|
return status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
|
|
}
|
|
|
|
opts := []s3Option{}
|
|
if storageAttributes.Region != "" {
|
|
opts = append(opts, WithRegion(storageAttributes.Region))
|
|
}
|
|
if storageAttributes.EndpointUrl != "" {
|
|
opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl))
|
|
}
|
|
s3Client, err := storageState.S3Client(stream.Context(), opts...)
|
|
if err != nil {
|
|
return status.Errorf(codes.InvalidArgument, "error getting S3 client: %s", err)
|
|
}
|
|
|
|
objectKey := path.Join(bucket.GetBucketPrefix(), req.GetKey())
|
|
resp, err := s3Client.GetObject(stream.Context(), &s3.GetObjectInput{
|
|
Bucket: aws.String(bucket.GetBucketName()),
|
|
Key: aws.String(objectKey),
|
|
})
|
|
if err != nil {
|
|
return parseAWSErrCode("error getting object from s3", err)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
reader := bufio.NewReader(resp.Body)
|
|
chunkSize := req.GetChunkSize()
|
|
if chunkSize == 0 {
|
|
chunkSize = defaultStreamChunkSize
|
|
}
|
|
for {
|
|
buffer := make([]byte, chunkSize)
|
|
n, err := reader.Read(buffer)
|
|
if err != nil && err != io.EOF {
|
|
return status.Errorf(codes.Internal, "error reading chunk from s3: %s", err)
|
|
}
|
|
if n > 0 {
|
|
if err := stream.Send(&pb.GetObjectResponse{
|
|
FileChunk: buffer[:n],
|
|
}); err != nil {
|
|
return status.Errorf(codes.Internal, "error sending chunk to client: %s", err)
|
|
}
|
|
}
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PutObject is called when putting objects into an s3 bucket.
|
|
func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest) (*pb.PutObjectResponse, error) {
|
|
if req == nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "request is required")
|
|
}
|
|
if req.GetKey() == "" {
|
|
return nil, status.Errorf(codes.InvalidArgument, "key is required")
|
|
}
|
|
bucket := req.GetBucket()
|
|
if bucket == nil {
|
|
return nil, status.Error(codes.InvalidArgument, "bucket is required")
|
|
}
|
|
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")
|
|
}
|
|
if req.GetPath() == "" {
|
|
return nil, status.Errorf(codes.InvalidArgument, "path is required")
|
|
}
|
|
file, err := os.Open(req.GetPath())
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to open file")
|
|
}
|
|
info, err := file.Stat()
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to read file info")
|
|
}
|
|
if info.IsDir() {
|
|
return nil, status.Errorf(codes.InvalidArgument, "path is not a file")
|
|
}
|
|
if info.Size() == 0 {
|
|
return nil, status.Errorf(codes.InvalidArgument, "file is empty")
|
|
}
|
|
|
|
storageAttributes, err := getStorageAttributes(attrs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 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),
|
|
}, p.testCredStateOpts...)...,
|
|
)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
|
|
}
|
|
|
|
storageState, err := newAwsStoragePersistedState(
|
|
append([]awsStoragePersistedStateOption{
|
|
withCredentials(credState),
|
|
}, p.testStorageStateOpts...)...,
|
|
)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
|
|
}
|
|
|
|
opts := []s3Option{}
|
|
if storageAttributes.Region != "" {
|
|
opts = append(opts, WithRegion(storageAttributes.Region))
|
|
}
|
|
if storageAttributes.EndpointUrl != "" {
|
|
opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl))
|
|
}
|
|
s3Client, err := storageState.S3Client(ctx, opts...)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "error getting S3 client: %s", err)
|
|
}
|
|
|
|
hash := sha256.New()
|
|
if _, err := io.Copy(hash, file); err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to calcualte hash")
|
|
}
|
|
checksum := base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
|
|
|
_, err = file.Seek(0, io.SeekStart)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to rewind file pointer")
|
|
}
|
|
|
|
objectKey := path.Join(bucket.GetBucketPrefix(), req.GetKey())
|
|
resp, err := s3Client.PutObject(ctx, &s3.PutObjectInput{
|
|
Bucket: aws.String(bucket.GetBucketName()),
|
|
Key: aws.String(objectKey),
|
|
Body: file,
|
|
ChecksumAlgorithm: types.ChecksumAlgorithmSha256,
|
|
ChecksumSHA256: aws.String(checksum),
|
|
})
|
|
if err != nil {
|
|
return nil, parseAWSErrCode("error putting object into s3", err)
|
|
}
|
|
if resp.ChecksumSHA256 == nil {
|
|
return nil, status.Errorf(codes.Internal, "missing checksum response from aws")
|
|
}
|
|
if checksum != *resp.ChecksumSHA256 {
|
|
return nil, status.Errorf(codes.Internal, "mismatched checksum")
|
|
}
|
|
decodedChecksum, err := base64.StdEncoding.DecodeString(*resp.ChecksumSHA256)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to decode checksum value from aws")
|
|
}
|
|
return &pb.PutObjectResponse{
|
|
ChecksumSha_256: decodedChecksum,
|
|
}, nil
|
|
}
|
|
|
|
// dryRunValidation validates that the IAM role policy attached to the given secrets of the
|
|
// storage bucket has the minimum required permissions needed for this plugin to function
|
|
// as expected. This function will create an object in the s3 bucket to validate it has
|
|
// PutObject permissions. This function will read the same file it had uploaded to validate
|
|
// it has GetObject permissions. This function will read the metadata of the same file it
|
|
// had uploaded to validate it has GetObjectAttributes permissions.
|
|
func dryRunValidation(ctx context.Context, state *awsStoragePersistedState, attrs *StorageAttributes, bucket *storagebuckets.StorageBucket) error {
|
|
if state == nil {
|
|
return status.Error(codes.InvalidArgument, "persisted state is required")
|
|
}
|
|
if attrs == nil {
|
|
return status.Error(codes.InvalidArgument, "attributes is required")
|
|
}
|
|
if bucket == nil {
|
|
return status.Error(codes.InvalidArgument, "bucket is required")
|
|
}
|
|
|
|
opts := []s3Option{}
|
|
if attrs.Region != "" {
|
|
opts = append(opts, WithRegion(attrs.Region))
|
|
}
|
|
if attrs.EndpointUrl != "" {
|
|
opts = append(opts, WithEndpoint(attrs.EndpointUrl))
|
|
}
|
|
|
|
client, err := state.S3Client(ctx, opts...)
|
|
if err != nil {
|
|
return status.Errorf(codes.InvalidArgument, "error getting S3 client: %s", err)
|
|
}
|
|
|
|
objectKey := path.Join(bucket.GetBucketPrefix(), uuid.New().String())
|
|
if _, err := client.PutObject(ctx, &s3.PutObjectInput{
|
|
Bucket: aws.String(bucket.GetBucketName()),
|
|
Key: aws.String(objectKey),
|
|
Body: bytes.NewReader([]byte("hashicorp boundary aws plugin access test")),
|
|
}); err != nil {
|
|
return parseAWSErrCode("error failed to put object", err)
|
|
}
|
|
|
|
if _, err := client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: aws.String(bucket.GetBucketName()),
|
|
Key: aws.String(objectKey),
|
|
}); err != nil {
|
|
return parseAWSErrCode("error failed to get object", err)
|
|
}
|
|
|
|
if _, err := client.HeadObject(ctx, &s3.HeadObjectInput{
|
|
Bucket: aws.String(bucket.GetBucketName()),
|
|
Key: aws.String(objectKey),
|
|
}); err != nil {
|
|
return parseAWSErrCode("error failed to get head object", err)
|
|
}
|
|
|
|
// attempt to delete the test object created for the dry run validation,
|
|
// this step is allowed to fail because DeleteObject is not a required
|
|
// operation for the plugin.
|
|
client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
|
Bucket: aws.String(bucket.GetBucketName()),
|
|
Key: aws.String(objectKey),
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseAWSErrCode detects if an aws error is a throttle error.
|
|
// If the error is a throttle error, then an Unavailable status code
|
|
// is returned. Otherwise the default status code is an Internal error.
|
|
func parseAWSErrCode(msg string, err error) error {
|
|
errCode := codes.Internal
|
|
retryErr := retry.ThrottleErrorCode{
|
|
Codes: retry.DefaultThrottleErrorCodes,
|
|
}
|
|
if retryErr.IsErrorThrottle(err).Bool() {
|
|
errCode = codes.Unavailable
|
|
}
|
|
return status.Errorf(errCode, "%s: %s", msg, err)
|
|
}
|