FastAPI — Types Are the Contract
A valid token does not grant document ownership
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
- 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
-
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. -
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. -
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. -
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. -
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. -
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. -
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
- You work in the existing lab-dev environment with no internet and no package installation.
- Each step runs within a 45-second grading budget. Do not add real sleeps or network calls.
- The grader loads the submitted module fresh and checks it with independent inputs and a temporary DB. Implement the contract instead of returning the expected values as constants.
- FastAPI official documentation · pytest official documentation · Python sqlite3
- Limitation: 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.
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.