The ObjectQL Security Plugin implements comprehensive security for the ObjectQL framework following the @objectstack/spec protocol. It provides Role-Based Access Control (RBAC), Field-Level Security (FLS), and Row-Level Security (RLS) with minimal performance impact.
Security is a cross-cutting concern, not business logic. The plugin uses hooks to inject security checks without modifying business code.
Implements the @objectstack/spec permission protocol, ensuring compatibility with the specification and enabling declarative security configuration.
Can be enabled or disabled via configuration without code changes. When disabled, there's zero performance impact.
- Pre-compilation: Converts permission rules to bitmasks and lookup maps at startup
- AST-Level Modifications: Modifies queries before SQL generation
- Caching: Stores permission check results in memory
┌─────────────────────────────────────────────────┐
│ ObjectQLSecurityPlugin │
│ (Main Plugin Entry) │
├─────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌───────────────────┐ │
│ │ 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
Responsibilities:
- Load permission configurations from various sources (memory, Redis, database, custom)
- Pre-compile permission rules into optimized data structures
- Provide fast access to compiled rules
Pre-compilation Process:
// Original Permission Rule
{
name: "owner_can_edit",
condition: {
field: "owner_id",
operator: "=",
value: "$current_user.id"
},
permissions: {
read: true,
update: true,
delete: true
}
}
// Compiled to:
{
ruleName: "owner_can_edit",
permissionBitmask: 0b111, // read | update | delete
roleLookup: new Map(),
evaluator: (context) => context.record.owner_id === context.user.id,
priority: 10
}Key Features:
- Supports multiple storage backends (memory, Redis, database, custom)
- Converts conditions to executable JavaScript functions
- Builds bitmasks for O(1) permission checks
- Handles complex conditions with AND/OR logic
Responsibilities:
- Execute permission checks for CRUD operations
- Cache permission check results
- Log audit trails
Permission Check Flow:
1. Check cache for existing result
├─ Hit → Return cached result
└─ Miss → Continue
2. Load permission configuration
└─ No config → Grant access
3. Check object-level permissions
├─ Has role → Continue
└─ No role → Deny
4. Check field-level permissions (if applicable)
├─ Has role → Continue
└─ No role → Deny
5. Check record-level rules (if applicable)
├─ Rule matches → Grant/Deny based on rule
└─ No match → Continue
6. Check row-level security
├─ Has bypass → Grant
└─ Apply filters → Grant (with filters)
7. Cache result and return
Caching Strategy:
- Cache key:
{userId}:{objectName}:{operation}:{recordId}:{field} - TTL: Configurable (default: 60 seconds)
- Auto-invalidation on permission config reload
Responsibilities:
- Apply row-level security to queries
- Convert permission conditions to query filters
- Optimize impossible queries
Query Modification Process:
// Original Query
{
filters: { status: 'active' }
}
// After RLS (for non-admin user)
{
filters: {
$and: [
{ status: 'active' },
{ owner_id: currentUser.id } // Added by RLS
]
}
}
// Generated SQL (example)
SELECT * FROM projects
WHERE status = 'active'
AND owner_id = '123' -- RLS filterKey Features:
- Works at AST level before SQL generation
- Zero runtime overhead (filtering at database level)
- Supports complex conditions (AND/OR)
- Detects impossible queries for early termination
Responsibilities:
- Remove unauthorized fields from results
- Mask sensitive field values
- Support various masking formats
Masking Formats:
| Format | Example Input | Example Output | Use Case |
|---|---|---|---|
**** |
"secret123" | "********" | General masking |
{last4} |
"1234567890123456" | "************3456" | Credit cards |
{first1}*** |
"John" | "J***" | Names |
***@***.*** |
"user@example.com" | "u**@e******.com" | Emails |
Field Removal Process:
// Original Record
{
id: "1",
name: "Project A",
budget: 100000, // Restricted to admin
owner_id: "123",
internal_notes: "Secret" // Restricted to admin
}
// After FLS (for non-admin user)
{
id: "1",
name: "Project A",
owner_id: "123"
// budget and internal_notes removed
}Purpose: Apply row-level security before database query
Flow:
- Extract user context and object name
- Check if object is exempt
- Apply RLS filters to query
- Apply record rule filters
- Check if query is impossible → skip database call
Purpose: Check permissions before create/update/delete
Flow:
- Extract user context and operation
- Check if object is exempt
- Check object-level permissions
- Check field-level permissions (for updates)
- Grant or deny operation
- Log audit entry
Purpose: Apply field-level security to results
Flow:
- Extract user context
- Check if object is exempt
- Remove unauthorized fields
- Mask sensitive values
- Return cleaned results
At Startup:
// Convert all permission rules to optimized structures
await loader.loadAll(); // Pre-compiles all rules
// Result: Bitmasks and compiled evaluators ready
// Permission checks become O(1) lookups instead of rule parsingCache Structure:
Map<string, PermissionCacheEntry> {
"user123:project:read::": {
result: { granted: true },
timestamp: 1642345678000
}
}Cache Hit Rate Optimization:
- Common operations cached longest
- Cache warming at startup
- Automatic invalidation on config reload
Why AST Level?
- Filters applied by database engine
- No post-query filtering needed
- Database can use indexes efficiently
- Memory usage stays constant
Example:
-- Without AST modification (slow)
SELECT * FROM projects; -- 1 million rows
-- Filter in application: 999,999 rows discarded
-- With AST modification (fast)
SELECT * FROM projects WHERE owner_id = '123'; -- 100 rows
-- Database uses index, returns only needed rows// ✓ Good: Explicit roles
roles: ['admin', 'manager', 'developer', 'viewer']
// ✗ Bad: Generic or missing roles
roles: ['user'] // Too broad// ✓ Good: Minimal necessary permissions
{
read: ['admin', 'manager', 'developer'],
update: ['admin', 'manager'],
delete: ['admin']
}
// ✗ Bad: Over-permissive
{
read: ['admin', 'manager', 'developer', 'viewer', 'guest'],
update: ['admin', 'manager', 'developer'],
delete: ['admin', 'manager']
}// ✓ Good: Default deny with explicit exceptions
row_level_security: {
enabled: true,
default_rule: {
field: 'owner_id',
operator: '=',
value: '$current_user.id'
},
exceptions: [
{ role: 'admin', bypass: true }
]
}
// ✗ Bad: Default allow
row_level_security: {
enabled: false // Everyone sees everything
}// ✓ Good: Enable for sensitive operations
{
enableAudit: true,
audit: {
events: ['access_denied', 'sensitive_field_access'],
retention_days: 90
}
}- Test each component in isolation
- Mock dependencies
- Cover edge cases
- Test plugin integration with ObjectQL
- Test hook execution
- Test end-to-end permission flows
- Measure pre-compilation time
- Measure permission check latency
- Measure cache hit rate
- Test privilege escalation attempts
- Test SQL injection in conditions
- Test cache poisoning
1. Permission Denied Unexpectedly
- Check user roles in context
- Verify permission configuration is loaded
- Check cache TTL (might be using stale permissions)
2. Slow Permission Checks
- Enable pre-compilation
- Enable caching
- Check cache hit rate
3. Fields Not Being Masked
- Check field names match exactly
- Verify user roles
- Ensure afterQuery hook is registered
4. Row-Level Security Not Working
- Verify RLS is enabled in config
- Check default rule syntax
- Ensure beforeQuery hook is registered
- Redis Storage - Implement Redis-backed permission storage
- Database Storage - Implement database-backed permission storage
- Permission Inheritance - Support role inheritance
- Dynamic Permissions - Support time-based permissions
- Permission UI - Admin UI for managing permissions
- Performance Monitoring - Built-in metrics and dashboards