WIP - permissions progress
This commit is contained in:
211
docs/SALESFORCE_AUTHORIZATION.md
Normal file
211
docs/SALESFORCE_AUTHORIZATION.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user