Casbin#
Casbin evaluates access-control models (ACL, RBAC, ABAC) written in its own
model language, with policies stored wherever its adapters reach: a file, a database, a policy
service. CasbinAuthorizer asks the casbin.Enforcer you already use about each chunk, in your
process.
pip install "wardowl[casbin]"
import casbin
from wardowl import PermissionedRetriever
from wardowl.authorizers.casbin import CasbinAuthorizer
enforcer = casbin.Enforcer("rbac_model.conf", adapter) # your model and policy adapter
retriever = PermissionedRetriever(backend, CasbinAuthorizer(enforcer))
The request#
Each chunk is asked about as (subject.id, object, action): the object is metadata["doc_id"]
(resource_field; the chunk id when it is None) and the action read (map others with
actions={"read": "view"}). A chunk without its object is denied. Decisions go through
batch_enforce, one call per retrieval.
For ABAC models whose matchers read attributes, request builds the request yourself:
from types import SimpleNamespace
def request(subject, hit, action):
return (SimpleNamespace(id=subject.id, **subject.attributes), SimpleNamespace(**hit.metadata), action)
authorizer = CasbinAuthorizer(enforcer, request=request)
With request, wardowl sends exactly the request the function returns, one per chunk, and nothing
more: group_subjects, group_prefix, the refusal of user ids that start with the prefix, and
resource_field are all skipped, and the function receives the Casbin action (after actions). If
your policies name groups in the subject field (p, group:legal, contract-7, read), a user whose id
is group:legal would then get that group’s grants. A custom request must therefore:
refuse an id that could pass for a group or a role, by raising
AuthorizationError(anything it raises makes the retrieval raiseAuthorizationError);carry the subject’s groups itself when they come from the token, for example as an attribute the matcher reads, since
group_subjectsdoes not apply;describe a chunk that lacks the attributes the matcher reads in a way the policy denies.
from wardowl import AuthorizationError
def request(subject, hit, action):
if subject.id.startswith("group:"):
raise AuthorizationError(f"subject id {subject.id!r} could pass for a group")
who = SimpleNamespace(id=subject.id, groups=sorted(subject.groups), **subject.attributes)
return (who, SimpleNamespace(**hit.metadata), action)
Groups from the token#
If group memberships live in Casbin’s g policies, nothing else is needed. If they come from your
identity provider, group_subjects=True also asks with each of the subject’s groups in place of its
id, and allows a chunk if any answer does. Use it only with allow-only models: with deny effects,
a deny for one group would not override an allow for another, so keep memberships in g instead.
Users, roles and these groups all arrive in Casbin’s one subject field, so each group is sent with
group_prefix in front ("group:" by default): the group eng is asked about as group:eng, and
policies that grant to groups must name them that way.
p, group:eng, design-doc, read
p, alice, design-doc, read
Without the prefix, a group named like a user or a role (admin, ceo@example.com) would be given
that user’s or role’s access. A subject whose id starts with the prefix raises
AuthorizationError, since it could pass for a group; pick a prefix your user ids never start with
(group_prefix="team/", say). An empty prefix is refused.
Pre-filtering and failures#
Casbin answers yes or no per request and has no reverse lookup, so filter_for returns None and
the retriever post-filters, deciding every candidate. An enforcer that raises, or answers with
anything but one boolean per request, makes the retrieval raise AuthorizationError. To share one
enforcer between threads while policies change, use casbin.SyncedEnforcer.