OpenFGA#
Relationship-based access control in the style of Google Zanzibar. No extra dependency: requests use the standard library.
from wardowl.authorizers.openfga import OpenFGAAuthorizer
authorizer = OpenFGAAuthorizer("http://localhost:8080", store_id, resource_field="doc_id")
The model#
The authorizer asks whether user:<subject id> has a relation (by default viewer for the action
read) on document:<resource id>. A model such as this one fits:
model
schema 1.1
type user
type group
relations
define member: [user]
type document
relations
define viewer: [user, user:*, group#member]
Chunks of one source document share its id in metadata (doc_id above), so a permission 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, or whose field holds anything but one string (a list,
a number), is denied. So is a chunk whose resource id OpenFGA refuses in an object: one that
contains :, #, a space or a control character, the id *, or an id that makes
document:<id> longer than 256 characters. Such an id is never sent: OpenFGA would refuse the
whole BatchCheck request, so every retrieval that found the chunk would return nothing.
Subject ids and group names are written into OpenFGA’s type:id strings, where bob#manager
would be a userset, * the wildcard and a : another type. An id or group name that is empty or
contains :, #, * or whitespace therefore raises AuthorizationError instead of being sent.
Change the names with object_type=, user_type=, relations={"read": "viewer"},
group_type= and group_relation=. Pass authorization_model_id= to pin a model version.
Checks#
check sends BatchCheck requests, batch_size (default 50) resources at a time, and asks about each
distinct resource once even when several chunks share it. A per-item error, or a response that does
not decide every resource, raises AuthorizationError.
Pre-filtering with ListObjects#
filter_for calls ListObjects for every document the user may view and returns
In(resource_field, ids), so the vector store pre-filters.
OpenFGA stops a ListObjects call when it has found OPENFGA_LIST_OBJECTS_MAX_RESULTS objects (1000
by default) or when it reaches OPENFGA_LIST_OBJECTS_DEADLINE (3 seconds by default), and answers
with what it found so far, without marking the answer as partial. A partial list would never let
anyone read more than they may, but it would hide documents they may read while the result still
looked complete. So when the answer reaches list_objects_limit (default 1000), or the call took
list_objects_deadline seconds or more (default 3), the authorizer returns no filter and the
retriever post-filters with BatchCheck instead. Set both to your server’s values;
list_objects_deadline=None suits a server configured without a deadline. With the default
timeout of 2 seconds, a call slow enough to meet the default deadline fails first and raises.
In matches a list field when any of its elements is listed, while check denies a chunk whose
resource field is not one string. The retriever’s re-check of pre-filtered hits (recheck=True,
the default) drops such a chunk and counts it in violations. With recheck=False the filter
alone decides, so the resource field must be one string on every chunk.
Consistency#
consistency="HIGHER_CONSISTENCY" asks OpenFGA to skip its cache for every check and list, so a
tuple written a moment ago counts at once, at a cost in latency. "MINIMIZE_LATENCY", OpenFGA’s
default, may answer from its cache when one is configured (caching is off by default), so a revoked
tuple can keep granting access until that cache entry expires. Leaving consistency unset sends
nothing and lets the server decide. Choose it together with a
CachingAuthorizer’s ttl: an answer the cache keeps may
itself have come from OpenFGA’s cache, so the revocation delay can reach the two added together.
Groups from your identity provider#
With contextual_groups=True (the default), the subject’s groups are sent with every request as
contextual tuples (user:<id> member group:<name>), so group memberships from your identity
provider take effect without being written to OpenFGA first. They are trusted as given, so they
must come from authentication, not from the client.
Tenants in one store#
With every tenant in one store, qualify group names with the tenant, in the tuples and in the
Subject’s groups: acme/hr, not hr (:, #, * and spaces are refused). A contextual tuple
otherwise makes the user a member of group:hr, the one group of that name for every tenant, so
check grants them every tenant’s documents shared with hr, and ListObjects lists them all. A
long enough list reaches list_objects_limit, the authorizer then returns no filter, and every
retrieval quietly post-filters. Qualify document ids the same way, and add the tenant check,
AllOf(TenantAuthorizer(), OpenFGAAuthorizer(...)), so that no tuple can grant across tenants
(Combining). A store per tenant, with an authorizer each, keeps
them apart too.
Failing closed#
Every failure raises AuthorizationError: an unreachable server, a timeout (timeout=, 2 seconds
by default, for the whole call except the DNS lookup of the host, as
for OPA), an unexpected response or one larger than max_response_bytes=
(10 MiB by default), or an action with no configured relation. headers= adds headers to every
request, for example an API token; an Authorization header over http:// to another machine
needs allow_insecure=True, as for OPA. Connections are reused and
transient failures retried once (retries=); circuit_breaker= and ssl_context= work as
described under resilience.
For high traffic, wrap it in a CachingAuthorizer.