История коммитов

.
refactor(library): ask permissions instead of the rights number
Fourteen comparisons against users.rights become one permission: library.moderate, held by
the library moderator, the super moderator and the administrator. 'rights > 4' meant that
and nothing else — creating and editing sections, editing and deleting articles, deleting
their images, the pre-moderation queue, and reading an article that has not been published
yet.

The one check that was not about the library asks the core permission it belongs to: an
article rendered the smilies kept for the staff for anybody with a number above zero, and
now for whoever holds system.smilies.admin.

The provider is registered by hand in the module's services file: this module does not
load its Services directory as a whole, and a provider nobody instantiates declares
nothing.

What is left in the module for later: the mod_lib config, which moves when the config keys
become permissions of the guest and the user roles.

An installation that has already run auth:apply-default-permissions needs it once more
with --force.
.
refactor(album): ask permissions instead of the rights number
Eighteen comparisons against users.rights become two permissions the albums declare, and
the two constants every use case carried its own copy of — MODERATOR_RIGHTS = 6 and
ADMIN_RIGHTS = 7 — are gone with them.

The two numbers stood for one thing. Deleting somebody else's photo took a 6, editing
their album took a 7, opening a private album took a 7 while downloading a photo out of
the same album took a 6 — differences no rule explains and none of them survives as a
distinction: acting on the albums and the photos of another account is album.moderate,
which the super moderator and the administrator hold by default. The bypass argument of
the access guard goes away with them; there is one answer to 'may this visitor look past
the lock', not one per caller.

The comments of the photos are switched by the setting of the downloads — this module
took them from there — and the 'rights >= 7' that read them past a switched-off setting is
album.comments.always_view. The setting stays a setting; only who may pass it is a
permission.

An installation that has already run auth:apply-default-permissions needs it once more
with --force.
.
refactor(downloads): ask permissions instead of the rights number
Twenty-odd comparisons against users.rights become four permissions the downloads declare.
'rights === 4 || rights >= 6' says downloads.moderate, the 'rights <= 6' of moving a file
says downloads.file.move, the two 'rights === 9' of the folder form say
downloads.upload_rules.manage, and the 'rights >= 7' that let somebody past a switched-off
setting says downloads.comments.always_view — which is what it always meant, since the
toggle of the comments is a setting of the module and not a permission of anybody.

Two dead ends close. The Move File item sat in the same menu as Edit and Delete, shown to
the staff of the section, while the screen behind it asked for an administrator: it now
asks the permission the screen asks. And creating a folder took 'users may upload here'
from the request whatever the visitor was allowed — only the check of the formats was
gated — so a folder could be opened up by anybody who could reach the form; both the field
and its formats now belong to the one permission.

What is left in the module for later: the checks bound to the mod_down config, which move
when the config keys become permissions of the guest and the user roles, and the staff
smilies of the comment author, which need the column until stage 6 finds it a replacement.

An installation that has already run auth:apply-default-permissions needs it once more
with --force.
.
refactor(profile): ask permissions instead of the rights number
Fifty-odd comparisons against users.rights become twelve permissions the profile declares.
The one that repeated ten times — 'rights < 7 hides an account awaiting confirmation' — is
users.unconfirmed.view now, and the rest name what they were actually about: editing
somebody else's profile, changing their password, resetting their settings, reading the
notes the staff keep, reading an address its owner hides, and the bans.

'Is my number bigger than theirs' was the other half of every one of those checks, and it
is the level of the roles now, next to the permission. The guards and the buttons leading
to them ask the same question at last: the Edit button appeared for a supervisor and for
an administrator strictly above the target, while the guard behind it let any holder edit
a peer, and the Delete button asked for a number the screen it leads to no longer checks.

The ban gets a shape. Which section a ban may be applied to came from matching the number
of the section against the number in users.rights — 3 was the forum because 3 was 3 — and
that mapping is now one table in BanAccess, from the kind of ban to the permission it
needs. A kind nobody declared is refused rather than allowed; the numbers 12, 14 and 16
belonged to access levels nobody documented, no form has offered them for years, and
users.ban.downloads is not declared because nothing would ever check it. How long a ban
may last follows the same permissions: a day for whoever may only ban one section, a
month for the general ban, longer for whoever may also lift one.

'Position on the Site' leaves the profile editor. It wrote users.rights directly, and
since the roles arrived that column is computed from them — the radio buttons were
writing to a mirror. The block is a link to /admin/users/{id}/roles, which is where roles
are handed out.

What is left in the module for later: the entries of the admin club count as activity by
the number, because who may enter the club is the guestbook's own check and becomes a
permission when that module is converted, and the reads of the profile owner's own rights
— the label under the nickname, the staff smilies of an old post, 'no karma for the
administration' — which need the column until stage 6 finds it a replacement.

An installation that has already run auth:apply-default-permissions needs it once more
with --force.
.
refactor(forum): ask permissions instead of the rights number
Seventy-odd comparisons against users.rights become eight permissions the forum declares.
What used to read as 'rights === 3 || rights >= 6' now says forum.topic.moderate, what read
as 'rights >= 7' says forum.deleted.view, forum.curators.manage or forum.poll.voters.view
depending on which of the three it meant, and the two 'rights === 9' checks say
forum.topic.destroy and forum.post.destroy.

The curator finally has a shape. The old code gave a curator the number of a forum moderator
for the length of a request, which made them a moderator of the whole forum for anything
that looked at the number afterwards. A curator is now allowed forum.topic.moderate for one
subject — the topic they were appointed to — through a voter of the module. That second
argument of can() existed from the start for exactly this.

Two of the defaults sharpen on purpose. Moderating the forum goes to the forum moderator,
the super moderator and the administrator, not to whoever had a number above zero: the
downloads and the library moderators were never forum staff, and roles are the point at
which those stop being the same job. They keep the staff smilies, which is what the number
really bought them there.

'Topics by the period' showed the deleted ones to the supervisor alone while every other
listing showed them from administrator up; it asks the same forum.deleted.view now.

Posting into a closed topic and posting into a deleted one were one condition and are two
now, each asking the permission it is about.

What is left in the module for later: the checks bound to the mod_forum config, which move
when the config keys become permissions of the guest and the user roles, and the reads of
the author's own rights — the staff smilies of an old post, the label under a nickname —
which need the column until stage 6 finds it a replacement.

An installation that has already run auth:apply-default-permissions needs it once more with
--force.
.
fix(auth): collect the voters and permissions declared by modules
The tags of the three extension points came from instanceof rules in the core services file,
and such a rule reaches the services declared in that same file. A module implementing
PermissionProviderInterface or AccessVoterInterface compiled fine and was never asked
anything: its permissions never reached the role editor, so nobody could grant them, and a
voter of its own would never have voted.

They are registered on the container builder now, before anything is loaded, which is what
makes them apply to the modules as well. Two tests hold the line: one that a permission
declared by a module is collected, one that nothing carries a tag twice — the same tag
applied by both mechanisms would have every voter vote twice.

Found while converting the forum, on the two permissions the admin module declared one
commit ago.
.
refactor(admin): gate the panel by permissions, not by the number
Every check of the panel now names what it is about. The gate of the panel asks
admin.access, the gate of what changes the site as a whole asks admin.settings.manage,
destroying a forum section with its content and emptying the hidden lists ask permissions
of their own, and the address of a visitor is shown to whoever holds users.origin.view.

The sign-in screen of the panel asks the same admin.access, about the account that just
proved who it is rather than about the visitor of the request: nobody is signed in at that
point, so the roles of that account are resolved on the spot.

Deleting an account keeps its rule — nobody deletes somebody standing above them — but the
standing is now the level of the roles rather than a comparison of two numbers, the same
rule the role editor already applies.

One nuance is dropped on purpose. The lists of hidden topics and posts used to compare the
visitor's number against the author's, so an author with more of it kept their address
hidden. It now takes users.origin.view like everywhere else: the permission is granted from
super-moderator up, which is a narrower audience than the old rights >= 3, and the check
cost a role lookup per row.

UserRights, the enum of the magic numbers, is gone with the last of its callers.
.
refactor(system): ask permissions instead of the rights number
The first of the call sites the roles were built for. The core no longer compares against
users.rights: the debug details and the developer panel ask system.debug.view, the antiflood
delay asks system.antiflood.relaxed, and the exact page a visitor is on is shown to whoever
holds users.origin.view.

Two of these change who sees what, on purpose. Stack traces used to reach anybody with a
number above zero and the panel only administrators; both now answer the same permission,
granted by default to the administrator alone — a moderator has no business reading a stack
trace. And 'is an administrator around', which the adaptive antiflood mode asks, now means
'holds a role that was granted', the same definition the staff list uses.

An upgraded site needs its built-in roles to carry these keys, or a moderator would quietly
lose what the number used to give them. DefaultPermissions is that matrix, written as the
checks are converted and read by two callers that must not disagree: the seeder creating
the roles of a fresh installation, and auth:apply-default-permissions bringing an existing
one up to the same set. The command only ever adds, and is guarded like the other data
migrations — a site that revoked a default on purpose should not have it handed back by an
absent-minded second run. After an update that declares new permissions, run it with
--force.
.
feat(admin): manage roles and hand them out
Adds the two screens the role model was missing: /admin/roles, where a role carries the
permission keys the code checks, and /admin/users/{id}/roles, where an account is given a
role — with an end date, for moderation that is meant to run out on its own.

The matrix is grouped by module and lists what the installed modules declare. Keys nothing
declares — a module switched off, a pattern such as forum.* — are shown apart and carried
over untouched on save: dropping what the editor could not draw would quietly undo what a
site had granted. Keys the catalogue does not know are refused, so a request made by hand
cannot invent a permission.

The level is what keeps the editor from being a way up. Somebody who may edit roles hands
out what they hold, never what stands above them: a role above their own level is listed
but not opened, a level above their own is refused, and the roles of an account outranking
them are out of reach. Without that, admin.roles.manage — which the seeder grants to the
admin role — would read as 'any administrator may make themselves supervisor'.

A role at supervisor level is allowed everything by SuperAdminVoter, so no matrix is drawn
for it and a save leaves its permissions alone. The screen says as much instead of showing
checkboxes that nothing reads, and the rule itself now lives in one place,
SystemRole::grantsEverything(), which the voter and the panel both ask.

/admin/staff groups by role instead of comparing against rights >= 1, and the sidebar
counts the accounts holding a granted role. Somebody holding two roles appears under both,
which is the point of roles and something one number could not express.

users.rights is recomputed after every change, so the checks still comparing against it go
on working while they are converted module by module.

The new strings arrive translated into Russian. Regenerating the templates also picks up
the strings of the two previous commits, which never ran the scanner: the names of the
built-in roles, the permission labels, and the leftovers of the CSRF and validator work.
.
feat(auth): convert access levels into roles
Gives every member of staff the role their users.rights value stood for, and keeps
that number in step with the roles from now on.

Only accounts above zero get a row: the role everybody has is applied without
one, so a site with a hundred thousand accounts comes out of this with a handful
of rows rather than a hundred thousand.

users.rights does not go away yet. Three hundred checks across the modules and
the templates still compare against it, and they cannot all be rewritten in one
commit without leaving the site broken in between — so the number stays, computed
from the roles instead of being the source of truth. Granting a role updates it,
and every one of those checks goes on working while they are converted one module
at a time.

The undocumented values are the reason this is not a straight lookup. The column
is a tinyint and checks like `rights >= 1` gave 1, 2 and 8 meaning too, so
accounts holding them exist. Each is mapped to the closest role below it, which
never grants more than the account had, and every one of them is listed in the
report for somebody to look at.

The numbers are written to a file before anything else happens. The mirror
recomputes the column from the roles afterwards, so 8 becomes 7 and the original
is otherwise unrecoverable.

Running again leaves accounts that already have roles alone: somebody may have
arranged them by hand after the first run, and a second run must not undo that.
--reset redoes them anyway, for when that is what is wanted.