package rbac import ( "context" "fmt" "time" "code.nochebuena.dev/einherjar/contracts/security" ) var _ security.PermissionProvider = (*cachedPermissionProvider)(nil) type cachedConfig struct { keyFn func(security.SecurityBag, string, string) string } type cachedPermissionProvider struct { inner security.PermissionProvider cache Cache ttl time.Duration cfg cachedConfig } // NewCachedPermissionProvider wraps any PermissionProvider with a TTL cache. // // Default cache key format: // - "rbac:{uid}:{resource}" — single-tenant (no TenantID in bag) // - "rbac:{tenantID}:{uid}:{resource}" — multi-tenant (TenantID non-empty) // // TenantID is read automatically from the [security.SecurityBag] in context — // no API change required. Use [WithCacheKey] to override the key function when // additional bag attributes (hardware IDs, grant codes) must be part of the key. // // Cache errors are silently swallowed — falls through to the inner provider. // Set errors are ignored (cache is best-effort). func NewCachedPermissionProvider(inner security.PermissionProvider, cache Cache, ttl time.Duration, opts ...CachedOpt) security.PermissionProvider { cfg := cachedConfig{} for _, o := range opts { o(&cfg) } return &cachedPermissionProvider{inner: inner, cache: cache, ttl: ttl, cfg: cfg} } func (p *cachedPermissionProvider) ResolveMask(ctx context.Context, uid, resource string) (security.PermissionMask, error) { var key string if p.cfg.keyFn != nil { bag, _ := security.BagFromContext(ctx) key = p.cfg.keyFn(bag, uid, resource) } else { key = p.defaultKey(ctx, uid, resource) } if cached, ok, err := p.cache.Get(ctx, key); err == nil && ok { return security.PermissionMask(cached), nil } mask, err := p.inner.ResolveMask(ctx, uid, resource) if err != nil { return 0, err } _ = p.cache.Set(ctx, key, int64(mask), p.ttl) return mask, nil } func (p *cachedPermissionProvider) defaultKey(ctx context.Context, uid, resource string) string { bag, ok := security.BagFromContext(ctx) if ok && bag.Identity().TenantID != "" { return fmt.Sprintf("rbac:%s:%s:%s", bag.Identity().TenantID, uid, resource) } return fmt.Sprintf("rbac:%s:%s", uid, resource) }