package inline_test import ( "testing" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace/noop" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/inline" "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" ) func TestIntegration_InlineSecureValue_CanReference(t *testing.T) { t.Parallel() tracer := noop.NewTracerProvider().Tracer("test") defaultNs := "org-1234" owner := common.ObjectReference{ APIGroup: "prometheus.datasource.grafana.app", APIVersion: "v1alpha1", Kind: "DataSourceConfig", Name: "test-datasource", Namespace: defaultNs, } t.Run("happy path with owned and shared secure values", func(t *testing.T) { t.Parallel() tu := testutils.Setup(t) sv1 := "test-secure-value-1" createdSv1, err := tu.CreateSv(t.Context(), func(cfg *testutils.CreateSvConfig) { cfg.Sv.Name = sv1 cfg.Sv.Namespace = defaultNs cfg.Sv.OwnerReferences = []metav1.OwnerReference{owner.ToOwnerReference()} }) require.NoError(t, err) require.NotNil(t, createdSv1) sv2 := "test-secure-value-2" createdSv2, err := tu.CreateSv(t.Context(), func(cfg *testutils.CreateSvConfig) { cfg.Sv.Name = sv2 cfg.Sv.Namespace = defaultNs }) require.NoError(t, err) require.NotNil(t, createdSv2) ctx := testutils.CreateUserAuthContext(t.Context(), defaultNs, map[string][]string{ "securevalues:read": {"securevalues:uid:" + sv2}, }) svc := inline.NewLocalInlineSecureValueService(tracer, tu.SecureValueService, tu.AccessClient) err = svc.CanReference(ctx, owner, sv1, sv2) require.NoError(t, err) }) t.Run("when the auth info is missing it returns an error", func(t *testing.T) { t.Parallel() svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil) err := svc.CanReference(t.Context(), common.ObjectReference{}) require.Error(t, err) }) t.Run("when the owner namespace does not match auth info namespace it returns an error", func(t *testing.T) { t.Parallel() svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil) reqNs := "org-2345" ctx := testutils.CreateUserAuthContext(t.Context(), reqNs, map[string][]string{}) err := svc.CanReference(ctx, owner) require.Error(t, err) }) t.Run("when the owner namespace is empty it returns an error", func(t *testing.T) { t.Parallel() svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil) ctx := testutils.CreateUserAuthContext(t.Context(), defaultNs, map[string][]string{}) err := svc.CanReference(ctx, common.ObjectReference{}) require.Error(t, err) }) t.Run("when the owner reference has empty fields it returns an error", func(t *testing.T) { t.Parallel() svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil) owner := common.ObjectReference{ Namespace: defaultNs, } ctx := testutils.CreateUserAuthContext(t.Context(), defaultNs, map[string][]string{}) err := svc.CanReference(ctx, owner) require.Error(t, err) owner.APIGroup = "prometheus.datasource.grafana.app" require.Error(t, svc.CanReference(ctx, owner)) owner.APIGroup = "" owner.APIVersion = "v1alpha1" require.Error(t, svc.CanReference(ctx, owner)) owner.APIVersion = "" owner.Kind = "DataSourceConfig" require.Error(t, svc.CanReference(ctx, owner)) owner.Kind = "" owner.Name = "test-datasource" require.Error(t, svc.CanReference(ctx, owner)) owner.Name = "" }) t.Run("when no secure values are provided it returns an error", func(t *testing.T) { t.Parallel() svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil) ctx := testutils.CreateUserAuthContext(t.Context(), defaultNs, map[string][]string{}) err := svc.CanReference(ctx, owner) require.Error(t, err) }) t.Run("when the secure value does not exist, it returns an error", func(t *testing.T) { t.Parallel() tu := testutils.Setup(t) svc := inline.NewLocalInlineSecureValueService(tracer, tu.SecureValueService, nil) ctx := testutils.CreateUserAuthContext(t.Context(), defaultNs, map[string][]string{}) err := svc.CanReference(ctx, owner, "non-existent-sv") require.Error(t, err) }) t.Run("when the secure value is owned by a different resource, it returns an error", func(t *testing.T) { t.Parallel() tu := testutils.Setup(t) differentOwner := common.ObjectReference{ APIGroup: "prometheus.datasource.grafana.app", APIVersion: "v1alpha1", Kind: "DataSourceConfig", Name: "different-datasource", Namespace: defaultNs, } sv1 := "test-secure-value-1" createdSv1, err := tu.CreateSv(t.Context(), func(cfg *testutils.CreateSvConfig) { cfg.Sv.Name = sv1 cfg.Sv.Namespace = defaultNs cfg.Sv.OwnerReferences = []metav1.OwnerReference{differentOwner.ToOwnerReference()} }) require.NoError(t, err) require.NotNil(t, createdSv1) ctx := testutils.CreateUserAuthContext(t.Context(), defaultNs, map[string][]string{}) svc := inline.NewLocalInlineSecureValueService(tracer, tu.SecureValueService, nil) err = svc.CanReference(ctx, owner, sv1) require.Error(t, err) }) t.Run("when the request identity is not a user nor a service account, it returns an error", func(t *testing.T) { t.Parallel() tu := testutils.Setup(t) sv1 := "test-secure-value-1" _, err := tu.CreateSv(t.Context(), func(cfg *testutils.CreateSvConfig) { cfg.Sv.Name = sv1 cfg.Sv.Namespace = defaultNs }) require.NoError(t, err) ctx := identity.WithServiceIdentityContext(t.Context(), 1234) svc := inline.NewLocalInlineSecureValueService(tracer, tu.SecureValueService, nil) err = svc.CanReference(ctx, owner, sv1) require.Error(t, err) }) t.Run("when the identity does not have permissions to read the secure value, it returns an error", func(t *testing.T) { t.Parallel() tu := testutils.Setup(t) sv1 := "test-secure-value-1" _, err := tu.CreateSv(t.Context(), func(cfg *testutils.CreateSvConfig) { cfg.Sv.Name = sv1 cfg.Sv.Namespace = defaultNs }) require.NoError(t, err) svc := inline.NewLocalInlineSecureValueService(tracer, tu.SecureValueService, tu.AccessClient) ctx := testutils.CreateUserAuthContext(t.Context(), defaultNs, map[string][]string{ "securevalues:read": {"securevalues:uid:another-sv"}, // can read, but another resource! }) err = svc.CanReference(ctx, owner, sv1) require.Error(t, err) ctx = testutils.CreateUserAuthContext(t.Context(), defaultNs, nil) err = svc.CanReference(ctx, owner, sv1) require.Error(t, err) }) } func TestIntegration_InlineSecureValue_CreateInline(t *testing.T) { t.Parallel() tracer := noop.NewTracerProvider().Tracer("test") defaultNs := "org-1234" owner := common.ObjectReference{ APIGroup: "prometheus.datasource.grafana.app", APIVersion: "v1alpha1", Kind: "DataSourceConfig", Name: "test-datasource", Namespace: defaultNs, } t.Run("happy path creates an inline secure value", func(t *testing.T) { t.Parallel() tu := testutils.Setup(t) rawSecret := "test-value" secret := common.NewSecretValue(rawSecret) serviceIdentity := "service-identity" createAuthCtx := testutils.CreateOBOAuthContext(t.Context(), serviceIdentity, owner.Namespace, nil, nil) svc := inline.NewLocalInlineSecureValueService(tracer, tu.SecureValueService, nil) createdName, err := svc.CreateInline(createAuthCtx, owner, secret) require.NoError(t, err) require.NotEmpty(t, createdName) decryptedValues, err := tu.DecryptService.Decrypt(t.Context(), serviceIdentity, owner.Namespace, createdName) require.NoError(t, err) decryptedResult, ok := decryptedValues[createdName] require.True(t, ok) require.Equal(t, decryptedResult.Value().DangerouslyExposeAndConsumeValue(), rawSecret) // can also decrypt with the owner.APIGroup as a decrypter decryptedValues, err = tu.DecryptService.Decrypt(t.Context(), owner.APIGroup, owner.Namespace, createdName) require.NoError(t, err) decryptedResult, ok = decryptedValues[createdName] require.True(t, ok) require.Equal(t, decryptedResult.Value().DangerouslyExposeAndConsumeValue(), rawSecret) // Ignores empty values and duplicates decryptedValues, err = tu.DecryptService.Decrypt(t.Context(), owner.APIGroup, owner.Namespace, "", createdName, "", createdName, "") // Empty and duplicate requested names require.NoError(t, err) require.Len(t, decryptedValues, 1) _, ok = decryptedValues[createdName] require.True(t, ok) // empty request decryptedValues, err = tu.DecryptService.Decrypt(t.Context(), owner.APIGroup, owner.Namespace, "") require.NoError(t, err) require.Len(t, decryptedValues, 0) // << empty }) t.Run("when the auth info is missing it returns an error", func(t *testing.T) { t.Parallel() svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil) _, err := svc.CreateInline(t.Context(), common.ObjectReference{}, "") require.Error(t, err) }) t.Run("when the request identity is not a user nor a service account, it returns an error", func(t *testing.T) { t.Parallel() svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil) createAuthCtx := testutils.CreateServiceAuthContext(t.Context(), "service-identity", defaultNs, nil) _, err := svc.CreateInline(createAuthCtx, common.ObjectReference{}, "") require.Error(t, err) }) t.Run("when the owner namespace does not match auth info namespace it returns an error", func(t *testing.T) { t.Parallel() svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil) reqNs := "org-2345" createAuthCtx := testutils.CreateOBOAuthContext(t.Context(), "service-identity", reqNs, nil, nil) _, err := svc.CreateInline(createAuthCtx, owner, "") require.Error(t, err) }) t.Run("when the owner namespace is empty it returns an error", func(t *testing.T) { t.Parallel() svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil) createAuthCtx := testutils.CreateOBOAuthContext(t.Context(), "service-identity", defaultNs, nil, nil) _, err := svc.CreateInline(createAuthCtx, common.ObjectReference{}, "") require.Error(t, err) }) t.Run("when the owner reference has empty fields it returns an error", func(t *testing.T) { t.Parallel() svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil) owner := common.ObjectReference{ Namespace: defaultNs, } createAuthCtx := testutils.CreateOBOAuthContext(t.Context(), "service-identity", defaultNs, nil, nil) _, err := svc.CreateInline(createAuthCtx, owner, "") require.Error(t, err) owner.APIGroup = "prometheus.datasource.grafana.app" _, err = svc.CreateInline(createAuthCtx, owner, "") require.Error(t, err) owner.APIVersion = "v1alpha1" _, err = svc.CreateInline(createAuthCtx, owner, "") require.Error(t, err) owner.Kind = "DataSourceConfig" _, err = svc.CreateInline(createAuthCtx, owner, "") require.Error(t, err) owner.Kind = "" owner.Name = "test-datasource" _, err = svc.CreateInline(createAuthCtx, owner, "") require.Error(t, err) }) t.Run("when an empty secret is provided it returns an error", func(t *testing.T) { t.Parallel() svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil) createAuthCtx := testutils.CreateOBOAuthContext(t.Context(), "service-identity", defaultNs, nil, nil) _, err := svc.CreateInline(createAuthCtx, owner, "") require.Error(t, err) }) } func TestIntegration_InlineSecureValue_DeleteWhenOwnedByResource(t *testing.T) { t.Parallel() tracer := noop.NewTracerProvider().Tracer("test") defaultNs := "org-1234" owner := common.ObjectReference{ APIGroup: "prometheus.datasource.grafana.app", APIVersion: "v1alpha1", Kind: "DataSourceConfig", Name: "test-datasource", Namespace: defaultNs, } t.Run("happy path deletes an owned secure value", func(t *testing.T) { t.Parallel() tu := testutils.Setup(t) sv1 := "test-secure-value-1" createdSv1, err := tu.CreateSv(t.Context(), func(cfg *testutils.CreateSvConfig) { cfg.Sv.Name = sv1 cfg.Sv.Namespace = defaultNs cfg.Sv.OwnerReferences = []metav1.OwnerReference{owner.ToOwnerReference()} }) require.NoError(t, err) require.NotNil(t, createdSv1) svc := inline.NewLocalInlineSecureValueService(tracer, tu.SecureValueService, nil) ctx := testutils.CreateServiceAuthContext(t.Context(), "", defaultNs, nil) err = svc.DeleteWhenOwnedByResource(ctx, owner, sv1) require.NoError(t, err) // make sure it got deleted sv, err := tu.SecureValueService.Read(ctx, xkube.Namespace(owner.Namespace), sv1) require.ErrorIs(t, err, contracts.ErrSecureValueNotFound) require.Nil(t, sv) }) t.Run("when the auth info is missing it returns an error", func(t *testing.T) { t.Parallel() svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil) err := svc.DeleteWhenOwnedByResource(t.Context(), common.ObjectReference{}, "") require.Error(t, err) }) t.Run("when the owner namespace does not match auth info namespace it returns an error", func(t *testing.T) { t.Parallel() svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil) reqNs := "org-2345" ctx := testutils.CreateUserAuthContext(t.Context(), reqNs, map[string][]string{}) err := svc.DeleteWhenOwnedByResource(ctx, owner, "") require.Error(t, err) }) t.Run("when the owner namespace is empty it returns an error", func(t *testing.T) { t.Parallel() svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil) ctx := testutils.CreateUserAuthContext(t.Context(), defaultNs, map[string][]string{}) err := svc.DeleteWhenOwnedByResource(ctx, common.ObjectReference{}, "") require.Error(t, err) }) t.Run("when the owner reference has empty fields it returns an error", func(t *testing.T) { t.Parallel() svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil) owner := common.ObjectReference{ Namespace: defaultNs, } createAuthCtx := testutils.CreateOBOAuthContext(t.Context(), "service-identity", defaultNs, nil, nil) require.Error(t, svc.DeleteWhenOwnedByResource(createAuthCtx, owner, "")) owner.APIGroup = "prometheus.datasource.grafana.app" require.Error(t, svc.DeleteWhenOwnedByResource(createAuthCtx, owner, "")) owner.APIVersion = "v1alpha1" require.Error(t, svc.DeleteWhenOwnedByResource(createAuthCtx, owner, "")) owner.Kind = "DataSourceConfig" require.Error(t, svc.DeleteWhenOwnedByResource(createAuthCtx, owner, "")) owner.Kind = "" owner.Name = "test-datasource" require.Error(t, svc.DeleteWhenOwnedByResource(createAuthCtx, owner, "")) }) t.Run("when the secure value exists but the owner does not match, it returns an error", func(t *testing.T) { t.Parallel() tu := testutils.Setup(t) sv1 := "test-secure-value-1" createdSv1, err := tu.CreateSv(t.Context(), func(cfg *testutils.CreateSvConfig) { cfg.Sv.Name = sv1 cfg.Sv.Namespace = defaultNs cfg.Sv.OwnerReferences = []metav1.OwnerReference{ { APIVersion: "another.example.com/v0alpha1", Kind: "another-kind", Name: "another-name", }, } }) require.NoError(t, err) require.NotNil(t, createdSv1) svc := inline.NewLocalInlineSecureValueService(tracer, tu.SecureValueService, nil) ctx := testutils.CreateServiceAuthContext(t.Context(), "", defaultNs, nil) err = svc.DeleteWhenOwnedByResource(ctx, owner, sv1) require.Error(t, err) // make sure it still exists sv, err := tu.SecureValueService.Read(ctx, xkube.Namespace(owner.Namespace), sv1) require.NoError(t, err) require.NotNil(t, sv) require.Equal(t, sv1, sv.GetName()) }) t.Run("when the secure value exists but it is shared (no owner), it does not return an error (noop)", func(t *testing.T) { t.Parallel() tu := testutils.Setup(t) sv1 := "test-secure-value-1" createdSv1, err := tu.CreateSv(t.Context(), func(cfg *testutils.CreateSvConfig) { cfg.Sv.Name = sv1 cfg.Sv.Namespace = defaultNs }) require.NoError(t, err) require.NotNil(t, createdSv1) svc := inline.NewLocalInlineSecureValueService(tracer, tu.SecureValueService, nil) ctx := testutils.CreateServiceAuthContext(t.Context(), "", defaultNs, nil) err = svc.DeleteWhenOwnedByResource(ctx, owner, sv1) require.NoError(t, err) // make sure it still exists sv, err := tu.SecureValueService.Read(ctx, xkube.Namespace(owner.Namespace), sv1) require.NoError(t, err) require.NotNil(t, sv) require.Equal(t, sv1, sv.GetName()) }) t.Run("when a secure value is owned and exists but another one doesnt, it deletes the first one but returns an error", func(t *testing.T) { t.Parallel() tu := testutils.Setup(t) sv1 := "test-secure-value-1" createdSv1, err := tu.CreateSv(t.Context(), func(cfg *testutils.CreateSvConfig) { cfg.Sv.Name = sv1 cfg.Sv.Namespace = defaultNs cfg.Sv.OwnerReferences = []metav1.OwnerReference{owner.ToOwnerReference()} }) require.NoError(t, err) require.NotNil(t, createdSv1) svc := inline.NewLocalInlineSecureValueService(tracer, tu.SecureValueService, nil) ctx := testutils.CreateServiceAuthContext(t.Context(), "", defaultNs, nil) err = svc.DeleteWhenOwnedByResource(ctx, owner, sv1, "does-not-exist") require.ErrorIs(t, err, contracts.ErrSecureValueNotFound) // got deleted sv, err := tu.SecureValueService.Read(ctx, xkube.Namespace(owner.Namespace), sv1) require.ErrorIs(t, err, contracts.ErrSecureValueNotFound) require.Nil(t, sv) }) }