A FastAPI dependency is a callable that supplies a value before a path operation runs. Dependencies can compose: one reads an authenticated reviewer, another obtains a repository, and a third uses both to resolve a permitted case. This is useful for request-scoped policy and test overrides, but dependency injection is not itself authorization. A dependency that only verifies a bearer token still leaves every case ID reachable unless the resource lookup includes the reviewer. A private case lookup must be defined by server-side relationships and used by every endpoint that exposes or mutates that case. The path parameter identifies the requested object; it is never evidence of permission.
FastAPI Dependencies and Object Authorization
Working case
A permit API has reviewer 47 assigned to case 62 and reviewer 81 assigned elsewhere. A detail route checks the token, then fetches case 62 by primary key. Reviewer 81 can change the URL and receive private notes. A second endpoint exports a case from the same unscoped repository method. The repair makes a dependency resolve a case under the current reviewer mapping and returns the same private-resource response for missing and forbidden cases. The approval service repeats the permission test inside its write transaction. That second check matters when a background command, another route, or a changed assignment reaches the service after the initial read.
Implementation boundary
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException
from .models import PermitCase, PermitView, Reviewer
from .services import PermitRepository, current_reviewer, get_repository
router = APIRouter()
def authorized_case(
case_id: int,
reviewer: Annotated[Reviewer, Depends(current_reviewer)],
repository: Annotated[PermitRepository, Depends(get_repository)],
) -> PermitCase:
permit = repository.find_for_reviewer(reviewer.id, case_id)
if permit is None:
raise HTTPException(status_code=404, detail="Case not found")
return permit
@router.get("/permits/{case_id}", response_model=PermitView)
def read_permit(permit: Annotated[PermitCase, Depends(authorized_case)]):
return {"id": permit.id, "title": permit.title, "status": permit.status}Use a dependency to parse and verify the caller's identity, rejecting an absent or invalid credential before private repository work. Compose it with a repository dependency and a case dependency that filters by reviewer ID and case ID together. Keep any public error detail short enough that a forbidden case title or owner cannot leak. Share the scoped lookup between detail and export routes, but do not treat a previously resolved ORM object as permanent permission for a later write. Put the current-state check in the command service as well. A test can override the identity and repository dependencies to exercise every case without bypassing the policy function itself. Make the policy callable small enough to audit.
Cost and boundaries
Dependency evaluation is request work, and a private object lookup usually adds one database round trip. A careless dependency graph can repeat expensive work or open multiple sessions per request; inspect actual query counts and the chosen cache behavior rather than assuming reuse. A scoped query with indexes on case identity and reviewer mapping should avoid loading every assignment in Python. Returning 404 for both forbidden and missing cases may simplify disclosure, but it can make support diagnostics harder; logs can retain a request identifier and private reason without copying those details to the response. Measure allowed, forbidden, anonymous, and missing paths under representative reviewer assignment counts.
Failure trace
Call the detail and export endpoints with no credential, a malformed credential, reviewer 47, reviewer 81, and an unknown case. All private routes should use the same disclosure rule. Temporarily replace the scoped lookup with a raw primary-key lookup to demonstrate the guessed-ID leak, then restore it. Change a case assignment between the initial read and approval; the write must reject the stale permission. Verify that dependency overrides in tests still exercise the real policy with controlled identities and repository rows. Check that a repository exception becomes an internal response without revealing SQL, tokens, or case titles. Count session and query creation for each path.
Verification
- Authentication is resolved before private repository work.
- The repository filters by reviewer and case together.
- Every write service verifies permission again under current state.
Practice drill
Implement a case detail route, an export metadata route, and an approval command for two reviewers. Write the authorization predicate once as a repository operation that accepts reviewer and case IDs. Use it in a composed dependency for reads and inside the approval transaction for writes. Add a test that guesses a valid case ID from the wrong reviewer account. Add another that revokes assignment after a detail read but before approval. Capture request IDs in server logs while returning the same short response for private missing and forbidden cases. Compare query counts before and after factoring dependencies.
Decision note
Use dependencies to assemble request context; enforce exact object permission in the scoped lookup and in every write service.
Common Mistakes
- Assuming a verified token authorizes every case ID.
- Overriding away the policy dependency in tests.
- Reusing a past read permission for a later state-changing command.
Related lessons
FastAPI Contract and Resource Boundaries; FastAPI Input, Output, and Error Contracts; FastAPI Async Sessions and Transaction Ownership; FastAPI Lifespan, Background Work, and Delivery State; Authorization and Tenant Boundaries; Django Middleware, Sessions, and Object Permissions.
Apply and check
Build Project: FastAPI permit service boundaries and review Web Development: FastAPI contract and resource quiz.
