Calendar: another User's Event answers 403 instead of 404 #1581

Open
opened 2026-10-12 02:01:12 +00:00 by kayg · 3 comments
Owner

A Calendar mutation on an Event that belongs to another User returns 403. Other Plugins answer 404 so a User cannot learn that an item exists (cross-User isolation rule). Make Calendar answer 404 the same way and extend the isolation matrix.

Found by the merge round 41 adversarial run (branch job/merge41 adf8205c9; log artifacts/merge41-gates/adversarial-final.log in worktree /home/kayg/Developer/calternal-wt/merge41, report on #1573). Not a merge blocker under the CLAUDE.md rule. Owner of the fix: confirm whether it also happens on production c0f8da461, fix with a regression test in tests/adversarial.

A Calendar mutation on an Event that belongs to another User returns 403. Other Plugins answer 404 so a User cannot learn that an item exists (cross-User isolation rule). Make Calendar answer 404 the same way and extend the isolation matrix. Found by the merge round 41 adversarial run (branch job/merge41 adf8205c9; log artifacts/merge41-gates/adversarial-final.log in worktree /home/kayg/Developer/calternal-wt/merge41, report on #1573). Not a merge blocker under the CLAUDE.md rule. Owner of the fix: confirm whether it also happens on production c0f8da461, fix with a regression test in tests/adversarial.
Author
Owner

Starting #1581 on branch job/cal404-1581, based at 8e84096297. I read CLAUDE.md, CONTEXT.md and the Calendar and sharing sections of docs/DESIGN.md. I am tracing all requested Calendar routes and the existing shared cross-User item helper before making the smallest fix.

Starting #1581 on branch job/cal404-1581, based at 8e840962979acd2bdf58141888618ab5560f728e. I read CLAUDE.md, CONTEXT.md and the Calendar and sharing sections of docs/DESIGN.md. I am tracing all requested Calendar routes and the existing shared cross-User item helper before making the smallest fix.
Author
Owner

Finding: view::principal_user uses the shared PluginRequestContext::data_user() check, but maps a foreign resource_scope to 403. The Event and subscription routers also resolve the User separately, so they do not apply the same boundary. The #331 matrix already seeds an A-owned Event and subscription; I will extend its live assertions to cover Calendar list projections and Task duplication as well as the existing ID mutation comparisons.

Finding: `view::principal_user` uses the shared `PluginRequestContext::data_user()` check, but maps a foreign `resource_scope` to 403. The Event and subscription routers also resolve the User separately, so they do not apply the same boundary. The #331 matrix already seeds an A-owned Event and subscription; I will extend its live assertions to cover Calendar list projections and Task duplication as well as the existing ID mutation comparisons.
Author
Owner

Implemented #1581 on job/cal404-1581.

Commit: f89383febc8d7fa6237ca9d86c6d4fba264fe038

Changes

  • Extended the existing Calendar view::principal_user boundary. An authenticated foreign resource_scope.user_id now gets the generic Calendar 404 before data-scope denial. Calendar Events, Items, range/year/tags, subscriptions, and account/feed routes use this boundary. No new authorization helper was added; the shared PluginRequestContext::data_user remains the data authority check.
  • Added route and subscription regression tests, updated OpenAPI 404 responses and the generated API client, and extended tests/adversarial/xuser_matrix.py with Event, Task, subscription, Items, and range comparisons.
  • Added the local scope hot-path profile at bench/calendar-scope-1581.sh and recorded it in docs/perf/baseline.json.

Files

crates/plugins/calendar/src/{routes.rs,view.rs,items.rs,feeds/publication.rs,feeds/subscriptions.rs}; contracts/openapi.json; packages/api-client/src/generated.ts; tests/adversarial/xuser_matrix.py; scripts/perf_guards/{rules.py,test_rules.py}; contracts/perf/{registry.json,exceptions.json,adoption-1058.json,ratchet.json}; bench/calendar-scope-1581.sh; docs/perf/baseline.json.

Gates (post-sync)

git fetch origin && git merge origin/dev returned:

Already up to date.

cargo fmt --check exited 0 with no output.

$ cargo clippy -p calternal-plugin-calendar --all-targets -- -D warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.91s
$ cargo test -p calternal-plugin-calendar
test result: ok. 113 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 12.93s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.28s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.17s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
$ cargo clippy -p calternal-server --all-targets -- -D warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 18.13s
$ cargo test -p calternal-server
test result: ok. 280 passed; 0 failed; 12 ignored; 0 measured; 0 filtered out; finished in 134.05s
test deterministic_performance_guards ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 33.43s
test sqlite_index_sidecars_and_snapshots_use_mode_0600 ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.14s
$ cd apps/web && bun run check
perf-lint: PASS; 0 violations; 20343 scoped exceptions
svelte-check found 0 errors and 0 warnings
$ cd apps/web && bun run test -- --maxWorkers=2
Test Files  321 passed (321)
     Tests  2339 passed (2339)

cd packages/api-client && bun test src/index.test.ts: 24 pass, 0 fail. python3 -m py_compile tests/adversarial/xuser_matrix.py: exit 0. cargo clean removed 19550 files, 17.0GiB total.

Performance

Expected work is O(S) over session scopes plus one constant-time resource-scope User ID lookup per request; this check performs no database or filesystem IO. On the local Linux host (load average before/after [10.19, 6.6, 5.97]), the boundary-only profile measured:

  • One scope, 3,000 checks: p50 0.000245 ms, p95 0.000322 ms.
  • Two scopes with data last, 10,000-check burst: p50 0.000254 ms, p95 0.000329 ms.
  • Process CPU: user 0.015819 s, system 0.007904 s; peak RSS 28,360,704 bytes.

These measurements isolate the principal boundary; existing end-to-end Calendar route baselines are context, not a direct comparison.

UX gaps closed / left

API-only change; no UI controls or screens changed. Not applicable.

Known gap / merge round

The live XUser/adversarial matrix was not run in this feature job under the verification policy. The merge round should run:

AUTHZ_MATRIX_ONLY=1 AUTHZ_MATRIX_WITH_XUSER=1 tests/adversarial/run.sh

This must prove the added Calendar owner-vs-missing comparisons against the real local server for Event detail/update, Task lookup/duplicate, subscriptions, Items, and range.

Decisions for owner confirmation

  • For an authenticated principal, a foreign resource scope takes precedence over missing data scope and returns 404. Missing identity and ordinary missing-data-scope cases retain 401/403.
  • Subscription routes now use the shared Calendar data boundary, so a signed-in context without data scope receives 403. This aligns them with the Calendar plugin's declared data authority.
Implemented #1581 on `job/cal404-1581`. **Commit:** `f89383febc8d7fa6237ca9d86c6d4fba264fe038` ### Changes - Extended the existing Calendar `view::principal_user` boundary. An authenticated foreign `resource_scope.user_id` now gets the generic Calendar 404 before data-scope denial. Calendar Events, Items, range/year/tags, subscriptions, and account/feed routes use this boundary. No new authorization helper was added; the shared `PluginRequestContext::data_user` remains the data authority check. - Added route and subscription regression tests, updated OpenAPI 404 responses and the generated API client, and extended `tests/adversarial/xuser_matrix.py` with Event, Task, subscription, Items, and range comparisons. - Added the local scope hot-path profile at `bench/calendar-scope-1581.sh` and recorded it in `docs/perf/baseline.json`. ### Files `crates/plugins/calendar/src/{routes.rs,view.rs,items.rs,feeds/publication.rs,feeds/subscriptions.rs}`; `contracts/openapi.json`; `packages/api-client/src/generated.ts`; `tests/adversarial/xuser_matrix.py`; `scripts/perf_guards/{rules.py,test_rules.py}`; `contracts/perf/{registry.json,exceptions.json,adoption-1058.json,ratchet.json}`; `bench/calendar-scope-1581.sh`; `docs/perf/baseline.json`. ### Gates (post-sync) `git fetch origin && git merge origin/dev` returned: ```text Already up to date. ``` `cargo fmt --check` exited 0 with no output. ```text $ cargo clippy -p calternal-plugin-calendar --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.91s $ cargo test -p calternal-plugin-calendar test result: ok. 113 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 12.93s test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.28s test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.17s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s $ cargo clippy -p calternal-server --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 18.13s $ cargo test -p calternal-server test result: ok. 280 passed; 0 failed; 12 ignored; 0 measured; 0 filtered out; finished in 134.05s test deterministic_performance_guards ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 33.43s test sqlite_index_sidecars_and_snapshots_use_mode_0600 ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.14s ``` ```text $ cd apps/web && bun run check perf-lint: PASS; 0 violations; 20343 scoped exceptions svelte-check found 0 errors and 0 warnings $ cd apps/web && bun run test -- --maxWorkers=2 Test Files 321 passed (321) Tests 2339 passed (2339) ``` `cd packages/api-client && bun test src/index.test.ts`: `24 pass`, `0 fail`. `python3 -m py_compile tests/adversarial/xuser_matrix.py`: exit 0. `cargo clean` removed `19550 files, 17.0GiB total`. ### Performance Expected work is O(S) over session scopes plus one constant-time resource-scope User ID lookup per request; this check performs no database or filesystem IO. On the local Linux host (load average before/after `[10.19, 6.6, 5.97]`), the boundary-only profile measured: - One scope, 3,000 checks: p50 0.000245 ms, p95 0.000322 ms. - Two scopes with `data` last, 10,000-check burst: p50 0.000254 ms, p95 0.000329 ms. - Process CPU: user 0.015819 s, system 0.007904 s; peak RSS 28,360,704 bytes. These measurements isolate the principal boundary; existing end-to-end Calendar route baselines are context, not a direct comparison. ### UX gaps closed / left API-only change; no UI controls or screens changed. Not applicable. ### Known gap / merge round The live XUser/adversarial matrix was not run in this feature job under the verification policy. The merge round should run: ```text AUTHZ_MATRIX_ONLY=1 AUTHZ_MATRIX_WITH_XUSER=1 tests/adversarial/run.sh ``` This must prove the added Calendar owner-vs-missing comparisons against the real local server for Event detail/update, Task lookup/duplicate, subscriptions, Items, and range. ### Decisions for owner confirmation - For an authenticated principal, a foreign resource scope takes precedence over missing data scope and returns 404. Missing identity and ordinary missing-data-scope cases retain 401/403. - Subscription routes now use the shared Calendar data boundary, so a signed-in context without `data` scope receives 403. This aligns them with the Calendar plugin's declared data authority.
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#1581
No description provided.