Handle permission violations with detailed error context, customizable messages, and utility methods for authorization failures.
Error class for permission violations with detailed context and customizable error messages.
/**
* Error class for permission violations
*/
class ForbiddenError<T extends AnyAbility = AnyAbility> extends Error {
/** The ability instance that determined the permission was forbidden */
readonly ability: T;
/** The action that was attempted */
readonly action: string;
/** The subject the action was attempted on */
readonly subject: any;
/** The specific field if field-level access was attempted */
readonly field?: string;
/** The detected type of the subject */
readonly subjectType: string;
/**
* Create a ForbiddenError instance
* @param ability - The ability instance
*/
constructor(ability: T);
}Usage Examples:
import { createMongoAbility, ForbiddenError } from "@casl/ability";
const ability = createMongoAbility([
{ action: 'read', subject: 'Post' },
{ action: 'update', subject: 'Post', conditions: { authorId: 'user123' } },
{ action: 'delete', subject: 'Post', inverted: true, conditions: { published: true } }
]);
// Manual error creation using from() method
const post = { __type: 'Post', id: 1, authorId: 'other', published: true };
try {
if (ability.cannot('update', post)) {
// Check stores context, then create error
const error = ForbiddenError.from(ability);
throw error;
}
} catch (error) {
if (error instanceof ForbiddenError) {
console.log(error.action); // Last checked action
console.log(error.subject); // Last checked subject
console.log(error.subjectType); // Detected subject type
console.log(error.ability === ability); // true
console.log(error.message); // Default message
}
}
// Direct constructor usage (requires setting properties manually)
try {
const error = new ForbiddenError(ability);
// Properties would need to be set based on last permission check
throw error;
} catch (error) {
if (error instanceof ForbiddenError) {
console.log(error.ability === ability); // true
}
}Static methods for creating ForbiddenError instances and configuring default messages.
/**
* Static methods for ForbiddenError creation and configuration
*/
interface ForbiddenErrorStatic {
/**
* Set the default error message template or generator function
* @param messageOrFn - Static message string or function to generate messages
*/
setDefaultMessage(messageOrFn: string | ErrorMessageFactory): void;
/**
* Create a ForbiddenError from an ability instance using the last failed check
* @param ability - Ability instance to create error from
* @returns New ForbiddenError instance
*/
from<U extends AnyAbility>(ability: U): ForbiddenError<U>;
}
/**
* Function type for generating custom error messages
*/
type ErrorMessageFactory = (error: {
action: string;
subject: any;
subjectType: string;
field?: string;
}) => string;Usage Examples:
import { createMongoAbility, ForbiddenError } from "@casl/ability";
// Set custom default message
ForbiddenError.setDefaultMessage(
({ action, subjectType, field }) => {
const fieldPart = field ? ` field "${field}" of` : '';
return `Access denied: cannot ${action}${fieldPart} ${subjectType}`;
}
);
// Set simple string message
ForbiddenError.setDefaultMessage('Insufficient permissions');
// Create error from ability (requires recent failed check)
const ability = createMongoAbility([
{ action: 'read', subject: 'Post' }
]);
const post = { __type: 'Post', id: 1 };
// This will store the failed check internally
const canDelete = ability.can('delete', post); // false
if (!canDelete) {
const error = ForbiddenError.from(ability);
console.log(error.action); // 'delete'
console.log(error.subject); // post object
console.log(error.message); // Custom message
}
// Custom message factory with context
ForbiddenError.setDefaultMessage(({ action, subjectType, field, subject }) => {
const context = [];
if (field) {
context.push(`field "${field}"`);
}
if (subject?.id) {
context.push(`${subjectType} ${subject.id}`);
} else {
context.push(subjectType);
}
const contextStr = context.join(' of ');
return `Permission denied: cannot ${action} ${contextStr}`;
});
const restrictedPost = { __type: 'Post', id: 42, title: 'Secret' };
ability.can('delete', restrictedPost); // false, stores context
const contextualError = ForbiddenError.from(ability);
console.log(contextualError.message); // "Permission denied: cannot delete Post 42"Methods for customizing error messages and handling permission checks with automatic error throwing.
/**
* Instance methods for ForbiddenError
*/
interface ForbiddenErrorInstance<T extends AnyAbility> {
/**
* Set a custom error message for this specific error instance
* @param message - Custom error message
* @returns This error instance for chaining
*/
setMessage(message: string): this;
/**
* Throw this error unless the specified action is allowed
* @param action - Action to check
* @param subject - Subject to check (optional)
* @param field - Field to check (optional)
* @throws ForbiddenError if action is not allowed
*/
throwUnlessCan(...args: Parameters<T['can']>): void;
/**
* Return this error unless the specified action is allowed
* @param action - Action to check
* @param subject - Subject to check (optional)
* @param field - Field to check (optional)
* @returns This error if action is not allowed, undefined if allowed
*/
unlessCan(...args: Parameters<T['can']>): this | undefined;
}Usage Examples:
import { createMongoAbility, ForbiddenError } from "@casl/ability";
const ability = createMongoAbility([
{ action: 'read', subject: 'Post' },
{ action: 'update', subject: 'Post', conditions: { authorId: 'user123' } }
]);
const post = { __type: 'Post', id: 1, authorId: 'other' };
// Custom message for specific error
const error = new ForbiddenError(ability, 'update', post)
.setMessage('You can only update your own posts');
console.log(error.message); // 'You can only update your own posts'
// Throw unless permission granted
try {
new ForbiddenError(ability, 'update', post)
.setMessage('Update not allowed for this post')
.throwUnlessCan('update', post);
} catch (thrownError) {
console.log(thrownError.message); // 'Update not allowed for this post'
}
// Conditional error return
const conditionalError = new ForbiddenError(ability, 'delete', post)
.setMessage('Delete operation not permitted')
.unlessCan('delete', post);
if (conditionalError) {
console.log('Delete is forbidden:', conditionalError.message);
} else {
console.log('Delete is allowed');
}
// Permission checking with automatic error throwing
function updatePost(postData: any, userId: string) {
const ability = createMongoAbility([
{ action: 'update', subject: 'Post', conditions: { authorId: userId } }
]);
// This will throw if user cannot update the post
ForbiddenError.from(ability)
.setMessage(`User ${userId} cannot update this post`)
.throwUnlessCan('update', postData);
// Update logic here - only reached if permission granted
console.log('Updating post:', postData.id);
}
// Usage
try {
updatePost({ __type: 'Post', id: 1, authorId: 'other' }, 'user123');
} catch (error) {
console.log(error.message); // Permission error
}
updatePost({ __type: 'Post', id: 2, authorId: 'user123' }, 'user123'); // SuccessCommon patterns for integrating error handling into application logic.
/**
* Utility patterns for error handling integration
*/
interface ErrorHandlingPatterns {
/** Middleware pattern for express-like frameworks */
middleware: (req: any, res: any, next: any) => void;
/** Guard pattern for method protection */
guard: <T>(action: string, subject: any, operation: () => T) => T;
/** Decorator pattern for class methods */
authorize: (action: string, subjectField?: string) => MethodDecorator;
}Usage Examples:
import { createMongoAbility, ForbiddenError } from "@casl/ability";
// Express middleware pattern
function createAuthMiddleware(getAbility: (req: any) => any) {
return (action: string, subject?: string) => {
return (req: any, res: any, next: any) => {
try {
const ability = getAbility(req);
const targetSubject = subject || req.params.resource;
ForbiddenError.from(ability)
.setMessage(`Access denied for ${action} on ${targetSubject}`)
.throwUnlessCan(action, targetSubject);
next();
} catch (error) {
if (error instanceof ForbiddenError) {
res.status(403).json({ error: error.message });
} else {
next(error);
}
}
};
};
}
// Usage with Express
// app.get('/posts/:id',
// createAuthMiddleware(getUserAbility)('read', 'Post'),
// getPostController
// );
// Guard pattern for business logic
function createPermissionGuard(ability: any) {
return <T>(action: string, subject: any, operation: () => T): T => {
ForbiddenError.from(ability)
.setMessage(`Operation "${action}" is not permitted`)
.throwUnlessCan(action, subject);
return operation();
};
}
// Usage
const ability = createMongoAbility([
{ action: 'delete', subject: 'Post', conditions: { authorId: 'user123' } }
]);
const guard = createPermissionGuard(ability);
try {
const result = guard('delete', { __type: 'Post', authorId: 'user123' }, () => {
// Protected operation
console.log('Deleting post...');
return { success: true };
});
console.log(result); // { success: true }
} catch (error) {
console.log('Access denied:', error.message);
}
// Service layer pattern
class PostService {
constructor(private ability: any) {}
async deletePost(postId: string) {
const post = await this.findPost(postId);
// Check permission before operation
if (this.ability.cannot('delete', post)) {
throw new ForbiddenError(this.ability, 'delete', post)
.setMessage(`Cannot delete post ${postId}: insufficient permissions`);
}
// Perform deletion
return this.performDelete(post);
}
async updatePost(postId: string, data: any) {
const post = await this.findPost(postId);
// Use throwUnlessCan for automatic error handling
new ForbiddenError(this.ability, 'update', post)
.setMessage(`Cannot update post ${postId}`)
.throwUnlessCan('update', post);
return this.performUpdate(post, data);
}
private async findPost(id: string) {
// Mock implementation
return { __type: 'Post', id, authorId: 'user123' };
}
private async performDelete(post: any) {
console.log('Deleting:', post.id);
return { deleted: true };
}
private async performUpdate(post: any, data: any) {
console.log('Updating:', post.id, data);
return { ...post, ...data };
}
}
// Usage
const service = new PostService(ability);
try {
await service.deletePost('post1');
await service.updatePost('post2', { title: 'New Title' });
} catch (error) {
if (error instanceof ForbiddenError) {
console.log('Permission error:', error.message);
}
}