From 9d0a23e1f5d138afaf087a115183cf7501c0fe6c Mon Sep 17 00:00:00 2001 From: Bruno Date: Fri, 11 Jul 2025 09:40:50 -0300 Subject: [PATCH] Secrets: add crudl+decrypt state machine test (#107971) * Secrets: add state machine test for CRUDL+decrpt operations * make update-workspace * make update-workspace * make enterprise-dev * make update-workspace * fix go.mod * make update-workspace * fix gomod * make update-workspace --------- Co-authored-by: Matheus Macabu --- go.mod | 1 + go.sum | 2 + go.work.sum | 2 + .../apis/secret/service/secure_value.go | 1 + .../apis/secret/testutils/testutils.go | 52 ++- .../secret/metadata/secure_value_test.go | 419 ++++++++++++++++++ 6 files changed, 473 insertions(+), 4 deletions(-) create mode 100644 pkg/storage/secret/metadata/secure_value_test.go diff --git a/go.mod b/go.mod index 3096b0ef889..dfb884a040c 100644 --- a/go.mod +++ b/go.mod @@ -219,6 +219,7 @@ require ( k8s.io/kube-aggregator v0.33.1 // @grafana/grafana-app-platform-squad k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // @grafana/grafana-app-platform-squad k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // @grafana/partner-datasources + pgregory.net/rapid v1.2.0 // @grafana/grafana-operator-experience-squad sigs.k8s.io/randfill v1.0.0 // @grafana/grafana-app-platform-squad sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // @grafana-app-platform-squad xorm.io/builder v0.3.6 // @grafana/grafana-backend-group diff --git a/go.sum b/go.sum index c9040b526ff..0a022fc6562 100644 --- a/go.sum +++ b/go.sum @@ -3632,6 +3632,8 @@ modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/go.work.sum b/go.work.sum index a2b8c460b40..754974ef77b 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1434,6 +1434,8 @@ modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJ modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao= modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= diff --git a/pkg/registry/apis/secret/service/secure_value.go b/pkg/registry/apis/secret/service/secure_value.go index 01b382edbf7..2611eb5a41b 100644 --- a/pkg/registry/apis/secret/service/secure_value.go +++ b/pkg/registry/apis/secret/service/secure_value.go @@ -194,6 +194,7 @@ func (s *SecureValueService) Delete(ctx context.Context, namespace xkube.Namespa )) defer span.End() + // TODO: does this need to be for update? sv, err := s.secureValueMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{ForUpdate: true}) if err != nil { return nil, fmt.Errorf("fetching secure value: %+w", err) diff --git a/pkg/registry/apis/secret/testutils/testutils.go b/pkg/registry/apis/secret/testutils/testutils.go index 281304320c8..b85b098d834 100644 --- a/pkg/registry/apis/secret/testutils/testutils.go +++ b/pkg/registry/apis/secret/testutils/testutils.go @@ -4,6 +4,9 @@ import ( "context" "testing" + "github.com/grafana/authlib/authn" + "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/identity" secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" encryptionstorage "github.com/grafana/grafana/pkg/storage/secret/encryption" "go.opentelemetry.io/otel/trace/noop" @@ -70,7 +73,10 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { // Initialize access client + access control accessControl := &actest.FakeAccessControl{ExpectedEvaluate: true} - accessClient := accesscontrol.NewLegacyAccessClient(accessControl) + accessClient := accesscontrol.NewLegacyAccessClient(accessControl, accesscontrol.ResourceAuthorizerOptions{ + Resource: "securevalues", + Attr: "uid", + }) defaultKey := "SdlklWklckeLS" cfg := &setting.Cfg{ @@ -112,13 +118,22 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { decryptStorage, err := metadata.ProvideDecryptStorage(features, tracer, keeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, nil) require.NoError(t, err) - return Sut{SecureValueService: secureValueService, SecureValueMetadataStorage: secureValueMetadataStorage, Database: database, DecryptStorage: decryptStorage} + decryptService := decrypt.ProvideDecryptService(decryptStorage) + + return Sut{ + SecureValueService: secureValueService, + SecureValueMetadataStorage: secureValueMetadataStorage, + Database: database, + DecryptStorage: decryptStorage, + DecryptService: decryptService, + } } type Sut struct { SecureValueService *service.SecureValueService SecureValueMetadataStorage contracts.SecureValueMetadataStorage DecryptStorage contracts.DecryptStorage + DecryptService service.DecryptService Database *database.Database } @@ -142,6 +157,7 @@ func (s *Sut) CreateSv(ctx context.Context, opts ...func(*CreateSvConfig)) (*sec Spec: secretv0alpha1.SecureValueSpec{ Description: "desc1", Value: secretv0alpha1.NewExposedSecureValue("v1"), + Decrypters: []string{"decrypter1"}, }, Status: secretv0alpha1.SecureValueStatus{}, }, @@ -150,7 +166,7 @@ func (s *Sut) CreateSv(ctx context.Context, opts ...func(*CreateSvConfig)) (*sec opt(&cfg) } - createdSv, err := s.SecureValueService.Create(ctx, cfg.Sv, "actor") + createdSv, err := s.SecureValueService.Create(ctx, cfg.Sv, "actor-uid") if err != nil { return nil, err } @@ -158,7 +174,7 @@ func (s *Sut) CreateSv(ctx context.Context, opts ...func(*CreateSvConfig)) (*sec } func (s *Sut) UpdateSv(ctx context.Context, sv *secretv0alpha1.SecureValue) (*secretv0alpha1.SecureValue, error) { - newSv, _, err := s.SecureValueService.Update(ctx, sv, "actor") + newSv, _, err := s.SecureValueService.Update(ctx, sv, "actor-uid") return newSv, err } @@ -178,3 +194,31 @@ func newKeeperServiceWrapper(keeper contracts.Keeper) *keeperServiceWrapper { func (wrapper *keeperServiceWrapper) KeeperForConfig(cfg secretv0alpha1.KeeperConfig) (contracts.Keeper, error) { return wrapper.keeper, nil } + +func CreateUserAuthContext(ctx context.Context, namespace string, permissions map[string][]string) context.Context { + orgID := int64(1) + requester := &identity.StaticRequester{ + Namespace: namespace, + Type: types.TypeUser, + UserID: 1, + OrgID: orgID, + Permissions: map[int64]map[string][]string{ + orgID: permissions, + }, + } + + return types.WithAuthInfo(ctx, requester) +} + +func CreateServiceAuthContext(ctx context.Context, serviceIdentity string, permissions []string) context.Context { + requester := &identity.StaticRequester{ + AccessTokenClaims: &authn.Claims[authn.AccessTokenClaims]{ + Rest: authn.AccessTokenClaims{ + Permissions: permissions, + ServiceIdentity: serviceIdentity, + }, + }, + } + + return types.WithAuthInfo(ctx, requester) +} diff --git a/pkg/storage/secret/metadata/secure_value_test.go b/pkg/storage/secret/metadata/secure_value_test.go new file mode 100644 index 00000000000..459db29f74a --- /dev/null +++ b/pkg/storage/secret/metadata/secure_value_test.go @@ -0,0 +1,419 @@ +package metadata_test + +import ( + "fmt" + "slices" + "testing" + + secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/service" + "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "pgregory.net/rapid" +) + +type modelSecureValue struct { + *secretv0alpha1.SecureValue + active bool +} + +// A simplified model of the grafana secrets manager +type model struct { + secureValues []*modelSecureValue +} + +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(sv *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, error) { + modelSv := &modelSecureValue{sv, false} + modelSv.Status.Version = m.getNewVersionNumber(modelSv.Namespace, modelSv.Name) + modelSv.Status.ExternalID = fmt.Sprintf("%d", modelSv.Status.Version) + m.secureValues = append(m.secureValues, modelSv) + m.setVersionToActive(modelSv.Namespace, modelSv.Name, modelSv.Status.Version) + return modelSv.SecureValue, nil +} + +func (m *model) update(newSecureValue *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, bool, error) { + // If the payload doesn't contain a value, get the value from current version + if newSecureValue.Spec.Value == "" { + sv := m.readActiveVersion(newSecureValue.Namespace, newSecureValue.Name) + if sv == nil { + return nil, false, contracts.ErrSecureValueNotFound + } + newSecureValue.Spec.Value = sv.Spec.Value + } + createdSv, err := m.create(newSecureValue, actorUID) + return createdSv, true, err +} + +func (m *model) delete(namespace, name string) (*secretv0alpha1.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) (*secretv0alpha1.SecureValueList, error) { + out := make([]secretv0alpha1.SecureValue, 0) + + for _, v := range m.secureValues { + if v.Namespace == namespace && v.active { + out = append(out, *v.SecureValue) + } + } + + return &secretv0alpha1.SecureValueList{Items: out}, nil +} + +func (m *model) decrypt(decrypter, namespace, name string) (map[string]service.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]service.DecryptResult{ + name: service.NewDecryptResultValue(&v.DeepCopy().Spec.Value), + }, nil + } + + return map[string]service.DecryptResult{ + name: service.NewDecryptResultErr(contracts.ErrDecryptNotAuthorized), + }, nil + } + } + return map[string]service.DecryptResult{ + name: service.NewDecryptResultErr(contracts.ErrDecryptNotFound), + }, nil +} + +func (m *model) read(namespace, name string) (*secretv0alpha1.SecureValue, error) { + modelSv := m.readActiveVersion(namespace, name) + if modelSv == nil { + return nil, contracts.ErrSecureValueNotFound + } + return modelSv.SecureValue, nil +} + +var ( + decryptersGen = rapid.SampledFrom([]string{"svc1", "svc2", "svc3", "svc4", "svc5"}) + nameGen = rapid.SampledFrom([]string{"n1", "n2", "n3", "n4", "n5"}) + namespaceGen = rapid.SampledFrom([]string{"ns1", "ns2", "ns3", "ns4", "ns5"}) + anySecureValueGen = rapid.Custom(func(t *rapid.T) *secretv0alpha1.SecureValue { + return &secretv0alpha1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: nameGen.Draw(t, "name"), + Namespace: namespaceGen.Draw(t, "ns"), + }, + Spec: secretv0alpha1.SecureValueSpec{ + Description: rapid.SampledFrom([]string{"d1", "d2", "d3", "d4", "d5"}).Draw(t, "description"), + Value: secretv0alpha1.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: secretv0alpha1.SecureValueStatus{}, + } + }) + updateSecureValueGen = rapid.Custom(func(t *rapid.T) *secretv0alpha1.SecureValue { + sv := anySecureValueGen.Draw(t, "sv") + // Maybe update the secret value, maybe not + if !rapid.Bool().Draw(t, "should_update_value") { + sv.Spec.Value = "" + } + 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: nameGen.Draw(t, "name"), + decrypter: decryptersGen.Draw(t, "decrypter"), + } + }) +) + +type decryptInput struct { + namespace string + name string + decrypter string +} + +func TestModel(t *testing.T) { + t.Parallel() + + sv := &secretv0alpha1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sv1", + Namespace: "ns1", + }, + Spec: secretv0alpha1.SecureValueSpec{ + Description: "desc1", + Value: secretv0alpha1.NewExposedSecureValue("v1"), + Decrypters: []string{"decrypter1"}, + }, + Status: secretv0alpha1.SecureValueStatus{}, + } + + t.Run("creating secure values", func(t *testing.T) { + t.Parallel() + + m := newModel() + + // Create a secure value + sv1, err := m.create(sv.DeepCopy(), "actor-uid") + 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(sv.DeepCopy(), "actor-uid") + require.NoError(t, err) + require.Equal(t, sv.Namespace, sv2.Namespace) + require.Equal(t, sv.Name, sv2.Name) + require.EqualValues(t, 2, sv2.Status.Version) + }) + + t.Run("updating secure values", func(t *testing.T) { + t.Parallel() + + m := newModel() + + sv1, err := m.create(sv.DeepCopy(), "actor-uid") + require.NoError(t, err) + + // Create a new version of a secure value by updating it + sv2, _, err := m.update(sv1.DeepCopy(), "actor-uid") + require.NoError(t, err) + require.Equal(t, sv.Namespace, sv2.Namespace) + require.Equal(t, sv.Name, sv2.Name) + require.EqualValues(t, 2, sv2.Status.Version) + + // Try updating a secure value that doesn't exist without specifying a value for it + sv3 := sv2.DeepCopy() + sv3.Name = "i_dont_exist" + sv3.Spec.Value = "" + _, _, err = m.update(sv3, "actor-uid") + 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 = secretv0alpha1.NewExposedSecureValue("sv4") + sv4, _, err = m.update(sv4, "actor-uid") + require.NoError(t, err) + require.EqualValues(t, 1, sv4.Status.Version) + }) + + t.Run("deleting a secure value", func(t *testing.T) { + t.Parallel() + + m := newModel() + + sv1, err := m.create(sv.DeepCopy(), "actor-uid") + require.NoError(t, err) + + // Deleting a secure value + 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) + require.ErrorIs(t, err, contracts.ErrSecureValueNotFound) + }) + + t.Run("listing secure values", func(t *testing.T) { + t.Parallel() + + m := newModel() + + // No secure values exist yet + 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(sv.DeepCopy(), "actor-uid") + require.NoError(t, err) + + // 1 secure value exists and it should be returned + 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) + require.Equal(t, sv1.Name, list.Items[0].Name) + require.EqualValues(t, sv1.Status.Version, list.Items[0].Status.Version) + }) + + t.Run("decrypting secure values", func(t *testing.T) { + t.Parallel() + + m := newModel() + + // Decrypting a secure value that does not exist + result, err := m.decrypt("decrypter", "namespace", "name") + require.NoError(t, err) + require.Equal(t, 1, len(result)) + require.Nil(t, result["name"].Value()) + require.ErrorIs(t, result["name"].Error(), contracts.ErrDecryptNotFound) + + // Create a secure value + sv1, err := m.create(sv.DeepCopy(), "actor-uid") + require.NoError(t, err) + + // Decrypt the just created secure value + result, err = m.decrypt(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, result[sv1.Name].Value().DangerouslyExposeAndConsumeValue(), sv.DeepCopy().Spec.Value.DangerouslyExposeAndConsumeValue()) + }) +} + +func TestStateMachine(t *testing.T) { + t.Parallel() + + tt := t + + rapid.Check(t, func(t *rapid.T) { + sut := testutils.Setup(tt) + model := newModel() + + t.Repeat(map[string]func(*rapid.T){ + "create": func(t *rapid.T) { + sv := anySecureValueGen.Draw(t, "sv") + modelCreatedSv, modelErr := model.create(sv.DeepCopy(), "actor-uid") + + 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) + }, + "update": func(t *rapid.T) { + sv := updateSecureValueGen.Draw(t, "sv") + modelCreatedSv, _, modelErr := model.update(sv.DeepCopy(), "actor-uid") + createdSv, err := sut.UpdateSv(t.Context(), 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) + }, + "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) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + require.Equal(t, modelSv.Namespace, deletedSv.Namespace) + require.Equal(t, modelSv.Name, deletedSv.Name) + 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{ + "securevalues:read": {"securevalues:uid:*"}, + }) + modelList, modelErr := model.list(sv.Namespace) + list, err := sut.SecureValueService.List(authCtx, xkube.Namespace(sv.Namespace)) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + + require.Equal(t, len(modelList.Items), len(list.Items)) + + // PERFORMANCE: The lists are always small + for _, v1 := range modelList.Items { + if !slices.ContainsFunc(list.Items, func(v2 secretv0alpha1.SecureValue) bool { + return v2.Namespace == v1.Namespace && v2.Name == v1.Name && v2.Status.Version == v1.Status.Version + }) { + t.Fatalf("expected sut to return secure value ns=%+v name=%+v version=%+v in the result", v1.Namespace, v1.Name, v1.Status.Version) + } + } + }, + "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) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + require.Equal(t, modelSv.Namespace, readSv.Namespace) + require.Equal(t, modelSv.Name, readSv.Name) + require.Equal(t, modelSv.Status.Version, readSv.Status.Version) + }, + "decrypt": func(t *rapid.T) { + input := decryptGen.Draw(t, "decryptInput") + authCtx := testutils.CreateServiceAuthContext(t.Context(), input.decrypter, []string{fmt.Sprintf("secret.grafana.app/securevalues/%+v:decrypt", input.name)}) + modelResult, modelErr := model.decrypt(input.decrypter, input.namespace, input.name) + result, err := sut.DecryptService.Decrypt(authCtx, input.namespace, input.name) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + + require.Equal(t, len(modelResult), len(result)) + for name := range modelResult { + require.Equal(t, modelResult[name].Value(), result[name].Value()) + require.Equal(t, modelResult[name].Error(), result[name].Error()) + } + }, + }) + }) +}