Revocation and consistency#
When someone loses access to a document, how soon does needtoknow stop returning it? The delay is the sum of what each layer adds. This page lists the layers, then two ways to configure them: a stated bound on staleness, which suits most workloads, and strict revocation, for access that must stop as soon as it is withdrawn.
Where the delay comes from#
Where the permission lives. With permissions on the chunks (
MetadataACLAuthorizer,TenantAuthorizer, or an OPA or Cerbos policy that reads chunk metadata), a change takes effect when the chunks are updated:set_metadata, a re-index, or the next run of a permission sync such as Google Drive. With a permission service (OpenFGA, SpiceDB, OPA data, Cerbos policies), it takes effect when the service has the change.The permission service’s own consistency.
OpenFGA with
consistency="MINIMIZE_LATENCY"(its default) may answer from its cache, when one is configured;"HIGHER_CONSISTENCY"skips it. See OpenFGA.SpiceDB with
{"minimizeLatency": True}(the default here) answers from a recent snapshot, up to the datastore’s quantization interval (5 seconds by default);{"fullyConsistent": True}, or{"atLeastAsFresh": {"token": zedtoken}}with the token from your write, answers from current data.OPA and Cerbos answer from the policies and data they have loaded; bundle polling or a policy reload adds its interval.
CachingAuthorizer. A cached decision is used for at mostttlseconds.invalidate()drops decisions at once in its own process, including answers that were still being fetched, and reaches other replicas sharing a Redis withinlocal_ttl. See permission caching.The store’s visibility delay, when permissions live on chunks: some stores show an update to searches only after a refresh (Elasticsearch and OpenSearch refresh every second by default).
Requests already running. A retrieval that started before the revocation may finish with the decision it already had. needtoknow never caches such an answer after
invalidate(), but it cannot recall a result already returned, or what a model has already been given.
Bounded staleness#
The usual choice: answers are cached, and the longest a revoked permission keeps granting access is known and chosen.
authorizer = CachingAuthorizer(
OpenFGAAuthorizer(FGA_URL, STORE_ID),
ttl=30,
shared=RedisCache(redis_client, signing_key=CACHE_SIGNING_KEY),
local_ttl=1,
)
Revocations nobody reports (a change made directly in the permission service): the delay is at most the service’s own staleness plus
ttl.Revocations you report by calling
authorizer.invalidate(user_id)after the change: no retrieval that starts in that process afterwards uses the old decision, and other replicas stop withinlocal_ttl. The tests revoke a permission under concurrent retrievals, with one and three replicas, and check exactly this.
Choose ttl as a security decision. 30 seconds removes most repeated calls within a conversation;
a few seconds still removes many.
Strict revocation#
For access that must stop at once, such as when an account is disabled, make every layer current and put the revocation in the request path of the change itself:
Ask the service for current answers:
OpenFGAAuthorizer(..., consistency="HIGHER_CONSISTENCY"), orSpiceDBAuthorizer(..., consistency={"fullyConsistent": True}), or pass SpiceDB the zedtoken of your write withatLeastAsFresh.Do not cache, or invalidate before you report success. Either leave
CachingAuthorizerout, or callinvalidate(user_id)in the same code that applies the revocation, and only then tell the administrator it is done.invalidate()raises if it cannot reach Redis.For permissions on chunks, call
set_metadatain that same code, and allow for the store’s visibility delay.Keep
recheck=True(the default), so every pre-filtered hit is decided by the authorizer as well as by the filter.
The price is a call to the permission service for every retrieval, with its latency and load; the benchmarks show what an OPA call and a cache cost under load. Even so, a retrieval already running when the revocation happens can complete with its earlier answer: strict means no request that starts afterwards sees the document, not that one in flight is recalled.
Checking it#
RetrievalEvent.started_at and the returned chunk ids, in your audit log, show whether a revoked
document was returned after a given time. To measure your own deployment’s delay, revoke a test
permission while traffic runs and find the last retrieval that returned the document, as
tests/test_revocation.py does.