Review cross-User Money Job cancellation response distinction #1443

Closed
opened 2026-10-08 04:55:22 +00:00 by kayg · 4 comments
Owner

Observed during Forgejo #1402's XUser matrix against the shared release server at d395b6e25.

Route: DELETE /api/v1/money/imports/jobs/{job_id} (money_import_job_cancel). The fixture created a real A-owned detached Import Job. As User B, deleting A's Job returned HTTP 404; deleting a same-shape absent UUID returned HTTP 204. The matrix recorded: exists/missing response profiles differ: (404, 69, '69', 'application/json') vs (204, 0, None, '').

The route contract documents 204 for a missing/already-stopped Job and 404 for a Job owned by another User. No cross-User cancellation succeeded, but the response reveals whether the UUID belongs to another User. Please decide whether this idempotent cancellation behavior is acceptable under the XUser isolation rule. No product code was changed.

Observed during Forgejo #1402's XUser matrix against the shared release server at `d395b6e25`. Route: `DELETE /api/v1/money/imports/jobs/{job_id}` (`money_import_job_cancel`). The fixture created a real A-owned detached Import Job. As User B, deleting A's Job returned HTTP 404; deleting a same-shape absent UUID returned HTTP 204. The matrix recorded: `exists/missing response profiles differ: (404, 69, '69', 'application/json') vs (204, 0, None, '')`. The route contract documents 204 for a missing/already-stopped Job and 404 for a Job owned by another User. No cross-User cancellation succeeded, but the response reveals whether the UUID belongs to another User. Please decide whether this idempotent cancellation behavior is acceptable under the XUser isolation rule. No product code was changed.
Author
Owner

Starting investigation on branch job/xcancel-1443, based on d395b6e25d. I have read issues #1443 and #1444 and will compare foreign and missing responses across the Money routes before changing code.

Starting investigation on branch job/xcancel-1443, based on d395b6e25da20d8c67b8b9fc1a593b813939c8a7. I have read issues #1443 and #1444 and will compare foreign and missing responses across the Money routes before changing code.
Author
Owner

Finding: cancel_import_job calls the owner-filtered durable cancellation helper, then returns import_job_not_found() when its in-memory UUID belongs to another User; a missing UUID falls through to 204. The durable SQL update is scoped by owner_user_id, so the response branch is the leak. I will make foreign cancellation an unchanged 204 and add exact-response and owner-state regressions.

Finding: `cancel_import_job` calls the owner-filtered durable cancellation helper, then returns `import_job_not_found()` when its in-memory UUID belongs to another User; a missing UUID falls through to 204. The durable SQL update is scoped by `owner_user_id`, so the response branch is the leak. I will make foreign cancellation an unchanged 204 and add exact-response and owner-state regressions.
Author
Owner

Completed: #1443 and #1444

Foreign Money import Job DELETE requests now return the same idempotent 204 response as missing or stopped jobs, and they do not cancel or otherwise change the foreign Job. Foreign and missing preview DELETE requests return byte-identical 404 responses. The same fixed 404 behavior is documented for preview confirmation. The Money route audit found no other foreign-versus-missing response-body differences.

Rust route regression coverage compares status, headers, and raw body bytes for foreign and missing progress, result, Job cancellation, preview confirmation, and preview cancellation. It also verifies that the owner's durable result remains unchanged and publishable. The XUser matrix now creates a real import fixture and checks those identities against a live API.

Files: crates/plugins/money/src/routes.rs, crates/plugins/money/src/tests.rs, crates/plugins/money/src/durable_import_tests.rs, crates/plugins/money/src/review_1140.rs, tests/adversarial/xuser_matrix.py, tests/adversarial/reliability_1202.mjs, contracts/openapi.json, packages/api-client/src/generated.ts, and docs/DESIGN.md.

Decisions: Followed the supplied cross-User rule: foreign Job DELETE is indistinguishable from missing/stopped (204); foreign preview DELETE/confirm is indistinguishable from missing/expired (404). No other Money route needed a response change. The cancellation preflight adds no I/O: it uses the keyed Job lookup and the pending-preview map, which is admission-limited to one pending preview.

Verification after merging origin/dev:

  • cargo fmt --check — passed, no output.
  • cargo clippy -p calternal-plugin-money --all-targets -- -D warnings — passed:
    Finished \dev` profile [unoptimized + debuginfo] target(s) in 1.13s`
  • cargo test -p calternal-plugin-money — passed:
    test result: ok. 125 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 121.18s
    test result: ok. 29 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 23.70s
    test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
  • The system python3 -m pytest tests/adversarial/test_xuser_classification.py could not start because pytest is not installed in that interpreter (/home/kayg/.nix-profile/bin/python3: No module named pytest). The same gate passed in an ephemeral uv environment with uv run --with pytest python -m pytest tests/adversarial/test_xuser_classification.py: 20 passed in 1.29s.
  • cargo clean — Removed 12444 files, 8.7GiB total.

Known gap / merge round: The full live XUser matrix was not run in this focused job. Run ADVERSARIAL_MATRIX_DIRECT=1 python3 xuser_matrix.py from tests/adversarial/ against the disposable live server; it must prove the new foreign-versus-missing comparisons and owner-state preservation.

Commits: 7f0e43df320916e63fc30e781bdaa6691407d133 (fix), d77accdf04d1e1225d32f69cfefcff143d660ec7 (contract/docs). Final head: d45ceeda6eac95d2944ce907c3b378dce942341e. Worktree is clean.

## Completed: #1443 and #1444 Foreign Money import Job DELETE requests now return the same idempotent 204 response as missing or stopped jobs, and they do not cancel or otherwise change the foreign Job. Foreign and missing preview DELETE requests return byte-identical 404 responses. The same fixed 404 behavior is documented for preview confirmation. The Money route audit found no other foreign-versus-missing response-body differences. Rust route regression coverage compares status, headers, and raw body bytes for foreign and missing progress, result, Job cancellation, preview confirmation, and preview cancellation. It also verifies that the owner's durable result remains unchanged and publishable. The XUser matrix now creates a real import fixture and checks those identities against a live API. Files: `crates/plugins/money/src/routes.rs`, `crates/plugins/money/src/tests.rs`, `crates/plugins/money/src/durable_import_tests.rs`, `crates/plugins/money/src/review_1140.rs`, `tests/adversarial/xuser_matrix.py`, `tests/adversarial/reliability_1202.mjs`, `contracts/openapi.json`, `packages/api-client/src/generated.ts`, and `docs/DESIGN.md`. Decisions: Followed the supplied cross-User rule: foreign Job DELETE is indistinguishable from missing/stopped (204); foreign preview DELETE/confirm is indistinguishable from missing/expired (404). No other Money route needed a response change. The cancellation preflight adds no I/O: it uses the keyed Job lookup and the pending-preview map, which is admission-limited to one pending preview. Verification after merging `origin/dev`: - `cargo fmt --check` — passed, no output. - `cargo clippy -p calternal-plugin-money --all-targets -- -D warnings` — passed: ` Finished \`dev\` profile [unoptimized + debuginfo] target(s) in 1.13s` - `cargo test -p calternal-plugin-money` — passed: `test result: ok. 125 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 121.18s` `test result: ok. 29 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 23.70s` `test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s` - The system `python3 -m pytest tests/adversarial/test_xuser_classification.py` could not start because pytest is not installed in that interpreter (`/home/kayg/.nix-profile/bin/python3: No module named pytest`). The same gate passed in an ephemeral uv environment with `uv run --with pytest python -m pytest tests/adversarial/test_xuser_classification.py`: `20 passed in 1.29s`. - `cargo clean` — `Removed 12444 files, 8.7GiB total`. Known gap / merge round: The full live XUser matrix was not run in this focused job. Run `ADVERSARIAL_MATRIX_DIRECT=1 python3 xuser_matrix.py` from `tests/adversarial/` against the disposable live server; it must prove the new foreign-versus-missing comparisons and owner-state preservation. Commits: `7f0e43df320916e63fc30e781bdaa6691407d133` (fix), `d77accdf04d1e1225d32f69cfefcff143d660ec7` (contract/docs). Final head: `d45ceeda6eac95d2944ce907c3b378dce942341e`. Worktree is clean.
kayg referenced this issue from a commit 2026-10-08 19:43:32 +00:00
Author
Owner

Deployed to calternal.cloud in merge round 35b (build 9dc65437e, live 2026-10-09 ~11:07 IST). Closing.

Deployed to calternal.cloud in merge round 35b (build 9dc65437e, live 2026-10-09 ~11:07 IST). Closing.
kayg closed this issue 2026-10-09 05:36:34 +00:00
Commenting is not possible because the repository is archived.
No labels
No milestone
No project
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal-v0#1443
No description provided.