feat(plugin): Implement OnCreateStorageBucket and ValidatePermissions
This commit is contained in:
1 parent
317f73eb5d
commit
07ec485161
11 files changed
+1872
-26
No files matched your search
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary-plugin-minio/internal/errors"
|
||||
"github.com/hashicorp/boundary-plugin-minio/internal/values"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
const (
|
||||
ConstEndpointUrl = "endpoint_url"
|
||||
ConstAccessKeyId = "access_key_id"
|
||||
ConstSecretAccessKey = "secret_access_key"
|
||||
ConstRegion = "region"
|
||||
)
|
||||
|
||||
type StorageAttributes struct {
|
||||
// AccessKeyId is the MinIO Access Key Id. This field is required and comes
|
||||
// from user input.
|
||||
AccessKeyId string
|
||||
// SecretAccessKey is the MinIO Secret Access Key. This field is required
|
||||
// and comes from user input.
|
||||
SecretAccessKey string
|
||||
// EndpointUrl is the MinIO server URL. This field is required and comes
|
||||
// from user input.
|
||||
EndpointUrl string
|
||||
// Region is the MinIO region. This field is optional and comes from user
|
||||
// input.
|
||||
Region string
|
||||
// UseSSL determines if the MinIO SDK will use SSL when communicating with
|
||||
// the server. This field is determined based on the endpoint URL prefix.
|
||||
UseSSL bool
|
||||
}
|
||||
|
||||
// ToMap returns a map StorageAttributes's secret fields as a map.
|
||||
func (sa *StorageAttributes) SecretsToMap() map[string]any {
|
||||
return map[string]any{
|
||||
ConstAccessKeyId: sa.AccessKeyId,
|
||||
ConstSecretAccessKey: sa.SecretAccessKey,
|
||||
}
|
||||
}
|
||||
|
||||
func getStorageAttributes(inAttributes *structpb.Struct, inSecrets *structpb.Struct) (*StorageAttributes, error) {
|
||||
if inAttributes == nil || inAttributes.GetFields() == nil || len(inAttributes.GetFields()) == 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty attributes input")
|
||||
}
|
||||
if inSecrets == nil || inSecrets.GetFields() == nil || len(inSecrets.GetFields()) == 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty secrets input")
|
||||
}
|
||||
|
||||
badFields := make(map[string]string)
|
||||
|
||||
unknownAttrFields := values.StructFields(inAttributes)
|
||||
endpointUrl, err := values.GetStringValue(inAttributes, ConstEndpointUrl, true)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstEndpointUrl)] = err.Error()
|
||||
}
|
||||
delete(unknownAttrFields, ConstEndpointUrl)
|
||||
|
||||
// The MinIO SDK doesn't want a fully-qualified URL, so we have to strip it
|
||||
// here and transform it to a combination of URL + useSSL.
|
||||
useSSL := true // Assume SSL by default.
|
||||
if strings.HasPrefix(endpointUrl, "https://") {
|
||||
endpointUrl = strings.TrimPrefix(endpointUrl, "https://")
|
||||
} else if strings.HasPrefix(endpointUrl, "http://") {
|
||||
useSSL = false
|
||||
endpointUrl = strings.TrimPrefix(endpointUrl, "http://")
|
||||
} else {
|
||||
badFields[fmt.Sprintf("attributes.%s.format", ConstEndpointUrl)] = "unknown protocol, should be http:// or https://"
|
||||
}
|
||||
|
||||
region, err := values.GetStringValue(inAttributes, ConstRegion, false)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstRegion)] = err.Error()
|
||||
}
|
||||
delete(unknownAttrFields, ConstRegion)
|
||||
|
||||
for s := range unknownAttrFields {
|
||||
badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field"
|
||||
}
|
||||
|
||||
unknownSecretFields := values.StructFields(inSecrets)
|
||||
accessKeyId, err := values.GetStringValue(inSecrets, ConstAccessKeyId, true)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = err.Error()
|
||||
}
|
||||
delete(unknownSecretFields, ConstAccessKeyId)
|
||||
|
||||
secretAccessKey, err := values.GetStringValue(inSecrets, ConstSecretAccessKey, true)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = err.Error()
|
||||
}
|
||||
delete(unknownSecretFields, ConstSecretAccessKey)
|
||||
|
||||
for s := range unknownSecretFields {
|
||||
badFields[fmt.Sprintf("secrets.%s", s)] = "unrecognized field"
|
||||
}
|
||||
|
||||
if len(badFields) > 0 {
|
||||
return nil, errors.InvalidArgumentError("invalid or unrecognized attributes/secrets found", badFields)
|
||||
}
|
||||
return &StorageAttributes{
|
||||
AccessKeyId: accessKeyId,
|
||||
SecretAccessKey: secretAccessKey,
|
||||
EndpointUrl: endpointUrl,
|
||||
Region: region,
|
||||
UseSSL: useSSL,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
func TestGetStorageAttributes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inAttributes *structpb.Struct
|
||||
inSecrets *structpb.Struct
|
||||
expStorageAttributes *StorageAttributes
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nilAttributes",
|
||||
inAttributes: nil,
|
||||
inSecrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
expErrMsg: "empty attributes input",
|
||||
},
|
||||
{
|
||||
name: "nilAttributesFields",
|
||||
inAttributes: &structpb.Struct{Fields: nil},
|
||||
inSecrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
expErrMsg: "empty attributes input",
|
||||
},
|
||||
{
|
||||
name: "emptyAttributesFields",
|
||||
inAttributes: &structpb.Struct{Fields: map[string]*structpb.Value{}},
|
||||
inSecrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
expErrMsg: "empty attributes input",
|
||||
},
|
||||
{
|
||||
name: "nilSecrets",
|
||||
inAttributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
inSecrets: nil,
|
||||
expErrMsg: "empty secrets input",
|
||||
},
|
||||
{
|
||||
name: "nilSecretsFields",
|
||||
inAttributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
inSecrets: &structpb.Struct{Fields: nil},
|
||||
expErrMsg: "empty secrets input",
|
||||
},
|
||||
{
|
||||
name: "emptySecretsFields",
|
||||
inAttributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
inSecrets: &structpb.Struct{Fields: map[string]*structpb.Value{}},
|
||||
expErrMsg: "empty secrets input",
|
||||
},
|
||||
{
|
||||
name: "noEndpointUrlAttribute",
|
||||
inAttributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
inSecrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("bar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
expErrMsg: "attributes.endpoint_url: missing required value \"endpoint_url\"",
|
||||
},
|
||||
{
|
||||
name: "emptyEndpointUrlAttribute",
|
||||
inAttributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewStringValue(""),
|
||||
},
|
||||
},
|
||||
inSecrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("bar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
expErrMsg: "attributes.endpoint_url: value \"endpoint_url\" cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "endpointUrlAttributeBadType",
|
||||
inAttributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewBoolValue(true),
|
||||
},
|
||||
},
|
||||
inSecrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("bar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
expErrMsg: "attributes.endpoint_url: unexpected type for value \"endpoint_url\": want string, got bool",
|
||||
},
|
||||
{
|
||||
name: "badEndpointUrlAttributeFormat",
|
||||
inAttributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewStringValue("bad://foo.bar"),
|
||||
},
|
||||
},
|
||||
inSecrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("bar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
expErrMsg: "attributes.endpoint_url.format: unknown protocol, should be http:// or https://",
|
||||
},
|
||||
{
|
||||
name: "regionAttributeBadType",
|
||||
inAttributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"),
|
||||
ConstRegion: structpb.NewBoolValue(true),
|
||||
},
|
||||
},
|
||||
inSecrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("bar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
expErrMsg: "attributes.region: unexpected type for value \"region\": want string, got bool",
|
||||
},
|
||||
{
|
||||
name: "unknownAttribute",
|
||||
inAttributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"),
|
||||
ConstRegion: structpb.NewStringValue("us-east-1"),
|
||||
"foo": structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
inSecrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("bar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
expErrMsg: "attributes.foo: unrecognized field",
|
||||
},
|
||||
{
|
||||
name: "noAccessKeyIdSecret",
|
||||
inAttributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"),
|
||||
ConstRegion: structpb.NewStringValue("us-east-1"),
|
||||
},
|
||||
},
|
||||
inSecrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
expErrMsg: "secrets.access_key_id: missing required value \"access_key_id\"",
|
||||
},
|
||||
{
|
||||
name: "emptyAccessKeyIdSecret",
|
||||
inAttributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"),
|
||||
ConstRegion: structpb.NewStringValue("us-east-1"),
|
||||
},
|
||||
},
|
||||
inSecrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue(""),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
expErrMsg: "secrets.access_key_id: value \"access_key_id\" cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "accessKeyIdSecretBadType",
|
||||
inAttributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"),
|
||||
ConstRegion: structpb.NewStringValue("us-east-1"),
|
||||
},
|
||||
},
|
||||
inSecrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewBoolValue(true),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
expErrMsg: "secrets.access_key_id: unexpected type for value \"access_key_id\": want string, got bool",
|
||||
},
|
||||
{
|
||||
name: "noSecretAccessKeySecret",
|
||||
inAttributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"),
|
||||
ConstRegion: structpb.NewStringValue("us-east-1"),
|
||||
},
|
||||
},
|
||||
inSecrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
expErrMsg: "secrets.secret_access_key: missing required value \"secret_access_key\"",
|
||||
},
|
||||
{
|
||||
name: "emptySecretAccessKeySecret",
|
||||
inAttributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"),
|
||||
ConstRegion: structpb.NewStringValue("us-east-1"),
|
||||
},
|
||||
},
|
||||
inSecrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("bar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue(""),
|
||||
},
|
||||
},
|
||||
expErrMsg: "secrets.secret_access_key: value \"secret_access_key\" cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "secretAccessKeySecretBadType",
|
||||
inAttributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"),
|
||||
ConstRegion: structpb.NewStringValue("us-east-1"),
|
||||
},
|
||||
},
|
||||
inSecrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("bar"),
|
||||
ConstSecretAccessKey: structpb.NewBoolValue(true),
|
||||
},
|
||||
},
|
||||
expErrMsg: "secrets.secret_access_key: unexpected type for value \"secret_access_key\": want string, got bool",
|
||||
},
|
||||
{
|
||||
name: "unknownSecret",
|
||||
inAttributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"),
|
||||
ConstRegion: structpb.NewStringValue("us-east-1"),
|
||||
},
|
||||
},
|
||||
inSecrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("bar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bar"),
|
||||
"foo": structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
expErrMsg: "secrets.foo: unrecognized field",
|
||||
},
|
||||
{
|
||||
name: "successWithNoSSL",
|
||||
inAttributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"),
|
||||
ConstRegion: structpb.NewStringValue("us-east-1"),
|
||||
},
|
||||
},
|
||||
inSecrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foo"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
expStorageAttributes: &StorageAttributes{
|
||||
AccessKeyId: "foo",
|
||||
SecretAccessKey: "bar",
|
||||
EndpointUrl: "foo.bar",
|
||||
Region: "us-east-1",
|
||||
UseSSL: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "successWithSSL",
|
||||
inAttributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewStringValue("https://foo.bar"),
|
||||
ConstRegion: structpb.NewStringValue("us-east-1"),
|
||||
},
|
||||
},
|
||||
inSecrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foo"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bar"),
|
||||
},
|
||||
},
|
||||
expStorageAttributes: &StorageAttributes{
|
||||
AccessKeyId: "foo",
|
||||
SecretAccessKey: "bar",
|
||||
EndpointUrl: "foo.bar",
|
||||
Region: "us-east-1",
|
||||
UseSSL: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sa, err := getStorageAttributes(tt.inAttributes, tt.inSecrets)
|
||||
if tt.expErrMsg != "" {
|
||||
require.ErrorContains(t, err, tt.expErrMsg)
|
||||
require.Nil(t, sa)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, sa)
|
||||
require.EqualValues(t, tt.expStorageAttributes, sa)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorageAttributesSecretsToMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in *StorageAttributes
|
||||
expMap map[string]any
|
||||
}{
|
||||
{
|
||||
name: "completeObject",
|
||||
in: &StorageAttributes{
|
||||
AccessKeyId: "access_key_id_value",
|
||||
SecretAccessKey: "secret_access_key_value",
|
||||
EndpointUrl: "endpoint_url_value",
|
||||
Region: "region_value",
|
||||
UseSSL: true,
|
||||
},
|
||||
expMap: map[string]any{
|
||||
"access_key_id": "access_key_id_value",
|
||||
"secret_access_key": "secret_access_key_value",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := tt.in.SecretsToMap()
|
||||
require.EqualValues(t, tt.expMap, m)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,20 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
madmin "github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
var _ pb.StoragePluginServiceServer = (*StoragePlugin)(nil)
|
||||
@@ -20,7 +30,45 @@ type StoragePlugin struct {
|
||||
|
||||
// OnCreateStorageBucket is a hook that runs when a storage bucket is created.
|
||||
func (sp *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCreateStorageBucketRequest) (*pb.OnCreateStorageBucketResponse, error) {
|
||||
return nil, fmt.Errorf("unimplemented")
|
||||
bucket := req.GetBucket()
|
||||
if bucket == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "no storage bucket information found")
|
||||
}
|
||||
if bucket.GetBucketName() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "storage bucket name is required")
|
||||
}
|
||||
|
||||
sa, err := getStorageAttributes(bucket.GetAttributes(), bucket.GetSecrets())
|
||||
if err != nil {
|
||||
return nil, err // getStorageAttributes already returns a "status"-type error.
|
||||
}
|
||||
|
||||
err = ensureServiceAccount(ctx, sa)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "failed to ensure service account: %v", err)
|
||||
}
|
||||
|
||||
cl, err := minio.New(sa.EndpointUrl, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(sa.AccessKeyId, sa.SecretAccessKey, ""),
|
||||
Secure: sa.UseSSL,
|
||||
Region: sa.Region,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unknown, "failed to create minio sdk client: %v", err)
|
||||
}
|
||||
|
||||
err = dryRun(ctx, cl, bucket)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unknown, "failed to verify provided minio environment: %v", err)
|
||||
}
|
||||
|
||||
persistedData, err := structpb.NewStruct(sa.SecretsToMap())
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to convert storage attributes to protobuf struct: %v", err)
|
||||
}
|
||||
return &pb.OnCreateStorageBucketResponse{
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{Data: persistedData},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// OnUpdateStorageBucket is a hook that runs when a storage bucket is updated.
|
||||
@@ -36,7 +84,39 @@ func (sp *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDe
|
||||
// ValidatePermissions is a hook that checks if the secrets associated with the
|
||||
// storage bucket meet the requirements of the plugin.
|
||||
func (sp *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.ValidatePermissionsRequest) (*pb.ValidatePermissionsResponse, error) {
|
||||
return nil, fmt.Errorf("unimplemented")
|
||||
bucket := req.GetBucket()
|
||||
if bucket == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "no storage bucket information found")
|
||||
}
|
||||
if bucket.GetBucketName() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "storage bucket name is required")
|
||||
}
|
||||
|
||||
sa, err := getStorageAttributes(bucket.GetAttributes(), bucket.GetSecrets())
|
||||
if err != nil {
|
||||
return nil, err // getStorageAttributes already returns a "status"-type error.
|
||||
}
|
||||
|
||||
err = ensureServiceAccount(ctx, sa)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "failed to ensure service account: %v", err)
|
||||
}
|
||||
|
||||
cl, err := minio.New(sa.EndpointUrl, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(sa.AccessKeyId, sa.SecretAccessKey, ""),
|
||||
Secure: sa.UseSSL,
|
||||
Region: sa.Region,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unknown, "failed to create minio sdk client: %v", err)
|
||||
}
|
||||
|
||||
err = dryRun(ctx, cl, bucket)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unknown, "failed to verify provided minio environment: %v", err)
|
||||
}
|
||||
|
||||
return &pb.ValidatePermissionsResponse{}, nil
|
||||
}
|
||||
|
||||
// HeadObject is a hook that retrieves metadata about an object.
|
||||
@@ -60,3 +140,56 @@ func (sp *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest
|
||||
func (sp *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjectsRequest) (*pb.DeleteObjectsResponse, error) {
|
||||
return nil, fmt.Errorf("unimplemented")
|
||||
}
|
||||
|
||||
func dryRun(ctx context.Context, cl *minio.Client, bucket *storagebuckets.StorageBucket) error {
|
||||
objectKey := path.Join(bucket.GetBucketPrefix(), uuid.New().String())
|
||||
rd := bytes.NewReader([]byte("hashicorp boundary minio plugin access test"))
|
||||
_, err := cl.PutObject(ctx, bucket.GetBucketName(), objectKey, rd, rd.Size(), minio.PutObjectOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to put object at %q: %w", objectKey, err)
|
||||
}
|
||||
|
||||
_, err = cl.StatObject(ctx, bucket.GetBucketName(), objectKey, minio.StatObjectOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat object at %q: %w", objectKey, err)
|
||||
}
|
||||
|
||||
_, err = cl.GetObject(ctx, bucket.GetBucketName(), objectKey, minio.GetObjectOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get object at %q: %w", objectKey, err)
|
||||
}
|
||||
|
||||
oiCh := cl.ListObjects(ctx, bucket.GetBucketName(), minio.ListObjectsOptions{Recursive: true})
|
||||
for oi := range oiCh {
|
||||
if oi.Err != nil {
|
||||
return fmt.Errorf("failed to list objects in bucket %q: %w", bucket.GetBucketName(), oi.Err)
|
||||
}
|
||||
}
|
||||
|
||||
err = cl.RemoveObject(ctx, bucket.GetBucketName(), objectKey, minio.RemoveObjectOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove object at %q: %w", objectKey, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureServiceAccount ensures the credentials we received belong to a MinIO
|
||||
// service account. This plugin does not support using user credentials for its
|
||||
// configuration.
|
||||
func ensureServiceAccount(ctx context.Context, sa *StorageAttributes) error {
|
||||
cl, err := madmin.NewWithOptions(sa.EndpointUrl, &madmin.Options{
|
||||
Creds: credentials.NewStaticV4(sa.AccessKeyId, sa.SecretAccessKey, ""),
|
||||
Secure: sa.UseSSL,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create madmin client: %w", err)
|
||||
}
|
||||
|
||||
_, err = cl.InfoServiceAccount(ctx, sa.AccessKeyId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to obtain service account info: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -4,16 +4,374 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
internaltest "github.com/hashicorp/boundary-plugin-minio/internal/testing"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/testing/protocmp"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
func TestOnCreateStorageBucket(t *testing.T) {}
|
||||
var (
|
||||
policyDenyPutObject = []byte(`{
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [ "s3:*" ],
|
||||
"Effect": "Allow",
|
||||
"Resource": "arn:aws:s3:::*"
|
||||
},
|
||||
{
|
||||
"Action": [ "s3:PutObject" ],
|
||||
"Effect": "Deny",
|
||||
"Resource": "arn:aws:s3:::*"
|
||||
},
|
||||
{
|
||||
"Action": [
|
||||
"admin:CreateServiceAccount",
|
||||
"admin:RemoveServiceAccount"
|
||||
],
|
||||
"Effect": "Allow"
|
||||
}
|
||||
],
|
||||
"Version": "2012-10-17"
|
||||
}`)
|
||||
policyDenyStatObject = []byte(`{
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [ "s3:*" ],
|
||||
"Effect": "Allow",
|
||||
"Resource": "arn:aws:s3:::*"
|
||||
},
|
||||
{
|
||||
"Action": [
|
||||
"s3:GetObject",
|
||||
"s3:GetObjectVersion",
|
||||
"s3:GetObjectAttributes",
|
||||
"s3:GetObjectVersionAttributes"
|
||||
],
|
||||
"Effect": "Deny",
|
||||
"Resource": "arn:aws:s3:::*"
|
||||
},
|
||||
{
|
||||
"Action": [
|
||||
"admin:CreateServiceAccount",
|
||||
"admin:RemoveServiceAccount"
|
||||
],
|
||||
"Effect": "Allow"
|
||||
}
|
||||
],
|
||||
"Version": "2012-10-17"
|
||||
}`)
|
||||
policyDenyListBucket = []byte(`{
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [ "s3:*" ],
|
||||
"Effect": "Allow",
|
||||
"Resource": "arn:aws:s3:::*"
|
||||
},
|
||||
{
|
||||
"Action": [ "s3:ListBucket" ],
|
||||
"Effect": "Deny",
|
||||
"Resource": "arn:aws:s3:::*"
|
||||
},
|
||||
{
|
||||
"Action": [
|
||||
"admin:CreateServiceAccount",
|
||||
"admin:RemoveServiceAccount"
|
||||
],
|
||||
"Effect": "Allow"
|
||||
}
|
||||
],
|
||||
"Version": "2012-10-17"
|
||||
}`)
|
||||
policyDenyDeleteObject = []byte(`{
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [ "s3:*" ],
|
||||
"Effect": "Allow",
|
||||
"Resource": "arn:aws:s3:::*"
|
||||
},
|
||||
{
|
||||
"Action": [ "s3:DeleteObject" ],
|
||||
"Effect": "Deny",
|
||||
"Resource": "arn:aws:s3:::*"
|
||||
},
|
||||
{
|
||||
"Action": [
|
||||
"admin:CreateServiceAccount",
|
||||
"admin:RemoveServiceAccount"
|
||||
],
|
||||
"Effect": "Allow"
|
||||
}
|
||||
],
|
||||
"Version": "2012-10-17"
|
||||
}`)
|
||||
)
|
||||
|
||||
func TestOnCreateStorageBucket(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
server := internaltest.NewMinioServer(t)
|
||||
|
||||
bucketName := "test-bucket"
|
||||
err := server.Client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
minioClFn func(t *testing.T) *minio.Client
|
||||
req *plugin.OnCreateStorageBucketRequest
|
||||
expRsp *plugin.OnCreateStorageBucketResponse
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nilBucket",
|
||||
req: &plugin.OnCreateStorageBucketRequest{Bucket: nil},
|
||||
expErrMsg: "no storage bucket information found",
|
||||
},
|
||||
{
|
||||
name: "noBucketName",
|
||||
req: &plugin.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "",
|
||||
},
|
||||
},
|
||||
expErrMsg: "storage bucket name is required",
|
||||
},
|
||||
{
|
||||
name: "badStorageAttributes",
|
||||
req: &plugin.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
},
|
||||
},
|
||||
expErrMsg: "empty attributes input",
|
||||
},
|
||||
{
|
||||
name: "usingNonServiceAccount",
|
||||
req: &plugin.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewStringValue("http://" + server.ApiAddr),
|
||||
},
|
||||
},
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue(server.RootUsername),
|
||||
ConstSecretAccessKey: structpb.NewStringValue(server.RootPassword),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expErrMsg: "The specified service account is not found (Specified service account does not exist)",
|
||||
},
|
||||
{
|
||||
name: "dryRunFail",
|
||||
req: &plugin.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewStringValue("http://" + server.ApiAddr),
|
||||
},
|
||||
},
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: func() map[string]*structpb.Value {
|
||||
creds, err := server.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
|
||||
Policy: policyDenyPutObject,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue(creds.AccessKey),
|
||||
ConstSecretAccessKey: structpb.NewStringValue(creds.SecretKey),
|
||||
}
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
expErrMsg: "failed to verify provided minio environment: failed to put object",
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
req: &plugin.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewStringValue("http://" + server.ApiAddr),
|
||||
},
|
||||
},
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue(server.ServiceAccountAccessKeyId),
|
||||
ConstSecretAccessKey: structpb.NewStringValue(server.ServiceAccountSecretAccessKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expRsp: &plugin.OnCreateStorageBucketResponse{
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue(server.ServiceAccountAccessKeyId),
|
||||
ConstSecretAccessKey: structpb.NewStringValue(server.ServiceAccountSecretAccessKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sp := new(StoragePlugin)
|
||||
rsp, err := sp.OnCreateStorageBucket(ctx, tt.req)
|
||||
if tt.expErrMsg != "" {
|
||||
require.ErrorContains(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rsp)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rsp)
|
||||
require.Empty(t, cmp.Diff(tt.expRsp, rsp, protocmp.Transform()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnUpdateStorageBucket(t *testing.T) {}
|
||||
|
||||
func TestOnDeleteStorageBucket(t *testing.T) {}
|
||||
|
||||
func TestValidatePermissions(t *testing.T) {}
|
||||
func TestValidatePermissions(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
server := internaltest.NewMinioServer(t)
|
||||
|
||||
bucketName := "test-bucket"
|
||||
err := server.Client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
minioClFn func(t *testing.T) *minio.Client
|
||||
req *plugin.ValidatePermissionsRequest
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nilBucket",
|
||||
req: &plugin.ValidatePermissionsRequest{Bucket: nil},
|
||||
expErrMsg: "no storage bucket information found",
|
||||
},
|
||||
{
|
||||
name: "noBucketName",
|
||||
req: &plugin.ValidatePermissionsRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "",
|
||||
},
|
||||
},
|
||||
expErrMsg: "storage bucket name is required",
|
||||
},
|
||||
{
|
||||
name: "badStorageAttributes",
|
||||
req: &plugin.ValidatePermissionsRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
},
|
||||
},
|
||||
expErrMsg: "empty attributes input",
|
||||
},
|
||||
{
|
||||
name: "usingNonServiceAccount",
|
||||
req: &plugin.ValidatePermissionsRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewStringValue("http://" + server.ApiAddr),
|
||||
},
|
||||
},
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue(server.RootUsername),
|
||||
ConstSecretAccessKey: structpb.NewStringValue(server.RootPassword),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expErrMsg: "The specified service account is not found (Specified service account does not exist)",
|
||||
},
|
||||
{
|
||||
name: "dryRunFail",
|
||||
req: &plugin.ValidatePermissionsRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewStringValue("http://" + server.ApiAddr),
|
||||
},
|
||||
},
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: func() map[string]*structpb.Value {
|
||||
creds, err := server.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
|
||||
Policy: policyDenyPutObject,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue(creds.AccessKey),
|
||||
ConstSecretAccessKey: structpb.NewStringValue(creds.SecretKey),
|
||||
}
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
expErrMsg: "failed to verify provided minio environment: failed to put object",
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
req: &plugin.ValidatePermissionsRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstEndpointUrl: structpb.NewStringValue("http://" + server.ApiAddr),
|
||||
},
|
||||
},
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue(server.ServiceAccountAccessKeyId),
|
||||
ConstSecretAccessKey: structpb.NewStringValue(server.ServiceAccountSecretAccessKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sp := new(StoragePlugin)
|
||||
rsp, err := sp.ValidatePermissions(ctx, tt.req)
|
||||
if tt.expErrMsg != "" {
|
||||
require.ErrorContains(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rsp)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rsp)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeadObject(t *testing.T) {}
|
||||
|
||||
@@ -22,3 +380,210 @@ func TestGetObject(t *testing.T) {}
|
||||
func TestPutObject(t *testing.T) {}
|
||||
|
||||
func TestDeleteObjects(t *testing.T) {}
|
||||
|
||||
func TestDryRun(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
minioClFn func(t *testing.T, s *internaltest.MinioServer) *minio.Client
|
||||
makeBucketFn func(t *testing.T, cl *minio.Client) string
|
||||
// This test verifies that each message in the err slice is contained in
|
||||
// the error string, effectively &&-ing the checks together.
|
||||
expErrMsgs []string
|
||||
}{
|
||||
{
|
||||
name: "bucketNotExists",
|
||||
makeBucketFn: func(t *testing.T, cl *minio.Client) string { return "unknownbucket" },
|
||||
expErrMsgs: []string{"The specified bucket does not exist"},
|
||||
},
|
||||
{
|
||||
name: "putObjectFail",
|
||||
minioClFn: func(t *testing.T, s *internaltest.MinioServer) *minio.Client {
|
||||
creds, err := s.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
|
||||
Policy: policyDenyPutObject,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
cl, err := minio.New(s.ApiAddr, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(creds.AccessKey, creds.SecretKey, ""),
|
||||
Secure: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return cl
|
||||
},
|
||||
makeBucketFn: func(t *testing.T, cl *minio.Client) string {
|
||||
bucketName := "testdryrun-putobjectfail"
|
||||
err := cl.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
return bucketName
|
||||
},
|
||||
expErrMsgs: []string{
|
||||
"failed to put object",
|
||||
"Access Denied",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "statObjectFail",
|
||||
minioClFn: func(t *testing.T, s *internaltest.MinioServer) *minio.Client {
|
||||
creds, err := s.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
|
||||
Policy: policyDenyStatObject,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
cl, err := minio.New(s.ApiAddr, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(creds.AccessKey, creds.SecretKey, ""),
|
||||
Secure: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return cl
|
||||
},
|
||||
makeBucketFn: func(t *testing.T, cl *minio.Client) string {
|
||||
bucketName := "testdryrun-statobjectfail"
|
||||
err := cl.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
return bucketName
|
||||
},
|
||||
expErrMsgs: []string{
|
||||
"failed to stat object",
|
||||
"Access Denied",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "listObjectsFail",
|
||||
minioClFn: func(t *testing.T, s *internaltest.MinioServer) *minio.Client {
|
||||
creds, err := s.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
|
||||
Policy: policyDenyListBucket,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
cl, err := minio.New(s.ApiAddr, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(creds.AccessKey, creds.SecretKey, ""),
|
||||
Secure: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return cl
|
||||
},
|
||||
makeBucketFn: func(t *testing.T, cl *minio.Client) string {
|
||||
bucketName := "testdryrun-listobjectsfail"
|
||||
err := cl.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
return bucketName
|
||||
},
|
||||
expErrMsgs: []string{
|
||||
"failed to list objects in bucket",
|
||||
"Access Denied",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "removeObjectFail",
|
||||
minioClFn: func(t *testing.T, s *internaltest.MinioServer) *minio.Client {
|
||||
creds, err := s.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
|
||||
Policy: policyDenyDeleteObject,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
cl, err := minio.New(s.ApiAddr, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(creds.AccessKey, creds.SecretKey, ""),
|
||||
Secure: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return cl
|
||||
},
|
||||
makeBucketFn: func(t *testing.T, cl *minio.Client) string {
|
||||
bucketName := "testdryrun-removeobjectfail"
|
||||
err := cl.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
return bucketName
|
||||
},
|
||||
expErrMsgs: []string{
|
||||
"failed to remove object",
|
||||
"Access Denied",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
makeBucketFn: func(t *testing.T, cl *minio.Client) string {
|
||||
bucketName := "testdryrun-success"
|
||||
err := cl.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{})
|
||||
require.NoError(t, err)
|
||||
return bucketName
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
server := internaltest.NewMinioServer(t)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
minioClient := server.Client
|
||||
if tt.minioClFn != nil {
|
||||
minioClient = tt.minioClFn(t, server)
|
||||
}
|
||||
|
||||
bucketName := tt.makeBucketFn(t, minioClient)
|
||||
err := dryRun(ctx, minioClient, &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
})
|
||||
if len(tt.expErrMsgs) > 0 {
|
||||
for _, errMsg := range tt.expErrMsgs {
|
||||
require.ErrorContains(t, err, errMsg)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureServiceAccount(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
saFunc func(s *internaltest.MinioServer) *StorageAttributes
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nonServiceAccount",
|
||||
saFunc: func(s *internaltest.MinioServer) *StorageAttributes {
|
||||
return &StorageAttributes{
|
||||
AccessKeyId: s.RootUsername,
|
||||
SecretAccessKey: s.RootPassword,
|
||||
EndpointUrl: s.ApiAddr,
|
||||
UseSSL: false,
|
||||
}
|
||||
},
|
||||
expErrMsg: "The specified service account is not found (Specified service account does not exist)",
|
||||
},
|
||||
{
|
||||
name: "serviceAccount",
|
||||
saFunc: func(s *internaltest.MinioServer) *StorageAttributes {
|
||||
return &StorageAttributes{
|
||||
AccessKeyId: s.ServiceAccountAccessKeyId,
|
||||
SecretAccessKey: s.ServiceAccountSecretAccessKey,
|
||||
EndpointUrl: s.ApiAddr,
|
||||
UseSSL: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
server := internaltest.NewMinioServer(t)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ensureServiceAccount(context.Background(), tt.saFunc(server))
|
||||
if tt.expErrMsg != "" {
|
||||
require.ErrorContains(t, err, tt.expErrMsg)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user