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:
Stephanie Hingtgen
2025-07-15 15:43:17 +02:00
committed by GitHub
co-authored by Roberto Jimenez Sanchez Roberto Jiménez Sánchez
parent 68b9a5f57c
commit d39a47a89b
30 changed files with 2591 additions and 194 deletions
@@ -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 {