Goal of this module

Create an editor policy and a viewer policy, attach them to two users, and prove the difference by watching a viewer's write get denied at the gateway.

Permissions come only from ACL

Every app request is evaluated against ACL policies. There's no special admin API — policies, groups, roles, and the links between them are ordinary tenant system entities you manage with the same CRUD endpoints you already used for Project and Task.

A role is a label, not a permission

A tenant userrole (and the JWT role claim) grants nothing — it's a free-form label for your own UI logic. To change what a user can do, attach an aclpolicy, directly or through a group.

The IAM system entities

  • aclpolicyname + statements[]. The actual grant.
  • groupname + acl_policies[]. A bundle attached to many users at once.
  • useraclpolicy — a link: user_id + acl_policy_id. One policy → one user.
  • usergroup — a link: user_id + group_id. Puts a user in a group.
  • userrole — a name only. A label; grants nothing.

A statement

Actions map from the route: POSTcreate, PUTupdate, DELETEdelete, GET /list/*list, GET /item/*get. Forms include createtask (verb + entity), a wildcard like get*, or * for everything. Evaluation gathers all of a user's statements — direct and via groups — and applies: any matching Deny wins; else any matching Allow allows; else implicit deny.

json
{
  "Sid": "EditTasks",
  "Effect": "Allow",
  "Action": ["createtask", "updatetask", "deletetask"],
  "Resource": ["*"]
}

1 · The editor policy

bash
POST /app/{orgCode}/create/aclpolicy
{ "name": "task-editor",
  "statements": [
    { "Sid": "Read",  "Effect": "Allow", "Action": ["get*","list*","count*"], "Resource": ["*"] },
    { "Sid": "Write", "Effect": "Allow",
      "Action": ["createtask","updatetask","deletetask"], "Resource": ["*"] }
  ] }
# -> { "id": "<editorAclId>" }

2 · The viewer policy

bash
POST /app/{orgCode}/create/aclpolicy
{ "name": "task-viewer",
  "statements": [
    { "Sid": "ReadOnly", "Effect": "Allow", "Action": ["get*","list*","count*"], "Resource": ["*"] }
  ] }
# -> { "id": "<viewerAclId>" }

3 · Attach to users

Attach a policy directly to a user, or bundle it in a group and add the user to the group. Either works; groups scale better when many users share access.

bash
# direct attach — make Bob an editor
POST /app/{orgCode}/create/useraclpolicy
{ "user_id": "<bobId>", "acl_policy_id": "<editorAclId>" }

# via a reusable group — make Vera a viewer
POST /app/{orgCode}/create/group
{ "name": "viewers", "acl_policies": ["<viewerAclId>"] }   # -> { "id": "<groupId>" }
POST /app/{orgCode}/create/usergroup
{ "user_id": "<veraId>", "group_id": "<groupId>" }

Send only the ids on links

For useraclpolicy and usergroup, send just the id fields — the platform populates the lookup keys the authorizer needs. Never set those by hand.

4 · Watch the deny

1As Bob (editor), POST /app/{orgCode}/create/task — succeeds.
2As Vera (viewer), GET /app/{orgCode}/list/task — succeeds (read is allowed).
3As Vera, POST /app/{orgCode}/create/taskdenied by the authorizer. No matching Allow, implicit deny.

Changes take effect on the next token

The authorizer caches a user's resolved ACL per token. After changing policies, have the user re-login (or wait out the cache) to pick them up.

Try it

Create both policies, make one user an editor and one a viewer, then trigger the viewer's denied write. Seeing that denial — without writing a single permission check yourself — is the whole point.

Go deeper