Abilities (CASL)
Glasswork provides utilities for building type-safe CASL abilities that integrate seamlessly with Prisma. Abilities define what actions users can perform on which resources.
Defining Abilities
Role-Based Configuration
Use defineRoleAbilities() for a declarative, role-based approach:
import { defineRoleAbilities } from 'glasswork/auth';
import type { Subjects } from '@casl/prisma';
import type { User, Project, Organization } from '@prisma/client';
// Define your subject types from Prisma models
type AppSubjects = Subjects<{
User: User;
Project: Project;
Organization: Organization;
}> | 'all';
type AppAction = 'create' | 'read' | 'update' | 'delete' | 'manage';
type AppRole = 'admin' | 'member' | 'viewer';
export const abilities = defineRoleAbilities<AppSubjects, AppAction, AppRole>({
admin: ({ can }) => {
can('manage', 'all'); // Full access to everything
},
member: ({ can, cannot, user }) => {
// Organization-scoped access
can('read', 'Organization', { id: user.tenantId });
can('manage', 'Project', { organizationId: user.tenantId });
cannot('delete', 'Project'); // Override: no delete even with manage
},
viewer: ({ can, user }) => {
can('read', 'Organization', { id: user.tenantId });
can('read', 'Project', { organizationId: user.tenantId });
},
});Factory Pattern
For more complex logic, use createAbilityFactory():
import { createAbilityFactory } from 'glasswork/auth';
type AppSubjects = 'Project' | 'Organization' | 'all';
type AppAction = 'create' | 'read' | 'update' | 'delete' | 'manage';
const defineAbility = createAbilityFactory<AppSubjects, AppAction>()(
(can, cannot, user) => {
switch (user.role) {
case 'admin':
can('manage', 'all');
break;
case 'member':
can('read', 'Project', { organizationId: user.tenantId });
can('create', 'Project', { organizationId: user.tenantId });
can('update', 'Project', { createdBy: user.id });
break;
default:
// Guest: no abilities
}
}
);
// Usage
const ability = defineAbility(user);Permission Checks
Using assertCan
Throw an exception if the user lacks permission:
import { assertCan, subject } from 'glasswork/auth';
// Check permission on a subject type
assertCan(ability, 'create', 'Project');
// Check permission on a specific resource
const project = await projectService.findById(id);
assertCan(ability, 'update', subject('Project', project));
// Custom error message
assertCan(ability, 'delete', subject('Project', project), 'Cannot delete this project');Using can
Check permission without throwing:
import { can, subject } from 'glasswork/auth';
if (can(ability, 'delete', subject('Project', project))) {
// Show delete button
}In Route Handlers
Use the authorize option for automatic enforcement:
router.post('/projects', ...route({
authorize: { action: 'create', subject: 'Project' },
body: CreateProjectSchema,
handler: async ({ body, ability, user }) => {
// Route won't execute unless user can create Project
return projectService.create(body, user!.id);
},
}));Or check manually in the handler:
handler: async ({ params, ability }) => {
const project = await projectService.findById(params.id);
if (!project) throw new NotFoundException('Project not found');
assertCan(ability, 'update', subject('Project', project));
return projectService.update(params.id, body);
}Prisma Integration
Filtering with accessibleBy
CASL's Prisma integration lets you filter database queries based on abilities:
import { accessibleBy } from '@casl/prisma';
import type { AppAbility } from './abilities';
export class ProjectService {
constructor(private prisma: PrismaClient) {}
async findAll(ability: AppAbility): Promise<Project[]> {
return this.prisma.project.findMany({
where: accessibleBy(ability).ofType('Project'),
orderBy: { createdAt: 'desc' },
});
}
}This automatically filters to only projects the user can access based on their abilities.
Tenant Isolation with RLS
accessibleBy translates abilities into Prisma query filters. It applies to the queries where you use it. For a database-enforced tenant boundary, combine it with Row Level Security: PostgreSQL then limits the query to the active tenant, while the CASL filter further limits records by the user's permissions.
For example, RLS can limit projects to the active tenant while CASL allows a member to update only projects they created. Keep route authorize and service assertCan checks for actions and specific resources. RLS does not enforce those application rules. Conversely, an admin ability such as can('manage', 'all') does not grant access to other tenants through RLS.
Use the same validated tenant membership for both the ability's user and the RLS context. Authentication middleware establishes that context before the request reaches application services.
Combining with Other Filters
async findByStatus(ability: AppAbility, status: string): Promise<Project[]> {
return this.prisma.project.findMany({
where: {
AND: [
accessibleBy(ability).ofType('Project'),
{ status },
],
},
});
}Type Safety
Inferring Ability Type
import { type InferAbility } from 'glasswork/auth';
// Extract ability type from your definition
type AppAbility = ReturnType<typeof abilities.for>;
// Or use InferAbility helper
type AppAbility = InferAbility<typeof defineAbility>;Type-Safe Subject Helper
The subject() helper ensures type safety when checking permissions on instances:
import { subject } from 'glasswork/auth';
// TypeScript ensures project matches Project model
const project: Project = await projectService.findById(id);
assertCan(ability, 'update', subject('Project', project));
// Compile error: 'Foo' is not a valid subject
assertCan(ability, 'update', subject('Foo', project)); // ❌ Type errorCommon Patterns
Multi-Tenancy
Restrict access to resources within a tenant:
member: ({ can, user }) => {
// User can only access resources in their organization
can('read', 'Project', { organizationId: user.tenantId });
can('read', 'User', { organizationId: user.tenantId });
can('read', 'Organization', { id: user.tenantId });
},Ownership-Based Access
Allow users to manage only their own resources:
member: ({ can, user }) => {
// Can read all projects in org
can('read', 'Project', { organizationId: user.tenantId });
// Can only update/delete own projects
can('update', 'Project', { createdBy: user.id });
can('delete', 'Project', { createdBy: user.id });
},Field-Level Permissions
Restrict which fields can be updated:
member: ({ can, user }) => {
// Can update name and description
can('update', 'Project', ['name', 'description'], {
organizationId: user.tenantId
});
// Cannot update status (admin only)
},Hierarchical Roles
Build role hierarchies where higher roles inherit lower role permissions:
const basePermissions = (can, user) => {
can('read', 'Project', { organizationId: user.tenantId });
};
const memberPermissions = (can, user) => {
basePermissions(can, user);
can('create', 'Project', { organizationId: user.tenantId });
};
const adminPermissions = (can, user) => {
memberPermissions(can, user);
can('manage', 'Project', { organizationId: user.tenantId });
};
export const abilities = defineRoleAbilities({
admin: ({ can, user }) => adminPermissions(can, user),
member: ({ can, user }) => memberPermissions(can, user),
viewer: ({ can, user }) => basePermissions(can, user),
});Testing Abilities
Test abilities in isolation:
import { abilities } from './abilities';
import { subject } from 'glasswork/auth';
describe('abilities', () => {
describe('admin', () => {
const ability = abilities.forRole('admin');
it('can manage all resources', () => {
expect(ability.can('manage', 'all')).toBe(true);
expect(ability.can('delete', 'Project')).toBe(true);
});
});
describe('member', () => {
const ability = abilities.forRole('member', {
id: 'user-1',
tenantId: 'org-1',
});
it('can read projects in their organization', () => {
expect(ability.can('read', subject('Project', {
organizationId: 'org-1',
}))).toBe(true);
});
it('cannot read projects in other organizations', () => {
expect(ability.can('read', subject('Project', {
organizationId: 'org-2',
}))).toBe(false);
});
it('can only update projects they created', () => {
expect(ability.can('update', subject('Project', {
organizationId: 'org-1',
createdBy: 'user-1',
}))).toBe(true);
expect(ability.can('update', subject('Project', {
organizationId: 'org-1',
createdBy: 'user-2',
}))).toBe(false);
});
});
});Next Steps
- Middleware - Configure auth middleware with abilities
- Row Level Security - Add PostgreSQL tenant isolation alongside CASL
- Testing - Testing patterns for auth flows
