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] + "***" }