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:
Bruno
2026-01-06 11:30:04 -03:00
committed by GitHub
parent bbaf91ed9c
commit 7698970f22
40 changed files with 1485 additions and 682 deletions
@@ -145,6 +145,14 @@ func (s *decryptStorage) Decrypt(ctx context.Context, namespace xkube.Namespace,
return "", fmt.Errorf("failed to get keeper for config: %v (%w)", err, contracts.ErrDecryptFailed)
}
if sv.Spec.Ref != nil {
exposedValue, err := keeper.RetrieveReference(ctx, keeperConfig, *sv.Spec.Ref)
if err != nil {
return "", fmt.Errorf("failed to expose secret using reference: %v (%w)", err, contracts.ErrDecryptFailed)
}
return exposedValue, nil
}
exposedValue, err := keeper.Expose(ctx, keeperConfig, namespace, name, sv.Status.Version)
if err != nil {
return "", fmt.Errorf("failed to expose secret: %v (%w)", err, contracts.ErrDecryptFailed)
@@ -9,12 +9,14 @@ import (
"github.com/grafana/grafana-app-sdk/logging"
"github.com/stretchr/testify/require"
grpcmetadata "google.golang.org/grpc/metadata"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/testutils"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/grafana/grafana/pkg/util/testutil"
)
@@ -324,6 +326,66 @@ func TestIntegrationDecrypt(t *testing.T) {
}
}
})
t.Run("happy path, referencing a secret in a 3rd party store", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
tokenSvcIdentity := "svc"
stSvcIdentity := "st-svc"
// Create auth context with proper permissions that match the decrypters
authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues:decrypt"}, tokenSvcIdentity, types.TypeUser)
// Needs to be incoming because we are pretending we received the metadata from a gRPC request
ctx = grpcmetadata.NewIncomingContext(authCtx, grpcmetadata.New(map[string]string{
contracts.HeaderGrafanaServiceIdentityName: stSvcIdentity,
}))
// Setup service
sut := testutils.Setup(t)
// Create a secret on the 3rd party secret store
sut.ModelSecretsManager.Create("ref1", "value")
// Create a 3rd party keeper
keeper, err := sut.KeeperMetadataStorage.Create(t.Context(), &secretv1beta1.Keeper{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
Name: "k1",
},
Spec: secretv1beta1.KeeperSpec{
Aws: &secretv1beta1.KeeperAWSConfig{},
},
}, "actor-uid")
require.NoError(t, err)
require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(keeper.Namespace), keeper.Name))
// Create a secure value
sv := &secretv1beta1.SecureValue{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
Name: "sv-test",
},
Spec: secretv1beta1.SecureValueSpec{
Description: "description",
Decrypters: []string{tokenSvcIdentity},
Ref: ptr.To("ref1"),
}}
_, err = sut.CreateSv(ctx, testutils.CreateSvWithSv(sv))
require.NoError(t, err)
fakeLogger := &mockLogger{}
loggerCtx := logging.Context(ctx, fakeLogger)
exposed, err := sut.DecryptStorage.Decrypt(loggerCtx, "default", "sv-test")
require.NoError(t, err)
require.Equal(t, "value", exposed.DangerouslyExposeAndConsumeValue())
})
}
func createAuthContext(ctx context.Context, namespace string, permissions []string, svc string, identityType types.IdentityType) context.Context {
+27 -23
View File
@@ -59,16 +59,16 @@ func (kp *keeperDB) toKubernetes() (*secretv1beta1.Keeper, error) {
}
// Obtain provider configs
provider := toProvider(secretv1beta1.KeeperType(kp.Type), kp.Payload)
provider := parseKeeperConfigJson(kp.Name, secretv1beta1.KeeperType(kp.Type), kp.Payload)
switch v := provider.(type) {
case *secretv1beta1.KeeperAWSConfig:
resource.Spec.Aws = v
case *secretv1beta1.KeeperAzureConfig:
resource.Spec.Azure = v
case *secretv1beta1.KeeperGCPConfig:
resource.Spec.Gcp = v
case *secretv1beta1.KeeperHashiCorpConfig:
resource.Spec.HashiCorpVault = v
case *secretv1beta1.NamedKeeperConfig[*secretv1beta1.KeeperAWSConfig]:
resource.Spec.Aws = v.Cfg
case *secretv1beta1.NamedKeeperConfig[*secretv1beta1.KeeperAzureConfig]:
resource.Spec.Azure = v.Cfg
case *secretv1beta1.NamedKeeperConfig[*secretv1beta1.KeeperGCPConfig]:
resource.Spec.Gcp = v.Cfg
case *secretv1beta1.NamedKeeperConfig[*secretv1beta1.KeeperHashiCorpConfig]:
resource.Spec.HashiCorpVault = v.Cfg
}
// Set all meta fields here for consistency.
@@ -214,34 +214,34 @@ func toTypeAndPayload(kp *secretv1beta1.Keeper) (secretv1beta1.KeeperType, strin
return "", "", fmt.Errorf("no keeper type found")
}
// toProvider maps a KeeperType and payload into a provider config struct.
// parseKeeperConfigJson maps a KeeperType and payload into a provider config struct.
// TODO: Move as method of KeeperType
func toProvider(keeperType secretv1beta1.KeeperType, payload string) secretv1beta1.KeeperConfig {
func parseKeeperConfigJson(keeperName string, keeperType secretv1beta1.KeeperType, payload string) secretv1beta1.KeeperConfig {
switch keeperType {
case secretv1beta1.AWSKeeperType:
aws := &secretv1beta1.KeeperAWSConfig{}
if err := json.Unmarshal([]byte(payload), aws); err != nil {
return nil
}
return aws
return secretv1beta1.NewNamedKeeperConfig(keeperName, aws)
case secretv1beta1.AzureKeeperType:
azure := &secretv1beta1.KeeperAzureConfig{}
if err := json.Unmarshal([]byte(payload), azure); err != nil {
return nil
}
return azure
return secretv1beta1.NewNamedKeeperConfig(keeperName, azure)
case secretv1beta1.GCPKeeperType:
gcp := &secretv1beta1.KeeperGCPConfig{}
if err := json.Unmarshal([]byte(payload), gcp); err != nil {
return nil
}
return gcp
return secretv1beta1.NewNamedKeeperConfig(keeperName, gcp)
case secretv1beta1.HashiCorpKeeperType:
hashicorp := &secretv1beta1.KeeperHashiCorpConfig{}
if err := json.Unmarshal([]byte(payload), hashicorp); err != nil {
return nil
}
return hashicorp
return secretv1beta1.NewNamedKeeperConfig(keeperName, hashicorp)
default:
return nil
}
@@ -253,12 +253,16 @@ func extractSecureValues(kp *secretv1beta1.Keeper) map[string]struct{} {
case kp.Spec.Aws != nil:
secureValues := make(map[string]struct{}, 0)
if kp.Spec.Aws.AccessKeyID.SecureValueName != "" {
secureValues[kp.Spec.Aws.AccessKeyID.SecureValueName] = struct{}{}
if kp.Spec.Aws.AccessKey == nil {
return secureValues
}
if kp.Spec.Aws.SecretAccessKey.SecureValueName != "" {
secureValues[kp.Spec.Aws.SecretAccessKey.SecureValueName] = struct{}{}
if kp.Spec.Aws.AccessKey.AccessKeyID.SecureValueName != "" {
secureValues[kp.Spec.Aws.AccessKey.AccessKeyID.SecureValueName] = struct{}{}
}
if kp.Spec.Aws.AccessKey.SecretAccessKey.SecureValueName != "" {
secureValues[kp.Spec.Aws.AccessKey.SecretAccessKey.SecureValueName] = struct{}{}
}
return secureValues
@@ -284,13 +288,13 @@ func extractSecureValues(kp *secretv1beta1.Keeper) map[string]struct{} {
func getKeeperConfig(keeper *secretv1beta1.Keeper) secretv1beta1.KeeperConfig {
switch keeper.Spec.GetType() {
case secretv1beta1.AWSKeeperType:
return keeper.Spec.Aws
return secretv1beta1.NewNamedKeeperConfig(keeper.Name, keeper.Spec.Aws)
case secretv1beta1.AzureKeeperType:
return keeper.Spec.Azure
return secretv1beta1.NewNamedKeeperConfig(keeper.Name, keeper.Spec.Azure)
case secretv1beta1.GCPKeeperType:
return keeper.Spec.Gcp
return secretv1beta1.NewNamedKeeperConfig(keeper.Name, keeper.Spec.Gcp)
case secretv1beta1.HashiCorpKeeperType:
return keeper.Spec.HashiCorpVault
return secretv1beta1.NewNamedKeeperConfig(keeper.Name, keeper.Spec.HashiCorpVault)
default:
return nil
}
+3 -10
View File
@@ -609,7 +609,7 @@ func (s *keeperMetadataStorage) GetKeeperConfig(ctx context.Context, namespace s
// Check if keeper is the systemwide one.
if name == contracts.SystemKeeperName {
return &secretv1beta1.SystemKeeperConfig{}, nil
return secretv1beta1.NewNamedKeeperConfig(contracts.SystemKeeperName, &secretv1beta1.SystemKeeperConfig{}), nil
}
// Load keeper config from metadata store, or TODO: keeper cache.
@@ -618,7 +618,7 @@ func (s *keeperMetadataStorage) GetKeeperConfig(ctx context.Context, namespace s
return nil, err
}
keeperConfig := toProvider(secretv1beta1.KeeperType(kp.Type), kp.Payload)
keeperConfig := parseKeeperConfigJson(kp.Name, secretv1beta1.KeeperType(kp.Type), kp.Payload)
// TODO: this would be a good place to check if credentials are secure values and load them.
return keeperConfig, nil
@@ -636,13 +636,6 @@ func (s *keeperMetadataStorage) SetAsActive(ctx context.Context, namespace xkube
return fmt.Errorf("template %q: %w", sqlKeeperSetAsActive.Name(), err)
}
// Check keeper exists. No need to worry about time of check to time of use
// since trying to activate a just deleted keeper will result in all
// keepers being inactive and defaulting to the system keeper.
if _, err := s.read(ctx, namespace.String(), name, contracts.ReadOpts{}); err != nil {
return fmt.Errorf("reading keeper before setting as active: %w", err)
}
_, err = s.db.ExecContext(ctx, query, req.GetArgs()...)
if err != nil {
return fmt.Errorf("setting keeper as active %q: %w", query, err)
@@ -726,7 +719,7 @@ func (s *keeperMetadataStorage) GetActiveKeeperConfig(ctx context.Context, names
if err != nil {
// When there are not active keepers, default to the system keeper
if errors.Is(err, contracts.ErrKeeperNotFound) {
return contracts.SystemKeeperName, &secretv1beta1.SystemKeeperConfig{}, nil
return contracts.SystemKeeperName, secretv1beta1.NewNamedKeeperConfig(contracts.SystemKeeperName, &secretv1beta1.SystemKeeperConfig{}), nil
}
return "", nil, fmt.Errorf("fetching active keeper from db: %w", err)
}
@@ -43,7 +43,7 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) {
// get system keeper config
keeperConfig, err := keeperMetadataStorage.GetKeeperConfig(ctx, defaultKeeperNS, contracts.SystemKeeperName, contracts.ReadOpts{})
require.NoError(t, err)
require.IsType(t, &secretv1beta1.SystemKeeperConfig{}, keeperConfig)
require.IsType(t, &secretv1beta1.NamedKeeperConfig[*secretv1beta1.SystemKeeperConfig]{}, keeperConfig)
})
t.Run("get test keeper config", func(t *testing.T) {
@@ -188,11 +188,13 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) {
Spec: secretv1beta1.KeeperSpec{
Description: "initial description",
Aws: &secretv1beta1.KeeperAWSConfig{
AccessKeyID: secretv1beta1.KeeperCredentialValue{
ValueFromEnv: "AWS_ACCESS_KEY_ID_1",
},
SecretAccessKey: secretv1beta1.KeeperCredentialValue{
ValueFromEnv: "AWS_SECRET_ACCESS_KEY_1",
AccessKey: &secretv1beta1.KeeperAWSAccessKey{
AccessKeyID: secretv1beta1.KeeperCredentialValue{
ValueFromEnv: "AWS_ACCESS_KEY_ID_1",
},
SecretAccessKey: secretv1beta1.KeeperCredentialValue{
ValueFromEnv: "AWS_SECRET_ACCESS_KEY_1",
},
},
KmsKeyID: ptr.To("kms-key-id-1"),
},
@@ -208,8 +210,8 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) {
// Verify initial AWS config
keeper, err := keeperMetadataStorage.Read(ctx, xkube.Namespace(keeperNamespaceTest), keeperTest, contracts.ReadOpts{})
require.NoError(t, err)
require.Equal(t, "AWS_ACCESS_KEY_ID_1", keeper.Spec.Aws.AccessKeyID.ValueFromEnv)
require.Equal(t, "AWS_SECRET_ACCESS_KEY_1", keeper.Spec.Aws.SecretAccessKey.ValueFromEnv)
require.Equal(t, "AWS_ACCESS_KEY_ID_1", keeper.Spec.Aws.AccessKey.AccessKeyID.ValueFromEnv)
require.Equal(t, "AWS_SECRET_ACCESS_KEY_1", keeper.Spec.Aws.AccessKey.SecretAccessKey.ValueFromEnv)
require.Equal(t, "kms-key-id-1", *keeper.Spec.Aws.KmsKeyID)
// Update with new AWS config
@@ -217,11 +219,13 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) {
Spec: secretv1beta1.KeeperSpec{
Description: "updated description",
Aws: &secretv1beta1.KeeperAWSConfig{
AccessKeyID: secretv1beta1.KeeperCredentialValue{
ValueFromEnv: "AWS_ACCESS_KEY_ID_2",
},
SecretAccessKey: secretv1beta1.KeeperCredentialValue{
ValueFromEnv: "AWS_SECRET_ACCESS_KEY_2",
AccessKey: &secretv1beta1.KeeperAWSAccessKey{
AccessKeyID: secretv1beta1.KeeperCredentialValue{
ValueFromEnv: "AWS_ACCESS_KEY_ID_2",
},
SecretAccessKey: secretv1beta1.KeeperCredentialValue{
ValueFromEnv: "AWS_SECRET_ACCESS_KEY_2",
},
},
KmsKeyID: ptr.To("kms-key-id-2"),
},
@@ -237,8 +241,8 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) {
// Verify updated AWS config
updatedKeeper, err = keeperMetadataStorage.Read(ctx, xkube.Namespace(keeperNamespaceTest), keeperTest, contracts.ReadOpts{})
require.NoError(t, err)
require.Equal(t, "AWS_ACCESS_KEY_ID_2", updatedKeeper.Spec.Aws.AccessKeyID.ValueFromEnv)
require.Equal(t, "AWS_SECRET_ACCESS_KEY_2", updatedKeeper.Spec.Aws.SecretAccessKey.ValueFromEnv)
require.Equal(t, "AWS_ACCESS_KEY_ID_2", updatedKeeper.Spec.Aws.AccessKey.AccessKeyID.ValueFromEnv)
require.Equal(t, "AWS_SECRET_ACCESS_KEY_2", updatedKeeper.Spec.Aws.AccessKey.SecretAccessKey.ValueFromEnv)
require.Equal(t, "kms-key-id-2", *updatedKeeper.Spec.Aws.KmsKeyID)
})
@@ -278,11 +282,13 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) {
Spec: secretv1beta1.KeeperSpec{
Description: "initial description",
Aws: &secretv1beta1.KeeperAWSConfig{
AccessKeyID: secretv1beta1.KeeperCredentialValue{
ValueFromEnv: "AWS_ACCESS_KEY_ID",
},
SecretAccessKey: secretv1beta1.KeeperCredentialValue{
ValueFromEnv: "AWS_SECRET_ACCESS_KEY",
AccessKey: &secretv1beta1.KeeperAWSAccessKey{
AccessKeyID: secretv1beta1.KeeperCredentialValue{
ValueFromEnv: "AWS_ACCESS_KEY_ID",
},
SecretAccessKey: secretv1beta1.KeeperCredentialValue{
ValueFromEnv: "AWS_SECRET_ACCESS_KEY",
},
},
},
},
@@ -194,12 +194,12 @@ func TestPropertySecureValueMetadataStorage(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
sut := testutils.Setup(tt)
model := newModel()
model := testutils.NewModelGsm(nil)
t.Repeat(map[string]func(*rapid.T){
"create": func(t *rapid.T) {
sv := anySecureValueGen.Draw(t, "sv")
modelCreatedSv, modelErr := model.create(sut.Clock.Now(), sv.DeepCopy())
sv := testutils.AnySecureValueGen.Draw(t, "sv")
modelCreatedSv, modelErr := model.Create(sut.Clock.Now(), sv.DeepCopy())
createdSv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(sv.DeepCopy()))
if err != nil || modelErr != nil {
require.ErrorIs(t, err, modelErr)
@@ -209,10 +209,23 @@ func TestPropertySecureValueMetadataStorage(t *testing.T) {
require.Equal(t, modelCreatedSv.Name, createdSv.Name)
require.Equal(t, modelCreatedSv.Status.Version, createdSv.Status.Version)
},
"read": func(t *rapid.T) {
ns := testutils.NamespaceGen.Draw(t, "ns")
name := testutils.SecureValueNameGen.Draw(t, "name")
modelSv, modelErr := model.Read(ns, name)
sv, err := sut.SecureValueMetadataStorage.Read(t.Context(), xkube.Namespace(ns), name, contracts.ReadOpts{})
if err != nil || modelErr != nil {
require.ErrorIs(t, err, modelErr)
return
}
require.Equal(t, modelSv.Namespace, sv.Namespace)
require.Equal(t, modelSv.Name, sv.Name)
require.Equal(t, modelSv.Status.Version, sv.Status.Version)
},
"delete": func(t *rapid.T) {
ns := namespaceGen.Draw(t, "ns")
name := secureValueNameGen.Draw(t, "name")
modelSv, modelErr := model.delete(ns, name)
ns := testutils.NamespaceGen.Draw(t, "ns")
name := testutils.SecureValueNameGen.Draw(t, "name")
modelSv, modelErr := model.Delete(ns, name)
sv, err := sut.DeleteSv(t.Context(), ns, name)
if err != nil || modelErr != nil {
require.ErrorIs(t, err, modelErr)
@@ -227,7 +240,7 @@ func TestPropertySecureValueMetadataStorage(t *testing.T) {
minAge := 300 * time.Second
leaseTTL := 30 * time.Second
maxBatchSize := rapid.Uint16Range(1, 10).Draw(t, "maxBatchSize")
modelSvs, modelErr := model.leaseInactiveSecureValues(sut.Clock.Now(), minAge, leaseTTL, maxBatchSize)
modelSvs, modelErr := model.LeaseInactiveSecureValues(sut.Clock.Now(), minAge, leaseTTL, maxBatchSize)
svs, err := sut.SecureValueMetadataStorage.LeaseInactiveSecureValues(t.Context(), maxBatchSize)
require.ErrorIs(t, err, modelErr)
require.Equal(t, len(modelSvs), len(svs))
+116 -341
View File
@@ -1,7 +1,6 @@
package metadata_test
import (
"fmt"
"slices"
"testing"
"time"
@@ -12,305 +11,11 @@ import (
"pgregory.net/rapid"
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"
"github.com/grafana/grafana/pkg/registry/apis/secret/testutils"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
)
type modelSecureValue struct {
*secretv1beta1.SecureValue
active bool
created time.Time
leaseCreated time.Time
}
type modelKeeper struct {
namespace string
name string
active bool
}
// A simplified model of the grafana secrets manager
type model struct {
secureValues []*modelSecureValue
keepers []*modelKeeper
}
func newModel() *model {
return &model{}
}
func (m *model) 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 *model) 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 *model) 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 *model) 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 *model) create(now time.Time, sv *secretv1beta1.SecureValue) (*secretv1beta1.SecureValue, error) {
keeper := m.getActiveKeeper(sv.Namespace)
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 *model) 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}
}
func (m *model) keeperExists(namespace, name string) bool {
return m.findKeeper(namespace, name) != nil
}
func (m *model) 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}
}
for _, k := range m.keepers {
if k.namespace == namespace && k.name == name {
return k
}
}
return nil
}
func (m *model) createKeeper(keeper *secretv1beta1.Keeper) (*secretv1beta1.Keeper, error) {
if m.keeperExists(keeper.Namespace, keeper.Name) {
return nil, contracts.ErrKeeperAlreadyExists
}
m.keepers = append(m.keepers, &modelKeeper{namespace: keeper.Namespace, name: keeper.Name})
return keeper.DeepCopy(), nil
}
func (m *model) setKeeperAsActive(namespace, keeperName string) error {
keeper := m.findKeeper(namespace, keeperName)
if keeper == nil {
return contracts.ErrKeeperNotFound
}
// Set the keeper as active
keeper.active = true
// Set every other keeper in the namespace as inactive
for _, k := range m.keepers {
if k.namespace == namespace && k.name != keeperName {
k.active = false
}
}
return nil
}
func (m *model) 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, get the value from current version
if newSecureValue.Spec.Value == nil {
newSecureValue.Spec.Value = sv.Spec.Value
}
createdSv, err := m.create(now, newSecureValue)
return createdSv, true, err
}
func (m *model) 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 *model) 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 *model) decrypt(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 }) {
return map[string]decrypt.DecryptResult{
name: decrypt.NewDecryptResultValue(v.DeepCopy().Spec.Value),
}, 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 *model) 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 *model) 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
}
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"})
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{},
}
})
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
})
// Any secure value will do
deleteSecureValueGen = anySecureValueGen
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
}
func TestModel(t *testing.T) {
t.Parallel()
@@ -330,18 +35,18 @@ func TestModel(t *testing.T) {
t.Run("creating secure values", func(t *testing.T) {
t.Parallel()
m := newModel()
m := testutils.NewModelGsm(nil)
now := time.Now()
// Create a secure value
sv1, err := m.create(now, sv.DeepCopy())
sv1, err := m.Create(now, sv.DeepCopy())
require.NoError(t, err)
require.Equal(t, sv.Namespace, sv1.Namespace)
require.Equal(t, sv.Name, sv1.Name)
require.EqualValues(t, 1, sv1.Status.Version)
// Create a new version of a secure value
sv2, err := m.create(now, sv.DeepCopy())
sv2, err := m.Create(now, sv.DeepCopy())
require.NoError(t, err)
require.Equal(t, sv.Namespace, sv2.Namespace)
require.Equal(t, sv.Name, sv2.Name)
@@ -351,15 +56,15 @@ func TestModel(t *testing.T) {
t.Run("updating secure values", func(t *testing.T) {
t.Parallel()
m := newModel()
m := testutils.NewModelGsm(nil)
now := time.Now()
sv1, err := m.create(now, sv.DeepCopy())
sv1, err := m.Create(now, sv.DeepCopy())
require.NoError(t, err)
// Create a new version of a secure value by updating it
sv2, _, err := m.update(now, sv1.DeepCopy())
sv2, _, err := m.Update(now, sv1.DeepCopy())
require.NoError(t, err)
require.Equal(t, sv.Namespace, sv2.Namespace)
require.Equal(t, sv.Name, sv2.Name)
@@ -369,55 +74,55 @@ func TestModel(t *testing.T) {
sv3 := sv2.DeepCopy()
sv3.Name = "i_dont_exist"
sv3.Spec.Value = nil
_, _, err = m.update(now, sv3)
_, _, err = m.Update(now, sv3)
require.ErrorIs(t, err, contracts.ErrSecureValueNotFound)
// Updating a value that doesn't exist creates a new version
sv4 := sv3.DeepCopy()
sv4.Name = "i_dont_exist"
sv4.Spec.Value = ptr.To(secretv1beta1.NewExposedSecureValue("sv4"))
_, _, err = m.update(now, sv4)
_, _, err = m.Update(now, sv4)
require.ErrorIs(t, err, contracts.ErrSecureValueNotFound)
})
t.Run("deleting a secure value", func(t *testing.T) {
t.Parallel()
m := newModel()
m := testutils.NewModelGsm(nil)
now := time.Now()
sv1, err := m.create(now, sv.DeepCopy())
sv1, err := m.Create(now, sv.DeepCopy())
require.NoError(t, err)
// Deleting a secure value
deletedSv, err := m.delete(sv1.Namespace, sv1.Name)
deletedSv, err := m.Delete(sv1.Namespace, sv1.Name)
require.NoError(t, err)
require.Equal(t, sv1.Namespace, deletedSv.Namespace)
require.Equal(t, sv1.Name, deletedSv.Name)
require.EqualValues(t, sv1.Status.Version, deletedSv.Status.Version)
// Deleting a secure value that doesn't exist results in an error
_, err = m.delete(sv1.Namespace, sv1.Name)
_, err = m.Delete(sv1.Namespace, sv1.Name)
require.ErrorIs(t, err, contracts.ErrSecureValueNotFound)
})
t.Run("listing secure values", func(t *testing.T) {
t.Parallel()
m := newModel()
m := testutils.NewModelGsm(nil)
now := time.Now()
// No secure values exist yet
list, err := m.list(sv.Namespace)
list, err := m.List(sv.Namespace)
require.NoError(t, err)
require.Equal(t, 0, len(list.Items))
// Create a secure value
sv1, err := m.create(now, sv.DeepCopy())
sv1, err := m.Create(now, sv.DeepCopy())
require.NoError(t, err)
// 1 secure value exists and it should be returned
list, err = m.list(sv.Namespace)
list, err = m.List(sv.Namespace)
require.NoError(t, err)
require.Equal(t, 1, len(list.Items))
require.Equal(t, sv1.Namespace, list.Items[0].Namespace)
@@ -428,11 +133,11 @@ func TestModel(t *testing.T) {
t.Run("decrypting secure values", func(t *testing.T) {
t.Parallel()
m := newModel()
m := testutils.NewModelGsm(nil)
now := time.Now()
// Decrypting a secure value that does not exist
result, err := m.decrypt("decrypter", "namespace", "name")
result, err := m.Decrypt(t.Context(), "decrypter", "namespace", "name")
require.NoError(t, err)
require.Equal(t, 1, len(result))
require.Nil(t, result["name"].Value())
@@ -440,16 +145,62 @@ func TestModel(t *testing.T) {
// Create a secure value
secret := "v1"
sv1, err := m.create(now, sv.DeepCopy())
sv1, err := m.Create(now, sv.DeepCopy())
require.NoError(t, err)
// Decrypt the just created secure value
result, err = m.decrypt(sv1.Spec.Decrypters[0], sv1.Namespace, sv1.Name)
result, err = m.Decrypt(t.Context(), sv1.Spec.Decrypters[0], sv1.Namespace, sv1.Name)
require.NoError(t, err)
require.Equal(t, 1, len(result))
require.Nil(t, result[sv1.Name].Error())
require.Equal(t, secret, result[sv1.Name].Value().DangerouslyExposeAndConsumeValue())
})
t.Run("decrypting with reference", func(t *testing.T) {
t.Parallel()
secretsManager := testutils.NewModelSecretsManager()
m := testutils.NewModelGsm(secretsManager)
now := time.Now()
keeper, err := m.CreateKeeper(&secretv1beta1.Keeper{
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns1",
Name: "k1",
},
Spec: secretv1beta1.KeeperSpec{
Aws: &secretv1beta1.KeeperAWSConfig{},
},
})
require.NoError(t, err)
require.NoError(t, m.SetKeeperAsActive(keeper.Namespace, keeper.Name))
// Store the secret on the 3rd party secrets store
secret := "v1"
secretsManager.Create("ref1", secret)
// Create a secure value that references the secret on the 3rd party secret store
sv, err := m.Create(now, &secretv1beta1.SecureValue{
ObjectMeta: metav1.ObjectMeta{
Name: "sv1",
Namespace: "ns1",
},
Spec: secretv1beta1.SecureValueSpec{
Description: "desc1",
Ref: ptr.To("ref1"),
Decrypters: []string{"decrypter1"},
},
Status: secretv1beta1.SecureValueStatus{},
})
require.NoError(t, err)
// Decrypt the just created secure value
result, err := m.Decrypt(t.Context(), sv.Spec.Decrypters[0], sv.Namespace, sv.Name)
require.NoError(t, err)
require.Equal(t, 1, len(result))
require.Nil(t, result[sv.Name].Error())
require.Equal(t, secret, result[sv.Name].Value().DangerouslyExposeAndConsumeValue())
})
}
func TestStateMachine(t *testing.T) {
@@ -459,14 +210,13 @@ func TestStateMachine(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
sut := testutils.Setup(tt)
model := newModel()
model := testutils.NewModelGsm(sut.ModelSecretsManager)
t.Repeat(map[string]func(*rapid.T){
"create": func(t *rapid.T) {
sv := anySecureValueGen.Draw(t, "sv")
modelCreatedSv, modelErr := model.create(sut.Clock.Now(), sv.DeepCopy())
"createSecureValueWithSecretValue": func(t *rapid.T) {
sv := testutils.AnySecureValueGen.Draw(t, "sv")
modelCreatedSv, modelErr := model.Create(sut.Clock.Now(), sv.DeepCopy())
createdSv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(sv.DeepCopy()))
if err != nil || modelErr != nil {
require.ErrorIs(t, err, modelErr)
@@ -476,9 +226,27 @@ func TestStateMachine(t *testing.T) {
require.Equal(t, modelCreatedSv.Name, createdSv.Name)
require.Equal(t, modelCreatedSv.Status.Version, createdSv.Status.Version)
},
"createSecureValueWithRef": func(t *rapid.T) {
sv := testutils.AnySecureValueWithRefGen.Draw(t, "sv")
modelCreatedSv, modelErr := model.Create(sut.Clock.Now(), sv.DeepCopy())
createdSv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(sv.DeepCopy()))
if err != nil || modelErr != nil {
require.ErrorIs(t, err, modelErr)
return
}
require.Equal(t, modelCreatedSv.Namespace, createdSv.Namespace)
require.Equal(t, modelCreatedSv.Name, createdSv.Name)
require.Equal(t, modelCreatedSv.Status.Version, createdSv.Status.Version)
},
"createSecretOn3rdPartyKeeper": func(t *rapid.T) {
name := testutils.SecretsToRefGen.Draw(t, "name")
value := rapid.String().Draw(t, "value")
sut.ModelSecretsManager.Create(name, value)
},
"update": func(t *rapid.T) {
sv := updateSecureValueGen.Draw(t, "sv")
modelCreatedSv, _, modelErr := model.update(sut.Clock.Now(), sv.DeepCopy())
sv := testutils.UpdateSecureValueGen.Draw(t, "sv")
modelCreatedSv, _, modelErr := model.Update(sut.Clock.Now(), sv.DeepCopy())
createdSv, err := sut.UpdateSv(t.Context(), sv.DeepCopy())
if err != nil || modelErr != nil {
require.ErrorIs(t, err, modelErr)
@@ -489,9 +257,10 @@ func TestStateMachine(t *testing.T) {
require.Equal(t, modelCreatedSv.Status.Version, createdSv.Status.Version)
},
"delete": func(t *rapid.T) {
sv := deleteSecureValueGen.Draw(t, "sv")
modelSv, modelErr := model.delete(sv.Namespace, sv.Name)
deletedSv, err := sut.DeleteSv(t.Context(), sv.Namespace, sv.Name)
ns := testutils.NamespaceGen.Draw(t, "ns")
name := testutils.SecureValueNameGen.Draw(t, "name")
modelSv, modelErr := model.Delete(ns, name)
deletedSv, err := sut.DeleteSv(t.Context(), ns, name)
if err != nil || modelErr != nil {
require.ErrorIs(t, err, modelErr)
return
@@ -501,12 +270,12 @@ func TestStateMachine(t *testing.T) {
require.Equal(t, modelSv.Status.Version, deletedSv.Status.Version)
},
"list": func(t *rapid.T) {
sv := anySecureValueGen.Draw(t, "sv")
authCtx := testutils.CreateUserAuthContext(t.Context(), sv.Namespace, map[string][]string{
ns := testutils.NamespaceGen.Draw(t, "ns")
authCtx := testutils.CreateUserAuthContext(t.Context(), ns, map[string][]string{
"securevalues:read": {"securevalues:uid:*"},
})
modelList, modelErr := model.list(sv.Namespace)
list, err := sut.SecureValueService.List(authCtx, xkube.Namespace(sv.Namespace))
modelList, modelErr := model.List(ns)
list, err := sut.SecureValueService.List(authCtx, xkube.Namespace(ns))
if err != nil || modelErr != nil {
require.ErrorIs(t, err, modelErr)
return
@@ -525,9 +294,10 @@ func TestStateMachine(t *testing.T) {
}
},
"get": func(t *rapid.T) {
sv := anySecureValueGen.Draw(t, "sv")
modelSv, modelErr := model.read(sv.Namespace, sv.Name)
readSv, err := sut.SecureValueService.Read(t.Context(), xkube.Namespace(sv.Namespace), sv.Name)
ns := testutils.NamespaceGen.Draw(t, "ns")
name := testutils.SecureValueNameGen.Draw(t, "name")
modelSv, modelErr := model.Read(ns, name)
readSv, err := sut.SecureValueService.Read(t.Context(), xkube.Namespace(ns), name)
if err != nil || modelErr != nil {
require.ErrorIs(t, err, modelErr)
return
@@ -537,9 +307,9 @@ func TestStateMachine(t *testing.T) {
require.Equal(t, modelSv.Status.Version, readSv.Status.Version)
},
"decrypt": func(t *rapid.T) {
input := decryptGen.Draw(t, "decryptInput")
modelResult, modelErr := model.decrypt(input.decrypter, input.namespace, input.name)
result, err := sut.DecryptService.Decrypt(t.Context(), input.decrypter, input.namespace, input.name)
input := testutils.DecryptGen.Draw(t, "decryptInput")
modelResult, modelErr := model.Decrypt(t.Context(), input.Decrypter, input.Namespace, input.Name)
result, err := sut.DecryptService.Decrypt(t.Context(), input.Decrypter, input.Namespace, input.Name)
if err != nil || modelErr != nil {
require.ErrorIs(t, err, modelErr)
return
@@ -547,13 +317,13 @@ func TestStateMachine(t *testing.T) {
require.Equal(t, len(modelResult), len(result))
for name := range modelResult {
require.Equal(t, modelResult[name].Error(), result[name].Error())
require.ErrorIs(t, modelResult[name].Error(), result[name].Error())
require.Equal(t, modelResult[name].Value(), result[name].Value())
}
},
"createKeeper": func(t *rapid.T) {
input := anyKeeperGen.Draw(t, "keeper")
modelKeeper, modelErr := model.createKeeper(input)
input := testutils.AnyKeeperGen.Draw(t, "keeper")
modelKeeper, modelErr := model.CreateKeeper(input)
keeper, err := sut.KeeperMetadataStorage.Create(t.Context(), input, "actor-uid")
if err != nil || modelErr != nil {
require.ErrorIs(t, err, modelErr)
@@ -562,9 +332,14 @@ func TestStateMachine(t *testing.T) {
require.Equal(t, modelKeeper.Name, keeper.Name)
},
"setKeeperAsActive": func(t *rapid.T) {
namespace := namespaceGen.Draw(t, "namespace")
keeper := keeperNameGen.Draw(t, "keeper")
modelErr := model.setKeeperAsActive(namespace, keeper)
namespace := testutils.NamespaceGen.Draw(t, "namespace")
var keeper string
if rapid.Bool().Draw(t, "systemKeeper") {
keeper = contracts.SystemKeeperName
} else {
keeper = testutils.KeeperNameGen.Draw(t, "keeper")
}
modelErr := model.SetKeeperAsActive(namespace, keeper)
err := sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(namespace), keeper)
if err != nil || modelErr != nil {
require.ErrorIs(t, err, modelErr)