diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 7e8ca9cf848..30ed736c915 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -210,6 +210,10 @@ export interface FeatureToggles { */ provisioning?: boolean; /** + * Experimental feature to use the secrets service for provisioning instead of the legacy secrets + */ + provisioningSecretsService?: boolean; + /** * Start an additional https handler and write kubectl options */ grafanaAPIServerEnsureKubectlAccess?: boolean; diff --git a/pkg/registry/apis/provisioning/controller/repository.go b/pkg/registry/apis/provisioning/controller/repository.go index 7fed0bad25d..4b673907e0f 100644 --- a/pkg/registry/apis/provisioning/controller/repository.go +++ b/pkg/registry/apis/provisioning/controller/repository.go @@ -25,7 +25,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" ) @@ -60,7 +59,6 @@ type RepositoryController struct { repoSynced cache.InformerSynced parsers resources.ParserFactory logger logging.Logger - secrets secrets.Service dualwrite dualwrite.Service jobs jobs.Queue @@ -87,7 +85,6 @@ func NewRepositoryController( clients resources.ClientFactory, tester RepositoryTester, jobs jobs.Queue, - secrets secrets.Service, dualwrite dualwrite.Service, ) (*RepositoryController, error) { rc := &RepositoryController{ @@ -110,7 +107,6 @@ func NewRepositoryController( tester: tester, jobs: jobs, logger: logging.DefaultLogger.With("logger", loggerName), - secrets: secrets, dualwrite: dualwrite, } diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index dde0de8abc5..9c77819e68c 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -57,7 +57,6 @@ import ( "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" - grafanasecrets "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/storage/unified/resource" @@ -89,17 +88,17 @@ type APIBuilder struct { jobs.Queue jobs.Store } - jobHistory jobs.History - tester *RepositoryTester - resourceLister resources.ResourceLister - repositoryLister listers.RepositoryLister - legacyMigrator legacy.LegacyMigrator - storageStatus dualwrite.Service - unified resource.ResourceClient - secrets secrets.Service - client client.ProvisioningV0alpha1Interface - access authlib.AccessChecker - statusPatcher *controller.RepositoryStatusPatcher + jobHistory jobs.History + tester *RepositoryTester + resourceLister resources.ResourceLister + repositoryLister listers.RepositoryLister + legacyMigrator legacy.LegacyMigrator + storageStatus dualwrite.Service + unified resource.ResourceClient + repositorySecrets secrets.RepositorySecrets + client client.ProvisioningV0alpha1Interface + access authlib.AccessChecker + statusPatcher *controller.RepositoryStatusPatcher // Extras provides additional functionality to the API. extras []Extra availableRepositoryTypes map[provisioning.RepositoryType]bool @@ -117,7 +116,7 @@ func NewAPIBuilder( ghFactory *github.Factory, legacyMigrator legacy.LegacyMigrator, storageStatus dualwrite.Service, - secrets secrets.Service, + repositorySecrets secrets.RepositorySecrets, access authlib.AccessChecker, tracer tracing.Tracer, extraBuilders []ExtraBuilder, @@ -139,7 +138,7 @@ func NewAPIBuilder( legacyMigrator: legacyMigrator, storageStatus: storageStatus, unified: unified, - secrets: secrets, + repositorySecrets: repositorySecrets, access: access, jobHistory: jobs.NewJobHistoryCache(), availableRepositoryTypes: map[provisioning.RepositoryType]bool{ @@ -177,8 +176,7 @@ func RegisterAPIService( legacyMigrator legacy.LegacyMigrator, storageStatus dualwrite.Service, usageStatsService usagestats.Service, - // FIXME: use multi-tenant service when one exists. In this state, we can't make this a multi-tenant service! - secretsSvc grafanasecrets.Service, + repositorySecrets secrets.RepositorySecrets, tracer tracing.Tracer, extraBuilders []ExtraBuilder, ) (*APIBuilder, error) { @@ -195,7 +193,8 @@ func RegisterAPIService( filepath.Join(cfg.DataPath, "clone"), // where repositories are cloned (temporarialy for now) configProvider, ghFactory, legacyMigrator, storageStatus, - secrets.NewSingleTenant(secretsSvc), access, + repositorySecrets, + access, tracer, extraBuilders, ) @@ -466,11 +465,11 @@ func (b *APIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admis } if err := b.encryptGithubToken(ctx, r); err != nil { - return fmt.Errorf("failed to encrypt secrets: %w", err) + return fmt.Errorf("failed to encrypt github secrets: %w", err) } if err := b.encryptGitToken(ctx, r); err != nil { - return fmt.Errorf("failed to encrypt secrets: %w", err) + return fmt.Errorf("failed to encrypt git secrets: %w", err) } // Mutate the repository with any extra mutators @@ -485,28 +484,32 @@ func (b *APIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admis // TODO: move this to a more appropriate place func (b *APIBuilder) encryptGithubToken(ctx context.Context, repo *provisioning.Repository) error { - var err error if repo.Spec.GitHub != nil && repo.Spec.GitHub.Token != "" { - repo.Spec.GitHub.EncryptedToken, err = b.secrets.Encrypt(ctx, []byte(repo.Spec.GitHub.Token)) + secretName := repo.Name + "-github-token" + + nameOrValue, err := b.repositorySecrets.Encrypt(ctx, repo, secretName, repo.Spec.GitHub.Token) if err != nil { return err } repo.Spec.GitHub.Token = "" + repo.Spec.GitHub.EncryptedToken = nameOrValue } return nil } // TODO: move this to a more appropriate place +// TODO: make this one more generic func (b *APIBuilder) encryptGitToken(ctx context.Context, repo *provisioning.Repository) error { - var err error - if repo.Spec.Git != nil && - repo.Spec.Git.Token != "" { - repo.Spec.Git.EncryptedToken, err = b.secrets.Encrypt(ctx, []byte(repo.Spec.Git.Token)) + if repo.Spec.Git != nil && repo.Spec.Git.Token != "" { + secretName := repo.Name + "-git-token" + nameOrValue, err := b.repositorySecrets.Encrypt(ctx, repo, secretName, repo.Spec.Git.Token) if err != nil { return err } + + repo.Spec.Git.EncryptedToken = nameOrValue repo.Spec.Git.Token = "" } @@ -701,7 +704,6 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH b.clients, &repository.Tester{}, b.jobs, - b.secrets, b.storageStatus, ) if err != nil { @@ -1215,8 +1217,8 @@ func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repositor case provisioning.GitRepositoryType: // Decrypt token if needed token := r.Spec.Git.Token - if token == "" { - decrypted, err := b.secrets.Decrypt(ctx, r.Spec.Git.EncryptedToken) + if token == "" && len(r.Spec.Git.EncryptedToken) > 0 { + decrypted, err := b.repositorySecrets.Decrypt(ctx, r, string(r.Spec.Git.EncryptedToken)) if err != nil { return nil, fmt.Errorf("decrypt git token: %w", err) } @@ -1242,7 +1244,7 @@ func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repositor // Decrypt GitHub token if needed ghToken := ghCfg.Token if ghToken == "" && len(ghCfg.EncryptedToken) > 0 { - decrypted, err := b.secrets.Decrypt(ctx, ghCfg.EncryptedToken) + decrypted, err := b.repositorySecrets.Decrypt(ctx, r, string(ghCfg.EncryptedToken)) if err != nil { return nil, fmt.Errorf("decrypt github token: %w", err) } diff --git a/pkg/registry/apis/provisioning/secrets/legacy_secret.go b/pkg/registry/apis/provisioning/secrets/legacy_secret.go new file mode 100644 index 00000000000..6190e1389ff --- /dev/null +++ b/pkg/registry/apis/provisioning/secrets/legacy_secret.go @@ -0,0 +1,37 @@ +package secrets + +import ( + "context" + + "github.com/grafana/grafana/pkg/services/secrets" +) + +// A secrets encryption service. It only operates on values, no names or similar. +// It is likely we will need to change this when the multi-tenant service comes around. +// +// FIXME: this is a temporary service/package until we can make use of +// the new secrets service in app platform. +// +//go:generate mockery --name LegacyService --structname MockLegacyService --inpackage --filename legacy_secret_mock.go --with-expecter +type LegacyService interface { + Encrypt(ctx context.Context, data []byte) ([]byte, error) + Decrypt(ctx context.Context, data []byte) ([]byte, error) +} + +var _ LegacyService = (*singleTenant)(nil) + +type singleTenant struct { + inner secrets.Service +} + +func NewSingleTenant(svc secrets.Service) LegacyService { + return &singleTenant{svc} +} + +func (s *singleTenant) Encrypt(ctx context.Context, data []byte) ([]byte, error) { + return s.inner.Encrypt(ctx, data, secrets.WithoutScope()) +} + +func (s *singleTenant) Decrypt(ctx context.Context, data []byte) ([]byte, error) { + return s.inner.Decrypt(ctx, data) +} diff --git a/pkg/registry/apis/provisioning/secrets/legacy_secret_mock.go b/pkg/registry/apis/provisioning/secrets/legacy_secret_mock.go new file mode 100644 index 00000000000..01f3ee1f853 --- /dev/null +++ b/pkg/registry/apis/provisioning/secrets/legacy_secret_mock.go @@ -0,0 +1,154 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package secrets + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// MockLegacyService is an autogenerated mock type for the LegacyService type +type MockLegacyService struct { + mock.Mock +} + +type MockLegacyService_Expecter struct { + mock *mock.Mock +} + +func (_m *MockLegacyService) EXPECT() *MockLegacyService_Expecter { + return &MockLegacyService_Expecter{mock: &_m.Mock} +} + +// Decrypt provides a mock function with given fields: ctx, data +func (_m *MockLegacyService) Decrypt(ctx context.Context, data []byte) ([]byte, error) { + ret := _m.Called(ctx, data) + + if len(ret) == 0 { + panic("no return value specified for Decrypt") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []byte) ([]byte, error)); ok { + return rf(ctx, data) + } + if rf, ok := ret.Get(0).(func(context.Context, []byte) []byte); ok { + r0 = rf(ctx, data) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []byte) error); ok { + r1 = rf(ctx, data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockLegacyService_Decrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Decrypt' +type MockLegacyService_Decrypt_Call struct { + *mock.Call +} + +// Decrypt is a helper method to define mock.On call +// - ctx context.Context +// - data []byte +func (_e *MockLegacyService_Expecter) Decrypt(ctx interface{}, data interface{}) *MockLegacyService_Decrypt_Call { + return &MockLegacyService_Decrypt_Call{Call: _e.mock.On("Decrypt", ctx, data)} +} + +func (_c *MockLegacyService_Decrypt_Call) Run(run func(ctx context.Context, data []byte)) *MockLegacyService_Decrypt_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]byte)) + }) + return _c +} + +func (_c *MockLegacyService_Decrypt_Call) Return(_a0 []byte, _a1 error) *MockLegacyService_Decrypt_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockLegacyService_Decrypt_Call) RunAndReturn(run func(context.Context, []byte) ([]byte, error)) *MockLegacyService_Decrypt_Call { + _c.Call.Return(run) + return _c +} + +// Encrypt provides a mock function with given fields: ctx, data +func (_m *MockLegacyService) Encrypt(ctx context.Context, data []byte) ([]byte, error) { + ret := _m.Called(ctx, data) + + if len(ret) == 0 { + panic("no return value specified for Encrypt") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []byte) ([]byte, error)); ok { + return rf(ctx, data) + } + if rf, ok := ret.Get(0).(func(context.Context, []byte) []byte); ok { + r0 = rf(ctx, data) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []byte) error); ok { + r1 = rf(ctx, data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockLegacyService_Encrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Encrypt' +type MockLegacyService_Encrypt_Call struct { + *mock.Call +} + +// Encrypt is a helper method to define mock.On call +// - ctx context.Context +// - data []byte +func (_e *MockLegacyService_Expecter) Encrypt(ctx interface{}, data interface{}) *MockLegacyService_Encrypt_Call { + return &MockLegacyService_Encrypt_Call{Call: _e.mock.On("Encrypt", ctx, data)} +} + +func (_c *MockLegacyService_Encrypt_Call) Run(run func(ctx context.Context, data []byte)) *MockLegacyService_Encrypt_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]byte)) + }) + return _c +} + +func (_c *MockLegacyService_Encrypt_Call) Return(_a0 []byte, _a1 error) *MockLegacyService_Encrypt_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockLegacyService_Encrypt_Call) RunAndReturn(run func(context.Context, []byte) ([]byte, error)) *MockLegacyService_Encrypt_Call { + _c.Call.Return(run) + return _c +} + +// NewMockLegacyService creates a new instance of MockLegacyService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockLegacyService(t interface { + mock.TestingT + Cleanup(func()) +}) *MockLegacyService { + mock := &MockLegacyService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/secrets/mocks/decrypt_service_mock.go b/pkg/registry/apis/provisioning/secrets/mocks/decrypt_service_mock.go new file mode 100644 index 00000000000..50ad9773e93 --- /dev/null +++ b/pkg/registry/apis/provisioning/secrets/mocks/decrypt_service_mock.go @@ -0,0 +1,111 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package mocks + +import ( + context "context" + + service "github.com/grafana/grafana/pkg/registry/apis/secret/service" + mock "github.com/stretchr/testify/mock" +) + +// MockDecryptService is an autogenerated mock type for the DecryptService type +type MockDecryptService struct { + mock.Mock +} + +type MockDecryptService_Expecter struct { + mock *mock.Mock +} + +func (_m *MockDecryptService) EXPECT() *MockDecryptService_Expecter { + return &MockDecryptService_Expecter{mock: &_m.Mock} +} + +// Decrypt provides a mock function with given fields: ctx, namespace, names +func (_m *MockDecryptService) Decrypt(ctx context.Context, namespace string, names ...string) (map[string]service.DecryptResult, error) { + _va := make([]interface{}, len(names)) + for _i := range names { + _va[_i] = names[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, namespace) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Decrypt") + } + + var r0 map[string]service.DecryptResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...string) (map[string]service.DecryptResult, error)); ok { + return rf(ctx, namespace, names...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, ...string) map[string]service.DecryptResult); ok { + r0 = rf(ctx, namespace, names...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]service.DecryptResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, ...string) error); ok { + r1 = rf(ctx, namespace, names...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockDecryptService_Decrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Decrypt' +type MockDecryptService_Decrypt_Call struct { + *mock.Call +} + +// Decrypt is a helper method to define mock.On call +// - ctx context.Context +// - namespace string +// - names ...string +func (_e *MockDecryptService_Expecter) Decrypt(ctx interface{}, namespace interface{}, names ...interface{}) *MockDecryptService_Decrypt_Call { + return &MockDecryptService_Decrypt_Call{Call: _e.mock.On("Decrypt", + append([]interface{}{ctx, namespace}, names...)...)} +} + +func (_c *MockDecryptService_Decrypt_Call) Run(run func(ctx context.Context, namespace string, names ...string)) *MockDecryptService_Decrypt_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]string, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + run(args[0].(context.Context), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *MockDecryptService_Decrypt_Call) Return(_a0 map[string]service.DecryptResult, _a1 error) *MockDecryptService_Decrypt_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockDecryptService_Decrypt_Call) RunAndReturn(run func(context.Context, string, ...string) (map[string]service.DecryptResult, error)) *MockDecryptService_Decrypt_Call { + _c.Call.Return(run) + return _c +} + +// NewMockDecryptService creates a new instance of MockDecryptService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockDecryptService(t interface { + mock.TestingT + Cleanup(func()) +}) *MockDecryptService { + mock := &MockDecryptService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/secrets/repository.go b/pkg/registry/apis/provisioning/secrets/repository.go new file mode 100644 index 00000000000..43ec7d31dc4 --- /dev/null +++ b/pkg/registry/apis/provisioning/secrets/repository.go @@ -0,0 +1,98 @@ +package secrets + +import ( + "context" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/secret/service" + "github.com/grafana/grafana/pkg/services/featuremgmt" + grafanasecrets "github.com/grafana/grafana/pkg/services/secrets" +) + +func ProvideRepositorySecrets( + features featuremgmt.FeatureToggles, + legacySecretsSvc grafanasecrets.Service, + secretsSvc *service.SecureValueService, + decryptSvc service.DecryptService, +) RepositorySecrets { + return NewRepositorySecrets(features, NewSecretsService(secretsSvc, decryptSvc), NewSingleTenant(legacySecretsSvc)) +} + +//go:generate mockery --name RepositorySecrets --structname MockRepositorySecrets --inpackage --filename repository_secrets_mock.go --with-expecter +type RepositorySecrets interface { + Encrypt(ctx context.Context, r *provisioning.Repository, name string, data string) (nameOrValue []byte, err error) + Decrypt(ctx context.Context, r *provisioning.Repository, nameOrValue string) (data []byte, err error) +} + +// repositorySecrets provides a unified interface for encrypting and decrypting repository secrets, +// supporting both the legacy and new secrets services. The active backend is determined by the +// FlagProvisioningSecretsService feature flag: +// - If enabled, operations use the new secrets service. +// - If disabled, operations use the legacy secrets service. +// +// This abstraction enables a seamless migration path between secret backends without breaking +// existing functionality. Once migration is complete and the legacy service is deprecated, +// this wrapper should be removed. +type repositorySecrets struct { + features featuremgmt.FeatureToggles + secretsSvc Service + legacySecrets LegacyService +} + +func NewRepositorySecrets(features featuremgmt.FeatureToggles, secretsSvc Service, legacySecrets LegacyService) RepositorySecrets { + return &repositorySecrets{ + features: features, + secretsSvc: secretsSvc, + legacySecrets: legacySecrets, + } +} + +// Encrypt encrypts the data and returns the name or value of the encrypted data +// If the feature flag is disabled, it uses the legacy secrets service +// If the feature flag is enabled, it uses the secrets service +func (s *repositorySecrets) Encrypt(ctx context.Context, r *provisioning.Repository, name string, data string) (nameOrValue []byte, err error) { + if s.features.IsEnabled(ctx, featuremgmt.FlagProvisioningSecretsService) { + encrypted, err := s.secretsSvc.Encrypt(ctx, r.GetNamespace(), name, data) + if err != nil { + return nil, err + } + return []byte(encrypted), err + } + + encrypted, err := s.legacySecrets.Encrypt(ctx, []byte(data)) + if err != nil { + return nil, err + } + + return encrypted, nil +} + +// Decrypt attempts to retrieve and decrypt secret data for a repository. +// If the provisioning secrets service feature flag is enabled, it tries the new secrets service first. +// - On success, returns the decrypted data. +// - On failure, falls back to the legacy secrets service. +// +// If the feature flag is disabled, it tries the legacy secrets service first. +// - On success, returns the decrypted data. +// - On failure, falls back to the new secrets service. +// +// This dual-path logic is intended to support migration between secret backends. +func (s *repositorySecrets) Decrypt(ctx context.Context, r *provisioning.Repository, nameOrValue string) ([]byte, error) { + if s.features.IsEnabled(ctx, featuremgmt.FlagProvisioningSecretsService) { + data, err := s.secretsSvc.Decrypt(ctx, r.GetNamespace(), nameOrValue) + if err == nil { + return data, nil + } + + // If the new service fails, fall back to legacy + return s.legacySecrets.Decrypt(ctx, []byte(nameOrValue)) + } + + // If the new service is disabled, use the legacy service first + data, err := s.legacySecrets.Decrypt(ctx, []byte(nameOrValue)) + if err == nil { + return data, nil + } + + return s.secretsSvc.Decrypt(ctx, r.GetNamespace(), nameOrValue) +} diff --git a/pkg/registry/apis/provisioning/secrets/repository_secrets_mock.go b/pkg/registry/apis/provisioning/secrets/repository_secrets_mock.go new file mode 100644 index 00000000000..526f10b6251 --- /dev/null +++ b/pkg/registry/apis/provisioning/secrets/repository_secrets_mock.go @@ -0,0 +1,158 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package secrets + +import ( + context "context" + + v0alpha1 "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + mock "github.com/stretchr/testify/mock" +) + +// MockRepositorySecrets is an autogenerated mock type for the RepositorySecrets type +type MockRepositorySecrets struct { + mock.Mock +} + +type MockRepositorySecrets_Expecter struct { + mock *mock.Mock +} + +func (_m *MockRepositorySecrets) EXPECT() *MockRepositorySecrets_Expecter { + return &MockRepositorySecrets_Expecter{mock: &_m.Mock} +} + +// Decrypt provides a mock function with given fields: ctx, r, nameOrValue +func (_m *MockRepositorySecrets) Decrypt(ctx context.Context, r *v0alpha1.Repository, nameOrValue string) ([]byte, error) { + ret := _m.Called(ctx, r, nameOrValue) + + if len(ret) == 0 { + panic("no return value specified for Decrypt") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string) ([]byte, error)); ok { + return rf(ctx, r, nameOrValue) + } + if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string) []byte); ok { + r0 = rf(ctx, r, nameOrValue) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v0alpha1.Repository, string) error); ok { + r1 = rf(ctx, r, nameOrValue) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRepositorySecrets_Decrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Decrypt' +type MockRepositorySecrets_Decrypt_Call struct { + *mock.Call +} + +// Decrypt is a helper method to define mock.On call +// - ctx context.Context +// - r *v0alpha1.Repository +// - nameOrValue string +func (_e *MockRepositorySecrets_Expecter) Decrypt(ctx interface{}, r interface{}, nameOrValue interface{}) *MockRepositorySecrets_Decrypt_Call { + return &MockRepositorySecrets_Decrypt_Call{Call: _e.mock.On("Decrypt", ctx, r, nameOrValue)} +} + +func (_c *MockRepositorySecrets_Decrypt_Call) Run(run func(ctx context.Context, r *v0alpha1.Repository, nameOrValue string)) *MockRepositorySecrets_Decrypt_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*v0alpha1.Repository), args[2].(string)) + }) + return _c +} + +func (_c *MockRepositorySecrets_Decrypt_Call) Return(data []byte, err error) *MockRepositorySecrets_Decrypt_Call { + _c.Call.Return(data, err) + return _c +} + +func (_c *MockRepositorySecrets_Decrypt_Call) RunAndReturn(run func(context.Context, *v0alpha1.Repository, string) ([]byte, error)) *MockRepositorySecrets_Decrypt_Call { + _c.Call.Return(run) + return _c +} + +// Encrypt provides a mock function with given fields: ctx, r, name, data +func (_m *MockRepositorySecrets) Encrypt(ctx context.Context, r *v0alpha1.Repository, name string, data string) ([]byte, error) { + ret := _m.Called(ctx, r, name, data) + + if len(ret) == 0 { + panic("no return value specified for Encrypt") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string, string) ([]byte, error)); ok { + return rf(ctx, r, name, data) + } + if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string, string) []byte); ok { + r0 = rf(ctx, r, name, data) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v0alpha1.Repository, string, string) error); ok { + r1 = rf(ctx, r, name, data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRepositorySecrets_Encrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Encrypt' +type MockRepositorySecrets_Encrypt_Call struct { + *mock.Call +} + +// Encrypt is a helper method to define mock.On call +// - ctx context.Context +// - r *v0alpha1.Repository +// - name string +// - data string +func (_e *MockRepositorySecrets_Expecter) Encrypt(ctx interface{}, r interface{}, name interface{}, data interface{}) *MockRepositorySecrets_Encrypt_Call { + return &MockRepositorySecrets_Encrypt_Call{Call: _e.mock.On("Encrypt", ctx, r, name, data)} +} + +func (_c *MockRepositorySecrets_Encrypt_Call) Run(run func(ctx context.Context, r *v0alpha1.Repository, name string, data string)) *MockRepositorySecrets_Encrypt_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*v0alpha1.Repository), args[2].(string), args[3].(string)) + }) + return _c +} + +func (_c *MockRepositorySecrets_Encrypt_Call) Return(nameOrValue []byte, err error) *MockRepositorySecrets_Encrypt_Call { + _c.Call.Return(nameOrValue, err) + return _c +} + +func (_c *MockRepositorySecrets_Encrypt_Call) RunAndReturn(run func(context.Context, *v0alpha1.Repository, string, string) ([]byte, error)) *MockRepositorySecrets_Encrypt_Call { + _c.Call.Return(run) + return _c +} + +// NewMockRepositorySecrets creates a new instance of MockRepositorySecrets. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockRepositorySecrets(t interface { + mock.TestingT + Cleanup(func()) +}) *MockRepositorySecrets { + mock := &MockRepositorySecrets{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/secrets/repository_test.go b/pkg/registry/apis/provisioning/secrets/repository_test.go new file mode 100644 index 00000000000..f2f9ac29e87 --- /dev/null +++ b/pkg/registry/apis/provisioning/secrets/repository_test.go @@ -0,0 +1,237 @@ +package secrets + +import ( + "context" + "errors" + "testing" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type testSetup struct { + rs RepositorySecrets + mockFeatures *featuremgmt.MockFeatureToggles + mockSecrets *MockService + mockLegacy *MockLegacyService + repo *provisioning.Repository + ctx context.Context +} + +func setupTest(t *testing.T, namespace string) *testSetup { + mockFeatures := featuremgmt.NewMockFeatureToggles(t) + mockSecrets := NewMockService(t) + mockLegacy := NewMockLegacyService(t) + + rs := NewRepositorySecrets(mockFeatures, mockSecrets, mockLegacy) + + repo := &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: namespace, + }, + } + + return &testSetup{ + rs: rs, + mockFeatures: mockFeatures, + mockSecrets: mockSecrets, + mockLegacy: mockLegacy, + repo: repo, + ctx: context.Background(), + } +} + +func (s *testSetup) expectFeatureFlag(enabled bool) { + s.mockFeatures.EXPECT().IsEnabled( + mock.AnythingOfType("context.backgroundCtx"), + featuremgmt.FlagProvisioningSecretsService, + ).Return(enabled) +} + +func TestRepositorySecrets_Encrypt(t *testing.T) { + tests := []struct { + name string + namespace string + featureEnabled bool + setupMocks func(*testSetup) + expectedResult []byte + expectedError string + }{ + { + name: "new service success", + namespace: "test-namespace", + featureEnabled: true, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(true) + s.mockSecrets.EXPECT().Encrypt(s.ctx, "test-namespace", "test-secret", "secret-data").Return("encrypted-name", nil) + }, + expectedResult: []byte("encrypted-name"), + }, + { + name: "legacy service success", + namespace: "test-namespace", + featureEnabled: false, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(false) + s.mockLegacy.EXPECT().Encrypt(s.ctx, []byte("secret-data")).Return([]byte("encrypted-legacy-data"), nil) + }, + expectedResult: []byte("encrypted-legacy-data"), + }, + { + name: "new service error", + namespace: "test-namespace", + featureEnabled: true, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(true) + s.mockSecrets.EXPECT().Encrypt(s.ctx, "test-namespace", "test-secret", "secret-data").Return("", errors.New("encryption failed")) + }, + expectedError: "encryption failed", + }, + { + name: "legacy service error", + namespace: "test-namespace", + featureEnabled: false, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(false) + s.mockLegacy.EXPECT().Encrypt(s.ctx, []byte("secret-data")).Return(nil, errors.New("legacy encryption failed")) + }, + expectedError: "legacy encryption failed", + }, + { + name: "empty namespace handling", + namespace: "", + featureEnabled: true, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(true) + s.mockSecrets.EXPECT().Encrypt(s.ctx, "", "test-secret", "secret-data").Return("encrypted", nil) + }, + expectedResult: []byte("encrypted"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setup := setupTest(t, tt.namespace) + tt.setupMocks(setup) + + result, err := setup.rs.Encrypt(setup.ctx, setup.repo, "test-secret", "secret-data") + + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedResult, result) + } + }) + } +} + +func TestRepositorySecrets_Decrypt(t *testing.T) { + tests := []struct { + name string + namespace string + featureEnabled bool + setupMocks func(*testSetup) + expectedResult []byte + expectedError string + }{ + { + name: "new service success", + namespace: "test-namespace", + featureEnabled: true, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(true) + s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "encrypted-value").Return([]byte("decrypted-data"), nil) + }, + expectedResult: []byte("decrypted-data"), + }, + { + name: "legacy service success", + namespace: "test-namespace", + featureEnabled: false, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(false) + s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("encrypted-value")).Return([]byte("decrypted-legacy-data"), nil) + }, + expectedResult: []byte("decrypted-legacy-data"), + }, + { + name: "new service fails, fallback to legacy succeeds", + namespace: "test-namespace", + featureEnabled: true, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(true) + s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "encrypted-value").Return(nil, errors.New("new service failed")) + s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("encrypted-value")).Return([]byte("decrypted-fallback-data"), nil) + }, + expectedResult: []byte("decrypted-fallback-data"), + }, + { + name: "legacy service fails, fallback to new succeeds", + namespace: "test-namespace", + featureEnabled: false, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(false) + s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("encrypted-value")).Return(nil, errors.New("legacy service failed")) + s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "encrypted-value").Return([]byte("decrypted-new-data"), nil) + }, + expectedResult: []byte("decrypted-new-data"), + }, + { + name: "both services fail (feature flag enabled)", + namespace: "test-namespace", + featureEnabled: true, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(true) + s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "encrypted-value").Return(nil, errors.New("new service failed")) + s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("encrypted-value")).Return(nil, errors.New("legacy service failed")) + }, + expectedError: "legacy service failed", + }, + { + name: "both services fail (feature flag disabled)", + namespace: "test-namespace", + featureEnabled: false, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(false) + s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("encrypted-value")).Return(nil, errors.New("legacy service failed")) + s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "encrypted-value").Return(nil, errors.New("new service failed")) + }, + expectedError: "new service failed", + }, + { + name: "custom namespace handling", + namespace: "custom-namespace", + featureEnabled: true, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(true) + s.mockSecrets.EXPECT().Decrypt(s.ctx, "custom-namespace", "encrypted-value").Return([]byte("test-data"), nil) + }, + expectedResult: []byte("test-data"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setup := setupTest(t, tt.namespace) + tt.setupMocks(setup) + + result, err := setup.rs.Decrypt(setup.ctx, setup.repo, "encrypted-value") + + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedResult, result) + } + }) + } +} diff --git a/pkg/registry/apis/provisioning/secrets/secret.go b/pkg/registry/apis/provisioning/secrets/secret.go index 10e09f548d3..570afb906c2 100644 --- a/pkg/registry/apis/provisioning/secrets/secret.go +++ b/pkg/registry/apis/provisioning/secrets/secret.go @@ -2,36 +2,108 @@ package secrets import ( "context" + "errors" - "github.com/grafana/grafana/pkg/services/secrets" + "github.com/grafana/authlib/types" + 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" + grafanasecrets "github.com/grafana/grafana/pkg/registry/apis/secret/service" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// A secrets encryption service. It only operates on values, no names or similar. -// It is likely we will need to change this when the multi-tenant service comes around. -// -// FIXME: this is a temporary service/package until we can make use of -// the new secrets service in app platform. -// +const svcName = "provisioning" + +//go:generate mockery --name SecureValueService --structname MockSecureValueService --inpackage --filename secure_value_mock.go --with-expecter +type SecureValueService interface { + Create(ctx context.Context, sv *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, error) + Update(ctx context.Context, newSecureValue *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, bool, error) + Read(ctx context.Context, namespace xkube.Namespace, name string) (*secretv1beta1.SecureValue, error) +} + //go:generate mockery --name Service --structname MockService --inpackage --filename secret_mock.go --with-expecter type Service interface { - Encrypt(ctx context.Context, data []byte) ([]byte, error) - Decrypt(ctx context.Context, data []byte) ([]byte, error) + Encrypt(ctx context.Context, namespace, name string, data string) (string, error) + Decrypt(ctx context.Context, namespace string, name string) ([]byte, error) } -var _ Service = (*singleTenant)(nil) +var _ Service = (*secretsService)(nil) -type singleTenant struct { - inner secrets.Service +//go:generate mockery --name DecryptService --structname MockDecryptService --srcpkg=github.com/grafana/grafana/pkg/registry/apis/secret/service --filename decrypt_service_mock.go --with-expecter +type secretsService struct { + secretsSvc SecureValueService + decryptSvc grafanasecrets.DecryptService } -func NewSingleTenant(svc secrets.Service) *singleTenant { - return &singleTenant{svc} +func NewSecretsService(secretsSvc SecureValueService, decryptSvc grafanasecrets.DecryptService) Service { + return &secretsService{ + secretsSvc: secretsSvc, + decryptSvc: decryptSvc, + } } -func (s *singleTenant) Encrypt(ctx context.Context, data []byte) ([]byte, error) { - return s.inner.Encrypt(ctx, data, secrets.WithoutScope()) +func (s *secretsService) Encrypt(ctx context.Context, namespace, name string, data string) (string, error) { + user, err := identity.GetRequester(ctx) + if err != nil { + return "", err + } + + val := secretv1beta1.NewExposedSecureValue(data) + secret := &secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Spec: secretv1beta1.SecureValueSpec{ + Description: "provisioning: " + name, + Value: &val, + Decrypters: []string{svcName}, + }, + } + + existing, err := s.secretsSvc.Read(ctx, xkube.Namespace(namespace), name) + if err != nil && !errors.Is(err, contracts.ErrSecureValueNotFound) { + return "", err + } + + if existing != nil { + existing.Spec.Value = &val + existing, _, err = s.secretsSvc.Update(ctx, existing, user.GetUID()) + if err != nil { + return "", err + } + + return existing.GetName(), nil + } + + finalSecret, err := s.secretsSvc.Create(ctx, secret, user.GetUID()) + if err != nil { + return "", err + } + + return finalSecret.GetName(), nil } -func (s *singleTenant) Decrypt(ctx context.Context, data []byte) ([]byte, error) { - return s.inner.Decrypt(ctx, data) +func (s *secretsService) Decrypt(ctx context.Context, namespace string, name string) ([]byte, error) { + ns, err := types.ParseNamespace(namespace) + if err != nil { + return nil, err + } + ctx = identity.WithServiceIdentityContext(ctx, ns.OrgID, identity.WithServiceIdentityName(svcName)) + + results, err := s.decryptSvc.Decrypt(ctx, namespace, name) + if err != nil { + return nil, err + } + + if res, ok := results[name]; ok { + if res.Error() == nil { + return []byte(res.Value().DangerouslyExposeAndConsumeValue()), nil + } + + return nil, res.Error() + } + + return nil, contracts.ErrDecryptNotFound } diff --git a/pkg/registry/apis/provisioning/secrets/secret_mock.go b/pkg/registry/apis/provisioning/secrets/secret_mock.go index 58505ed8ab7..f4675a7aa5c 100644 --- a/pkg/registry/apis/provisioning/secrets/secret_mock.go +++ b/pkg/registry/apis/provisioning/secrets/secret_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.53.4. DO NOT EDIT. +// Code generated by mockery v2.52.4. DO NOT EDIT. package secrets @@ -21,9 +21,9 @@ func (_m *MockService) EXPECT() *MockService_Expecter { return &MockService_Expecter{mock: &_m.Mock} } -// Decrypt provides a mock function with given fields: ctx, data -func (_m *MockService) Decrypt(ctx context.Context, data []byte) ([]byte, error) { - ret := _m.Called(ctx, data) +// Decrypt provides a mock function with given fields: ctx, namespace, name +func (_m *MockService) Decrypt(ctx context.Context, namespace string, name string) ([]byte, error) { + ret := _m.Called(ctx, namespace, name) if len(ret) == 0 { panic("no return value specified for Decrypt") @@ -31,19 +31,19 @@ func (_m *MockService) Decrypt(ctx context.Context, data []byte) ([]byte, error) var r0 []byte var r1 error - if rf, ok := ret.Get(0).(func(context.Context, []byte) ([]byte, error)); ok { - return rf(ctx, data) + if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]byte, error)); ok { + return rf(ctx, namespace, name) } - if rf, ok := ret.Get(0).(func(context.Context, []byte) []byte); ok { - r0 = rf(ctx, data) + if rf, ok := ret.Get(0).(func(context.Context, string, string) []byte); ok { + r0 = rf(ctx, namespace, name) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]byte) } } - if rf, ok := ret.Get(1).(func(context.Context, []byte) error); ok { - r1 = rf(ctx, data) + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) } else { r1 = ret.Error(1) } @@ -58,14 +58,15 @@ type MockService_Decrypt_Call struct { // Decrypt is a helper method to define mock.On call // - ctx context.Context -// - data []byte -func (_e *MockService_Expecter) Decrypt(ctx interface{}, data interface{}) *MockService_Decrypt_Call { - return &MockService_Decrypt_Call{Call: _e.mock.On("Decrypt", ctx, data)} +// - namespace string +// - name string +func (_e *MockService_Expecter) Decrypt(ctx interface{}, namespace interface{}, name interface{}) *MockService_Decrypt_Call { + return &MockService_Decrypt_Call{Call: _e.mock.On("Decrypt", ctx, namespace, name)} } -func (_c *MockService_Decrypt_Call) Run(run func(ctx context.Context, data []byte)) *MockService_Decrypt_Call { +func (_c *MockService_Decrypt_Call) Run(run func(ctx context.Context, namespace string, name string)) *MockService_Decrypt_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].([]byte)) + run(args[0].(context.Context), args[1].(string), args[2].(string)) }) return _c } @@ -75,34 +76,32 @@ func (_c *MockService_Decrypt_Call) Return(_a0 []byte, _a1 error) *MockService_D return _c } -func (_c *MockService_Decrypt_Call) RunAndReturn(run func(context.Context, []byte) ([]byte, error)) *MockService_Decrypt_Call { +func (_c *MockService_Decrypt_Call) RunAndReturn(run func(context.Context, string, string) ([]byte, error)) *MockService_Decrypt_Call { _c.Call.Return(run) return _c } -// Encrypt provides a mock function with given fields: ctx, data -func (_m *MockService) Encrypt(ctx context.Context, data []byte) ([]byte, error) { - ret := _m.Called(ctx, data) +// Encrypt provides a mock function with given fields: ctx, namespace, name, data +func (_m *MockService) Encrypt(ctx context.Context, namespace string, name string, data string) (string, error) { + ret := _m.Called(ctx, namespace, name, data) if len(ret) == 0 { panic("no return value specified for Encrypt") } - var r0 []byte + var r0 string var r1 error - if rf, ok := ret.Get(0).(func(context.Context, []byte) ([]byte, error)); ok { - return rf(ctx, data) + if rf, ok := ret.Get(0).(func(context.Context, string, string, string) (string, error)); ok { + return rf(ctx, namespace, name, data) } - if rf, ok := ret.Get(0).(func(context.Context, []byte) []byte); ok { - r0 = rf(ctx, data) + if rf, ok := ret.Get(0).(func(context.Context, string, string, string) string); ok { + r0 = rf(ctx, namespace, name, data) } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } + r0 = ret.Get(0).(string) } - if rf, ok := ret.Get(1).(func(context.Context, []byte) error); ok { - r1 = rf(ctx, data) + if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok { + r1 = rf(ctx, namespace, name, data) } else { r1 = ret.Error(1) } @@ -117,24 +116,26 @@ type MockService_Encrypt_Call struct { // Encrypt is a helper method to define mock.On call // - ctx context.Context -// - data []byte -func (_e *MockService_Expecter) Encrypt(ctx interface{}, data interface{}) *MockService_Encrypt_Call { - return &MockService_Encrypt_Call{Call: _e.mock.On("Encrypt", ctx, data)} +// - namespace string +// - name string +// - data string +func (_e *MockService_Expecter) Encrypt(ctx interface{}, namespace interface{}, name interface{}, data interface{}) *MockService_Encrypt_Call { + return &MockService_Encrypt_Call{Call: _e.mock.On("Encrypt", ctx, namespace, name, data)} } -func (_c *MockService_Encrypt_Call) Run(run func(ctx context.Context, data []byte)) *MockService_Encrypt_Call { +func (_c *MockService_Encrypt_Call) Run(run func(ctx context.Context, namespace string, name string, data string)) *MockService_Encrypt_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].([]byte)) + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string)) }) return _c } -func (_c *MockService_Encrypt_Call) Return(_a0 []byte, _a1 error) *MockService_Encrypt_Call { +func (_c *MockService_Encrypt_Call) Return(_a0 string, _a1 error) *MockService_Encrypt_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockService_Encrypt_Call) RunAndReturn(run func(context.Context, []byte) ([]byte, error)) *MockService_Encrypt_Call { +func (_c *MockService_Encrypt_Call) RunAndReturn(run func(context.Context, string, string, string) (string, error)) *MockService_Encrypt_Call { _c.Call.Return(run) return _c } diff --git a/pkg/registry/apis/provisioning/secrets/secret_test.go b/pkg/registry/apis/provisioning/secrets/secret_test.go new file mode 100644 index 00000000000..2e857bee4d3 --- /dev/null +++ b/pkg/registry/apis/provisioning/secrets/secret_test.go @@ -0,0 +1,412 @@ +package secrets + +import ( + "context" + "errors" + "testing" + + 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/provisioning/secrets/mocks" + "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/xkube" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestNewSecretsService(t *testing.T) { + mockSecretsSvc := NewMockSecureValueService(t) + mockDecryptSvc := &mocks.MockDecryptService{} + + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + + assert.NotNil(t, svc) + assert.IsType(t, &secretsService{}, svc) +} + +//nolint:gocyclo // This test is complex but it's a good test for the SecretsService. +func TestSecretsService_Encrypt(t *testing.T) { + tests := []struct { + name string + namespace string + secretName string + data string + setupMocks func(*MockSecureValueService, *mocks.MockDecryptService) + expectedName string + expectedError string + }{ + { + name: "successfully create new secret", + namespace: "test-namespace", + secretName: "test-secret", + data: "secret-data", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + // Assert Read call with correct parameters + mockSecretsSvc.EXPECT().Read( + mock.MatchedBy(func(ctx context.Context) bool { + requester, err := identity.GetRequester(ctx) + return err == nil && requester != nil && requester.GetUID() == ":test-uid" + }), + xkube.Namespace("test-namespace"), + "test-secret", + ).Return(nil, contracts.ErrSecureValueNotFound) + + // Assert Create call with detailed validation + mockSecretsSvc.EXPECT().Create( + mock.MatchedBy(func(ctx context.Context) bool { + requester, err := identity.GetRequester(ctx) + return err == nil && requester != nil && requester.GetUID() == ":test-uid" + }), + mock.MatchedBy(func(sv *secretv1beta1.SecureValue) bool { + if sv.Namespace != "test-namespace" || sv.Name != "test-secret" { + return false + } + if sv.Spec.Description != "provisioning: test-secret" { + return false + } + if sv.Spec.Value == nil { + return false + } + if len(sv.Spec.Decrypters) != 1 || sv.Spec.Decrypters[0] != svcName { + return false + } + // Verify the actual secret value + if sv.Spec.Value.DangerouslyExposeAndConsumeValue() != "secret-data" { + return false + } + return true + }), + ":test-uid", + ).Return(&secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "test-namespace", + }, + }, nil) + }, + expectedName: "test-secret", + }, + { + name: "successfully update existing secret", + namespace: "test-namespace", + secretName: "existing-secret", + data: "new-secret-data", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + existingSecret := &secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-secret", + Namespace: "test-namespace", + }, + Spec: secretv1beta1.SecureValueSpec{ + Description: "provisioning: existing-secret", + Decrypters: []string{svcName}, + }, + } + + // Assert Read call with context validation + mockSecretsSvc.EXPECT().Read( + mock.MatchedBy(func(ctx context.Context) bool { + requester, err := identity.GetRequester(ctx) + return err == nil && requester != nil && requester.GetUID() == ":test-uid" + }), + xkube.Namespace("test-namespace"), + "existing-secret", + ).Return(existingSecret, nil) + + // Assert Update call with detailed validation + mockSecretsSvc.EXPECT().Update( + mock.MatchedBy(func(ctx context.Context) bool { + requester, err := identity.GetRequester(ctx) + return err == nil && requester != nil && requester.GetUID() == ":test-uid" + }), + mock.MatchedBy(func(sv *secretv1beta1.SecureValue) bool { + if sv.Namespace != "test-namespace" || sv.Name != "existing-secret" { + return false + } + if sv.Spec.Value == nil { + return false + } + // Verify the updated secret value + if sv.Spec.Value.DangerouslyExposeAndConsumeValue() != "new-secret-data" { + return false + } + return true + }), + ":test-uid", + ).Return(&secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-secret", + Namespace: "test-namespace", + }, + }, true, nil) + }, + expectedName: "existing-secret", + }, + { + name: "error reading existing secret", + namespace: "test-namespace", + secretName: "test-secret", + data: "secret-data", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + mockSecretsSvc.EXPECT().Read( + mock.MatchedBy(func(ctx context.Context) bool { + requester, err := identity.GetRequester(ctx) + return err == nil && requester != nil && requester.GetUID() == ":test-uid" + }), + xkube.Namespace("test-namespace"), + "test-secret", + ).Return(nil, errors.New("database error")) + }, + expectedError: "database error", + }, + { + name: "error creating new secret", + namespace: "test-namespace", + secretName: "test-secret", + data: "secret-data", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + mockSecretsSvc.EXPECT().Read( + mock.MatchedBy(func(ctx context.Context) bool { + requester, err := identity.GetRequester(ctx) + return err == nil && requester != nil && requester.GetUID() == ":test-uid" + }), + xkube.Namespace("test-namespace"), + "test-secret", + ).Return(nil, contracts.ErrSecureValueNotFound) + + mockSecretsSvc.EXPECT().Create( + mock.MatchedBy(func(ctx context.Context) bool { + requester, err := identity.GetRequester(ctx) + return err == nil && requester != nil && requester.GetUID() == ":test-uid" + }), + mock.MatchedBy(func(sv *secretv1beta1.SecureValue) bool { + return sv.Namespace == "test-namespace" && + sv.Name == "test-secret" && + sv.Spec.Value != nil && + sv.Spec.Value.DangerouslyExposeAndConsumeValue() == "secret-data" + }), + ":test-uid", + ).Return(nil, errors.New("creation failed")) + }, + expectedError: "creation failed", + }, + { + name: "error updating existing secret", + namespace: "test-namespace", + secretName: "existing-secret", + data: "new-secret-data", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + existingSecret := &secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-secret", + Namespace: "test-namespace", + }, + Spec: secretv1beta1.SecureValueSpec{ + Description: "provisioning: existing-secret", + Decrypters: []string{svcName}, + }, + } + + mockSecretsSvc.EXPECT().Read( + mock.MatchedBy(func(ctx context.Context) bool { + requester, err := identity.GetRequester(ctx) + return err == nil && requester != nil && requester.GetUID() == ":test-uid" + }), + xkube.Namespace("test-namespace"), + "existing-secret", + ).Return(existingSecret, nil) + + mockSecretsSvc.EXPECT().Update( + mock.MatchedBy(func(ctx context.Context) bool { + requester, err := identity.GetRequester(ctx) + return err == nil && requester != nil && requester.GetUID() == ":test-uid" + }), + mock.MatchedBy(func(sv *secretv1beta1.SecureValue) bool { + return sv.Namespace == "test-namespace" && + sv.Name == "existing-secret" && + sv.Spec.Value != nil && + sv.Spec.Value.DangerouslyExposeAndConsumeValue() == "new-secret-data" + }), + ":test-uid", + ).Return(nil, false, errors.New("update failed")) + }, + expectedError: "update failed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockSecretsSvc := NewMockSecureValueService(t) + mockDecryptSvc := &mocks.MockDecryptService{} + + tt.setupMocks(mockSecretsSvc, mockDecryptSvc) + + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + + ctx := context.Background() + ctx = identity.WithRequester(ctx, &identity.StaticRequester{ + UserUID: "test-uid", + }) + + result, err := svc.Encrypt(ctx, tt.namespace, tt.secretName, tt.data) + + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedName, result) + } + }) + } +} + +func TestSecretsService_Encrypt_NoIdentity(t *testing.T) { + mockSecretsSvc := NewMockSecureValueService(t) + mockDecryptSvc := &mocks.MockDecryptService{} + + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + + ctx := context.Background() + + result, err := svc.Encrypt(ctx, "test-namespace", "test-secret", "secret-data") + + assert.Error(t, err) + assert.Empty(t, result) +} + +func TestSecretsService_Decrypt(t *testing.T) { + tests := []struct { + name string + namespace string + secretName string + setupMocks func(*MockSecureValueService, *mocks.MockDecryptService) + expectedResult []byte + expectedError string + }{ + { + name: "successfully decrypt secret", + namespace: "test-namespace", + secretName: "test-secret", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + exposedValue := secretv1beta1.NewExposedSecureValue("decrypted-data") + mockResult := service.NewDecryptResultValue(&exposedValue) + + mockDecryptSvc.EXPECT().Decrypt( + mock.MatchedBy(func(ctx context.Context) bool { + // Verify that the context is not nil (the service creates a new StaticRequester) + return ctx != nil + }), + "test-namespace", + "test-secret", + ).Return(map[string]service.DecryptResult{ + "test-secret": mockResult, + }, nil) + }, + expectedResult: []byte("decrypted-data"), + }, + { + name: "decrypt service error", + namespace: "test-namespace", + secretName: "test-secret", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + mockDecryptSvc.EXPECT().Decrypt( + mock.MatchedBy(func(ctx context.Context) bool { + return ctx != nil + }), + "test-namespace", + "test-secret", + ).Return(nil, errors.New("decrypt service error")) + }, + expectedError: "decrypt service error", + }, + { + name: "secret not found in results", + namespace: "test-namespace", + secretName: "test-secret", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + mockDecryptSvc.EXPECT().Decrypt( + mock.MatchedBy(func(ctx context.Context) bool { + return ctx != nil + }), + "test-namespace", + "test-secret", + ).Return(map[string]service.DecryptResult{}, nil) + }, + expectedError: contracts.ErrDecryptNotFound.Error(), + }, + { + name: "decrypt result has error", + namespace: "test-namespace", + secretName: "test-secret", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + mockResult := service.NewDecryptResultErr(errors.New("decryption failed")) + + mockDecryptSvc.EXPECT().Decrypt( + mock.MatchedBy(func(ctx context.Context) bool { + return ctx != nil + }), + "test-namespace", + "test-secret", + ).Return(map[string]service.DecryptResult{ + "test-secret": mockResult, + }, nil) + }, + expectedError: "decryption failed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockSecretsSvc := NewMockSecureValueService(t) + mockDecryptSvc := &mocks.MockDecryptService{} + + tt.setupMocks(mockSecretsSvc, mockDecryptSvc) + + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + + ctx := context.Background() + + result, err := svc.Decrypt(ctx, tt.namespace, tt.secretName) + + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedResult, result) + } + }) + } +} + +// Test to verify that the Decrypt method creates the correct StaticRequester +func TestSecretsService_Decrypt_StaticRequesterCreation(t *testing.T) { + mockSecretsSvc := NewMockSecureValueService(t) + mockDecryptSvc := &mocks.MockDecryptService{} + + exposedValue := secretv1beta1.NewExposedSecureValue("test-data") + mockResult := service.NewDecryptResultValue(&exposedValue) + + // Create a more detailed context matcher to verify the StaticRequester is created correctly + mockDecryptSvc.EXPECT().Decrypt( + mock.MatchedBy(func(ctx context.Context) bool { + // At minimum, verify the context is not nil and is different from the original + return ctx != nil + }), + "test-namespace", + "test-secret", + ).Return(map[string]service.DecryptResult{ + "test-secret": mockResult, + }, nil) + + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + + ctx := context.Background() + result, err := svc.Decrypt(ctx, "test-namespace", "test-secret") + + assert.NoError(t, err) + assert.Equal(t, []byte("test-data"), result) +} diff --git a/pkg/registry/apis/provisioning/secrets/secure_value_mock.go b/pkg/registry/apis/provisioning/secrets/secure_value_mock.go new file mode 100644 index 00000000000..f3703fe03a0 --- /dev/null +++ b/pkg/registry/apis/provisioning/secrets/secure_value_mock.go @@ -0,0 +1,226 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package secrets + +import ( + context "context" + + v1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + mock "github.com/stretchr/testify/mock" + + xkube "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" +) + +// MockSecureValueService is an autogenerated mock type for the SecureValueService type +type MockSecureValueService struct { + mock.Mock +} + +type MockSecureValueService_Expecter struct { + mock *mock.Mock +} + +func (_m *MockSecureValueService) EXPECT() *MockSecureValueService_Expecter { + return &MockSecureValueService_Expecter{mock: &_m.Mock} +} + +// Create provides a mock function with given fields: ctx, sv, actorUID +func (_m *MockSecureValueService) Create(ctx context.Context, sv *v1beta1.SecureValue, actorUID string) (*v1beta1.SecureValue, error) { + ret := _m.Called(ctx, sv, actorUID) + + if len(ret) == 0 { + panic("no return value specified for Create") + } + + var r0 *v1beta1.SecureValue + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v1beta1.SecureValue, string) (*v1beta1.SecureValue, error)); ok { + return rf(ctx, sv, actorUID) + } + if rf, ok := ret.Get(0).(func(context.Context, *v1beta1.SecureValue, string) *v1beta1.SecureValue); ok { + r0 = rf(ctx, sv, actorUID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1beta1.SecureValue) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v1beta1.SecureValue, string) error); ok { + r1 = rf(ctx, sv, actorUID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockSecureValueService_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type MockSecureValueService_Create_Call struct { + *mock.Call +} + +// Create is a helper method to define mock.On call +// - ctx context.Context +// - sv *v1beta1.SecureValue +// - actorUID string +func (_e *MockSecureValueService_Expecter) Create(ctx interface{}, sv interface{}, actorUID interface{}) *MockSecureValueService_Create_Call { + return &MockSecureValueService_Create_Call{Call: _e.mock.On("Create", ctx, sv, actorUID)} +} + +func (_c *MockSecureValueService_Create_Call) Run(run func(ctx context.Context, sv *v1beta1.SecureValue, actorUID string)) *MockSecureValueService_Create_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*v1beta1.SecureValue), args[2].(string)) + }) + return _c +} + +func (_c *MockSecureValueService_Create_Call) Return(_a0 *v1beta1.SecureValue, _a1 error) *MockSecureValueService_Create_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockSecureValueService_Create_Call) RunAndReturn(run func(context.Context, *v1beta1.SecureValue, string) (*v1beta1.SecureValue, error)) *MockSecureValueService_Create_Call { + _c.Call.Return(run) + return _c +} + +// Read provides a mock function with given fields: ctx, namespace, name +func (_m *MockSecureValueService) Read(ctx context.Context, namespace xkube.Namespace, name string) (*v1beta1.SecureValue, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for Read") + } + + var r0 *v1beta1.SecureValue + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, xkube.Namespace, string) (*v1beta1.SecureValue, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, xkube.Namespace, string) *v1beta1.SecureValue); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1beta1.SecureValue) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, xkube.Namespace, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockSecureValueService_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read' +type MockSecureValueService_Read_Call struct { + *mock.Call +} + +// Read is a helper method to define mock.On call +// - ctx context.Context +// - namespace xkube.Namespace +// - name string +func (_e *MockSecureValueService_Expecter) Read(ctx interface{}, namespace interface{}, name interface{}) *MockSecureValueService_Read_Call { + return &MockSecureValueService_Read_Call{Call: _e.mock.On("Read", ctx, namespace, name)} +} + +func (_c *MockSecureValueService_Read_Call) Run(run func(ctx context.Context, namespace xkube.Namespace, name string)) *MockSecureValueService_Read_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(xkube.Namespace), args[2].(string)) + }) + return _c +} + +func (_c *MockSecureValueService_Read_Call) Return(_a0 *v1beta1.SecureValue, _a1 error) *MockSecureValueService_Read_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockSecureValueService_Read_Call) RunAndReturn(run func(context.Context, xkube.Namespace, string) (*v1beta1.SecureValue, error)) *MockSecureValueService_Read_Call { + _c.Call.Return(run) + return _c +} + +// Update provides a mock function with given fields: ctx, newSecureValue, actorUID +func (_m *MockSecureValueService) Update(ctx context.Context, newSecureValue *v1beta1.SecureValue, actorUID string) (*v1beta1.SecureValue, bool, error) { + ret := _m.Called(ctx, newSecureValue, actorUID) + + if len(ret) == 0 { + panic("no return value specified for Update") + } + + var r0 *v1beta1.SecureValue + var r1 bool + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, *v1beta1.SecureValue, string) (*v1beta1.SecureValue, bool, error)); ok { + return rf(ctx, newSecureValue, actorUID) + } + if rf, ok := ret.Get(0).(func(context.Context, *v1beta1.SecureValue, string) *v1beta1.SecureValue); ok { + r0 = rf(ctx, newSecureValue, actorUID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1beta1.SecureValue) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v1beta1.SecureValue, string) bool); ok { + r1 = rf(ctx, newSecureValue, actorUID) + } else { + r1 = ret.Get(1).(bool) + } + + if rf, ok := ret.Get(2).(func(context.Context, *v1beta1.SecureValue, string) error); ok { + r2 = rf(ctx, newSecureValue, actorUID) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// MockSecureValueService_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' +type MockSecureValueService_Update_Call struct { + *mock.Call +} + +// Update is a helper method to define mock.On call +// - ctx context.Context +// - newSecureValue *v1beta1.SecureValue +// - actorUID string +func (_e *MockSecureValueService_Expecter) Update(ctx interface{}, newSecureValue interface{}, actorUID interface{}) *MockSecureValueService_Update_Call { + return &MockSecureValueService_Update_Call{Call: _e.mock.On("Update", ctx, newSecureValue, actorUID)} +} + +func (_c *MockSecureValueService_Update_Call) Run(run func(ctx context.Context, newSecureValue *v1beta1.SecureValue, actorUID string)) *MockSecureValueService_Update_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*v1beta1.SecureValue), args[2].(string)) + }) + return _c +} + +func (_c *MockSecureValueService_Update_Call) Return(_a0 *v1beta1.SecureValue, _a1 bool, _a2 error) *MockSecureValueService_Update_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *MockSecureValueService_Update_Call) RunAndReturn(run func(context.Context, *v1beta1.SecureValue, string) (*v1beta1.SecureValue, bool, error)) *MockSecureValueService_Update_Call { + _c.Call.Return(run) + return _c +} + +// NewMockSecureValueService creates a new instance of MockSecureValueService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockSecureValueService(t interface { + mock.TestingT + Cleanup(func()) +}) *MockSecureValueService { + mock := &MockSecureValueService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/webhooks/register.go b/pkg/registry/apis/provisioning/webhooks/register.go index c366eddf143..561050b661a 100644 --- a/pkg/registry/apis/provisioning/webhooks/register.go +++ b/pkg/registry/apis/provisioning/webhooks/register.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks/pullrequest" + "github.com/grafana/grafana/pkg/registry/apis/secret/service" "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/rendering" @@ -37,8 +38,9 @@ type WebhookExtraBuilder struct { func ProvideWebhooks( cfg *setting.Cfg, features featuremgmt.FeatureToggles, - // FIXME: use multi-tenant service when one exists. In this state, we can't make this a multi-tenant service! - secretsSvc grafanasecrets.Service, + legacySecretsSvc grafanasecrets.Service, + secretsSvc *service.SecureValueService, + decryptSvc service.DecryptService, ghFactory *github.Factory, renderer rendering.Service, blobstore resource.ResourceClient, @@ -65,13 +67,13 @@ func ProvideWebhooks( evaluator := pullrequest.NewEvaluator(screenshotRenderer, parsers, urlProvider) commenter := pullrequest.NewCommenter() pullRequestWorker := pullrequest.NewPullRequestWorker(evaluator, commenter) + repositorySecrets := secrets.NewRepositorySecrets(features, secrets.NewSecretsService(secretsSvc, decryptSvc), secrets.NewSingleTenant(legacySecretsSvc)) return NewWebhookExtra( - features, render, webhook, urlProvider, - secrets.NewSingleTenant(secretsSvc), + repositorySecrets, ghFactory, filepath.Join(cfg.DataPath, "clone"), parsers, @@ -84,11 +86,10 @@ func ProvideWebhooks( // WebhookExtra implements the Extra interface for webhooks // to wrap around type WebhookExtra struct { - features featuremgmt.FeatureToggles render *renderConnector webhook *webhookConnector urlProvider func(namespace string) string - secrets secrets.Service + secrets secrets.RepositorySecrets ghFactory *github.Factory clonedir string parsers resources.ParserFactory @@ -96,18 +97,16 @@ type WebhookExtra struct { } func NewWebhookExtra( - features featuremgmt.FeatureToggles, render *renderConnector, webhook *webhookConnector, urlProvider func(namespace string) string, - secrets secrets.Service, + secrets secrets.RepositorySecrets, ghFactory *github.Factory, clonedir string, parsers resources.ParserFactory, workers []jobs.Worker, ) *WebhookExtra { return &WebhookExtra{ - features: features, render: render, webhook: webhook, urlProvider: urlProvider, @@ -133,11 +132,13 @@ func (e *WebhookExtra) Authorize(ctx context.Context, a authorizer.Attributes) ( func (e *WebhookExtra) Mutate(ctx context.Context, r *provisioning.Repository) error { // Encrypt webhook secret if present if r.Status.Webhook != nil && r.Status.Webhook.Secret != "" { - encryptedSecret, err := e.secrets.Encrypt(ctx, []byte(r.Status.Webhook.Secret)) + secretName := r.GetName() + "-webhook-secret" + nameOrValue, err := e.secrets.Encrypt(ctx, r, secretName, r.Status.Webhook.Secret) if err != nil { return fmt.Errorf("failed to encrypt webhook secret: %w", err) } - r.Status.Webhook.EncryptedSecret = encryptedSecret + + r.Status.Webhook.EncryptedSecret = nameOrValue r.Status.Webhook.Secret = "" } @@ -190,7 +191,7 @@ func (e *WebhookExtra) AsRepository(ctx context.Context, r *provisioning.Reposit // Decrypt GitHub token if needed ghToken := ghCfg.Token if ghToken == "" && len(ghCfg.EncryptedToken) > 0 { - decrypted, err := e.secrets.Decrypt(ctx, ghCfg.EncryptedToken) + decrypted, err := e.secrets.Decrypt(ctx, r, string(ghCfg.EncryptedToken)) if err != nil { return nil, fmt.Errorf("decrypt github token: %w", err) } diff --git a/pkg/registry/apis/provisioning/webhooks/repository.go b/pkg/registry/apis/provisioning/webhooks/repository.go index 451863fceb8..c1b3116469e 100644 --- a/pkg/registry/apis/provisioning/webhooks/repository.go +++ b/pkg/registry/apis/provisioning/webhooks/repository.go @@ -36,7 +36,7 @@ type githubWebhookRepository struct { config *provisioning.Repository owner string repo string - secrets secrets.Service + secrets secrets.RepositorySecrets gh pgh.Client webhookURL string } @@ -44,7 +44,7 @@ type githubWebhookRepository struct { func NewGithubWebhookRepository( basic pgh.GithubRepository, webhookURL string, - secrets secrets.Service, + secrets secrets.RepositorySecrets, ) GithubWebhookRepository { return &githubWebhookRepository{ GithubRepository: basic, @@ -63,7 +63,7 @@ func (r *githubWebhookRepository) Webhook(ctx context.Context, req *http.Request return nil, fmt.Errorf("unexpected webhook request") } - secret, err := r.secrets.Decrypt(ctx, r.config.Status.Webhook.EncryptedSecret) + secret, err := r.secrets.Decrypt(ctx, r.config, string(r.config.Status.Webhook.EncryptedSecret)) if err != nil { return nil, fmt.Errorf("failed to decrypt secret: %w", err) } diff --git a/pkg/registry/apis/provisioning/webhooks/repository_test.go b/pkg/registry/apis/provisioning/webhooks/repository_test.go index 1b0eb4b1122..fd0caedeff9 100644 --- a/pkg/registry/apis/provisioning/webhooks/repository_test.go +++ b/pkg/registry/apis/provisioning/webhooks/repository_test.go @@ -116,7 +116,7 @@ func TestGitHubRepository_Webhook(t *testing.T) { config *provisioning.Repository webhookSecret string setupRequest func() *http.Request - mockSetup func(t *testing.T, mockSecrets *secrets.MockService) + mockSetup func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) expected *provisioning.WebhookResponse expectedError error }{ @@ -156,8 +156,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { req, _ := http.NewRequest("POST", "/webhook", nil) return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return(nil, errors.New("decryption failed")) }, expectedError: fmt.Errorf("failed to decrypt secret: decryption failed"), @@ -183,8 +183,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { req.Header.Set("Content-Type", "application/json") return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expectedError: apierrors.NewUnauthorized("invalid signature"), @@ -218,8 +218,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expected: &provisioning.WebhookResponse{ @@ -264,8 +264,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expected: &provisioning.WebhookResponse{ @@ -312,8 +312,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expected: &provisioning.WebhookResponse{ @@ -358,8 +358,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expectedError: fmt.Errorf("missing repository in push event"), @@ -398,8 +398,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expectedError: fmt.Errorf("repository mismatch"), @@ -441,8 +441,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expected: &provisioning.WebhookResponse{ @@ -497,8 +497,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expected: &provisioning.WebhookResponse{ @@ -564,8 +564,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expected: &provisioning.WebhookResponse{ @@ -628,8 +628,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expected: &provisioning.WebhookResponse{ @@ -682,8 +682,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expected: &provisioning.WebhookResponse{ @@ -733,8 +733,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expectedError: fmt.Errorf("missing repository in pull request event"), @@ -782,8 +782,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expectedError: fmt.Errorf("missing GitHub config"), @@ -833,8 +833,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expectedError: fmt.Errorf("repository mismatch"), @@ -873,12 +873,136 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expectedError: fmt.Errorf("expected PR in event"), }, + { + name: "secret decryption error with new secrets store", + config: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + }, + }, + Status: provisioning.RepositoryStatus{ + Webhook: &provisioning.WebhookStatus{ + EncryptedSecret: []byte("test-secret"), + }, + }, + }, + setupRequest: func() *http.Request { + req, _ := http.NewRequest("POST", "/webhook", nil) + return req + }, + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "test-secret"). + Return(nil, errors.New("decryption failed")) + }, + expectedError: fmt.Errorf("failed to decrypt secret: decryption failed"), + }, + { + name: "ping event with new secrets store", + config: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + }, + }, + Status: provisioning.RepositoryStatus{ + Webhook: &provisioning.WebhookStatus{ + EncryptedSecret: []byte("test-secret"), + }, + }, + }, + webhookSecret: "webhook-secret", + setupRequest: func() *http.Request { + payload := `{}` + req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload)) + req.Header.Set("X-GitHub-Event", "ping") + req.Header.Set("Content-Type", "application/json") + + // Create a valid signature + mac := hmac.New(sha256.New, []byte("webhook-secret")) + mac.Write([]byte(payload)) + signature := hex.EncodeToString(mac.Sum(nil)) + req.Header.Set("X-Hub-Signature-256", "sha256="+signature) + + return req + }, + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "test-secret"). + Return([]byte("webhook-secret"), nil) + }, + expected: &provisioning.WebhookResponse{ + Code: http.StatusOK, + Message: "ping received", + }, + }, + { + name: "push event for main branch with new secrets store", + config: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + }, + Sync: provisioning.SyncOptions{ + Enabled: true, + }, + }, + Status: provisioning.RepositoryStatus{ + Webhook: &provisioning.WebhookStatus{ + EncryptedSecret: []byte("test-secret"), + }, + }, + }, + webhookSecret: "webhook-secret", + setupRequest: func() *http.Request { + payload := `{ + "ref": "refs/heads/main", + "repository": { + "full_name": "grafana/grafana" + } + }` + req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload)) + req.Header.Set("X-GitHub-Event", "push") + req.Header.Set("Content-Type", "application/json") + + // Create a valid signature + mac := hmac.New(sha256.New, []byte("webhook-secret")) + mac.Write([]byte(payload)) + signature := hex.EncodeToString(mac.Sum(nil)) + req.Header.Set("X-Hub-Signature-256", "sha256="+signature) + + return req + }, + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "test-secret"). + Return([]byte("webhook-secret"), nil) + }, + expected: &provisioning.WebhookResponse{ + Code: http.StatusAccepted, + Job: &provisioning.JobSpec{ + Repository: "test-repo", + Action: provisioning.JobActionPull, + Pull: &provisioning.SyncJobOptions{ + Incremental: true, + }, + }, + }, + }, { name: "unsupported event type", config: &provisioning.Repository{ @@ -908,8 +1032,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expected: &provisioning.WebhookResponse{ @@ -922,7 +1046,7 @@ func TestGitHubRepository_Webhook(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create a mock secrets service - mockSecrets := secrets.NewMockService(t) + mockSecrets := secrets.NewMockRepositorySecrets(t) // Set up the mock expectations if tt.mockSetup != nil { diff --git a/pkg/registry/apis/secret/decrypt/service.go b/pkg/registry/apis/secret/decrypt/service.go index 115c073aee2..d35fb6eb2e9 100644 --- a/pkg/registry/apis/secret/decrypt/service.go +++ b/pkg/registry/apis/secret/decrypt/service.go @@ -14,7 +14,7 @@ type OSSDecryptService struct { var _ service.DecryptService = &OSSDecryptService{} -func ProvideDecryptService(decryptStore contracts.DecryptStorage) *OSSDecryptService { +func ProvideDecryptService(decryptStore contracts.DecryptStorage) service.DecryptService { return &OSSDecryptService{ decryptStore: decryptStore, } diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index 61221680dde..a8d2db6b131 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage" "github.com/grafana/grafana/pkg/registry/apis/ofrep" "github.com/grafana/grafana/pkg/registry/apis/provisioning" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks" "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/registry/apis/secret" @@ -57,6 +58,7 @@ var WireSet = wire.NewSet( folders.RegisterAPIService, iam.RegisterAPIService, ProvisioningExtras, + secrets.ProvideRepositorySecrets, provisioning.RegisterAPIService, service.RegisterAPIService, query.RegisterAPIService, diff --git a/pkg/server/test_env.go b/pkg/server/test_env.go index fdf52796a8a..ff151cf7b60 100644 --- a/pkg/server/test_env.go +++ b/pkg/server/test_env.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/grpcserver" @@ -33,6 +34,7 @@ func ProvideTestEnv( resourceClient resource.ResourceClient, idService auth.IDService, githubFactory *github.Factory, + repositorySecrets secrets.RepositorySecrets, ) (*TestEnv, error) { return &TestEnv{ TestingT: testingT, @@ -48,6 +50,7 @@ func ProvideTestEnv( ResourceClient: resourceClient, IDService: idService, GitHubFactory: githubFactory, + RepositorySecrets: repositorySecrets, }, nil } @@ -69,4 +72,5 @@ type TestEnv struct { ResourceClient resource.ResourceClient IDService auth.IDService GitHubFactory *github.Factory + RepositorySecrets secrets.RepositorySecrets } diff --git a/pkg/server/wire.go b/pkg/server/wire.go index ff3bb2fa9b5..a828453388c 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -427,6 +427,7 @@ var wireBasicSet = wire.NewSet( secretmetadata.ProvideDecryptStorage, secretdecrypt.ProvideDecryptAuthorizer, secretdecrypt.ProvideDecryptAllowList, + secretdecrypt.ProvideDecryptService, secretencryption.ProvideDataKeyStorage, secretencryption.ProvideEncryptedValueStorage, secretsecurevalueservice.ProvideSecureValueService, @@ -502,7 +503,8 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser func InitializeForTest(t sqlutil.ITestDB, testingT interface { mock.TestingT Cleanup(func()) -}, cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*TestEnv, error) { +}, cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions, +) (*TestEnv, error) { wire.Build(wireExtsTestSet) return &TestEnv{Server: &Server{}, TestingT: testingT, SQLStore: &sqlstore.SQLStore{}, Cfg: &setting.Cfg{}}, nil } diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index bf627c84716..f7134904ba3 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -56,13 +56,15 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/ofrep" provisioning2 "github.com/grafana/grafana/pkg/registry/apis/provisioning" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks" query2 "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/registry/apis/secret" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/decrypt" - encryption3 "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" + encryption2 "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" manager4 "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" + "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper" service11 "github.com/grafana/grafana/pkg/registry/apis/secret/service" validator3 "github.com/grafana/grafana/pkg/registry/apis/secret/validator" "github.com/grafana/grafana/pkg/registry/apis/userstorage" @@ -113,7 +115,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources/guardian" service7 "github.com/grafana/grafana/pkg/services/datasources/service" - "github.com/grafana/grafana/pkg/services/encryption" + encryption3 "github.com/grafana/grafana/pkg/services/encryption" "github.com/grafana/grafana/pkg/services/encryption/provider" service2 "github.com/grafana/grafana/pkg/services/encryption/service" "github.com/grafana/grafana/pkg/services/extsvcauth" @@ -193,7 +195,7 @@ import ( "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/services/searchusers" "github.com/grafana/grafana/pkg/services/searchusers/filters" - "github.com/grafana/grafana/pkg/services/secrets" + secrets2 "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/database" kvstore2 "github.com/grafana/grafana/pkg/services/secrets/kvstore" migrations2 "github.com/grafana/grafana/pkg/services/secrets/kvstore/migrations" @@ -235,7 +237,7 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" database5 "github.com/grafana/grafana/pkg/storage/secret/database" - encryption2 "github.com/grafana/grafana/pkg/storage/secret/encryption" + "github.com/grafana/grafana/pkg/storage/secret/encryption" "github.com/grafana/grafana/pkg/storage/secret/metadata" migrator2 "github.com/grafana/grafana/pkg/storage/secret/migrator" "github.com/grafana/grafana/pkg/storage/unified" @@ -471,8 +473,8 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser } extSvcAccountsService := extsvcaccounts.ProvideExtSvcAccountsService(acimplService, cfg, inProcBus, sqlStore, featureToggles, registerer, serviceAccountsService, secretsService, tracingService) registryRegistry := registry2.ProvideExtSvcRegistry(cfg, extSvcAccountsService, serverLockService, featureToggles) - service11 := service4.ProvideService(sqlStore, secretsService) - serviceregistrationService := serviceregistration.ProvideService(cfg, featureToggles, registryRegistry, service11) + service12 := service4.ProvideService(sqlStore, secretsService) + serviceregistrationService := serviceregistration.ProvideService(cfg, featureToggles, registryRegistry, service12) initialize := pipeline.ProvideInitializationStage(pluginManagementCfg, inMemory, providerService, processService, serviceregistrationService, acimplService, actionSetService, envVarsProvider, tracingService) terminate, err := pipeline.ProvideTerminationStage(pluginManagementCfg, inMemory, processService) if err != nil { @@ -495,7 +497,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser return nil, err } pluginService := service5.ProvideDashboardPluginService(featureToggles, dashboardServiceImpl) - service12 := service6.ProvideService(fileStoreManager, pluginService) + service13 := service6.ProvideService(fileStoreManager, pluginService) orgRoleMapper := connectors.ProvideOrgRoleMapper(cfg, orgService) ssosettingsimplService := ssosettingsimpl.ProvideService(cfg, sqlStore, accessControl, routeRegisterImpl, featureToggles, secretsService, usageStats, registerer, ossImpl, ossLicensingService) socialService := socialimpl.ProvideService(cfg, featureToggles, usageStats, bundleregistryService, remoteCache, orgRoleMapper, ssosettingsimplService) @@ -540,11 +542,11 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser datasourcePermissionsService := ossaccesscontrol.ProvideDatasourcePermissionsService(cfg, featureToggles, sqlStore) requestConfigProvider := pluginconfig.NewRequestConfigProvider(pluginInstanceCfg) baseProvider := plugincontext.ProvideBaseService(cfg, requestConfigProvider) - service13, err := service7.ProvideService(sqlStore, secretsService, secretsKVStore, cfg, featureToggles, accessControl, datasourcePermissionsService, quotaService, pluginstoreService, middlewareHandler, baseProvider) + service14, err := service7.ProvideService(sqlStore, secretsService, secretsKVStore, cfg, featureToggles, accessControl, datasourcePermissionsService, quotaService, pluginstoreService, middlewareHandler, baseProvider) if err != nil { return nil, err } - correlationsService, err := correlations.ProvideService(sqlStore, routeRegisterImpl, service13, accessControl, inProcBus, quotaService, cfg) + correlationsService, err := correlations.ProvideService(sqlStore, routeRegisterImpl, service14, accessControl, inProcBus, quotaService, cfg) if err != nil { return nil, err } @@ -561,14 +563,14 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser if err != nil { return nil, err } - provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service13, correlationsService, dashboardService, folderimplService, service11, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService) + provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service14, correlationsService, dashboardService, folderimplService, service12, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService) if err != nil { return nil, err } - dataSourceProxyService := datasourceproxy.ProvideService(cacheServiceImpl, ossDataSourceRequestValidator, pluginstoreService, cfg, httpclientProvider, oauthtokenService, service13, tracingService, secretsService, featureToggles) + dataSourceProxyService := datasourceproxy.ProvideService(cacheServiceImpl, ossDataSourceRequestValidator, pluginstoreService, cfg, httpclientProvider, oauthtokenService, service14, tracingService, secretsService, featureToggles) starService := starimpl.ProvideService(sqlStore) searchSearchService := search2.ProvideService(cfg, sqlStore, starService, dashboardService, folderimplService, featureToggles, sortService) - plugincontextProvider := plugincontext.ProvideService(cfg, cacheService, pluginstoreService, cacheServiceImpl, service13, service11, requestConfigProvider) + plugincontextProvider := plugincontext.ProvideService(cfg, cacheService, pluginstoreService, cacheServiceImpl, service14, service12, requestConfigProvider) exprService := expr.ProvideService(cfg, middlewareHandler, plugincontextProvider, featureToggles, registerer, tracingService) queryServiceImpl := query.ProvideService(cfg, cacheServiceImpl, exprService, ossDataSourceRequestValidator, middlewareHandler, plugincontextProvider) repositoryImpl := annotationsimpl.ProvideService(sqlStore, cfg, featureToggles, tagimplService, tracingService, dBstore, dashboardService, registerer) @@ -582,7 +584,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser contexthandlerContextHandler := contexthandler.ProvideService(cfg, authnAuthenticator, featureToggles) logger := loggermw.Provide(cfg, featureToggles) ngAlert := metrics2.ProvideService() - alertNG, err := ngalert.ProvideService(cfg, featureToggles, cacheServiceImpl, service13, routeRegisterImpl, sqlStore, kvStore, exprService, dataSourceProxyService, quotaService, secretsService, notificationService, ngAlert, folderimplService, accessControl, dashboardService, renderingService, inProcBus, acimplService, repositoryImpl, pluginstoreService, tracingService, dBstore, httpclientProvider, plugincontextProvider, receiverPermissionsService, userService) + alertNG, err := ngalert.ProvideService(cfg, featureToggles, cacheServiceImpl, service14, routeRegisterImpl, sqlStore, kvStore, exprService, dataSourceProxyService, quotaService, secretsService, notificationService, ngAlert, folderimplService, accessControl, dashboardService, renderingService, inProcBus, acimplService, repositoryImpl, pluginstoreService, tracingService, dBstore, httpclientProvider, plugincontextProvider, receiverPermissionsService, userService) if err != nil { return nil, err } @@ -619,7 +621,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser csrfCSRF := csrf.ProvideCSRFFilter(cfg) playlistService := playlistimpl.ProvideService(sqlStore, tracingService) secretsMigrator := migrator.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) - dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service13, kvStore, featureToggles) + dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service14, kvStore, featureToggles) secretMigrationProviderImpl := migrations2.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) publicDashboardServiceImpl := service3.ProvideService(cfg, featureToggles, publicDashboardStoreImpl, queryServiceImpl, repositoryImpl, accessControl, publicDashboardServiceWrapperImpl, dashboardService, ossLicensingService) middleware := api2.ProvideMiddleware() @@ -630,7 +632,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser return nil, err } authnService := authnimpl.ProvideAuthnService(authnimplService) - navtreeService := navtreeimpl.ProvideService(cfg, accessControl, pluginstoreService, service11, starService, featureToggles, dashboardService, acimplService, kvStore, apikeyService, ossLicensingService, authnService) + navtreeService := navtreeimpl.ProvideService(cfg, accessControl, pluginstoreService, service12, starService, featureToggles, dashboardService, acimplService, kvStore, apikeyService, ossLicensingService, authnService) searchHTTPService := searchV2.ProvideSearchHTTPService(searchService) statsService := statsimpl.ProvideService(cfg, sqlStore, dashboardService, folderimplService, orgService, featureToggles) gatherer := metrics.ProvideGatherer() @@ -647,7 +649,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser } idimplService := idimpl.ProvideService(cfg, localSigner, remoteCache, authnService, registerer, tracer) verifier := userimpl.ProvideVerifier(cfg, userService, tempuserService, notificationService, idimplService) - httpServer, err := api.ProvideHTTPServer(apiOpts, cfg, routeRegisterImpl, inProcBus, renderingService, ossLicensingService, hooksService, cacheService, sqlStore, ossDataSourceRequestValidator, pluginstoreService, service12, pluginstoreService, middlewareHandler, pluginerrsStore, pluginInstaller, ossImpl, cacheServiceImpl, userAuthTokenService, cleanUpService, shortURLService, queryHistoryService, correlationsService, remoteCache, provisioningServiceImpl, accessControl, dataSourceProxyService, searchSearchService, grafanaLive, gateway, plugincontextProvider, contexthandlerContextHandler, logger, featureToggles, alertNG, libraryPanelService, libraryElementService, quotaService, socialService, tracingService, serviceService, grafanaService, pluginsService, ossService, service13, queryServiceImpl, filestoreService, serviceAccountsProxy, pluginassetsService, authinfoimplService, storageService, notificationService, dashboardService, dashboardProvisioningService, folderimplService, ossProvider, serviceImpl, service11, avatarCacheServer, prefService, folderPermissionsService, dashboardPermissionsService, dashverService, starService, csrfCSRF, noop, playlistService, apikeyService, kvStore, secretsMigrator, secretsService, secretMigrationProviderImpl, secretsKVStore, apiApi, userService, tempuserService, loginattemptimplService, orgService, deletionService, teamService, acimplService, navtreeService, repositoryImpl, tagimplService, searchHTTPService, oauthtokenService, statsService, authnService, pluginscdnService, gatherer, apiAPI, registerer, eventualRestConfigProvider, anonDeviceService, verifier, preinstallImpl) + httpServer, err := api.ProvideHTTPServer(apiOpts, cfg, routeRegisterImpl, inProcBus, renderingService, ossLicensingService, hooksService, cacheService, sqlStore, ossDataSourceRequestValidator, pluginstoreService, service13, pluginstoreService, middlewareHandler, pluginerrsStore, pluginInstaller, ossImpl, cacheServiceImpl, userAuthTokenService, cleanUpService, shortURLService, queryHistoryService, correlationsService, remoteCache, provisioningServiceImpl, accessControl, dataSourceProxyService, searchSearchService, grafanaLive, gateway, plugincontextProvider, contexthandlerContextHandler, logger, featureToggles, alertNG, libraryPanelService, libraryElementService, quotaService, socialService, tracingService, serviceService, grafanaService, pluginsService, ossService, service14, queryServiceImpl, filestoreService, serviceAccountsProxy, pluginassetsService, authinfoimplService, storageService, notificationService, dashboardService, dashboardProvisioningService, folderimplService, ossProvider, serviceImpl, service12, avatarCacheServer, prefService, folderPermissionsService, dashboardPermissionsService, dashverService, starService, csrfCSRF, noop, playlistService, apikeyService, kvStore, secretsMigrator, secretsService, secretMigrationProviderImpl, secretsKVStore, apiApi, userService, tempuserService, loginattemptimplService, orgService, deletionService, teamService, acimplService, navtreeService, repositoryImpl, tagimplService, searchHTTPService, oauthtokenService, statsService, authnService, pluginscdnService, gatherer, apiAPI, registerer, eventualRestConfigProvider, anonDeviceService, verifier, preinstallImpl) if err != nil { return nil, err } @@ -660,12 +662,12 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser if err != nil { return nil, err } - statscollectorService := statscollector.ProvideService(usageStats, validatorService, statsService, cfg, sqlStore, socialService, pluginstoreService, featureManager, service13, httpclientProvider, sandboxService, advisorService) + statscollectorService := statscollector.ProvideService(usageStats, validatorService, statsService, cfg, sqlStore, socialService, pluginstoreService, featureManager, service14, httpclientProvider, sandboxService, advisorService) internalMetricsService, err := metrics.ProvideService(cfg, registerer, gatherer) if err != nil { return nil, err } - supportbundlesimplService, err := supportbundlesimpl.ProvideService(accessControl, acimplService, bundleregistryService, cfg, featureToggles, httpServer, kvStore, service11, pluginstoreService, routeRegisterImpl, ossImpl, sqlStore, usageStats, tracingService) + supportbundlesimplService, err := supportbundlesimpl.ProvideService(accessControl, acimplService, bundleregistryService, cfg, featureToggles, httpServer, kvStore, service12, pluginstoreService, routeRegisterImpl, ossImpl, sqlStore, usageStats, tracingService) if err != nil { return nil, err } @@ -673,7 +675,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser if err != nil { return nil, err } - scopedPluginDatasourceProvider := datasource.ProvideDefaultPluginConfigs(service13, cacheServiceImpl, plugincontextProvider) + scopedPluginDatasourceProvider := datasource.ProvideDefaultPluginConfigs(service14, cacheServiceImpl, plugincontextProvider) v := builder.ProvideDefaultBuildHandlerChainFuncFromBuilders() aggregatorRunner := aggregatorrunner.ProvideNoopAggregatorConfigurator() apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner) @@ -691,15 +693,15 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser zanzanaReconciler := dualwrite2.ProvideZanzanaReconciler(cfg, featureToggles, client, sqlStore, serverLockService, folderimplService) playlistAppProvider := playlist.RegisterApp(playlistService, cfg, featureToggles) investigationsAppProvider := investigations.RegisterApp(cfg) - checkregistryService := checkregistry.ProvideService(service13, pluginstoreService, plugincontextProvider, middlewareHandler, plugincheckerService, repoManager, preinstallImpl, noop, provisionedpluginsNoop, ssosettingsimplService, cfg, pluginerrsStore) + checkregistryService := checkregistry.ProvideService(service14, pluginstoreService, plugincontextProvider, middlewareHandler, plugincheckerService, repoManager, preinstallImpl, noop, provisionedpluginsNoop, ssosettingsimplService, cfg, pluginerrsStore) advisorAppProvider := advisor2.RegisterApp(checkregistryService, cfg) alertingNotificationsAppProvider := notifications2.RegisterApp(cfg, alertNG) appregistryService, err := appregistry.ProvideRegistryServiceSink(apiserverService, eventualRestConfigProvider, featureToggles, playlistAppProvider, investigationsAppProvider, advisorAppProvider, alertingNotificationsAppProvider, cfg) if err != nil { return nil, err } - importDashboardService := service9.ProvideService(routeRegisterImpl, quotaService, service12, pluginstoreService, libraryPanelService, dashboardService, accessControl, folderimplService, featureToggles) - dashboardUpdater := service6.ProvideDashboardUpdater(inProcBus, pluginstoreService, service12, importDashboardService, service11, pluginService, dashboardService) + importDashboardService := service9.ProvideService(routeRegisterImpl, quotaService, service13, pluginstoreService, libraryPanelService, dashboardService, accessControl, folderimplService, featureToggles) + dashboardUpdater := service6.ProvideDashboardUpdater(inProcBus, pluginstoreService, service13, importDashboardService, service12, pluginService, dashboardService) sanitizerProvider := sanitizer.ProvideService(renderingService) healthService, err := grpcserver.ProvideHealthService(cfg, grpcserverProvider) if err != nil { @@ -713,7 +715,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService) ldapImpl := service10.ProvideService(cfg, featureToggles, ssosettingsimplService) apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService) - dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service13, dashboardServiceImpl, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, dashboardFolderStoreImpl, libraryPanelService, eventualRestConfigProvider, userService) + dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service14, dashboardServiceImpl, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, dashboardFolderStoreImpl, libraryPanelService, eventualRestConfigProvider, userService) snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer) featureFlagAPIBuilder := featuretoggle.RegisterAPIService(featureManager, accessControl, apiserverService, cfg, registerer) dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, accessControl, registerer) @@ -726,17 +728,52 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser if err != nil { return nil, err } - legacyDataSourceLookup := service7.ProvideLegacyDataSourceLookup(service13) - queryAPIBuilder, err := query2.RegisterAPIService(featureToggles, apiserverService, service13, pluginstoreService, accessControl, middlewareHandler, plugincontextProvider, registerer, tracingService, legacyDataSourceLookup) + legacyDataSourceLookup := service7.ProvideLegacyDataSourceLookup(service14) + queryAPIBuilder, err := query2.RegisterAPIService(featureToggles, apiserverService, service14, pluginstoreService, accessControl, middlewareHandler, plugincontextProvider, registerer, tracingService, legacyDataSourceLookup) if err != nil { return nil, err } userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer) factory := github.ProvideFactory() legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl) - webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, secretsService, factory, renderingService, resourceClient, eventualRestConfigProvider) + databaseDatabase := database5.ProvideDatabase(sqlStore, tracer) + secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(databaseDatabase, tracer, featureToggles, registerer) + if err != nil { + return nil, err + } + keeperMetadataStorage, err := metadata.ProvideKeeperMetadataStorage(databaseDatabase, tracer, featureToggles, registerer) + if err != nil { + return nil, err + } + encryptedValueStorage, err := encryption.ProvideEncryptedValueStorage(databaseDatabase, tracer, featureToggles) + if err != nil { + return nil, err + } + dataKeyStorage, err := encryption.ProvideDataKeyStorage(databaseDatabase, tracer, featureToggles, registerer) + if err != nil { + return nil, err + } + providerMap := encryption2.ProvideThirdPartyProviderMap() + encryptionManager, err := manager4.ProvideEncryptionManager(tracer, dataKeyStorage, cfg, usageStats, providerMap) + if err != nil { + return nil, err + } + ossKeeperService, err := secretkeeper.ProvideService(tracer, encryptedValueStorage, encryptionManager, registerer) + if err != nil { + return nil, err + } + secureValueService := service11.ProvideSecureValueService(tracer, accessClient, databaseDatabase, secureValueMetadataStorage, keeperMetadataStorage, ossKeeperService) + decryptAllowList := decrypt.ProvideDecryptAllowList() + decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer, decryptAllowList) + decryptStorage, err := metadata.ProvideDecryptStorage(featureToggles, tracer, ossKeeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, registerer) + if err != nil { + return nil, err + } + decryptService := decrypt.ProvideDecryptService(decryptStorage) + repositorySecrets := secrets.ProvideRepositorySecrets(featureToggles, secretsService, secureValueService, decryptService) + webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, secretsService, secureValueService, decryptService, factory, renderingService, resourceClient, eventualRestConfigProvider) v2 := apiregistry.MergeProvisioningExtras(webhookExtraBuilder) - apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, secretsService, tracingService, v2) + apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, repositorySecrets, tracingService, v2) if err != nil { return nil, err } @@ -755,7 +792,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser return nil, err } teamAPI := teamapi.ProvideTeamAPI(routeRegisterImpl, teamService, acimplService, accessControl, teamPermissionsService, userService, ossLicensingService, cfg, prefService, dashboardService, featureToggles) - cloudmigrationService, err := cloudmigrationimpl.ProvideService(cfg, httpclientProvider, featureToggles, sqlStore, service13, secretsKVStore, secretsService, routeRegisterImpl, registerer, tracingService, dashboardService, folderimplService, pluginstoreService, service11, accessControl, acimplService, kvStore, libraryElementService, alertNG) + cloudmigrationService, err := cloudmigrationimpl.ProvideService(cfg, httpclientProvider, featureToggles, sqlStore, service14, secretsKVStore, secretsService, routeRegisterImpl, registerer, tracingService, dashboardService, folderimplService, pluginstoreService, service12, accessControl, acimplService, kvStore, libraryElementService, alertNG) if err != nil { return nil, err } @@ -977,8 +1014,8 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { } extSvcAccountsService := extsvcaccounts.ProvideExtSvcAccountsService(acimplService, cfg, inProcBus, sqlStore, featureToggles, registerer, serviceAccountsService, secretsService, tracingService) registryRegistry := registry2.ProvideExtSvcRegistry(cfg, extSvcAccountsService, serverLockService, featureToggles) - service11 := service4.ProvideService(sqlStore, secretsService) - serviceregistrationService := serviceregistration.ProvideService(cfg, featureToggles, registryRegistry, service11) + service12 := service4.ProvideService(sqlStore, secretsService) + serviceregistrationService := serviceregistration.ProvideService(cfg, featureToggles, registryRegistry, service12) initialize := pipeline.ProvideInitializationStage(pluginManagementCfg, inMemory, providerService, processService, serviceregistrationService, acimplService, actionSetService, envVarsProvider, tracingService) terminate, err := pipeline.ProvideTerminationStage(pluginManagementCfg, inMemory, processService) if err != nil { @@ -1001,7 +1038,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { return nil, err } pluginService := service5.ProvideDashboardPluginService(featureToggles, dashboardServiceImpl) - service12 := service6.ProvideService(fileStoreManager, pluginService) + service13 := service6.ProvideService(fileStoreManager, pluginService) oauthtokentestService := oauthtokentest.ProvideService() ossCachingService := caching.ProvideCachingService() middlewareHandler, err := pluginsintegration.ProvideClientWithMiddlewares(cfg, inMemory, oauthtokentestService, tracingService, ossCachingService, featureToggles, registerer) @@ -1041,11 +1078,11 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { datasourcePermissionsService := ossaccesscontrol.ProvideDatasourcePermissionsService(cfg, featureToggles, sqlStore) requestConfigProvider := pluginconfig.NewRequestConfigProvider(pluginInstanceCfg) baseProvider := plugincontext.ProvideBaseService(cfg, requestConfigProvider) - service13, err := service7.ProvideService(sqlStore, secretsService, secretsKVStore, cfg, featureToggles, accessControl, datasourcePermissionsService, quotaService, pluginstoreService, middlewareHandler, baseProvider) + service14, err := service7.ProvideService(sqlStore, secretsService, secretsKVStore, cfg, featureToggles, accessControl, datasourcePermissionsService, quotaService, pluginstoreService, middlewareHandler, baseProvider) if err != nil { return nil, err } - correlationsService, err := correlations.ProvideService(sqlStore, routeRegisterImpl, service13, accessControl, inProcBus, quotaService, cfg) + correlationsService, err := correlations.ProvideService(sqlStore, routeRegisterImpl, service14, accessControl, inProcBus, quotaService, cfg) if err != nil { return nil, err } @@ -1062,7 +1099,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { if err != nil { return nil, err } - provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service13, correlationsService, dashboardService, folderimplService, service11, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService) + provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service14, correlationsService, dashboardService, folderimplService, service12, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService) if err != nil { return nil, err } @@ -1072,10 +1109,10 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { loginStore := authinfoimpl.ProvideStore(sqlStore, secretsService) authinfoimplService := authinfoimpl.ProvideService(loginStore, remoteCache, secretsService) oauthtokenService := oauthtoken.ProvideService(socialService, authinfoimplService, cfg, registerer, serverLockService, tracingService, userAuthTokenService, featureToggles) - dataSourceProxyService := datasourceproxy.ProvideService(cacheServiceImpl, ossDataSourceRequestValidator, pluginstoreService, cfg, httpclientProvider, oauthtokenService, service13, tracingService, secretsService, featureToggles) + dataSourceProxyService := datasourceproxy.ProvideService(cacheServiceImpl, ossDataSourceRequestValidator, pluginstoreService, cfg, httpclientProvider, oauthtokenService, service14, tracingService, secretsService, featureToggles) starService := starimpl.ProvideService(sqlStore) searchSearchService := search2.ProvideService(cfg, sqlStore, starService, dashboardService, folderimplService, featureToggles, sortService) - plugincontextProvider := plugincontext.ProvideService(cfg, cacheService, pluginstoreService, cacheServiceImpl, service13, service11, requestConfigProvider) + plugincontextProvider := plugincontext.ProvideService(cfg, cacheService, pluginstoreService, cacheServiceImpl, service14, service12, requestConfigProvider) exprService := expr.ProvideService(cfg, middlewareHandler, plugincontextProvider, featureToggles, registerer, tracingService) queryServiceImpl := query.ProvideService(cfg, cacheServiceImpl, exprService, ossDataSourceRequestValidator, middlewareHandler, plugincontextProvider) repositoryImpl := annotationsimpl.ProvideService(sqlStore, cfg, featureToggles, tagimplService, tracingService, dBstore, dashboardService, registerer) @@ -1090,7 +1127,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { logger := loggermw.Provide(cfg, featureToggles) notificationServiceMock := notifications.MockNotificationService() ngAlert := metrics2.ProvideServiceForTest() - alertNG, err := ngalert.ProvideService(cfg, featureToggles, cacheServiceImpl, service13, routeRegisterImpl, sqlStore, kvStore, exprService, dataSourceProxyService, quotaService, secretsService, notificationServiceMock, ngAlert, folderimplService, accessControl, dashboardService, renderingService, inProcBus, acimplService, repositoryImpl, pluginstoreService, tracingService, dBstore, httpclientProvider, plugincontextProvider, receiverPermissionsService, userService) + alertNG, err := ngalert.ProvideService(cfg, featureToggles, cacheServiceImpl, service14, routeRegisterImpl, sqlStore, kvStore, exprService, dataSourceProxyService, quotaService, secretsService, notificationServiceMock, ngAlert, folderimplService, accessControl, dashboardService, renderingService, inProcBus, acimplService, repositoryImpl, pluginstoreService, tracingService, dBstore, httpclientProvider, plugincontextProvider, receiverPermissionsService, userService) if err != nil { return nil, err } @@ -1127,7 +1164,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { csrfCSRF := csrf.ProvideCSRFFilter(cfg) playlistService := playlistimpl.ProvideService(sqlStore, tracingService) secretsMigrator := migrator.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) - dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service13, kvStore, featureToggles) + dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service14, kvStore, featureToggles) secretMigrationProviderImpl := migrations2.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) publicDashboardServiceImpl := service3.ProvideService(cfg, featureToggles, publicDashboardStoreImpl, queryServiceImpl, repositoryImpl, accessControl, publicDashboardServiceWrapperImpl, dashboardService, ossLicensingService) middleware := api2.ProvideMiddleware() @@ -1138,7 +1175,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { return nil, err } authnService := authnimpl.ProvideAuthnService(authnimplService) - navtreeService := navtreeimpl.ProvideService(cfg, accessControl, pluginstoreService, service11, starService, featureToggles, dashboardService, acimplService, kvStore, apikeyService, ossLicensingService, authnService) + navtreeService := navtreeimpl.ProvideService(cfg, accessControl, pluginstoreService, service12, starService, featureToggles, dashboardService, acimplService, kvStore, apikeyService, ossLicensingService, authnService) searchHTTPService := searchV2.ProvideSearchHTTPService(searchService) statsService := statsimpl.ProvideService(cfg, sqlStore, dashboardService, folderimplService, orgService, featureToggles) gatherer := metrics.ProvideGathererForTest(registerer) @@ -1155,7 +1192,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { } idimplService := idimpl.ProvideService(cfg, localSigner, remoteCache, authnService, registerer, tracer) verifier := userimpl.ProvideVerifier(cfg, userService, tempuserService, notificationServiceMock, idimplService) - httpServer, err := api.ProvideHTTPServer(apiOpts, cfg, routeRegisterImpl, inProcBus, renderingService, ossLicensingService, hooksService, cacheService, sqlStore, ossDataSourceRequestValidator, pluginstoreService, service12, pluginstoreService, middlewareHandler, pluginerrsStore, pluginInstaller, ossImpl, cacheServiceImpl, userAuthTokenService, cleanUpService, shortURLService, queryHistoryService, correlationsService, remoteCache, provisioningServiceImpl, accessControl, dataSourceProxyService, searchSearchService, grafanaLive, gateway, plugincontextProvider, contexthandlerContextHandler, logger, featureToggles, alertNG, libraryPanelService, libraryElementService, quotaService, socialService, tracingService, serviceService, grafanaService, pluginsService, ossService, service13, queryServiceImpl, filestoreService, serviceAccountsProxy, pluginassetsService, authinfoimplService, storageService, notificationServiceMock, dashboardService, dashboardProvisioningService, folderimplService, ossProvider, serviceImpl, service11, avatarCacheServer, prefService, folderPermissionsService, dashboardPermissionsService, dashverService, starService, csrfCSRF, noop, playlistService, apikeyService, kvStore, secretsMigrator, secretsService, secretMigrationProviderImpl, secretsKVStore, apiApi, userService, tempuserService, loginattemptimplService, orgService, deletionService, teamService, acimplService, navtreeService, repositoryImpl, tagimplService, searchHTTPService, oauthtokentestService, statsService, authnService, pluginscdnService, gatherer, apiAPI, registerer, eventualRestConfigProvider, anonDeviceService, verifier, preinstallImpl) + httpServer, err := api.ProvideHTTPServer(apiOpts, cfg, routeRegisterImpl, inProcBus, renderingService, ossLicensingService, hooksService, cacheService, sqlStore, ossDataSourceRequestValidator, pluginstoreService, service13, pluginstoreService, middlewareHandler, pluginerrsStore, pluginInstaller, ossImpl, cacheServiceImpl, userAuthTokenService, cleanUpService, shortURLService, queryHistoryService, correlationsService, remoteCache, provisioningServiceImpl, accessControl, dataSourceProxyService, searchSearchService, grafanaLive, gateway, plugincontextProvider, contexthandlerContextHandler, logger, featureToggles, alertNG, libraryPanelService, libraryElementService, quotaService, socialService, tracingService, serviceService, grafanaService, pluginsService, ossService, service14, queryServiceImpl, filestoreService, serviceAccountsProxy, pluginassetsService, authinfoimplService, storageService, notificationServiceMock, dashboardService, dashboardProvisioningService, folderimplService, ossProvider, serviceImpl, service12, avatarCacheServer, prefService, folderPermissionsService, dashboardPermissionsService, dashverService, starService, csrfCSRF, noop, playlistService, apikeyService, kvStore, secretsMigrator, secretsService, secretMigrationProviderImpl, secretsKVStore, apiApi, userService, tempuserService, loginattemptimplService, orgService, deletionService, teamService, acimplService, navtreeService, repositoryImpl, tagimplService, searchHTTPService, oauthtokentestService, statsService, authnService, pluginscdnService, gatherer, apiAPI, registerer, eventualRestConfigProvider, anonDeviceService, verifier, preinstallImpl) if err != nil { return nil, err } @@ -1168,12 +1205,12 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { if err != nil { return nil, err } - statscollectorService := statscollector.ProvideService(usageStats, validatorService, statsService, cfg, sqlStore, socialService, pluginstoreService, featureManager, service13, httpclientProvider, sandboxService, advisorService) + statscollectorService := statscollector.ProvideService(usageStats, validatorService, statsService, cfg, sqlStore, socialService, pluginstoreService, featureManager, service14, httpclientProvider, sandboxService, advisorService) internalMetricsService, err := metrics.ProvideService(cfg, registerer, gatherer) if err != nil { return nil, err } - supportbundlesimplService, err := supportbundlesimpl.ProvideService(accessControl, acimplService, bundleregistryService, cfg, featureToggles, httpServer, kvStore, service11, pluginstoreService, routeRegisterImpl, ossImpl, sqlStore, usageStats, tracingService) + supportbundlesimplService, err := supportbundlesimpl.ProvideService(accessControl, acimplService, bundleregistryService, cfg, featureToggles, httpServer, kvStore, service12, pluginstoreService, routeRegisterImpl, ossImpl, sqlStore, usageStats, tracingService) if err != nil { return nil, err } @@ -1181,7 +1218,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { if err != nil { return nil, err } - scopedPluginDatasourceProvider := datasource.ProvideDefaultPluginConfigs(service13, cacheServiceImpl, plugincontextProvider) + scopedPluginDatasourceProvider := datasource.ProvideDefaultPluginConfigs(service14, cacheServiceImpl, plugincontextProvider) v := builder.ProvideDefaultBuildHandlerChainFuncFromBuilders() aggregatorRunner := aggregatorrunner.ProvideNoopAggregatorConfigurator() apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner) @@ -1199,15 +1236,15 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { zanzanaReconciler := dualwrite2.ProvideZanzanaReconciler(cfg, featureToggles, client, sqlStore, serverLockService, folderimplService) playlistAppProvider := playlist.RegisterApp(playlistService, cfg, featureToggles) investigationsAppProvider := investigations.RegisterApp(cfg) - checkregistryService := checkregistry.ProvideService(service13, pluginstoreService, plugincontextProvider, middlewareHandler, plugincheckerService, repoManager, preinstallImpl, noop, provisionedpluginsNoop, ssosettingsimplService, cfg, pluginerrsStore) + checkregistryService := checkregistry.ProvideService(service14, pluginstoreService, plugincontextProvider, middlewareHandler, plugincheckerService, repoManager, preinstallImpl, noop, provisionedpluginsNoop, ssosettingsimplService, cfg, pluginerrsStore) advisorAppProvider := advisor2.RegisterApp(checkregistryService, cfg) alertingNotificationsAppProvider := notifications2.RegisterApp(cfg, alertNG) appregistryService, err := appregistry.ProvideRegistryServiceSink(apiserverService, eventualRestConfigProvider, featureToggles, playlistAppProvider, investigationsAppProvider, advisorAppProvider, alertingNotificationsAppProvider, cfg) if err != nil { return nil, err } - importDashboardService := service9.ProvideService(routeRegisterImpl, quotaService, service12, pluginstoreService, libraryPanelService, dashboardService, accessControl, folderimplService, featureToggles) - dashboardUpdater := service6.ProvideDashboardUpdater(inProcBus, pluginstoreService, service12, importDashboardService, service11, pluginService, dashboardService) + importDashboardService := service9.ProvideService(routeRegisterImpl, quotaService, service13, pluginstoreService, libraryPanelService, dashboardService, accessControl, folderimplService, featureToggles) + dashboardUpdater := service6.ProvideDashboardUpdater(inProcBus, pluginstoreService, service13, importDashboardService, service12, pluginService, dashboardService) sanitizerProvider := sanitizer.ProvideService(renderingService) healthService, err := grpcserver.ProvideHealthService(cfg, grpcserverProvider) if err != nil { @@ -1221,7 +1258,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService) ldapImpl := service10.ProvideService(cfg, featureToggles, ssosettingsimplService) apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService) - dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service13, dashboardServiceImpl, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, dashboardFolderStoreImpl, libraryPanelService, eventualRestConfigProvider, userService) + dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service14, dashboardServiceImpl, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, dashboardFolderStoreImpl, libraryPanelService, eventualRestConfigProvider, userService) snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer) featureFlagAPIBuilder := featuretoggle.RegisterAPIService(featureManager, accessControl, apiserverService, cfg, registerer) dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, accessControl, registerer) @@ -1234,17 +1271,52 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { if err != nil { return nil, err } - legacyDataSourceLookup := service7.ProvideLegacyDataSourceLookup(service13) - queryAPIBuilder, err := query2.RegisterAPIService(featureToggles, apiserverService, service13, pluginstoreService, accessControl, middlewareHandler, plugincontextProvider, registerer, tracingService, legacyDataSourceLookup) + legacyDataSourceLookup := service7.ProvideLegacyDataSourceLookup(service14) + queryAPIBuilder, err := query2.RegisterAPIService(featureToggles, apiserverService, service14, pluginstoreService, accessControl, middlewareHandler, plugincontextProvider, registerer, tracingService, legacyDataSourceLookup) if err != nil { return nil, err } userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer) factory := github.ProvideFactory() legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl) - webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, secretsService, factory, renderingService, resourceClient, eventualRestConfigProvider) + databaseDatabase := database5.ProvideDatabase(sqlStore, tracer) + secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(databaseDatabase, tracer, featureToggles, registerer) + if err != nil { + return nil, err + } + keeperMetadataStorage, err := metadata.ProvideKeeperMetadataStorage(databaseDatabase, tracer, featureToggles, registerer) + if err != nil { + return nil, err + } + encryptedValueStorage, err := encryption.ProvideEncryptedValueStorage(databaseDatabase, tracer, featureToggles) + if err != nil { + return nil, err + } + dataKeyStorage, err := encryption.ProvideDataKeyStorage(databaseDatabase, tracer, featureToggles, registerer) + if err != nil { + return nil, err + } + providerMap := encryption2.ProvideThirdPartyProviderMap() + encryptionManager, err := manager4.ProvideEncryptionManager(tracer, dataKeyStorage, cfg, usageStats, providerMap) + if err != nil { + return nil, err + } + ossKeeperService, err := secretkeeper.ProvideService(tracer, encryptedValueStorage, encryptionManager, registerer) + if err != nil { + return nil, err + } + secureValueService := service11.ProvideSecureValueService(tracer, accessClient, databaseDatabase, secureValueMetadataStorage, keeperMetadataStorage, ossKeeperService) + decryptAllowList := decrypt.ProvideDecryptAllowList() + decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer, decryptAllowList) + decryptStorage, err := metadata.ProvideDecryptStorage(featureToggles, tracer, ossKeeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, registerer) + if err != nil { + return nil, err + } + decryptService := decrypt.ProvideDecryptService(decryptStorage) + repositorySecrets := secrets.ProvideRepositorySecrets(featureToggles, secretsService, secureValueService, decryptService) + webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, secretsService, secureValueService, decryptService, factory, renderingService, resourceClient, eventualRestConfigProvider) v2 := apiregistry.MergeProvisioningExtras(webhookExtraBuilder) - apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, secretsService, tracingService, v2) + apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, repositorySecrets, tracingService, v2) if err != nil { return nil, err } @@ -1263,7 +1335,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { return nil, err } teamAPI := teamapi.ProvideTeamAPI(routeRegisterImpl, teamService, acimplService, accessControl, teamPermissionsService, userService, ossLicensingService, cfg, prefService, dashboardService, featureToggles) - cloudmigrationService, err := cloudmigrationimpl.ProvideService(cfg, httpclientProvider, featureToggles, sqlStore, service13, secretsKVStore, secretsService, routeRegisterImpl, registerer, tracingService, dashboardService, folderimplService, pluginstoreService, service11, accessControl, acimplService, kvStore, libraryElementService, alertNG) + cloudmigrationService, err := cloudmigrationimpl.ProvideService(cfg, httpclientProvider, featureToggles, sqlStore, service14, secretsKVStore, secretsService, routeRegisterImpl, registerer, tracingService, dashboardService, folderimplService, pluginstoreService, service12, accessControl, acimplService, kvStore, libraryElementService, alertNG) if err != nil { return nil, err } @@ -1279,7 +1351,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { if err != nil { return nil, err } - testEnv, err := ProvideTestEnv(testingT, server, sqlStore, cfg, notificationServiceMock, grpcserverProvider, inMemory, httpclientProvider, oauthtokentestService, featureToggles, resourceClient, idimplService, factory) + testEnv, err := ProvideTestEnv(testingT, server, sqlStore, cfg, notificationServiceMock, grpcserverProvider, inMemory, httpclientProvider, oauthtokentestService, featureToggles, resourceClient, idimplService, factory, repositorySecrets) if err != nil { return nil, err } @@ -1428,7 +1500,7 @@ var withOTelSet = wire.NewSet( otelTracer, grpcserver.ProvideService, interceptors.ProvideAuthenticator, ) -var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator2.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service10.ProvideService, wire.Bind(new(service10.LDAP), new(*service10.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service7.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service7.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database4.DashboardSnapshotStore)), database4.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service8.ServiceImpl)), service8.ProvideService, service7.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service7.Service)), service7.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager2.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service5.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service5.DashboardServiceImpl)), service5.ProvideDashboardService, service5.ProvideDashboardProvisioningService, service5.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), folderimpl.ProvideDashboardFolderStore, wire.Bind(new(folder.FolderStore), new(*folderimpl.DashboardFolderStoreImpl)), service9.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service9.ImportDashboardService)), service6.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service6.Service)), service6.ProvideDashboardUpdater, sanitizer.ProvideService, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, decrypt.ProvideDecryptAllowList, encryption2.ProvideDataKeyStorage, encryption2.ProvideEncryptedValueStorage, service11.ProvideSecureValueService, validator3.ProvideKeeperValidator, validator3.ProvideSecureValueValidator, migrator2.NewWithEngine, database5.ProvideDatabase, wire.Bind(new(contracts.Database), new(*database5.Database)), manager4.ProvideEncryptionManager, encryption3.ProvideThirdPartyProviderMap, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet) +var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator2.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service10.ProvideService, wire.Bind(new(service10.LDAP), new(*service10.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service7.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service7.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption3.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets2.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets2.Store), new(*database.SecretsStoreImpl)), grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database4.DashboardSnapshotStore)), database4.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service8.ServiceImpl)), service8.ProvideService, service7.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service7.Service)), service7.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager2.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service5.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service5.DashboardServiceImpl)), service5.ProvideDashboardService, service5.ProvideDashboardProvisioningService, service5.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), folderimpl.ProvideDashboardFolderStore, wire.Bind(new(folder.FolderStore), new(*folderimpl.DashboardFolderStoreImpl)), service9.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service9.ImportDashboardService)), service6.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service6.Service)), service6.ProvideDashboardUpdater, sanitizer.ProvideService, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, decrypt.ProvideDecryptAllowList, decrypt.ProvideDecryptService, encryption.ProvideDataKeyStorage, encryption.ProvideEncryptedValueStorage, service11.ProvideSecureValueService, validator3.ProvideKeeperValidator, validator3.ProvideSecureValueValidator, migrator2.NewWithEngine, database5.ProvideDatabase, wire.Bind(new(contracts.Database), new(*database5.Database)), manager4.ProvideEncryptionManager, encryption2.ProvideThirdPartyProviderMap, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet) var wireSet = wire.NewSet( wireBasicSet, metrics.WireSet, sqlstore.ProvideService, metrics2.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationService)), wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), prefimpl.ProvideService, oauthtoken.ProvideService, wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)), wire.Bind(new(cleanup.AlertRuleService), new(*store2.DBstore)), diff --git a/pkg/services/featuremgmt/feature_toggles_mock.go b/pkg/services/featuremgmt/feature_toggles_mock.go new file mode 100644 index 00000000000..db29d9a2159 --- /dev/null +++ b/pkg/services/featuremgmt/feature_toggles_mock.go @@ -0,0 +1,177 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package featuremgmt + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// MockFeatureToggles is an autogenerated mock type for the FeatureToggles type +type MockFeatureToggles struct { + mock.Mock +} + +type MockFeatureToggles_Expecter struct { + mock *mock.Mock +} + +func (_m *MockFeatureToggles) EXPECT() *MockFeatureToggles_Expecter { + return &MockFeatureToggles_Expecter{mock: &_m.Mock} +} + +// GetEnabled provides a mock function with given fields: ctx +func (_m *MockFeatureToggles) GetEnabled(ctx context.Context) map[string]bool { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetEnabled") + } + + var r0 map[string]bool + if rf, ok := ret.Get(0).(func(context.Context) map[string]bool); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]bool) + } + } + + return r0 +} + +// MockFeatureToggles_GetEnabled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetEnabled' +type MockFeatureToggles_GetEnabled_Call struct { + *mock.Call +} + +// GetEnabled is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockFeatureToggles_Expecter) GetEnabled(ctx interface{}) *MockFeatureToggles_GetEnabled_Call { + return &MockFeatureToggles_GetEnabled_Call{Call: _e.mock.On("GetEnabled", ctx)} +} + +func (_c *MockFeatureToggles_GetEnabled_Call) Run(run func(ctx context.Context)) *MockFeatureToggles_GetEnabled_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockFeatureToggles_GetEnabled_Call) Return(_a0 map[string]bool) *MockFeatureToggles_GetEnabled_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockFeatureToggles_GetEnabled_Call) RunAndReturn(run func(context.Context) map[string]bool) *MockFeatureToggles_GetEnabled_Call { + _c.Call.Return(run) + return _c +} + +// IsEnabled provides a mock function with given fields: ctx, flag +func (_m *MockFeatureToggles) IsEnabled(ctx context.Context, flag string) bool { + ret := _m.Called(ctx, flag) + + if len(ret) == 0 { + panic("no return value specified for IsEnabled") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok { + r0 = rf(ctx, flag) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// MockFeatureToggles_IsEnabled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsEnabled' +type MockFeatureToggles_IsEnabled_Call struct { + *mock.Call +} + +// IsEnabled is a helper method to define mock.On call +// - ctx context.Context +// - flag string +func (_e *MockFeatureToggles_Expecter) IsEnabled(ctx interface{}, flag interface{}) *MockFeatureToggles_IsEnabled_Call { + return &MockFeatureToggles_IsEnabled_Call{Call: _e.mock.On("IsEnabled", ctx, flag)} +} + +func (_c *MockFeatureToggles_IsEnabled_Call) Run(run func(ctx context.Context, flag string)) *MockFeatureToggles_IsEnabled_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *MockFeatureToggles_IsEnabled_Call) Return(_a0 bool) *MockFeatureToggles_IsEnabled_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockFeatureToggles_IsEnabled_Call) RunAndReturn(run func(context.Context, string) bool) *MockFeatureToggles_IsEnabled_Call { + _c.Call.Return(run) + return _c +} + +// IsEnabledGlobally provides a mock function with given fields: flag +func (_m *MockFeatureToggles) IsEnabledGlobally(flag string) bool { + ret := _m.Called(flag) + + if len(ret) == 0 { + panic("no return value specified for IsEnabledGlobally") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(string) bool); ok { + r0 = rf(flag) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// MockFeatureToggles_IsEnabledGlobally_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsEnabledGlobally' +type MockFeatureToggles_IsEnabledGlobally_Call struct { + *mock.Call +} + +// IsEnabledGlobally is a helper method to define mock.On call +// - flag string +func (_e *MockFeatureToggles_Expecter) IsEnabledGlobally(flag interface{}) *MockFeatureToggles_IsEnabledGlobally_Call { + return &MockFeatureToggles_IsEnabledGlobally_Call{Call: _e.mock.On("IsEnabledGlobally", flag)} +} + +func (_c *MockFeatureToggles_IsEnabledGlobally_Call) Run(run func(flag string)) *MockFeatureToggles_IsEnabledGlobally_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockFeatureToggles_IsEnabledGlobally_Call) Return(_a0 bool) *MockFeatureToggles_IsEnabledGlobally_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockFeatureToggles_IsEnabledGlobally_Call) RunAndReturn(run func(string) bool) *MockFeatureToggles_IsEnabledGlobally_Call { + _c.Call.Return(run) + return _c +} + +// NewMockFeatureToggles creates a new instance of MockFeatureToggles. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockFeatureToggles(t interface { + mock.TestingT + Cleanup(func()) +}) *MockFeatureToggles { + mock := &MockFeatureToggles{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/featuremgmt/models.go b/pkg/services/featuremgmt/models.go index 018cb6046e6..4acca519bef 100644 --- a/pkg/services/featuremgmt/models.go +++ b/pkg/services/featuremgmt/models.go @@ -6,6 +6,7 @@ import ( "encoding/json" ) +//go:generate mockery --name FeatureToggles --structname MockFeatureToggles --inpackage --filename feature_toggles_mock.go --with-expecter type FeatureToggles interface { // IsEnabled checks if a feature is enabled for a given context. // The settings may be per user, tenant, or globally set in the cloud diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 009190be1f4..bde952f6be8 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -337,6 +337,13 @@ var ( RequiresRestart: true, Owner: grafanaAppPlatformSquad, }, + { + Name: "provisioningSecretsService", + Description: "Experimental feature to use the secrets service for provisioning instead of the legacy secrets", + Stage: FeatureStageExperimental, + RequiresRestart: true, + Owner: grafanaAppPlatformSquad, + }, { Name: "grafanaAPIServerEnsureKubectlAccess", Description: "Start an additional https handler and write kubectl options", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 079fdfcfc62..95a254efd7d 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -43,6 +43,7 @@ mlExpressions,experimental,@grafana/alerting-squad,false,false,false datasourceAPIServers,experimental,@grafana/grafana-app-platform-squad,false,true,false grafanaAPIServerWithExperimentalAPIs,experimental,@grafana/grafana-app-platform-squad,true,true,false provisioning,experimental,@grafana/grafana-app-platform-squad,false,true,false +provisioningSecretsService,experimental,@grafana/grafana-app-platform-squad,false,true,false grafanaAPIServerEnsureKubectlAccess,experimental,@grafana/grafana-app-platform-squad,true,true,false featureToggleAdminPage,experimental,@grafana/grafana-operator-experience-squad,false,true,false awsAsyncQueryCaching,GA,@grafana/aws-datasources,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index e5b5bb90af3..0d47b99a68b 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -183,6 +183,10 @@ const ( // Next generation provisioning... and git FlagProvisioning = "provisioning" + // FlagProvisioningSecretsService + // Experimental feature to use the secrets service for provisioning instead of the legacy secrets + FlagProvisioningSecretsService = "provisioningSecretsService" + // FlagGrafanaAPIServerEnsureKubectlAccess // Start an additional https handler and write kubectl options FlagGrafanaAPIServerEnsureKubectlAccess = "grafanaAPIServerEnsureKubectlAccess" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 94b96a800f5..2ab40f4e0f5 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2506,6 +2506,22 @@ "requiresRestart": true } }, + { + "metadata": { + "name": "provisioningSecretsService", + "resourceVersion": "1752501981893", + "creationTimestamp": "2025-07-14T14:05:50Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-07-14 14:06:21.89322 +0000 UTC" + } + }, + "spec": { + "description": "Experimental feature to use the secrets service for provisioning instead of the legacy secrets", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresRestart": true + } + }, { "metadata": { "name": "publicDashboardsEmailSharing", diff --git a/pkg/tests/apis/provisioning/helper_test.go b/pkg/tests/apis/provisioning/helper_test.go index 24ffe18b0e2..bd398addb04 100644 --- a/pkg/tests/apis/provisioning/helper_test.go +++ b/pkg/tests/apis/provisioning/helper_test.go @@ -197,6 +197,13 @@ func withLogs(opts *testinfra.GrafanaOpts) { opts.EnableLog = true } +func useAppPlatformSecrets(opts *testinfra.GrafanaOpts) { + opts.EnableFeatureToggles = append(opts.EnableFeatureToggles, + featuremgmt.FlagProvisioningSecretsService, + featuremgmt.FlagSecretsManagementAppPlatform, + ) +} + func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper { provisioningPath := t.TempDir() opts := testinfra.GrafanaOpts{ diff --git a/pkg/tests/apis/provisioning/secrets_test.go b/pkg/tests/apis/provisioning/secrets_test.go new file mode 100644 index 00000000000..41b1d27aa59 --- /dev/null +++ b/pkg/tests/apis/provisioning/secrets_test.go @@ -0,0 +1,447 @@ +package provisioning + +import ( + "context" + "encoding/base64" + "fmt" + "strings" + "testing" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestIntegrationProvisioning_LegacySecrets(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + helper := runGrafana(t) + createOptions := metav1.CreateOptions{FieldValidation: "Strict"} + ctx := context.Background() + + type expectedField struct { + Path []string + ExpectedDecryptedValue string + } + + secretsService := helper.GetEnv().RepositorySecrets + tests := []struct { + name string + values map[string]any + inputFile string + expectedFields []expectedField + }{ + { + name: "github token encrypted", + values: map[string]any{ + "Token": "some-token", + }, + inputFile: "testdata/github-readonly.json.tmpl", + expectedFields: []expectedField{ + { + Path: []string{"spec", "github", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "github", "encryptedToken"}, + ExpectedDecryptedValue: "some-token", + }, + }, + }, + { + name: "git token encrypted", + values: map[string]any{ + "Token": "some-token", + }, + inputFile: "testdata/git-readonly.json.tmpl", + expectedFields: []expectedField{ + { + Path: []string{"spec", "git", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "git", "encryptedToken"}, + ExpectedDecryptedValue: "some-token", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + input := helper.RenderObject(t, test.inputFile, test.values) + _, err := helper.Repositories.Resource.Create(ctx, input, createOptions) + require.NoError(t, err, "failed to create resource") + + name := mustNestedString(input.Object, "metadata", "name") + output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{}) + require.NoError(t, err, "failed to read back resource") + + // Move encrypted token mutation + for _, expectedField := range test.expectedFields { + value, decrypted := encryptedField(t, secretsService, nil, output.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "") + require.False(t, strings.HasPrefix(value, name), "value should not be prefixed with the repository name") + require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted) + } + }) + } +} + +func TestIntegrationProvisioning_Secrets_LegacyUpdate(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + helper := runGrafana(t) + createOptions := metav1.CreateOptions{FieldValidation: "Strict"} + updateOptions := metav1.UpdateOptions{} + ctx := context.Background() + + secretsService := helper.GetEnv().RepositorySecrets + + type expectedField struct { + Path []string + ExpectedValue string + ExpectedDecryptedValue string + } + + tests := []struct { + name string + values map[string]any + inputFile string + updateValues map[string]any + expectedFields []expectedField + }{ + { + name: "update github token (legacy secrets)", + values: map[string]any{ + "Token": "initial-token", + }, + inputFile: "testdata/github-readonly.json.tmpl", + updateValues: map[string]any{ + "Token": "updated-token", + }, + expectedFields: []expectedField{ + { + Path: []string{"spec", "github", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "github", "encryptedToken"}, + ExpectedDecryptedValue: "updated-token", + }, + }, + }, + { + name: "update git token (legacy secrets)", + values: map[string]any{ + "Token": "initial-token", + }, + inputFile: "testdata/git-readonly.json.tmpl", + updateValues: map[string]any{ + "Token": "updated-token", + }, + expectedFields: []expectedField{ + { + Path: []string{"spec", "git", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "git", "encryptedToken"}, + ExpectedDecryptedValue: "updated-token", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Create initial resource + input := helper.RenderObject(t, test.inputFile, test.values) + _, err := helper.Repositories.Resource.Create(ctx, input, createOptions) + require.NoError(t, err, "failed to create resource") + + name := mustNestedString(input.Object, "metadata", "name") + + // Prepare updated resource + updatedInput := helper.RenderObject(t, test.inputFile, test.updateValues) + // Set the same name and resourceVersion for update + updatedInput.Object["metadata"].(map[string]any)["name"] = name + + // Fetch current resourceVersion + current, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{}) + require.NoError(t, err, "failed to get current resource for update") + updatedInput.Object["metadata"].(map[string]any)["resourceVersion"] = + current.Object["metadata"].(map[string]any)["resourceVersion"] + + _, err = helper.Repositories.Resource.Update(ctx, updatedInput, updateOptions) + require.NoError(t, err, "failed to update resource") + + output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{}) + require.NoError(t, err, "failed to read back resource after update") + + for _, expectedField := range test.expectedFields { + value, decrypted := encryptedField(t, secretsService, nil, output.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "") + require.False(t, strings.HasPrefix(value, name), "value should not be prefixed with the repository name") + require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted) + } + }) + } +} + +func TestIntegrationProvisioning_Secrets(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + helper := runGrafana(t, useAppPlatformSecrets) + createOptions := metav1.CreateOptions{FieldValidation: "Strict"} + ctx := context.Background() + + secretsService := helper.GetEnv().RepositorySecrets + + type expectedField struct { + Path []string + ExpectedValue string + ExpectedDecryptedValue string + } + // TODO: Add test of fallbacks + tests := []struct { + name string + values map[string]any + inputFile string + expectedFields []expectedField + }{ + { + name: "github token encrypted", + values: map[string]any{ + "Token": "some-token", + }, + inputFile: "testdata/github-readonly.json.tmpl", + expectedFields: []expectedField{ + { + Path: []string{"spec", "github", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "github", "encryptedToken"}, + ExpectedValue: "github-token", + ExpectedDecryptedValue: "some-token", + }, + }, + }, + { + name: "git token encrypted", + values: map[string]any{ + "Token": "some-token", + }, + inputFile: "testdata/git-readonly.json.tmpl", + expectedFields: []expectedField{ + { + Path: []string{"spec", "git", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "git", "encryptedToken"}, + ExpectedValue: "git-token", + ExpectedDecryptedValue: "some-token", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + input := helper.RenderObject(t, test.inputFile, test.values) + _, err := helper.Repositories.Resource.Create(ctx, input, createOptions) + require.NoError(t, err, "failed to create resource") + + name := mustNestedString(input.Object, "metadata", "name") + output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{}) + require.NoError(t, err, "failed to read back resource") + repo := unstructuredToRepository(t, output) + + // Move encrypted token mutation + for _, expectedField := range test.expectedFields { + value, decrypted := encryptedField(t, secretsService, repo, output.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "") + + if expectedField.ExpectedValue != "" { + require.Equal(t, name+"-"+expectedField.ExpectedValue, value) + } + + if expectedField.ExpectedDecryptedValue != "" { + require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted) + } + } + }) + } +} + +func TestIntegrationProvisioning_Secrets_Update(t *testing.T) { + ctx := context.Background() + helper := runGrafana(t, useAppPlatformSecrets) + secretsService := helper.GetEnv().RepositorySecrets + createOptions := metav1.CreateOptions{} + updateOptions := metav1.UpdateOptions{} + + type expectedField struct { + Path []string + ExpectedValue string + ExpectedDecryptedValue string + } + + tests := []struct { + name string + inputFile string + values map[string]interface{} + updateValues map[string]interface{} + expectedFields []expectedField + updatedFields []expectedField + }{ + { + name: "update encrypted git token", + inputFile: "testdata/git-readonly.json.tmpl", + values: map[string]interface{}{ + "Token": "initial-token", + }, + updateValues: map[string]interface{}{ + "Token": "updated-token", + }, + expectedFields: []expectedField{ + { + Path: []string{"spec", "git", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "git", "encryptedToken"}, + ExpectedValue: "git-token", + ExpectedDecryptedValue: "initial-token", + }, + }, + updatedFields: []expectedField{ + { + Path: []string{"spec", "git", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "git", "encryptedToken"}, + ExpectedValue: "git-token", + ExpectedDecryptedValue: "updated-token", + }, + }, + }, + { + name: "update encrypted github token", + inputFile: "testdata/github-readonly.json.tmpl", + values: map[string]interface{}{ + "Token": "initial-token", + }, + updateValues: map[string]interface{}{ + "Token": "updated-token", + }, + expectedFields: []expectedField{ + { + Path: []string{"spec", "github", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "github", "encryptedToken"}, + ExpectedValue: "github-token", + ExpectedDecryptedValue: "initial-token", + }, + }, + updatedFields: []expectedField{ + { + Path: []string{"spec", "github", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "github", "encryptedToken"}, + ExpectedValue: "github-token", + ExpectedDecryptedValue: "updated-token", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Create initial resource + input := helper.RenderObject(t, test.inputFile, test.values) + _, err := helper.Repositories.Resource.Create(ctx, input, createOptions) + require.NoError(t, err, "failed to create resource") + + name := mustNestedString(input.Object, "metadata", "name") + output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{}) + require.NoError(t, err, "failed to read back resource") + + // Update the resource + updatedInput := helper.RenderObject(t, test.inputFile, test.updateValues) + // Set the same name and resourceVersion for update + _ = unstructured.SetNestedField(updatedInput.Object, name, "metadata", "name") + _ = unstructured.SetNestedField(updatedInput.Object, output.GetResourceVersion(), "metadata", "resourceVersion") + _, err = helper.Repositories.Resource.Update(ctx, updatedInput, updateOptions) + require.NoError(t, err, "failed to update resource") + + updatedOutput, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{}) + require.NoError(t, err, "failed to read back updated resource") + updatedRepo := unstructuredToRepository(t, updatedOutput) + + // Check updated fields + for _, expectedField := range test.updatedFields { + value, decrypted := encryptedField(t, secretsService, updatedRepo, updatedOutput.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "") + + if expectedField.ExpectedValue != "" { + require.Equal(t, name+"-"+expectedField.ExpectedValue, value) + } + + if expectedField.ExpectedDecryptedValue != "" { + require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted) + } + } + }) + } +} + +func encryptedField(t *testing.T, secretsService secrets.RepositorySecrets, repo *provisioning.Repository, obj map[string]any, path []string, expectedValue bool) (string, string) { + value, found, err := base64DecodedField(obj, path) + if err != nil { + require.NoError(t, err, "failed to decode base64 value") + } + + if expectedValue { + decrypted, err := secretsService.Decrypt(context.Background(), repo, value) + require.NoError(t, err, "failed to eecrypt value") + return value, string(decrypted) + } else { + require.False(t, found, "value should not be found") + return "", "" + } +} + +func base64DecodedField(obj map[string]any, path []string) (string, bool, error) { + value, found, err := unstructured.NestedFieldNoCopy(obj, path...) + if err != nil { + return "", false, err + } + + if !found { + return "", false, nil + } + + valueStr, ok := value.(string) + if !ok { + return "", false, fmt.Errorf("value is not a string") + } + + decodedValue, err := base64.StdEncoding.DecodeString(valueStr) + if err != nil { + return "", false, fmt.Errorf("failed to decode base64 valueStr: %w", err) + } + + return string(decodedValue), true, nil +} diff --git a/pkg/tests/apis/provisioning/testdata/git-readonly.json.tmpl b/pkg/tests/apis/provisioning/testdata/git-readonly.json.tmpl new file mode 100644 index 00000000000..38f0201b49f --- /dev/null +++ b/pkg/tests/apis/provisioning/testdata/git-readonly.json.tmpl @@ -0,0 +1,24 @@ +{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Repository", + "metadata": { + "name": "{{ or .Name "git-repository" }}" + }, + "spec": { + "title": "{{ or .Title .Name "Git repository" }}", + "description": "{{ or .Description .Name "Load grafana dashboard from fake repository" }}", + "type": "git", + "git": { + "url": "{{ or .URL "https://github.com/grafana/grafana-git-sync-demo" }}", + "branch": "{{ or .Branch "integration-test" }}", + "token": "{{ or .Token "" }}", + "path": "{{ or .Path "grafana/" }}" + }, + "sync": { + "enabled": {{ if .SyncEnabled }} true {{ else }} false {{ end }}, + "target": "{{ or .SyncTarget "folder" }}", + "intervalSeconds": {{ or .SyncIntervalSeconds 60 }} + }, + "workflows": [] + } +}