* 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
441 lines
15 KiB
Go
441 lines
15 KiB
Go
package metadata_test
|
|
|
|
import (
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/utils/ptr"
|
|
"pgregory.net/rapid"
|
|
|
|
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
|
|
"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"
|
|
)
|
|
|
|
func TestModel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sv := &secretv1beta1.SecureValue{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "sv1",
|
|
Namespace: "ns1",
|
|
},
|
|
Spec: secretv1beta1.SecureValueSpec{
|
|
Description: "desc1",
|
|
Value: ptr.To(secretv1beta1.NewExposedSecureValue("v1")),
|
|
Decrypters: []string{"decrypter1"},
|
|
},
|
|
Status: secretv1beta1.SecureValueStatus{},
|
|
}
|
|
|
|
t.Run("creating secure values", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := testutils.NewModelGsm(nil)
|
|
now := time.Now()
|
|
|
|
// Create a secure value
|
|
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())
|
|
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 := testutils.NewModelGsm(nil)
|
|
|
|
now := time.Now()
|
|
|
|
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())
|
|
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 = nil
|
|
_, _, 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)
|
|
require.ErrorIs(t, err, contracts.ErrSecureValueNotFound)
|
|
})
|
|
|
|
t.Run("deleting a secure value", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := testutils.NewModelGsm(nil)
|
|
now := time.Now()
|
|
|
|
sv1, err := m.Create(now, sv.DeepCopy())
|
|
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 := testutils.NewModelGsm(nil)
|
|
now := time.Now()
|
|
|
|
// 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(now, sv.DeepCopy())
|
|
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 := testutils.NewModelGsm(nil)
|
|
now := time.Now()
|
|
|
|
// Decrypting a secure value that does not exist
|
|
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())
|
|
require.ErrorIs(t, result["name"].Error(), contracts.ErrDecryptNotFound)
|
|
|
|
// Create a secure value
|
|
secret := "v1"
|
|
sv1, err := m.Create(now, sv.DeepCopy())
|
|
require.NoError(t, err)
|
|
|
|
// Decrypt the just created secure value
|
|
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) {
|
|
t.Parallel()
|
|
|
|
tt := t
|
|
|
|
rapid.Check(t, func(t *rapid.T) {
|
|
sut := testutils.Setup(tt)
|
|
model := testutils.NewModelGsm(sut.ModelSecretsManager)
|
|
|
|
t.Repeat(map[string]func(*rapid.T){
|
|
"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)
|
|
return
|
|
}
|
|
require.Equal(t, modelCreatedSv.Namespace, createdSv.Namespace)
|
|
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 := 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)
|
|
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) {
|
|
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
|
|
}
|
|
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) {
|
|
ns := testutils.NamespaceGen.Draw(t, "ns")
|
|
authCtx := testutils.CreateUserAuthContext(t.Context(), ns, map[string][]string{
|
|
"securevalues:read": {"securevalues:uid:*"},
|
|
})
|
|
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
|
|
}
|
|
|
|
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 secretv1beta1.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) {
|
|
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
|
|
}
|
|
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 := 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
|
|
}
|
|
|
|
require.Equal(t, len(modelResult), len(result))
|
|
for name := range modelResult {
|
|
require.ErrorIs(t, modelResult[name].Error(), result[name].Error())
|
|
require.Equal(t, modelResult[name].Value(), result[name].Value())
|
|
}
|
|
},
|
|
"createKeeper": func(t *rapid.T) {
|
|
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)
|
|
return
|
|
}
|
|
require.Equal(t, modelKeeper.Name, keeper.Name)
|
|
},
|
|
"setKeeperAsActive": func(t *rapid.T) {
|
|
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)
|
|
return
|
|
}
|
|
},
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestSecureValueServiceExampleBased(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("shouldn't be able to decrypt using deleted secure value", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sut := testutils.Setup(t)
|
|
|
|
sv, err := sut.CreateSv(t.Context())
|
|
require.NoError(t, err)
|
|
|
|
readSv, err := sut.SecureValueService.Read(t.Context(), xkube.Namespace(sv.Namespace), sv.Name)
|
|
require.NoError(t, err)
|
|
require.Equal(t, sv.Status.Version, readSv.Status.Version)
|
|
|
|
deletedSv, err := sut.DeleteSv(t.Context(), sv.Namespace, sv.Name)
|
|
require.NoError(t, err)
|
|
require.Equal(t, sv.Status.Version, deletedSv.Status.Version)
|
|
|
|
result, err := sut.DecryptService.Decrypt(t.Context(), sv.Spec.Decrypters[0], sv.Namespace, sv.Name)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(result))
|
|
require.ErrorIs(t, result[sv.Name].Error(), contracts.ErrDecryptNotFound)
|
|
})
|
|
|
|
t.Run("should be able to use secrets that were created with a keeper that's inactive", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sut := testutils.Setup(t)
|
|
|
|
// - Create a secret with k1
|
|
k1, err := sut.KeeperMetadataStorage.Create(t.Context(), &secretv1beta1.Keeper{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "n1",
|
|
Name: "k1",
|
|
},
|
|
Spec: secretv1beta1.KeeperSpec{
|
|
Description: "description",
|
|
Aws: &secretv1beta1.KeeperAWSConfig{},
|
|
},
|
|
}, "actor-uid")
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(k1.Namespace), k1.Name))
|
|
|
|
value := secretv1beta1.NewExposedSecureValue("v1")
|
|
sv1, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(&secretv1beta1.SecureValue{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: k1.Namespace, Name: "s1"},
|
|
Spec: secretv1beta1.SecureValueSpec{
|
|
Description: "desc",
|
|
Value: &value,
|
|
},
|
|
}))
|
|
require.NoError(t, err)
|
|
require.Equal(t, k1.Name, sv1.Status.Keeper)
|
|
|
|
// - Set a new keeper as active
|
|
k2, err := sut.KeeperMetadataStorage.Create(t.Context(), &secretv1beta1.Keeper{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "n1",
|
|
Name: "k2",
|
|
},
|
|
Spec: secretv1beta1.KeeperSpec{
|
|
Description: "description",
|
|
Aws: &secretv1beta1.KeeperAWSConfig{},
|
|
},
|
|
}, "actor-uid")
|
|
require.NoError(t, err)
|
|
require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(k2.Namespace), k2.Name))
|
|
|
|
// - Read secure value created with inactive keeper
|
|
readSv, err := sut.SecureValueService.Read(t.Context(), xkube.Namespace(sv1.Namespace), sv1.Name)
|
|
require.NoError(t, err)
|
|
require.Equal(t, sv1.Namespace, readSv.Namespace)
|
|
require.Equal(t, sv1.Name, readSv.Name)
|
|
require.Equal(t, k1.Name, readSv.Status.Keeper)
|
|
|
|
// - Update secure value created with inactive keeper
|
|
newSv1 := sv1.DeepCopy()
|
|
newSv1.Spec.Description = "updated desc"
|
|
updatedSv, _, err := sut.SecureValueService.Update(t.Context(), newSv1, "actor-uid")
|
|
require.NoError(t, err)
|
|
require.Equal(t, sv1.Namespace, updatedSv.Namespace)
|
|
require.Equal(t, sv1.Name, updatedSv.Name)
|
|
require.Equal(t, k1.Name, updatedSv.Status.Keeper)
|
|
require.Equal(t, newSv1.Spec.Description, updatedSv.Spec.Description)
|
|
})
|
|
}
|