Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7833b5df47 | ||
|
|
6bda6813aa | ||
|
|
bdd27e2562 | ||
|
|
f17f05ec82 | ||
|
|
899c62ce06 | ||
|
|
ce22c1c46a | ||
|
|
2f9163ad0e | ||
|
|
7caff84c06 |
No files matched your search
@@ -4,3 +4,9 @@ plan.tfplan
|
||||
*.log
|
||||
*.out
|
||||
*.html
|
||||
|
||||
# IntelliJ IDEA project files
|
||||
.idea
|
||||
*.iprg
|
||||
|
||||
.DS_Store
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo, unless a later match takes precedence.
|
||||
|
||||
@hashicorp/boundary
|
||||
|
||||
* @hashicorp/boundary
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/hashicorp/boundary-plugin-aws
|
||||
|
||||
go 1.21
|
||||
go 1.23.3
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.20.1
|
||||
@@ -11,7 +11,7 @@ require (
|
||||
github.com/aws/smithy-go v1.14.1
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/hashicorp/boundary/sdk v0.0.43-0.20240717182311-a20aae98794a
|
||||
github.com/hashicorp/boundary/sdk v0.0.49
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.0.0
|
||||
github.com/hashicorp/terraform-json v0.14.0
|
||||
|
||||
@@ -78,8 +78,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/boundary/sdk v0.0.43-0.20240717182311-a20aae98794a h1:SwVze6sYE5o+J9qdcgj29auY1k6O8oSo9DC2+2Gb9rw=
|
||||
github.com/hashicorp/boundary/sdk v0.0.43-0.20240717182311-a20aae98794a/go.mod h1:9iOT7kDM6mYcSkKxNuZlv8rP7U5BG1kXoevjLLL8lNQ=
|
||||
github.com/hashicorp/boundary/sdk v0.0.49 h1:XOb6mSKyrU/wI20+5xTYBHQUP7eIeKcLxKSCpCs4yzM=
|
||||
github.com/hashicorp/boundary/sdk v0.0.49/go.mod h1:IHP79to8aIi22FiY58pgBqJL96/U9D8ZAUhS2DdC+Us=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
|
||||
@@ -114,7 +114,7 @@ func (s *AwsCredentialPersistedState) ValidateCreds(ctx context.Context) error {
|
||||
return errors.BadRequestStatus("missing credentials config")
|
||||
}
|
||||
if _, err := s.CredentialsConfig.GetCallerIdentity(ctx, s.testOpts...); err != nil {
|
||||
st, _ := errors.ParseAWSError(err, "validating credentials")
|
||||
st, _ := errors.ParseAWSError("validating credentials", err)
|
||||
return st.Err()
|
||||
}
|
||||
return nil
|
||||
@@ -135,7 +135,7 @@ func (s *AwsCredentialPersistedState) RotateCreds(ctx context.Context) error {
|
||||
if err := s.CredentialsConfig.RotateKeys(ctx, append([]awsutil.Option{
|
||||
awsutil.WithValidityCheckTimeout(rotationWaitTimeout),
|
||||
}, s.testOpts...)...); err != nil {
|
||||
st, _ := errors.ParseAWSError(err, "error rotating credentials")
|
||||
st, _ := errors.ParseAWSError("rotating credentials", err)
|
||||
return st.Err()
|
||||
}
|
||||
s.CredsLastRotatedTime = time.Now()
|
||||
@@ -192,7 +192,7 @@ func (s *AwsCredentialPersistedState) DeleteCreds(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Otherwise treat it as an actual error.
|
||||
st, _ := errors.ParseAWSError(err, "failed to delete access key")
|
||||
st, _ := errors.ParseAWSError("deleting credentials", err)
|
||||
return st.Err()
|
||||
}
|
||||
|
||||
@@ -252,17 +252,17 @@ func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *Crede
|
||||
|
||||
accessKeyId, err := values.GetStringValue(secrets, ConstAccessKeyId, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||
return nil, fmt.Errorf("persisted state integrity: %w", err)
|
||||
}
|
||||
|
||||
secretAccessKey, err := values.GetStringValue(secrets, ConstSecretAccessKey, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||
return nil, fmt.Errorf("persisted state integrity: %w", err)
|
||||
}
|
||||
|
||||
credsLastRotatedTime, err := values.GetTimeValue(secrets, ConstCredsLastRotatedTime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||
return nil, fmt.Errorf("persisted state integrity: %w", err)
|
||||
}
|
||||
|
||||
s, err := NewAwsCredentialPersistedState(opts...)
|
||||
|
||||
@@ -186,7 +186,7 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expectedErr: "persisted state integrity error: could not parse time in value \"creds_last_rotated_time\": parsing time \"notatime\" as \"2006-01-02T15:04:05.999999999Z07:00\": cannot parse \"notatime\" as \"2006\"",
|
||||
expectedErr: "persisted state integrity: could not parse time in value \"creds_last_rotated_time\": parsing time \"notatime\" as \"2006-01-02T15:04:05.999999999Z07:00\": cannot parse \"notatime\" as \"2006\"",
|
||||
},
|
||||
{
|
||||
name: "option error",
|
||||
@@ -458,7 +458,7 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) {
|
||||
),
|
||||
},
|
||||
},
|
||||
expectedErr: "aws service unknown: unknown error: error rotating credentials",
|
||||
expectedErr: "aws service unknown: unknown error: rotating credentials",
|
||||
expectedStatusCode: codes.Unknown,
|
||||
},
|
||||
{
|
||||
@@ -568,7 +568,7 @@ func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) {
|
||||
},
|
||||
},
|
||||
credentialConfig: &awsutil.CredentialsConfig{},
|
||||
expectedErr: "aws service unknown: unknown error: failed to delete access key",
|
||||
expectedErr: "aws service unknown: unknown error: deleting credentials",
|
||||
expectedStatusCode: codes.Unknown,
|
||||
},
|
||||
{
|
||||
@@ -693,7 +693,7 @@ func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) {
|
||||
),
|
||||
},
|
||||
},
|
||||
expectedErr: "aws service unknown: unknown error: failed to delete access key",
|
||||
expectedErr: "aws service unknown: unknown error: deleting credentials",
|
||||
expectedStatusCode: codes.Unknown,
|
||||
},
|
||||
{
|
||||
|
||||
+15
-14
@@ -81,11 +81,12 @@ func InvalidArgumentError(msg string, f map[string]string) error {
|
||||
// and credentials. This method will fallback to parsing the http status code
|
||||
// when it cannot match an aws error code. This method does not handle specific
|
||||
// service type errors such as S3 or EC2.
|
||||
func ParseAWSError(err error, msg string) (st *status.Status, permission *pb.Permission) {
|
||||
func ParseAWSError(op string, err error) (st *status.Status, permission *pb.Permission) {
|
||||
if err == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("%s: %v", op, err)
|
||||
// find the service name of the aws api
|
||||
serviceName := "unknown"
|
||||
var oe *smithy.OperationError
|
||||
@@ -113,7 +114,7 @@ func ParseAWSError(err error, msg string) (st *status.Status, permission *pb.Per
|
||||
Codes: retry.DefaultThrottleErrorCodes,
|
||||
}
|
||||
if throttleErr.IsErrorThrottle(err).Bool() {
|
||||
statusMsg := fmt.Sprintf("aws service %s: throttling error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: throttling: %s", serviceName, msg)
|
||||
return status.New(codes.Unavailable, statusMsg), nil
|
||||
}
|
||||
|
||||
@@ -126,31 +127,31 @@ func ParseAWSError(err error, msg string) (st *status.Status, permission *pb.Per
|
||||
case awsErrorInvalidAccessKeyId:
|
||||
fallthrough
|
||||
case awsErrorExpiredToken:
|
||||
statusMsg := fmt.Sprintf("aws service %s: invalid credentials error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: invalid credentials: %s", serviceName, msg)
|
||||
return status.New(codes.PermissionDenied, statusMsg), &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
ErrorDetails: apiErr.ErrorMessage(),
|
||||
CheckedAt: timestamppb.Now(),
|
||||
}
|
||||
case awsErrorNoSuchBucket:
|
||||
statusMsg := fmt.Sprintf("aws %s error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
|
||||
return status.New(codes.NotFound, statusMsg), &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
ErrorDetails: apiErr.ErrorMessage(),
|
||||
CheckedAt: timestamppb.Now(),
|
||||
}
|
||||
case awsErrorBadDigest:
|
||||
statusMsg := fmt.Sprintf("aws %s error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
|
||||
return status.New(codes.Aborted, statusMsg), nil
|
||||
case awsErrorNoSuchKey:
|
||||
fallthrough
|
||||
case awsErrorInvalidObjectState:
|
||||
statusMsg := fmt.Sprintf("aws %s error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
|
||||
return status.New(codes.NotFound, statusMsg), nil
|
||||
case awsErrorRequestTimeout:
|
||||
fallthrough
|
||||
case awsErrorRequestTimeoutException:
|
||||
statusMsg := fmt.Sprintf("aws %s error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
|
||||
return status.New(codes.DeadlineExceeded, statusMsg), nil
|
||||
}
|
||||
}
|
||||
@@ -165,39 +166,39 @@ func ParseAWSError(err error, msg string) (st *status.Status, permission *pb.Per
|
||||
defer httpErr.Response.Body.Close()
|
||||
switch httpErr.HTTPStatusCode() {
|
||||
case http.StatusBadRequest:
|
||||
statusMsg := fmt.Sprintf("aws service %s: bad request error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: bad request: %s", serviceName, msg)
|
||||
return status.New(codes.InvalidArgument, statusMsg), nil
|
||||
case http.StatusUnauthorized:
|
||||
statusMsg := fmt.Sprintf("aws service %s: invalid credentials error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: invalid credentials: %s", serviceName, msg)
|
||||
return status.New(codes.PermissionDenied, statusMsg), &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
ErrorDetails: buf.String(),
|
||||
CheckedAt: timestamppb.Now(),
|
||||
}
|
||||
case http.StatusForbidden:
|
||||
statusMsg := fmt.Sprintf("aws service %s: invalid credentials error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: invalid credentials: %s", serviceName, msg)
|
||||
return status.New(codes.PermissionDenied, statusMsg), &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
ErrorDetails: buf.String(),
|
||||
CheckedAt: timestamppb.Now(),
|
||||
}
|
||||
case http.StatusNotFound:
|
||||
statusMsg := fmt.Sprintf("aws service %s: resource not found error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: resource not found: %s", serviceName, msg)
|
||||
return status.New(codes.NotFound, statusMsg), nil
|
||||
case http.StatusTooManyRequests:
|
||||
statusMsg := fmt.Sprintf("aws service %s: throttling error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: throttling: %s", serviceName, msg)
|
||||
return status.New(codes.Unavailable, statusMsg), nil
|
||||
case http.StatusInternalServerError:
|
||||
fallthrough
|
||||
case http.StatusBadGateway:
|
||||
fallthrough
|
||||
case http.StatusServiceUnavailable:
|
||||
statusMsg := fmt.Sprintf("aws service %s: connectivity error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: connectivity: %s", serviceName, msg)
|
||||
return status.New(codes.Unavailable, statusMsg), nil
|
||||
case http.StatusRequestTimeout:
|
||||
fallthrough
|
||||
case http.StatusGatewayTimeout:
|
||||
statusMsg := fmt.Sprintf("aws service %s: timeout error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: timeout: %s", serviceName, msg)
|
||||
return status.New(codes.DeadlineExceeded, statusMsg), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-bad-request",
|
||||
err: TestAwsHttpResponseError(http.StatusBadRequest, "bad request"),
|
||||
expectedStatusCode: codes.InvalidArgument,
|
||||
expectedStatusMsg: "aws service unknown: bad request error: test",
|
||||
expectedStatusMsg: "aws service unknown: bad request: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -53,7 +53,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-unauthorized",
|
||||
err: TestAwsHttpResponseError(http.StatusUnauthorized, "unauthorized request"),
|
||||
expectedStatusCode: codes.PermissionDenied,
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials error: test",
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
ErrorDetails: "unauthorized request",
|
||||
@@ -64,7 +64,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-forbidden",
|
||||
err: TestAwsHttpResponseError(http.StatusForbidden, "forbidden request"),
|
||||
expectedStatusCode: codes.PermissionDenied,
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials error: test",
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
ErrorDetails: "forbidden request",
|
||||
@@ -75,7 +75,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-not-found",
|
||||
err: TestAwsHttpResponseError(http.StatusNotFound, "not found request"),
|
||||
expectedStatusCode: codes.NotFound,
|
||||
expectedStatusMsg: "aws service unknown: resource not found error: test",
|
||||
expectedStatusMsg: "aws service unknown: resource not found: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -84,7 +84,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-too-many-requests",
|
||||
err: TestAwsHttpResponseError(http.StatusTooManyRequests, "too many requests"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -93,7 +93,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-request-timeout",
|
||||
err: TestAwsHttpResponseError(http.StatusRequestTimeout, "request timeout"),
|
||||
expectedStatusCode: codes.DeadlineExceeded,
|
||||
expectedStatusMsg: "aws service unknown: timeout error: test",
|
||||
expectedStatusMsg: "aws service unknown: timeout: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -102,7 +102,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-internal-server-error",
|
||||
err: TestAwsHttpResponseError(http.StatusInternalServerError, "internal server error"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: connectivity error: test",
|
||||
expectedStatusMsg: "aws service unknown: connectivity: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -111,7 +111,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-bad-gateway",
|
||||
err: TestAwsHttpResponseError(http.StatusBadGateway, "bad gateway"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: connectivity error: test",
|
||||
expectedStatusMsg: "aws service unknown: connectivity: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -120,7 +120,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-service-unavailable",
|
||||
err: TestAwsHttpResponseError(http.StatusServiceUnavailable, "service unavailable"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: connectivity error: test",
|
||||
expectedStatusMsg: "aws service unknown: connectivity: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -129,7 +129,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-gateway-timeout",
|
||||
err: TestAwsHttpResponseError(http.StatusGatewayTimeout, "gateway timeout"),
|
||||
expectedStatusCode: codes.DeadlineExceeded,
|
||||
expectedStatusMsg: "aws service unknown: timeout error: test",
|
||||
expectedStatusMsg: "aws service unknown: timeout: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -138,7 +138,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-throttling",
|
||||
err: TestAwsError("Throttling", "throttling error"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -147,7 +147,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-throttling-exception",
|
||||
err: TestAwsError("ThrottlingException", "throttling exception"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -156,7 +156,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-throttled-exception",
|
||||
err: TestAwsError("ThrottledException", "throttled exception"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -165,7 +165,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-throttled-exception",
|
||||
err: TestAwsError("RequestThrottledException", "request throttled exception"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -174,7 +174,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-too-many-requests-exception",
|
||||
err: TestAwsError("TooManyRequestsException", "too many requests exception"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -183,7 +183,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-provisioned-throughput-exceeded-exception",
|
||||
err: TestAwsError("ProvisionedThroughputExceededException", "provisioned throughput exceeded exception"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -192,7 +192,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-transaction-in-progress-exception",
|
||||
err: TestAwsError("TransactionInProgressException", "transaction in progress exception"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -201,7 +201,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-limit-exceeded",
|
||||
err: TestAwsError("RequestLimitExceeded", "request limit exceeded"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -210,7 +210,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-bandwidth-limit-exceeded",
|
||||
err: TestAwsError("BandwidthLimitExceeded", "bandwidth limit exceeded"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -219,7 +219,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-limit-exceeded-exception",
|
||||
err: TestAwsError("LimitExceededException", "limit exceeded exception"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -228,7 +228,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-throttled",
|
||||
err: TestAwsError("RequestThrottled", "request throttled"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -237,7 +237,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-slow-down",
|
||||
err: TestAwsError("SlowDown", "slow down"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -246,7 +246,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-prior-request-not-complete",
|
||||
err: TestAwsError("PriorRequestNotComplete", "prior request not complete"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -255,7 +255,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-ec2-throttled-exception",
|
||||
err: TestAwsError("EC2ThrottledException", "ec2 throttled exception"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -264,7 +264,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-timeout",
|
||||
err: TestAwsError("RequestTimeout", "request timeout"),
|
||||
expectedStatusCode: codes.DeadlineExceeded,
|
||||
expectedStatusMsg: "aws unknown error: test",
|
||||
expectedStatusMsg: "aws service unknown: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -273,7 +273,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-timeout-exception",
|
||||
err: TestAwsError("RequestTimeoutException", "request timeout exception"),
|
||||
expectedStatusCode: codes.DeadlineExceeded,
|
||||
expectedStatusMsg: "aws unknown error: test",
|
||||
expectedStatusMsg: "aws service unknown: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -282,7 +282,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-access-denied",
|
||||
err: TestAwsError(awsErrorAccessDenied, "not authorized to perform action"),
|
||||
expectedStatusCode: codes.PermissionDenied,
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials error: test",
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
ErrorDetails: "not authorized to perform action",
|
||||
@@ -293,7 +293,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-invalid-access-key-id",
|
||||
err: TestAwsError(awsErrorInvalidAccessKeyId, "The AWS Access Key Id you provided does not exist in our records."),
|
||||
expectedStatusCode: codes.PermissionDenied,
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials error: test",
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
ErrorDetails: "The AWS Access Key Id you provided does not exist in our records.",
|
||||
@@ -304,7 +304,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-expired-token",
|
||||
err: TestAwsError(awsErrorExpiredToken, "The security token included in the request is expired."),
|
||||
expectedStatusCode: codes.PermissionDenied,
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials error: test",
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
ErrorDetails: "The security token included in the request is expired.",
|
||||
@@ -316,14 +316,14 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require, assert := require.New(t), assert.New(t)
|
||||
|
||||
actualStatus, permission := ParseAWSError(tt.err, "test")
|
||||
actualStatus, permission := ParseAWSError("test", tt.err)
|
||||
if tt.expectedStatusCode == codes.OK {
|
||||
require.Nil(actualStatus)
|
||||
return
|
||||
}
|
||||
|
||||
require.Equal(tt.expectedStatusCode, actualStatus.Code())
|
||||
require.Equal(tt.expectedStatusMsg, actualStatus.Message())
|
||||
require.Contains(actualStatus.Message(), tt.expectedStatusMsg)
|
||||
require.Len(actualStatus.Details(), 0)
|
||||
CheckSimilarPermission(assert, tt.expectedPermission, permission, true)
|
||||
})
|
||||
|
||||
+1
-1
@@ -32,6 +32,6 @@ type AwsPlugin struct {
|
||||
func NewAwsPlugin() *AwsPlugin {
|
||||
return &AwsPlugin{
|
||||
HostPlugin: &host.HostPlugin{},
|
||||
StoragePlugin: &storage.StoragePlugin{},
|
||||
StoragePlugin: storage.New(),
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,14 @@ The following `attributes` are valid on an AWS host catalog resource:
|
||||
be performed. See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
|
||||
- `region` (string): The region to configure the host catalog for. All host sets
|
||||
in this catalog will be configured for this region.
|
||||
- `role_arn` (string): The AWS Role ARN used for `AssumeRole` authentication. If
|
||||
provided, `disable_credential_rotation` must be also set to `true.
|
||||
- `role_external_id` (string): The external id configured for the `AssumeRole`
|
||||
provider.
|
||||
- `role_session_name` (string): The session name configured for the `AssumeRole`
|
||||
provider.
|
||||
- `role_tags` (object): The key-value pair tags configured for the `AssumeRole`
|
||||
provider.
|
||||
|
||||
The following `secrets` are required on an AWS host catalog resource:
|
||||
|
||||
|
||||
@@ -683,7 +683,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
|
||||
),
|
||||
}),
|
||||
},
|
||||
expectedErrContains: "error rotating credentials",
|
||||
expectedErrContains: "rotating credentials",
|
||||
expectedErrCode: codes.Unknown,
|
||||
},
|
||||
{
|
||||
@@ -1089,7 +1089,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
|
||||
),
|
||||
}),
|
||||
},
|
||||
expectedErrContains: "failed to delete access key",
|
||||
expectedErrContains: "deleting credentials",
|
||||
expectedErrCode: codes.Unknown,
|
||||
},
|
||||
{
|
||||
@@ -1167,7 +1167,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
|
||||
),
|
||||
}),
|
||||
},
|
||||
expectedErrContains: "error rotating credentials",
|
||||
expectedErrContains: "rotating credentials",
|
||||
expectedErrCode: codes.Unknown,
|
||||
},
|
||||
{
|
||||
@@ -1298,7 +1298,7 @@ func TestPluginOnDeleteCatalogErr(t *testing.T) {
|
||||
),
|
||||
}),
|
||||
},
|
||||
expectedErrContains: "aws service unknown: unknown error: failed to delete access key",
|
||||
expectedErrContains: "aws service unknown: unknown error: deleting credentials:",
|
||||
expectedErrCode: codes.Unknown,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -50,12 +50,12 @@ func setStatePermission(req any, permission *pb.Permission) *pb.StorageBucketCre
|
||||
// credentials, and connectivity error types.
|
||||
//
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html
|
||||
func parseS3Error(err error, msg string, req any) *status.Status {
|
||||
func parseS3Error(op string, err error, req any) *status.Status {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
st, p := errors.ParseAWSError(err, msg)
|
||||
st, p := errors.ParseAWSError(op, err)
|
||||
if p != nil {
|
||||
state := setStatePermission(req, p)
|
||||
if st, err = st.WithDetails(state); err != nil {
|
||||
|
||||
@@ -34,7 +34,7 @@ func Test_ParseS3Error(t *testing.T) {
|
||||
req: &pb.GetObjectRequest{},
|
||||
err: TestAwsS3Error("NoSuchBucket", "GetObject", "no such bucket"),
|
||||
expectedStatusCode: codes.NotFound,
|
||||
expectedStatusMsg: "aws s3 error: test",
|
||||
expectedStatusMsg: "aws service s3: test:",
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Read: &pb.Permission{
|
||||
@@ -50,7 +50,7 @@ func Test_ParseS3Error(t *testing.T) {
|
||||
req: &pb.GetObjectRequest{},
|
||||
err: TestAwsS3Error("NoSuchKey", "GetObject", "no such key"),
|
||||
expectedStatusCode: codes.NotFound,
|
||||
expectedStatusMsg: "aws s3 error: test",
|
||||
expectedStatusMsg: "aws service s3: test:",
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Read: &pb.Permission{
|
||||
@@ -66,7 +66,7 @@ func Test_ParseS3Error(t *testing.T) {
|
||||
err: errors.TestAwsHttpResponseError(404, "not found"),
|
||||
expectedStatusCode: codes.NotFound,
|
||||
// cannot embed services within mock http errors, therefore unknown service
|
||||
expectedStatusMsg: "aws service unknown: resource not found error: test",
|
||||
expectedStatusMsg: "aws service unknown: resource not found: test:",
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Read: &pb.Permission{
|
||||
@@ -81,7 +81,7 @@ func Test_ParseS3Error(t *testing.T) {
|
||||
req: &pb.GetObjectRequest{},
|
||||
err: TestAwsS3Error("InvalidObjectState", "GetObject", "invalid object state"),
|
||||
expectedStatusCode: codes.NotFound,
|
||||
expectedStatusMsg: "aws s3 error: test",
|
||||
expectedStatusMsg: "aws service s3: test:",
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Read: &pb.Permission{
|
||||
@@ -96,7 +96,7 @@ func Test_ParseS3Error(t *testing.T) {
|
||||
req: &pb.PutObjectRequest{},
|
||||
err: TestAwsS3Error(s3ErrorBadDigest, "PutObject", "checksum mismatch"),
|
||||
expectedStatusCode: codes.Aborted,
|
||||
expectedStatusMsg: "aws s3 error: test",
|
||||
expectedStatusMsg: "aws service s3: test:",
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Write: &pb.Permission{
|
||||
@@ -111,7 +111,7 @@ func Test_ParseS3Error(t *testing.T) {
|
||||
req: &pb.GetObjectRequest{},
|
||||
err: TestAwsS3Error("Throttling", "action", "throttling exception"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service s3: throttling error: test",
|
||||
expectedStatusMsg: "aws service s3: throttling: test:",
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Read: &pb.Permission{
|
||||
@@ -126,7 +126,7 @@ func Test_ParseS3Error(t *testing.T) {
|
||||
req: &pb.GetObjectRequest{},
|
||||
err: TestAwsS3Error("RequestTimeout", "action", "request timeout"),
|
||||
expectedStatusCode: codes.DeadlineExceeded,
|
||||
expectedStatusMsg: "aws s3 error: test",
|
||||
expectedStatusMsg: "aws service s3: test:",
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Read: &pb.Permission{
|
||||
@@ -141,7 +141,7 @@ func Test_ParseS3Error(t *testing.T) {
|
||||
req: &pb.GetObjectRequest{},
|
||||
err: TestAwsS3Error("AccessDenied", "action", "access denied"),
|
||||
expectedStatusCode: codes.PermissionDenied,
|
||||
expectedStatusMsg: "aws service s3: invalid credentials error: test",
|
||||
expectedStatusMsg: "aws service s3: invalid credentials: test:",
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Read: &pb.Permission{
|
||||
@@ -157,14 +157,14 @@ func Test_ParseS3Error(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require, assert := require.New(t), assert.New(t)
|
||||
|
||||
actualStatus := parseS3Error(tt.err, "test", tt.req)
|
||||
actualStatus := parseS3Error("test", tt.err, tt.req)
|
||||
if tt.expectedStatusCode == codes.OK {
|
||||
require.Nil(actualStatus)
|
||||
return
|
||||
}
|
||||
|
||||
require.Equal(tt.expectedStatusCode, actualStatus.Code())
|
||||
require.Equal(tt.expectedStatusMsg, actualStatus.Message())
|
||||
require.Contains(actualStatus.Message(), tt.expectedStatusMsg)
|
||||
require.Len(actualStatus.Details(), 1)
|
||||
|
||||
foundDetail := false
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
@@ -32,6 +33,14 @@ import (
|
||||
// Ensure that we are implementing StoragePluginServiceServer
|
||||
var _ pb.StoragePluginServiceServer = (*StoragePlugin)(nil)
|
||||
|
||||
// clientCache caches previously initialized S3API clients created with a RoleARN attribute.
|
||||
type clientCache struct {
|
||||
// cache is a map of cached s3 clients. The key of the map
|
||||
// is the public id of the storage bucket
|
||||
cache map[string]S3API
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// StoragePlugin implements the StoragePluginServiceServer interface for the
|
||||
// AWS storage service plugin.
|
||||
type StoragePlugin struct {
|
||||
@@ -42,6 +51,112 @@ type StoragePlugin struct {
|
||||
|
||||
// testStorageStateOpts are passed in to the stored state to control test behavior
|
||||
testStorageStateOpts []awsStoragePersistedStateOption
|
||||
|
||||
clients *clientCache
|
||||
}
|
||||
|
||||
// New creates a new StoragePlugin
|
||||
func New() *StoragePlugin {
|
||||
c := &clientCache{
|
||||
cache: make(map[string]S3API),
|
||||
}
|
||||
return &StoragePlugin{
|
||||
clients: c,
|
||||
}
|
||||
}
|
||||
|
||||
// getClient returns an S3API client for the given storage bucket id.
|
||||
func (p *StoragePlugin) getClient(ctx context.Context,
|
||||
storageBucketId string,
|
||||
storageState *awsStoragePersistedState,
|
||||
opt ...s3Option) (S3API, error) {
|
||||
if storageBucketId == "" {
|
||||
// No storage bucket ID to key cache on, create new client and return
|
||||
client, err := storageState.s3Client(ctx, opt...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatus("error creating S3 client: %s", err)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing options in cache: %w", err)
|
||||
}
|
||||
|
||||
if opts.withCacheRefresh {
|
||||
// Forced cache refresh
|
||||
p.clients.Lock()
|
||||
defer p.clients.Unlock()
|
||||
client, err := storageState.s3Client(ctx, opt...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatus("error creating S3 client: %s", err)
|
||||
}
|
||||
p.clients.cache[storageBucketId] = client
|
||||
return client, nil
|
||||
}
|
||||
|
||||
p.clients.RLock()
|
||||
client, ok := p.clients.cache[storageBucketId]
|
||||
p.clients.RUnlock()
|
||||
|
||||
if !ok || client.Credentials().Expired() {
|
||||
// We got a cache miss or the credentials has expired, time to refresh
|
||||
p.clients.Lock()
|
||||
defer p.clients.Unlock()
|
||||
|
||||
// Check cache again in case another caller updated it since we got the lock
|
||||
client, ok = p.clients.cache[storageBucketId]
|
||||
if ok && !client.Credentials().Expired() {
|
||||
// Got a cache hit with valid credentials, return cached client
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// Create new client and cache it
|
||||
var err error
|
||||
client, err = storageState.s3Client(ctx, opt...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatus("error creating S3 client: %s", err)
|
||||
}
|
||||
p.clients.cache[storageBucketId] = client
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
type s3Caller func(client S3API) (any, error)
|
||||
|
||||
// call gets the s3client and passes it to the s3caller function, if a permission error is returned
|
||||
// it forces a cache refresh and tries again.
|
||||
func (p *StoragePlugin) call(
|
||||
ctx context.Context,
|
||||
fn s3Caller,
|
||||
storageBucketId string,
|
||||
storageState *awsStoragePersistedState,
|
||||
opts ...s3Option) (any, error) {
|
||||
|
||||
s3Client, err := p.getClient(ctx, storageBucketId, storageState, opts...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatus("error getting S3 client: %s", err)
|
||||
}
|
||||
firstAttempt := true
|
||||
for {
|
||||
resp, err := fn(s3Client)
|
||||
if err != nil {
|
||||
st, _ := errors.ParseAWSError("", err)
|
||||
if st.Code() == codes.PermissionDenied && firstAttempt {
|
||||
// We got a permission error, this is the first time so refresh cache and try again
|
||||
firstAttempt = false
|
||||
opts = append(opts, WithCacheRefresh(true))
|
||||
s3Client, err = p.getClient(ctx, storageBucketId, storageState, opts...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatus("error refreshing S3 client: %s", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
// OnCreateStorageBucket is called when a storage bucket is created.
|
||||
@@ -346,19 +461,20 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques
|
||||
if storageAttributes.EndpointUrl != "" {
|
||||
opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl))
|
||||
}
|
||||
s3Client, err := storageState.S3Client(ctx, opts...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatus("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, parseS3Error(err, "failed to head object", req).Err()
|
||||
headCall := func(s3Client S3API) (any, error) {
|
||||
return s3Client.HeadObject(ctx, &s3.HeadObjectInput{
|
||||
Bucket: aws.String(bucket.GetBucketName()),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
}
|
||||
headResp, err := p.call(ctx, headCall, bucket.GetId(), storageState, opts...)
|
||||
if err != nil {
|
||||
return nil, parseS3Error("head object", err, req).Err()
|
||||
}
|
||||
resp := headResp.(*s3.HeadObjectOutput)
|
||||
|
||||
return &pb.HeadObjectResponse{
|
||||
ContentLength: resp.ContentLength,
|
||||
LastModified: timestamppb.New(*resp.LastModified),
|
||||
@@ -472,19 +588,19 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu
|
||||
if storageAttributes.EndpointUrl != "" {
|
||||
opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl))
|
||||
}
|
||||
s3Client, err := storageState.S3Client(stream.Context(), opts...)
|
||||
if err != nil {
|
||||
return errors.BadRequestStatus("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 parseS3Error(err, "failed to get object", req).Err()
|
||||
getCall := func(s3Client S3API) (any, error) {
|
||||
return s3Client.GetObject(stream.Context(), &s3.GetObjectInput{
|
||||
Bucket: aws.String(bucket.GetBucketName()),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
}
|
||||
getResp, err := p.call(stream.Context(), getCall, bucket.GetId(), storageState, opts...)
|
||||
if err != nil {
|
||||
return parseS3Error("get object", err, req).Err()
|
||||
}
|
||||
resp := getResp.(*s3.GetObjectOutput)
|
||||
|
||||
defer resp.Body.Close()
|
||||
reader := bufio.NewReader(resp.Body)
|
||||
@@ -583,10 +699,6 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
|
||||
if storageAttributes.EndpointUrl != "" {
|
||||
opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl))
|
||||
}
|
||||
s3Client, err := storageState.S3Client(ctx, opts...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatus("error getting S3 client: %s", err)
|
||||
}
|
||||
|
||||
hash := sha256.New()
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
@@ -598,18 +710,23 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
|
||||
if err != nil {
|
||||
return nil, errors.UnknownStatus("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, parseS3Error(err, "failed to put object", req).Err()
|
||||
|
||||
putCall := func(s3Client S3API) (any, error) {
|
||||
return s3Client.PutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucket.GetBucketName()),
|
||||
Key: aws.String(objectKey),
|
||||
Body: file,
|
||||
ChecksumAlgorithm: types.ChecksumAlgorithmSha256,
|
||||
ChecksumSHA256: aws.String(checksum),
|
||||
})
|
||||
}
|
||||
putResp, err := p.call(ctx, putCall, bucket.GetId(), storageState, opts...)
|
||||
if err != nil {
|
||||
return nil, parseS3Error("put object", err, req).Err()
|
||||
}
|
||||
resp := putResp.(*s3.PutObjectOutput)
|
||||
|
||||
if resp.ChecksumSHA256 == nil {
|
||||
return nil, errors.UnknownStatus("missing checksum response from aws")
|
||||
}
|
||||
@@ -678,10 +795,6 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects
|
||||
if storageAttributes.EndpointUrl != "" {
|
||||
opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl))
|
||||
}
|
||||
client, err := storageState.S3Client(ctx, opts...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatus("error getting S3 client: %s", err)
|
||||
}
|
||||
|
||||
prefix := path.Join(bucket.GetBucketPrefix(), req.GetKeyPrefix())
|
||||
if strings.HasSuffix(req.GetKeyPrefix(), "/") {
|
||||
@@ -691,12 +804,19 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects
|
||||
}
|
||||
|
||||
if !req.Recursive {
|
||||
_, err := client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucket.GetBucketName()),
|
||||
Key: aws.String(prefix),
|
||||
})
|
||||
deleteCall := func(s3Client S3API) (any, error) {
|
||||
return s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucket.GetBucketName()),
|
||||
Key: aws.String(prefix),
|
||||
})
|
||||
}
|
||||
_, err := p.call(ctx, deleteCall, bucket.GetId(), storageState, opts...)
|
||||
if err != nil {
|
||||
return nil, parseS3Error(err, "failed to delete object", req).Err()
|
||||
return nil, parseS3Error("delete object", err, req).Err()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, parseS3Error("delete object", err, req).Err()
|
||||
}
|
||||
return &pb.DeleteObjectsResponse{
|
||||
ObjectsDeleted: uint32(1),
|
||||
@@ -704,10 +824,13 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects
|
||||
}
|
||||
|
||||
const maxkeys = 1000
|
||||
|
||||
objects := []types.ObjectIdentifier{}
|
||||
client, err := p.getClient(ctx, bucket.GetId(), storageState, opts...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatus("error getting S3 client: %s", err)
|
||||
}
|
||||
var conToken *string
|
||||
truncated := true
|
||||
truncated, firstAttempt := true, true
|
||||
for truncated {
|
||||
res, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(bucket.GetBucketName()),
|
||||
@@ -716,7 +839,19 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects
|
||||
ContinuationToken: conToken,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, parseS3Error(err, "failed to list objects", req).Err()
|
||||
st, _ := errors.ParseAWSError("", err)
|
||||
if st.Code() == codes.PermissionDenied && firstAttempt {
|
||||
// This is first attempt and we get a permission error, refresh cache and try again
|
||||
firstAttempt = false
|
||||
opts = append(opts, WithCacheRefresh(true))
|
||||
client, err = p.getClient(ctx, bucket.GetId(), storageState, opts...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatus("error refreshing S3 client: %s", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, parseS3Error("list objects", err, req).Err()
|
||||
}
|
||||
truncated = res.IsTruncated
|
||||
conToken = res.NextContinuationToken
|
||||
@@ -738,7 +873,7 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, parseS3Error(err, "failed to delete objects", req).Err()
|
||||
return nil, parseS3Error("delete objects", err, req).Err()
|
||||
}
|
||||
deleted += len(res.Deleted)
|
||||
}
|
||||
@@ -770,7 +905,7 @@ func dryRunValidation(ctx context.Context, state *awsStoragePersistedState, attr
|
||||
opts = append(opts, WithEndpoint(attrs.EndpointUrl))
|
||||
}
|
||||
|
||||
client, err := state.S3Client(ctx, opts...)
|
||||
client, err := state.s3Client(ctx, opts...)
|
||||
if err != nil {
|
||||
return status.New(codes.InvalidArgument, fmt.Sprintf("error getting S3 client: %s", err))
|
||||
}
|
||||
@@ -786,7 +921,7 @@ func dryRunValidation(ctx context.Context, state *awsStoragePersistedState, attr
|
||||
Body: bytes.NewReader([]byte("hashicorp boundary aws plugin access test")),
|
||||
}); err != nil {
|
||||
var st *status.Status
|
||||
st, permissions.Write = errors.ParseAWSError(err, "failed to put object")
|
||||
st, permissions.Write = errors.ParseAWSError("put object", err)
|
||||
errs = multierror.Append(errs, st.Err())
|
||||
} else {
|
||||
permissions.Write = &pb.Permission{State: pb.StateType_STATE_TYPE_OK, CheckedAt: timestamppb.Now()}
|
||||
@@ -797,7 +932,7 @@ func dryRunValidation(ctx context.Context, state *awsStoragePersistedState, attr
|
||||
Key: aws.String(objectKey),
|
||||
}); err != nil {
|
||||
var st *status.Status
|
||||
st, permissions.Read = errors.ParseAWSError(err, "failed to get object")
|
||||
st, permissions.Read = errors.ParseAWSError("get object", err)
|
||||
errs = multierror.Append(errs, st.Err())
|
||||
} else {
|
||||
permissions.Read = &pb.Permission{State: pb.StateType_STATE_TYPE_OK, CheckedAt: timestamppb.Now()}
|
||||
@@ -808,7 +943,7 @@ func dryRunValidation(ctx context.Context, state *awsStoragePersistedState, attr
|
||||
Key: aws.String(objectKey),
|
||||
}); err != nil {
|
||||
var st *status.Status
|
||||
st, permissions.Read = errors.ParseAWSError(err, "failed to head object")
|
||||
st, permissions.Read = errors.ParseAWSError("head object", err)
|
||||
errs = multierror.Append(errs, st.Err())
|
||||
}
|
||||
|
||||
@@ -817,7 +952,7 @@ func dryRunValidation(ctx context.Context, state *awsStoragePersistedState, attr
|
||||
Prefix: aws.String(objectKey),
|
||||
}); err != nil {
|
||||
var st *status.Status
|
||||
st, permissions.Read = errors.ParseAWSError(err, "failed to list object")
|
||||
st, permissions.Read = errors.ParseAWSError("list object", err)
|
||||
errs = multierror.Append(errs, st.Err())
|
||||
} else if res == nil || len(res.Contents) != 1 || *res.Contents[0].Key != objectKey {
|
||||
permissions.Read = &pb.Permission{State: pb.StateType_STATE_TYPE_UNKNOWN, CheckedAt: timestamppb.Now()}
|
||||
@@ -829,7 +964,7 @@ func dryRunValidation(ctx context.Context, state *awsStoragePersistedState, attr
|
||||
Key: aws.String(objectKey),
|
||||
}); err != nil {
|
||||
var st *status.Status
|
||||
st, permissions.Delete = errors.ParseAWSError(err, "failed to delete object")
|
||||
st, permissions.Delete = errors.ParseAWSError("delete object", err)
|
||||
errs = multierror.Append(errs, st.Err())
|
||||
} else {
|
||||
permissions.Delete = &pb.Permission{State: pb.StateType_STATE_TYPE_OK, CheckedAt: timestamppb.Now()}
|
||||
|
||||
@@ -153,7 +153,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
),
|
||||
}),
|
||||
},
|
||||
expectedErrContains: "aws service unknown: unknown error: error rotating credentials",
|
||||
expectedErrContains: "aws service unknown: unknown error: rotating credentials:",
|
||||
expectedErrCode: codes.Unknown,
|
||||
expectedDetails: nil,
|
||||
},
|
||||
@@ -178,7 +178,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
}),
|
||||
)),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to put object",
|
||||
expectedErrContains: "aws service s3: invalid credentials: put object:",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -211,7 +211,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
}),
|
||||
)),
|
||||
},
|
||||
expectedErrContains: "aws service s3: throttling error: failed to put object",
|
||||
expectedErrContains: "aws service s3: throttling: put object:",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -245,7 +245,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to get object",
|
||||
expectedErrContains: "aws service s3: invalid credentials: get object:",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -280,7 +280,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: throttling error: failed to get object",
|
||||
expectedErrContains: "aws service s3: throttling: get object:",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -314,7 +314,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to head object",
|
||||
expectedErrContains: "aws service s3: invalid credentials: head object:",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -349,7 +349,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: throttling error: failed to head object",
|
||||
expectedErrContains: "aws service s3: throttling: head object:",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -384,7 +384,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to delete object",
|
||||
expectedErrContains: "aws service s3: invalid credentials: delete object:",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -857,7 +857,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
),
|
||||
}),
|
||||
},
|
||||
expectedErrContains: "aws service unknown: unknown error: failed to delete access key",
|
||||
expectedErrContains: "aws service unknown: unknown error: deleting credentials:",
|
||||
expectedErrCode: codes.Unknown,
|
||||
},
|
||||
{
|
||||
@@ -896,7 +896,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
),
|
||||
}),
|
||||
},
|
||||
expectedErrContains: "aws service unknown: unknown error: error rotating credentials",
|
||||
expectedErrContains: "aws service unknown: unknown error: rotating credentials",
|
||||
expectedErrCode: codes.Unknown,
|
||||
},
|
||||
{
|
||||
@@ -920,7 +920,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
testMockS3WithPutObjectError(TestAwsS3Error("AccessDenied", "PutObject", "not authorized")),
|
||||
)),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to put object",
|
||||
expectedErrContains: "aws service s3: invalid credentials: put object:",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -953,7 +953,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
testMockS3WithPutObjectError(TestAwsS3Error("ThrottlingException", "PutObject", "throttling exception")),
|
||||
)),
|
||||
},
|
||||
expectedErrContains: "aws service s3: throttling error: failed to put object",
|
||||
expectedErrContains: "aws service s3: throttling: put object",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -987,7 +987,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to get object",
|
||||
expectedErrContains: "aws service s3: invalid credentials: get object",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -1022,7 +1022,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: throttling error: failed to get object",
|
||||
expectedErrContains: "aws service s3: throttling: get object",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -1056,7 +1056,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to head object",
|
||||
expectedErrContains: "aws service s3: invalid credentials: head object",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -1091,7 +1091,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: throttling error: failed to head object",
|
||||
expectedErrContains: "aws service s3: throttling: head object",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -1255,7 +1255,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to head object",
|
||||
expectedErrContains: "aws service s3: invalid credentials: head object",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -1363,7 +1363,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to put object",
|
||||
expectedErrContains: "aws service s3: invalid credentials: put object",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -1486,7 +1486,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
),
|
||||
}),
|
||||
},
|
||||
expectedErrContains: "aws service unknown: unknown error: failed to delete access key",
|
||||
expectedErrContains: "aws service unknown: unknown error: deleting credentials",
|
||||
expectedErrCode: codes.Unknown,
|
||||
},
|
||||
{
|
||||
@@ -1599,7 +1599,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to put object",
|
||||
expectedErrContains: "aws service s3: invalid credentials: put object",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -1738,7 +1738,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
),
|
||||
}),
|
||||
},
|
||||
expectedErrContains: "aws service unknown: unknown error: error rotating credentials",
|
||||
expectedErrContains: "aws service unknown: unknown error: rotating credentials",
|
||||
expectedErrCode: codes.Unknown,
|
||||
},
|
||||
{
|
||||
@@ -1973,7 +1973,7 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) {
|
||||
),
|
||||
}),
|
||||
},
|
||||
expectedErrContains: "aws service unknown: unknown error: failed to delete access key",
|
||||
expectedErrContains: "aws service unknown: unknown error: deleting credentials",
|
||||
expectedErrCode: codes.Unknown,
|
||||
},
|
||||
{
|
||||
@@ -2172,7 +2172,7 @@ func TestStoragePlugin_HeadObject(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to head object",
|
||||
expectedErrContains: "aws service s3: invalid credentials: head object",
|
||||
expectedErrCode: codes.PermissionDenied,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -2196,7 +2196,7 @@ func TestStoragePlugin_HeadObject(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: throttling error: failed to head object",
|
||||
expectedErrContains: "aws service s3: throttling: head object",
|
||||
expectedErrCode: codes.Unavailable,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -2219,7 +2219,7 @@ func TestStoragePlugin_HeadObject(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws s3 error: failed to head object",
|
||||
expectedErrContains: "aws service s3: head object",
|
||||
expectedErrCode: codes.NotFound,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -2425,7 +2425,7 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) {
|
||||
}),
|
||||
)),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to put object",
|
||||
expectedErrContains: "aws service s3: invalid credentials: put object",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -2458,7 +2458,7 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) {
|
||||
}),
|
||||
)),
|
||||
},
|
||||
expectedErrContains: "aws service s3: throttling error: failed to put object",
|
||||
expectedErrContains: "aws service s3: throttling: put object",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -2492,7 +2492,7 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to get object",
|
||||
expectedErrContains: "aws service s3: invalid credentials: get object",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -2527,7 +2527,7 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: throttling error: failed to get object",
|
||||
expectedErrContains: "aws service s3: throttling: get object",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -2561,7 +2561,7 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to head object",
|
||||
expectedErrContains: "aws service s3: invalid credentials: head object",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -2596,7 +2596,7 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: throttling error: failed to head object",
|
||||
expectedErrContains: "aws service s3: throttling: head object",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -2683,7 +2683,7 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) {
|
||||
}),
|
||||
)),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to put object",
|
||||
expectedErrContains: "aws service s3: invalid credentials: put object",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -2851,7 +2851,7 @@ func TestStoragePlugin_GetObject(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to get object",
|
||||
expectedErrContains: "aws service s3: invalid credentials: get object",
|
||||
expectedErrCode: codes.PermissionDenied,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -2875,7 +2875,7 @@ func TestStoragePlugin_GetObject(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws s3 error: failed to get object",
|
||||
expectedErrContains: "aws service s3: get object",
|
||||
expectedErrCode: codes.NotFound,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -2898,7 +2898,7 @@ func TestStoragePlugin_GetObject(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws s3 error: failed to get object",
|
||||
expectedErrContains: "aws service s3: get object",
|
||||
expectedErrCode: codes.NotFound,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -2921,7 +2921,7 @@ func TestStoragePlugin_GetObject(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws s3 error: failed to get object",
|
||||
expectedErrContains: "aws service s3: get object",
|
||||
expectedErrCode: codes.NotFound,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -2945,7 +2945,7 @@ func TestStoragePlugin_GetObject(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: throttling error: failed to get object",
|
||||
expectedErrContains: "aws service s3: throttling: get object",
|
||||
expectedErrCode: codes.Unavailable,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -3300,7 +3300,7 @@ func TestStoragePlugin_PutObject(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to put object",
|
||||
expectedErrContains: "aws service s3: invalid credentials: put object",
|
||||
expectedErrCode: codes.PermissionDenied,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -3324,7 +3324,7 @@ func TestStoragePlugin_PutObject(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws s3 error: failed to put object",
|
||||
expectedErrContains: "aws service s3: put object",
|
||||
expectedErrCode: codes.NotFound,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -3348,7 +3348,7 @@ func TestStoragePlugin_PutObject(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: throttling error: failed to put object",
|
||||
expectedErrContains: "aws service s3: throttling: put object",
|
||||
expectedErrCode: codes.Unavailable,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -3640,7 +3640,7 @@ func TestStoragePlugin_DeleteObjects(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to delete object",
|
||||
expectedErrContains: "aws service s3: invalid credentials: delete object",
|
||||
expectedErrCode: codes.PermissionDenied,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -3664,7 +3664,7 @@ func TestStoragePlugin_DeleteObjects(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to list objects",
|
||||
expectedErrContains: "aws service s3: invalid credentials: list objects",
|
||||
expectedErrCode: codes.PermissionDenied,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -3696,7 +3696,7 @@ func TestStoragePlugin_DeleteObjects(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: invalid credentials error: failed to delete objects",
|
||||
expectedErrContains: "aws service s3: invalid credentials: delete objects",
|
||||
expectedErrCode: codes.PermissionDenied,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -3720,7 +3720,7 @@ func TestStoragePlugin_DeleteObjects(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: throttling error: failed to delete object",
|
||||
expectedErrContains: "aws service s3: throttling: delete object",
|
||||
expectedErrCode: codes.Unavailable,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -3743,7 +3743,7 @@ func TestStoragePlugin_DeleteObjects(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: throttling error: failed to list objects",
|
||||
expectedErrContains: "aws service s3: throttling: list objects",
|
||||
expectedErrCode: codes.Unavailable,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -3774,7 +3774,7 @@ func TestStoragePlugin_DeleteObjects(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedErrContains: "aws service s3: throttling error: failed to delete objects",
|
||||
expectedErrContains: "aws service s3: throttling: delete objects",
|
||||
expectedErrCode: codes.Unavailable,
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
@@ -3917,3 +3917,89 @@ func TestStoragePlugin_DeleteObjects(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3ClientCache(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
cred1, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithRegion("us-east-1"),
|
||||
awsutil.WithAccessKey("cred1"),
|
||||
awsutil.WithSecretKey("secret1"),
|
||||
)
|
||||
require.NoError(err)
|
||||
cred2, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithRegion("us-east-1"),
|
||||
awsutil.WithAccessKey("cred2"),
|
||||
awsutil.WithSecretKey("secret2"),
|
||||
)
|
||||
require.NoError(err)
|
||||
cred3, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithRegion("us-east-1"),
|
||||
awsutil.WithAccessKey("cred3"),
|
||||
awsutil.WithSecretKey("secret3"),
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
p := New()
|
||||
cred, err := credential.NewAwsCredentialPersistedState(
|
||||
credential.WithCredentialsConfig(cred1),
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
state, err := newAwsStoragePersistedState(withCredentials(cred))
|
||||
require.NoError(err)
|
||||
|
||||
// Cache should be empty before first call
|
||||
assert.Empty(p.clients.cache)
|
||||
|
||||
client, err := p.getClient(ctx, "id", state)
|
||||
require.NoError(err)
|
||||
require.NotNil(client)
|
||||
assert.Equal(cred1.AccessKey, client.Credentials().AccessKeyID)
|
||||
assert.Equal(cred1.SecretKey, client.Credentials().SecretAccessKey)
|
||||
|
||||
// Update state with new credentials
|
||||
cred, err = credential.NewAwsCredentialPersistedState(
|
||||
credential.WithCredentialsConfig(cred2),
|
||||
)
|
||||
require.NoError(err)
|
||||
state, err = newAwsStoragePersistedState(withCredentials(cred))
|
||||
require.NoError(err)
|
||||
|
||||
// Client is cached and creds valid so it should still grab initial client
|
||||
client, err = p.getClient(ctx, "id", state)
|
||||
require.NoError(err)
|
||||
require.NotNil(client)
|
||||
assert.Equal(cred1.AccessKey, client.Credentials().AccessKeyID)
|
||||
assert.Equal(cred1.SecretKey, client.Credentials().SecretAccessKey)
|
||||
|
||||
// Force credentials to expire
|
||||
c := client.(*s3Client)
|
||||
c.creds.CanExpire = true
|
||||
p.clients.Lock()
|
||||
p.clients.cache["id"] = c
|
||||
p.clients.Unlock()
|
||||
|
||||
// Getting client should now create new client on cred2 since the credentials have expired
|
||||
client, err = p.getClient(ctx, "id", state)
|
||||
require.NoError(err)
|
||||
require.NotNil(client)
|
||||
assert.Equal(cred2.AccessKey, client.Credentials().AccessKeyID)
|
||||
assert.Equal(cred2.SecretKey, client.Credentials().SecretAccessKey)
|
||||
|
||||
// Update state with new credentials again
|
||||
cred, err = credential.NewAwsCredentialPersistedState(
|
||||
credential.WithCredentialsConfig(cred3),
|
||||
)
|
||||
require.NoError(err)
|
||||
state, err = newAwsStoragePersistedState(withCredentials(cred))
|
||||
require.NoError(err)
|
||||
|
||||
// Getting client and force cache refresh should return cred3 based client
|
||||
client, err = p.getClient(ctx, "id", state, WithCacheRefresh(true))
|
||||
require.NoError(err)
|
||||
require.NotNil(client)
|
||||
assert.Equal(cred3.AccessKey, client.Credentials().AccessKeyID)
|
||||
assert.Equal(cred3.SecretKey, client.Credentials().SecretAccessKey)
|
||||
}
|
||||
@@ -27,6 +27,16 @@ type S3API interface {
|
||||
PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
|
||||
DeleteObjects(ctx context.Context, params *s3.DeleteObjectsInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectsOutput, error)
|
||||
ListObjectsV2(ctx context.Context, params *s3.ListObjectsV2Input, optFns ...func(*s3.Options)) (*s3.ListObjectsV2Output, error)
|
||||
Credentials() aws.Credentials
|
||||
}
|
||||
|
||||
type s3Client struct {
|
||||
*s3.Client
|
||||
creds aws.Credentials
|
||||
}
|
||||
|
||||
func (c *s3Client) Credentials() aws.Credentials {
|
||||
return c.creds
|
||||
}
|
||||
|
||||
var customClient = &http.Client{
|
||||
@@ -91,9 +101,9 @@ func (s *awsStoragePersistedState) toProto() (*storagebuckets.StorageBucketPersi
|
||||
return &storagebuckets.StorageBucketPersisted{Data: data}, nil
|
||||
}
|
||||
|
||||
// S3Client returns a configured S3 client based on the session
|
||||
// s3Client returns a configured S3 client based on the session
|
||||
// information stored in the state.
|
||||
func (s *awsStoragePersistedState) S3Client(ctx context.Context, opt ...s3Option) (S3API, error) {
|
||||
func (s *awsStoragePersistedState) s3Client(ctx context.Context, opt ...s3Option) (S3API, error) {
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing options when fetching S3 client: %w", err)
|
||||
@@ -114,7 +124,16 @@ func (s *awsStoragePersistedState) S3Client(ctx context.Context, opt ...s3Option
|
||||
if s.testS3APIFunc != nil {
|
||||
return s.testS3APIFunc(*awsCfg)
|
||||
}
|
||||
return s3.NewFromConfig(*awsCfg, s3Opts...), nil
|
||||
|
||||
// Retrieve the credentials provider from the client
|
||||
credsProvider := awsCfg.Credentials
|
||||
creds, err := credsProvider.Retrieve(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve credentials: %v\n", err)
|
||||
}
|
||||
|
||||
c := s3.NewFromConfig(*awsCfg, s3Opts...)
|
||||
return &s3Client{Client: c, creds: creds}, nil
|
||||
}
|
||||
|
||||
// getOpts iterates the inbound s3Options and returns a struct
|
||||
@@ -136,13 +155,22 @@ type s3Option func(*s3Options) error
|
||||
|
||||
// options = how options are represented
|
||||
type s3Options struct {
|
||||
withEndpoint string
|
||||
withEndpoint string
|
||||
withCacheRefresh bool
|
||||
}
|
||||
|
||||
func getDefaultS3Options() s3Options {
|
||||
return s3Options{}
|
||||
}
|
||||
|
||||
// WithCacheRefresh controls if the cache should be forced to refresh
|
||||
func WithCacheRefresh(refresh bool) s3Option {
|
||||
return func(o *s3Options) error {
|
||||
o.withCacheRefresh = refresh
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithEndpoint contains the endpoint to use
|
||||
func WithEndpoint(with string) s3Option {
|
||||
return func(o *s3Options) error {
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestAwsStoragePersistedStateS3Client(t *testing.T) {
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
clientRaw, err := state.S3Client(context.Background(), tc.s3Opts...)
|
||||
clientRaw, err := state.s3Client(context.Background(), tc.s3Opts...)
|
||||
require.NoError(err)
|
||||
require.NotNil(clientRaw)
|
||||
client, ok := clientRaw.(*testMockS3)
|
||||
|
||||
+112
-7
@@ -17,12 +17,16 @@ import (
|
||||
"github.com/hashicorp/boundary-plugin-aws/plugin/service/storage"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
// NOTE: this test does not vaildate against dynamic credentials because
|
||||
// this credential type can only be tested on an EC2 instance.
|
||||
func TestStoragePlugin(t *testing.T) {
|
||||
region := os.Getenv("AWS_REGION")
|
||||
if region == "" {
|
||||
@@ -75,6 +79,12 @@ func TestStoragePlugin(t *testing.T) {
|
||||
iamSecretAccessKeyMissingPutObject, err := tf.GetOutputString("iam_secret_access_key_missing_put_obj")
|
||||
require.NoError(err)
|
||||
|
||||
iamAccessKeyMissingDeleteObject, err := tf.GetOutputString("iam_access_key_missing_delete_obj")
|
||||
require.NoError(err)
|
||||
|
||||
iamSecretAccessKeyMissingDeleteObject, err := tf.GetOutputString("iam_secret_access_key_missing_delete_obj")
|
||||
require.NoError(err)
|
||||
|
||||
// Start the workflow now. Set up the storage bucket. Note that this
|
||||
// will cause the state to go out of drift above in the sense that
|
||||
// the access key ID/secret access key will no longer be valid. We
|
||||
@@ -121,7 +131,7 @@ func TestStoragePlugin(t *testing.T) {
|
||||
testPluginOnDeleteStorageBucket(ctx, t, p, bucketName, region, keyid, secret, true)
|
||||
|
||||
// ********************
|
||||
// * Object Methods: PutObject, GetObject, HeadObject
|
||||
// * Object Methods: PutObject, GetObject, HeadObject, DeleteObjects
|
||||
// ********************
|
||||
//
|
||||
// Reassign the keyid and secret first.
|
||||
@@ -132,11 +142,59 @@ func TestStoragePlugin(t *testing.T) {
|
||||
// * Validate Permissions
|
||||
// ********************
|
||||
//
|
||||
var expectedSBCState *pb.StorageBucketCredentialState
|
||||
|
||||
// Validate error is returned for missing get object permission
|
||||
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingGetObject, iamSecretAccessKeyMissingGetObject)
|
||||
expectedSBCState = &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Write: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_OK,
|
||||
},
|
||||
Read: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
},
|
||||
Delete: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_OK,
|
||||
},
|
||||
},
|
||||
}
|
||||
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingGetObject, iamSecretAccessKeyMissingGetObject, expectedSBCState)
|
||||
|
||||
// Validate error is returned for missing put object permission
|
||||
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingPutObject, iamSecretAccessKeyMissingPutObject)
|
||||
expectedSBCState = &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Write: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
},
|
||||
Read: &pb.Permission{
|
||||
// Since we cannot write an object to S3,
|
||||
// we cannot test against the ability to read from S3.
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
Delete: &pb.Permission{
|
||||
// We can still try deleting an object that does
|
||||
// not exist in the S3 bucket to validate access.
|
||||
State: pb.StateType_STATE_TYPE_OK,
|
||||
},
|
||||
},
|
||||
}
|
||||
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingPutObject, iamSecretAccessKeyMissingPutObject, expectedSBCState)
|
||||
|
||||
// Validate error is returned for missing delete object permission
|
||||
expectedSBCState = &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Write: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_OK,
|
||||
},
|
||||
Read: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_OK,
|
||||
},
|
||||
Delete: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
},
|
||||
},
|
||||
}
|
||||
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingDeleteObject, iamSecretAccessKeyMissingDeleteObject, expectedSBCState)
|
||||
}
|
||||
|
||||
func testPluginOnCreateStorageBucket(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string, rotate bool) (string, string) {
|
||||
@@ -289,10 +347,12 @@ func testPluginOnDeleteStorageBucket(ctx context.Context, t *testing.T, p *stora
|
||||
}
|
||||
}
|
||||
|
||||
func testPluginValidatePermissions(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string) {
|
||||
func testPluginValidatePermissions(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string, expectedState *pb.StorageBucketCredentialState) {
|
||||
t.Helper()
|
||||
t.Logf("testing ValidatePermissions")
|
||||
require := require.New(t)
|
||||
require, assert := require.New(t), assert.New(t)
|
||||
|
||||
require.NotNil(expectedState)
|
||||
|
||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
@@ -315,6 +375,51 @@ func testPluginValidatePermissions(ctx context.Context, t *testing.T, p *storage
|
||||
|
||||
_, err = p.ValidatePermissions(ctx, req)
|
||||
require.Error(err)
|
||||
|
||||
var actualSBCState *pb.StorageBucketCredentialState
|
||||
if st, ok := status.FromError(err); ok {
|
||||
for _, detail := range st.Details() {
|
||||
if statusDetail, ok := detail.(*pb.StorageBucketCredentialState); ok {
|
||||
actualSBCState = statusDetail
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
require.NotNil(actualSBCState)
|
||||
require.NotNil(actualSBCState.GetState())
|
||||
|
||||
expectedReadState := expectedState.GetState().GetRead()
|
||||
if expectedReadState != nil {
|
||||
actualReadState := actualSBCState.GetState().GetRead()
|
||||
require.NotNil(actualReadState)
|
||||
assert.Equal(expectedReadState.GetState(), actualReadState.GetState())
|
||||
assert.NotEmpty(actualReadState.GetCheckedAt())
|
||||
if expectedReadState.GetState() == pb.StateType_STATE_TYPE_ERROR {
|
||||
assert.NotEmpty(actualReadState.GetErrorDetails())
|
||||
}
|
||||
}
|
||||
|
||||
expectedWriteState := expectedState.GetState().GetWrite()
|
||||
if expectedWriteState != nil {
|
||||
actualWriteState := actualSBCState.GetState().GetWrite()
|
||||
require.NotNil(actualWriteState)
|
||||
assert.Equal(expectedWriteState.GetState(), actualWriteState.GetState())
|
||||
assert.NotEmpty(actualWriteState.GetCheckedAt())
|
||||
if expectedWriteState.GetState() == pb.StateType_STATE_TYPE_ERROR {
|
||||
assert.NotEmpty(actualWriteState.GetErrorDetails())
|
||||
}
|
||||
}
|
||||
|
||||
expectedDeleteState := expectedState.GetState().GetDelete()
|
||||
if expectedDeleteState != nil {
|
||||
actualDeleteState := actualSBCState.GetState().GetDelete()
|
||||
require.NotNil(actualDeleteState)
|
||||
assert.Equal(expectedDeleteState.GetState(), actualDeleteState.GetState())
|
||||
assert.NotEmpty(actualDeleteState.GetCheckedAt())
|
||||
if expectedDeleteState.GetState() == pb.StateType_STATE_TYPE_ERROR {
|
||||
assert.NotEmpty(actualDeleteState.GetErrorDetails())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testPluginObjectMethods(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string) {
|
||||
@@ -369,7 +474,7 @@ func testPluginObjectMethods(ctx context.Context, t *testing.T, p *storage.Stora
|
||||
|
||||
// Need to create a GRPC server to test GetObject
|
||||
t.Logf("testing GetObject")
|
||||
lis, err := net.Listen("tcp", "localhost:2030")
|
||||
lis, err := net.Listen("tcp", "[::1]:2030")
|
||||
require.NoError(err)
|
||||
grpcServer := grpc.NewServer()
|
||||
pb.RegisterStoragePluginServiceServer(grpcServer, &storage.StoragePlugin{})
|
||||
@@ -377,7 +482,7 @@ func testPluginObjectMethods(ctx context.Context, t *testing.T, p *storage.Stora
|
||||
grpcServer.Serve(lis)
|
||||
}()
|
||||
|
||||
conn, err := grpc.Dial("localhost:2030", grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
conn, err := grpc.Dial("[::1]:2030", grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
require.NoError(err)
|
||||
defer conn.Close()
|
||||
client := pb.NewStoragePluginServiceClient(conn)
|
||||
|
||||
Vendored
+97
-12
@@ -32,7 +32,9 @@ resource "aws_iam_policy" "valid" {
|
||||
"Action": [
|
||||
"s3:PutObject",
|
||||
"s3:GetObject",
|
||||
"s3:GetObjectAttributes"
|
||||
"s3:DeleteObject",
|
||||
"s3:GetObjectAttributes",
|
||||
"s3:ListBucket"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_s3_bucket.test.arn}*"
|
||||
@@ -79,7 +81,9 @@ resource "aws_iam_policy" "missing_put_obj" {
|
||||
{
|
||||
"Action": [
|
||||
"s3:GetObject",
|
||||
"s3:GetObjectAttributes"
|
||||
"s3:GetObjectAttributes",
|
||||
"s3:ListBucket",
|
||||
"s3:DeleteObject"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_s3_bucket.test.arn}*"
|
||||
@@ -125,7 +129,7 @@ resource "aws_iam_policy" "missing_get_obj" {
|
||||
{
|
||||
"Action": [
|
||||
"s3:PutObject",
|
||||
"s3:GetObjectAttributes"
|
||||
"s3:DeleteObject"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_s3_bucket.test.arn}*"
|
||||
@@ -141,18 +145,69 @@ resource "aws_iam_role_policy_attachment" "missing_get_obj" {
|
||||
policy_arn = aws_iam_policy.missing_get_obj.arn
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "missing_delete_obj" {
|
||||
name = "${random_id.prefix.dec}-missing-delete-obj"
|
||||
tags = local.tags
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Action = "sts:AssumeRole"
|
||||
Effect = "Allow"
|
||||
Sid = ""
|
||||
Principal = {
|
||||
AWS: data.aws_caller_identity.current.user_id
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_policy" "missing_delete_obj" {
|
||||
name = "${random_id.prefix.dec}-missing-delete-obj"
|
||||
tags = local.tags
|
||||
|
||||
policy = <<EOT
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"s3:PutObject",
|
||||
"s3:GetObject",
|
||||
"s3:GetObjectAttributes",
|
||||
"s3:ListBucket"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_s3_bucket.test.arn}*"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
EOT
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "missing_delete_obj" {
|
||||
role = aws_iam_role.missing_delete_obj.name
|
||||
policy_arn = aws_iam_policy.missing_delete_obj.arn
|
||||
}
|
||||
|
||||
# Static Credential Testing
|
||||
resource "random_id" "user_name" {
|
||||
count = var.iam_user_count
|
||||
prefix = random_id.prefix.dec
|
||||
prefix = "demo-${local.hashicorp_email}-boundary-iam-user"
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
resource "aws_iam_user" "test" {
|
||||
count = var.iam_user_count
|
||||
name = random_id.user_name[count.index].dec
|
||||
force_destroy = true
|
||||
tags = local.tags
|
||||
count = var.iam_user_count
|
||||
name = random_id.user_name[count.index].dec
|
||||
force_destroy = true
|
||||
permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/DemoUser"
|
||||
tags = {
|
||||
"boundary-demo" = local.hashicorp_email
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_access_key" "test" {
|
||||
@@ -195,17 +250,44 @@ resource "aws_iam_user_policy_attachment" "test_credentials" {
|
||||
policy_arn = aws_iam_policy.credentials[count.index].arn
|
||||
}
|
||||
|
||||
# Edge case for missing DeleteObject permission
|
||||
resource "random_id" "missing_delete_obj" {
|
||||
prefix = "demo-${local.hashicorp_email}-boundary-iam-user"
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
resource "aws_iam_user" "missing_delete_obj" {
|
||||
name = random_id.missing_delete_obj.dec
|
||||
force_destroy = true
|
||||
permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/DemoUser"
|
||||
tags = {
|
||||
"boundary-demo" = local.hashicorp_email
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_access_key" "missing_delete_obj" {
|
||||
user = aws_iam_user.missing_delete_obj.name
|
||||
}
|
||||
|
||||
resource "aws_iam_user_policy_attachment" "missing_delete_obj" {
|
||||
user = aws_iam_user.missing_delete_obj.name
|
||||
policy_arn = aws_iam_policy.missing_delete_obj.arn
|
||||
}
|
||||
|
||||
# Edge case for missing GetObject permission
|
||||
|
||||
resource "random_id" "missing_get_obj" {
|
||||
prefix = random_id.prefix.dec
|
||||
prefix = "demo-${local.hashicorp_email}-boundary-iam-user"
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
resource "aws_iam_user" "missing_get_obj" {
|
||||
name = random_id.missing_get_obj.dec
|
||||
force_destroy = true
|
||||
tags = local.tags
|
||||
permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/DemoUser"
|
||||
tags = {
|
||||
"boundary-demo" = local.hashicorp_email
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_access_key" "missing_get_obj" {
|
||||
@@ -220,14 +302,17 @@ resource "aws_iam_user_policy_attachment" "missing_get_obj" {
|
||||
# Edge case for missing PutObject permission
|
||||
|
||||
resource "random_id" "missing_put_obj" {
|
||||
prefix = random_id.prefix.dec
|
||||
prefix = "demo-${local.hashicorp_email}-boundary-iam-user"
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
resource "aws_iam_user" "missing_put_obj" {
|
||||
name = random_id.missing_put_obj.dec
|
||||
force_destroy = true
|
||||
tags = local.tags
|
||||
permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/DemoUser"
|
||||
tags = {
|
||||
"boundary-demo" = local.hashicorp_email
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_access_key" "missing_put_obj" {
|
||||
|
||||
Vendored
+15
-1
@@ -13,10 +13,14 @@ output "missing_put_obj_policy_arn" {
|
||||
value = aws_iam_role.missing_put_obj.arn
|
||||
}
|
||||
|
||||
output "missing_put_get_policy_arn" {
|
||||
output "missing_get_obj_policy_arn" {
|
||||
value = aws_iam_role.missing_get_obj.arn
|
||||
}
|
||||
|
||||
output "missing_delete_obj_policy_arn" {
|
||||
value = aws_iam_role.missing_delete_obj.arn
|
||||
}
|
||||
|
||||
output "iam_user_names" {
|
||||
value = aws_iam_user.test.*.name
|
||||
}
|
||||
@@ -53,4 +57,14 @@ output "iam_access_key_missing_get_obj" {
|
||||
output "iam_secret_access_key_missing_get_obj" {
|
||||
value = aws_iam_access_key.missing_get_obj.secret
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "iam_access_key_missing_delete_obj" {
|
||||
value = aws_iam_access_key.missing_delete_obj.id
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "iam_secret_access_key_missing_delete_obj" {
|
||||
value = aws_iam_access_key.missing_delete_obj.secret
|
||||
sensitive = true
|
||||
}
|
||||
Vendored
+9
-1
@@ -4,6 +4,14 @@
|
||||
resource "aws_s3_bucket" "test" {
|
||||
bucket = random_id.prefix.dec
|
||||
force_destroy = true
|
||||
acl = "private"
|
||||
tags = local.tags
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_public_access_block" "secure_access" {
|
||||
bucket = aws_s3_bucket.test.id
|
||||
|
||||
block_public_acls = true
|
||||
block_public_policy = true
|
||||
ignore_public_acls = true
|
||||
restrict_public_buckets = true
|
||||
}
|
||||
Vendored
+6
-4
@@ -4,7 +4,7 @@
|
||||
data "aws_caller_identity" "current" {}
|
||||
|
||||
resource "random_id" "prefix" {
|
||||
prefix = "storage-e2e-test"
|
||||
prefix = "aws-plugin-e2e-test"
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
@@ -13,8 +13,10 @@ variable "iam_user_count" {
|
||||
}
|
||||
|
||||
locals {
|
||||
hashicorp_email = split(":", data.aws_caller_identity.current.user_id)[1]
|
||||
tags = {
|
||||
user_id = split(":", data.aws_caller_identity.current.user_id)[1]
|
||||
repo = "boundary-plugin-aws"
|
||||
User = split(":", data.aws_caller_identity.current.user_id)[1]
|
||||
Repo = "boundary-plugin-aws"
|
||||
Environment = "Demo"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user