Permissions
Shokupan provides a built-in permission system supporting Role-Based Access Control (RBAC), resource-based permissions, custom resolvers, and wildcard matching.
Quick Start
Section titled “Quick Start”import { Shokupan, PermissionPlugin } from 'shokupan';
const app = new Shokupan();
const permissions = new PermissionPlugin({ roles: [ { name: 'admin', description: 'Administrator with full access', permissions: [ { resource: '*', action: '*' } ] }, { name: 'editor', description: 'Can create and edit content', permissions: [ { resource: 'posts', action: 'create' }, { resource: 'posts', action: 'read' }, { resource: 'posts', action: 'update' }, { resource: 'posts', action: 'delete' } ] }, { name: 'viewer', description: 'Read-only access', permissions: [ { resource: 'posts', action: 'read' } ] } ]});
app.register(permissions);
app.get('/posts', permissions.requirePermission('posts', 'read'), (ctx) => { return ctx.json({ posts: [] }); });
await app.listen();Protecting Routes
Section titled “Protecting Routes”Require a Specific Permission
Section titled “Require a Specific Permission”app.get('/posts', permissions.requirePermission('posts', 'read'), (ctx) => { return ctx.json({ posts: [] }); });Require a Role
Section titled “Require a Role”app.get('/admin', permissions.requireRole('admin'), (ctx) => { return ctx.json({ message: 'Admin panel' }); });Require Any of Multiple Permissions
Section titled “Require Any of Multiple Permissions”app.get('/editor', permissions.requireAnyPermission( { resource: 'posts', action: 'create' }, { resource: 'posts', action: 'update' } ), (ctx) => { return ctx.json({ message: 'Editor panel' }); });Require All Permissions
Section titled “Require All Permissions”app.get('/profile', permissions.requireAllPermissions( { resource: 'profile', action: 'read' }, { resource: 'profile', action: 'update' } ), (ctx) => { return ctx.json({ profile: {} }); });Configuration
Section titled “Configuration”PermissionConfig Options
Section titled “PermissionConfig Options”interface PermissionConfig { // Pre-defined roles roles?: Role[];
// Custom permission resolvers customResolvers?: Map<string, PermissionResolver>;
// Function to extract user permissions getUserPermissions?: (user: any, ctx: ShokupanContext) => Permission[] | Promise<Permission[]>;
// Function to extract user roles getUserRoles?: (user: any, ctx: ShokupanContext) => string[] | Promise<string[]>;
// Custom unauthorized handler onUnauthorized?: (ctx: ShokupanContext, check: PermissionCheck) => Response | Promise<Response>;
// Enable wildcard matching (default: true) enableWildcards?: boolean;
// Case-sensitive matching (default: false) caseSensitive?: boolean;}Role Inheritance
Section titled “Role Inheritance”Roles can inherit permissions from other roles:
const permissions = new PermissionPlugin({ roles: [ { name: 'viewer', permissions: [ { resource: 'posts', action: 'read' } ] }, { name: 'moderator', permissions: [ { resource: 'posts', action: 'update' }, { resource: 'posts', action: 'delete' } ], inherits: ['viewer'] // Inherits read permission } ]});Custom Permission Resolvers
Section titled “Custom Permission Resolvers”Implement custom logic for dynamic permissions. Resolvers are checked before static permissions.
// Resource-level resolverpermissions.addCustomResolver('posts', async (user, check, ctx) => { if (user.isAdmin) return true; return false;});
// Action-level resolverpermissions.addCustomResolver('posts:update', async (user, check, ctx) => { const postId = ctx.params.id;
const hasGeneralPermission = user.permissions?.some((p: any) => p.resource === 'posts' && p.action === 'update' );
if (hasGeneralPermission) return true;
// Check ownership via context if (check.context?.ownerId === user.id) { return true; }
return false;});
// Global resolver (fallback)permissions.addCustomResolver('*', async (user, check, ctx) => { return user.isSuperAdmin === true;});Context-Aware Permissions
Section titled “Context-Aware Permissions”Pass runtime context for conditional permission checks using custom resolvers:
permissions.addCustomResolver('posts:update', async (user, check, ctx) => { // check.context is passed from the middleware if (check.context?.ownerId === user.id) { return true; } return false;});
app.put('/posts/:id', async (ctx, next) => { const post = await getPost(ctx.params.id); // Attach ownership context for the permission check (ctx as any).checkContext = { ownerId: post.ownerId }; return next(); }, async (ctx, next) => { // Pass context at request time via a wrapper const user = (ctx as any).user; const hasPermission = await permissions.checkPermission(user, { resource: 'posts', action: 'update', context: (ctx as any).checkContext }, ctx);
if (!hasPermission) { return ctx.json({ error: 'Forbidden' }, 403); }
return next(); }, (ctx) => { return ctx.json({ message: 'Post updated' }); });Alternatively, check permissions directly in the route handler:
app.put('/posts/:id', async (ctx) => { const post = await getPost(ctx.params.id); const user = (ctx as any).user;
const canUpdate = await permissions.checkPermission(user, { resource: 'posts', action: 'update', context: { ownerId: post.ownerId } }, ctx);
if (!canUpdate) { return ctx.json({ error: 'Forbidden' }, 403); }
// Update post... return ctx.json({ message: 'Post updated' });});Integration with Auth Plugin
Section titled “Integration with Auth Plugin”The permission system integrates with the Auth plugin:
import { AuthPlugin, PermissionPlugin } from 'shokupan';
const auth = new AuthPlugin({ jwtSecret: process.env.JWT_SECRET!, onSuccess: async (user, ctx) => { if (user.email?.endsWith('@admin.com')) { user.roles = ['admin']; } else { user.roles = ['viewer']; }
user.permissions = ['profile:read', 'profile:update']; }, providers: { /* ... */ }});
const permissions = new PermissionPlugin({ getUserPermissions: async (user, ctx) => { if (user.permissions && Array.isArray(user.permissions)) { return user.permissions.map((p: any) => { if (typeof p === 'string') { const [resource, action] = p.split(':'); return { resource, action }; } return p; }); } return []; }, getUserRoles: async (user, ctx) => { return user.roles || []; }});
// Apply auth middleware globallyapp.use(auth.getMiddleware());
app.register(auth);app.register(permissions);Permission Format
Section titled “Permission Format”Role Definitions (Object Format)
Section titled “Role Definitions (Object Format)”Roles and their permissions are defined as objects:
{ resource: 'posts', action: 'read', conditions?: { ownerId: '123' }}User Permissions (String or Object Format)
Section titled “User Permissions (String or Object Format)”User-level permissions passed through getUserPermissions can be strings and will be parsed automatically:
// String format (parsed in getUserPermissions)'posts:read'
// Object format (passed through as-is){ resource: 'posts', action: 'read' }Wildcard Patterns
Section titled “Wildcard Patterns”When enableWildcards is true (default), you can use * and ? in patterns:
| Pattern | Matches |
|---|---|
* | Any value |
posts:* | Any action on posts |
*:read | read action on any resource |
doc? | doc1, doc2, docA, etc. |
{ name: 'admin', permissions: [ { resource: '*', action: '*' } // All resources, all actions ]}
{ name: 'posts-manager', permissions: [ { resource: 'posts', action: '*' } // All actions on posts ]}Built-in API Endpoints
Section titled “Built-in API Endpoints”The plugin exposes RESTful endpoints relative to its mount path:
GET /permissions/roles
Section titled “GET /permissions/roles”List all defined roles and their permissions.
curl http://localhost:3000/permissions/rolesGET /permissions/check
Section titled “GET /permissions/check”Check if the current user has a specific permission.
curl "http://localhost:3000/permissions/check?resource=posts&action=read" \ -H "Cookie: auth_token=..."GET /permissions/user
Section titled “GET /permissions/user”Get the current user’s permissions and roles.
curl http://localhost:3000/permissions/user \ -H "Cookie: auth_token=..."Custom Unauthorized Handler
Section titled “Custom Unauthorized Handler”Customize the response when permission checks fail:
const permissions = new PermissionPlugin({ onUnauthorized: async (ctx, check) => { return ctx.json({ error: 'Access Denied', message: `You don't have permission to ${check.action} ${check.resource}`, requiredPermission: check }, 403); }});Programmatic Permission Checks
Section titled “Programmatic Permission Checks”Check permissions directly in your handlers:
app.get('/conditional', async (ctx) => { const user = (ctx as any).user;
const canEdit = await permissions.checkPermission( user, { resource: 'posts', action: 'update' }, ctx );
return ctx.json({ message: 'Resource', canEdit });});Managing Roles Dynamically
Section titled “Managing Roles Dynamically”Add or remove roles at runtime:
// Add a new rolepermissions.addRole({ name: 'contributor', description: 'Can contribute content', permissions: [ { resource: 'posts', action: 'create' }, { resource: 'posts', action: 'read' } ]});
// Remove a rolepermissions.removeRole('contributor');
// Get role detailsconst role = permissions.getRole('admin');TypeScript Support
Section titled “TypeScript Support”Full TypeScript support with exported types:
import type { Permission, Role, PermissionCheck, PermissionResolver, PermissionConfig } from 'shokupan';
const role: Role = { name: 'admin', permissions: [ { resource: 'posts', action: 'read' } ]};Security Best Practices
Section titled “Security Best Practices”Next Steps
Section titled “Next Steps”- Authentication - OAuth2 support
- Sessions - Session management
- Rate Limiting - Protect permission endpoints