From 85b9adea81f6b5d29300c484a5609f079b789285 Mon Sep 17 00:00:00 2001 From: Damian Debkowski Date: Tue, 30 Jul 2024 10:15:57 -0700 Subject: [PATCH] feat(loopback): support dynamic credentials (#4982) --- internal/plugin/loopback/const.go | 9 ++ internal/plugin/loopback/storage.go | 45 +++++-- internal/plugin/loopback/storage_test.go | 151 +++++++++++++++++++++-- 3 files changed, 182 insertions(+), 23 deletions(-) create mode 100644 internal/plugin/loopback/const.go diff --git a/internal/plugin/loopback/const.go b/internal/plugin/loopback/const.go new file mode 100644 index 000000000..40ba82713 --- /dev/null +++ b/internal/plugin/loopback/const.go @@ -0,0 +1,9 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package loopback + +const ( + // ConstDynamicCredentials should be used as a key for an storage bucket attribute, to mock dynamic credentials. + ConstDynamicCredentials = "dynamic_credentials" +) diff --git a/internal/plugin/loopback/storage.go b/internal/plugin/loopback/storage.go index 3ac1b6aa2..3fbcc68d2 100644 --- a/internal/plugin/loopback/storage.go +++ b/internal/plugin/loopback/storage.go @@ -145,11 +145,21 @@ func (l *LoopbackStorage) onCreateStorageBucket(ctx context.Context, req *plgpb. return nil, status.Errorf(err.ErrCode, err.ErrMsg) } } - secrets := req.GetBucket().GetSecrets() - if secrets == nil { - secrets = &structpb.Struct{ - Fields: make(map[string]*structpb.Value), - } + secrets := &structpb.Struct{ + Fields: make(map[string]*structpb.Value), + } + var hasDynamicCreds bool + attrs := req.GetBucket().GetAttributes() + if attrs != nil { + _, hasDynamicCreds = attrs.Fields[ConstDynamicCredentials] + } + var hasStaticCreds bool + if req.GetBucket().GetSecrets() != nil && len(req.GetBucket().GetSecrets().AsMap()) > 0 { + hasStaticCreds = true + secrets = req.GetBucket().GetSecrets() + } + if hasDynamicCreds && hasStaticCreds { + return nil, status.Errorf(codes.InvalidArgument, "%s: cannot use both dynamic and static credentials", op) } return &plgpb.OnCreateStorageBucketResponse{ Persisted: &storagebuckets.StorageBucketPersisted{ @@ -179,14 +189,25 @@ func (l *LoopbackStorage) onUpdateStorageBucket(ctx context.Context, req *plgpb. return nil, status.Errorf(err.ErrCode, "%s: %s", op, err.ErrMsg) } } - var secrets *structpb.Struct - if req.GetNewBucket().GetSecrets() != nil { + secrets := &structpb.Struct{ + Fields: make(map[string]*structpb.Value), + } + var hasDynamicCreds bool + attrs := req.GetNewBucket().GetAttributes() + if attrs != nil { + _, hasDynamicCreds = attrs.Fields[ConstDynamicCredentials] + } + var hasStaticCreds bool + if req.GetNewBucket().GetSecrets() != nil && len(req.GetNewBucket().GetSecrets().AsMap()) > 0 { + hasStaticCreds = true secrets = req.GetNewBucket().GetSecrets() - } else if req.GetPersisted().GetData() != nil { - secrets = req.GetPersisted().GetData() - } else { - secrets = &structpb.Struct{ - Fields: make(map[string]*structpb.Value), + } + if hasDynamicCreds && hasStaticCreds { + return nil, status.Errorf(codes.InvalidArgument, "%s: cannot use both dynamic and static credentials", op) + } + if !hasDynamicCreds && len(secrets.AsMap()) == 0 { + if req.GetPersisted() != nil && req.GetPersisted().GetData() != nil && len(req.GetPersisted().GetData().AsMap()) > 0 { + secrets = req.GetPersisted().GetData() } } return &plgpb.OnUpdateStorageBucketResponse{ diff --git a/internal/plugin/loopback/storage_test.go b/internal/plugin/loopback/storage_test.go index 7e7367c07..d11893fa1 100644 --- a/internal/plugin/loopback/storage_test.go +++ b/internal/plugin/loopback/storage_test.go @@ -52,9 +52,11 @@ func TestLoopbackOnCreateStorageBucket(t *testing.T) { } tests := []struct { - name string - request *plgpb.OnCreateStorageBucketRequest - expectedErr codes.Code + name string + request *plgpb.OnCreateStorageBucketRequest + expectedErr codes.Code + expectedStaticCreds bool + expectedDynamicCreds bool }{ { name: "missing request", @@ -104,6 +106,22 @@ func TestLoopbackOnCreateStorageBucket(t *testing.T) { }, expectedErr: codes.PermissionDenied, }, + { + name: "error both credential types", + request: &plgpb.OnCreateStorageBucketRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Secrets: secrets, + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "endpoint": structpb.NewStringValue("0.0.0.0"), + ConstDynamicCredentials: structpb.NewBoolValue(true), + }, + }, + }, + }, + expectedErr: codes.InvalidArgument, + }, { name: "valid without credentials", request: &plgpb.OnCreateStorageBucketRequest{ @@ -116,9 +134,25 @@ func TestLoopbackOnCreateStorageBucket(t *testing.T) { }, }, }, + expectedDynamicCreds: true, }, { - name: "valid with credentials", + name: "valid with dynamic credentials", + request: &plgpb.OnCreateStorageBucketRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "endpoint": structpb.NewStringValue("0.0.0.0"), + ConstDynamicCredentials: structpb.NewBoolValue(true), + }, + }, + }, + }, + expectedDynamicCreds: true, + }, + { + name: "valid with static credentials", request: &plgpb.OnCreateStorageBucketRequest{ Bucket: &storagebuckets.StorageBucket{ BucketName: "aws_s3_mock", @@ -130,6 +164,7 @@ func TestLoopbackOnCreateStorageBucket(t *testing.T) { }, }, }, + expectedStaticCreds: true, }, } for _, tt := range tests { @@ -148,7 +183,17 @@ func TestLoopbackOnCreateStorageBucket(t *testing.T) { require.NotNil(response) assert.NotNil(response.GetPersisted()) assert.NotNil(response.GetPersisted().GetData()) - assert.EqualValues(tt.request.GetBucket().GetSecrets().AsMap(), response.GetPersisted().GetData().AsMap()) + + if tt.expectedDynamicCreds { + assert.Empty(response.GetPersisted().GetData().AsMap()) + return + } + + if tt.expectedStaticCreds { + assert.NotEmpty(response.GetPersisted().GetData().AsMap()) + assert.EqualValues(tt.request.GetBucket().GetSecrets().AsMap(), response.GetPersisted().GetData().AsMap()) + return + } }) } } @@ -181,9 +226,11 @@ func TestLoopbackOnUpdateStorageBucket(t *testing.T) { } tests := []struct { - name string - request *plgpb.OnUpdateStorageBucketRequest - expectedErr codes.Code + name string + request *plgpb.OnUpdateStorageBucketRequest + expectedErr codes.Code + expectedStaticCreds bool + expectedDynamicCreds bool }{ { name: "missing request", @@ -236,7 +283,23 @@ func TestLoopbackOnUpdateStorageBucket(t *testing.T) { expectedErr: codes.PermissionDenied, }, { - name: "valid without credentials", + name: "error both credential types", + request: &plgpb.OnUpdateStorageBucketRequest{ + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Secrets: secrets, + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "endpoint": structpb.NewStringValue("0.0.0.0"), + ConstDynamicCredentials: structpb.NewBoolValue(true), + }, + }, + }, + }, + expectedErr: codes.InvalidArgument, + }, + { + name: "valid without static credentials", request: &plgpb.OnUpdateStorageBucketRequest{ NewBucket: &storagebuckets.StorageBucket{ BucketName: "aws_s3_mock", @@ -247,9 +310,25 @@ func TestLoopbackOnUpdateStorageBucket(t *testing.T) { }, }, }, + expectedDynamicCreds: true, }, { - name: "valid with credentials", + name: "valid with dynamic credentials attribute", + request: &plgpb.OnUpdateStorageBucketRequest{ + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "endpoint": structpb.NewStringValue("0.0.0.0"), + ConstDynamicCredentials: structpb.NewBoolValue(true), + }, + }, + }, + }, + expectedDynamicCreds: true, + }, + { + name: "valid with both persisted and static credentials", request: &plgpb.OnUpdateStorageBucketRequest{ NewBucket: &storagebuckets.StorageBucket{ BucketName: "aws_s3_mock", @@ -262,6 +341,37 @@ func TestLoopbackOnUpdateStorageBucket(t *testing.T) { }, Persisted: persisted, }, + expectedStaticCreds: true, + }, + { + name: "valid with only persisted static credentials", + request: &plgpb.OnUpdateStorageBucketRequest{ + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "endpoint": structpb.NewStringValue("0.0.0.0"), + }, + }, + }, + Persisted: persisted, + }, + expectedStaticCreds: true, + }, + { + name: "valid with only static credentials", + request: &plgpb.OnUpdateStorageBucketRequest{ + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Secrets: secrets, + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "endpoint": structpb.NewStringValue("0.0.0.0"), + }, + }, + }, + }, + expectedStaticCreds: true, }, } for _, tt := range tests { @@ -280,7 +390,26 @@ func TestLoopbackOnUpdateStorageBucket(t *testing.T) { require.NotNil(response) assert.NotNil(response.GetPersisted()) assert.NotNil(response.GetPersisted().GetData()) - assert.EqualValues(tt.request.GetNewBucket().GetSecrets().AsMap(), response.GetPersisted().GetData().AsMap()) + + if tt.expectedDynamicCreds { + assert.Empty(response.GetPersisted().GetData().AsMap()) + return + } + + if tt.expectedStaticCreds { + assert.NotEmpty(response.GetPersisted().GetData().AsMap()) + newSecrets := tt.request.GetNewBucket().GetSecrets() + if newSecrets != nil && len(newSecrets.AsMap()) > 0 { + assert.EqualValues(newSecrets.AsMap(), response.GetPersisted().GetData().AsMap()) + return + } + oldSecrets := tt.request.GetPersisted().GetData() + if oldSecrets != nil && len(oldSecrets.AsMap()) > 0 { + assert.EqualValues(oldSecrets.AsMap(), response.GetPersisted().GetData().AsMap()) + return + } + return + } }) } }