Cedar¶
Experimental
New in 0.6: its API may still change in any release, as its use shows what's missing. Scope and stability lists the tools that are settled.
Who may do what, in Cedar's policy language, evaluated in the
process by CedarEnforcer for FastAPIAuthz and AuthzMiddleware. The cedar extra installs
cedarpy, the bindings of Cedar's engine.
In an app: the example. How the dependency and the middleware work: Authorization. Every class and function: the API reference.
A Cedar policy permits or forbids a principal an action on a resource, with conditions on their
attributes; what no policy permits is denied, and a forbid beats every permit:
policies.cedar
// The members of a document's team read it.
permit (principal, action == Action::"read", resource)
when { principal.team == resource.team };
// Its owner reads and edits it.
permit (principal, action in [Action::"read", Action::"edit"], resource)
when { resource.owner == principal };
// Admins delete documents.
permit (principal in Role::"admin", action == Action::"delete", resource == Resource::"documents");
// Contractors never see a confidential document, whatever else allows it.
forbid (principal, action, resource)
when { principal.contractor && resource is Document && resource.confidential };
from fastapi_alcazaba.authz import FastAPIAuthz
from fastapi_alcazaba.authz.cedar import CedarEnforcer
enforcer = CedarEnforcer(
Path("policies.cedar").read_text(),
entities=[{"uid": {"type": "Role", "id": "admin"}, "attrs": {}, "parents": []}],
schema=Path("schema.cedarschema").read_text(), # the policies are validated against it
)
authz = FastAPIAuthz(enforcer, subject=current_user) # returns the user's entity
@app.get("/documents/{document_id}")
async def read_document(document_id: int, user: SignedIn) -> Document:
document = await load_document(document_id)
if document is None or not await authz.enforce(user, document_entity(document), "read"):
raise HTTPException(404, "No such document")
return document
- The requests:
FastAPIAuthz's (subject, object, action) become Cedar's (principal, action, resource). A name is an entity ofprincipal_type(User),action_type(Action) orresource_type(Resource):Depends(authz)asks whetherUser::"ana"mayAction::"PUT"the resourceResource::"/articles/7", andauthz.require("documents", "delete")aboutResource::"documents". Each resource named so gets anidattribute, its name, so that a policy matches paths:when { resource.id like "/articles/*" }. - Entities: what the policies read about the principal and the resource.
entities=are those of every request (roles and their hierarchy, resources with their attributes).enforcealso takes a whole entity instead of a name ({"uid": ..., "attrs": ..., "parents": [...]}): the user your authentication loaded, with its roles as parents, and the row the route just read.entities_of(subject)adds others for each request: a plain function for what's at hand, anasync deffor what it reads from the database. An entity given twice (inentities=and by the route) must be the same both times, orenforceraises ValueError, a 500: neither copy wins, as aforbidwould depend on which. context:await authz.enforce(user, resource, "deploy", {"hour": now.hour})gives the policiescontext.hour.- A schema (
schema=, in Cedar's language or JSON) validates the policies when the enforcer is made: a misspelled attribute or an action that doesn't apply to a type raises ValueError at start, not a wrong answer later. The entities are checked against it too. Invalid policies raise too. - Fails closed: a policy that can't be evaluated (an attribute an entity doesn't have, an
entity that doesn't exist) refuses the request, with a warning that says why. Cedar on its own
skips such a policy, and a
forbidthat fails would let the request through. - Several processes: each one evaluates its own copy of the policies, made at start: to
change them, deploy them, or make a new
CedarEnforcerwhen they change.