TT Lab
Get started
Learn Learning paths Courses

FastAPI — Types Are the Contract

A valid token does not grant document ownership: design principles

Continue in TT Lab

Summary

Separate authentication, authorization, and ownership, and hide the existence of a resource behind the same 404.

Why this matters

A logged-in user changed only the document number in the address and read another team's document. That a token is valid and that you have the right to read a particular document are different things. In this lab we use a fixed token dictionary in place of an authentication server. It is not a lab about issuing tokens or implementing JWT signatures; it is a lab about implementing the authorization boundary after the authentication result.

How it works

A request goes through, in order, a check of the Bearer header format, a token lookup, a scope check, and an owner check. If the credentials are missing or wrong, the result is 401 and the WWW-Authenticate header is sent. If the identity is confirmed but there is no read permission, it is 403. If a user who has read permission requests a document that does not exist or belongs to someone else, both cases are 404. The public response keeps only id and title so that the internal owner and cost are not leaked.

헤더 → 인증 401 → scope 403 → 소유권/존재 404 → 공개 필드 200

A worksheet for reading the contract and predicting failures

What follows is not an answer key to memorize an implementation but a step-by-step code review. Each change fragment deliberately breaks the contract. Note that the normal case may still pass after the change. Before running it, predict which input, exception, or state you would observe to expose the difference, and after implementing it, compare that prediction with the result.

1. Split the Bearer header

bearer(header) returns the token when the header starts with exactly 'Bearer ' and is followed by one token with no whitespace. None, an empty token, a different scheme, and extra whitespace are ValueError.

Basis for the judgment: if you split the header into several pieces arbitrarily, a whitespace error can be accepted as a normal token.

Faulty change fragment to review:

header[6:]

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

2. Copy and return the identity

principal(token, users) returns the user {id, scopes} from the token dictionary, but copies the scopes list as well. An unknown token is ValueError.

Basis for the judgment: if modifying the returned scopes also changes the original user's permissions, permissions get mixed between requests.

Faulty change fragment to review:

user["scopes"]

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

3. Compare permissions exactly

require_scope(user, scope) returns None when the scope string is exactly present in scopes, and raises PermissionError otherwise. read-all is not read.

Basis for the judgment: a substring comparison mistakes a longer permission name for a different permission.

Faulty change fragment to review:

if False:

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

4. Check ownership separately

visible(user, document) is True only when document is not None and owner is exactly equal to the user's id.

Basis for the judgment: a missing resource and a resource owned by someone else are bundled into the same decision.

Faulty change fragment to review:

True

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

5. Choose response fields with an allow list

public_document(document) is a new dictionary that has only id and title. It does not include owner or internal_cost.

Basis for the judgment: do not delete fields from the original; assemble a new response.

Faulty change fragment to review:

("id", "title", "owner")

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

6. Match errors to the HTTP contract

authenticate(header, users) connects bearer and principal. ValueError becomes HTTPException(401), and the WWW-Authenticate value in headers is Bearer.

Basis for the judgment: do not lump an authentication failure and an application error together into a single 500.

Faulty change fragment to review:

HTTPException(403,

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

7. Fix the order of rejection

read_document(user, documents, document_id) raises HTTPException(403) if there is no read scope, HTTPException(404) if the document does not exist or belongs to someone else, and otherwise returns the result of public_document.

Basis for the judgment: even after authentication, scope and ownership must each be checked.

Faulty change fragment to review:

HTTPException(403, "not found")

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

8. Close the boundary in a real request

create_app(users, documents) returns a FastAPI app that receives the Authorization header in GET /documents/{document_id} and calls authenticate and read_document. Verify 200, 401, 403, 404, and the removal of private fields with real requests.

Basis for the judgment: even if the functions are each correct, access control is not applied if the route forgets to call them.

Faulty change fragment to review:

authorization: str | None = None

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

What it looks like in the field

A fixed token dictionary is teaching input. Production authentication additionally needs expiry, signatures, revocation, and safe storage. Making the 404s identical also does not remove every inference through response time or access logs. Also check that a rejected request did not change the original data.

What you will do in the next lab

Eight steps lead to one runnable result. Split the Bearer header → copy and return the identity → compare permissions exactly → check ownership separately → choose response fields with an allow list → match errors to the HTTP contract → fix the order of rejection → close the boundary in a real request.

Each step checks actual return values, exceptions, and state changes, not the fact that a function or file exists. After you see the answer, deliberately change a boundary comparison or the cleanup code and check which tests fail. Explain why the earlier tests are kept in the next steps, and write down one operating condition that this lab does not guarantee.