SpiceDB#
Relationship-based access control in the style of Google Zanzibar, through SpiceDB’s HTTP API. No extra dependency: requests use the standard library. Start SpiceDB with its HTTP gateway:
spicedb serve --grpc-preshared-key "$SPICEDB_KEY" --http-enabled # HTTP on :8443
from needtoknow.authorizers.spicedb import SpiceDBAuthorizer
authorizer = SpiceDBAuthorizer("http://localhost:8443", token=SPICEDB_KEY, resource_field="doc_id")
The schema#
The authorizer asks whether user:<subject id> has a permission (by default view for the action
read) on document:<resource id>. A schema such as this one fits:
definition user {}
definition group {
relation member: user
}
definition document {
relation viewer: user | user:* | group#member
permission view = viewer
}
Chunks of one source document share its id in metadata (doc_id above), so a relationship is
written once per document, not once per chunk. With resource_field=None the chunk id is the
resource. A chunk without its resource id is denied. Change the names with resource_type=,
subject_type=, permissions={"read": "view"}, group_type= and group_relation=.
SpiceDB object ids allow letters, digits and /_|-=+. Store such an id in the chunk’s
resource_field and use such subject ids (for example a stable user id rather than an email
address); an id SpiceDB rejects makes the request fail, so retrieval fails closed.
Groups from your identity provider#
By default the subject’s groups are checked as subject sets, group:<name>#member, as well as the
user itself. A document shared with group:eng#member is then readable by anyone whose Subject
lists eng, without syncing group memberships into SpiceDB. If memberships do live in SpiceDB,
pass group_subjects=False so only the user is checked.
Checks#
check sends CheckBulkPermissions requests of batch_size questions (100 by default): one per
distinct resource and subject. Answers are matched to questions by the request SpiceDB echoes
with each one, not by position. A conditional answer (a caveat whose context was not supplied) is
not a decision and raises AuthorizationError; supply caveat context with
caveat_context=lambda subject: {...}.
Pre-filtering#
filter_for calls LookupResources for the user and for each group subject, and turns the union of
the documents found into In("doc_id", ...), so the vector store filters inside the search. If a
lookup returns lookup_limit documents (1000 by default) the list may be truncated, so no filter
is returned and the retriever post-filters instead; it does the same when a lookup result is
conditional on a caveat. Subjects who may read very many documents are therefore post-filtered,
which stays correct.
Consistency and revocation#
The default consistency, {"minimizeLatency": True}, lets SpiceDB answer from a recent snapshot.
A revoked permission can then keep granting access for up to the datastore’s revision quantization
interval, 5 seconds by default. For fresher answers pass consistency={"fullyConsistent": True},
or {"atLeastAsFresh": {"token": zedtoken}} with the ZedToken from your last write. Combined with
CachingAuthorizer, the revocation bound is the cache’s ttl plus this interval.
Failures#
Every failure raises AuthorizationError, so retrieval returns nothing: an unreachable server, a
timeout, an HTTP error, a malformed answer, a per-item error, or a missing answer. Connections are
reused, transient failures are retried once, and a CircuitBreaker can pause requests to a failing
server (see Operations).
CI checks the authorizer against a real SpiceDB (v1.56.2): for random relationship graphs, every
strategy returns exactly the reference answer, and check and filter_for pass the
conformance kit.