Secrets: changes to allow a 3rd party keeper / secret references (#115156)
* Secrets: changes to allow a 3rd party keeper / secret references * fix test * make gofmt * lint * fix tests * assign aws secrets manager to @grafana/grafana-operator-experience-squad * rename Keeper.Reference to Keeper.RetrieveReference * rename ModelSecretsManager to ModelAWSSecretsManager * validator: ensure that only one of keeper.Spec.Aws.AccessKey or keeper.Spec.Aws.AssumeRole are set * move secrets manager dep / go mod tidy * move secrets manager dep * keeper validator: move 3rd party secret stores validation to their own functions * add github.com/aws/aws-sdk-go-v2/service/secretsmanager pkg/extensions/enterprise_imports * make update-workspace * undo go.mod changes in /apps * make update-workspace * fix test * add github.com/aws/aws-sdk-go-v2/service/secretsmanager to enterprise_imports * make update-workspace * gcworker: handle refs * make update-workspace * create toggle: FeatureStageExperimental * allow features.IsEnabled for now * format
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/utils/ptr"
|
||||
"pgregory.net/rapid"
|
||||
)
|
||||
|
||||
var (
|
||||
DecryptersGen = rapid.SampledFrom([]string{"svc1", "svc2", "svc3", "svc4", "svc5"})
|
||||
SecureValueNameGen = rapid.SampledFrom([]string{"n1", "n2", "n3", "n4", "n5"})
|
||||
KeeperNameGen = rapid.SampledFrom([]string{"k1", "k2", "k3", "k4", "k5"})
|
||||
NamespaceGen = rapid.SampledFrom([]string{"ns1", "ns2", "ns3", "ns4", "ns5"})
|
||||
SecretsToRefGen = rapid.SampledFrom([]string{"ref1", "ref2", "ref3", "ref4", "ref5"})
|
||||
// Generator for secure values that specify a secret value
|
||||
AnySecureValueGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.SecureValue {
|
||||
return &secretv1beta1.SecureValue{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: SecureValueNameGen.Draw(t, "name"),
|
||||
Namespace: NamespaceGen.Draw(t, "ns"),
|
||||
},
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Description: rapid.SampledFrom([]string{"d1", "d2", "d3", "d4", "d5"}).Draw(t, "description"),
|
||||
Value: ptr.To(secretv1beta1.NewExposedSecureValue(rapid.SampledFrom([]string{"v1", "v2", "v3", "v4", "v5"}).Draw(t, "value"))),
|
||||
Decrypters: rapid.SliceOfDistinct(DecryptersGen, func(v string) string { return v }).Draw(t, "decrypters"),
|
||||
},
|
||||
Status: secretv1beta1.SecureValueStatus{},
|
||||
}
|
||||
})
|
||||
// Generator for secure values that reference values from 3rd party stores
|
||||
AnySecureValueWithRefGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.SecureValue {
|
||||
return &secretv1beta1.SecureValue{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: SecureValueNameGen.Draw(t, "name"),
|
||||
Namespace: NamespaceGen.Draw(t, "ns"),
|
||||
},
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Description: rapid.SampledFrom([]string{"d1", "d2", "d3", "d4", "d5"}).Draw(t, "description"),
|
||||
Ref: ptr.To(SecretsToRefGen.Draw(t, "ref")),
|
||||
Decrypters: rapid.SliceOfDistinct(DecryptersGen, func(v string) string { return v }).Draw(t, "decrypters"),
|
||||
},
|
||||
Status: secretv1beta1.SecureValueStatus{},
|
||||
}
|
||||
})
|
||||
UpdateSecureValueGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.SecureValue {
|
||||
sv := AnySecureValueGen.Draw(t, "sv")
|
||||
// Maybe update the secret value, maybe not
|
||||
if !rapid.Bool().Draw(t, "should_update_value") {
|
||||
sv.Spec.Value = nil
|
||||
}
|
||||
return sv
|
||||
})
|
||||
DecryptGen = rapid.Custom(func(t *rapid.T) DecryptInput {
|
||||
return DecryptInput{
|
||||
Namespace: NamespaceGen.Draw(t, "ns"),
|
||||
Name: SecureValueNameGen.Draw(t, "name"),
|
||||
Decrypter: DecryptersGen.Draw(t, "decrypter"),
|
||||
}
|
||||
})
|
||||
AnyKeeperGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.Keeper {
|
||||
spec := secretv1beta1.KeeperSpec{
|
||||
Description: rapid.String().Draw(t, "description"),
|
||||
}
|
||||
|
||||
keeperType := rapid.SampledFrom([]string{"isAwsKeeper", "isAzureKeeper", "isGcpKeeper", "isVaultKeeper"}).Draw(t, "keeperType")
|
||||
switch keeperType {
|
||||
case "isAwsKeeper":
|
||||
spec.Aws = &secretv1beta1.KeeperAWSConfig{}
|
||||
case "isAzureKeeper":
|
||||
spec.Azure = &secretv1beta1.KeeperAzureConfig{}
|
||||
case "isGcpKeeper":
|
||||
spec.Gcp = &secretv1beta1.KeeperGCPConfig{}
|
||||
case "isVaultKeeper":
|
||||
spec.HashiCorpVault = &secretv1beta1.KeeperHashiCorpConfig{}
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled keeper type '%+v', did you forget a switch case?", keeperType))
|
||||
}
|
||||
|
||||
return &secretv1beta1.Keeper{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: KeeperNameGen.Draw(t, "name"),
|
||||
Namespace: NamespaceGen.Draw(t, "ns"),
|
||||
},
|
||||
Spec: spec,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
type DecryptInput struct {
|
||||
Namespace string
|
||||
Name string
|
||||
Decrypter string
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
|
||||
"github.com/grafana/grafana/apps/secret/pkg/decrypt"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
)
|
||||
|
||||
type ModelSecureValue struct {
|
||||
*secretv1beta1.SecureValue
|
||||
active bool
|
||||
created time.Time
|
||||
leaseCreated time.Time
|
||||
}
|
||||
|
||||
type ModelKeeper struct {
|
||||
namespace string
|
||||
name string
|
||||
active bool
|
||||
keeperType secretv1beta1.KeeperType
|
||||
}
|
||||
|
||||
// A simplified in memoruy model of the grafana secrets manager
|
||||
type ModelGsm struct {
|
||||
SecureValues []*ModelSecureValue
|
||||
Keepers []*ModelKeeper
|
||||
modelSecretsManager *ModelAWSSecretsManager
|
||||
}
|
||||
|
||||
func NewModelGsm(modelSecretsManager *ModelAWSSecretsManager) *ModelGsm {
|
||||
return &ModelGsm{modelSecretsManager: modelSecretsManager}
|
||||
}
|
||||
|
||||
func (m *ModelGsm) getNewVersionNumber(namespace, name string) int64 {
|
||||
latestVersion := int64(0)
|
||||
for _, sv := range m.SecureValues {
|
||||
if sv.Namespace == namespace && sv.Name == name {
|
||||
latestVersion = max(latestVersion, sv.Status.Version)
|
||||
}
|
||||
}
|
||||
return latestVersion + 1
|
||||
}
|
||||
|
||||
func (m *ModelGsm) SetVersionToActive(namespace, name string, version int64) {
|
||||
for _, sv := range m.SecureValues {
|
||||
if sv.Namespace == namespace && sv.Name == name {
|
||||
sv.active = sv.Status.Version == version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ModelGsm) SetVersionToInactive(namespace, name string, version int64) {
|
||||
for _, sv := range m.SecureValues {
|
||||
if sv.Namespace == namespace && sv.Name == name && sv.Status.Version == version {
|
||||
sv.active = false
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ModelGsm) ReadActiveVersion(namespace, name string) *ModelSecureValue {
|
||||
for _, sv := range m.SecureValues {
|
||||
if sv.Namespace == namespace && sv.Name == name && sv.active {
|
||||
return sv
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) Create(now time.Time, sv *secretv1beta1.SecureValue) (*secretv1beta1.SecureValue, error) {
|
||||
keeper := m.getActiveKeeper(sv.Namespace)
|
||||
|
||||
if sv.Spec.Ref != nil && keeper.keeperType == secretv1beta1.SystemKeeperType {
|
||||
return nil, contracts.ErrReferenceWithSystemKeeper
|
||||
}
|
||||
|
||||
sv = sv.DeepCopy()
|
||||
|
||||
// Preserve the original creation time if this secure value already exists
|
||||
created := now
|
||||
if sv := m.ReadActiveVersion(sv.Namespace, sv.Name); sv != nil {
|
||||
created = sv.created
|
||||
}
|
||||
|
||||
modelSv := &ModelSecureValue{SecureValue: sv, active: false, created: created}
|
||||
modelSv.Status.Version = m.getNewVersionNumber(modelSv.Namespace, modelSv.Name)
|
||||
modelSv.Status.ExternalID = fmt.Sprintf("%d", modelSv.Status.Version)
|
||||
modelSv.Status.Keeper = keeper.name
|
||||
m.SecureValues = append(m.SecureValues, modelSv)
|
||||
m.SetVersionToActive(modelSv.Namespace, modelSv.Name, modelSv.Status.Version)
|
||||
return modelSv.SecureValue, nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) getActiveKeeper(namespace string) *ModelKeeper {
|
||||
for _, k := range m.Keepers {
|
||||
if k.namespace == namespace && k.active {
|
||||
return k
|
||||
}
|
||||
}
|
||||
|
||||
// Default to the system keeper when there are no active keepers in the namespace
|
||||
return &ModelKeeper{
|
||||
namespace: namespace,
|
||||
name: contracts.SystemKeeperName,
|
||||
active: true,
|
||||
keeperType: secretv1beta1.SystemKeeperType,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ModelGsm) keeperExists(namespace, name string) bool {
|
||||
return m.findKeeper(namespace, name) != nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) findKeeper(namespace, name string) *ModelKeeper {
|
||||
// The system keeper is not in the list of keepers
|
||||
if name == contracts.SystemKeeperName {
|
||||
return &ModelKeeper{namespace: namespace, name: contracts.SystemKeeperName, active: true, keeperType: secretv1beta1.SystemKeeperType}
|
||||
}
|
||||
for _, k := range m.Keepers {
|
||||
if k.namespace == namespace && k.name == name {
|
||||
return k
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) CreateKeeper(keeper *secretv1beta1.Keeper) (*secretv1beta1.Keeper, error) {
|
||||
if m.keeperExists(keeper.Namespace, keeper.Name) {
|
||||
return nil, contracts.ErrKeeperAlreadyExists
|
||||
}
|
||||
|
||||
var keeperType secretv1beta1.KeeperType
|
||||
switch {
|
||||
case keeper.Spec.Aws != nil:
|
||||
keeperType = secretv1beta1.AWSKeeperType
|
||||
case keeper.Spec.Gcp != nil:
|
||||
keeperType = secretv1beta1.GCPKeeperType
|
||||
case keeper.Spec.Azure != nil:
|
||||
keeperType = secretv1beta1.AzureKeeperType
|
||||
case keeper.Spec.HashiCorpVault != nil:
|
||||
keeperType = secretv1beta1.HashiCorpKeeperType
|
||||
default:
|
||||
keeperType = secretv1beta1.SystemKeeperType
|
||||
}
|
||||
|
||||
m.Keepers = append(m.Keepers, &ModelKeeper{namespace: keeper.Namespace, name: keeper.Name, keeperType: keeperType})
|
||||
|
||||
return keeper.DeepCopy(), nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) SetKeeperAsActive(namespace, keeperName string) error {
|
||||
// Set every other keeper in the namespace as inactive
|
||||
for _, k := range m.Keepers {
|
||||
if k.namespace == namespace {
|
||||
k.active = k.name == keeperName
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) Update(now time.Time, newSecureValue *secretv1beta1.SecureValue) (*secretv1beta1.SecureValue, bool, error) {
|
||||
sv := m.ReadActiveVersion(newSecureValue.Namespace, newSecureValue.Name)
|
||||
if sv == nil {
|
||||
return nil, false, contracts.ErrSecureValueNotFound
|
||||
}
|
||||
|
||||
// If the keeper doesn't exist, return an error
|
||||
if !m.keeperExists(sv.Namespace, sv.Status.Keeper) {
|
||||
return nil, false, contracts.ErrKeeperNotFound
|
||||
}
|
||||
|
||||
// If the payload doesn't contain a value and it's not using a reference, get the value from current version
|
||||
if newSecureValue.Spec.Value == nil && newSecureValue.Spec.Ref == nil {
|
||||
// Tried to update a secure value without providing a new value or a ref
|
||||
if sv.Spec.Value == nil {
|
||||
return nil, false, contracts.ErrSecureValueMissingSecretAndRef
|
||||
}
|
||||
newSecureValue.Spec.Value = sv.Spec.Value
|
||||
}
|
||||
|
||||
createdSv, err := m.Create(now, newSecureValue)
|
||||
|
||||
return createdSv, true, err
|
||||
}
|
||||
|
||||
func (m *ModelGsm) Delete(namespace, name string) (*secretv1beta1.SecureValue, error) {
|
||||
modelSv := m.ReadActiveVersion(namespace, name)
|
||||
if modelSv == nil {
|
||||
return nil, contracts.ErrSecureValueNotFound
|
||||
}
|
||||
m.SetVersionToInactive(namespace, name, modelSv.Status.Version)
|
||||
return modelSv.SecureValue, nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) List(namespace string) (*secretv1beta1.SecureValueList, error) {
|
||||
out := make([]secretv1beta1.SecureValue, 0)
|
||||
|
||||
for _, v := range m.SecureValues {
|
||||
if v.Namespace == namespace && v.active {
|
||||
out = append(out, *v.SecureValue)
|
||||
}
|
||||
}
|
||||
|
||||
return &secretv1beta1.SecureValueList{Items: out}, nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) Decrypt(ctx context.Context, decrypter, namespace, name string) (map[string]decrypt.DecryptResult, error) {
|
||||
for _, v := range m.SecureValues {
|
||||
if v.Namespace == namespace &&
|
||||
v.Name == name &&
|
||||
v.active {
|
||||
if slices.ContainsFunc(v.Spec.Decrypters, func(d string) bool { return d == decrypter }) {
|
||||
switch {
|
||||
// It's a secure value that specifies the secret
|
||||
case v.Spec.Value != nil:
|
||||
return map[string]decrypt.DecryptResult{
|
||||
name: decrypt.NewDecryptResultValue(v.DeepCopy().Spec.Value),
|
||||
}, nil
|
||||
|
||||
// It's a secure value that references a secret on a 3rd party store
|
||||
case v.Spec.Ref != nil:
|
||||
keeper := m.findKeeper(v.Namespace, v.Status.Keeper)
|
||||
switch keeper.keeperType {
|
||||
case secretv1beta1.AWSKeeperType:
|
||||
exposedValue, err := m.modelSecretsManager.RetrieveReference(ctx, nil, *v.Spec.Ref)
|
||||
if err != nil {
|
||||
return map[string]decrypt.DecryptResult{
|
||||
name: decrypt.NewDecryptResultErr(fmt.Errorf("%w: %w", contracts.ErrDecryptFailed, err)),
|
||||
}, nil
|
||||
}
|
||||
return map[string]decrypt.DecryptResult{
|
||||
name: decrypt.NewDecryptResultValue(&exposedValue),
|
||||
}, nil
|
||||
|
||||
// Other keepers are not implemented so we default to the system keeper
|
||||
default:
|
||||
// The system keeper doesn't implement Reference so decryption always fails
|
||||
return map[string]decrypt.DecryptResult{
|
||||
name: decrypt.NewDecryptResultErr(contracts.ErrDecryptFailed),
|
||||
}, nil
|
||||
}
|
||||
|
||||
default:
|
||||
panic("bug: secure value where Spec.Value and Spec.Ref are nil")
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]decrypt.DecryptResult{
|
||||
name: decrypt.NewDecryptResultErr(contracts.ErrDecryptNotAuthorized),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return map[string]decrypt.DecryptResult{
|
||||
name: decrypt.NewDecryptResultErr(contracts.ErrDecryptNotFound),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) Read(namespace, name string) (*secretv1beta1.SecureValue, error) {
|
||||
modelSv := m.ReadActiveVersion(namespace, name)
|
||||
if modelSv == nil {
|
||||
return nil, contracts.ErrSecureValueNotFound
|
||||
}
|
||||
return modelSv.SecureValue, nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) LeaseInactiveSecureValues(now time.Time, minAge, leaseTTL time.Duration, maxBatchSize uint16) ([]*ModelSecureValue, error) {
|
||||
out := make([]*ModelSecureValue, 0)
|
||||
|
||||
for _, sv := range m.SecureValues {
|
||||
if len(out) >= int(maxBatchSize) {
|
||||
break
|
||||
}
|
||||
if !sv.active && now.Sub(sv.created) > minAge && now.Sub(sv.leaseCreated) > leaseTTL {
|
||||
sv.leaseCreated = now
|
||||
out = append(out, sv)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) CleanupInactiveSecureValues(now time.Time, minAge time.Duration, maxBatchSize uint16) ([]*ModelSecureValue, error) {
|
||||
// Using a slice to allow duplicates
|
||||
toDelete := make([]*ModelSecureValue, 0)
|
||||
|
||||
// The implementation query sorts by created time ascending
|
||||
slices.SortFunc(m.SecureValues, func(a, b *ModelSecureValue) int {
|
||||
if a.created.Before(b.created) {
|
||||
return -1
|
||||
} else if a.created.After(b.created) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
for _, sv := range m.SecureValues {
|
||||
if len(toDelete) >= int(maxBatchSize) {
|
||||
break
|
||||
}
|
||||
|
||||
if !sv.active && now.Sub(sv.created) > minAge {
|
||||
toDelete = append(toDelete, sv)
|
||||
}
|
||||
}
|
||||
|
||||
// PERF: The slices are always small
|
||||
m.SecureValues = slices.DeleteFunc(m.SecureValues, func(v1 *ModelSecureValue) bool {
|
||||
return slices.ContainsFunc(toDelete, func(v2 *ModelSecureValue) bool {
|
||||
return v2.UID == v1.UID
|
||||
})
|
||||
})
|
||||
|
||||
return toDelete, nil
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package testutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -143,7 +144,8 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut {
|
||||
realMigrationExecutor, err := encryptionstorage.ProvideEncryptedValueMigrationExecutor(database, tracer, encryptedValueStorage, globalEncryptedValueStorage)
|
||||
require.NoError(t, err)
|
||||
|
||||
var keeperService contracts.KeeperService = newKeeperServiceWrapper(sqlKeeper)
|
||||
mockAwsKeeper := NewModelSecretsManager()
|
||||
var keeperService contracts.KeeperService = newKeeperServiceWrapper(sqlKeeper, mockAwsKeeper)
|
||||
|
||||
if setupCfg.KeeperService != nil {
|
||||
keeperService = setupCfg.KeeperService
|
||||
@@ -190,6 +192,7 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut {
|
||||
Clock: clock,
|
||||
KeeperService: keeperService,
|
||||
KeeperMetadataStorage: keeperMetadataStorage,
|
||||
ModelSecretsManager: mockAwsKeeper,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +215,8 @@ type Sut struct {
|
||||
Clock *FakeClock
|
||||
KeeperService contracts.KeeperService
|
||||
KeeperMetadataStorage contracts.KeeperMetadataStorage
|
||||
// A mock of AWS secrets manager that implements contracts.Keeper
|
||||
ModelSecretsManager *ModelAWSSecretsManager
|
||||
}
|
||||
|
||||
type CreateSvConfig struct {
|
||||
@@ -260,16 +265,54 @@ func (s *Sut) DeleteSv(ctx context.Context, namespace, name string) (*secretv1be
|
||||
return sv, err
|
||||
}
|
||||
|
||||
type keeperServiceWrapper struct {
|
||||
keeper contracts.Keeper
|
||||
type CreateKeeperConfig struct {
|
||||
// The default keeper payload. Mutate it to change which keeper ends up being created
|
||||
Keeper *secretv1beta1.Keeper
|
||||
}
|
||||
|
||||
func newKeeperServiceWrapper(keeper contracts.Keeper) *keeperServiceWrapper {
|
||||
return &keeperServiceWrapper{keeper: keeper}
|
||||
func (s *Sut) CreateAWSKeeper(ctx context.Context) (*secretv1beta1.Keeper, error) {
|
||||
return s.CreateKeeper(ctx, func(cfg *CreateKeeperConfig) {
|
||||
cfg.Keeper.Spec = secretv1beta1.KeeperSpec{
|
||||
Aws: &secretv1beta1.KeeperAWSConfig{},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Sut) CreateKeeper(ctx context.Context, opts ...func(*CreateKeeperConfig)) (*secretv1beta1.Keeper, error) {
|
||||
cfg := CreateKeeperConfig{
|
||||
Keeper: &secretv1beta1.Keeper{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "sv1",
|
||||
Namespace: "ns1",
|
||||
},
|
||||
Spec: secretv1beta1.KeeperSpec{
|
||||
Aws: &secretv1beta1.KeeperAWSConfig{},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
|
||||
return s.KeeperMetadataStorage.Create(ctx, cfg.Keeper, "actor-uid")
|
||||
}
|
||||
|
||||
type keeperServiceWrapper struct {
|
||||
sqlKeeper *sqlkeeper.SQLKeeper
|
||||
awsKeeper *ModelAWSSecretsManager
|
||||
}
|
||||
|
||||
func newKeeperServiceWrapper(sqlKeeper *sqlkeeper.SQLKeeper, awsKeeper *ModelAWSSecretsManager) *keeperServiceWrapper {
|
||||
return &keeperServiceWrapper{sqlKeeper: sqlKeeper, awsKeeper: awsKeeper}
|
||||
}
|
||||
|
||||
func (wrapper *keeperServiceWrapper) KeeperForConfig(cfg secretv1beta1.KeeperConfig) (contracts.Keeper, error) {
|
||||
return wrapper.keeper, nil
|
||||
switch cfg.(type) {
|
||||
case *secretv1beta1.NamedKeeperConfig[*secretv1beta1.KeeperAWSConfig]:
|
||||
return wrapper.awsKeeper, nil
|
||||
default:
|
||||
return wrapper.sqlKeeper, nil
|
||||
}
|
||||
}
|
||||
|
||||
func CreateUserAuthContext(ctx context.Context, namespace string, permissions map[string][]string) context.Context {
|
||||
@@ -390,3 +433,113 @@ type NoopMigrationExecutor struct {
|
||||
func (e *NoopMigrationExecutor) Execute(ctx context.Context) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// A mock of AWS secrets manager, used for testing.
|
||||
type ModelAWSSecretsManager struct {
|
||||
secrets map[string]entry
|
||||
alreadyDeleted map[string]bool
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
exposedValueOrRef string
|
||||
externalID string
|
||||
}
|
||||
|
||||
func NewModelSecretsManager() *ModelAWSSecretsManager {
|
||||
return &ModelAWSSecretsManager{
|
||||
secrets: make(map[string]entry),
|
||||
alreadyDeleted: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ModelAWSSecretsManager) Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) (externalID contracts.ExternalID, err error) {
|
||||
if exposedValueOrRef == "" {
|
||||
return "", fmt.Errorf("failed to satisfy constraint: Member must have length greater than or equal to 1")
|
||||
}
|
||||
|
||||
versionID := buildVersionID(namespace, name, version)
|
||||
if e, ok := m.secrets[versionID]; ok {
|
||||
// Ignore duplicated requests
|
||||
if e.exposedValueOrRef == exposedValueOrRef {
|
||||
return contracts.ExternalID(e.externalID), nil
|
||||
}
|
||||
|
||||
// Tried to create a secret that already exists
|
||||
return "", fmt.Errorf("ResourceExistsException: The operation failed because the secret %+v already exists", versionID)
|
||||
}
|
||||
|
||||
// First time creating the secret
|
||||
entry := entry{
|
||||
exposedValueOrRef: exposedValueOrRef,
|
||||
externalID: "external-id",
|
||||
}
|
||||
m.secrets[versionID] = entry
|
||||
|
||||
return contracts.ExternalID(entry.externalID), nil
|
||||
}
|
||||
|
||||
// Used to simulate the creation of secrets in the 3rd party secret store
|
||||
func (m *ModelAWSSecretsManager) Create(name, value string) {
|
||||
m.secrets[name] = entry{
|
||||
exposedValueOrRef: value,
|
||||
externalID: fmt.Sprintf("external_id_%+v", value),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ModelAWSSecretsManager) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) (exposedValue secretv1beta1.ExposedSecureValue, err error) {
|
||||
versionID := buildVersionID(namespace, name, version)
|
||||
|
||||
if m.deleted(versionID) {
|
||||
return "", fmt.Errorf("InvalidRequestException: You can't perform this operation on the secret because it was marked for deletion")
|
||||
}
|
||||
|
||||
entry, ok := m.secrets[versionID]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("ResourceNotFoundException: Secrets Manager can't find the specified secret")
|
||||
}
|
||||
|
||||
return secretv1beta1.ExposedSecureValue(entry.exposedValueOrRef), nil
|
||||
}
|
||||
|
||||
// TODO: this could be namespaced to make it more realistic
|
||||
func (m *ModelAWSSecretsManager) RetrieveReference(ctx context.Context, _ secretv1beta1.KeeperConfig, ref string) (secretv1beta1.ExposedSecureValue, error) {
|
||||
entry, ok := m.secrets[ref]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("ResourceNotFoundException: Secrets Manager can't find the specified secret")
|
||||
}
|
||||
return secretv1beta1.ExposedSecureValue(entry.exposedValueOrRef), nil
|
||||
}
|
||||
|
||||
func (m *ModelAWSSecretsManager) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) (err error) {
|
||||
versionID := buildVersionID(namespace, name, version)
|
||||
|
||||
// Deleting a secret that existed at some point is idempotent
|
||||
if m.deleted(versionID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the secret is being deleted for the first time
|
||||
if m.exists(versionID) {
|
||||
m.delete(versionID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ModelAWSSecretsManager) deleted(versionID string) bool {
|
||||
return m.alreadyDeleted[versionID]
|
||||
}
|
||||
|
||||
func (m *ModelAWSSecretsManager) exists(versionID string) bool {
|
||||
_, ok := m.secrets[versionID]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (m *ModelAWSSecretsManager) delete(versionID string) {
|
||||
m.alreadyDeleted[versionID] = true
|
||||
delete(m.secrets, versionID)
|
||||
}
|
||||
|
||||
func buildVersionID(namespace xkube.Namespace, name string, version int64) string {
|
||||
return fmt.Sprintf("%s/%s/%d", namespace, name, version)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user