5.8 KiB
5.8 KiB
Salesforce-Style Authorization System
Overview
Implemented a comprehensive authorization system based on Salesforce's model with:
- Org-Wide Defaults (OWD) for record visibility
- Role-based permissions for object and field access
- Record sharing for granular access control
- CASL for flexible permission evaluation
Architecture
1. Org-Wide Defaults (OWD)
Controls baseline record visibility for each object:
private: Only owner can see recordspublic_read: Everyone can see, only owner can edit/deletepublic_read_write: Everyone can see and modify all records
2. Role-Based Object Permissions
Table: role_object_permissions
canCreate: Can create new recordscanRead: Can read records (subject to OWD)canEdit: Can edit records (subject to OWD)canDelete: Can delete records (subject to OWD)canViewAll: Override OWD to see ALL recordscanModifyAll: Override OWD to edit ALL records
3. Field-Level Security
Table: role_field_permissions
canRead: Can view field valuecanEdit: Can modify field value
4. Record Sharing
Table: record_shares
Grants specific users access to individual records with:
{
"canRead": boolean,
"canEdit": boolean,
"canDelete": boolean
}
Permission Evaluation Flow
1. Check role_object_permissions
├─ Does user have canCreate/Read/Edit/Delete?
│ └─ NO → Deny
│ └─ YES → Continue
│
2. Check canViewAll / canModifyAll
├─ Does user have special "all" permissions?
│ └─ YES → Grant access
│ └─ NO → Continue
│
3. Check OWD (orgWideDefault)
├─ public_read_write → Grant access
├─ public_read → Grant read, check ownership for write
└─ private → Check ownership or sharing
4. Check Ownership
├─ Is user the record owner?
│ └─ YES → Grant access
│ └─ NO → Continue
│
5. Check Record Shares
└─ Is record explicitly shared with user?
└─ Check accessLevel permissions
Field-Level Security
Fields are filtered after record access is granted:
- User queries records → Apply record-level scope
- System filters readable fields based on
role_field_permissions - User updates records → System filters editable fields
Key Features
Multiple Role Support
- Users can have multiple roles
- Permissions are unioned (any role grants = user has it)
- More flexible than Salesforce's single profile model
Active Share Detection
- Shares can expire (
expiresAt) - Shares can be revoked (
revokedAt) - Only active shares are evaluated
CASL Integration
- Dynamic ability building per request
- Condition-based rules
- Field-level permission support
Usage Example
// In a controller/service
constructor(
private authService: AuthorizationService,
private tenantDbService: TenantDatabaseService,
) {}
async getRecords(tenantId: string, objectApiName: string, userId: string) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
// Get user with roles
const user = await User.query(knex)
.findById(userId)
.withGraphFetched('[roles.[objectPermissions, fieldPermissions]]');
// Get object definition
const objectDef = await ObjectDefinition.query(knex)
.findOne({ apiName: objectApiName });
// Build query with authorization scope
let query = knex(objectApiName.toLowerCase());
query = await this.authService.applyScopeToQuery(
query,
objectDef,
user,
'read',
knex,
);
const records = await query;
// Get field definitions
const fields = await FieldDefinition.query(knex)
.where('objectDefinitionId', objectDef.id);
// Filter fields user can read
const filteredRecords = await Promise.all(
records.map(record =>
this.authService.filterReadableFields(record, fields, user)
)
);
return filteredRecords;
}
async updateRecord(tenantId: string, objectApiName: string, recordId: string, data: any, userId: string) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const user = await User.query(knex)
.findById(userId)
.withGraphFetched('[roles.[objectPermissions, fieldPermissions]]');
const objectDef = await ObjectDefinition.query(knex)
.findOne({ apiName: objectApiName });
// Get existing record
const record = await knex(objectApiName.toLowerCase())
.where({ id: recordId })
.first();
if (!record) {
throw new NotFoundException('Record not found');
}
// Check if user can update this record
await this.authService.assertCanPerformAction(
'update',
objectDef,
record,
user,
knex,
);
// Get field definitions
const fields = await FieldDefinition.query(knex)
.where('objectDefinitionId', objectDef.id);
// Filter to only editable fields
const editableData = await this.authService.filterEditableFields(
data,
fields,
user,
);
// Perform update
await knex(objectApiName.toLowerCase())
.where({ id: recordId })
.update(editableData);
return knex(objectApiName.toLowerCase())
.where({ id: recordId })
.first();
}
Migration
Run the migration to add authorization tables:
npm run knex migrate:latest
The migration creates:
orgWideDefaultcolumn inobject_definitionsrole_object_permissionstablerole_field_permissionstablerecord_sharestable
Next Steps
- Migrate existing data: Set default
orgWideDefaultvalues for existing objects - Create default roles: Create Admin, Standard User, etc. with appropriate permissions
- Update API endpoints: Integrate authorization service into all CRUD operations
- UI for permission management: Build admin interface to manage role permissions
- Sharing UI: Build interface for users to share records with others