# Two-user isolation evidence

Captured: 2026-08-02

Scope: disposable local SQLite environment only. User A and User B used synthetic `@audit.invalid` identities. No production data or credentials were used.

## Results

| Resource or operation | UI level | API direct-ID test | Service/query protection | Result |
|---|---|---|---|---|
| Jobs | Browser blocked | A 200; B 404 for A's job; B list 0 | Explicit owner predicates and global `JobApplication` filter | Pass at API/data-query levels |
| Companies | Browser blocked | B 404 for A's company; B list 0 | Explicit owner predicates and global `Company` filter | Pass at API/data-query levels |
| Correspondence | Browser blocked | B list for A's job returned 0; copied ID returned 404 | Filter through owned `JobApplication` navigation | Pass at API/data-query levels |
| Attachments | Browser blocked | A upload/list/download 200; B job access and copied attachment download 404 | Owned-job query before file operation | Pass at API/data-query levels |
| Career Profile | Browser blocked | B's current-profile request 404 | Current-user lookup plus owner query filter | Pass at API/data-query levels |
| CV variants | Browser blocked | Copied A variant ID returned 404, but B's own list returned 500 | Owner predicate/filter exists; list blocked by SQLite translation | Partial: protection inspected and direct ID passed; list broken |
| Application workspace | Browser blocked | B copied A ID returned 404; A request returned 500 | Owner predicate exists; owner path blocked by SQLite translation | Partial |
| Checklist | Browser blocked | B copied A job returned 404; A 200 | Owner predicate on job/items | Pass at API/data-query levels |
| Timeline | Browser blocked | Both users received 500 | Ambiguous route selection occurs before authorization logic | Blocked by endpoint defect; no exposure observed |
| Interview preparation | Browser blocked | Both users received 500 | Ambiguous route selection occurs before authorization logic | Blocked by endpoint defect; no exposure observed |
| AI results | Browser blocked | History endpoint returned 500 | Owner predicates and query filter exist | Blocked by SQLite translation |
| Settings/session | Browser blocked | B `/auth/me` returned only B | Identity/session-bound current user | Pass at API level |
| Administrative operation | Browser blocked | B request returned 403 | `[Authorize(Roles = "Admin")]` | Pass at API level |
| Email threads/provider data | External providers not connected | Local correspondence passed; provider-specific IDs not live-tested | Owner-scoped connection and correspondence queries inspected | Code-inspected/partially tested |

## Protection layers observed

- UI: protected routes require an authenticated shell, but hiding was not counted as authorization.
- API: controllers use explicit local-auth or admin authorization attributes.
- Service: important reads carry `OwnerUserId` or owned-parent predicates.
- EF: global filters deny on null current user and match owner IDs.
- Database: tenant ownership is primarily enforced in application queries; many owner columns are not foreign keys to `AspNetUsers`, so database constraints alone do not provide tenant isolation.

## Limitations

- The in-app browser control client was missing, so UI navigation as A/B was not performed.
- Timeline/interview and several CV/AI paths failed before an ownership result could be observed.
- No Gmail, Microsoft Graph, IMAP, Stripe, cloud AI, or remote object storage was contacted.

No cross-user disclosure was confirmed in the paths that returned a meaningful result.
