diff --git a/pkg/registry/apis/secret/service/inline_secure_value.go b/pkg/registry/apis/secret/service/inline_secure_value.go index 9e0cb9fce41..665bc622f54 100644 --- a/pkg/registry/apis/secret/service/inline_secure_value.go +++ b/pkg/registry/apis/secret/service/inline_secure_value.go @@ -217,5 +217,40 @@ func (s *inlineSecureValueService) CreateInline(ctx context.Context, owner commo } func (s *inlineSecureValueService) DeleteWhenOwnedByResource(ctx context.Context, owner common.ObjectReference, name string) error { - return fmt.Errorf("not implemented yet") + ctx, span := s.tracer.Start(ctx, "InlineSecureValueService.DeleteWhenOwnedByResource", trace.WithAttributes( + attribute.String("owner.namespace", owner.Namespace), + attribute.String("owner.apiGroup", owner.APIGroup), + attribute.String("owner.apiVersion", owner.APIVersion), + attribute.String("owner.kind", owner.Kind), + attribute.String("owner.name", owner.Name), + attribute.String("secureValue.name", name), + )) + defer span.End() + + authInfo, ok := authlib.AuthInfoFrom(ctx) + if !ok { + return fmt.Errorf("missing auth info in context") + } + + if owner.Namespace == "" || !authlib.NamespaceMatches(authInfo.GetNamespace(), owner.Namespace) { + return fmt.Errorf("owner namespace %s does not match auth info namespace %s", owner.Namespace, authInfo.GetNamespace()) + } + + if owner.APIGroup == "" || owner.APIVersion == "" || owner.Kind == "" || owner.Name == "" { + return fmt.Errorf("owner reference must have a valid API group, API version, kind and name") + } + + owned, err := s.isSecureValueOwnedByResource(ctx, owner, name) + if err != nil { + return fmt.Errorf("error checking if secure value %s is owned by %v: %w", name, owner, err) + } + + if owned { + if _, err := s.secureValueService.Delete(ctx, xkube.Namespace(owner.Namespace), name); err != nil { + return fmt.Errorf("error deleting secure value %s for owner %v: %w", name, owner, err) + } + } + + // if it is not owned, this is a no-op + return nil } diff --git a/pkg/registry/apis/secret/service/inline_secure_value_test.go b/pkg/registry/apis/secret/service/inline_secure_value_test.go index d108ce8f047..3c8742bffb3 100644 --- a/pkg/registry/apis/secret/service/inline_secure_value_test.go +++ b/pkg/registry/apis/secret/service/inline_secure_value_test.go @@ -5,8 +5,10 @@ import ( 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/service" "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace/noop" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -347,3 +349,164 @@ func TestIntegration_InlineSecureValue_CreateInline(t *testing.T) { 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 := service.ProvideInlineSecureValueService(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 := service.ProvideInlineSecureValueService(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 := service.ProvideInlineSecureValueService(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 := service.ProvideInlineSecureValueService(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 := service.ProvideInlineSecureValueService(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 := service.ProvideInlineSecureValueService(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 := service.ProvideInlineSecureValueService(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()) + }) +}