Skip to content

Composites

Cedar has no functions, no modules, and no loops, so every repeated policy shape is copy-paste in .cedar text. A TypeScript factory is the only place the abstraction can live.

These follow chant’s general composite resources pattern: a function returning declared resources, discovered like any other export.

import { OwnerCanManage, DenyByDefaultSet } from "@intentius/chant-lexicon-cedar";

“The owner of a thing may act on it” — the most-repeated shape in any policy set, and where the two classic mistakes get made: the when guard names an attribute the schema spells differently, or the grant is written wide and the scoping clause is forgotten.

import { ReadAction, WriteAction } from "@intentius/chant-lexicon-cedar";
export const docOwner = OwnerCanManage({
entityType: "App::Document",
actions: [ReadAction, WriteAction],
principal: "App::User",
});

Emits:

@id("doc-owner")
@composite("OwnerCanManage")
@scopedTo("App::Document")
permit (
principal is App::User,
action in [App::Action::"read", App::Action::"write"],
resource is App::Document
)
when { resource.owner == principal };
OptionDefaultNotes
entityTyperequiredEntityTypeName — schema-checked
actionsunconstrainedOne action emits ==, several emit in [ … ]
ownerAttribute"owner"The attribute holding the owner
principalunconstrainedA bare type string becomes is T; a full scope passes through
whenAppended after the ownership test
unlessOmitted entirely when not asked for
annotationsMerged over the generated ones; an explicit id wins

Leaving actions off produces a wide grant, so it has to be asked for by omitting the field rather than arriving by accident.

A guarded forbid and the permits it governs, returned from one call. The pattern teams write by hand is a forbid at the top of a file and a pile of permits under it with nothing tying the two together — delete the forbid and the permits keep working, wider than anyone intended.

import { DeleteAction } from "@intentius/chant-lexicon-cedar";
const guarded = DenyByDefaultSet({
policies: [docOwner],
entityType: "App::Document",
actions: DeleteAction,
when: ['resource.classification == "confidential"'],
unless: ['principal == App::User::"archivist"'],
});
export const confidentialFloor = guarded.floor;
export const documentOwnerGrant = guarded.members[0];
ReturnedWhat it is
floorThe forbid policy
membersThe permits, unchanged, in the order given
all[floor, ...members]

when is required. An unguarded forbid overrides every permit in the set, so the result would authorize nothing — the composite throws rather than emit it.

They return Declarable values like any other resource, so exporting them from a discovered file is all that is needed:

export const [floor, grant] = DenyByDefaultSet({ /* … */ }).all;

The floor is emitted first. Cedar’s evaluation is order-independent — a forbid wins wherever it sits — but a file that reads floor-first matches how the set is reasoned about.