@objectql/plugin-security
v4.2.2
Published
Security plugin for ObjectQL - RBAC, Field-Level Security, and Row-Level Security enforcement
Maintainers
Readme
@objectql/plugin-security
Security plugin for ObjectQL - Comprehensive RBAC, Field-Level Security, and Row-Level Security enforcement.
Features
🔒 Role-Based Access Control (RBAC)
- Object-level permissions (create, read, update, delete, view_all, modify_all)
- Field-level permissions (read, update)
- Dynamic record-level rules with conditions
🎯 Row-Level Security (RLS)
- Automatic query filtering based on user permissions
- AST-level modifications for zero performance overhead
- Support for complex conditions (AND/OR logic)
🛡️ Field-Level Security (FLS)
- Automatic field masking for sensitive data
- Configurable mask formats (credit cards, emails, etc.)
- Role-based field visibility
⚡ Performance Optimized
- Pre-compilation of permission rules to bitmasks and lookup maps
- In-memory caching of permission checks
- Query trimming at AST level (before SQL generation)
📊 Audit Logging
- Track all permission checks and access attempts
- Configurable retention and alert thresholds
Installation
pnpm add @objectql/plugin-securityQuick Start
1. 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
},
{
role: 'manager',
condition: {
field: 'department_id',
operator: '=',
value: '$current_user.department_id'
}
}
]
},
// Record-level rules
record_rules: [
{
name: 'owner_can_edit',
priority: 10,
condition: {
field: 'owner_id',
operator: '=',
value: '$current_user.id'
},
permissions: {
read: true,
update: true,
delete: true
}
},
{
name: 'team_members_can_read',
priority: 5,
condition: {
field: 'team_id',
operator: '=',
value: '$current_user.team_id'
},
permissions: {
read: true
}
}
],
// Field masking
field_masking: {
ssn: {
mask_format: '***-**-{last4}',
visible_to: ['admin']
},
credit_card: {
mask_format: '****-****-****-{last4}',
visible_to: ['admin', 'finance']
}
}
};2. Register the Plugin
import { ObjectQLSecurityPlugin } from '@objectql/plugin-security';
import { createKernel } from '@objectstack/runtime';
const kernel = createKernel({
plugins: [
new ObjectQLSecurityPlugin({
enabled: true,
storageType: 'memory',
permissions: [
projectPermissions,
// ... other permission configurations
],
// Exemption list - skip security for these objects
exemptObjects: ['system_config', 'public_data'],
// Performance options
precompileRules: true,
enableCache: true,
cacheTTL: 60000, // 1 minute
// Behavior options
throwOnDenied: true,
enableAudit: true,
// Feature toggles
enableRowLevelSecurity: true,
enableFieldLevelSecurity: true
})
]
});3. Use with ObjectQL
// 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
// Try to create a record
await kernel.create('project', {
name: 'New Project',
owner_id: currentUser.id
});
// Permission check is automatically performedArchitecture
Component Overview
┌─────────────────────────────────────────────────┐
│ ObjectQLSecurityPlugin │
├─────────────────────────────────────────────────┤
│ │
│ ┌────────────────┐ ┌──────────────────┐ │
│ │ PermissionLoader│ │ PermissionGuard │ │
│ │ │ │ │ │
│ │ - Load configs │ │ - Check perms │ │
│ │ - Pre-compile │ │ - Cache results │ │
│ │ - Bitmasks │ │ - Audit logs │ │
│ └────────────────┘ └──────────────────┘ │
│ │
│ ┌────────────────┐ ┌──────────────────┐ │
│ │ QueryTrimmer │ │ FieldMasker │ │
│ │ │ │ │ │
│ │ - RLS filtering │ │ - FLS masking │ │
│ │ - AST mods │ │ - Field removal │ │
│ │ - Row isolation │ │ - Value masking │ │
│ └────────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────┘
▲ ▲
│ │
beforeQuery afterQuery
beforeMutationHooks Integration
The plugin registers three hooks:
- beforeQuery: Applies row-level security by modifying query filters
- beforeMutation: Checks permissions before create/update/delete operations
- afterQuery: Applies field-level security to query results
Advanced Usage
Custom Permission Storage
import { IPermissionStorage } from '@objectql/plugin-security';
class RedisPermissionStorage implements IPermissionStorage {
async load(objectName: string) {
// Load from Redis
}
async loadAll() {
// Load all from Redis
}
async reload() {
// Refresh cache
}
}
const plugin = new ObjectQLSecurityPlugin({
storageType: 'custom',
storage: new RedisPermissionStorage()
});Complex Conditions
const complexRule: RecordRule = {
name: 'senior_managers_all_projects',
priority: 20,
condition: {
type: 'complex',
expression: [
{ field: 'status', operator: '=', value: 'active' },
{ field: 'priority', operator: '>=', value: 'high' },
'and'
]
},
permissions: {
read: true,
update: true
}
};Formula-Based Conditions
const formulaRule: RecordRule = {
name: 'custom_access_logic',
condition: {
type: 'formula',
formula: 'record.created_by === user.id || user.roles.includes("admin")'
},
permissions: {
read: true
}
};Performance Considerations
Pre-compilation
Permission rules are pre-compiled at startup into:
- Bitmasks for quick permission checks (O(1))
- Lookup Maps for role-based access (O(1))
- Evaluator Functions for condition matching (optimized)
Caching
Permission check results are cached in-memory with configurable TTL:
{
enableCache: true,
cacheTTL: 60000 // 1 minute
}Query Optimization
The QueryTrimmer operates at the AST level, modifying the query before it's sent to the database. This means:
- No post-filtering overhead
- Database can use indexes efficiently
- Minimal memory usage
Security Best Practices
- Always define permissions explicitly - Don't rely on defaults
- Use exemptObjects sparingly - Only for truly public data
- Enable audit logging in production - Track permission violations
- Regular permission reviews - Audit who has access to what
- Principle of least privilege - Grant minimum required permissions
- Formula condition security - When using formula-based conditions:
- Only allow trusted administrators to define formulas
- Formulas are evaluated in a restricted context but still pose risks
- Consider using simple or complex conditions instead for better security
- For production, consider implementing a sandboxed evaluator (e.g., vm2, isolated-vm)
- Regularly audit formula definitions for security issues
Security Considerations for Formula Conditions
Formula conditions use the JavaScript Function constructor for evaluation. While the execution context is restricted, there are still potential security risks:
Current Implementation:
// Restricted context - only exposes record and user
const evalContext = {
record: context.record,
user: context.user,
$current_user: context.user
};Recommendations:
- Use formula conditions only for internal tools or trusted environments
- For production systems with untrusted users, use simple or complex conditions
- Consider implementing a custom expression parser or sandboxed evaluator
- Audit all formula definitions before deployment
- Implement rate limiting and monitoring for formula evaluation
Alternative Approaches:
// ✓ Preferred: Use simple conditions
{
field: 'status',
operator: '=',
value: 'active'
}
// ✓ Preferred: Use complex conditions
{
type: 'complex',
expression: [
{ field: 'status', operator: '=', value: 'active' },
{ field: 'owner_id', operator: '=', value: '$current_user.id' },
'and'
]
}
// ⚠️ Use with caution: Formula conditions
{
type: 'formula',
formula: 'record.status === "active" && record.owner_id === user.id'
}API Reference
See API Documentation for complete API reference.
License
MIT
