Policy Functions
The problem with scattered checks
Throughout this course, we have added requirePermission calls to every route handler. This works, but the authorization logic is scattered across many files. If you need to change a rule (like “editors can now delete their own notes”), you have to find every route that checks notes:delete and add the exception.
A policy function centralizes all authorization decisions into one place. Instead of checking permissions directly in handlers, you ask: “Can this user do this action on this resource?”
The can function
Create src/policy.ts:
// src/policy.ts
import { getMembership } from "./membership.js";
import { getPermissions } from "./permissions.js";
import type { AuthUser } from "./auth.js";
interface PolicyContext {
user: AuthUser;
orgId: string;
resourceOwnerId?: string; // who created the resource
}
export function can(ctx: PolicyContext, action: string): boolean {
const membership = getMembership(ctx.user.id, ctx.orgId);
if (!membership) return false;
const permissions = getPermissions(membership.role, ctx.orgId);
// Check basic permission
if (!permissions.has(action)) {
// Special case: creators can edit their own resources
if (action === "notes:edit" && ctx.resourceOwnerId === ctx.user.id) {
return permissions.has("notes:read"); // at minimum, they need read access
}
return false;
}
// Check API key scopes
if (ctx.user.scopes !== null && !ctx.user.scopes.includes(action)) {
return false;
}
return true;
} The can function takes a context (who is acting, in which org, on which resource) and an action. It returns true or false. All the logic is in one place.
Notice the special case: creators can edit their own notes even if their role does not include notes:edit. This kind of rule is easy to add in a policy function and messy to scatter across route handlers.
Using it in route handlers
Create a helper that turns the boolean into a Response:
// src/policy.ts — add this
export function authorize(ctx: PolicyContext, action: string): true | Response {
if (!can(ctx, action)) {
// If user is not a member, return 404. If they are but lack permission, return 403.
const membership = getMembership(ctx.user.id, ctx.orgId);
if (!membership) return Response.json({ error: "Not found" }, { status: 404 });
return Response.json({ error: "Forbidden" }, { status: 403 });
}
return true;
} Use it in routes:
route.put("/orgs/:orgId/notes/:noteId", {
resolve: async (c) => {
const user = authenticate(c.request);
if (user instanceof Response) return user;
// Look up the note to get its creator
const note = db.prepare("SELECT * FROM notes WHERE id = ? AND org_id = ?")
.get(c.params.noteId, c.params.orgId) as any;
if (!note) return Response.json({ error: "Not found" }, { status: 404 });
const auth = authorize(
{ user, orgId: c.params.orgId, resourceOwnerId: note.created_by },
"notes:edit",
);
if (auth instanceof Response) return auth;
// ... update the note
},
}), The handler passes the resource’s creator (note.created_by) in the context. The policy function uses this to apply the “creators can edit their own notes” rule.
Why policy functions are better
Centralized rules. All authorization logic is in src/policy.ts. When you add or change a rule, you change one file.
Testable. You can test can() directly without making HTTP requests. Test every combination of user, role, org, resource ownership, and action.
Readable. Route handlers say what they want (authorize(ctx, "notes:edit")) without explaining the rules. The rules live in the policy.
Extensible. Adding a new rule (like “viewers can edit notes tagged with ‘draft’”) is one if block in the policy function, not a change to every route.
Exercises
Exercise 1: Implement the can function and the authorize helper. Refactor the notes routes to use authorize instead of requirePermission.
Exercise 2: Test the “creators can edit their own notes” rule. Log in as Carol (viewer at Acme). Create a note as Alice, then try editing it as Carol. It should fail. Now create a note as Carol (if you give viewers notes:create — or test via seed data with Carol as the creator). Carol should be able to edit her own note even as a viewer.
Exercise 3: Add a new rule: “owners can perform any action without checking permissions.” Update the can function to skip the permission check for owners.
What is the main advantage of a centralized policy function over scattered permission checks?