May this user do this?
Authorization
Authentication answers who is calling. Authorization answers whether that caller may perform this action on this record. Roles gate whole routes; policies decide the question a role cannot — may this user edit this post?
Pick the right mechanism
| Concern | Mechanism |
|---|---|
| A page needs a signed-in session | Route privacy in src/lib/auth/auth_config.py |
| An RPC needs a session or role | @rpc(require_auth=True, allowed_roles=[...]) |
| A caller may perform a named action | A casp.authorization ability |
| A caller may change this record | A resource policy |
Define policies
Put policies in src/lib/auth/policies.py. main.py
imports the file at startup when it exists, and the decorators register on the shared gate.
# src/lib/auth/policies.py
from casp.authorization import gate
@gate.before
def admins_can_do_anything(user, ability):
return True if user.get("roleName") == "admin" else None
@gate.define("reports.view")
def view_reports(user):
return "viewReports" in user.get("permissions", [])
@gate.define_resource("posts.update")
def update_post(user, post):
return post.authorId == user["id"]
@gate.define("posts.list", allow_guests=True)
def list_posts(user):
return True
-
useris the signed-in session payload (auth.get_payload()). -
Ability names are app-wide (1–128 of letters, digits,
. _ : -); use namespaces likeposts.update. Duplicates fail at startup. -
Policies are synchronous and must return exactly
True. Load rows in the async handler first. -
allow_guests=Trueruns the policy withuser=None; otherwise signed-out callers are unauthenticated. -
beforehooks returnTrue,False, orNone(continue), and only run for defined abilities and signed-in users — a misspelled ability can never be granted by an override.
Enforce in the handler
from casp.authorization import authorize
from casp.errors import HttpError
from casp.rpc import rpc
@rpc(require_auth=True)
async def update_post(post_id: int, title: str):
post = await prisma.post.find_unique(where={"id": post_id})
if post is None:
raise HttpError.not_found("Post not found")
authorize("posts.update", post) # 401 signed out, 403 denied
return await prisma.post.update(where={"id": post_id}, data={"title": title})
authorize(...) raises HttpError(401) for a
signed-out caller and HttpError(403) for a denied or undefined ability —
pages render their error boundary,
RPCs return error, and route.py endpoints return
Problem Details. Take identity from the session, never from an argument, and test with two users.
Session-only abilities on an RPC
can= accepts a string or list and is checked before the payload is parsed.
Resource policies cannot be checked there — call authorize(ability, resource) inside.
@rpc(can="reports.view")
async def export_report(month: str):
...
Hiding UI
allows(...) returns a boolean and templates get a can(...) global.
Use a Jinja if block — a server value in hidden renders as text and hides either way.
{% if can('posts.update', post) %}
<button onclick="{editPost()}">Edit</button>
{% endif %}
Hiding a control is presentation only. The receiving handler must still call authorize(...).
Decisions deny by default
inspect_ability(ability, resource?) returns a Decision. Everything that is not ALLOWED denies, and policy exceptions, undefined abilities, and resource mismatches are logged to stderr without reaching the client.
ALLOWED
The policy or a before hook granted it.
UNAUTHENTICATED
No session, and guests are not allowed.
DENIED
Returned non-True, raised, or got the wrong resource.
UNDEFINED
No policy defines the ability, or the name is invalid.
Test through the real handler with three sessions: signed out (401), signed in but not allowed (403), and allowed.
In unit tests pass user= explicitly, or install a separate gate with
casp.authorization.configure(Gate()) and restore the previous one afterwards.
Authentication
Sessions, route privacy, roles, and OAuth providers.
Request Pipeline
Middleware runs after auth, so allows(...) works there too.