Skip to content

Write policies

Create policies that permit or forbid access, then update or delete them as your rules change. Each policy has a stable id; writing to the same id replaces it.

Use the server client with an admin connection to a fresh database with the extension installed. The later pages check access against the policies you create here.

Give putPolicy a stable ID and the rule in Cedar text. This rule lets engineers on the eng team view documents in the reports folder.

const policy = `permit (
principal in Team::"eng",
action == Action::"view",
resource in Folder::"reports"
);`
const savedTeamPolicy = await permissions.cedar.admin.putPolicy({
policyId: 'eng-view-reports',
policy,
})
if (savedTeamPolicy.error) throw savedTeamPolicy.error

Writing to an existing policy id replaces that policy. The function checks syntax and reports the line and column of an error. It does not validate against a Cedar schema:

const broken = await permissions.cedar.admin.putPolicy({
policyId: 'broken',
policy: 'permit (principal, action, resource) when {',
})
// broken.error.details: cedar:parse_error line=1 column=44

Each call takes exactly one policy. Text with two policies is an error. Call the function once for each policy.

A when condition must be true for the policy to apply. An unless condition must be false. In a condition, principal, action, resource and context are the parts of the request. An attribute of an entity comes from the stored entity (see Entities).

This policy lets the owner of a document edit it. The attribute owner of the document is an entity:

const ownerEdit = `permit (
principal,
action == Action::"edit",
resource
)
when { resource.owner == principal };`
const savedOwnerPolicy = await permissions.cedar.admin.putPolicy({
policyId: 'owner-edit',
policy: ownerEdit,
})
if (savedOwnerPolicy.error) throw savedOwnerPolicy.error

Cedar also has a JSON policy format. Use policy_json for a policy that a program writes, or one that the Cedar CLI translated. The extension stores the JSON as you give it. It rejects JSON with an unknown key, a wrong op, or a wrong shape, and the error names the path.

This forbid policy denies every action on a document with the level confidential:

const noConfidential = {
effect: 'forbid',
principal: { op: 'All' },
action: { op: 'All' },
resource: { op: 'All' },
conditions: [
{
kind: 'when',
body: {
'==': {
left: {
'.': { left: { Var: 'resource' }, attr: 'level' },
},
right: { Value: 'confidential' },
},
},
},
],
}
const savedForbid = await permissions.cedar.admin.putPolicy({
policyId: 'no-confidential',
policyJson: noConfidential,
})
if (savedForbid.error) throw savedForbid.error

Pass either policy for text or policyJson for JSON (policy_json in SQL). A call with both is the error cedar:invalid_argument. A call with neither is the error cedar:null_argument. A policy that has a slot (?principal or ?resource) is a template. See Templates.

The policyId argument names the policy. An admin check returns that ID in data.diagnostics.reason (reasons in the SQL response).

An @id("...") annotation in the text does not name the policy. It stays an annotation, and it does not have to agree with policy_id. The JSON format has no id field, so an id always comes from the call. Cedar’s own call for one policy, Policy::parse, also takes the id as an argument.

Put each policy with its own call and a stable id. The extension has no function that loads a file with many policies. A loader that names policies by position would change the ids when you add a policy at the top of the file. Split the file in your own code and call cedar_put_policy once for each policy.

Static policies, template links and templates share one set of ids. A put can replace a policy that has the same id. If the id belongs to a template or a link, the put raises cedar:id_in_use.

An annotation is metadata on a policy. It has no effect on a decision. A JSON value of null is the same as "". Use annotations for notes, owners or ticket numbers. The put stores them, and cedar_policies returns them:

const notes = `@id("old-name")
@advice("Ask the security team first.")
forbid (principal, action, resource)
when { context.maintenance };`
const savedNotes = await permissions.cedar.admin.putPolicy({
policyId: 'notes',
policy: notes,
})
if (savedNotes.error) throw savedNotes.error
const { data: annotatedPolicies } = await permissions.cedar.admin.policies()
const found = annotatedPolicies?.find((p) => p.policyId === 'notes')
// found?.annotations:
// { id: 'old-name', advice: 'Ask the security team first.' }

Use policies() to list every policy, ordered by ID. Each row includes its effect, original Cedar text (when supplied), JSON representation (est), annotations, and template details for a linked policy:

const { data: policies } = await permissions.cedar.admin.policies()
// [
// { policyId: 'eng-view-reports', effect: 'permit', ... },
// { policyId: 'no-confidential', effect: 'forbid', ... },
// ...
// ]

source is null for a policy that you put as JSON. est always holds the policy in the JSON format, so you can read a text policy as JSON.

Use deletePolicy to remove a policy or a template link by ID. An unknown ID returns an error with a hint listing the IDs that exist.

const removedNotes = await permissions.cedar.admin.deletePolicy({ policyId: 'notes' })
if (removedNotes.error) throw removedNotes.error

The policy is gone, so a second delete fails:

const deletedAgain = await permissions.cedar.admin.deletePolicy({
policyId: 'notes',
})
// deletedAgain.error.details: cedar:unknown_policy policy_id='notes'

A policy has at most 64 KiB of text, 4,096 expression nodes and 64 levels of depth. Limits lists every limit. The number of policies has no limit. A check reads only the policies whose scope can match the request. A policy with the scope All can match every request.

Continue with Store entities, then Use policy templates. The access checks later in the walkthrough use both.