Course outline

FastAPI / Intermediate: application integration and security / Lesson 11

Resource authorization and tenant scope

Apply a trusted actor to the actual resource and operation, with deny-by-default behavior and tests that exercise the real policy.

About 5 min readContent reviewed
On this page

Lesson goals

  • Explain the assumptions and boundaries of resource authorization and tenant scope.
  • Interpret both complete examples and justify their observed outputs.
  • Diagnose the listed failures and apply the worked solutions while keeping identity, state, and lifecycle ownership explicit.

Identity is the beginning of the decision

Authentication establishes who the actor is; authorization decides whether that actor may perform this operation on this resource now. A valid login is not permission to read every object. Define policy inputs such as tenant, ownership, role, operation, and resource state explicitly. The first pure policy example allows an owner or a separately trusted administrator while denying anonymous and unrelated actors. It uses synthetic server-side objects, not a clientโ€™s claimed role. Never copy owner_id or is_admin from a request body into trusted context just because Pydantic accepted its type.

python Complete pure ownership-policy check
def may_edit(actor, document):
    return bool(actor and (actor["role"] == "admin" or actor["id"] == document["owner_id"]))
document = {"owner_id": 7}
print(may_edit(None, document))
print(may_edit({"id": 7, "role": "user"}, document))
print(may_edit({"id": 8, "role": "user"}, document))
print(may_edit({"id": 8, "role": "admin"}, document))
Expected output
False
True
False
True

The result matrix is deny, allow, deny, allow. The role and actor are trusted synthetic inputs; reading them from an unverified request would invalidate the policy boundary.

Load and authorize the actual target

A route identifier must be parsed, resolved under the intended tenant or scope, and checked against the operation policy. The second example composes resource lookup and ownership as a dependency, then keeps that real policy active while the test changes the trusted synthetic actor. An absent object and an existing forbidden object follow the declared 404 and 403 contract. Some applications intentionally hide existence; choose that policy consistently instead of leaking it accidentally through different error timing or output. Do not authorize one object and later fetch a different unchecked object through another repository path.

python Complete HTTP resource-policy test
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
from fastapi.testclient import TestClient
def current_user(): return {"id": 7}
def authorized_document(document_id: int, user: Annotated[dict, Depends(current_user)]):
    document = {1: {"id": 1, "owner_id": 7, "text": "public lab text"}}.get(document_id)
    if document is None: raise HTTPException(status_code=404, detail="Missing")
    if document["owner_id"] != user["id"]: raise HTTPException(status_code=403, detail="Forbidden")
    return document
app = FastAPI()
@app.get("/documents/{document_id}")
def document(value: Annotated[dict, Depends(authorized_document)]): return {"id": value["id"]}
with TestClient(app) as client:
    print(client.get("/documents/1").status_code)
    app.dependency_overrides[current_user] = lambda: {"id": 8}
    try:
        print(client.get("/documents/1").status_code)
    finally:
        app.dependency_overrides.clear()
Expected output
200
403

The synthetic owner succeeds and another synthetic user is forbidden while the actual lookup and policy remain active. current_user is a lab provider, not a real authenticator.

Keep authorization close to state change

Ownership or membership can change between an early check and a later write. When that matters, bind the authorization condition to the transactional repository operation rather than trusting stale loaded state. A permissions check is not a database transaction and dependency injection does not make an asynchronous sequence atomic. For multi-tenant data, query and update within the approved tenant scope selected by the server. Caches must include the relevant authorization context or be limited to data that is truly shared. A response model can hide fields, but it cannot replace a missing permission check on the object itself.

Review every exposure path

Read, update, delete, list, export, attachment, and background operations can each expose a resource. A protected detail route does not secure an unfiltered list or download route. OAuth2 scopes or roles describe authority only under the verified identity and application policy; they are not a substitute for ownership or tenant evaluation. Keep privileged actions auditable through safe actor, operation, and resource references without logging credentials or sensitive payloads. Review router-level and endpoint-level dependencies together, including routes introduced during refactoring. A policy omitted from one operation can defeat the protection of all the others.

Test an authorization matrix

Include anonymous, owner, unrelated user, wrong tenant, privileged actor, and relevant resource-state cases. Override identity or repository inputs only where needed and retain the policy under test. Verify public status and absence of unauthorized data, not just that a helper returned false. All examples are isolated and use public synthetic data, with no real token, user account, database, or network server. A good policy is explainable in business terms and has explicit trusted inputs. Its correctness must be rechecked as roles, resource states, and persistence behavior evolve rather than assumed from a route decorator forever.

Common errors and fixes

Every authenticated user can access every object

Why it happens
Login checks replaced resource authorization.
How to fix it
Evaluate actor, target, tenant, and operation explicitly.
Prevent it next time
Test unrelated-user and cross-tenant requests.

A client chooses its own trusted owner

Why it happens
A body or header claim became server identity.
How to fix it
Derive authority from a verified principal and stored target.
Prevent it next time
Keep input models separate from identity context.

An early authorization check becomes stale

Why it happens
State changed before the write completed.
How to fix it
Bind relevant conditions to the transactional operation.
Prevent it next time
Test competing ownership or membership changes.

Check your understanding

1. A detail endpoint checks ownership but its export route does not. Is the resource protected?

Show worked solution

No. Every exposure path must enforce the relevant policy. Enumerate list, export, attachment, mutation, and background flows and test them with an unrelated actor. Securing one route does not compensate for another returning the same data without authorization.

2. Why is overriding the ownership function with always true a weak authorization test?

Show worked solution

It removes the behavior the test should protect. Instead supply controlled principals and resources and execute the real policy, asserting both allowance and denial. That catches missing route attachment and policy regressions at the public HTTP boundary.

Key takeaways

  • Authentication establishes who the actor is; authorization decides whether that actor may perform this operation on this resource now.
  • Ownership or membership can change between an early check and a later write.
  • Include anonymous, owner, unrelated user, wrong tenant, privileged actor, and relevant resource-state cases.

Official references