179 lines
5.4 KiB
Go
179 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/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), zap.String("callback_ref", callbackRef.Hex()))
|
|
|
|
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
|
|
}
|