C&C Docs
Log in
Platform services

$can and the policy

Who may read or change a row is decided by rules the app ships in a migration. The page never checks an owner or a role itself: it reads, writes and asks $can, and the rules answer. Three notes were seeded by the platform: one published, one embargoed, one draft. Add your own and see which ones each rule lets you see and change.

Signed out, you read the published note and nothing else, and every write is refused. Log in or create an account to own notes of your own.

  • Release notes (the platform seeded this, and published it)
    published
// migrations/050-g2-security-policy.json (excerpt)
{ "op": "setPolicy", "channel": "security-tour", "rules": [
  "create(SecNote, User) :- has(User.email)",
  "read(SecNote, u)      :- _owner(SecNote, u) || eq(SecNote.state, \"published\")",
  "deny_read(SecNote, u) :- eq(SecNote.state, \"embargoed\")",
  "delete(SecNote, User) :- _owner(SecNote, User)",
  "publish(SecNote, User) :- _owner(SecNote, User)"
] }

// A rule reads: verb(Target, Actor) :- condition
// Nothing is allowed until a rule allows it, and a deny_ rule always wins.

Buttons only for what you may do

The Publish and Delete buttons carry x-can: where the answer is no, the button is not in the page at all. $can(verb, target) asks the same question from code.

<button x-can="publish" x-can-on="n.key" @click="$publish(n.key)">Publish</button>
<button x-can="delete"  x-can-on="n.key" @click="$remove(n.key)">Delete</button>

// publish is a verb the app made up: the store never checks it, so the
// method asks before it acts.
async $publish(key) {
  if (!(await this.$can('publish', key))) return
  await this.$data.update(key, { state: 'published' })
}

A hidden button is not the protection. The seeded note has no Delete button for you; call the delete anyway and the store refuses it.