Files
neo/docs/SALESFORCE_AUTHORIZATION.md
2026-01-05 07:48:22 +01:00

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 records
  • public_read: Everyone can see, only owner can edit/delete
  • public_read_write: Everyone can see and modify all records

2. Role-Based Object Permissions

Table: role_object_permissions

  • canCreate: Can create new records
  • canRead: 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 records
  • canModifyAll: Override OWD to edit ALL records

3. Field-Level Security

Table: role_field_permissions

  • canRead: Can view field value
  • canEdit: 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:

  1. User queries records → Apply record-level scope
  2. System filters readable fields based on role_field_permissions
  3. 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:

  • orgWideDefault column in object_definitions
  • role_object_permissions table
  • role_field_permissions table
  • record_shares table

Next Steps

  1. Migrate existing data: Set default orgWideDefault values for existing objects
  2. Create default roles: Create Admin, Standard User, etc. with appropriate permissions
  3. Update API endpoints: Integrate authorization service into all CRUD operations
  4. UI for permission management: Build admin interface to manage role permissions
  5. Sharing UI: Build interface for users to share records with others