Policy
A policy is a function that receives a subject and optionally a resource and returns an authorization object, either granted or denied.
A policy defines an action that can be taken in your application, optionally on a resource, such as documents:read
, users:update
, comments:delete
.
In a nutshell…
- Policies can be nested as deep as you want, forming a hierarchy.
- Policies always take the subject as the first argument.
- Policies can optionally take in a resource as the second argument.
- Policies can be asynchronous functions and fetch data from external sources for deciding access.
- A policy must explicitly either grant (with
return grant(subject)
) or deny (withreturn deny(reason?)
) access.
Defining policies
Define policies by constructing a policies
object (with as const satisfies Policyset<MySubjectType>
for improved type-safety).
import { deny, grant, type Policyset } from "@kilpi/core";
export const policies = { // Example hierarchy documents: {
// Allow all authed members to read docs read(user) { if (!user) return deny("Unauthenticated"); return grant(user); },
}} as const satisfies Policyset<MySubjectType>;
Defining a basic policy
A basic policy takes in a subject
(often aliased as user
) and returns an authorization with return grant(user)
or return deny(reason?)
.
The subject is automatically typed by
satisfies Policyset<MySubjectType>
.
export const policies = { comments: { create(user) { if (!user) return deny("Unauthenticated"); return grant(user); } }} as const satisfies Policyset<MySubjectType>;
await Kilpi.authorize("comments:create");
Defining a policy with a resource
A policy can optionally receive a resource as the second argument.
export const policies = { comments: { delete(user, comment: Comment) { if (!user) return deny("Unauthenticated"); return comment.userId === user.id ? grant(user) : deny(); } }} as const satisfies Policyset<MySubjectType>;
await Kilpi.authorize("comments:delete", myComment);
Defining an asynchronous data-fetching policy
Policies can even fetch data during evaluation, as they are by design always asynchronous.
export const policies = { comments: { async archive(user, comment: Comment) { if (!user) return deny("Unauthenticated"); const response = await getAiResponse("Allow deleting comment: Yes or no?"); return response.includes("Yes") ? grant(user) : deny(); } }} as const satisfies Policyset<MySubjectType>;
await Kilpi.authorize("comments:delete", myComment);
Tip: Utility functions
Almost always, your policies will be more readable when using utility functions. See below for examples on how to use utility functions for e.g. data fetching, common denials and more.
const unauthed = () => deny("Unauthenticated");
const isAdmin = (subject: Subject) => subject.role === "admin";
const isMember = async (subject: Subject, orgId: string) => { const memberships = await db.getMembershipsForUser(subject.id); return memberships.some(m => m.orgId === orgId && m.role === role);};
export const policies = { documents: { async create(user, orgId: string) { if (!user) return deny("Unauthenticated"); if (user.role === "admin") return grant(user); const memberships = await db.getMembershipsForUser(user.id); return memberships.some(m => m.orgId === orgId && m.role === "manager") ? grant(user) : deny(); } async create(user, orgId: string) { if (!user) return unauthed(); return isAdmin(user) || (await isMember(user, orgId)) ? grant(user) : deny(); }} as const satisfies Policyset<MySubjectType>;
Narrowed down subject type
Due to this method of defining policies, the final subject type is automatically narrowed down by TypeScript.
export const policies = { documents: { read(user) { if (!user) return deny("Unauthenticated"); return grant(user); }, }} as const satisfies Policyset<{ userId: string } | null>;
// User is inferred to be non-nullconst user = await Kilpi.authorize("documents:read");// ^? { userId: string } | null
Structuring policies
Initially, you can start off with a simple policies object containing all your application’s policies. However, as your application grows, so will the policies
object. At some point, it might be beneficial to split your policies into separate files.
See project structure for more information on how to structure your project when splitting policies into multiple files.
export const documentPolicies = { read(subject, document) { ... }, create(subject, document) { ... }, delete(subject, document) { ... },} as const satisfies Policyset<MySubjectType>;
export const organizationPolicies = { read(subject, document) { ... }, create(subject, document) { ... }, delete(subject, document) { ... },} as const satisfies Policyset<MySubjectType>;
export const policies = { documents: documentPolicies, organizations: organizationPolicies,} as const satisfies Policyset<MySubjectType>;