Roles and permissions
Authorization is Casbin-backed and organized into two planes that no single user can span, because a user holds exactly one role and a role belongs to exactly one plane.
The two planes
Section titled “The two planes”| Plane | Roles | What they do |
|---|---|---|
| Structural | superadmin, admin |
Design the system: profiles, table definitions, scripts, validations, roles, and user assignments |
| Data | guest, accountant, and every role an admin creates |
Work inside the tables the structural roles defined |
The boundary is a Rust-level invariant, not a policy row: a structural role can never write row data, no matter what grants exist. This is checked in code, so no amount of grant editing can turn a structural role into one that posts data.
Built-in roles
Section titled “Built-in roles”| Role | Kind | Holds |
|---|---|---|
superadmin |
structural | Everything admin holds, plus backups and read access to all data, all journals, and all ECB conversion data |
admin |
structural | The seven structural areas: profile, table, script, validation, role, user, accounting — but no data access and no backups |
guest |
data | Nothing at all, until an admin grants it something or moves the user |
accountant |
data | Seeded for convenience but deliberately not built-in: it starts empty and this deployment’s admin decides what it may do |
superadmin, admin, and guest are built-in: they cannot be renamed or removed through the API. accountant is seeded but removable like any other data role.
Why can an admin not take a backup? A dump contains every row. Backup therefore sits with superadmin alone, because admin does not read data.
Authority ranking
Section titled “Authority ranking”Every “may X administer Y” question is answered by one rule:
superadmin (3) > admin (2) > every data role (1)An actor may edit a role, edit its grants, or assign it to a user only when the target ranks strictly below the actor. Nobody may touch a role at or above their own authority:
- an admin cannot mint another admin or edit superadmin;
- an admin cannot promote a user past themselves;
- a data role cannot administer another data role.
Because roles created at runtime are always data roles, an admin who takes a data role gives up being an admin — the two planes stay separate.
Permission objects and actions
Section titled “Permission objects and actions”An object is a string with a fixed shape:
| Shape | Meaning |
|---|---|
struct:<area> |
A structural area, granted only by the built-in policy, never through the API |
data:<profile>/<table> |
One root table and its whole template family |
data:<profile>/* |
Every table in a profile, including tables defined later |
data:* |
Every table everywhere |
journal:<profile> |
The accounting journal of one profile |
journal:* |
Every profile’s journal |
ecb:<profile> |
ECB conversion previews and evidence for one profile |
ecb:* |
ECB conversion data everywhere |
Objects match by prefix, so the * wildcards cover tables that do not exist yet. Actions match exactly, so a grant never widens into a neighboring action.
| Plane | Actions |
|---|---|
| Data | read, insert, update, delete |
| Structural | manage only |
| ECB data | read only — conversion data is written by the server |
One grant covers a template family
Section titled “One grant covers a template family”An invoice header and its line-item children share one root table definition. A grant on the family root — for example data:acme/invoices — covers every child table, including children added by a later regeneration. Only family roots are grantable; naming a child directly is rejected.
What an admin may grant
Section titled “What an admin may grant”The grant API enforces every rule that keeps the planes apart:
- the object must be a data-plane object; structural areas cannot be granted;
- the target role must rank below the actor;
- a structural role may receive
readon data but never a write action; - ECB objects accept
readonly; - the profile and table named must actually exist;
- only family roots are grantable.
ListGrantableObjects returns the current menu: the three global objects, then per-profile data:<profile>/*, journal:<profile>, ecb:<profile>, and one object per root table. A grant on a profile’s tables never includes its journal or its ECB data — those are separate objects an admin grants deliberately.
Role inheritance
Section titled “Role inheritance”A data role may inherit from another data role. Inherited grants count as the role’s own, so removing a parent removes what the child gained from it.
Inheritance may never cross planes: a data role inheriting a structural one would smuggle system permissions onto the data plane, so such an edge is refused when the role is created and ignored if it ever appears in storage.
User administration
Section titled “User administration”ListUsers returns every account. AssignUserRole moves a user to another role and checks both ends of the change: the new role must rank below the actor, and the user being moved must not already hold a role at or above the actor’s authority.
A role cannot be removed while users hold it or other roles inherit from it; those refusals fail loudly instead of silently stripping someone’s access. Removing a role removes its grants, which cascade with it.
How a change takes effect
Section titled “How a change takes effect”Every role or grant write rebuilds the in-memory policy from the database. The next authorization query sees the change immediately. The snapshot returned by login or GetAuthorization is computed from the same rebuilt policy.
The audit log
Section titled “The audit log”Every authentication and authorization event is appended to an append-only auth_events table:
| Event | Meaning |
|---|---|
login_succeeded |
A password was accepted and a token minted |
login_failed |
A login was refused, with the reason in detail |
password_claimed |
A bootstrap account was claimed |
user_registered |
Someone self-registered |
role_created, role_removed |
A data role was created or removed |
permission_granted, permission_revoked |
A grant was added to or removed from a role |
user_role_assigned |
A user was moved to a different role |
The log records failures as well as successes — a run of refused logins is the only trace a password-guessing attempt leaves behind. On a failed login the record stores the identifier as it was presented, because that is all that is known about the caller.
Audit records deliberately have no foreign keys: the record must survive the deletion of the user it names. “Who removed this account, and when” is worthless if the record disappears with the account.
Writing an audit record never fails the request. If the insert errors, the server logs it loudly and the operation carries on, so a full disk or a broken connection cannot lock every user out.
Continue
Section titled “Continue”- Authentication and accounts covers registration, login, tokens, and bootstrap.
- Search and Analytics reuse table read grants rather than defining their own permissions.