The as-is submission-authorization matrix, measured by driving every user category through every entry point with a real HTTP integration probe — not read off the code. Contest mode is a later pass.
Success means a Submission row was actually created (web) or HTTP 201 (API) — not
just a 200 page. On top of the fixture data, every contests_problems row was flipped to
enabled=false before both runs; the matrices below were unchanged by it (F4).
/main/submit
Struck-through = Submission#must_have_valid_problem,
which is a silent no-op on this Rails version (F2). The teal controller checks are the only
enforcement that exists. :submit / :edit / :report refer to
User#problems_for_action; admins bypass every gate.
| Actor | Problem | Manage pgGET /problems | New pgdirect_edit | Web submitPOST /main/submit | Edit pgown submission | API submitPOST …/submissions |
|---|---|---|---|---|---|---|
| adminrole: admin | A · available + in group | ✓ | ✓ | ✓ | ✓ | ✓ 201 |
| B · available, gp.enabled=false | ✓ | ✓ | ✓ | ✓ 201 | ||
| C · draft, in group | ✓ | ✓ | ✓ | ✓ 201 | ||
| D · available, no group | ✓ | ✓ | ✓ | ✓ 201 | ||
| editormary · group_a | A · available + in group | ✓ | ✓ | ✓ | ✓ | ✓ 201 |
| B · available, gp.enabled=false | ✓ | ✓ savesF1 | ✓ | ✗ 403F1 | ||
| C · draft, in group | ✓ | ✓ savesF1 | ✓ | ✗ 403F1 | ||
| D · available, no group | ✗ | ✗ | ✗ | ✗ 403 | ||
| reporterreba · group_a | A · available + in group | ✗ | ✓ | ✓ | ✓ | ✓ 201 |
| B · available, gp.enabled=false | ✓ pageF3 | ✗F3 | ✓ pageF3 | ✗ 403 | ||
| C · draft, in group | ✗ | ✗ | ✗ | ✗ 403 | ||
| D · available, no group | ✗ | ✗ | ✗ | ✗ 403 | ||
| memberjohn · group_a | A · available + in group | ✗ | ✓ | ✓ | ✓ | ✓ 201 |
| B · available, gp.enabled=false | ✗ | ✗ | ✗ | ✗ 403 | ||
| C · draft, in group | ✗ | ✗ | ✗ | ✗ 403 | ||
| D · available, no group | ✗ | ✗ | ✗ | ✗ 403 | ||
| no-groupjack | A · available + in group | ✗ | ✗ | ✗ | ✗ | ✗ 403 |
| B · available, gp.enabled=false | ✗ | ✗ | ✗ | ✗ 403 | ||
| C · draft, in group | ✗ | ✗ | ✗ | ✗ 403 | ||
| D · available, no group | ✗ | ✗ | ✗ | ✗ 403 |
Groups, membership roles, and both join-table enabled flags become completely inert.
Problems A, B, D all behave identically as “an available problem.” Only two rules remain:
| Actor | Manage pg | Any available problemall web + API routes | Draft problemavailable=false |
|---|---|---|---|
| admin | ✓ | ✓ | ✓ |
| editorstill an editor of group_a | ✓ | ✓ | ✗ |
| reporter / member / no-group | ✗ | ✓ | ✗ |
The editor keeps the manage page even here — group_editor_authorization checks
group membership rows directly and deliberately ignores use_problem_group. But their draft-submit
privilege disappears: in this mode problems_for_action(:edit) is Problem.none for
non-admins, so the :edit OR in the web submit has nothing to match.
The intended design was confirmed: the running web app is authoritative, and the editor
test-submit (F1) is a feature. All layers now share one predicate, User#can_submit_to_problem?:
the API honors the editor test-submit (F1), the model validation is resurrected on the shared gate with
the binary-skip hole closed (F2), and the reporter's dead-end submit form is now a view-only page (F3).
Two follow-up decisions also landed in rev 1996: a disabled membership row grants no role
(previously a disabled editor kept problem-level powers and a disabled reporter kept sight — an axis this
report did not originally drive), and viva start uses the same gate (viva authorization matches normal
problems). Regression tests: test/models/submission_authorization_lock_test.rb,
test/integration/submission_view_only_test.rb.
main#submit accepts a problem in :submit or :edit, and the
editor’s :edit scope ignores available, group.enabled, and
groups_problems.enabled. So an editor can submit-and-save to a draft (C) or group-disabled (B)
problem from the browser — plausibly intended, “editor tests their own draft.” The API checks only
:submit and returns 403 for the identical request. One of the two is wrong; decide the intent,
then align the other side.
:submit ∪ :edit) · api/v1/submissions_controller.rb:53 (:submit only)Submission#must_have_valid_problem guards non-admin submits with
errors[:base] << "Authorization error…". On Rails ≥ 6.1, errors[:base] returns a
throwaway array — the shovel adds nothing. Verified directly: 0 errors after <<, 1 after
errors.add. The matrix confirms it: the editor’s draft submit saved even though the problem is not
in their :submit scope. Today every route happens to be covered by controller checks, so nothing
exploitable surfaced — but the intended last line of defense is dead, and any future code path that saves a
Submission directly gets no protection. The same method also returns early when
source is nil, so binary submissions were always exempt. Note: fixing it naïvely to
errors.add would break the F1 web path unless the check also accepts :edit.
# app/models/submission.rb:401 — never fires
errors[:base] << "Authorization error: you have no right to submit to this problem" \
if (!self.user.problems_for_action(:submit).include?(self.problem)) and (self.new_record?)
The reporter scope deliberately ignores groups_problems.enabled (“a problem disabled within a
group is a student-only hide; staff still report”). Since page access is gated on
:report ∪ :submit, a reporter can open the New page and their own past submissions for problem B —
the full editor UI with a Submit button — yet the POST always bounces (“not available for you”) and the API
403s. Consistent with the visibility intent, but the UI presents an action that can never succeed. Drafts (C)
are different: the reporter scope requires available=true, so those are fully hidden.
Both matrix runs executed with every contests_problems row flipped to
enabled=false (fixtures place problems A and D inside contests). Every cell was identical to the
earlier run with the flags on. The normal-mode branch of problems_for_action never consults contest
tables; this flag belongs on the contest-mode matrix, in the later pass.
group_submittable_by_user has no role condition: editor, reporter, and plain member all submit
under exactly the same rule (enabled membership × available × group.enabled × gp.enabled). The reporter and
editor roles only add powers on top; they never restrict.
test/models/user_authorization_test.rb — ~50 tests on problems_for_action across standard/group/contest modes, plus all the can_*? predicatestest/models/problem_scope_authorization_test.rb — editor-vs-reporter scope semantics (drafts, archived groups)test/integration/authorization_test.rb — GET-page access: direct_edit in standard-no-group + contest modes, manage-page gating, submission viewingauthorization_spec.rb); token/lockdown sweep; one 201 happy path + 422 (submissions_spec.rb)main_controller_test.rb:32)POST /main/submit at all — the actual mutating endpoint; all existing web tests stop at the GET pages:edit-OR web path (F1) — the web/API asymmetry is invisible to the suitemust_have_valid_problem — zero tests, which is why the dead validation (F2) went unnoticedgroups_problems.enabled=false at the HTTP level (member hidden, reporter page-not-submit):edit to the API check; if not, drop it from main#submit.errors.add(:base, …), matching whichever
semantics decision (1) lands on — and decide whether binary (source-less) submissions should keep
their exemption.POST /main/submit,
the F1 pair (web ✓ / API behavior), and the F3 reporter row. The probe file already exercises all of them.contests_problems.enabled, contest windows, and per-user time offsets enter.