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 raise AuthorizationError);

  • carry the subject’s groups itself when they come from the token, for example as an attribute the matcher reads, since group_subjects does 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.