callbacks service draft
This commit is contained in:
33
api/edge/callbacks/internal/secrets/module.go
Normal file
33
api/edge/callbacks/internal/secrets/module.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/vault/kv"
|
||||
)
|
||||
|
||||
// Provider resolves secrets by reference.
|
||||
type Provider interface {
|
||||
GetSecret(ctx context.Context, ref string) (string, error)
|
||||
}
|
||||
|
||||
// VaultOptions configure Vault KV secret resolution.
|
||||
type VaultOptions struct {
|
||||
Config kv.Config
|
||||
DefaultField string
|
||||
}
|
||||
|
||||
// Options configure secret lookup behavior.
|
||||
type Options struct {
|
||||
Logger mlogger.Logger
|
||||
Static map[string]string
|
||||
CacheTTL time.Duration
|
||||
Vault VaultOptions
|
||||
}
|
||||
|
||||
// New creates secrets provider.
|
||||
func New(opts Options) (Provider, error) {
|
||||
return newProvider(opts)
|
||||
}
|
||||
224
api/edge/callbacks/internal/secrets/service.go
Normal file
224
api/edge/callbacks/internal/secrets/service.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/vault/kv"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultVaultField = "value"
|
||||
vaultRefPrefix = "vault:"
|
||||
)
|
||||
|
||||
type cacheEntry struct {
|
||||
value string
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
type provider struct {
|
||||
logger mlogger.Logger
|
||||
static map[string]string
|
||||
ttl time.Duration
|
||||
vault kv.Client
|
||||
vaultEnabled bool
|
||||
vaultDefField string
|
||||
|
||||
mu sync.RWMutex
|
||||
cache map[string]cacheEntry
|
||||
}
|
||||
|
||||
func newProvider(opts Options) (Provider, error) {
|
||||
logger := opts.Logger
|
||||
if logger == nil {
|
||||
logger = zap.NewNop()
|
||||
}
|
||||
|
||||
static := map[string]string{}
|
||||
for k, v := range opts.Static {
|
||||
key := strings.TrimSpace(k)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
static[key] = v
|
||||
}
|
||||
|
||||
vaultField := strings.TrimSpace(opts.Vault.DefaultField)
|
||||
if vaultField == "" {
|
||||
vaultField = defaultVaultField
|
||||
}
|
||||
|
||||
var vaultClient kv.Client
|
||||
vaultEnabled := false
|
||||
hasVaultConfig := strings.TrimSpace(opts.Vault.Config.Address) != "" ||
|
||||
strings.TrimSpace(opts.Vault.Config.TokenEnv) != "" ||
|
||||
strings.TrimSpace(opts.Vault.Config.MountPath) != ""
|
||||
if hasVaultConfig {
|
||||
client, err := kv.New(kv.Options{
|
||||
Logger: logger.Named("vault"),
|
||||
Config: opts.Vault.Config,
|
||||
Component: "callbacks secrets",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vaultClient = client
|
||||
vaultEnabled = true
|
||||
}
|
||||
|
||||
return &provider{
|
||||
logger: logger.Named("secrets"),
|
||||
static: static,
|
||||
ttl: opts.CacheTTL,
|
||||
vault: vaultClient,
|
||||
vaultEnabled: vaultEnabled,
|
||||
vaultDefField: vaultField,
|
||||
cache: map[string]cacheEntry{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *provider) GetSecret(ctx context.Context, ref string) (string, error) {
|
||||
key := strings.TrimSpace(ref)
|
||||
if key == "" {
|
||||
return "", merrors.InvalidArgument("secret reference is required", "secret_ref")
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
if value, ok := p.fromCache(key); ok {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
value, err := p.resolve(ctx, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return "", merrors.NoData("secret reference resolved to empty value")
|
||||
}
|
||||
|
||||
p.toCache(key, value)
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (p *provider) resolve(ctx context.Context, key string) (string, error) {
|
||||
if value, ok := p.static[key]; ok {
|
||||
return value, nil
|
||||
}
|
||||
if strings.HasPrefix(key, "env:") {
|
||||
envKey := strings.TrimSpace(strings.TrimPrefix(key, "env:"))
|
||||
if envKey == "" {
|
||||
return "", merrors.InvalidArgument("secret env reference is invalid", "secret_ref")
|
||||
}
|
||||
value := strings.TrimSpace(os.Getenv(envKey))
|
||||
if value == "" {
|
||||
return "", merrors.NoData("secret env variable not set: " + envKey)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(strings.ToLower(key), vaultRefPrefix) && !p.vaultEnabled {
|
||||
return "", merrors.InvalidArgument("vault secret reference provided but vault is not configured", "secret_ref")
|
||||
}
|
||||
if p.vaultEnabled {
|
||||
value, resolved, err := p.resolveVault(ctx, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resolved {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", merrors.NoData("secret reference not found: " + key)
|
||||
}
|
||||
|
||||
func (p *provider) resolveVault(ctx context.Context, ref string) (string, bool, error) {
|
||||
path, field, resolved, err := parseVaultRef(ref, p.vaultDefField)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if !resolved {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
value, err := p.vault.GetString(ctx, path, field)
|
||||
if err != nil {
|
||||
p.logger.Warn("Failed to resolve vault secret", zap.String("path", path), zap.String("field", field), zap.Error(err))
|
||||
return "", true, err
|
||||
}
|
||||
return value, true, nil
|
||||
}
|
||||
|
||||
func parseVaultRef(ref, defaultField string) (string, string, bool, error) {
|
||||
raw := strings.TrimSpace(ref)
|
||||
lowered := strings.ToLower(raw)
|
||||
explicit := false
|
||||
if strings.HasPrefix(lowered, vaultRefPrefix) {
|
||||
explicit = true
|
||||
raw = strings.TrimSpace(raw[len(vaultRefPrefix):])
|
||||
}
|
||||
|
||||
if !explicit && !strings.Contains(raw, "/") && !strings.Contains(raw, "#") {
|
||||
return "", "", false, nil
|
||||
}
|
||||
|
||||
field := strings.TrimSpace(defaultField)
|
||||
if field == "" {
|
||||
field = defaultVaultField
|
||||
}
|
||||
|
||||
if idx := strings.Index(raw, "#"); idx >= 0 {
|
||||
field = strings.TrimSpace(raw[idx+1:])
|
||||
raw = strings.TrimSpace(raw[:idx])
|
||||
if field == "" {
|
||||
return "", "", false, merrors.InvalidArgument("vault secret field is required", "secret_ref")
|
||||
}
|
||||
}
|
||||
|
||||
path := strings.Trim(strings.TrimSpace(raw), "/")
|
||||
if path == "" {
|
||||
return "", "", false, merrors.InvalidArgument("vault secret path is required", "secret_ref")
|
||||
}
|
||||
|
||||
return path, field, true, nil
|
||||
}
|
||||
|
||||
func (p *provider) fromCache(key string) (string, bool) {
|
||||
if p.ttl <= 0 {
|
||||
return "", false
|
||||
}
|
||||
p.mu.RLock()
|
||||
entry, ok := p.cache[key]
|
||||
p.mu.RUnlock()
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
if time.Now().After(entry.expiresAt) {
|
||||
p.mu.Lock()
|
||||
delete(p.cache, key)
|
||||
p.mu.Unlock()
|
||||
return "", false
|
||||
}
|
||||
return entry.value, true
|
||||
}
|
||||
|
||||
func (p *provider) toCache(key, value string) {
|
||||
if p.ttl <= 0 {
|
||||
return
|
||||
}
|
||||
p.mu.Lock()
|
||||
p.cache[key] = cacheEntry{
|
||||
value: value,
|
||||
expiresAt: time.Now().Add(p.ttl),
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
Reference in New Issue
Block a user