Files
grafana/pkg/services/secrets/migrator/provisioning.go
Peter Štibraný 1ffe653156 Revert change of order of checks when setting up resource DB (#105514)
* Revert change of order in newResourceDBProvider

* Ignore error in provisioningSecrets when working with wrong DB.

* Update workspace.

* Add owner.
2025-05-16 11:35:36 +03:00

189 lines
5.5 KiB
Go

package migrator
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/encryption"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/manager"
)
var errNoEncryptedValue = errors.New("no encrypted value was found")
var _ SecretsRotator = provisioningSecrets{}
type provisioningSecrets struct{}
func (p provisioningSecrets) ReEncrypt(ctx context.Context, svc *manager.SecretsService, sqlStore db.DB) bool {
return p.reEncrypt(ctx, svc, func(ctx context.Context, payload []byte) ([]byte, error) {
return svc.Encrypt(ctx, payload, secrets.WithoutScope())
}, sqlStore, "rotation")
}
func (p provisioningSecrets) Rollback(ctx context.Context, svc *manager.SecretsService, internal encryption.Internal, sqlStore db.DB, secretKey string) bool {
return p.reEncrypt(ctx, svc, func(ctx context.Context, payload []byte) ([]byte, error) {
return internal.Encrypt(ctx, payload, secretKey)
}, sqlStore, "rollback")
}
func (p provisioningSecrets) reEncrypt(
ctx context.Context,
svc *manager.SecretsService,
encrypt func(context.Context, []byte) ([]byte, error),
sqlStore db.DB,
action string,
) bool {
var rows []struct {
Guid string
Value []byte
}
if err := sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
return sess.Table("resource").Where("`group` = 'provisioning.grafana.app' AND `resource` = 'repositories'").
Select("guid, value").
Find(&rows)
}); err != nil {
logger.Warn("Could not find any provisioning secrets to re-encrypt", "error", err, "action", action)
// resource table may not exists (when not using unified storage and right db), so we don't report error here.
return true
}
var failures int
for _, row := range rows {
var resource map[string]any
if err := json.Unmarshal(row.Value, &resource); err != nil {
logger.Error("Failed to decode resource", "guid", row.Guid, "error", err, "action", action)
failures++
continue
}
err := p.reEncryptGitHubToken(ctx, svc, encrypt, sqlStore, resource)
if err != nil && !errors.Is(err, errNoEncryptedValue) {
logger.Error("Failed to rotate GitHub token", "guid", row.Guid, "error", err, "action", action)
failures++
}
update := err == nil
err = p.reEncryptWebhookSecret(ctx, svc, encrypt, sqlStore, resource)
if err != nil && !errors.Is(err, errNoEncryptedValue) {
logger.Error("Failed to rotate webhook secret", "guid", row.Guid, "error", err, "action", action)
failures++
}
update = update || err == nil
if update {
// Do it...
encoded, err := json.Marshal(resource)
if err != nil {
logger.Error("Failed to marshal resource to JSON", "guid", row.Guid, "error", err, "action", action)
failures++
continue
}
if err := sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
// TODO: Figure out what resource_version is supposed to contain, and update it.
_, err = sess.Exec("UPDATE resource SET value = ? WHERE guid = ?", string(encoded), row.Guid)
return err
}); err != nil {
logger.Error("Failed to update resource with re-encrypted values", "guid", row.Guid, "error", err, "action", action)
failures++
}
}
}
if failures > 0 {
logger.Warn("Failed to rotate provisioning secrets", "failures", failures, "action", action)
} else {
logger.Info("Successfully rotated provisioning secrets", "action", action)
}
return failures == 0
}
func (provisioningSecrets) reEncryptGitHubToken(
ctx context.Context,
svc *manager.SecretsService,
encrypt func(context.Context, []byte) ([]byte, error),
sqlStore db.DB,
obj map[string]any,
) error {
spec, _ := getCast[map[string]any](obj, "spec")
github, _ := getCast[map[string]any](spec, "github")
b64Token, ok := getCast[string](github, "encryptedToken")
if !ok {
return errNoEncryptedValue
}
token, err := base64.StdEncoding.DecodeString(b64Token)
if err != nil {
return fmt.Errorf("failed to decode base64 encrypted token: %w", err)
}
decrypted, err := svc.Decrypt(ctx, token)
if err != nil {
return fmt.Errorf("failed to decrypt existing encrypted token: %w", err)
}
encrypted, err := encrypt(ctx, decrypted)
if err != nil {
return fmt.Errorf("failed to encrypt decrypted token: %w", err)
}
b64Token = base64.StdEncoding.EncodeToString(encrypted)
github["encryptedToken"] = b64Token
return nil
}
func (provisioningSecrets) reEncryptWebhookSecret(
ctx context.Context,
svc *manager.SecretsService,
encrypt func(context.Context, []byte) ([]byte, error),
sqlStore db.DB,
obj map[string]any,
) error {
status, _ := getCast[map[string]any](obj, "status")
webhook, _ := getCast[map[string]any](status, "webhook")
b64Token, ok := getCast[string](webhook, "encryptedSecret")
if !ok {
return errNoEncryptedValue
}
token, err := base64.StdEncoding.DecodeString(b64Token)
if err != nil {
return fmt.Errorf("failed to decode base64 encrypted secret: %w", err)
}
decrypted, err := svc.Decrypt(ctx, token)
if err != nil {
return fmt.Errorf("failed to decrypt existing encrypted secret: %w", err)
}
encrypted, err := encrypt(ctx, decrypted)
if err != nil {
return fmt.Errorf("failed to encrypt decrypted secret: %w", err)
}
b64Token = base64.StdEncoding.EncodeToString(encrypted)
webhook["encryptedSecret"] = b64Token
return nil
}
func getCast[T any](m map[string]any, name string) (T, bool) {
if m == nil {
var zero T
return zero, false
}
anyV, ok := m[name]
if ok {
t, ok := anyV.(T)
return t, ok
}
var zero T
return zero, false
}