| title | Security & Access Control |
|---|---|
| description | Comprehensive security model - ACL, field-level security, row-level permissions, and data isolation |
import { Shield, Lock, Users, Eye } from 'lucide-react';
ObjectQL implements a multi-layered security model that enforces access control at the data layer, before queries execute. Security is declarative—defined in configuration, not scattered in application code.
Traditional approach:
// Security in application code (easily bypassed)
app.get('/api/accounts', (req, res) => {
// Developer must remember to check permissions
if (!req.user.hasPermission('read_account')) {
return res.status(403).json({ error: 'Forbidden' });
}
// Easy to forget row-level filtering
const accounts = await db.query('SELECT * FROM account');
res.json(accounts);
});ObjectQL approach:
# Security as configuration (enforced automatically)
name: account
permissions:
read:
- role: sales_rep
filters:
- ['owner_id', '=', '{{CURRENT_USER_ID}}']// Security enforced automatically
const accounts = await ObjectQL.query({ object: 'account' });
// WHERE owner_id = current_user_id (injected by ObjectQL)} title="Object-Level Security (ACL)" description="Who can Create, Read, Update, Delete which objects" /> } title="Field-Level Security" description="Control access to individual fields (e.g., hide salary from non-managers)" /> } title="Row-Level Security" description="Filter which records users can see (e.g., see only own accounts)" /> } title="Data Masking" description="Obfuscate sensitive data (e.g., show last 4 digits of SSN)" />
# account.object.yml
name: account
permissions:
create:
- role: sales_rep
- role: account_manager
read:
- role: sales_rep
- role: account_manager
- role: executive
update:
- role: account_manager
delete:
- role: adminBehavior:
- Sales Rep: Can create and read accounts
- Account Manager: Can create, read, and update accounts
- Executive: Can read accounts (read-only)
- Admin: Full access (CRUD)
User requests: GET /api/account/123
↓
1. Check Object-Level Permission
✓ User has 'read' on 'account'?
↓
2. Apply Row-Level Filters
✓ User can see record 123?
↓
3. Apply Field-Level Security
✓ Remove restricted fields
↓
Return filtered result
# Only see records you own
name: opportunity
permissions:
read:
- role: sales_rep
filters:
- ['owner_id', '=', '{{CURRENT_USER_ID}}']Runtime behavior:
// User queries opportunities
const opportunities = await ObjectQL.query({ object: 'opportunity' });
// ObjectQL injects filter automatically:
// WHERE owner_id = 'user_123'# See records owned by your team
name: account
permissions:
read:
- role: sales_rep
filters:
- ['owner.team_id', '=', '{{CURRENT_USER.team_id}}']# See accounts in your territory
name: account
permissions:
read:
- role: sales_rep
filters:
- ['territory_id', 'in', '{{CURRENT_USER.territory_ids}}']# Managers see their records + subordinates' records
name: opportunity
permissions:
read:
- role: sales_manager
filters:
- ['or',
['owner_id', '=', '{{CURRENT_USER_ID}}'],
['owner.manager_id', '=', '{{CURRENT_USER_ID}}']
]# Strict tenant isolation
name: customer
tenant_scope: true # Auto-inject tenant filter
permissions:
read:
- role: user
filters:
- ['tenant_id', '=', '{{CURRENT_TENANT_ID}}']Guarantee: User in Tenant A cannot access Tenant B's data, even with direct API calls or SQL injection attempts.
# Different filters for different roles
name: account
permissions:
read:
# Sales reps: Own accounts only
- role: sales_rep
filters:
- ['owner_id', '=', '{{CURRENT_USER_ID}}']
# Managers: Team accounts
- role: manager
filters:
- ['owner.team_id', '=', '{{CURRENT_USER.team_id}}']
# Executives: All accounts
- role: executive
filters: [] # No restrictionsname: user
fields:
salary:
type: currency
label: Salary
visibility:
read:
- role: hr_manager
- role: executive
update:
- role: hr_manager
ssn:
type: text
label: Social Security Number
visibility:
read:
- role: hr_admin
update:
- role: hr_adminBehavior:
// Sales manager queries user
const user = await ObjectQL.findOne('user', '123');
// Result (salary/ssn stripped):
// {
// id: '123',
// name: 'John Doe',
// email: 'john@example.com'
// // salary: REMOVED
// // ssn: REMOVED
// }
// HR manager queries same user
const user = await ObjectQL.findOne('user', '123');
// Result (salary visible):
// {
// id: '123',
// name: 'John Doe',
// email: 'john@example.com',
// salary: { amount: 120000, currency: 'USD' }
// // ssn: STILL REMOVED (not hr_admin)
// }fields:
field_name:
visibility:
read: [role1, role2] # Who can see the field
update: [role1] # Who can modify the field
create: [role1, role2] # Who can set on creationfields:
commission_rate:
type: percent
label: Commission Rate
visibility:
read: [sales_manager, hr_admin]
total_commission:
type: formula
formula: "total_sales * commission_rate"
# Visibility inherited from commission_rate
# Users who can't see commission_rate can't see thisfields:
ssn:
type: text
label: SSN
masking:
mode: partial
visible_chars: 4
position: end
mask_char: '*'Display:
- Actual value:
123-45-6789 - Masked value:
***-**-6789
fields:
credit_card:
type: text
label: Credit Card
masking:
mode: full
mask_char: '*'Display:
- Actual value:
4111111111111111 - Masked value:
****************
fields:
salary:
type: currency
label: Salary
masking:
# Show full value to HR, masked to others
roles_with_access: [hr_manager, hr_admin]
mode: partial
mask_char: 'X'fields:
email:
type: email
label: Email
masking:
mode: hash
algorithm: sha256
# Returns: 5d41402abc4b2a76b9719d911017c592Grant additional access beyond base permissions:
name: account
permissions:
read:
- role: sales_rep
filters: [['owner_id', '=', '{{CURRENT_USER_ID}}']]
sharing_rules:
# Share enterprise accounts with all sales reps
- name: share_enterprise_accounts
criteria:
- ['account_type', '=', 'Enterprise']
access_level: read
shared_with:
- role: sales_repEffect: Sales reps see:
- Their own accounts (base permission)
- All enterprise accounts (sharing rule)
# Manual sharing via junction object
name: account_share
fields:
account_id:
type: lookup
reference_to: account
user_id:
type: lookup
reference_to: user
access_level:
type: select
options: [read, write]Query with sharing:
const accounts = await ObjectQL.query({
object: 'account',
// User sees:
// 1. Own accounts (owner_id = user)
// 2. Shared accounts (via account_share)
});permissions:
read:
- role: contractor
filters:
- ['contract_start', '<=', '{{TODAY()}}']
- ['contract_end', '>=', '{{TODAY()}}']Effect: Contractors only see data during contract period.
permissions:
read:
- role: user
ip_whitelist:
- 192.168.1.0/24
- 10.0.0.0/8permissions:
update:
- role: user
require_mfa: true # Require multi-factor authentication
allowed_devices: [desktop, mobile_app]
# Block: web_browser on mobileWhen multiple permission rules match:
-
Most specific wins
- Field-level > Object-level > Global
-
Deny overrides allow
- Explicit deny > Explicit allow
-
Role hierarchy
- Admin > Manager > User
Example:
# Global default: No access
default_access: none
# Object level: Sales can read
permissions:
read: [sales_rep]
# Field level: Salary hidden
fields:
salary:
visibility:
read: [hr_manager]
# Result for sales_rep:
# - Can read account (object-level)
# - Cannot see salary (field-level override)filters:
# Current user
- ['owner_id', '=', '{{CURRENT_USER_ID}}']
# Current user's team
- ['team_id', '=', '{{CURRENT_USER.team_id}}']
# Current tenant
- ['tenant_id', '=', '{{CURRENT_TENANT_ID}}']
# Current date/time
- ['created_at', '>', '{{TODAY() - 30}}']
# User attributes
- ['territory', 'in', '{{CURRENT_USER.territories}}']Track all data access:
name: account
enable:
audit: true # Enable audit logging
audit_fields: [owner_id, annual_revenue, status]Audit log captures:
- Who: User ID, role, IP address
- What: Object, record ID, fields changed
- When: Timestamp
- How: Old value → New value
Example log:
{
"timestamp": "2024-01-15T14:30:00Z",
"user_id": "user_123",
"user_role": "sales_rep",
"action": "update",
"object": "account",
"record_id": "acc_456",
"changes": {
"annual_revenue": {
"old": 1000000,
"new": 1500000
}
},
"ip_address": "192.168.1.100"
}Log all query attempts:
name: account
enable:
access_log: trueCaptures:
- Failed permission checks (security violations)
- Successful queries (for compliance)
- Export operations (GDPR requirements)
fields:
ssn:
type: text
label: SSN
encrypt: true
encryption_key: ssn_master_keyStorage:
- Database stores encrypted value
- Application decrypts on read (if user has permission)
- Encryption key stored in secure vault (not in database)
All ObjectQL communication uses TLS 1.3:
- API requests: HTTPS
- Database connections: SSL/TLS
- Internal services: mTLS (mutual TLS)
fields:
credit_card:
type: text
encrypt: true
encryption_algorithm: AES-256-GCM
key_rotation: 90 # Rotate key every 90 daysname: customer
enable:
gdpr_erasure: true
fields:
email:
pii: true # Mark as personally identifiable
name:
pii: trueErasure request:
await ObjectQL.gdprErase('customer', 'customer_123');
// Result:
// - PII fields set to null
// - Audit log preserved (who requested, when)
// - Related records handled per cascade rulesname: patient
compliance: hipaa
fields:
medical_record_number:
type: text
phi: true # Protected Health Information
encrypt: true
audit: trueAutomatic enforcement:
- Encryption at rest
- Access logging
- Minimum necessary principle (field-level security)
- Audit trail
name: financial_record
compliance: soc2
permissions:
read:
- role: accountant
require_mfa: true
session_timeout: 900 # 15 minutes
ip_whitelist: [corporate_network]// Check if user can access before querying
const canAccess = await ObjectQL.checkPermission({
user: currentUser,
action: 'read',
object: 'account',
record_id: 'acc_123'
});
if (!canAccess) {
// Show error message, don't attempt query
}// In tests
await expect(
ObjectQL.query({
object: 'salary_data',
user: salesRepUser
})
).toThrow(PermissionError);ObjectQL provides security test helpers:
// Test all permission combinations
await ObjectQL.testSuite.runSecurityTests({
objects: ['account', 'opportunity', 'contact'],
users: [admin, manager, salesRep, guest],
scenarios: ['create', 'read', 'update', 'delete']
});
// Report:
// ✓ Admin: Full access to all objects
// ✓ Manager: Read access to team accounts
// ✗ Sales Rep: SECURITY ISSUE - Can read salary field
// ✓ Guest: No access (as expected)name: customer
tenant_scope: true
permissions:
read:
- role: user
filters:
- ['tenant_id', '=', '{{CURRENT_TENANT_ID}}']
- ['or',
['owner_id', '=', '{{CURRENT_USER_ID}}'],
['shared_with', 'contains', '{{CURRENT_USER_ID}}']
]name: patient_record
compliance: hipaa
permissions:
read:
# Doctors: Own patients
- role: doctor
filters:
- ['assigned_doctor_id', '=', '{{CURRENT_USER_ID}}']
# Nurses: Same department
- role: nurse
filters:
- ['department_id', '=', '{{CURRENT_USER.department_id}}']
# Patient: Own record only
- role: patient
filters:
- ['patient_id', '=', '{{CURRENT_USER.patient_id}}']
fields:
diagnosis:
visibility:
read: [doctor, nurse] # Hidden from patient
treatment_notes:
visibility:
read: [doctor] # Hidden from nurse and patientname: transaction
compliance: pci_dss
permissions:
read:
- role: account_holder
filters:
- ['account.owner_id', '=', '{{CURRENT_USER_ID}}']
require_mfa: true
update:
- role: admin
require_approval: true # Transactions require 2-person approval
fields:
card_number:
type: text
encrypt: true
masking:
mode: partial
visible_chars: 4
position: end
cvv:
type: text
encrypt: true
masking:
mode: full# ❌ Bad: Grant broad access by default
permissions:
read: [user] # All users can read everything
# ✅ Good: Restrict by default, grant explicitly
permissions:
read:
- role: account_owner
filters: [['owner_id', '=', '{{CURRENT_USER_ID}}']]# Multiple security layers
permissions:
read:
- role: user
filters: [['owner_id', '=', '{{CURRENT_USER_ID}}']]
require_mfa: true
ip_whitelist: [corporate_network]
fields:
sensitive_data:
encrypt: true
masking: { mode: partial }
audit: true# Auto-inject security fields
name: project
fields:
owner_id:
type: lookup
reference_to: user
default_value: '{{CURRENT_USER_ID}}' # Auto-assign owner
tenant_id:
type: lookup
reference_to: tenant
default_value: '{{CURRENT_TENANT_ID}}' # Auto-assign tenant# Generate security report
objectstack security:audit
# Output:
# Objects with no permissions: 3
# Users with admin access: 5
# Unencrypted PII fields: 2
# Shared records: 42