Permissions
The string form, the two scopes, the resolution rules, the three shipped templates, and all 52 keys.
Ta strona jest po angielsku
The Server is what enforces authorisation, so the whole vocabulary lives in one
place: AlgoJudge.Server/Authorization/Permissions.cs. The Client fetches it
rather than hard-coding it, so an installation that adds an entry does not need
a Client release to show it.
The form
resource:operation[:qualifier]A permission is a string, not a column. A schema that enumerated these in columns could not express one that did not exist when the migration was written.
own versus all is baked into the operation rather than evaluated as a
separate ownership test. That covers the two cases the product has and is
deliberately not general: "everyone in my group" would need a new operation, not
a different predicate.
The two scopes
A grant holds a set of these strings and is scoped:
system-wide (ActivityId null) or to one activity. The same string means the
same thing at either scope; what changes is how far it reaches.
| Scope column below | Where the key is meaningful |
|---|---|
global | in a system grant only |
activity | in an activity grant only |
both | in either — in a system grant it means "in every activity" |
Resolution
In this order. The order is the decision.
- An activity grant carrying
OverrideSystemis authoritative inside its activity, and nothing else applies there — not evensystem:administrator. Checked first, because checking the administrator bypass before it is exactly how the override would be lost. system:administrator, held in any system contribution, bypasses every other permission check at every other scope. It does not put its holder in an activity, and submitting asks that separately.- Otherwise the effective set is the union: every system contribution, plus the activity grant when one activity is asked about.
- There is no subtraction anywhere in the model.
- Nobody may grant, or map onto, a permission they do not themselves hold. Enforced where a grant is written, not where it is read.
There is no deny list to reach for
"A manager everywhere, except in this contest where I compete" is
OverrideSystem — an activity grant that replaces the system contributions
wholesale — and never a permission taken away. A model with denies has to say
which of two contributions wins where they disagree, and every answer to that is
somebody's surprise.
An Invited grant confers nothing. A grant whose Permissions column does not
parse grants nothing either — the alternatives being to lock every user out of
every screen, or to treat a corrupt row as an administrator.
System scope is several rows
A user holds one system contribution per source: the manual one, plus one per linked identity provider. They union. This is the one place the model is additive.
Two answers a caller must not conflate
The Server's "which activities is this permission held in" query returns null for not restricted and a list for these activities. An empty list is a third answer and means none.
The three shipped templates
Choosing a template copies its permissions into the grant. Nothing points back afterwards, which is the whole reason it is a template rather than a role: a role that grants keep pointing at is an object somebody has to keep correct for the lifetime of the installation.
The cost is stated plainly: a correction has to be applied to existing grants by hand.
One object does hold a live reference
An identity provider's mapping rule names a template, and the contribution is re-derived from it at every sign-in. Editing a template touches nobody who already used it — except the people whose grants a mapping rule rewrites.
participant — 7 keys
activity:read submission:read:own submission:create result:read:own
question:read:own question:create ranking:readThis list is load-bearing beyond the grant editor: it is what the staff test measures against, and therefore what decides who appears in a ranking.
manager — 32 keys
Everything a participant has, plus:
activity:update activity:archive activity:enroll
problem:read:own problem:create problem:update
problem:share problem:archive problem:attach
submission:read:all submission:source:read:all
submission:rejudge submission:cancel submission:exclude
result:read:all result:log:read:all
question:read:all question:answer question:publish
announcement:create
ranking:read:unfrozen ranking:unfreeze
user:create:temporary
grant:read:all grant:updateThe other twenty are deliberately not included: activity:create,
activity:delete, trial:run, problem:read:all, problem:delete,
problem:import:external, every user: key beyond temporary accounts,
template:read, template:manage, the four runner: keys, instance:update,
provider:manage and system:administrator. A manager runs an activity; they
do not run the installation it lives in.
admin — 1 key
system:administratorOne entry, because it bypasses the rest. An administrator with a list of individual permissions is an administrator who can be quietly trimmed.
Staff, and the ranking
A grant is systemic — out of the participant count and out of the ranking — when it carries any permission an ordinary participant does not hold. The Server computes that on every write and never accepts it from the caller.
Two flags are kept per catalogue entry rather than one, because they are two questions: does the participant template grant it and does holding it make somebody staff. They differ for exactly one key.
`trial:run` is held without becoming staff
It is outside the participant template, because a trial spends a Runner and opening it is a manager's decision in one activity. It is not staff-conferring, because spending your own time on your own package is not seeing or changing other people's work.
Inferring one flag from the other would have emptied a whole activity's board the moment somebody was allowed to time their own code.
A key the catalogue does not describe counts as staff. An unknown right is more likely to be a new one somebody has been given than an ordinary participant's, and guessing the other way would quietly put them in the ranking.
The catalogue — 52 keys
Activity
| Permission | Scope | Means |
|---|---|---|
activity:read | activity | see the activity and its series at all |
activity:create | global | create activities |
activity:update | both | settings, series, dates, freeze |
activity:archive | both | close an activity: still readable, nothing new accepted |
activity:delete | both | destroy an activity and everything in it |
activity:enroll | both | add and remove members |
trial:run | both | ask for a package to be run without it being anybody's problem |
Problems
The library is private by default; these say what a manager may do with a problem they can already see. Which problems those are is decided by ownership and the share list.
| Permission | Scope | Means |
|---|---|---|
problem:read:own | global | see problems they own or that are shared with them |
problem:read:all | global | see every problem in the installation, sharing ignored |
problem:create | global | add a problem to the library |
problem:update | global | edit statement, package, configuration, publish a version |
problem:delete | global | destroy a problem, refused while it is attached anywhere |
problem:share | global | change who else can see it |
problem:archive | global | retire a problem without breaking what uses it |
problem:attach | both | attach a problem to a series and configure the assignment |
problem:import:external | global | create a problem whose slug carries a reserved prefix |
The reserved list is configuration, never a literal in the Server — it must not learn the name of any particular archive.
Submissions
| Permission | Scope | Means |
|---|---|---|
submission:read:own | activity | see one's own submissions |
submission:read:all | both | see everyone's submissions |
submission:create | activity | submit a solution. Membership is asked separately — see below |
submission:source:read:all | both | read the source of someone else's submission |
submission:rejudge | both | queue another evaluation attempt |
submission:cancel | both | cancel a queued or running job |
submission:exclude | both | rule that one submission counts towards no standing |
submission:source:read:all is separate from submission:read:all on purpose:
seeing that someone submitted, and reading what they wrote, are different
disclosures. submission:exclude is separate from submission:rejudge because
rejudging asks the Runner to look again while excluding decides an outcome by
hand.
Holding the key is not being in the activity
submission:create is the first of two questions; the second is membership.
Submitting is refused — 403, code enrolment.required — unless the submitter
holds an active grant in that activity. Resolution unions permissions, and
membership is not a permission: nothing carries it in from anywhere else.
Without the second question a submission would be graded and listed among the
manager's rows while belonging to nobody on the standings — the ranking builds
its contestants from activity grants.
system:administrator is not exempt, and that is the decision here worth
arguing with: the bypass is a bypass of permissions, and this is not a
permission question. An administrator who wants to compete takes an activity
grant, which costs them one request. An Invited grant is not membership
either — the resolver discards it, and so does this.
Two things it does not reach. Creating an activity writes its creator an active
grant in it, so nobody is shut out of what they just made. And trial:run is a
different door: a trial is nobody's submission, so nothing about it asks who is
in the activity.
Results
| Permission | Scope | Means |
|---|---|---|
result:read:own | activity | see one's own verdicts and per-test detail |
result:read:all | both | see everyone's |
result:log:read:all | both | read the evaluation log of any submission |
The activity's attachment rules govern participants; result:log:read:all
governs managers, and is what lets a jury read a log the participants cannot.
Questions and announcements
| Permission | Scope | Means |
|---|---|---|
question:read:own | activity | see one's own questions and every published one |
question:read:all | activity | see every question, published or not |
question:create | activity | ask a question |
question:answer | activity | answer a question |
question:publish | activity | make an answered question visible to everyone |
announcement:create | activity | publish an announcement |
Ranking
| Permission | Scope | Means |
|---|---|---|
ranking:read | activity | see the ranking as published |
ranking:read:unfrozen | both | see through a freeze |
ranking:unfreeze | both | lift the freeze for everyone |
Users
| Permission | Scope | Means |
|---|---|---|
user:read:all | global | see the account list |
user:create | global | create an account |
user:update | global | edit an account |
user:block | global | stop an account signing in |
user:create:temporary | both | create temporary accounts in bulk |
user:merge | global | carry one account's work onto another |
user:create:temporary is the one account operation an activity manager needs —
a contest hands out accounts at the door — so it is the one that exists at
activity scope. user:merge is its own key rather than part of user:update:
it hands one person's submissions and points to somebody else, and it should not
arrive with ordinary account editing.
Grants and templates
| Permission | Scope | Means |
|---|---|---|
grant:read:all | both | see who holds what |
grant:update | both | change what someone holds |
template:read | global | see the templates — and so does grant:update, held anywhere |
template:manage | global | create, edit and delete templates |
grant:update is why "nobody may grant a permission they do not hold" exists.
An activity manager needs it to run their own group, and without that rule it is
the same as being able to make oneself an administrator.
The template list answers to either key, held anywhere. template:read is
global, and a global key in an activity grant confers nothing, so an activity
manager could never hold it — while applying a template is exactly what they do
when they enrol somebody by hand. So GET /api/v1/permission-templates admits
template:read or grant:update from any grant the caller holds, and
putting template:read into a manager set of your own is not the fix it looks
like. Reading one discloses nothing: a template is copied into a grant, never
referenced, and the excess rule still refuses to hand on a permission the caller
does not hold.
Runners and the installation
| Permission | Scope | Means |
|---|---|---|
runner:read | global | see registered Runners and their state |
runner:approve | global | approve a self-registered key |
runner:revoke | global | revoke a key |
runner:update | global | rename, retag, add a note |
instance:update | global | what the installation is called, the mark it shows, the documents it publishes |
provider:manage | global | register identity providers, edit their claim mapping, enable and disable them |
system:administrator | global | bypasses every permission check and cannot be trimmed. Refused outright in an activity grant, and the last one cannot be taken away — see below |
provider:manage is the most dangerous key here after system:administrator,
because a mapping rule decides what a token buys. Its two guards are described
in Identity, and both are enforced rather than
documented.
instance:update is not in the manager template: running an activity is not
running the installation it lives in.
system:administrator is refused in an activity grant
It is a bypass only in a system-scoped grant. Writing it into an activity
grant is refused outright rather than stored and ignored — the code is
grant.permission.scope, and the message says the key is installation-wide and
means nothing in an activity.
The refusal is narrow on purpose. Several problem:* keys are declared global
too and are equally inert in an activity grant, and they are not refused,
because the shipped manager template contains them: a rule that refused every
global key would refuse the template this installation hands out.
The installation cannot lose its last administrator
Revoking the only active system grant that carries the key is refused. So is
rewriting it so that it no longer carries the key, and so is demoting it to
invited — the resolver reads active grants only, so an invited administrator
administers nothing. All three answer 403 with the code
grant.administrator.last, and the message says to grant it to somebody else
first.
The question is asked about the row rather than the person: a second administrator anywhere in the installation makes every one of those acts legal again, including on the row that was refused a moment earlier.
The way back is a restart, because aj-admin has no command for grants.
Where no active system grant carries the key at all, the Server grants the
admin template back to the seeded admin account on start and logs a warning.
It never rewrites a system grant somebody made by hand: where that account
already holds one that does not administer, the log says so and the row is left
alone, so an operator who moved the role onto a named person keeps their
arrangement.
A restart returns the rights, not the password. If nobody knows that either,
aj-admin password sets one — see
Troubleshooting.
Archiving versus deletion
Both follow the same shape: archive is the ordinary act, deletion is the forceful one, and they are separate permissions so a manager can have the first without the second.
- An archived activity stays readable and accepts nothing new. It keeps its ranking and its history, because that is what participants come back for.
- An archived problem disappears from the attach picker and takes no new versions, while every assignment already using it keeps working.
- A problem that is attached anywhere cannot be deleted. The database refuses it on its own, whether or not the service layer remembers to check.