polizy is a flexible, Zanzibar-inspired authorization library for Node.js and TypeScript applications. It allows you to define complex permission models based on relationships between users, groups, and resources directly within your application code.
Managing permissions in applications can quickly become complex. Traditional Role-Based Access Control (RBAC) often falls short when dealing with fine-grained permissions based on relationships (e.g., "user A can edit document B because they are in group C, which owns document B"). polizy provides a structured way to define and check these kinds of permissions.
- Embeddable Library: Unlike self-hosted authorization services (e.g., OpenFGA, Cerbos, Ory Keto),
polizyis integrated directly into your application, simplifying deployment and infrastructure. - Type-Safe Schema: Define your authorization model using TypeScript for compile-time checks and better developer experience.
- Relationship-Based Access: Permissions are determined by relationships (tuples) stored between subjects and objects.
- Hierarchy Support: Define parent-child relationships (e.g., folders and files) and automatically propagate permissions.
- Group Support: Manage permissions through group memberships.
- Pluggable Storage: Comes with an in-memory adapter for testing/development and a Prisma adapter for persistent storage. Easily extendable with custom adapters.
# Using npm
npm install polizy
# Using yarn
yarn add polizy
# Using pnpm
pnpm add polizy- Subject: Who is performing an action (e.g.,
user:alice). - Object: What is the action being performed on (e.g.,
document:xyz,folder:abc). Can also represent groups or hierarchical parents. - Relation: The relationship between a Subject and an Object (e.g.,
owner,editor,viewer,member,parent). - Action: The specific operation a Subject wants to perform on an Object (e.g.,
view,edit,delete). - Tuple: A stored record representing a relationship (
SubjecthasRelationtoObject, optionally withCondition). E.g.,(user:alice, owner, document:xyz). - Schema: Defines the possible
SubjectTypes,ObjectTypes,Relations, and howActionsmap toRelations. Also defines relation types (direct,group,hierarchy). - Storage Adapter: Handles the persistence of tuples (e.g.,
InMemoryStorageAdapter,PrismaStorageAdapter).
To start using polizy, you need to:
- Define a Schema: Create an authorization model using
defineSchema. This specifies your object types, subject types, the relationships between them, and how actions map to these relationships. - Choose a Storage Adapter: Select how relationship tuples will be stored. Use
InMemoryStorageAdapterfor quick starts orPrismaStorageAdapter(requires@prisma/client) for database persistence. - Instantiate AuthSystem: Create an instance of
AuthSystemwith your schema and storage adapter.
Use defineSchema to create your authorization model.
import { defineSchema } from 'polizy';
const mySchema = defineSchema({
subjectTypes: ['user', 'service_account'],
objectTypes: ['document', 'folder', 'team'], // Ensure 'team' is an object type if used in relations
relations: {
// Direct relations
owner: { type: 'direct' },
editor: { type: 'direct' },
viewer: { type: 'direct' },
// Group relation
member: { type: 'group' }, // 'member' relation links subjects to 'team' objects
// Hierarchy relation
parent: { type: 'hierarchy' }, // 'parent' relation links 'document'/'folder' to 'folder' objects
},
actionToRelations: {
// Define which relations grant which actions
view: ['viewer', 'editor', 'owner', 'member'], // Direct viewers/editors/owners OR members of a team linked via 'viewer'/'editor'/'owner'
edit: ['editor', 'owner'],
delete: ['owner'],
manage_members: ['owner'], // Only owners of a 'team' can manage members
share: ['owner', 'editor'],
},
// Optional: Define how permissions propagate up hierarchies
hierarchyPropagation: {
// If a user can 'view' the parent 'folder', they can also 'view' the child 'document'/'folder'
view: ['view'],
// If a user can 'edit' the parent 'folder', they can also 'edit' the child 'document'/'folder'
edit: ['edit'],
// Actions without propagation rules can be omitted or explicitly empty
delete: [],
manage_members: [],
share: [],
}
});Polizy provides adapters for persistence.
-
InMemoryStorageAdapter: Good for testing or simple use cases. Data is lost on restart. -
PrismaStorageAdapter: Persists tuples in your database using Prisma.- Requires
@prisma/clientto be installed. - Requires a Prisma model (typically named
PolizyTupleor similar) in yourschema.prismafile to store the relationship tuples. The model should include the following fields:subjectType: StringsubjectId: Stringrelation: StringobjectType: StringobjectId: Stringcondition: Json? (Optional, for attribute-based access control)
- It's highly recommended to add a unique constraint and relevant indexes for performance.
Example Prisma Model:
// filepath: prisma/schema.prisma model PolizyTuple { id String @id @default(uuid()) // Optional, but good practice subjectType String // e.g., 'user', 'team' subjectId String // e.g., 'alice', 'team-alpha' relation String // e.g., 'owner', 'member', 'parent' objectType String // e.g., 'document', 'folder' objectId String // e.g., 'doc1', 'folder-a' condition Json? // Optional ABAC conditions createdAt DateTime @default(now()) // Optional timestamp // Ensure each relationship is unique @@unique([subjectType, subjectId, relation, objectType, objectId]) // Index for finding relationships FOR a subject @@index([subjectType, subjectId, relation]) // Index for finding relationships ON an object @@index([objectType, objectId, relation]) }
- Instantiate with
new PrismaStorageAdapter(prismaClientInstance).
- Requires
// filepath: /path/to/your/auth/setup.ts
import { InMemoryStorageAdapter } from 'polizy';
// OR
import { PrismaStorageAdapter } from 'polizy/prisma-storage';
import { PrismaClient } from '@prisma/client'; // Adjust import based on your generated client location
// const prisma = new PrismaClient();
const storage = new InMemoryStorageAdapter();
// const storage = new PrismaStorageAdapter(prisma); // Example using PrismaCombine the schema and storage adapter.
import { AuthSystem } from 'polizy';
const authz = new AuthSystem({
schema: mySchema,
storage: storage,
});Use allow, disallowAllMatching, addMember, removeMember, setParent, removeParent.
// Grant direct permission
await authz.allow({
who: { type: 'user', id: 'alice' },
toBe: 'owner',
onWhat: { type: 'document', id: 'doc1' },
});
// Grant conditional permission (e.g., time-based)
await authz.allow({
who: { type: 'user', id: 'bob' },
toBe: 'viewer',
onWhat: { type: 'document', id: 'doc1' },
when: { validUntil: new Date(Date.now() + 3600 * 1000) } // Valid for 1 hour
});
// Add user to a team
await authz.addMember({
member: { type: 'user', id: 'carol' },
group: { type: 'team', id: 'team-alpha' }, // 'team' must be an objectType
});
// Set a parent folder
await authz.setParent({
child: { type: 'document', id: 'doc2' },
parent: { type: 'folder', id: 'folder-a' },
});
// Revoke a specific permission (equivalent to old disallow)
await authz.disallowAllMatching({
who: { type: 'user', id: 'alice' },
was: 'owner',
onWhat: { type: 'document', id: 'doc1' },
});
// Revoke all permissions for a specific user on a specific object
await authz.disallowAllMatching({
who: { type: 'user', id: 'bob' },
onWhat: { type: 'document', id: 'doc1' },
});
// Revoke all 'viewer' permissions on a specific object
await authz.disallowAllMatching({
was: 'viewer',
onWhat: { type: 'document', id: 'doc3' },
});
// Revoke ALL permissions associated with a specific object (e.g., when deleting the object)
await authz.disallowAllMatching({
onWhat: { type: 'document', id: 'doc-to-delete' },
});
// Revoke ALL permissions granted to a specific user (e.g., when deactivating the user)
await authz.disallowAllMatching({
who: { type: 'user', id: 'user-to-deactivate' },
});Use the check method.
const canAliceView = await authz.check({
who: { type: 'user', id: 'alice' },
canThey: 'view',
onWhat: { type: 'document', id: 'doc1' },
});
// Result: false (since we removed the 'owner' relation for alice above, and view requires owner/editor/viewer)
const canBobView = await authz.check({
who: { type: 'user', id: 'bob' },
canThey: 'view',
onWhat: { type: 'document', id: 'doc1' },
});
// Result: true (if within the validUntil time)
// Check permission potentially inherited via hierarchy
const canAliceViewDoc2 = await authz.check({
who: { type: 'user', id: 'alice' }, // Assuming alice has 'view' on 'folder-a'
canThey: 'view',
onWhat: { type: 'document', id: 'doc2' }, // doc2 is child of folder-a
});
// Result: true (if hierarchyPropagation is set and alice can view folder-a)
// Check permission potentially inherited via group membership
const canCarolViewDoc3 = await authz.check({
who: { type: 'user', id: 'carol' }, // carol is member of team-alpha
canThey: 'view',
onWhat: { type: 'document', id: 'doc3' }, // Assuming team-alpha was granted 'viewer' on doc3
});
// Result: trueUse the listAccessibleObjects method to find all objects of a specific type a subject can interact with, along with the specific actions allowed for each object identifier (including field-level identifiers). This is useful for building UI elements that only show items a user can interact with.
// Find all documents Alice can access and what she can do with each
const aliceDocs = await authz.listAccessibleObjects({
who: { type: 'user', id: 'alice' },
ofType: 'document',
});
/* Example Result for aliceDocs.accessible:
[
{
object: { type: 'document', id: 'doc1' }, // Alice is owner
// Note: Actions depend on the specific schema. 'manage_members' might appear if 'owner' grants it.
actions: [ 'delete', 'edit', 'manage_members', 'share', 'view' ]
},
{
object: { type: 'document', id: 'doc2' }, // Alice has view via hierarchy from folder-a
actions: [ 'view' ]
},
{
object: { type: 'document', id: 'doc9#field' }, // Alice has direct view on field
actions: [ 'view' ]
}
// Note: Base object 'doc9' would only appear if Alice had direct/group/hierarchy access to it specifically.
]
*/
// Find only documents that Carol (member of team-alpha) can 'edit'
const carolEditableDocs = await authz.listAccessibleObjects({
who: { type: 'user', id: 'carol' },
ofType: 'document',
canThey: 'edit', // Optional filter
});
/* Example Result for carolEditableDocs.accessible:
[
{
object: { type: 'document', id: 'doc3' }, // team-alpha is editor
actions: [ 'edit', 'share', 'view' ] // Returns all allowed actions for the object, even when filtering by one action
}
// Assuming doc5 was also editable via hierarchy in the full setup
// { object: { type: 'document', id: 'doc5' }, actions: [ 'edit', 'share', 'view' ] }
]
*/See the test files in src/scenarios/ (especially polizy.listAccessibleObjects.test.ts) for more detailed examples covering different authorization patterns like RBAC, ABAC (via conditions), hierarchy, and group-based access.
(Example based on polizy.example1.test.ts)
// Schema for Performance Reviews
const reviewSchema = defineSchema({
subjectTypes: ["user"],
objectTypes: ["review"],
relations: {
owner: { type: "direct" },
viewer: { type: "direct" },
editor: { type: "direct" },
},
actionToRelations: {
view: ["viewer", "editor", "owner"],
edit: ["editor", "owner"],
manage: ["owner"],
},
});
const storage = new InMemoryStorageAdapter();
const authz = new AuthSystem({ storage, schema: reviewSchema });
// Manager owns the review
await authz.allow({
who: { type: "user", id: "manager1" },
toBe: "owner",
onWhat: { type: "review", id: "cert1" },
});
// Employee can view a specific section initially
await authz.allow({
who: { type: "user", id: "employee1" },
toBe: "viewer",
onWhat: { type: "review", id: "cert1#strengths" }, // Object with field-level granularity
});
// Check: Manager can manage
assert.ok(await authz.check({
who: { type: "user", id: "manager1" },
canThey: "manage",
onWhat: { type: "review", id: "cert1" },
})); // true
// Check: Employee can view strengths
assert.ok(await authz.check({
who: { type: "user", id: "employee1" },
canThey: "view",
onWhat: { type: "review", id: "cert1#strengths" },
})); // true
// Check: Employee cannot edit strengths initially
assert.strictEqual(await authz.check({
who: { type: "user", id: "employee1" },
canThey: "edit",
onWhat: { type: "review", id: "cert1#strengths" },
}), false); // false
// Grant edit permission to employee
await authz.allow({
who: { type: "user", id: "employee1" },
toBe: "editor",
onWhat: { type: "review", id: "cert1#strengths" },
});
// Check: Employee can now edit strengths
assert.ok(await authz.check({
who: { type: "user", id: "employee1" },
canThey: "edit",
onWhat: { type: "review", id: "cert1#strengths" },
})); // trueContributions are welcome! Please open an issue or submit a pull request.