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

212 lines
5.8 KiB
Markdown

# 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:
```json
{
"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
```typescript
// 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:
```bash
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