Provisioning: Begin using secrets store (#108044)
- Provisioning: Begin using secrets store - Refactor integration with secrets store - Add back the legacy service - Separate concerns for encrypt and decrypt - Handle update within Encrypt function - Add interface for secure value service - Add feature flag for using secrets service - Add the dual service for temporary solution. * Add first integration tests for encrypted tokens * Add integration test for app platform secrets * Validate it has the name or not * Create wire provider * Always save to the secret if provided secret --------- Co-authored-by: Roberto Jimenez Sanchez <roberto.jimenez@grafana.com> Co-authored-by: Roberto Jiménez Sánchez <jszroberto@gmail.com>
This commit is contained in:
co-authored by
Roberto Jimenez Sanchez
Roberto Jiménez Sánchez
parent
68b9a5f57c
commit
d39a47a89b
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user