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 }