feat(sbc): Add domain changes for SBC

This commit is contained in:
Damian Debkowski authored and Elim Tsiagbey committed 2024-07-17 14:23:11 -04:00
1 parent 7a3c484147
commit 162a857a29
16 files changed
+892 -242

No files matched your search

@@ -6,7 +6,7 @@
"data": {
"data": {
"limits": {
"alias": {
"alias": {
"create": [
{
"action": "create",
@@ -4266,4 +4266,4 @@
"version": "v0.1"
},
"datacontentype": "application/cloudevents"
}
}
@@ -4108,4 +4108,4 @@
"version": "v0.1"
},
"datacontentype": "application/cloudevents"
}
}
+8 -7
View File
@@ -9,6 +9,7 @@ import (
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/storage/storagebucketcredential/managedsecret"
"github.com/hashicorp/boundary/internal/util"
)
@@ -17,12 +18,12 @@ const (
)
func init() {
kms.RegisterTableRewrapFn(storageBucketCredentialManagedSecretTablename, storageBucketCredentialManagedSecretRewrapFn)
kms.RegisterTableRewrapFn(storageBucketCredentialManagedSecretTablename, storageBucketCredentialRewrapFn)
}
// storageBucketRewrapFn provides a kms.Rewrapfn for the StorageBucketCredentialManagedSecret type
func storageBucketCredentialManagedSecretRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo kms.GetWrapperer) error {
const op = "storage.storageBucketRewrapFn"
// storageBucketCredentialRewrapFn provides a kms.Rewrapfn for the StorageBucketCredentialManagedSecret type
func storageBucketCredentialRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo kms.GetWrapperer) error {
const op = "storage.storageBucketCredentialRewrapFn"
switch {
case dataKeyVersionId == "":
return errors.New(ctx, errors.InvalidParameter, op, "missing data key version id")
@@ -35,7 +36,7 @@ func storageBucketCredentialManagedSecretRewrapFn(ctx context.Context, dataKeyVe
case kmsRepo == nil:
return errors.New(ctx, errors.InvalidParameter, op, "missing kms repository")
}
var sbcms []*StorageBucketCredentialManagedSecret
var sbcms []*managedsecret.StorageBucketCredential
// The only index on this table is on the primary key (storage bucket id) and we can't find it that way.
// This is the fastest query we can use without creating a new index on key_id.
if err := reader.SearchWhere(ctx, &sbcms, "key_id=?", []any{dataKeyVersionId}, db.WithLimit(-1)); err != nil {
@@ -46,10 +47,10 @@ func storageBucketCredentialManagedSecretRewrapFn(ctx context.Context, dataKeyVe
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping"))
}
for _, secret := range sbcms {
if err := secret.decrypt(ctx, wrapper); err != nil {
if err := secret.Decrypt(ctx, wrapper); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt storage bucket secret"))
}
if err := secret.encrypt(ctx, wrapper); err != nil {
if err := secret.Encrypt(ctx, wrapper); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt storage bucket secret"))
}
if _, err := writer.Update(ctx, secret, []string{"CtSecrets", "KeyId"}, nil); err != nil {
+1 -77
View File
@@ -11,15 +11,9 @@ import (
"github.com/DATA-DOG/go-sqlmock"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/libs/patchstruct"
"github.com/hashicorp/boundary/internal/plugin"
"github.com/hashicorp/boundary/internal/storage/plugin/store"
"github.com/hashicorp/go-kms-wrapping/extras/kms/v2/migrations"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
)
func TestRewrap_storageBucketCredentialManagedSecretRewrapFn(t *testing.T) {
@@ -38,77 +32,7 @@ func TestRewrap_storageBucketCredentialManagedSecretRewrapFn(t *testing.T) {
mock.ExpectQuery(
`SELECT \* FROM "storage_plugin_storage_bucket_secret" WHERE key_id=\$1`,
).WillReturnError(errors.New("Query error"))
err := storageBucketCredentialManagedSecretRewrapFn(ctx, "some_id", "some_scope", rw, rw, kmsCache)
err := storageBucketCredentialRewrapFn(ctx, "some_id", "some_scope", rw, rw, kmsCache)
require.Error(t, err)
})
t.Run("success", func(t *testing.T) {
require, assert := require.New(t), assert.New(t)
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
plg := plugin.TestPlugin(t, conn, "test", plugin.WithStorageFlag(true))
kmsCache := kms.TestKms(t, conn, wrapper)
sb := &StorageBucket{
StorageBucket: &store.StorageBucket{
ScopeId: org.PublicId,
PluginId: plg.PublicId,
BucketName: "test-bucket-name",
Attributes: []byte{},
SecretsHmac: []byte("test"),
WorkerFilter: "test worker filter",
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
"AWS_ACCESS_KEY_ID": structpb.NewStringValue("access_key_id"),
"AWS_SECRET_ACCESS_KEY": structpb.NewStringValue("secret_access_key"),
},
},
}
// first store bucket
var err error
sb.PublicId, err = newStorageBucketId(ctx)
require.NoError(err)
sb.Attributes, err = patchstruct.PatchBytes([]byte{}, sb.Attributes)
require.NoError(err)
require.NoError(rw.Create(ctx, sb))
// now store secret
cred, err := newStorageBucketCredentialManagedSecret(ctx, sb.PublicId, sb.Secrets)
require.NoError(err)
kmsWrapper1, err := kmsCache.GetWrapper(context.Background(), org.GetPublicId(), 1)
require.NoError(err)
require.NoError(cred.encrypt(ctx, kmsWrapper1))
require.NoError(rw.Create(ctx, cred))
assert.NoError(cred.decrypt(ctx, kmsWrapper1))
// now things are stored in the db, we can rotate and rewrap
require.NoError(kmsCache.RotateKeys(ctx, org.Scope.GetPublicId()))
require.NoError(storageBucketCredentialManagedSecretRewrapFn(ctx, cred.KeyId, org.Scope.GetPublicId(), rw, rw, kmsCache))
// now we pull the config back from the db, decrypt it with the new key, and ensure things match
got := &StorageBucketCredentialManagedSecret{
StorageBucketCredentialManagedSecret: &store.StorageBucketCredentialManagedSecret{
StorageBucketId: sb.PublicId,
},
}
assert.NoError(rw.LookupById(ctx, got))
// fetch the new key version
kmsWrapper2, err := kmsCache.GetWrapper(ctx, org.Scope.GetPublicId(), kms.KeyPurposeDatabase, kms.WithKeyId(got.GetKeyId()))
assert.NoError(err)
newKeyVersion, err := kmsWrapper2.KeyId(ctx)
assert.NoError(err)
// decrypt with the new key version and check to make sure things match
assert.NoError(got.decrypt(ctx, kmsWrapper2))
assert.NotEmpty(got.GetKeyId())
assert.NotEqual(cred.GetKeyId(), got.GetKeyId())
assert.Equal(newKeyVersion, got.GetKeyId())
assert.Equal(cred.Secrets, got.GetSecrets())
})
}
+19 -155
View File
@@ -4,18 +4,10 @@
package plugin
import (
"context"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/libs/crypto"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/storage/plugin/store"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/boundary/internal/util"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
)
@@ -83,145 +75,23 @@ func (s *StorageBucket) oplog(op oplog.OpType) oplog.Metadata {
return metadata
}
type StorageBucketCredentialManagedSecret struct {
*store.StorageBucketCredentialManagedSecret
tableName string `gorm:"-"`
}
func allocStorageBucketCredentialManagedSecret() *StorageBucketCredentialManagedSecret {
return &StorageBucketCredentialManagedSecret{
StorageBucketCredentialManagedSecret: &store.StorageBucketCredentialManagedSecret{},
}
}
// newStorageBucketCredentialManagedSecret creates an in memory storage bucket secret from a json
// formatted generic struct.
func newStorageBucketCredentialManagedSecret(ctx context.Context, storageBucketId string, secret *structpb.Struct) (*StorageBucketCredentialManagedSecret, error) {
const op = "plugin.newStorageBucketCredentialManagedSecret"
s := &StorageBucketCredentialManagedSecret{
StorageBucketCredentialManagedSecret: &store.StorageBucketCredentialManagedSecret{
StorageBucketId: storageBucketId,
},
}
if secret != nil {
sec, err := proto.Marshal(secret)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.InvalidParameter))
}
s.Secrets = sec
}
return s, nil
}
func (sbs *StorageBucketCredentialManagedSecret) clone() *StorageBucketCredentialManagedSecret {
cp := proto.Clone(sbs.StorageBucketCredentialManagedSecret)
return &StorageBucketCredentialManagedSecret{
StorageBucketCredentialManagedSecret: cp.(*store.StorageBucketCredentialManagedSecret),
}
}
// TableName returns the table name for the storage bucket secrets.
func (sbs *StorageBucketCredentialManagedSecret) TableName() string {
if sbs.tableName != "" {
return sbs.tableName
}
return "storage_bucket_credential_managed_secret"
}
// SetTableName sets the table name.
func (sbs *StorageBucketCredentialManagedSecret) SetTableName(n string) {
sbs.tableName = n
}
// hmacField simply hmac's a field in a consistent manner for this pkg
func hmacField(ctx context.Context, cipher wrapping.Wrapper, field []byte, publicId string) ([]byte, error) {
const op = "plugin.hmacField"
hm, err := crypto.HmacSha256(ctx, field, cipher, []byte(publicId), nil)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return []byte(hm), nil
}
// only hmac's the secret's value. does not modify the underlying secret
// returns nil on failure
func (sbs *StorageBucketCredentialManagedSecret) hmacSecrets(ctx context.Context, cipher wrapping.Wrapper) ([]byte, error) {
const op = "plugin.(StorageBucketCredentialManagedSecret).hmacSecrets"
if cipher == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
}
var err error
if _, err = cipher.KeyId(ctx); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("failed to read cipher key id"))
}
var hmac []byte
if hmac, err = hmacField(ctx, cipher, sbs.Secrets, sbs.StorageBucketId); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("failed to hmac secrets"))
}
return hmac, nil
}
// encrypt the bind credential before writing it to the database
func (sbs *StorageBucketCredentialManagedSecret) encrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "plugin.(StorageBucketCredentialManagedSecret).encrypt"
if util.IsNil(cipher) {
return errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
}
if err := structwrapping.WrapStruct(ctx, cipher, sbs.StorageBucketCredentialManagedSecret); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt))
}
var err error
if sbs.KeyId, err = cipher.KeyId(ctx); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("failed to read cipher key id"))
}
return nil
}
func (sbs *StorageBucketCredentialManagedSecret) decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "plugin.(StorageBucketCredentialManagedSecret).decrypt"
if util.IsNil(cipher) {
return errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
}
if err := structwrapping.UnwrapStruct(ctx, cipher, sbs.StorageBucketCredentialManagedSecret, nil); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt))
}
sbs.CtSecrets = nil
return nil
}
func (sbs *StorageBucketCredentialManagedSecret) toPersisted(ctx context.Context) (*storagebuckets.StorageBucketPersisted, error) {
const op = "plugin.(StorageBucketCredentialManagedSecret).toPersisted"
if sbs.Secrets == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "secret data not populated")
}
sec := &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{},
}
if err := proto.Unmarshal(sbs.Secrets, sec.Data); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.InvalidParameter))
}
return sec, nil
}
type storageBucketAgg struct {
PublicId string `gorm:"primary_key"`
ScopeId string
Name string
Description string
CreateTime *timestamp.Timestamp
UpdateTime *timestamp.Timestamp
Version uint32
PluginId string
BucketName string
BucketPrefix string
WorkerFilter string
Attributes []byte
SecretsEncrypted []byte
SecretsHmac []byte
KeyId string
PublicId string `gorm:"primary_key"`
ScopeId string
Name string
Description string
CreateTime *timestamp.Timestamp
UpdateTime *timestamp.Timestamp
Version uint32
PluginId string
BucketName string
BucketPrefix string
WorkerFilter string
Attributes []byte
SecretsEncrypted []byte
SecretsHmac []byte
KeyId string
StorageBucketCredentialId string
}
// TableName returns the table name for gorm
@@ -233,7 +103,7 @@ func (sba *storageBucketAgg) GetPublicId() string {
return sba.PublicId
}
func (sba *storageBucketAgg) toStorageBucketAndSecret() (*StorageBucket, *StorageBucketCredentialManagedSecret) {
func (sba *storageBucketAgg) toStorageBucketAndSBC() (*StorageBucket, error) {
sb := allocStorageBucket()
sb.PublicId = sba.PublicId
sb.ScopeId = sba.ScopeId
@@ -248,15 +118,9 @@ func (sba *storageBucketAgg) toStorageBucketAndSecret() (*StorageBucket, *Storag
sb.WorkerFilter = sba.WorkerFilter
sb.Attributes = sba.Attributes
sb.SecretsHmac = sba.SecretsHmac
sb.StorageBucketCredentialId = sba.StorageBucketCredentialId
var sbs *StorageBucketCredentialManagedSecret
if len(sba.SecretsEncrypted) > 0 {
sbs = allocStorageBucketCredentialManagedSecret()
sbs.StorageBucketId = sba.PublicId
sbs.CtSecrets = sba.SecretsEncrypted
sbs.KeyId = sba.KeyId
}
return sb, sbs
return sb, nil
}
type deletedStorageBucket struct {
@@ -0,0 +1,14 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package environmental
// CreateStorageBucketCredentialEnvironmentalQuery is the query to create a storage bucket credential environmental
const CreateStorageBucketCredentialEnvironmentalQuery = `
insert into storage_bucket_credential_environmental (
storage_bucket_id
) values (
@storage_bucket_id -- storage_bucket_id
)
returning *;
`
@@ -0,0 +1,37 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package environmental
import (
"context"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/storage/storagebucketcredential"
)
type sbcHooks struct{}
func init() {
storagebucketcredential.Register(storagebucketcredential.EnvironmentalSubtype, sbcHooks{})
}
// Vet validates that the given storagebucketcredential.StorageBucketCredential is a environmental.StorageBucketCredential
// and that it has a StorageBucketCredential store.
func (h sbcHooks) Vet(ctx context.Context, sbc storagebucketcredential.StorageBucketCredential) error {
const op = "environmental.vet"
sbce, ok := sbc.(*StorageBucketCredential)
if !ok {
return errors.New(ctx, errors.InvalidParameter, op, "StorageBucketCredential is not a sbce.StorageBucketCredential")
}
if sbce == nil {
return errors.New(ctx, errors.InvalidParameter, op, "missing storage bucket credential")
}
if sbce.StorageBucketCredentialEnvironmental == nil {
return errors.New(ctx, errors.InvalidParameter, op, "missing storage bucket credential store")
}
return nil
}
@@ -0,0 +1,152 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
// Package environmental provides a SBC subtype for a Environmental SBC.
// Importing this package will register it with the SBC package and
// allow the sbc.Repository to support environmental.StorageBucketCredential.
package environmental
import (
"context"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/storage/plugin/store"
"github.com/hashicorp/boundary/internal/storage/storagebucketcredential"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"google.golang.org/protobuf/proto"
)
const (
defaultTableName = "storage_bucket_credential_environmental"
)
// StorageBucketCredential is a resources that represents an Environmental SBC
// It is a subtype of storagebucketcredential.StorageBucketCredential.
type StorageBucketCredential struct {
*store.StorageBucketCredentialEnvironmental
tableName string `gorm:"-"`
}
// Ensure StorageBucketCredential implements interfaces
var (
_ storagebucketcredential.StorageBucketCredential = (*StorageBucketCredential)(nil)
_ db.VetForWriter = (*StorageBucketCredential)(nil)
)
// NewStorageBucketCredential creates a new in memory environmental SBC.
func (h sbcHooks) NewStorageBucketCredential(
ctx context.Context,
storageBucketId string,
_ ...storagebucketcredential.Option,
) (storagebucketcredential.StorageBucketCredential, error) {
const op = "environmental.NewStorageBucketCredential"
if storageBucketId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing storage bucket id")
}
sbc := &StorageBucketCredential{
StorageBucketCredentialEnvironmental: &store.StorageBucketCredentialEnvironmental{
StorageBucketId: storageBucketId,
},
}
return sbc, nil
}
// AllocStorageBucketCredential will allocate a new StorageBucketCredential
func (h sbcHooks) AllocStorageBucketCredential() storagebucketcredential.StorageBucketCredential {
return &StorageBucketCredential{
StorageBucketCredentialEnvironmental: &store.StorageBucketCredentialEnvironmental{},
}
}
// Clone creates a clone of the StorageBucketCredential
func (sbc *StorageBucketCredential) Clone() storagebucketcredential.StorageBucketCredential {
cp := proto.Clone(sbc.StorageBucketCredentialEnvironmental)
return &StorageBucketCredential{
StorageBucketCredentialEnvironmental: cp.(*store.StorageBucketCredentialEnvironmental),
}
}
// VetForWrite implements db.VetForWrite() interface and validates the environmental SBC
// before it's written.
func (sbc *StorageBucketCredential) VetForWrite(ctx context.Context, _ db.Reader, opType db.OpType, _ ...db.Option) error {
const op = "environmental.(StorageBucketCredential).VetForWrite"
if sbc.StorageBucketId == "" {
return errors.New(ctx, errors.InvalidParameter, op, "missing storage bucket id")
}
return nil
}
// TableName returns the tablename to override the default gorm table name
func (sbc *StorageBucketCredential) TableName() string {
if sbc.tableName != "" {
return sbc.tableName
}
return defaultTableName
}
// SetTableName sets the tablename and satisfies the ReplayableMessage
// interface. If the caller attempts to set the name to "" the name will be
// reset to the default name.
func (sbc *StorageBucketCredential) SetTableName(n string) {
sbc.tableName = n
}
func (sbc *StorageBucketCredential) SetPrivateId(privateId string) {
sbc.PrivateId = privateId
}
func (sbc *StorageBucketCredential) SetStorageBucketId(storageBucketId string) {
sbc.StorageBucketId = storageBucketId
}
func (sbc *StorageBucketCredential) SetCtSecrets(_ []byte) {}
func (sbc *StorageBucketCredential) SetSecrets(_ []byte) {}
func (sbc *StorageBucketCredential) SetKeyId(_ string) {}
func (sbc *StorageBucketCredential) GetPrivateId() string {
return sbc.PrivateId
}
func (sbc *StorageBucketCredential) GetType() storagebucketcredential.Subtype {
return storagebucketcredential.EnvironmentalSubtype
}
func (sbc *StorageBucketCredential) GetCtSecrets() []byte {
return nil
}
func (sbc *StorageBucketCredential) GetSecrets() []byte {
return nil
}
func (sbc *StorageBucketCredential) GetKeyId() string {
return ""
}
func (sbc *StorageBucketCredential) CreateDBQuery() string {
return CreateStorageBucketCredentialEnvironmentalQuery
}
func (sbc *StorageBucketCredential) HmacSecrets(ctx context.Context, _ wrapping.Wrapper) ([]byte, error) {
const op = "environmental.(StorageBucketCredential).HmacSecrets"
return nil, errors.New(ctx, errors.Unknown, op, "HmacSecrets not implemented")
}
func (sbc *StorageBucketCredential) Encrypt(ctx context.Context, _ wrapping.Wrapper) error {
const op = "environmental.(StorageBucketCredential).Encrypt"
return errors.New(ctx, errors.Unknown, op, "Encrypt not implemented")
}
func (sbc *StorageBucketCredential) Decrypt(ctx context.Context, _ wrapping.Wrapper) error {
const op = "environmental.(StorageBucketCredential).Decrypt"
return errors.New(ctx, errors.Unknown, op, "Decrypt not implemented")
}
func (sbc *StorageBucketCredential) ToPersisted(ctx context.Context) (*storagebuckets.StorageBucketPersisted, error) {
const op = "environmental.(StorageBucketCredential).ToPersisted"
return nil, errors.New(ctx, errors.Unknown, op, "ToPersisted not implemented")
}
@@ -0,0 +1,18 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package managedsecret
// CreateStorageBucketCredentialManagedSecretQuery is the query to create a storage bucket credential managed secret
const CreateStorageBucketCredentialManagedSecretQuery = `
insert into storage_bucket_credential_managed_secret (
storage_bucket_id,
secrets_encrypted,
key_id
) values (
@storage_bucket_id, -- storage_bucket_id
@secrets_encrypted, -- secrets_encrypted
@key_id -- key_id
)
returning *;
`
@@ -0,0 +1,37 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package managedsecret
import (
"context"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/storage/storagebucketcredential"
)
type sbcHooks struct{}
func init() {
storagebucketcredential.Register(storagebucketcredential.ManagedSecretSubtype, sbcHooks{})
}
// Vet validates that the given storagebucketcredential.StorageBucketCredential is a managedsecret.StorageBucketCredential
// and that it has a StorageBucketCredential store.
func (h sbcHooks) Vet(ctx context.Context, sbc storagebucketcredential.StorageBucketCredential) error {
const op = "managedsecret.vet"
sbcms, ok := sbc.(*StorageBucketCredential)
if !ok {
return errors.New(ctx, errors.InvalidParameter, op, "StorageBucketCredential is not a sbcms.StorageBucketCredential")
}
if sbcms == nil {
return errors.New(ctx, errors.InvalidParameter, op, "missing storage bucket credential")
}
if sbcms.StorageBucketCredentialManagedSecret == nil {
return errors.New(ctx, errors.InvalidParameter, op, "missing storage bucket credential store")
}
return nil
}
@@ -0,0 +1,229 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
// Package managedsecret provides a SBC subtype for a Managed Secret SBC.
// Importing this package will register it with the SBC package and
// allow the sbc.Repository to support managedsecret.StorageBucketCredential.
package managedsecret
import (
"context"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/libs/crypto"
"github.com/hashicorp/boundary/internal/storage/plugin/store"
"github.com/hashicorp/boundary/internal/storage/storagebucketcredential"
"github.com/hashicorp/boundary/internal/util"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
)
const (
defaultTableName = "storage_bucket_credential_managed_secret"
)
// StorageBucketCredential is a resources that represents an Managed Secret SBC
// It is a subtype of storagebucketcredential.StorageBucketCredential.
type StorageBucketCredential struct {
*store.StorageBucketCredentialManagedSecret
tableName string `gorm:"-"`
}
// Ensure StorageBucketCredential implements interfaces
var (
_ storagebucketcredential.StorageBucketCredential = (*StorageBucketCredential)(nil)
_ db.VetForWriter = (*StorageBucketCredential)(nil)
)
// NewStorageBucketCredential creates a new in memory managed secret SBC.
func (h sbcHooks) NewStorageBucketCredential(
ctx context.Context,
storageBucketId string,
opt ...storagebucketcredential.Option,
) (storagebucketcredential.StorageBucketCredential, error) {
const op = "managedsecret.NewStorageBucketCredential"
opts := storagebucketcredential.GetOpts(opt...)
if storageBucketId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing storage bucket id")
}
if opts.WithSecret == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing secret")
}
var secrets []byte
if opts.WithSecret != nil {
var err error
secrets, err = proto.Marshal(opts.WithSecret)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to marshal secrets"))
}
}
sbc := &StorageBucketCredential{
StorageBucketCredentialManagedSecret: &store.StorageBucketCredentialManagedSecret{
StorageBucketId: storageBucketId,
Secrets: secrets,
KeyId: opts.WithKeyId,
},
}
return sbc, nil
}
// AllocStorageBucketCredential will allocate a new StorageBucketCredential
func (h sbcHooks) AllocStorageBucketCredential() storagebucketcredential.StorageBucketCredential {
return &StorageBucketCredential{
StorageBucketCredentialManagedSecret: &store.StorageBucketCredentialManagedSecret{},
}
}
// Clone creates a clone of the StorageBucketCredential
func (sbc *StorageBucketCredential) Clone() storagebucketcredential.StorageBucketCredential {
cp := proto.Clone(sbc.StorageBucketCredentialManagedSecret)
return &StorageBucketCredential{
StorageBucketCredentialManagedSecret: cp.(*store.StorageBucketCredentialManagedSecret),
}
}
func (sbc *StorageBucketCredential) CreateDBQuery() string {
return CreateStorageBucketCredentialManagedSecretQuery
}
// VetForWrite implements db.VetForWrite() interface and validates the managed secret SBC
// before it's written.
func (sbc *StorageBucketCredential) VetForWrite(ctx context.Context, _ db.Reader, opType db.OpType, _ ...db.Option) error {
const op = "managedsecret.(StorageBucketCredential).VetForWrite"
if sbc.StorageBucketId == "" {
return errors.New(ctx, errors.InvalidParameter, op, "missing storage bucket id")
}
return nil
}
// hmacField simply hmac's a field in a consistent manner for this pkg
func hmacField(ctx context.Context, cipher wrapping.Wrapper, field []byte, publicId string) ([]byte, error) {
const op = "plugin.hmacField"
hm, err := crypto.HmacSha256(ctx, field, cipher, []byte(publicId), nil)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return []byte(hm), nil
}
// only hmac's the secret's value. does not modify the underlying secret
// returns nil on failure
func (sbc *StorageBucketCredential) HmacSecrets(ctx context.Context, cipher wrapping.Wrapper) ([]byte, error) {
const op = "managedsecret.(StorageBucketCredential).hmacSecrets"
if cipher == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
}
var err error
if _, err = cipher.KeyId(ctx); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("failed to read cipher key id"))
}
var hmac []byte
if hmac, err = hmacField(ctx, cipher, sbc.Secrets, sbc.StorageBucketId); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("failed to hmac secrets"))
}
return hmac, nil
}
// encrypt the bind credential before writing it to the database
func (sbc *StorageBucketCredential) Encrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "managedsecret.(StorageBucketCredential)).encrypt"
if util.IsNil(cipher) {
return errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
}
if err := structwrapping.WrapStruct(ctx, cipher, sbc.StorageBucketCredentialManagedSecret); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt))
}
var err error
if sbc.KeyId, err = cipher.KeyId(ctx); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("failed to read cipher key id"))
}
return nil
}
func (sbc *StorageBucketCredential) Decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "managedsecret.(StorageBucketCredential).decrypt"
if util.IsNil(cipher) {
return errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
}
if err := structwrapping.UnwrapStruct(ctx, cipher, sbc.StorageBucketCredentialManagedSecret, nil); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt))
}
sbc.CtSecrets = nil
return nil
}
func (sbc *StorageBucketCredential) ToPersisted(ctx context.Context) (*storagebuckets.StorageBucketPersisted, error) {
const op = "plugin.(StorageBucketCredentialManagedSecret).toPersisted"
if sbc.Secrets == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "secret data not populated")
}
sec := &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{},
}
if err := proto.Unmarshal(sbc.Secrets, sec.Data); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.InvalidParameter))
}
return sec, nil
}
// TableName returns the tablename to override the default gorm table name
func (sbc *StorageBucketCredential) TableName() string {
if sbc.tableName != "" {
return sbc.tableName
}
return defaultTableName
}
// SetTableName sets the tablename and satisfies the ReplayableMessage
// interface. If the caller attempts to set the name to "" the name will be
// reset to the default name.
func (sbc *StorageBucketCredential) SetTableName(n string) {
sbc.tableName = n
}
func (sbc *StorageBucketCredential) SetPrivateId(privateId string) {
sbc.PrivateId = privateId
}
func (sbc *StorageBucketCredential) SetStorageBucketId(storageBucketId string) {
sbc.StorageBucketId = storageBucketId
}
func (sbc *StorageBucketCredential) SetCtSecrets(s []byte) {
sbc.CtSecrets = s
}
func (sbc *StorageBucketCredential) SetSecrets(s []byte) {
sbc.Secrets = s
}
func (sbc *StorageBucketCredential) SetKeyId(s string) {
sbc.KeyId = s
}
func (sbc *StorageBucketCredential) GetPrivateId() string {
return sbc.PrivateId
}
func (sbc *StorageBucketCredential) GetType() storagebucketcredential.Subtype {
return storagebucketcredential.ManagedSecretSubtype
}
func (sbc *StorageBucketCredential) GetCtSecrets() []byte {
return sbc.CtSecrets
}
func (sbc *StorageBucketCredential) GetSecrets() []byte {
return sbc.Secrets
}
func (sbc *StorageBucketCredential) GetKeyId() string {
return sbc.KeyId
}
@@ -0,0 +1,45 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package storagebucketcredential
import "google.golang.org/protobuf/types/known/structpb"
// GetOpts - iterate the inbound Options and return a struct
func GetOpts(opt ...Option) options {
opts := getDefaultOptions()
for _, o := range opt {
o(&opts)
}
return opts
}
// Option - how Options are passed as arguments
type Option func(*options)
// options = how options are represented
type options struct {
WithSecret *structpb.Struct
WithKeyId string
}
func getDefaultOptions() options {
return options{
WithSecret: nil,
WithKeyId: "",
}
}
// WithSecret provides an optional secret
func WithSecret(s *structpb.Struct) Option {
return func(o *options) {
o.WithSecret = s
}
}
// WithKeyId provides an key id
func WithKeyId(s string) Option {
return func(o *options) {
o.WithKeyId = s
}
}
@@ -0,0 +1,42 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package storagebucketcredential
import (
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/structpb"
)
// Test_GetOpts provides unit tests for GetOpts and all the options
func Test_GetOpts(t *testing.T) {
t.Parallel()
t.Run("WithSecret", func(t *testing.T) {
assert := assert.New(t)
opts := GetOpts(WithSecret(
&structpb.Struct{
Fields: map[string]*structpb.Value{
"ACCESS_KEY_ID": structpb.NewStringValue("access_key_id"),
"SECRET_ACCESS_KEY": structpb.NewStringValue("secret_access_key"),
},
},
))
testOpts := getDefaultOptions()
testOpts.WithSecret = &structpb.Struct{
Fields: map[string]*structpb.Value{
"ACCESS_KEY_ID": structpb.NewStringValue("access_key_id"),
"SECRET_ACCESS_KEY": structpb.NewStringValue("secret_access_key"),
},
}
assert.Equal(opts, testOpts)
})
t.Run("WithKeyId", func(t *testing.T) {
assert := assert.New(t)
opts := GetOpts(WithKeyId("test-key-id"))
testOpts := getDefaultOptions()
testOpts.WithKeyId = "test-key-id"
assert.Equal(opts, testOpts)
})
}
@@ -0,0 +1,126 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package storagebucketcredential
import (
"context"
"fmt"
"sync"
"github.com/hashicorp/boundary/internal/errors"
)
const Domain = "storage-bucket-credential"
// NewFunc is a function that creates a storage bucket credential with the provided storage bucket Id,
// and options.
type NewFunc func(ctx context.Context, storageBucketId string, opt ...Option) (StorageBucketCredential, error)
// AllocFunc is a function that creates an in-memory storage bucket credential.
type AllocFunc func() StorageBucketCredential
// VetFunc is a function that checks the given storage bucket credential to ensure it can
// be used by the Repository.
type VetFunc func(context.Context, StorageBucketCredential) error
// VetForUpdateFunc is a function that checks the given StorageBucketCredential and field mask
// paths are valid and be used to update a storage bucket credential in the Repository.
type VetForUpdateFunc func(context.Context, StorageBucketCredential, []string) error
// sbcHooks defines the interface containing all the hooks needed for
// managing storage bucket credential suptypes.
type sbcHooks interface {
// NewStorageBucketCredential creates a new in memory storage bucket credential.
NewStorageBucketCredential(ctx context.Context, storageBucketId string, opt ...Option) (StorageBucketCredential, error)
// AllocStorageBucketCredential will allocate an empty storage bucket credential.
AllocStorageBucketCredential() StorageBucketCredential
// Vet validates that the given StorageBucketCredential has the proper fields and values
// for creation in the database for this type of storage bucket credential.
Vet(ctx context.Context, t StorageBucketCredential) error
}
type registryEntry struct {
sbcHooks sbcHooks
}
type Registry struct {
m map[Subtype]*registryEntry
sync.RWMutex
}
func (r *Registry) set(s Subtype, entry *registryEntry) {
r.Lock()
defer r.Unlock()
_, previouslySet := r.m[s]
if previouslySet {
panic(fmt.Sprintf("storage bucket credential subtype %s already registered", s))
}
r.m[s] = entry
}
func (r *Registry) get(s Subtype) (*registryEntry, bool) {
r.RLock()
defer r.RUnlock()
entry, ok := r.m[s]
if ok {
return entry, ok
}
return nil, ok
}
func (r *Registry) NewFunc(s Subtype) (NewFunc, bool) {
entry, ok := r.get(s)
if !ok {
return nil, ok
}
return entry.sbcHooks.NewStorageBucketCredential, ok
}
func (r *Registry) AllocFunc(s Subtype) (AllocFunc, bool) {
entry, ok := r.get(s)
if !ok {
return nil, ok
}
return entry.sbcHooks.AllocStorageBucketCredential, ok
}
func (r *Registry) VetFunc(s Subtype) (VetFunc, bool) {
entry, ok := r.get(s)
if !ok {
return nil, ok
}
return entry.sbcHooks.Vet, ok
}
var SubtypeRegistry = Registry{
m: make(map[Subtype]*registryEntry),
}
// SubtypeFromType returns the Subtype from the provided string or if
// no Subtype was registered with that string Unknown is returned.
func SubtypeFromType(t string) Subtype {
return Subtype(t)
}
// New creates a StorageBucketCredential of the given subtype and storageBucketId.
func New(ctx context.Context, subtype Subtype, storageBucketId string, opt ...Option) (StorageBucketCredential, error) {
const op = "storagebucketcredential.New"
nf, ok := SubtypeRegistry.NewFunc(subtype)
if !ok {
return nil, errors.New(ctx, errors.InvalidParameter, op, "unsupported subtype")
}
return nf(ctx, storageBucketId, opt...)
}
// Register registers repository hooks for a provided Subtype. Register
// panics if the subtype has already been registered or if any of the
// prefixes are associated with another subtype.
func Register(s Subtype, sh sbcHooks) {
SubtypeRegistry.set(s, &registryEntry{
sbcHooks: sh,
})
}
@@ -0,0 +1,128 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package storagebucketcredential
import (
"context"
goerrs "errors"
"fmt"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
)
// StorageBucketCredential is a commmon interface for all storage bucket credential subtypes
type StorageBucketCredential interface {
HmacSecrets(context.Context, wrapping.Wrapper) ([]byte, error)
Encrypt(context.Context, wrapping.Wrapper) error
Decrypt(context.Context, wrapping.Wrapper) error
ToPersisted(context.Context) (*storagebuckets.StorageBucketPersisted, error)
Clone() StorageBucketCredential
CreateDBQuery() string
GetPrivateId() string
GetStorageBucketId() string
GetType() Subtype
GetSecrets() []byte
GetCtSecrets() []byte
GetKeyId() string
SetPrivateId(string)
SetStorageBucketId(string)
SetSecrets([]byte)
SetCtSecrets([]byte)
SetKeyId(string)
}
const (
sbcDefaultTable = "storage_bucket_credential_all_subtypes"
)
type Subtype string
const (
UnknownSubtype Subtype = "unknown"
ManagedSecretSubtype Subtype = "managed_secret"
EnvironmentalSubtype Subtype = "environmental"
)
var errSBCSubtypeNotFound = goerrs.New("storage bucket credential subtype not found")
// storageBucketCredential provides a common way to return storage bucket credentials
// regardless of their underlying type.
type storageBucketCredential struct {
// tableName is the name of the table in the database
tableName string `gorm:"-"`
// PrivateId is a surrogate key suitable for use via API.
PrivateId string `gorm:"primary_key"`
// StorageBucketId is the public id of the storage bucket.
StorageBucketId string `gorm:"not_null"`
// Secrets is the plain-text of the secret data. We are not storing this plain-text
// value in the database.
Secrets []byte `gorm:"-" wrapping:"pt,secrets_data"`
// CtSecrets is the ciphertext of the secret data stored in the db.
CtSecrets []byte `gorm:"column:secrets_encrypted;not_null" wrapping:"ct,secrets_data"`
// The KeyId of the kms database key used for encrypting this entry.
// It must be set.
KeyId string `gorm:"not_null"`
// Type represents the type of the Storage Bucket Credential
Type string `gorm:"default:null"`
}
// AllocStorageBucketCredential will allocate a storage bucket credential
func AllocStorageBucketCredential() *storageBucketCredential {
return &storageBucketCredential{}
}
// TableName provides an overridden gorm table name for storage bucket credentials.
func (sbc *storageBucketCredential) TableName() string {
if sbc.tableName != "" {
return sbc.tableName
}
return sbcDefaultTable
}
// SetTableName sets the table name for the resource. If the caller attempts to
// set the name to "" the name will be reset to the default name.
func (sbc *storageBucketCredential) SetTableName(n string) {
switch n {
case "":
sbc.tableName = sbcDefaultTable
default:
sbc.tableName = n
}
}
func (sbc *storageBucketCredential) Subtype() Subtype {
return Subtype(sbc.Type)
}
// SBCSubtype converts the storage bucket credential to the concrete subtype
func (sbc *storageBucketCredential) SBCSubtype(ctx context.Context) (StorageBucketCredential, error) {
const op = "storagebucketcredential.storageBucketCredential.sbcSubtype"
alloc, ok := SubtypeRegistry.AllocFunc(sbc.Subtype())
if !ok {
return nil, errors.Wrap(ctx,
errSBCSubtypeNotFound,
op,
errors.WithCode(errors.InvalidParameter),
errors.WithMsg(fmt.Sprintf("%s is an unknown sbc subtype of %s", sbc.PrivateId, sbc.Type)),
)
}
aSBC := alloc()
aSBC.SetPrivateId(sbc.PrivateId)
aSBC.SetStorageBucketId(sbc.StorageBucketId)
if sbc.Subtype() == ManagedSecretSubtype {
secrets := &structpb.Struct{}
if err := proto.Unmarshal(sbc.Secrets, secrets); err != nil {
return nil, err
}
aSBC.SetSecrets(sbc.Secrets)
aSBC.SetCtSecrets(sbc.CtSecrets)
}
return aSBC, nil
}
@@ -0,0 +1,33 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package storagebucketcredential
import (
"github.com/hashicorp/boundary/internal/server/store"
)
// NewWorkerStorageBucketCredentialState returns a new WorkerStorageBucketCredentialState.
func NewWorkerStorageBucketCredentialState() *WorkerStorageBucketCredentialState {
return &WorkerStorageBucketCredentialState{
WorkerStorageBucketCredentialState: &store.WorkerStorageBucketCredentialState{},
}
}
type WorkerStorageBucketCredentialState struct {
*store.WorkerStorageBucketCredentialState
tableName string `gorm:"-"`
}
// TableName returns the table name.
func (sbc *WorkerStorageBucketCredentialState) TableName() string {
if sbc.tableName != "" {
return sbc.tableName
}
return "worker_storage_bucket_credential_state"
}
// SetTableName sets the table name.
func (sbc *WorkerStorageBucketCredentialState) SetTableName(n string) {
sbc.tableName = n
}