Files
cdp/ingestion/ingest/internal/service/auth.go
2026-05-24 22:59:24 +07:00

116 lines
2.8 KiB
Go

package service
import (
"context"
"sync"
"time"
"github.com/redis/rueidis"
"go.uber.org/zap"
"github.com/dbiz/cdp/ingestion/ingest/internal/apperr"
"github.com/dbiz/cdp/ingestion/ingest/internal/model"
"github.com/dbiz/cdp/ingestion/ingest/internal/repo"
)
// AuthService resolves a plaintext Write Key into the workspace + source it
// authorizes for. Lookups are cached in process AND in Redis. Pub/sub
// invalidation lets the console revoke a key and have it propagate within
// the cache TTL.
type AuthService struct {
repo repo.WriteKeyRepo
redis rueidis.Client
log *zap.Logger
ttl time.Duration
mu sync.RWMutex
cache map[string]cachedKey
}
type cachedKey struct {
key *model.WriteKey
expires time.Time
}
const (
redisKeyWritePrefix = "wk:" // wk:{plaintext} -> json
pubsubChannel = "wk:invalidate"
)
func NewAuthService(r repo.WriteKeyRepo, redis rueidis.Client, ttl time.Duration, log *zap.Logger) *AuthService {
s := &AuthService{
repo: r,
redis: redis,
log: log,
ttl: ttl,
cache: make(map[string]cachedKey),
}
go s.watchInvalidations()
return s
}
// Resolve returns the WriteKey for a plaintext token. Cached.
func (s *AuthService) Resolve(ctx context.Context, plaintext string) (*model.WriteKey, error) {
if plaintext == "" {
return nil, apperr.Unauthorized("missing write key")
}
// in-process cache
s.mu.RLock()
if entry, ok := s.cache[plaintext]; ok && time.Now().Before(entry.expires) {
s.mu.RUnlock()
if entry.key.Revoked() {
return nil, apperr.Unauthorized("write key revoked")
}
return entry.key, nil
}
s.mu.RUnlock()
// fall through to DB (Redis cache is optional and intentionally skipped
// here -- the in-process map is plenty fast; Redis is only used for the
// pub/sub invalidation channel below)
k, err := s.repo.FindByPlaintext(ctx, plaintext)
if err != nil {
return nil, err
}
if k.Revoked() {
return nil, apperr.Unauthorized("write key revoked")
}
s.mu.Lock()
s.cache[plaintext] = cachedKey{key: k, expires: time.Now().Add(s.ttl)}
s.mu.Unlock()
return k, nil
}
// Invalidate clears the cache entry for one key. Called by the console via
// pub/sub when a key is revoked.
func (s *AuthService) Invalidate(plaintext string) {
s.mu.Lock()
delete(s.cache, plaintext)
s.mu.Unlock()
}
func (s *AuthService) watchInvalidations() {
if s.redis == nil {
return
}
ctx := context.Background()
err := s.redis.Receive(ctx, s.redis.B().Subscribe().Channel(pubsubChannel).Build(),
func(msg rueidis.PubSubMessage) {
s.Invalidate(msg.Message)
s.log.Info("write key invalidated via pubsub", zap.String("prefix", maskKey(msg.Message)))
})
if err != nil {
s.log.Warn("pubsub subscribe ended", zap.Error(err))
}
}
// maskKey returns the first 8 chars + "***" for safe logging.
func maskKey(k string) string {
if len(k) <= 8 {
return "***"
}
return k[:8] + "***"
}