feat(auth): add roles and permissions
Replaces one number with a model that can express more than one thing at a time:
roles carrying permission keys, and a chain of voters deciding what those keys
mean for a given request.
users.rights could only say how much authority somebody had, on a single scale.
A forum moderator and a downloads moderator were 3 and 4, and nothing could
describe someone who was both. Every check was a comparison against a magic
number, so what a role could do was spread across three hundred call sites
rather than written down anywhere.
The built-in roles keep the numeric value they stand for, so the old column can
go on answering while the call sites move over. Only the 'user' role is applied
without a row per account, which is what keeps user_roles small on a site with a
hundred thousand of them: the table holds the exceptions, not the rule.
Three voters arrive with it. Roles allow; a ban denies, and a Deny outranks an
Allow, which is how a ban now beats an administrator — the previous code got the
same effect by zeroing the number in memory, an approach that only worked while
everything read that one number. Supervisor level allows everything, so a
mistake in the permission matrix cannot lock a site out of its own admin panel.
Who a visitor is and what they may do are resolved separately, in that order.
Guests go through it too: anonymous visitors have a role, and it is what will
decide whether the forum and the library are open without signing in.
can() finally answers truthfully and is available to templates.
Existing installations need auth:upgrade-schema before this code serves a
request: the roles are read on every one, including a guest's.
roles carrying permission keys, and a chain of voters deciding what those keys
mean for a given request.
users.rights could only say how much authority somebody had, on a single scale.
A forum moderator and a downloads moderator were 3 and 4, and nothing could
describe someone who was both. Every check was a comparison against a magic
number, so what a role could do was spread across three hundred call sites
rather than written down anywhere.
The built-in roles keep the numeric value they stand for, so the old column can
go on answering while the call sites move over. Only the 'user' role is applied
without a row per account, which is what keeps user_roles small on a site with a
hundred thousand of them: the table holds the exceptions, not the rule.
Three voters arrive with it. Roles allow; a ban denies, and a Deny outranks an
Allow, which is how a ban now beats an administrator — the previous code got the
same effect by zeroing the number in memory, an approach that only worked while
everything read that one number. Supervisor level allows everything, so a
mistake in the permission matrix cannot lock a site out of its own admin panel.
Who a visitor is and what they may do are resolved separately, in that order.
Guests go through it too: anonymous visitors have a role, and it is what will
decide whether the forum and the library are open without signing in.
can() finally answers truthfully and is available to templates.
Existing installations need auth:upgrade-schema before this code serves a
request: the roles are read on every one, including a guest's.