Security plugin for ObjectQL - RBAC, Field-Level Security, and Row-Level Security enforcement
npm install @objectql/plugin-securitySecurity plugin for ObjectQL - Comprehensive RBAC, Field-Level Security, and Row-Level Security enforcement.
``bash`
pnpm add @objectql/plugin-security
`typescript
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']
}
}
};
`
`typescript
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
})
]
});
`
`typescript
// 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 performed
`
``
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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
beforeMutation
The plugin registers three hooks:
1. beforeQuery: Applies row-level security by modifying query filters
2. beforeMutation: Checks permissions before create/update/delete operations
3. afterQuery: Applies field-level security to query results
`typescript
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()
});
`
`typescript`
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
}
};
`typescript`
const formulaRule: RecordRule = {
name: 'custom_access_logic',
condition: {
type: 'formula',
formula: 'record.created_by === user.id || user.roles.includes("admin")'
},
permissions: {
read: true
}
};
typescript
{
enableCache: true,
cacheTTL: 60000 // 1 minute
}
`$3
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 usageSecurity Best Practices
1. Always define permissions explicitly - Don't rely on defaults
2. Use exemptObjects sparingly - Only for truly public data
3. Enable audit logging in production - Track permission violations
4. Regular permission reviews - Audit who has access to what
5. Principle of least privilege - Grant minimum required permissions
6. 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
$3
Formula conditions use the JavaScript
Function constructor for evaluation. While the execution context is restricted, there are still potential security risks:Current Implementation:
`typescript
// 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:
`typescript
// β 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'
}
``See API Documentation for complete API reference.
MIT