diff --git a/internal/plugin/loopback/options.go b/internal/plugin/loopback/options.go index 9c345a02f..7c1ec2bfb 100644 --- a/internal/plugin/loopback/options.go +++ b/internal/plugin/loopback/options.go @@ -47,12 +47,13 @@ const ( // PluginMockError is used to mock an error when interacting with an external object store. type PluginMockError struct { - BucketName string - BucketPrefix string - ObjectKey string - ErrMsg string - ErrCode codes.Code - ErrMethod Method + BucketName string + BucketPrefix string + ObjectKey string + ErrMsg string + ErrCode codes.Code + ErrMethod Method + StorageBucketCredentialState *plgpb.StorageBucketCredentialState } // match compares the given values from the parameters to the values provided in the mocked error. diff --git a/internal/plugin/loopback/storage.go b/internal/plugin/loopback/storage.go index 53818d83b..3ac1b6aa2 100644 --- a/internal/plugin/loopback/storage.go +++ b/internal/plugin/loopback/storage.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "crypto/sha256" + "fmt" "io" "os" "path" @@ -225,8 +226,12 @@ func (l *LoopbackStorage) validatePermissions(ctx context.Context, req *plgpb.Va if _, ok := l.buckets[BucketName(req.GetBucket().GetBucketName())]; !ok { return nil, status.Errorf(codes.NotFound, "%s: bucket not found", op) } + for _, err := range l.errs { if err.match(req.GetBucket(), "", ValidatePermissions) { + if err.StorageBucketCredentialState != nil { + return nil, createErrorWithBucketCredentialState(err.ErrCode, fmt.Sprintf("%s: %s", op, err.ErrMsg), err.StorageBucketCredentialState) + } return nil, status.Errorf(err.ErrCode, "%s: %s", op, err.ErrMsg) } } @@ -260,6 +265,9 @@ func (l *LoopbackStorage) headObject(ctx context.Context, req *plgpb.HeadObjectR } for _, err := range l.errs { if err.match(req.GetBucket(), req.GetKey(), HeadObject) { + if err.StorageBucketCredentialState != nil { + return nil, createErrorWithBucketCredentialState(err.ErrCode, fmt.Sprintf("%s: %s", op, err.ErrMsg), err.StorageBucketCredentialState) + } return nil, status.Errorf(err.ErrCode, "%s: %s", op, err.ErrMsg) } } @@ -300,6 +308,9 @@ func (l *LoopbackStorage) getObject(req *plgpb.GetObjectRequest, stream plgpb.St } for _, err := range l.errs { if err.match(req.GetBucket(), req.GetKey(), GetObject) { + if err.StorageBucketCredentialState != nil { + return createErrorWithBucketCredentialState(err.ErrCode, fmt.Sprintf("%s: %s", op, err.ErrMsg), err.StorageBucketCredentialState) + } return status.Errorf(err.ErrCode, "%s: %s", op, err.ErrMsg) } } @@ -368,6 +379,9 @@ func (l *LoopbackStorage) putObject(ctx context.Context, req *plgpb.PutObjectReq // return an expected mock error if one was provided for _, err := range l.errs { if err.match(req.GetBucket(), req.GetKey(), PutObject) { + if err.StorageBucketCredentialState != nil { + return nil, createErrorWithBucketCredentialState(err.ErrCode, fmt.Sprintf("%s: %s", op, err.ErrMsg), err.StorageBucketCredentialState) + } return nil, status.Errorf(err.ErrCode, "%s: %s", op, err.ErrMsg) } } @@ -526,3 +540,12 @@ func MockObject(data []Chunk) *storagePluginStorageInfo { func copyBytes(in []byte) []byte { return append([]byte{}, in...) } + +func createErrorWithBucketCredentialState(errCode codes.Code, msg string, sbcState *plgpb.StorageBucketCredentialState) error { + st := status.New(errCode, msg) + stWithDetails, stErr := st.WithDetails(sbcState) + if stErr == nil { + st = stWithDetails + } + return st.Err() +} diff --git a/internal/plugin/loopback/storage_test.go b/internal/plugin/loopback/storage_test.go index a500979a7..7e7367c07 100644 --- a/internal/plugin/loopback/storage_test.go +++ b/internal/plugin/loopback/storage_test.go @@ -24,6 +24,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" ) func TestLoopbackOnCreateStorageBucket(t *testing.T) { @@ -376,14 +377,37 @@ func TestLoopbackValidatePermissions(t *testing.T) { plg, err := NewLoopbackPlugin( WithMockBuckets(map[BucketName]Bucket{ - "aws_s3_mock": {}, - "aws_s3_err": {}, + "aws_s3_mock": {}, + "aws_s3_err": {}, + "aws_s3_err_with_sbc_state": {}, }), WithMockError(PluginMockError{ BucketName: "aws_s3_err", ErrMsg: "invalid credentials", ErrCode: codes.PermissionDenied, }), + WithMockError(PluginMockError{ + BucketName: "aws_s3_err_with_sbc_state", + ErrMsg: "invalid credentials", + ErrCode: codes.PermissionDenied, + StorageBucketCredentialState: &plgpb.StorageBucketCredentialState{ + State: &plgpb.Permissions{ + Write: &plgpb.Permission{ + State: plgpb.StateType_STATE_TYPE_ERROR, + CheckedAt: timestamppb.Now(), + ErrorDetails: "invalid credentials", + }, + Read: &plgpb.Permission{ + State: plgpb.StateType_STATE_TYPE_OK, + CheckedAt: timestamppb.Now(), + }, + Delete: &plgpb.Permission{ + State: plgpb.StateType_STATE_TYPE_OK, + CheckedAt: timestamppb.Now(), + }, + }, + }, + }), ) assert.NoError(err) @@ -396,9 +420,10 @@ func TestLoopbackValidatePermissions(t *testing.T) { } tests := []struct { - name string - request *plgpb.ValidatePermissionsRequest - expectedErr codes.Code + name string + request *plgpb.ValidatePermissionsRequest + expectedErr codes.Code + expectedSBCState *plgpb.StorageBucketCredentialState }{ { name: "missing request", @@ -475,6 +500,38 @@ func TestLoopbackValidatePermissions(t *testing.T) { }, }, }, + { + name: "mocked error with SBC state", + request: &plgpb.ValidatePermissionsRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_err_with_sbc_state", + Secrets: secrets, + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "endpoint": structpb.NewStringValue("0.0.0.0"), + }, + }, + }, + }, + expectedErr: codes.PermissionDenied, + expectedSBCState: &plgpb.StorageBucketCredentialState{ + State: &plgpb.Permissions{ + Write: &plgpb.Permission{ + State: plgpb.StateType_STATE_TYPE_ERROR, + CheckedAt: timestamppb.Now(), + ErrorDetails: "invalid credentials", + }, + Read: &plgpb.Permission{ + State: plgpb.StateType_STATE_TYPE_OK, + CheckedAt: timestamppb.Now(), + }, + Delete: &plgpb.Permission{ + State: plgpb.StateType_STATE_TYPE_OK, + CheckedAt: timestamppb.Now(), + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -485,6 +542,19 @@ func TestLoopbackValidatePermissions(t *testing.T) { s, ok := status.FromError(err) require.True(ok, "invalid error type") assert.Equal(tt.expectedErr, s.Code()) + + if tt.expectedSBCState != nil { + sbcState := parseStorageBucketCredentialStateFromError(err) + require.NotNil(sbcState) + assert.Equal(tt.expectedSBCState.State.Write.GetState(), sbcState.State.Write.GetState()) + assert.Equal(tt.expectedSBCState.State.Write.GetErrorDetails(), sbcState.State.Write.GetErrorDetails()) + + assert.Equal(tt.expectedSBCState.State.Read.GetState(), sbcState.State.Read.GetState()) + assert.Equal(tt.expectedSBCState.State.Read.GetErrorDetails(), sbcState.State.Read.GetErrorDetails()) + + assert.Equal(tt.expectedSBCState.State.Delete.GetState(), sbcState.State.Delete.GetState()) + assert.Equal(tt.expectedSBCState.State.Delete.GetErrorDetails(), sbcState.State.Delete.GetErrorDetails()) + } return } @@ -1504,3 +1574,34 @@ func TestLoopbackStoragePlugin(t *testing.T) { require.NotEmpty(objectData) assert.EqualValues(objectData, getObjectData) } + +func parseStorageBucketCredentialStateFromError(err error) *plgpb.StorageBucketCredentialState { + sbcState := &plgpb.StorageBucketCredentialState{ + State: &plgpb.Permissions{ + Write: &plgpb.Permission{ + State: plgpb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + Read: &plgpb.Permission{ + State: plgpb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + Delete: &plgpb.Permission{ + State: plgpb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + } + + // attempt to retrieve the StorageBucketCredentialState detail from the error. + if st, ok := status.FromError(err); ok { + for _, detail := range st.Details() { + if statusDetail, ok := detail.(*plgpb.StorageBucketCredentialState); ok { + sbcState = statusDetail + break + } + } + } + + return sbcState +}