Security & Permissions
Security best practices and comprehensive RBAC implementation with @objectql/plugin-security
Security Model
ObjectQL now provides comprehensive security through the @objectql/plugin-security package, offering:
- ✅ Role-Based Access Control (RBAC) - Object-level and field-level permissions
- ✅ Row-Level Security (RLS) - Automatic query filtering at AST level
- ✅ Field-Level Security (FLS) - Field masking and visibility control
- ✅ Audit Logging - Track permission checks and access attempts
- ✅ Pre-compiled Permissions - O(1) permission lookups with bitmasks
🎉 New in 4.0+: The security plugin provides production-ready permission enforcement out of the box!
Quick Start with Security Plugin
1. Installation
pnpm add @objectql/plugin-security2. Define Permission Configuration
import { PermissionConfig } from '@objectql/plugin-security';
const projectPermissions: PermissionConfig = {
name: 'project_permissions',
object: 'project',
// Object-level permissions
object_permissions: {
create: ['admin', 'manager'],
read: ['admin', 'manager', 'user'],
update: ['admin', 'manager'],
delete: ['admin'],
view_all: ['admin'],
modify_all: ['admin']
},
// Field-level security
field_permissions: {
budget: {
read: ['admin', 'manager'],
update: ['admin']
},
internal_notes: {
read: ['admin'],
update: ['admin']
}
},
// Row-level security
row_level_security: {
enabled: true,
default_rule: {
field: 'owner_id',
operator: '=',
value: '$current_user.id'
},
exceptions: [
{
role: 'admin',
bypass: true
}
]
}
};3. Register the Plugin
import { ObjectQLSecurityPlugin } from '@objectql/plugin-security';
import { createKernel } from '@objectstack/runtime';
const kernel = createKernel({
plugins: [
new ObjectQLSecurityPlugin({
enabled: true,
permissions: [projectPermissions],
precompileRules: true,
enableCache: true,
throwOnDenied: true,
enableAudit: true
})
]
});4. Security is Automatic
// Security is automatically applied to all queries and mutations
const projects = await kernel.find('project', {
filters: { status: 'active' }
});
// Results are automatically filtered by RLS and FLS
await kernel.create('project', {
name: 'New Project',
owner_id: currentUser.id
});
// Permission check is automatically performedImplementation Status
| Feature | Status | Implementation |
|---|---|---|
| Validation | ✅ Built-in | Automatic field/cross-field/state validation |
| Authentication | ⚠️ Application Layer | Implement using middleware |
| Authorization/RBAC | ✅ Security Plugin | @objectql/plugin-security |
| Row-Level Security | ✅ Security Plugin | AST-level query filtering |
| Field-Level Security | ✅ Security Plugin | Automatic field masking |
| Permission Audit | ✅ Security Plugin | Configurable audit logging |
| Data Audit Trail | ⚠️ Application Layer | Implement using after* hooks |
Security Patterns
The security plugin provides production-ready implementations for most security needs. However, you can also implement custom patterns using hooks.
Security Plugin Features
For complete documentation, see:
Key features:
- Pre-compiled Permissions: Rules compiled to bitmasks at startup (O(1) checks)
- AST-level RLS: Query filtering before SQL generation (zero overhead)
- Configurable Storage: Memory, Redis, Database, or custom backends
- Audit Logging: Track all permission checks with retention policies
- Formula Conditions: Support for complex permission logic
Alternative: Custom Hook-Based Patterns
If you need custom security logic beyond the plugin, you can implement it using hooks:
1. Authentication (Who Are You?)
Implement authentication in your server middleware layer:
import express from 'express';
import { ObjectQL } from '@objectql/core';
import jwt from 'jsonwebtoken';
const app = express();
const objectql = new ObjectQL({ /* config */ });
// Authentication Middleware
app.use('/api', async (req, res, next) => {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Store user info for ObjectQL context
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
});
// Create ObjectQL context with authenticated user
app.use('/api/objectql', (req, res) => {
const ctx = objectql.createContext({
userId: req.user.id,
user: req.user,
// Add any other context info
});
// Handle request with authenticated context
// ...
});2. Authorization (What Can You Do?)
Implement authorization using hooks to check permissions before operations:
Pattern A: Check Permissions in Hooks
// project.hook.ts
import { ObjectHookDefinition } from '@objectql/types';
const hooks: ObjectHookDefinition = {
beforeCreate: async (ctx) => {
// Only managers can create projects
if (!ctx.user?.roles?.includes('manager')) {
throw new Error('Unauthorized: Only managers can create projects');
}
},
beforeUpdate: async (ctx) => {
const project = ctx.previousData;
// Only owner or admin can update
if (project.owner !== ctx.user.id && !ctx.user.isAdmin) {
throw new Error('Unauthorized: You can only update your own projects');
}
},
beforeDelete: async (ctx) => {
// Only admins can delete
if (!ctx.user?.isAdmin) {
throw new Error('Unauthorized: Only admins can delete projects');
}
}
};
export default hooks;Pattern B: Permission Helper Function
// utils/permissions.ts
export class PermissionChecker {
constructor(private user: any) {}
can(action: string, resource: string, record?: any): boolean {
// Implement your RBAC logic here
const rolePermissions = {
admin: ['*'],
manager: ['create:project', 'update:project', 'read:*'],
user: ['read:*', 'update:own']
};
const userRole = this.user.role || 'user';
const permissions = rolePermissions[userRole] || [];
// Check wildcard
if (permissions.includes('*')) return true;
if (permissions.includes(`${action}:*`)) return true;
// Check specific permission
if (permissions.includes(`${action}:${resource}`)) {
// Additional check for "own" resources
if (record && permissions.includes('update:own')) {
return record.owner === this.user.id;
}
return true;
}
return false;
}
assertCan(action: string, resource: string, record?: any): void {
if (!this.can(action, resource, record)) {
throw new Error(`Unauthorized: Cannot ${action} ${resource}`);
}
}
}
// project.hook.ts
import { PermissionChecker } from './utils/permissions';
const hooks: ObjectHookDefinition = {
beforeCreate: async (ctx) => {
const permissions = new PermissionChecker(ctx.user);
permissions.assertCan('create', 'project');
},
beforeUpdate: async (ctx) => {
const permissions = new PermissionChecker(ctx.user);
permissions.assertCan('update', 'project', ctx.previousData);
}
};3. Row-Level Security (Filter Data by User)
Implement row-level security by injecting filters in beforeFind hooks:
// project.hook.ts
const hooks: ObjectHookDefinition = {
beforeFind: async (ctx) => {
// Non-admins can only see their own projects
if (!ctx.user?.isAdmin) {
// Inject filter to limit results
if (!ctx.query.filters) {
ctx.query.filters = [];
}
// Add owner filter
ctx.query.filters.push(['owner', '=', ctx.user.id]);
}
},
beforeCount: async (ctx) => {
// Same logic for count operations
if (!ctx.user?.isAdmin) {
if (!ctx.query.filters) {
ctx.query.filters = [];
}
ctx.query.filters.push(['owner', '=', ctx.user.id]);
}
}
};4. Field-Level Security (Hide Sensitive Fields)
Remove sensitive fields from results in afterFind hooks:
// user.hook.ts
const hooks: ObjectHookDefinition = {
afterFind: async (ctx) => {
// Hide sensitive fields from non-admins
if (!ctx.user?.isAdmin) {
ctx.result = ctx.result.map(record => {
const { password, social_security_number, salary, ...safeRecord } = record;
return safeRecord;
});
}
},
afterFindOne: async (ctx) => {
// Same for single record
if (!ctx.user?.isAdmin && ctx.result) {
const { password, social_security_number, salary, ...safeRecord } = ctx.result;
ctx.result = safeRecord;
}
}
};5. Audit Logging
Track all changes using after* hooks:
// Global audit hook (app.ts)
app.on('after:create', '*', async (ctx) => {
await ctx.object('audit_log').create({
action: 'create',
object_name: ctx.objectName,
record_id: ctx.result._id,
user_id: ctx.user?.id,
timestamp: new Date(),
data: ctx.data
});
});
app.on('after:update', '*', async (ctx) => {
await ctx.object('audit_log').create({
action: 'update',
object_name: ctx.objectName,
record_id: ctx.id,
user_id: ctx.user?.id,
timestamp: new Date(),
changes: {
before: ctx.previousData,
after: ctx.result
}
});
});
app.on('after:delete', '*', async (ctx) => {
await ctx.object('audit_log').create({
action: 'delete',
object_name: ctx.objectName,
record_id: ctx.id,
user_id: ctx.user?.id,
timestamp: new Date(),
deleted_data: ctx.previousData
});
});Multi-Tenancy
Implement tenant isolation using hooks:
// Global tenant filter
app.on('before:find', '*', async (ctx) => {
// Skip for system operations
if (ctx.isSystem) return;
// Inject tenant filter
const tenantId = ctx.user?.tenantId;
if (!tenantId) {
throw new Error('No tenant context');
}
if (!ctx.query.filters) {
ctx.query.filters = [];
}
ctx.query.filters.push(['tenant_id', '=', tenantId]);
});
app.on('before:create', '*', async (ctx) => {
// Auto-set tenant on create
const tenantId = ctx.user?.tenantId;
if (!tenantId) {
throw new Error('No tenant context');
}
ctx.data.tenant_id = tenantId;
});Best Practices
✅ Do
- Always validate user input - Use ObjectQL's validation system for data integrity
- Implement authentication - Verify user identity before processing requests
- Check permissions in hooks - Enforce authorization for all CRUD operations
- Use prepared statements - ObjectQL drivers use parameterized queries (SQL injection safe)
- Sanitize file uploads - Validate file types and sizes
- Log security events - Track authentication failures, permission denials
- Use HTTPS in production - Encrypt data in transit
- Rotate secrets - Regularly update JWT secrets, API keys
- Implement rate limiting - Prevent brute force attacks
- Test permissions - Write tests for all permission scenarios
❌ Don't
- Don't assume automatic permission enforcement - You must implement it
- Don't expose sensitive data - Filter fields in hooks
- Don't trust client input - Always validate server-side
- Don't hardcode credentials - Use environment variables
- Don't skip context validation - Always check ctx.user exists
- Don't expose internal errors - Return generic error messages to clients
- Don't allow arbitrary code execution - Sandbox formulas (already done by ObjectQL)
Example: Complete Security Setup
// app.ts
import { ObjectQL } from '@objectql/core';
import { SqlDriver } from '@objectql/driver-sql';
const app = new ObjectQL({
datasources: {
default: new SqlDriver({ /* config */ })
}
});
// 1. Row-Level Security
app.on('before:find', '*', async (ctx) => {
if (ctx.isSystem) return;
const tenantId = ctx.user?.tenantId;
if (tenantId) {
if (!ctx.query.filters) ctx.query.filters = [];
ctx.query.filters.push(['tenant_id', '=', tenantId]);
}
});
// 2. Permission Checks
app.on('before:create', 'project', async (ctx) => {
if (!canCreate(ctx.user, 'project')) {
throw new Error('Unauthorized');
}
});
app.on('before:update', 'project', async (ctx) => {
if (!canUpdate(ctx.user, 'project', ctx.previousData)) {
throw new Error('Unauthorized');
}
});
// 3. Audit Logging
app.on('after:create', '*', async (ctx) => {
await auditLog(ctx, 'create');
});
// 4. Field Masking
app.on('after:find', 'user', async (ctx) => {
ctx.result = ctx.result.map(maskSensitiveFields);
});
await app.init();Related Documentation
- Security Plugin Source - Plugin source code and README
- Hooks - Event lifecycle and hook patterns
- Actions - Custom RPC operations
- Server Integration - Express/Next.js setup
- API Reference - Complete API documentation
Migration Guide
If you previously implemented custom permission checks using hooks, you can migrate to the security plugin:
Before (Hook-based)
app.on('beforeFind', 'project', async (ctx) => {
if (!ctx.user?.isAdmin) {
ctx.query.filters.push(['owner', '=', ctx.user.id]);
}
});After (Security Plugin)
const projectPermissions: PermissionConfig = {
name: 'project_permissions',
object: 'project',
row_level_security: {
enabled: true,
default_rule: {
field: 'owner',
operator: '=',
value: '$current_user.id'
},
exceptions: [{ role: 'admin', bypass: true }]
}
};Benefits of migration:
- ✅ Pre-compiled permissions (faster)
- ✅ Centralized configuration
- ✅ Built-in audit logging
- ✅ Field-level security included
- ✅ Less code to maintain