Authorization Guards (RBAC)¶
Cello's guard system lets you declaratively protect any route by attaching one or more guard objects to the guards= parameter. Guards are evaluated before the handler runs; if any guard raises a ForbiddenError the request is rejected with a 403. Guards compose with And, Or, and Not combinators for expressive, reusable access-control logic.
This example shows how to implement role-based and permission-based guards, combine them with boolean logic, and write a fully custom IP-allowlist guard as a callable class.
Features Demonstrated¶
Role(["admin"])guard — restricts access to users with a specific rolePermission(["users:write"])guard — restricts access based on fine-grained permissions- Stacking multiple guards (implicit
AND) on a single route Or([...])combinator — allows access if any one guard passes- Custom callable guard class (
IPAllowlist) — arbitrary logic encapsulated as a guard ForbiddenError— raised inside guards to produce a 403 response- Mock login endpoint that returns user context consumed by downstream guards
Complete Source Code¶
from cello import App, Request
from cello.guards import Role, Permission, Authenticated, And, Or, Not, ForbiddenError
app = App()
@app.post("/login/{username}")
def login(request: Request):
username = request.params["username"]
if username == "admin":
user_data = {"id": 1, "username": "admin", "roles": ["admin"], "permissions": ["users:read", "users:write", "users:delete"]}
elif username == "mod":
user_data = {"id": 2, "username": "mod", "roles": ["moderator"], "permissions": ["users:read", "users:write"]}
else:
user_data = {"id": 3, "username": "user", "roles": ["user"], "permissions": ["users:read"]}
return {"token": "mock-token", "user": user_data}
@app.get("/admin", guards=[Role(["admin"])])
def admin_only(request):
return {"message": "Welcome Admin"}
@app.post("/users", guards=[Permission(["users:write"])])
def create_user(request):
return {"message": "User created"}
@app.delete("/users/{id}", guards=[Role(["admin"]), Permission(["users:delete"])])
def delete_user(request):
return {"message": f"User {request.params['id']} deleted"}
@app.get("/reports", guards=[Or([Role(["admin"]), Role(["moderator"])])])
def view_reports(request):
return {"message": "Reports view"}
class IPAllowlist:
def __init__(self, allowed_ips):
self.allowed_ips = allowed_ips
def __call__(self, request):
client_ip = request.headers.get("X-Real-IP", "127.0.0.1")
if client_ip not in self.allowed_ips:
raise ForbiddenError(f"IP {client_ip} not allowed")
return True
@app.get("/internal", guards=[IPAllowlist(["127.0.0.1"])])
def internal_api(request):
return {"message": "Internal API access granted"}
if __name__ == "__main__":
app.run(port=8080)
Running This Example¶
# Log in as admin and capture context
curl -X POST http://localhost:8080/login/admin
# Access admin-only route (succeeds with admin token)
curl http://localhost:8080/admin
# Access admin-only route as regular user (403 Forbidden)
curl http://localhost:8080/admin
# Create a user — requires users:write permission
curl -X POST http://localhost:8080/users
# Delete a user — requires admin role AND users:delete permission
curl -X DELETE http://localhost:8080/users/42
# Reports accessible to both admin and moderator roles
curl http://localhost:8080/reports
# Internal endpoint — accessible only from 127.0.0.1
curl http://localhost:8080/internal
# Simulate a blocked IP
curl -H "X-Real-IP: 10.0.0.1" http://localhost:8080/internal
Key Concepts¶
- Guard evaluation order — guards in the
guards=[]list are checked left-to-right; the first failure short-circuits and returns 403 - Implicit AND — listing multiple guards is equivalent to wrapping them in
And([...]), requiring all to pass Or([...])combinator — grants access when at least one of the provided guards succeeds, enabling multi-role access to shared endpointsForbiddenError— the standard way for any guard (built-in or custom) to signal that access should be denied- Custom callable guards — any Python callable that accepts
requestand either returnsTrueor raisesForbiddenErrorqualifies as a guard, enabling arbitrary logic (IP filtering, time-of-day restrictions, feature flags, etc.) Not([...])combinator — inverts a guard's result, useful for "deny if role is X" scenariosAuthenticatedguard — a built-in guard that checks whether the request carries a recognised authentication context