Web Security / Basic: web trust and identity contracts / Lesson 5
Authorization and tenant-safe resource scope
Enforce permission using trusted subject and resource context before reading or mutating protected state.
On this page
Lesson goals
- Define subject, tenant, resource, and action checks.
- Verify denial and unchanged state as well as legitimate access.
Write the permission rule before the route
Authorization is a decision about a specific action on a specific resource by a validated subject. Role alone is often insufficient: organization scope, assignment, ownership, resource state, and delegated authority can matter. For the fictional portal, an editor may update an assigned note only within the current validated organization. The subject context must come from the trusted authentication boundary, not an owner or role submitted in the request. Default to denial when required context is missing. Treat read, export, update, and administrative actions separately instead of assuming one successful check authorizes the entire workflow.
Check scope and assignment in a pure local model
The function demonstrates a narrow rule over fictional in-memory data. It checks validated context, organization equality, role, and assignment before allowing the action. Tests include a legitimate editor and an editor from another organization. No route, database, or identity provider is accessed. A real application must invoke equivalent policy before protected reads or mutations and must obtain both subject and resource data from trusted sources. A passing unit function cannot prove that a forgotten export endpoint uses it. Keep the policy name specific so developers do not reuse an edit rule for unrelated administrative actions.
def may_edit(subject, note):
if not subject or not note or subject.get("validated") is not True:
return False
organization = subject.get("organization")
subject_id = subject.get("id")
if not isinstance(organization, str) or not organization or not isinstance(subject_id, str) or not subject_id:
return False
return (organization == note.get("organization")
and subject.get("role") == "editor"
and subject_id in note.get("assigned", []))
note = {"organization": "lab-org-a", "assigned": ["lab-user-a"]}
owner = {"validated": True, "organization": "lab-org-a", "role": "editor", "id": "lab-user-a"}
other = {**owner, "organization": "lab-org-b"}
assert may_edit(owner, note)
assert not may_edit(other, note)
assert not may_edit(None, note)
assert not may_edit({**owner, "organization": None}, {**note, "organization": None})
The assertions exercise one allow and three deny paths using invented facts. validated is assumed to have been established elsewhere; the function is not authentication middleware. No protected data or configured service is accessed.
Enforce the rule at every resource path
A hidden button or unguessable identifier does not secure a record. Direct API calls, background jobs, downloads, search results, and exports must preserve the same applicable scope. Load or query the resource through an authorized scope rather than retrieving everything and hoping the interface filters it. Check permission before side effects, and avoid a separate check/update sequence that becomes invalid under concurrent state changes. The data store and transaction design should preserve relevant invariants. Every error path should avoid leaking private existence or content beyond the intended contract. Review caches because an authorized response can become unauthorized when reused across subjects.
Keep an explicit permission population
The matrix defines expected outcomes without sending requests. Include missing identity, wrong organization, missing assignment, and legitimate behavior. Test relevant role and resource-state transitions, not only the first page load. A denied mutation must leave state unchanged, including downstream jobs and notifications. Avoid broad privileged test accounts that hide missing checks. Real integration verification needs authorized isolated fixtures and a policy owner. This record does not create users or change access. It shows how a reviewer can identify which cases are required and whether evidence exists for each one.
{
"operation": "edit-assigned-note",
"requiredCases": ["assigned-same-organization", "unassigned-same-organization", "different-organization", "missing-subject"],
"denyInvariant": "no-record-job-or-notification-change",
"ownerRole": "access-policy-maintainer",
"environment": "synthetic-plan-only"
}
This application-defined record is not an executable policy or a standard control identifier. Each named case needs evidence from the relevant implementation boundary, including unchanged state for denial.
Reassess permission after identity and resource changes
Assignments, organization membership, roles, sharing settings, and account status can change while sessions remain active. Define how fresh those decisions must be and invalidate cached authority under policy. Service accounts and administrative maintenance need scoped permissions and an accountable owner too. Review policy changes as product behavior, with negative regression cases and a rollback or correction plan. The principle of least privilege covers action, resource, duration, and conditions. It does not mean indiscriminately blocking legitimate users. A useful design explains both why an operation is allowed and how an unsupported case is safely refused.
Common errors and fixes
A client-provided organization id becomes authority
- Why it happens
- Untrusted request data replaces validated subject scope.
- How to fix it
- Derive subject context from the trusted identity boundary.
- Prevent it next time
- Test conflicting client claims with synthetic fixtures.
The denied request still enqueues a job
- Why it happens
- Authorization is evaluated after a side effect.
- How to fix it
- Move enforcement before mutation and preserve atomic invariants.
- Prevent it next time
- Assert downstream state in negative tests.
A cache serves one userβs private result to another
- Why it happens
- Cache scope omits the relevant authorization context.
- How to fix it
- Review cache keys and response reuse policy.
- Prevent it next time
- Include cross-subject cache cases in verification.
Check your understanding
1. Why test both wrong organization and missing assignment?
Show worked solution
They are separate policy conditions. Passing one negative case does not establish the other is enforced. Keep expected outcomes and unchanged-state checks explicit, then verify the actual application paths rather than only the local rule.
2. An editor loses an assignment while a session is active. What must the policy address?
Show worked solution
The freshness and invalidation of operation permission. Authentication can remain valid while edit authority changes. Recheck the resource/action scope under the approved model and test cached decisions after membership or assignment changes.
Key takeaways
- Permission belongs to subject, resource, action, and current scope.
- Denial must precede protected side effects.
- Every path and cache needs the applicable boundary.