fix(security): scope auth session methods to tenant; pin login-helper reachability #190

Merged
David merged 2 commits from feat/pms-260-cross-tenant-leak-fixes into main 2026-06-13 13:22:11 +02:00
Owner

PMS-260: close known cross-tenant leak points that scoped by user_id only or not at all, with a regression test per item.

auth::get_user_sessions and auth::logout_all bound user_id only. A user_id that carries sessions under more than one tenant could enumerate or wipe sessions outside the caller's tenant. Both now bind tenant_id as well (the column already exists on user_sessions). The session-list route passes user.tenant_id; the only logout_all caller (reset_password) already has the resolved tenant in scope.

auth::find_user_placement and invitations::newest_pending_for read across tenants by design (resolving where a sub lives / which tenant invited an email is their whole job) and are only safe because the sole caller is the pre-session bunyip login/placement path (middleware::place_bunyip_user), before any tenant context exists. Documented that invariant on both and added a source-guard test (routes_do_not_reach_global_login_helpers) that fails if any */routes.rs ever references them from a request handler.

tenants::list_tenants runs no SQL tenant filter (listing every tenant is its job); the route's SuperAdmin guard is the only gate. Added a test pinning that a non-super-admin gets 403.

reports::dashboard aggregates were already tenant-scoped; added dashboard_is_tenant_scoped to pin that the COUNT(*) aggregates do not include a second tenant's tickets.

Tests: just check is green. New tests pass: get_user_sessions_is_tenant_scoped, logout_all_is_tenant_scoped, routes_do_not_reach_global_login_helpers (tests/auth.rs); list_tenants_rejects_non_super_admin (tests/tenants.rs); dashboard_is_tenant_scoped (tests/reports.rs). Two pre-existing failures in tests/settings.rs (enabled_module_response_unchanged, disabled_module_returns_404_on_route_access) are unrelated to this change and reproduce on pristine main (2f89fc7).

#PMS-260

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

PMS-260: close known cross-tenant leak points that scoped by user_id only or not at all, with a regression test per item. auth::get_user_sessions and auth::logout_all bound `user_id` only. A `user_id` that carries sessions under more than one tenant could enumerate or wipe sessions outside the caller's tenant. Both now bind `tenant_id` as well (the column already exists on user_sessions). The session-list route passes `user.tenant_id`; the only logout_all caller (reset_password) already has the resolved tenant in scope. auth::find_user_placement and invitations::newest_pending_for read across tenants by design (resolving where a sub lives / which tenant invited an email is their whole job) and are only safe because the sole caller is the pre-session bunyip login/placement path (middleware::place_bunyip_user), before any tenant context exists. Documented that invariant on both and added a source-guard test (routes_do_not_reach_global_login_helpers) that fails if any */routes.rs ever references them from a request handler. tenants::list_tenants runs no SQL tenant filter (listing every tenant is its job); the route's SuperAdmin guard is the only gate. Added a test pinning that a non-super-admin gets 403. reports::dashboard aggregates were already tenant-scoped; added dashboard_is_tenant_scoped to pin that the COUNT(*) aggregates do not include a second tenant's tickets. Tests: just check is green. New tests pass: get_user_sessions_is_tenant_scoped, logout_all_is_tenant_scoped, routes_do_not_reach_global_login_helpers (tests/auth.rs); list_tenants_rejects_non_super_admin (tests/tenants.rs); dashboard_is_tenant_scoped (tests/reports.rs). Two pre-existing failures in tests/settings.rs (enabled_module_response_unchanged, disabled_module_returns_404_on_route_access) are unrelated to this change and reproduce on pristine main (2f89fc7). #PMS-260 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(security): scope auth session methods to tenant; pin login-helper reachability
All checks were successful
E2E (staging) / Playwright against staging (pull_request) Successful in 34s
Check / fmt + clippy + compile + tests (pull_request) Successful in 1m23s
Build OCI container / Build and push mokosh-api image (push) Successful in 3m34s
cbcfad61e9
PMS-260: close known cross-tenant leak points that scoped by user_id only or not at all, with a regression test per item.

auth::get_user_sessions and auth::logout_all bound `user_id` only. A `user_id` that carries sessions under more than one tenant could enumerate or wipe sessions outside the caller's tenant. Both now bind `tenant_id` as well (the column already exists on user_sessions). The session-list route passes `user.tenant_id`; the only logout_all caller (reset_password) already has the resolved tenant in scope.

auth::find_user_placement and invitations::newest_pending_for read across tenants by design (resolving where a sub lives / which tenant invited an email is their whole job) and are only safe because the sole caller is the pre-session bunyip login/placement path (middleware::place_bunyip_user), before any tenant context exists. Documented that invariant on both and added a source-guard test (routes_do_not_reach_global_login_helpers) that fails if any */routes.rs ever references them from a request handler.

tenants::list_tenants runs no SQL tenant filter (listing every tenant is its job); the route's SuperAdmin guard is the only gate. Added a test pinning that a non-super-admin gets 403.

reports::dashboard aggregates were already tenant-scoped; added dashboard_is_tenant_scoped to pin that the COUNT(*) aggregates do not include a second tenant's tickets.

Tests: just check is green. New tests pass: get_user_sessions_is_tenant_scoped, logout_all_is_tenant_scoped, routes_do_not_reach_global_login_helpers (tests/auth.rs); list_tenants_rejects_non_super_admin (tests/tenants.rs); dashboard_is_tenant_scoped (tests/reports.rs). Two pre-existing failures in tests/settings.rs (enabled_module_response_unchanged, disabled_module_returns_404_on_route_access) are unrelated to this change and reproduce on pristine main (2f89fc7).

#PMS-260

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge branch 'main' into feat/pms-260-cross-tenant-leak-fixes
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
E2E (staging) / Playwright against staging (pull_request) Successful in 46s
Check / fmt + clippy + compile + tests (pull_request) Successful in 2m40s
Build OCI container / Build and push mokosh-api image (push) Successful in 4m11s
3c987d4ec6
David merged commit 2ef2fba271 into main 2026-06-13 13:22:11 +02:00
David deleted branch feat/pms-260-cross-tenant-leak-fixes 2026-06-13 13:22:11 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
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
psa-systems/mokosh-server!190
No description provided.