cached

package
v11.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 18, 2026 License: AGPL-3.0 Imports: 16 Imported by: 0

Documentation

Overview

Package cached wraps any authorization.PolicyResolver in a cache.

It is a decorator rather than a third backend: it composes with static (where it is redundant) and with database (where it is the difference between a query per session build and a query per policy change).

The cache is keyed by role names, not by principal. That is what makes it worth having — a deployment with five roles has five hot entries shared by every principal, rather than one per user, so the hit rate approaches one and the memory cost does not grow with traffic.

Why caching is safe here and would not be elsewhere

A resolved PermissionSet may live in a cache. It may never live in a credential. That distinction is the reason this package's seam sits at the resolver: a cache entry that fails to decode after a deploy degrades to a query, while a session or token that fails to decode logs its holder out. This package is consequently the only place in authorization where an encoding change can bite, and the only place where being bitten costs nothing — keys carry a format version, and any decode failure is treated as a miss rather than an error.

The cost of caching is staleness: a policy edit takes effect when the entry expires, not immediately. Invalidate narrows that window in the process that made the edit; other replicas wait out the TTL. Set the TTL to the longest delay you would accept between revoking a role's authority and it taking effect everywhere.

Both invalidation methods are declared by authorization.PolicyInvalidator, so a caller holding the interface authorizationcfg returns can type-assert for that rather than for this concrete type.

What it reports

Hit and miss counts come from the cache backend itself — cache/redis and cache/memory both emit them — so this package does not restate them. What it adds is the fault path, which the cache cannot report because from its side a stale or undecodable entry is a served result:

  • authorization_cached_read_faults, entries that could not be read and degraded to the inner resolver.
  • authorization_cached_write_faults, resolutions that could not be stored. A sustained rise here looks like a permanently cold cache, not a broken one, which is why it is worth a counter of its own.

Each resolution also carries keys.AuthorizationCacheOutcomeKey on its span, so a single slow request can be attributed to a miss without reading counters.

Index

Constants

View Source
const DefaultTTL = 5 * time.Minute

DefaultTTL is the entry lifetime when WithTTL is not supplied.

Variables

This section is empty.

Functions

This section is empty.

Types

type Option

type Option func(*Resolver)

Option configures a Resolver.

func WithLogger

func WithLogger(logger logging.Logger) Option

WithLogger attaches a logger. Cache faults are logged; hits and misses are not.

func WithMetricsProvider

func WithMetricsProvider(metricsProvider metrics.Provider) Option

WithMetricsProvider attaches a metrics provider.

The counters here deliberately do not duplicate the hit and miss counters the cache backends already emit (see cache/redis and cache/memory). They count faults: the entries this Resolver could not read or write and silently degraded around. That path is invisible from the cache's own metrics, because from the cache's side an undecodable entry is a served result.

func WithTTL

func WithTTL(ttl time.Duration) Option

WithTTL sets the entry lifetime. A non-positive value uses DefaultTTL.

func WithTracerProvider

func WithTracerProvider(tracerProvider tracing.Provider) Option

WithTracerProvider attaches a tracer provider, so that a resolution served from cache and one that fell through to the inner resolver are distinguishable in a trace rather than both being absent from it.

type Resolver

type Resolver struct {
	// contains filtered or unexported fields
}

Resolver caches the results of an inner PolicyResolver.

func NewResolver

NewResolver wraps inner with c.

func (*Resolver) Invalidate

func (r *Resolver) Invalidate(ctx context.Context, roles ...string) error

Invalidate drops the cached resolution for an exact set of roles.

Unlike a read fault, a failure here is returned rather than degraded around: the caller asked for stale policy to stop being served, and silently not doing that would leave revoked authority in place.

func (*Resolver) InvalidateAll

func (r *Resolver) InvalidateAll()

InvalidateAll makes every entry this Resolver previously wrote unreachable.

It bumps a generation counter held in memory, so it takes effect immediately in this process and not at all in others — the stale entries elsewhere expire on their TTL. Call it after a policy write so that the process that made the change stops serving what it just replaced; do not mistake it for a fleet-wide flush.

func (*Resolver) PermissionsForRoles

func (r *Resolver) PermissionsForRoles(ctx context.Context, roles ...string) (*authorization.PermissionSet, error)

PermissionsForRoles returns the cached resolution for roles, resolving and storing it on a miss.

A cache fault — unreachable backend, undecodable entry — is logged and treated as a miss. Authorization must not fail because a cache did: the inner resolver is still authoritative and still reachable, so degrading to it is both correct and the only answer that keeps requests flowing.

A fault is counted and recorded to the span, but deliberately does not set the span's status to error: the request succeeded. Marking it would make a cache blip indistinguishable from a denial in a trace search, which is exactly backwards — the whole point of the degradation is that the caller never notices. Look to the fault counters and the cache outcome attribute to notice on their behalf.

func (*Resolver) Roles

func (r *Resolver) Roles(ctx context.Context) ([]authorization.Role, error)

Roles delegates without caching. It serves admin tooling rather than the request path, and it is exactly the call an operator makes to confirm an edit landed — answering it from a cache would show them the state they were trying to change.

It is deliberately not traced. The call adds no behavior of its own, and the inner resolver traces itself, so a span here would only wrap another span with the same duration.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL