Compare commits

..
Author SHA1 Message Date
hashicorp-copywrite[bot] a0067502b9 [COMPLIANCE] Add Copyright and License Headers (#15)
Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2024-04-09 11:17:43 -07:00
Hugo bde78ecccd docs(plugin): Add Credential Rotation attribute and explanation 2024-04-04 18:03:37 +01:00
Hugo eb0c2b4682 feat(plugin): Implement Credential Rotation 2024-04-04 18:03:37 +01:00
Hugo 0214399017 feat(plugin): Add credential rotation primitives 2024-04-04 18:03:37 +01:00
Hugo 2c2bd3429b feat(values): Add methods to get bool and time values from proto 2024-04-04 18:03:37 +01:00
dani 09cece64bc feat(plugin): implement DeleteObjects functionality along with associated automated tests (#14) 2024-04-04 12:19:10 -04:00
7 changed files with 49 additions and 138 deletions

No files matched your search

+6
View File
@@ -13,6 +13,7 @@ boundary storage-buckets create \
-worker-filter '"minio-access" in "/tags/type"' \
-attr endpoint_url="https://my-minio-instance.dev/s3-api" \
-attr region=REGION \
-attr disable_credential_rotation=true \
-secret access_key_id='KEY' \
-secret secret_access_key='SECRET'
```
@@ -24,6 +25,11 @@ The following `attributes` are valid on a MinIO storage bucket resource:
- `endpoint_url` (string, required): Fully-qualified endpoint pointing to a
MinIO S3 API.
- `region` (string, optional): The region to configure the storage bucket for.
- `disable_credential_rotation` (bool, optional): Controls whether the plugin
will rotate the incoming credentials and manage a new MinIO service account.
If this attribute is set to `false`, or not provided, the plugin will rotate
the incoming credentials, using them to create a new MinIO service account,
then delete the incoming credentials.
### Secrets
+4 -4
View File
@@ -65,11 +65,11 @@ func (sa *StorageSecrets) AsMap() map[string]any {
// Clone returns a new StorageSecrets object with the same state as the incoming
// one.
func (ss *StorageSecrets) Clone() *StorageSecrets {
func (sec *StorageSecrets) Clone() *StorageSecrets {
return &StorageSecrets{
AccessKeyId: ss.AccessKeyId,
SecretAccessKey: ss.SecretAccessKey,
LastRotatedTime: ss.LastRotatedTime,
AccessKeyId: sec.AccessKeyId,
SecretAccessKey: sec.SecretAccessKey,
LastRotatedTime: sec.LastRotatedTime,
}
}
+5 -5
View File
@@ -333,9 +333,9 @@ func TestTestStorageSecretsClone(t *testing.T) {
LastRotatedTime: now,
}
ss := in.Clone()
require.Equal(t, in.AccessKeyId, ss.AccessKeyId)
require.Equal(t, in.AccessKeyId, ss.AccessKeyId)
require.Equal(t, in.LastRotatedTime, ss.LastRotatedTime)
require.NotSame(t, in, ss)
secCl := in.Clone()
require.Equal(t, in.AccessKeyId, secCl.AccessKeyId)
require.Equal(t, in.AccessKeyId, secCl.AccessKeyId)
require.Equal(t, in.LastRotatedTime, secCl.LastRotatedTime)
require.NotSame(t, in, secCl)
}
+4 -14
View File
@@ -189,11 +189,11 @@ func (sp *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUp
// account, then delete the incoming credentials. The existing
// persisted credentials have already been scheduled for deletion
// above (if applicable), so we don't need to do it here.
ss, fn, err := rotateCredentials(ctx, newBucket.GetBucketName(), nsa, newSec)
rotatedSec, fn, err := rotateCredentials(ctx, newBucket.GetBucketName(), nsa, newSec)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to rotate minio credentials: %v", err)
}
sec = ss
sec = rotatedSec
deleteRotatedCredsFn = fn
} else {
// We have new incoming credentials but we're not rotating. Set them
@@ -205,16 +205,8 @@ func (sp *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUp
} else {
if osa.DisableCredentialRotation && !nsa.DisableCredentialRotation {
// Handle the case where the user enables credential rotation but
// provides no new credentials. In this case, we use the existing
// persisted credentials to generate a new service account. The
// persisted credentials are, by definition, not being managed by us
// (credential rotation was disabled beforehand), so don't schedule
// them for deletion.
ss, _, err := rotateCredentials(ctx, newBucket.GetBucketName(), nsa, sec)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to rotate minio credentials: %v", err)
}
sec = ss
// provides no new credentials. This is an error.
return nil, status.Errorf(codes.InvalidArgument, "credential rotation enabled with no new credentials provided")
}
}
@@ -581,7 +573,6 @@ func (sp *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObject
Recursive: true,
}) {
if obj.Err != nil {
// check if context cancelled ?
return nil, status.Errorf(codes.Unknown, "error iterating minio bucket contents: %v", err)
}
objects = append(objects, obj)
@@ -599,7 +590,6 @@ func (sp *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObject
removed := 0
for res := range cl.RemoveObjectsWithResult(ctx, bucket.GetBucketName(), emitter, minio.RemoveObjectsOptions{}) {
if res.Err != nil {
// check if context cancelled ?
return nil, status.Errorf(codes.Unknown, "error deleting minio object(s): %v", err)
}
removed++
+8 -99
View File
@@ -907,7 +907,7 @@ func TestOnUpdateStorageBucket(t *testing.T) {
errCode: codes.InvalidArgument,
},
{
name: "credRotationFailNoNewCreds",
name: "credRotationNoNewCreds",
req: &plugin.OnUpdateStorageBucketRequest{
CurrentBucket: &storagebuckets.StorageBucket{
BucketName: bucketName,
@@ -919,32 +919,12 @@ func TestOnUpdateStorageBucket(t *testing.T) {
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: func() *structpb.Struct {
creds, err := server.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
Policy: json.RawMessage(`
{
"Statement": [
{
"Action": [
"admin:CreateServiceAccount",
"admin:RemoveServiceAccount"
],
"Effect": "Deny"
}
],
"Version": "2012-10-17"
}
`),
})
require.NoError(t, err)
return &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue(creds.AccessKey),
ConstSecretAccessKey: structpb.NewStringValue(creds.SecretKey),
},
}
}(),
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue(server.ServiceAccountAccessKeyId),
ConstSecretAccessKey: structpb.NewStringValue(server.ServiceAccountSecretAccessKey),
},
},
},
NewBucket: &storagebuckets.StorageBucket{
BucketName: bucketName,
@@ -956,7 +936,7 @@ func TestOnUpdateStorageBucket(t *testing.T) {
},
},
},
err: "failed to rotate minio credentials: failed to create new minio service account: Access Denied.",
err: "credential rotation enabled with no new credentials provided",
errCode: codes.InvalidArgument,
},
{
@@ -1111,77 +1091,6 @@ func TestOnUpdateStorageBucket_SuccessDisableCredRotationAndNewCreds(t *testing.
require.Len(t, lsa.Accounts, 2) // Creds in `server` + newCreds.
}
func TestOnUpdateStorageBucket_SuccessEnableCredRotationNoNewCreds(t *testing.T) {
ctx := context.Background()
server := internaltest.NewMinioServer(t)
bucketName := "test"
err := server.Client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{})
require.NoError(t, err)
persistedCreds, err := server.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{})
require.NoError(t, err)
req := &plugin.OnUpdateStorageBucketRequest{
CurrentBucket: &storagebuckets.StorageBucket{
BucketName: bucketName,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstEndpointUrl: structpb.NewStringValue("http://" + server.ApiAddr),
ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue(persistedCreds.AccessKey),
ConstSecretAccessKey: structpb.NewStringValue(persistedCreds.SecretKey),
},
},
},
NewBucket: &storagebuckets.StorageBucket{
BucketName: bucketName,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstEndpointUrl: structpb.NewStringValue("http://" + server.ApiAddr),
ConstDisableCredentialRotation: structpb.NewBoolValue(false),
},
},
},
}
// `disable_cred_rotation` disabled -> enable and provide no new creds.
// This should use the existing persisted credentials to generate new ones,
// then use those. The old persisted credentials should not be deleted.
sp := new(StoragePlugin)
rsp, err := sp.OnUpdateStorageBucket(ctx, req)
require.NoError(t, err)
require.NotNil(t, req)
outAccessKeyId, err := values.GetStringValue(rsp.GetPersisted().GetData(), ConstAccessKeyId, true)
require.NoError(t, err)
require.NotEqual(t, persistedCreds.AccessKey, outAccessKeyId)
outSecretAccessKey, err := values.GetStringValue(rsp.GetPersisted().GetData(), ConstSecretAccessKey, true)
require.NoError(t, err)
require.NotEqual(t, persistedCreds.SecretKey, outSecretAccessKey)
lastRotated, err := values.GetTimeValue(rsp.GetPersisted().GetData(), ConstLastRotatedTime)
require.NoError(t, err)
require.False(t, lastRotated.IsZero())
_, err = server.AdminClient.InfoServiceAccount(ctx, persistedCreds.AccessKey)
require.NoError(t, err)
_, err = server.AdminClient.InfoServiceAccount(ctx, outAccessKeyId)
require.NoError(t, err)
lsa, err := server.AdminClient.ListServiceAccounts(ctx, server.RootUsername)
require.NoError(t, err)
require.Len(t, lsa.Accounts, 3) // Creds in `server` + persistedCreds + newly rotated creds.
}
func TestOnUpdateStorageBucket_SuccessEnableCredRotationWithNewCreds(t *testing.T) {
ctx := context.Background()
server := internaltest.NewMinioServer(t)
+5 -2
View File
@@ -1,3 +1,6 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package storage
import (
@@ -73,9 +76,9 @@ func rotateCredentials(ctx context.Context, bucketName string, sa *StorageAttrib
LastRotatedTime: time.Now(),
}
inCl, newCl := inSec.Clone(), newSec.Clone()
inCl := inSec.Clone()
return newSec, sync.OnceValue(func() error {
ac, err := newMadminClient(sa, newCl)
ac, err := newMadminClient(sa, inCl)
if err != nil {
return fmt.Errorf("failed to create minio admin client: %w", err)
}
+17 -14
View File
@@ -1,3 +1,6 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package storage
import (
@@ -21,13 +24,13 @@ func TestRotateCredentials(t *testing.T) {
tests := []struct {
name string
ss *StorageSecrets
inSec *StorageSecrets
expErrMsg string
credDelErr bool
}{
{
name: "notUsingServiceAccount",
ss: &StorageSecrets{
inSec: &StorageSecrets{
AccessKeyId: server.RootUsername,
SecretAccessKey: server.RootPassword,
},
@@ -35,7 +38,7 @@ func TestRotateCredentials(t *testing.T) {
},
{
name: "addServiceAccountFail",
ss: func() *StorageSecrets {
inSec: func() *StorageSecrets {
creds, err := server.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
Policy: json.RawMessage(`
{
@@ -63,7 +66,7 @@ func TestRotateCredentials(t *testing.T) {
},
{
name: "credentialDeletionFail",
ss: func() *StorageSecrets {
inSec: func() *StorageSecrets {
creds, err := server.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{})
require.NoError(t, err)
@@ -76,7 +79,7 @@ func TestRotateCredentials(t *testing.T) {
},
{
name: "success",
ss: func() *StorageSecrets {
inSec: func() *StorageSecrets {
creds, err := server.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{})
require.NoError(t, err)
@@ -95,26 +98,26 @@ func TestRotateCredentials(t *testing.T) {
UseSSL: false,
}
ss, delFn, err := rotateCredentials(context.Background(), bucketName, sa, tt.ss)
outSec, delFn, err := rotateCredentials(context.Background(), bucketName, sa, tt.inSec)
if tt.expErrMsg != "" {
require.ErrorContains(t, err, tt.expErrMsg)
require.Nil(t, ss)
require.Nil(t, outSec)
require.Nil(t, delFn)
return
}
require.NoError(t, err)
require.NotNil(t, ss)
require.NotNil(t, outSec)
require.NotNil(t, delFn)
require.NotEqual(t, ss.AccessKeyId, tt.ss.AccessKeyId)
require.NotEqual(t, ss.SecretAccessKey, tt.ss.SecretAccessKey)
require.NotEmpty(t, ss.LastRotatedTime)
require.NotEqual(t, outSec.AccessKeyId, tt.inSec.AccessKeyId)
require.NotEqual(t, outSec.SecretAccessKey, tt.inSec.SecretAccessKey)
require.NotEmpty(t, outSec.LastRotatedTime)
// Ensure old creds still exist since the callback hasn't been
// called. We then make the delete call and finally assert that the
// credential was removed if there was no error when attempting to
// delete.
_, err = server.AdminClient.InfoServiceAccount(ctx, tt.ss.AccessKeyId)
_, err = server.AdminClient.InfoServiceAccount(ctx, tt.inSec.AccessKeyId)
require.NoError(t, err)
if tt.credDelErr {
@@ -122,7 +125,7 @@ func TestRotateCredentials(t *testing.T) {
require.ErrorContains(t, delFn(), "failed to delete minio service account")
require.ErrorContains(t, delFn(), "failed to delete minio service account") // 2nd call should return the same state (due to sync.Once).
_, err := server.AdminClient.InfoServiceAccount(ctx, tt.ss.AccessKeyId)
_, err := server.AdminClient.InfoServiceAccount(ctx, tt.inSec.AccessKeyId)
require.NoError(t, err)
return
} else {
@@ -130,7 +133,7 @@ func TestRotateCredentials(t *testing.T) {
require.NoError(t, delFn()) // 2nd call should return the same state (due to sync.Once).
}
_, err = server.AdminClient.InfoServiceAccount(ctx, tt.ss.AccessKeyId)
_, err = server.AdminClient.InfoServiceAccount(ctx, tt.inSec.AccessKeyId)
require.ErrorContains(t, err, "service account is not found")
})
}