Files
sendico/api/edge/bff/internal/server/callbacksimp/secrets.go
2026-03-03 01:07:35 +01:00

180 lines
5.4 KiB
Go

package callbacksimp
import (
"context"
"crypto/rand"
"encoding/base64"
"path"
"strings"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/pkg/vault/kv"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
type signingSecretManager interface {
Provision(ctx context.Context, organizationRef, callbackRef bson.ObjectID) (secretRef string, generatedSecret string, err error)
}
type vaultSigningSecretManager struct {
logger mlogger.Logger
store kv.Client
pathPrefix string
field string
secretLength int
}
const (
metricsResultSuccess = "success"
metricsResultError = "error"
)
var (
signingSecretMetricsOnce sync.Once
signingSecretStatus *prometheus.CounterVec
signingSecretLatency *prometheus.HistogramVec
)
func ensureSigningSecretMetrics() {
signingSecretMetricsOnce.Do(func() {
signingSecretStatus = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "sendico",
Subsystem: "bff_callbacks",
Name: "signing_secret_provision_total",
Help: "Total callback signing secret provisioning attempts.",
}, []string{"result"})
signingSecretLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "sendico",
Subsystem: "bff_callbacks",
Name: "signing_secret_provision_duration_seconds",
Help: "Duration of callback signing secret provisioning attempts.",
Buckets: prometheus.DefBuckets,
}, []string{"result"})
})
}
func newSigningSecretManager(logger mlogger.Logger, cfg callbacksConfig) (signingSecretManager, error) {
if err := cfg.validate(); err != nil {
return nil, err
}
if logger == nil {
logger = zap.NewNop()
}
manager := &vaultSigningSecretManager{
logger: logger.Named("callbacks_secrets"),
pathPrefix: strings.Trim(strings.TrimSpace(cfg.SecretPathPrefix), "/"),
field: strings.TrimSpace(cfg.SecretField),
secretLength: cfg.SecretLengthBytes,
}
if manager.pathPrefix == "" {
manager.pathPrefix = defaultSigningSecretPathPrefix
}
if manager.field == "" {
manager.field = defaultSigningSecretField
}
if isVaultConfigEmpty(cfg.Vault) {
manager.logger.Warn("Callbacks Vault config is not set; hmac signing secret generation is disabled")
ensureSigningSecretMetrics()
return manager, nil
}
store, err := kv.New(kv.Options{
Logger: manager.logger,
Config: kv.Config{
Address: strings.TrimSpace(cfg.Vault.Address),
TokenEnv: strings.TrimSpace(cfg.Vault.TokenEnv),
TokenFileEnv: strings.TrimSpace(cfg.Vault.TokenFileEnv),
TokenFile: strings.TrimSpace(cfg.Vault.TokenFile),
Namespace: strings.TrimSpace(cfg.Vault.Namespace),
MountPath: strings.TrimSpace(cfg.Vault.MountPath),
},
Component: "bff callbacks signing secret manager",
})
if err != nil {
return nil, err
}
manager.store = store
ensureSigningSecretMetrics()
return manager, nil
}
func (m *vaultSigningSecretManager) Provision(
ctx context.Context,
organizationRef,
callbackRef bson.ObjectID,
) (string, string, error) {
start := time.Now()
result := metricsResultSuccess
defer func() {
signingSecretStatus.WithLabelValues(result).Inc()
signingSecretLatency.WithLabelValues(result).Observe(time.Since(start).Seconds())
}()
if organizationRef.IsZero() {
result = metricsResultError
return "", "", merrors.InvalidArgument("organization reference is required", "organizationRef")
}
if callbackRef.IsZero() {
result = metricsResultError
return "", "", merrors.InvalidArgument("callback reference is required", "callbackRef")
}
if m.store == nil {
result = metricsResultError
return "", "", merrors.InvalidArgument("callbacks vault config is required to generate signing secrets", "api.callbacks.vault")
}
secret, err := generateSigningSecret(m.secretLength)
if err != nil {
result = metricsResultError
return "", "", err
}
secretPath := path.Join(m.pathPrefix, organizationRef.Hex(), callbackRef.Hex())
payload := map[string]interface{}{
m.field: secret,
"organization_ref": organizationRef.Hex(),
"callback_ref": callbackRef.Hex(),
"updated_at": time.Now().UTC().Format(time.RFC3339Nano),
}
if err := m.store.Put(ctx, secretPath, payload); err != nil {
result = metricsResultError
m.logger.Warn("Failed to store callback signing secret", zap.String("path", secretPath), zap.Error(err))
return "", "", err
}
secretRef := "vault:" + secretPath + "#" + m.field
m.logger.Info("Callback signing secret stored", zap.String("secret_ref", secretRef), mzap.ObjRef("callback_ref", callbackRef))
return secretRef, secret, nil
}
func isVaultConfigEmpty(cfg VaultConfig) bool {
return strings.TrimSpace(cfg.Address) == "" &&
strings.TrimSpace(cfg.TokenEnv) == "" &&
strings.TrimSpace(cfg.TokenFileEnv) == "" &&
strings.TrimSpace(cfg.TokenFile) == "" &&
strings.TrimSpace(cfg.MountPath) == "" &&
strings.TrimSpace(cfg.Namespace) == ""
}
func generateSigningSecret(length int) (string, error) {
if length <= 0 {
return "", merrors.InvalidArgument("secret length must be greater than zero", "secret_length")
}
raw := make([]byte, length)
if _, err := rand.Read(raw); err != nil {
return "", merrors.Internal("failed to generate signing secret: " + err.Error())
}
return base64.RawURLEncoding.EncodeToString(raw), nil
}