Introduction
Simple FastAPI declarative endpoint-level access control, somewhat inspired by Pyramid.
Installation
Requirements: Python 3.10+ · FastAPI 0.135.3+ · PyJWT 2.12.1+
Why use Missil?
Permission checks tend to look the same across every protected endpoint: extract the token, verify it, find the area, check the level. Missil moves all of that out of your route functions and into a single declarative line per endpoint — keeping your business logic clean and your access rules explicit and auditable at a glance.
It also goes beyond simple scope checks: because permissions are stored as numeric levels per business area, a single token can express fine-grained access across multiple areas of your application without requiring separate tokens or custom middleware.
End-to-end example
import missil
from fastapi import FastAPI, Response
app = FastAPI()
SECRET_KEY = "..."
# 1. Declare a bearer — reads token from cookie or Authorization header (see Bearers guide)
bearer = missil.TokenBearer("Authorization", SECRET_KEY, permissions_key="permissions")
# 2. Declare business areas as typed attributes (see Access Control guide)
class AppAreas(missil.AreasBase):
finances: missil.Area
it: missil.Area
areas = AppAreas(bearer)
# 3. Protect endpoints — one dependency, no boilerplate
@app.get("/finances/report", dependencies=[areas.finances.READ])
def finances_report(): ...
@app.get("/finances/edit", dependencies=[areas.finances.WRITE])
def finances_edit(): ...
@app.get("/it/admin", dependencies=[areas.it.ADMIN])
def it_admin(): ...
# 4. Issue the token (e.g. at login)
@app.post("/login")
def login(response: Response):
claims = {"sub": "user123", "permissions": {"finances": missil.WRITE, "it": missil.READ}}
token = missil.encode_jwt_token(claims, SECRET_KEY, expiration_hours=8)
response.set_cookie("Authorization", f"Bearer {token}", httponly=True)
return {"msg": "logged in"}
How it works
Missil works in three steps:
1. Issue a JWT token containing a permissions dict under a key of your choice:
claims = {
"sub": "user123",
"permissions": { # key name must match the bearer's permissions_key
"finances": 0, # READ
"it": 2, # ADMIN
},
}
token = missil.encode_jwt_token(claims, SECRET_KEY, expiration_hours=8)
2. Declare a bearer that knows where to find the token and which key holds permissions:
3. Declare areas and protect endpoints:
class AppAreas(missil.AreasBase):
finances: missil.Area
it: missil.Area
areas = AppAreas(bearer)
@app.get("/report", dependencies=[areas.finances.READ]) # level 0+
def report(): ...
@app.get("/dashboard", dependencies=[areas.it.ADMIN]) # level 2 only
def dashboard(): ...
On every request, Missil extracts the token, decodes it, looks up the area in the
permissions dict and checks that the user's level satisfies the required level.
If not, it raises PermissionDeniedException (HTTP 403).
JWT payload structure
Missil expects the JWT payload to include a dict under the key you passed as
permissions_key to the bearer. Each entry maps an area name to a numeric access level:
Key name must match
The key name in the JWT payload ("permissions" above) must exactly match
the permissions_key argument passed to your bearer constructor.
Permission Hierarchy
| Level | Constant | Satisfies |
|---|---|---|
| 0 | READ |
READ |
| 1 | WRITE |
READ, WRITE |
| 2 | ADMIN |
READ, WRITE, ADMIN |
Higher permission levels automatically satisfy lower requirements — a user with
ADMIN access can reach READ and WRITE protected endpoints without needing
separate permission entries. See the Access Control guide for details.
Grouping rules with Role
When multiple areas must be verified together, use Role to define the combination
once and reuse it across endpoints:
analyst = missil.Role(areas.finances.READ, areas.it.READ)
@app.get("/dashboard", dependencies=[analyst])
def dashboard(): ...
See the Access Control guide for full details.
Sending the token
Depending on which bearer you chose, the client sends the token differently:
Issue the token as a cookie at login — the bearer reads it automatically on every subsequent request:
@app.post("/login")
def login(response: Response):
token = missil.encode_jwt_token(claims, SECRET_KEY, expiration_hours=8)
response.set_cookie(
key="Authorization", # must match the bearer's token_key
value=f"Bearer {token}",
httponly=True,
)
return {"msg": "logged in"}
The client needs no extra code — the browser sends the cookie automatically.
License
This project is licensed under the terms of the MIT license.
