Authentication identifies a caller; authorization decides whether that caller may perform this action on this resource. A valid session does not imply access to every case ID. The server must check the reviewer-to-case relationship on both reads and writes, including direct API requests that bypass navigation. A hidden form field or disabled control is not permission evidence. The example keeps an assignment set per reviewer and rejects a write before it modifies the record map. For a real database, the check and mutation may need a transaction or a conditional write so a permission change cannot race between a read and update.
Authorization: check permission for this record on every request
Case study
Reviewer 29 is assigned to case 47, but not case 83. A POST for case 47 stores the note. A POST for case 83 returns 403 with no mutation, even if the reviewer edits the form field manually. If the reviewer loses the assignment while editing, the server must use the current permission at submit time rather than what the page displayed earlier. An administrator may have a broader role, but role alone still needs a resource policy. Audit logs should record the decision without copying sensitive note text unnecessarily.
Working contract
const assignments = new Map([[29, new Set([47])]]);
const inspectionNotes = new Map();
function writeNote(reviewerId, caseId, note) {
if (!assignments.get(reviewerId)?.has(caseId)) {
return { status: 403, code: "forbidden" };
}
if (typeof note !== "string" || !note.trim()) {
return { status: 422, code: "note_required" };
}
inspectionNotes.set(caseId, note.trim());
return { status: 200, code: "saved" };
}
console.log(writeNote(29, 83, "checked").status);
console.log(writeNote(29, 47, "seal checked").status);
console.log(inspectionNotes.has(83), inspectionNotes.get(47));Observed output
403
200
false seal checkedCost and tradeoffs
The in-memory assignment and record lookups are expected O(1); assignment storage is O(A) for A reviewer-case pairs. A database authorization join or indexed lookup has different costs and should be measured under realistic load. Combining the permission predicate with the write can prevent a time-of-check/time-of-use gap. Rechecking permission on every protected operation adds work, but caching permission without invalidation can leave revoked access active. Choose a policy whose revocation behavior is explicit.
Common Mistakes
- Do not equate a valid session with permission on every record.
- Do not rely on a hidden case ID or disabled button for access control.
- Do not check a read route and forget the corresponding write route.
- Do not assume a stale page reflects current assignments.
Continue through the stack
Sessions and CSRF: keep identity on the server; Routing: validate path parameters and return a stable error shape; Form submission: validate on the server and return field errors.
Broader connections
Relational Records and Database Constraints; Horizontal Scale and Shared State.
Failure trace
Reviewer 29 may view case 47 but not case 83. The UI hides case 83's link, yet a direct request to /cases/83 returns its details because the handler only checked that the reviewer was signed in. Filter by the authenticated user's assignment on every read and write, including nested notes and media. Never trust the case ID sent by the browser as proof of permission.
Verification
- Request an assigned case and an unassigned case with the same signed-in session.
- Change only the path ID in a write request and verify the protected record remains unchanged.
- Load a child attachment through its direct URL and confirm its parent permission is enforced.
Decision note
A fast permission lookup may be cached, but a stale cache can extend access after revocation. Decide the acceptable delay explicitly and test it during reassignment and sign-out.
Further connections
Search Permissions and Private Results.
