Features Apograph CMS on GitHub

Permissions and roles

Identity plugin @apograph/identity-server@apograph/identity-admin

The complete permission catalogue, the role matrix, and how token scopes map onto it.

Documents 0.5.2 Updated Edit this page Report a problem

On this page

Apograph has one global role per user and a flat catalogue of resource:action permission keys. Workspace membership is a separate axis deciding which workspaces a user can reach.

The catalogue

KeyAdminContributorViewer
workspaces:create
workspaces:read
workspaces:update
workspaces:delete
users:read
users:create
users:update
users:delete
activity:read
content:read
content:create
content:update
content:publish
content:approve
content:delete
content:export
content:import
media:read
media:create
media:update
media:delete
tokens:read
tokens:create
tokens:delete
copilot:use
copilot:skills:manage
alarms:read
alarms:manage
segments:read
segments:manage
views:share
webhooks:read
webhooks:manage
protection:manage

Thirty-four keys, three roles: admin holds all thirty-four, contributor eighteen, viewer seven. content:export and content:import arrived with the transfer plugin in 0.4.0; the alarms:*, segments:* and views:share keys came with alarms, segments and saved views in 0.4.3, which is also the release that granted contributor the two delete keys; the webhooks:* pair came with webhooks in 0.5.0; and content:approve and protection:manage came with publication protection.

The seeder reconciles the catalogue in both directions on every boot, so both new keys reach an existing installation by deploying — no migration, and no manual step.

The roles at a glance

RoleCanCannot
AdministratorEverything
ContributorCreate, edit, publish, approve and delete content; export and import it; upload, edit and delete media; share a saved viewManage users, mint tokens, read the audit log, write alarm or protection rules, manage audiences, or see webhooks at all
ViewerRead content, media, users and workspaces, and see which alarms and audiences applyChange anything

Admin holds the enumerated set rather than a wildcard, by decision. A new permission has to be added both to the catalogue and to admin’s grants — which means adding one cannot silently widen anybody’s access.

The grants worth explaining

copilot:use is granted to every role, viewer included. The copilot has no authority of its own, so a viewer’s copilot is provably read-only — it can only ever offer the tools that viewer’s own permissions already allow. Excluding the largest population from the feature to control cost would be solving the wrong problem; cost is handled with rate limits.

copilot:skills:manage is admin-only. A skill’s instructions are prompt text that runs for every member of the workspace, so authoring one is a configuration decision rather than a content one. Using a skill needs nothing beyond copilot:use.

alarms:read and segments:read reach the viewer, their manage halves stop at admin. Both reads are there so an editor is not working blind: findings show inline in the entry editor, and an editor who cannot see that an entry is restricted will publish one believing it is public. Both writes are configuration rather than editing — a rule is editorial policy, and renaming a segment’s tags changes who every entry naming it is visible to.

content:approve reaches the contributor; protection:manage stops at admin. Approving is an editorial act — the statement that a second person read the thing — so it lands on the role that edits rather than on a reviewer role this model does not have, and it is withheld from viewers for the same reason. Writing the rule is configuration of the same class as alarms:manage: it decides who may ship a whole content type. There is no protection:read beside it, the only asymmetry in this catalogue: a contributor does need to know that this entry wants two approvals, and that answer comes from the entry under content:read, not from the workspace’s rule table.

No API token scope grants content:approve. A token names nobody, and withholding an approve tool from every agent surface buys nothing if minting a key casts the vote the tool may not. See publication protection.

views:share gates sharing a saved view, not saving one. Every role can save a private view; that is a personal bookmark over content they can already read. A shared view becomes navigation for the whole workspace, which is an editorial decision. Viewers are not blocked from anything by its absence.

content:delete and media:delete reach the contributor. Removing the draft that should never have existed, or the wrong upload, is the same editorial act as writing it — and a role that can publish to the world but cannot retract is the more dangerous of the two. Both are audited, and a paranoid content type deletes to a tombstone that restore undoes; a non-paranoid entry and an asset’s bytes are gone, so this is a real grant.

Both webhooks:* keys stop at admin — even the read. Stricter than the other read/manage splits, deliberately: an endpoint is not scoped to a workspace, it reaches across every workspace it names, and its delivery log records where this installation talks to on the network. That is infrastructure configuration in the same family as an API token, not something an editor is already working on, so contributors do not see the page at all.

API token scopes

A token’s scope is turned into a permission set and evaluated by the same policy that evaluates a user’s. A route’s requirement means the same thing whichever kind of caller arrives.

ScopeGrants
readcontent:read
fullThe complete content CRUD set, plus media:create and media:read

media:update and media:delete are withheld from tokens deliberately: attaching an image to a record is content authoring, but curating the library is administration.

What the model cannot express

One role, everywhere

  • No per-workspace roles. A contributor is a contributor in every workspace they belong to.
  • No custom roles. The three are built in; there is no role editor and no way to define a fourth.
  • No per-type permissions. content:update covers every content type in every workspace the user belongs to.
  • No per-entry or field-level permissions. No ownership model, no “edit only what you created”, no locked fields.
  • No role for reviewing. Approving is a permission (content:approve) rather than a role, and a protection rule counts approvals from anybody who holds it except the author of the version being published — there is no assignment deciding whose approval counts, and no state between draft and published.

Workspace membership is the only tool for narrowing scope: put the content somebody should not reach in a workspace they are not a member of.

Enforcement

Server-side, @RequirePermissions plus a guard, evaluated against a pure policy. In the admin, a hook gates navigation and controls.

Hiding a control is a courtesy; the server decides. A plugin author should gate the UI and declare the permission on the route — see server plugins.