TT Lab
Get started
Learn Learning paths Courses

FastAPI — Types Are the Contract

A valid token does not grant document ownership

Continue in TT Lab

Goal

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

Why it 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.

Steps

  1. In /root/work/fa-ownership-lab/service.py, 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.

Prepare this once at the start. Existing files are not overwritten.

mkdir -p /root/work/fa-ownership-lab
test -e /root/work/fa-ownership-lab/service.py || cp /opt/fixtures/ten_labs/fa-ownership-lab/service.py /root/work/fa-ownership-lab/service.py
cd /root/work/fa-ownership-lab
  1. In /root/work/fa-ownership-lab/service.py, principal(token, users) returns the user {id, scopes} from the token dictionary, but copies the scopes list as well. An unknown token is ValueError.

  2. In /root/work/fa-ownership-lab/service.py, require_scope(user, scope) returns None when the scope string is exactly present in scopes, and raises PermissionError otherwise. read-all is not read.

  3. In /root/work/fa-ownership-lab/service.py, visible(user, document) is True only when document is not None and owner is exactly equal to the user's id.

  4. In /root/work/fa-ownership-lab/service.py, public_document(document) is a new dictionary that has only id and title. It does not include owner or internal_cost.

  5. In /root/work/fa-ownership-lab/service.py, authenticate(header, users) connects bearer and principal. ValueError becomes HTTPException(401), and the WWW-Authenticate value in headers is Bearer.

  6. In /root/work/fa-ownership-lab/service.py, 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.

  7. In /root/work/fa-ownership-lab/service.py, 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.

Notes

Split the Bearer header

In /root/work/fa-ownership-lab/service.py, 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.

Prepare this once at the start. Existing files are not overwritten.

mkdir -p /root/work/fa-ownership-lab
test -e /root/work/fa-ownership-lab/service.py || cp /opt/fixtures/ten_labs/fa-ownership-lab/service.py /root/work/fa-ownership-lab/service.py
cd /root/work/fa-ownership-lab

If you split the header into several pieces arbitrarily, a whitespace error can be accepted as a normal token.

After saving, check with bash /opt/lab/checks/fa-ownership-lab/01-contract.sh.

Copy and return the identity

In /root/work/fa-ownership-lab/service.py, principal(token, users) returns the user {id, scopes} from the token dictionary, but copies the scopes list as well. An unknown token is ValueError.

If modifying the returned scopes also changes the original user's permissions, permissions get mixed between requests.

After saving, check with bash /opt/lab/checks/fa-ownership-lab/02-contract.sh.

Compare permissions exactly

In /root/work/fa-ownership-lab/service.py, require_scope(user, scope) returns None when the scope string is exactly present in scopes, and raises PermissionError otherwise. read-all is not read.

A substring comparison mistakes a longer permission name for a different permission.

After saving, check with bash /opt/lab/checks/fa-ownership-lab/03-contract.sh.

Check ownership separately

In /root/work/fa-ownership-lab/service.py, visible(user, document) is True only when document is not None and owner is exactly equal to the user's id.

A missing resource and a resource owned by someone else are bundled into the same decision.

After saving, check with bash /opt/lab/checks/fa-ownership-lab/04-contract.sh.

Choose response fields with an allow list

In /root/work/fa-ownership-lab/service.py, public_document(document) is a new dictionary that has only id and title. It does not include owner or internal_cost.

Do not delete fields from the original; assemble a new response.

After saving, check with bash /opt/lab/checks/fa-ownership-lab/05-contract.sh.

Match errors to the HTTP contract

In /root/work/fa-ownership-lab/service.py, authenticate(header, users) connects bearer and principal. ValueError becomes HTTPException(401), and the WWW-Authenticate value in headers is Bearer.

Do not lump an authentication failure and an application error together into a single 500.

After saving, check with bash /opt/lab/checks/fa-ownership-lab/06-contract.sh.

Fix the order of rejection

In /root/work/fa-ownership-lab/service.py, 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.

Even after authentication, scope and ownership must each be checked.

After saving, check with bash /opt/lab/checks/fa-ownership-lab/07-contract.sh.

Close the boundary in a real request

In /root/work/fa-ownership-lab/service.py, 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.

Even if the functions are each correct, access control is not applied if the route forgets to call them.

After saving, check with bash /opt/lab/checks/fa-ownership-lab/08-contract.sh.