Typorithm · medium

Identity and object ownership are different checks

pythonfastapiindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Identity and object ownership are different checks

0%

Concept and contract

Use a local opaque-token fixture and deny cross-owner reads before returning data.

Authentication resolves who is making a request. Authorization decides whether that principal may perform an operation on a selected object. A valid credential does not imply permission to read every record with a predictable integer identifier. Test both boundaries separately.

This fixture maps two clearly synthetic opaque bearer strings to demo principals. It intentionally does not implement JWT signing, password storage or a production identity provider. The protected endpoint first obtains a principal through a dependency, then compares that principal with the record owner.

Mechanism and worked example

HTTPBearer extracts the credential representation.

HTTPBearer extracts the credential representation. The dependency rejects missing or unknown demo credentials with 401 and a bearer challenge. After identity resolution, the endpoint checks record existence and ownership. The chosen policy returns 404 for both an absent object and an object belonging to someone else.

The local record contains a private note, but the success response is a deliberately small mapping. The helper sends an optional Authorization header through TestClient and compares the real status and body. No real account, token, email or live user record is involved.

Complete worked example

python

from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi.testclient import TestClient

def inspect_example(request):
    app = FastAPI()
    bearer = HTTPBearer(auto_error=False)
    tokens = {"demo-alice": "alice", "demo-bob": "bob"}
    records = {1: {"owner": "alice", "title": "QA", "private_note": "hidden"}}
    def principal(credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(bearer)]):
        if credentials is None or credentials.credentials not in tokens:
            raise HTTPException(401, "Sign in", headers={"WWW-Authenticate": "Bearer"})
        return tokens[credentials.credentials]
    @app.get("/roles/{role_id}")
    def read(role_id: int, user: Annotated[str, Depends(principal)]):
        record = records.get(role_id)
        if record is None or record["owner"] != user:
            raise HTTPException(404, "Role not found")
        return {"id": role_id, "title": record["title"]}
    headers = {} if "token" not in request else {"Authorization": "Bearer " + request["token"]}
    with TestClient(app) as client:
        response = client.get(f"/roles/{request.get('id', 1)}", headers=headers)
        return {"status": response.status_code, "body": response.json()}
Reasoning
HTTPBearer extracts the credential representation. The dependency rejects missing or unknown demo credentials with 401 and a bearer challenge. After identity resolution, the endpoint checks record existence and ownership. The chosen policy returns 404 for both an absent object and an object belonging to someone else. The local record contains a private note, but the success response is a deliberately small mapping. The helper sends an optional Authorization header through TestClient and compares the real status and body. No real account, token, email or live user record is involved.

Focused decision: use the complete example for context

python

def owned_record(record, user):
    if record is None:
        raise HTTPException(404, "Role not found")
    if record["owner"] != user:
        raise HTTPException(404, "Role not found")
    return {"title": record["title"]}
Focused decision
Reconstruct this focused decision after the warm-up. The complete imports, model or application, and test client remain in the worked example. Typing records practice; only the downloadable computer lab executes Python.

Counterexamples and common mistakes

The counterexample removes the owner comparison while retaining authentication.

The counterexample removes the owner comparison while retaining authentication. A user with a valid demo token can then read another principal record. Checking only the no-token case would falsely suggest that the endpoint is protected correctly.

Do not derive the effective owner from an untrusted owner_id query parameter. Likewise, HTTPBearer is a parsing helper, not a credential verifier on its own. A real deployment needs verified credentials, expiry or revocation policy and an authorization model appropriate to its identity system.

Controlled mistake: observe the actual wrong behavior

python

from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi.testclient import TestClient

def inspect_example(request):
    app = FastAPI()
    bearer = HTTPBearer(auto_error=False)
    tokens = {"demo-alice": "alice", "demo-bob": "bob"}
    records = {1: {"owner": "alice", "title": "QA", "private_note": "hidden"}}
    def principal(credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(bearer)]):
        if credentials is None or credentials.credentials not in tokens:
            raise HTTPException(401, "Sign in", headers={"WWW-Authenticate": "Bearer"})
        return tokens[credentials.credentials]
    @app.get("/roles/{role_id}")
    def read(role_id: int, user: Annotated[str, Depends(principal)]):
        record = records.get(role_id)
        if record is None:
            raise HTTPException(404, "Role not found")
        return {"id": role_id, "title": record["title"]}
    headers = {} if "token" not in request else {"Authorization": "Bearer " + request["token"]}
    with TestClient(app) as client:
        response = client.get(f"/roles/{request.get('id', 1)}", headers=headers)
        return {"status": response.status_code, "body": response.json()}
Reasoning
The counterexample removes the owner comparison while retaining authentication. A user with a valid demo token can then read another principal record. Checking only the no-token case would falsely suggest that the endpoint is protected correctly. Do not derive the effective owner from an untrusted owner_id query parameter. Likewise, HTTPBearer is a parsing helper, not a credential verifier on its own. A real deployment needs verified credentials, expiry or revocation policy and an authorization model appropriate to its identity system.

Independent practice: one bounded change

Change the cross-owner policy to an explicit 403 while keeping absent objects as 404.

Change the cross-owner policy to an explicit 403 while keeping absent objects as 404. Preserve missing-credential 401 and successful own-object reads. This small task changes disclosure semantics deliberately rather than weakening ownership checks.

Test a known foreign object, an absent object and an own object. Consider the existence information disclosed by 403 versus the uniform 404 baseline. Neither status is universally right; the important point is choosing, documenting and testing a consistent product policy.

Step 1: Keep ownership enforced but return 403 for an existing foreign object and 404 for an absent object. Step 2: Retain the working imports and application structure. Change the stated policy, run the supplied practice cases, then explain the newly accepted or rejected boundary.

Practice starter: solve before reading the solution

python

from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi.testclient import TestClient

def inspect_example(request):
    app = FastAPI()
    bearer = HTTPBearer(auto_error=False)
    tokens = {"demo-alice": "alice", "demo-bob": "bob"}
    records = {1: {"owner": "alice", "title": "QA", "private_note": "hidden"}}
    def principal(credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(bearer)]):
        if credentials is None or credentials.credentials not in tokens:
            raise HTTPException(401, "Sign in", headers={"WWW-Authenticate": "Bearer"})
        return tokens[credentials.credentials]
    @app.get("/roles/{role_id}")
    def read(role_id: int, user: Annotated[str, Depends(principal)]):
        record = records.get(role_id)
        if record is None or record["owner"] != user:
            raise HTTPException(404, "Role not found")
        return {"id": role_id, "title": record["title"]}
    headers = {} if "token" not in request else {"Authorization": "Bearer " + request["token"]}
    with TestClient(app) as client:
        response = client.get(f"/roles/{request.get('id', 1)}", headers=headers)
        return {"status": response.status_code, "body": response.json()}

# Independent task: Keep ownership enforced but return 403 for an existing foreign object and 404 for an absent object.
Reasoning
Keep ownership enforced but return 403 for an existing foreign object and 404 for an absent object.
  • Hint 1: Find the one field or branch that owns this policy.
  • Hint 2: Compare the first changed acceptance case with the retained baseline.

Solution, tests and trade-offs

The solution separates the absent-record branch from the ownership branch, returning 403 only when the selected object exists but access is denied.

The solution separates the absent-record branch from the ownership branch, returning 403 only when the selected object exists but access is denied. It keeps the principal dependency and the response projection. The fixture checks the caller data remains unchanged.

A write operation also needs ownership checks before mutation and normally transactional concurrency handling. A local mapping is not a substitute for durable storage, audit records or a production credential system. Extend the policy at the service boundary instead of treating this demonstration token table as deployable security.

Practice solution and acceptance checks

python

from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi.testclient import TestClient

def inspect_example(request):
    app = FastAPI()
    bearer = HTTPBearer(auto_error=False)
    tokens = {"demo-alice": "alice", "demo-bob": "bob"}
    records = {1: {"owner": "alice", "title": "QA", "private_note": "hidden"}}
    def principal(credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(bearer)]):
        if credentials is None or credentials.credentials not in tokens:
            raise HTTPException(401, "Sign in", headers={"WWW-Authenticate": "Bearer"})
        return tokens[credentials.credentials]
    @app.get("/roles/{role_id}")
    def read(role_id: int, user: Annotated[str, Depends(principal)]):
        record = records.get(role_id)
        if record is None:
            raise HTTPException(404, "Role not found")
        if record["owner"] != user:
            raise HTTPException(403, "Access denied")
        return {"id": role_id, "title": record["title"]}
    headers = {} if "token" not in request else {"Authorization": "Bearer " + request["token"]}
    with TestClient(app) as client:
        response = client.get(f"/roles/{request.get('id', 1)}", headers=headers)
        return {"status": response.status_code, "body": response.json()}
Reasoning
The solution separates the absent-record branch from the ownership branch, returning 403 only when the selected object exists but access is denied. It keeps the principal dependency and the response projection. The fixture checks the caller data remains unchanged. A write operation also needs ownership checks before mutation and normally transactional concurrency handling. A local mapping is not a substitute for durable storage, audit records or a production credential system. Extend the policy at the service boundary instead of treating this demonstration token table as deployable security.

Interview reasoning and next step

In an interview, name the test that would catch authenticated cross-owner access.

In an interview, name the test that would catch authenticated cross-owner access. Explain why status selection can disclose existence and why a response schema cannot compensate for a missing owner check. Then distinguish bearer extraction from verification.

Next, manage resources with application lifespan. A correct security decision can still be undermined operationally by leaked connections or work performed before startup. Lifecycle tests should observe acquisition and cleanup, not merely instantiate the application object.

Optional stretch — after the core task works: Add one independently reasoned boundary case and explain its expected value before running it.

Computer practice: https://applydjinn.com/typorithm/python-api-lab-20261105.zip. Use an isolated Python environment and the pinned dependencies. Run verify.py --track fastapi, then edit the working exercise baseline and run --practice fa-authorization. The reference suite rejects meaningful counterexamples and checks values, types and unchanged inputs. All data is owned and all HTTP clients are in-process. No production database, feed, account or paid service is used.

Use a local opaque-token fixture and deny cross-owner reads before returning data.