AlgoJudge documentation0.2

Permissions

The string form, the two scopes, the resolution rules, the three shipped roles, and all 55 keys.

The Server is what enforces authorization, 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 belowWhere the key is meaningful
globalin a system grant only
activityin an activity grant only
bothin either — in a system grant it means "in every activity"

Resolution

In this order. The order is the decision.

  1. An activity grant carrying OverrideSystem is authoritative inside its activity, and nothing else applies there — not even system:administrator. Checked first, because checking the administrator bypass before it is exactly how the override would be lost.
  2. 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.
  3. Otherwise the effective set is the union: every system contribution, plus the activity grant when one activity is asked about.
  4. What one grant carries is itself a union — every role it links, and any entries of its own beside them. Either half may be empty, and a grant may link as many roles as it is given.
  5. There is no subtraction anywhere in the model.
  6. Nobody may grant, or map onto, a permission they do not themselves hold. Enforced where a grant is written — and where a role is written, since a grant links one — never where it is read. It is applied to what a caller adds: taking a permission away is never refused.

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.

A panel list asked without an activity narrows instead of refusing

An activity named in a query is a question at that scope, and the key is required there — no key in that activity, no list. No activity is a different question, everything I may read, and the answer to that one is a narrowing.

So a manager whose only grant is on one course opens the panel's unfiltered submissions list and is shown that course. Asking at system scope instead refuses them, because a grant on an activity contributes nothing to a question asked with no activity in it — and that is every manager an activity's grant editor makes.

Holding the key nowhere is still a refusal. An empty page would tell somebody who may not look that there is nothing to see, which is a different sentence and the wrong one.

Four of the panel's lists resolve their scope this way — submissions (submission:read:all), questions (question:read:all), grants (grant:read:all) and printouts (printout:manage). The rule lives in one place rather than in each list: a security rule written four times is a security rule that drifts three times.

An activity that withheld the key from itself drops out of the narrowing. An activity grant carrying OverrideSystem is the whole answer inside its activity, so one that does not carry the permission withholds it however widely it is held elsewhere — including from somebody who holds it system-wide.

Some surfaces ask only *anywhere*

The problem library is the installation's rather than one activity's and is still a manager's work, so it answers to problem:read:own held anywhere and then shows what that person owns, what the installation shares, and what is shared with them. problem:create, problem:update, problem:archive, problem:share and the enrollment picker's user:read:all ask the same way. Managing one activity is what admits somebody to them; asked at system scope they refuse every manager whose grant is on an activity.

Listing the installation's accounts and reading somebody's sessions are administration and still ask at system scope. /manager/users is not a manager's screen.

The three shipped roles

A grant links roles — any number of them — and what it confers is their union together with its own entries. Editing a role changes what everybody holding it may do, at once, including people enrolled long before the edit.

Several rather than one, because the two paths that decide what somebody holds both speak in sets: a claim may match several mapping rules, and a launch from a course platform may carry several roles. A union is commutative, so there is no ordering to decide and no precedence to explain.

An edit reaches everybody at once

This is a live link, so a role is worth treating as one. The panel shows how many grants an edit would reach before you save it, an activity's own role reaches only that activity, and nobody may put into a role a permission they do not themselves hold.

A provider owns which roles its contribution links

An identity provider's mapping rule names roles, and the contribution it writes links them like any other grant — so editing one of those roles reaches its holders at once, exactly as it does anywhere else. What the provider owns is which roles are linked: that set is rewritten from the rules at every sign-in, so editing a rule, or moving somebody between groups at the directory, reaches them the next time they sign in.

A role belongs either to the installation or to one activity. An activity's role may only be granted inside it, and is written by whoever holds role:manage:activity there — which is how a manager corrects what their own participants may do without touching what every participant in the installation may do. Writing the installation's roles is role:manage, which is global and an administrator's.

participant — 8 keys

activity:read        submission:read:own    submission:create    result:read:own
question:read:own    question:create        ranking:read         printout:request

This 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 — 37 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
printout:manage
ranking:read:unfrozen      ranking:unfreeze
user:read:all              user:create:temporary
grant:read:all             grant:update
role:read                  role:manage:activity

The other eighteen are deliberately not included: activity:create, activity:delete, trial:run, problem:read:all, problem:delete, problem:import:external, the user: keys that create and edit accounts, the four runner: keys, instance:update, provider:manage, role:manage and system:administrator. A manager runs an activity; they do not run the installation it lives in.

role:read and role:manage:activity are in the set. The second is scoped: held in an activity grant it writes that activity's roles and nothing else. Writing the installation's roles is role:manage, which is global — a role the installation shares is reachable from every activity, so writing one is an installation-wide act.

admin — 1 key

system:administrator

One 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 catalog entry rather than one, because they are two questions: does the participant role 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 role, 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 catalog 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 catalog — 55 keys

Activity

PermissionScopeMeans
activity:readactivitysee the activity and its series at all
activity:createglobalcreate activities
activity:updatebothsettings, series, dates, freeze
activity:archivebothclose an activity: still readable, nothing new accepted
activity:deletebothdestroy an activity and everything in it
activity:enrollbothadd and remove members
trial:runbothask 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.

PermissionScopeMeans
problem:read:ownglobalsee problems they own or that are shared with them
problem:read:allglobalsee every problem in the installation, sharing ignored
problem:createglobaladd a problem to the library
problem:updateglobaledit statement, package, configuration, publish a version
problem:deleteglobaldestroy a problem, refused while it is attached anywhere
problem:shareglobalchange who else can see it
problem:archiveglobalretire a problem without breaking what uses it
problem:attachbothattach a problem to a series and configure the assignment
problem:import:externalglobalcreate 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

PermissionScopeMeans
submission:read:ownactivitysee one's own submissions
submission:read:allbothsee everyone's submissions
submission:createactivitysubmit a solution. Membership is asked separately — see below
submission:source:read:allbothread the source of someone else's submission
submission:rejudgebothqueue another evaluation attempt
submission:cancelbothcancel a queued or running job
submission:excludebothrule that one submission counts toward 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 enrollment.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

PermissionScopeMeans
result:read:ownactivitysee one's own verdicts and per-test detail
result:read:allbothsee everyone's
result:log:read:allbothread 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

PermissionScopeMeans
question:read:ownactivitysee one's own questions and every published one
question:read:allactivitysee every question, published or not
question:createactivityask a question
question:answeractivityanswer a question
question:publishactivitymake an answered question visible to everyone
announcement:createactivitypublish an announcement

Printouts

PermissionScopeMeans
printout:requestactivityask for a page of source on paper
printout:managebothwork the queue: read a request and its source, print it, resolve it

printout:manage is the one key an installation can hand out entirely on its own. A grant carrying it and nothing else opens the printouts area of the panel and refuses every other area, which is what lets somebody standing at a printer be given that job and no other.

It is staff-conferring, and that has a consequence worth knowing before you hand it out: holding it in an activity takes the holder out of that activity's ranking. Reading source other people wrote is what staff means, and a volunteer who is also competing has to be given the key in an activity they do not compete in.

printout:request is in the participant role, so it confers nothing. Requesting is refused anyway unless the activity has the module switched on.

Ranking

PermissionScopeMeans
ranking:readactivitysee the ranking as published
ranking:read:unfrozenbothsee through a freeze
ranking:unfreezebothlift the freeze for everyone

Users

PermissionScopeMeans
user:read:allbothname a person — search accounts, and see the account list at system scope
user:createglobalcreate an account
user:updateglobaledit an account
user:blockglobalstop an account from signing in
user:create:temporarybothcreate temporary accounts in bulk
user:mergeglobalcarry 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 roles

PermissionScopeMeans
grant:read:allbothsee who holds what
grant:updatebothchange what someone holds
role:readbothsee the roles — and so does grant:update, held anywhere
role:manageglobalcreate, edit and delete the installation's roles
role:manage:activitybothcreate, edit and delete one activity's roles

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 same rule governs writing a role, because a grant links one: nobody may put into a role a permission they do not themselves hold.

Two keys, because they are two different powers. A role the installation shares is reachable from every activity, so writing one is an installation-wide act: role:manage is global and an administrator's. A role belonging to one activity reaches that activity and nothing else, so its manager may write it: role:manage:activity, held in the grant on that activity.

It was one key held at either scope until 2026-09-19, and that was an escalation rather than a convenience: a manager whose grant sat at system scope — which is what a directory group hands out — could edit the shipped roles, including emptying admin.

The role list answers to either key, held anywhere. GET /api/v1/roles admits role:read or grant:update from any grant the caller holds — applying a role is exactly what a manager does when they enroll somebody by hand, and grant:update is the manager grant written before roles existed. Reading a role discloses nothing, and the excess rule still refuses to hand on a permission the caller does not hold.

Runners and the installation

PermissionScopeMeans
runner:readglobalsee registered Runners and their state
runner:approveglobalapprove a self-registered key
runner:revokeglobalrevoke a key
runner:updateglobalrename, retag, add a note
instance:updateglobalwhat the installation is called, the mark it shows, the documents it publishes
provider:manageglobalregister identity providers, edit their claim mapping, enable and disable them
system:administratorglobalbypasses 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 role: 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 not refused in an activity grant, because the shipped manager role contains them: a rule that refused every global key would refuse the role this installation hands out.

Nor are they inert there. The problem library asks whether the key is held anywhere rather than at system scope, so managing one activity is what admits somebody to their own library — which is what the manager role promises when it says it runs an activity's problems.

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 role a grant links is defended the same way. Taking system:administrator out of the role that is how the last administrator holds it, or deleting that role, answers role.administrator.last and names the role. The shipped administrator role is fixed outright and answers role.builtIn.fixed: an empty permission set there would take the installation away from everybody linked to it in one save, so it is granted and revoked rather than rewritten.

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 role 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.

On this page