β

ObjectQL v4.0 is currently in Beta.

ObjectStack LogoObjectQL
Server & Deployment

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-security

2. 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 performed

Implementation Status

FeatureStatusImplementation
Validation✅ Built-inAutomatic field/cross-field/state validation
Authentication⚠️ Application LayerImplement using middleware
Authorization/RBAC✅ Security Plugin@objectql/plugin-security
Row-Level Security✅ Security PluginAST-level query filtering
Field-Level Security✅ Security PluginAutomatic field masking
Permission Audit✅ Security PluginConfigurable audit logging
Data Audit Trail⚠️ Application LayerImplement 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

  1. Always validate user input - Use ObjectQL's validation system for data integrity
  2. Implement authentication - Verify user identity before processing requests
  3. Check permissions in hooks - Enforce authorization for all CRUD operations
  4. Use prepared statements - ObjectQL drivers use parameterized queries (SQL injection safe)
  5. Sanitize file uploads - Validate file types and sizes
  6. Log security events - Track authentication failures, permission denials
  7. Use HTTPS in production - Encrypt data in transit
  8. Rotate secrets - Regularly update JWT secrets, API keys
  9. Implement rate limiting - Prevent brute force attacks
  10. Test permissions - Write tests for all permission scenarios

❌ Don't

  1. Don't assume automatic permission enforcement - You must implement it
  2. Don't expose sensitive data - Filter fields in hooks
  3. Don't trust client input - Always validate server-side
  4. Don't hardcode credentials - Use environment variables
  5. Don't skip context validation - Always check ctx.user exists
  6. Don't expose internal errors - Return generic error messages to clients
  7. 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();

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

On this page