Cerbos#

Policy-based access control with Cerbos, through the Cerbos PDP’s HTTP API. No extra dependency: requests use the standard library.

docker run -p 3592:3592 -v "$PWD/policies:/policies" ghcr.io/cerbos/cerbos:0.56.0
from needtoknow.authorizers.cerbos import CerbosAuthorizer

authorizer = CerbosAuthorizer("http://localhost:3592")

What Cerbos sees#

Each chunk is a resource of kind document (change it with resource_kind=) whose attributes are the chunk’s metadata, and whose id is the chunk id (or the metadata field resource_field=). The subject is the principal:

{"id": "alice", "roles": ["user"], "attr": {"groups": ["eng", "hr"], "region": "eu"}}

roles= is a list of roles for every principal, or a function of the subject; attr holds the subject’s groups and its attributes. A subject attribute named groups is refused with AuthorizationError, since it would replace the subject’s groups. Pass policy_version= and scope= to select a policy version and scope, and actions={"read": "view"} to map needtoknow’s action to yours.

A policy for permissions stored on the chunks (examples/cerbos/policies/document.yaml):

apiVersion: api.cerbos.dev/v1
resourcePolicy:
  version: default
  resource: document
  rules:
    - actions: ["read"]
      effect: EFFECT_ALLOW
      roles: ["*"]
      condition:
        match:
          any:
            of:
              - expr: request.principal.id in request.resource.attr.allowed_users
              - expr: '"*" in request.resource.attr.allowed_groups'
              - expr: hasIntersection(request.resource.attr.allowed_groups, request.principal.attr.groups)

Checks#

check sends CheckResources requests of batch_size resources (50, Cerbos’s default limit per request) and reads EFFECT_ALLOW or EFFECT_DENY for each. An answer for a different resource, a missing or unknown effect, or a wrong number of results raises AuthorizationError.

Pre-filtering from query plans#

filter_for asks PlanResources which documents the principal may read. Cerbos answers with a query plan: always allowed, always denied, or a condition over resource attributes, with the principal’s attributes already filled in. needtoknow translates the parts of the plan it can express exactly:

Condition

Filter

and, or

And, Or

hasIntersection(request.resource.attr.F, ["a", "b"])

In(F, ["a", "b"])

"a" in request.resource.attr.F (membership in a list field)

In(F, ["a"])

request.resource.attr.F == "a", request.resource.attr.F in ["a", "b"]

In(F, ...)

always denied

a filter that matches nothing

Anything else (!=, not, comparisons, exists and other macros, nested attribute paths, boolean or float values) and “always allowed” give no filter, and the retriever post-filters, with Cerbos deciding each candidate. Results stay correct either way; pre-filtering is what keeps recall high for users who may read little, so write the rules that decide reading in the subset above.

CEL’s == compares a list attribute as a whole, while needtoknow’s In matches any element of a list. Keep attributes compared with == scalar, and use hasIntersection or in for list attributes such as allowed_groups; otherwise pre-filtered hits fail the re-check and are reported as violations.

Failures#

Every failure raises AuthorizationError, so retrieval returns nothing. Connections are reused, transient failures are retried once, and a CircuitBreaker can pause requests to a failing PDP (see Operations).

CI runs the authorizer against Cerbos 0.56.0 with the example policy: the policy yields a pre-filter, every strategy returns exactly the reference answer for random documents, and check and filter_for pass the conformance kit.