Roles and claims
Every person has one role, stored on their users record and copied onto their sign-in account as a custom claim. The security rules and the Cloud Functions read the claim, and only an admin or the server can change it.
The roles
| Role | Who | What they can reach |
|---|---|---|
| (signed out) | Visitors | The public site: published content, tags, media, the public settings, signup forms. |
user | Everyone who signs up, unless the site sets another default | The member area; their own users record, except the fields listed below; reading their own storage folder and uploading their profile photo; whatever your app's rules grant on their own data. |
editor | Content staff | Writing content types, content and drafts, tags, media and the publish queue, and uploading files. Nothing in the admin area grants this role, and every admin page is for admins only, so in practice content staff are admins. |
admin | The people who run the site | Everything: the admin area, every users record, the settings, the admin callables and the whole storage bucket. |
Where each role lands after signing in (admins on /admin/dashboard, everyone else on /user/dashboard) is set in src/custom/home.ts: see Build the member area.
The two claims
| Claim | Holds | Read by |
|---|---|---|
arccms_role | The role from the users record | isAdmin() and isEditor() in the rules; isArcAdmin() and requireAdmin() in the functions |
arccms_uid | The id of the person's users record, which is not their Auth uid | ownsUserRecord(userDocId) in the rules; your app, from the ID token |
Arc CMS never reads or writes a plain role claim, and always merges its claims into the account's existing ones. So an app sharing the sign-in pool can use role for its own roles without giving anyone Arc CMS access, and claims it sets survive. Claims writes on one account take turns: each holds a short lease while it reads, writes and reads back, so Arc CMS's claims and an app's keep each other when both go through Arc CMS (an app sets its own with mergeAppClaims(); App accounts says exactly what is guaranteed). Do not name an app claim arccms_ anything. How an app reads the claims is in Follow the account contract.
How a role is granted and changed
- The first admin of a fresh install comes from the setup wizard, which calls
claimFirstAdmin. It grants admin once, and only while no admin exists. See Create the first admin. - A sign-up gets
user, or the site's default role if an admin set one (Settings, User Settings, Default Role for New Users). - An admin adds a person with a role, under Users. The
adminCreateUsercallable creates the account and the record and sets the claims at once. - An admin or the server changes
roleon ausersrecord. The Firebase console writes as the server, so it can repair a lost admin.
Whenever a record is created or its role changes, the onUserRoleChange function copies the role and the record id onto the account as claims. A blocked record gets no claims, and its sessions are ended. The admin screens are described in Users and roles.
At every sign-in the browser compares the token's claims with the record and, when they differ, asks the server to set them again (refreshMyClaims) before it lets the person in. Someone already signed in gets a new role when their ID token refreshes, within the hour, or at their next sign-in. An admin can re-apply every account's claims with the syncAllUserRoles callable.
Why nobody can promote themselves
The rules. Only admins create
usersrecords from the browser; every sign-up creates its record on the server. A person may update their own record, but neverrole,uid, the block switches (isActive,status), their email or phone, or the premium fields, andemailVerifiedmay only go to false.Settings/users, which holds the default role for sign-ups, is admin write only.The role trigger. If a role other than
useris ever written by someone who is not an admin (by the claim) and not the server,onUserRoleChangerefuses it, logs an error and puts the old role back. The claim does not follow. The server here means a write the database reports as a service account or the system, or one made by this project's own service accounts (such as<project-number>-compute@developer.gserviceaccount.com, which 2nd gen functions run as). Another project's service account is not trusted.The first admin, once.
claimFirstAdminchecks, in one transaction, that no admin has ever been made, and leaves a marker that no browser can read or write.
The browser's route guards (src/app/guards/role.guard.ts) read the role from the record (the device's copy when the offline cache has one, then the server's: What the guards wait for), but they only decide which pages show. The rules and the functions decide what data anyone can touch. See Security rules.