Course outline

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.

About 4 min readContent reviewed
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.

python A complete synthetic organization-and-assignment rule
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.

json A fictional authorization verification matrix
{
  "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.

Official references