init ingestion
This commit is contained in:
115
ingestion/ingest/internal/service/auth.go
Normal file
115
ingestion/ingest/internal/service/auth.go
Normal file
@@ -0,0 +1,115 @@
|
||||
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] + "***"
|
||||
}
|
||||
Reference in New Issue
Block a user