624 lines
20 KiB
Markdown
624 lines
20 KiB
Markdown
# Role-Based Access Control (RBAC) System
|
||
|
||
## Executive Summary
|
||
|
||
This document describes the implementation of a comprehensive Role-Based Access Control (RBAC) system that addresses the critical database efficiency issues identified in the [Database Efficiency Analysis](../DATABASE_EFFICIENCY_ANALYSIS.md). The new system moves access control logic from Python to the database level, providing granular permissions while dramatically improving performance.
|
||
|
||
## Problem Statement
|
||
|
||
The current User Access Management (UAM) system has significant performance bottlenecks:
|
||
|
||
- **Full Table Scans**: Loading entire tables before filtering in Python
|
||
- **Memory Overhead**: Storing complete datasets in memory
|
||
- **Scalability Issues**: Performance degrades with data growth
|
||
- **Code Complexity**: Access control logic scattered across interfaces
|
||
|
||
## Solution Overview
|
||
|
||
The new RBAC system implements a **matrix-based access model** with database-level filtering, providing:
|
||
|
||
- **Granular Permissions**: Table and field-level access control
|
||
- **Role-Based Security**: Centralized permission management
|
||
- **Database Optimization**: Filtering at database level
|
||
- **Mandate Isolation**: Secure multi-tenant access
|
||
- **Performance**: 80-95% reduction in data transfer and memory usage
|
||
|
||
## System Architecture
|
||
|
||
### Core Components
|
||
|
||
1. **Role Management**: Define roles with mandate scope
|
||
2. **Access Rules**: Matrix of permissions per role/table/field
|
||
3. **Database Integration**: Native filtering in SQL queries
|
||
4. **Migration Strategy**: Seamless transition from current UAM
|
||
|
||
## Data Models
|
||
|
||
### Access Rule Model
|
||
|
||
```python
|
||
class AccessRule(BaseModel, ModelMixin):
|
||
"""Data model for access control rules"""
|
||
id: str = Field(
|
||
default_factory=lambda: str(uuid.uuid4()),
|
||
description="Unique ID of the access rule",
|
||
frontend_type="text",
|
||
frontend_readonly=True,
|
||
frontend_required=False
|
||
)
|
||
roleLabel: str = Field(
|
||
description="Role label this rule applies to",
|
||
frontend_type="select",
|
||
frontend_readonly=False,
|
||
frontend_required=True,
|
||
frontend_options=[
|
||
{"value": "sysadmin", "label": {"en": "System Administrator", "fr": "Administrateur système"}},
|
||
{"value": "admin", "label": {"en": "Administrator", "fr": "Administrateur"}},
|
||
{"value": "user", "label": {"en": "User", "fr": "Utilisateur"}},
|
||
{"value": "viewer", "label": {"en": "Viewer", "fr": "Observateur"}}
|
||
]
|
||
)
|
||
tableName: Optional[str] = Field(
|
||
None,
|
||
description="Name of the database table (null = all tables)",
|
||
frontend_type="text",
|
||
frontend_readonly=False,
|
||
frontend_required=False
|
||
)
|
||
fieldName: Optional[str] = Field(
|
||
None,
|
||
description="Specific field name (null = entire table, requires tableName)",
|
||
frontend_type="text",
|
||
frontend_readonly=False,
|
||
frontend_required=False
|
||
)
|
||
read: AccessLevel = Field(
|
||
description="Read permission level",
|
||
frontend_type="select",
|
||
frontend_readonly=False,
|
||
frontend_required=True,
|
||
frontend_options=[
|
||
{"value": "a", "label": {"en": "All Records", "fr": "Tous les enregistrements"}},
|
||
{"value": "m", "label": {"en": "My Records", "fr": "Mes enregistrements"}},
|
||
{"value": "g", "label": {"en": "Mandate Records", "fr": "Enregistrements du mandat"}},
|
||
{"value": "n", "label": {"en": "No Access", "fr": "Aucun accès"}}
|
||
]
|
||
)
|
||
create: AccessLevel = Field(
|
||
description="Create permission level",
|
||
frontend_type="select",
|
||
frontend_readonly=False,
|
||
frontend_required=True,
|
||
frontend_options=[
|
||
{"value": "a", "label": {"en": "All Records", "fr": "Tous les enregistrements"}},
|
||
{"value": "m", "label": {"en": "My Records", "fr": "Mes enregistrements"}},
|
||
{"value": "g", "label": {"en": "Mandate Records", "fr": "Enregistrements du mandat"}},
|
||
{"value": "n", "label": {"en": "No Access", "fr": "Aucun accès"}}
|
||
]
|
||
)
|
||
update: AccessLevel = Field(
|
||
description="Update permission level",
|
||
frontend_type="select",
|
||
frontend_readonly=False,
|
||
frontend_required=True,
|
||
frontend_options=[
|
||
{"value": "a", "label": {"en": "All Records", "fr": "Tous les enregistrements"}},
|
||
{"value": "m", "label": {"en": "My Records", "fr": "Mes enregistrements"}},
|
||
{"value": "g", "label": {"en": "Mandate Records", "fr": "Enregistrements du mandat"}},
|
||
{"value": "n", "label": {"en": "No Access", "fr": "Aucun accès"}}
|
||
]
|
||
)
|
||
delete: AccessLevel = Field(
|
||
description="Delete permission level",
|
||
frontend_type="select",
|
||
frontend_readonly=False,
|
||
frontend_required=True,
|
||
frontend_options=[
|
||
{"value": "a", "label": {"en": "All Records", "fr": "Tous les enregistrements"}},
|
||
{"value": "m", "label": {"en": "My Records", "fr": "Mes enregistrements"}},
|
||
{"value": "g", "label": {"en": "Mandate Records", "fr": "Enregistrements du mandat"}},
|
||
{"value": "n", "label": {"en": "No Access", "fr": "Aucun accès"}}
|
||
]
|
||
)
|
||
```
|
||
|
||
### Access Level Enum
|
||
|
||
```python
|
||
class AccessLevel(str, Enum):
|
||
"""Access level enumeration"""
|
||
ALL = "a" # All records
|
||
MY = "m" # My records (created by me)
|
||
MANDATE = "g" # Mandate records (my mandate)
|
||
NONE = "n" # No access
|
||
```
|
||
|
||
### Updated User Model
|
||
|
||
```python
|
||
class User(BaseModel, ModelMixin):
|
||
# ... existing fields ...
|
||
roleLabel: str = Field(
|
||
description="Role label assigned to this user",
|
||
frontend_type="select",
|
||
frontend_readonly=False,
|
||
frontend_required=True,
|
||
frontend_options=[
|
||
{"value": "sysadmin", "label": {"en": "System Administrator", "fr": "Administrateur système"}},
|
||
{"value": "admin", "label": {"en": "Administrator", "fr": "Administrateur"}},
|
||
{"value": "user", "label": {"en": "User", "fr": "Utilisateur"}},
|
||
{"value": "viewer", "label": {"en": "Viewer", "fr": "Observateur"}}
|
||
]
|
||
)
|
||
# ... rest of existing fields ...
|
||
```
|
||
|
||
## Access Control Logic
|
||
|
||
### Rule Hierarchy and Specificity
|
||
|
||
The AccessRule system supports a three-level hierarchy for maximum flexibility:
|
||
|
||
1. **Generic Rules** (`tableName = null`, `fieldName = null`): Apply to all tables and fields
|
||
2. **Table Rules** (`tableName = "UserInDB"`, `fieldName = null`): Apply to all fields in a specific table
|
||
3. **Field Rules** (`tableName = "UserInDB"`, `fieldName = "email"`): Apply to a specific field in a specific table
|
||
|
||
**Rule Resolution Order** (most specific wins):
|
||
1. Field-specific rule for the exact table and field
|
||
2. Table-specific rule for the exact table
|
||
3. Generic rule for all tables
|
||
|
||
**Examples:**
|
||
- Generic rule: `{roleLabel: "viewer", tableName: null, fieldName: null, read: "g"}` - Viewers can read mandate records in all tables
|
||
- Table rule: `{roleLabel: "admin", tableName: "UserInDB", fieldName: null, read: "a"}` - Admins can read all users
|
||
- Field rule: `{roleLabel: "user", tableName: "UserInDB", fieldName: "email", read: "m"}` - Users can only read their own email
|
||
|
||
### Opening Rights Principle
|
||
|
||
The system implements **opening rights** where read permission (`R`) is a prerequisite for create/update/delete operations (`CUD`):
|
||
|
||
- **If Read = "n"**: No CUD operations allowed
|
||
- **If Read = "m"**: CUD operations limited to "m" or "n"
|
||
- **If Read = "g"**: CUD operations limited to "g", "m", or "n"
|
||
- **If Read = "a"**: CUD operations can be "a", "g", "m", or "n"
|
||
|
||
**Key Rule**: You can ONLY create/update/delete (CUD) if you have read (R) right.
|
||
|
||
### System Field Protection
|
||
|
||
**Critical Security Rule**: System fields are automatically protected and cannot be modified by users:
|
||
|
||
- **ID Fields**: All `id` fields are read-only for all roles
|
||
- **System Fields**: All fields starting with `_` (underscore) are read-only for all roles
|
||
- **Database Connector Only**: Only the database connector can modify these fields
|
||
- **Automatic Enforcement**: This protection is enforced at the database level, not just application level
|
||
|
||
**Examples of Protected Fields:**
|
||
- `id` - Primary key
|
||
- `_createdAt` - Creation timestamp
|
||
- `_createdBy` - Creator user ID
|
||
- `_updatedAt` - Last update timestamp
|
||
- `_updatedBy` - Last updater user ID
|
||
- `_version` - Record version number
|
||
|
||
**Access Rule Override**: Even if an AccessRule grants CUD permissions to system fields, the database connector will automatically restrict these operations to read-only.
|
||
|
||
### Role-Based Access Logic
|
||
|
||
Each user has **one role label** that directly maps to a set of access rules:
|
||
|
||
- **Simple Assignment**: User.roleLabel → AccessRule.roleLabel
|
||
- **Direct Mapping**: No complex role combinations
|
||
- **Clear Permissions**: Each role has well-defined access patterns
|
||
- **Easy Debugging**: Clear trace from user to permissions
|
||
|
||
### Permission Validation
|
||
|
||
```python
|
||
def validate_access_rule(rule: AccessRule) -> bool:
|
||
"""Validate that CUD permissions are allowed by read permission level"""
|
||
read_level = AccessLevel(rule.read)
|
||
|
||
# CUD operations are only allowed if read permission exists
|
||
for operation in [rule.create, rule.update, rule.delete]:
|
||
if operation == "n":
|
||
continue # No access is always valid
|
||
if read_level == AccessLevel.NONE:
|
||
return False # No CUD allowed if no read access
|
||
if read_level == AccessLevel.MY and operation not in ["n", "m"]:
|
||
return False
|
||
if read_level == AccessLevel.MANDATE and operation not in ["n", "m", "g"]:
|
||
return False
|
||
|
||
return True
|
||
|
||
def validate_rule_hierarchy(rule: AccessRule) -> bool:
|
||
"""Validate that field rules require table rules"""
|
||
if rule.fieldName is not None and rule.tableName is None:
|
||
return False # Field rules must have a table
|
||
return True
|
||
```
|
||
|
||
## Database Integration
|
||
|
||
### Enhanced Database Connector
|
||
|
||
The database connector will be extended to support RBAC filtering:
|
||
|
||
```python
|
||
class DatabaseConnector:
|
||
def getRecordsetWithRBAC(self,
|
||
model_class: Type[BaseModel],
|
||
current_user: User,
|
||
record_filter: Dict = None,
|
||
order_by: str = None,
|
||
limit: int = None) -> List[Dict]:
|
||
"""Get records with RBAC filtering applied at database level"""
|
||
|
||
# Get access rules for user's role
|
||
access_rules = self.getAccessRulesForRole(current_user.roleLabel, model_class.__tablename__)
|
||
|
||
# Build SQL query with RBAC WHERE clause
|
||
where_clause = self.build_rbac_where_clause(access_rules, current_user)
|
||
|
||
# Execute optimized query
|
||
return self.execute_query_with_rbac(
|
||
model_class,
|
||
record_filter,
|
||
where_clause,
|
||
order_by,
|
||
limit
|
||
)
|
||
|
||
def getAccessRulesForRole(self, role_label: str, table_name: str) -> List[AccessRule]:
|
||
"""Get access rules for a specific role and table"""
|
||
# Get both table-specific and generic rules
|
||
table_rules = self.getRecordset(AccessRule,
|
||
recordFilter={
|
||
"roleLabel": role_label,
|
||
"tableName": table_name
|
||
}
|
||
)
|
||
generic_rules = self.getRecordset(AccessRule,
|
||
recordFilter={
|
||
"roleLabel": role_label,
|
||
"tableName": None
|
||
}
|
||
)
|
||
return table_rules + generic_rules
|
||
|
||
def is_system_field(self, field_name: str) -> bool:
|
||
"""Check if a field is a protected system field"""
|
||
return field_name == "id" or field_name.startswith("_")
|
||
|
||
def enforce_system_field_protection(self, data: Dict, operation: str) -> Dict:
|
||
"""Remove system fields from CUD operations"""
|
||
if operation == "read":
|
||
return data # Read operations can access system fields
|
||
|
||
# For CUD operations, remove system fields
|
||
protected_data = {}
|
||
for key, value in data.items():
|
||
if not self.is_system_field(key):
|
||
protected_data[key] = value
|
||
|
||
return protected_data
|
||
```
|
||
|
||
### SQL Query Generation
|
||
|
||
```sql
|
||
-- Example: Get workflows with RBAC filtering for single role
|
||
SELECT w.*
|
||
FROM "ChatWorkflow" w
|
||
JOIN "AccessRule" ar ON ar.roleLabel = %s AND ar.tableName = 'ChatWorkflow'
|
||
WHERE
|
||
-- User access control based on role permissions
|
||
(
|
||
-- All records access
|
||
(ar.read = 'a')
|
||
OR
|
||
-- Mandate records access
|
||
(ar.read = 'g' AND w.mandateId = %s)
|
||
OR
|
||
-- My records access
|
||
(ar.read = 'm' AND w.createdBy = %s)
|
||
)
|
||
-- Additional filters
|
||
AND w.status = 'active'
|
||
ORDER BY w.createdAt DESC
|
||
LIMIT 100;
|
||
```
|
||
|
||
## Migration Strategy
|
||
|
||
### Phase 1: Database Schema
|
||
1. Create `AccessRule` table
|
||
2. Add `roleLabel` column to `User` table
|
||
3. Create indexes for performance
|
||
|
||
### Phase 2: Data Migration
|
||
1. Create default access rules for each role label
|
||
2. Migrate existing users to appropriate role labels
|
||
3. Create access rules based on current UAM logic
|
||
|
||
### Phase 3: Code Migration
|
||
1. Update database connector with RBAC support
|
||
2. Replace `_uam()` calls with `getRecordsetWithRBAC()`
|
||
3. Update all interface methods
|
||
|
||
### Phase 4: Testing & Optimization
|
||
1. Performance testing
|
||
2. Security validation
|
||
3. Query optimization
|
||
|
||
## Performance Impact
|
||
|
||
### Expected Improvements
|
||
|
||
| Operation | Current | Optimized | Improvement |
|
||
|-----------|---------|-----------|-------------|
|
||
| `getUserByUsername()` | Full table scan | Single record | 100% reduction |
|
||
| `getWorkflows()` | All workflows | User's workflows | 80-95% reduction |
|
||
| `getAllFiles()` | All files | User's files | 80-95% reduction |
|
||
| Memory usage | Full tables | Filtered records | 80-95% reduction |
|
||
| Query time | O(n) Python | O(log n) SQL | 10-100x faster |
|
||
|
||
### Database Load Reduction
|
||
|
||
- **Critical Operations**: 100% reduction in data transfer
|
||
- **High Impact Operations**: 80-95% reduction
|
||
- **Memory Usage**: 80-95% reduction for large tables
|
||
- **Query Performance**: 10-100x improvement
|
||
|
||
## Security Considerations
|
||
|
||
### Mandate Isolation
|
||
- Users can only access records within their mandate
|
||
- Global roles can access all mandates
|
||
- Cross-mandate access is explicitly controlled
|
||
|
||
### Permission Inheritance
|
||
- Global roles apply to all mandates
|
||
- Mandate-specific roles override global roles
|
||
- Most restrictive permission wins
|
||
|
||
### Audit Trail
|
||
- All access attempts logged
|
||
- Permission changes tracked
|
||
- Security events monitored
|
||
|
||
## Implementation Timeline
|
||
|
||
### Week 1-2: Database Schema
|
||
- Create tables and indexes
|
||
- Implement data models
|
||
- Set up migration scripts
|
||
|
||
### Week 3-4: Core RBAC Logic
|
||
- Implement access rule validation
|
||
- Build database connector extensions
|
||
- Create permission checking utilities
|
||
|
||
### Week 5-6: Interface Migration
|
||
- Update interfaceAppObjects
|
||
- Update interfaceChatObjects
|
||
- Update interfaceComponentObjects
|
||
|
||
### Week 7-8: Testing & Optimization
|
||
- Performance testing
|
||
- Security validation
|
||
- Query optimization
|
||
- Documentation updates
|
||
|
||
## Default Roles
|
||
|
||
### System Roles (Global)
|
||
- **SysAdmin**: Full access to all tables and mandates
|
||
- **SystemUser**: Limited system access
|
||
|
||
### Mandate Roles
|
||
- **Admin**: Full access within mandate
|
||
- **User**: Limited access within mandate
|
||
- **Viewer**: Read-only access within mandate
|
||
|
||
## Role Management
|
||
|
||
### Simple Role Assignment
|
||
|
||
Users have a single role label that directly maps to access rules:
|
||
|
||
```json
|
||
{
|
||
"userId": "user-123",
|
||
"roleLabel": "admin",
|
||
"mandateId": "mandate-456"
|
||
}
|
||
```
|
||
|
||
### Role Label Options
|
||
- **sysadmin**: System administrator with full access
|
||
- **admin**: Administrator with mandate-level access
|
||
- **user**: Regular user with own-record access
|
||
- **viewer**: Read-only access within mandate
|
||
|
||
## Access Rule Examples
|
||
|
||
### Generic Rules (All Tables)
|
||
|
||
Generic rules provide a powerful way to set default permissions that apply across all tables:
|
||
|
||
#### Example 1: Viewer Role - Read-Only Access to All Tables
|
||
```json
|
||
{
|
||
"roleLabel": "viewer",
|
||
"tableName": null,
|
||
"fieldName": null,
|
||
"read": "g",
|
||
"create": "n",
|
||
"update": "n",
|
||
"delete": "n"
|
||
}
|
||
```
|
||
**Effect**: Viewers can read mandate records in ALL tables, but cannot create, update, or delete anything.
|
||
|
||
#### Example 2: SysAdmin Role - Full Access to All Tables
|
||
```json
|
||
{
|
||
"roleLabel": "sysadmin",
|
||
"tableName": null,
|
||
"fieldName": null,
|
||
"read": "a",
|
||
"create": "a",
|
||
"update": "a",
|
||
"delete": "a"
|
||
}
|
||
```
|
||
**Effect**: System administrators have full access to ALL tables and fields.
|
||
|
||
#### Example 3: User Role - Own Records Only (Generic)
|
||
```json
|
||
{
|
||
"roleLabel": "user",
|
||
"tableName": null,
|
||
"fieldName": null,
|
||
"read": "m",
|
||
"create": "m",
|
||
"update": "m",
|
||
"delete": "m"
|
||
}
|
||
```
|
||
**Effect**: Users can only access their own records in ALL tables (where `_createdBy` matches their user ID).
|
||
|
||
### Table-Specific Rules
|
||
|
||
Table-specific rules override generic rules for particular tables:
|
||
|
||
#### Example 4: Admin Role - User Table Access
|
||
```json
|
||
{
|
||
"roleLabel": "admin",
|
||
"tableName": "UserInDB",
|
||
"fieldName": null,
|
||
"read": "g",
|
||
"create": "g",
|
||
"update": "g",
|
||
"delete": "n"
|
||
}
|
||
```
|
||
|
||
#### Example 5: User Role - File Access (Override Generic Rule)
|
||
```json
|
||
{
|
||
"roleLabel": "user",
|
||
"tableName": "FileItem",
|
||
"fieldName": null,
|
||
"read": "g",
|
||
"create": "g",
|
||
"update": "g",
|
||
"delete": "g"
|
||
}
|
||
```
|
||
**Effect**: Users can access all files in their mandate (overrides the generic "own records only" rule).
|
||
|
||
#### Example 6: Viewer Role - Workflow Access (Override Generic Rule)
|
||
```json
|
||
{
|
||
"roleLabel": "viewer",
|
||
"tableName": "ChatWorkflow",
|
||
"fieldName": null,
|
||
"read": "a",
|
||
"create": "n",
|
||
"update": "n",
|
||
"delete": "n"
|
||
}
|
||
```
|
||
**Effect**: Viewers can read ALL workflows (overrides the generic "mandate records only" rule).
|
||
|
||
### Field-Specific Rules
|
||
|
||
Field-specific rules provide the most granular control:
|
||
|
||
#### Example 7: User Role - Email Field Access
|
||
```json
|
||
{
|
||
"roleLabel": "user",
|
||
"tableName": "UserInDB",
|
||
"fieldName": "email",
|
||
"read": "a",
|
||
"create": "a",
|
||
"update": "a",
|
||
"delete": "n"
|
||
}
|
||
```
|
||
**Effect**: Users can read and modify email addresses of all users, but cannot delete them.
|
||
|
||
## Benefits of Generic Rules
|
||
|
||
### Simplified Access Management
|
||
|
||
Generic rules dramatically reduce the number of access rules needed:
|
||
|
||
**Without Generic Rules:**
|
||
- Need separate rules for each table (UserInDB, ChatWorkflow, FileItem, etc.)
|
||
- 10 tables × 4 roles = 40+ access rules
|
||
- Difficult to maintain and prone to inconsistencies
|
||
|
||
**With Generic Rules:**
|
||
- 1 generic rule per role = 4 access rules
|
||
- Override with specific rules only when needed
|
||
- Much easier to maintain and understand
|
||
|
||
### Common Use Cases for Generic Rules
|
||
|
||
1. **Default Permissions**: Set baseline permissions for all tables
|
||
2. **New Table Support**: Automatically apply permissions to new tables
|
||
3. **Bulk Permission Changes**: Update permissions across all tables with one rule
|
||
4. **Role Templates**: Create role templates that work out-of-the-box
|
||
|
||
### Rule Management Best Practices
|
||
|
||
1. **Start with Generic Rules**: Define broad permissions first
|
||
2. **Override Selectively**: Add specific rules only where needed
|
||
3. **Use Descriptive Names**: Make role labels clear and meaningful
|
||
4. **Test Incrementally**: Add rules one at a time and test
|
||
5. **Document Exceptions**: Keep track of why specific overrides exist
|
||
|
||
## System Field Protection Examples
|
||
|
||
### Example 1: User Attempts to Modify System Fields
|
||
```python
|
||
# User tries to update a record with system fields
|
||
user_data = {
|
||
"id": "new-id-123", # ❌ Blocked - system field
|
||
"name": "John Doe", # ✅ Allowed - user field
|
||
"_createdAt": 1640995200, # ❌ Blocked - system field
|
||
"_createdBy": "hacker-123", # ❌ Blocked - system field
|
||
"email": "john@example.com" # ✅ Allowed - user field
|
||
}
|
||
|
||
# Database connector automatically filters out system fields
|
||
protected_data = db_connector.enforce_system_field_protection(user_data, "update")
|
||
# Result: {"name": "John Doe", "email": "john@example.com"}
|
||
```
|
||
|
||
### Example 2: Access Rules Cannot Override System Protection
|
||
```json
|
||
{
|
||
"roleLabel": "admin",
|
||
"tableName": "UserInDB",
|
||
"fieldName": "id",
|
||
"read": "a",
|
||
"create": "a", // ❌ Ignored - system field protection
|
||
"update": "a", // ❌ Ignored - system field protection
|
||
"delete": "a" // ❌ Ignored - system field protection
|
||
}
|
||
```
|
||
|
||
### Example 3: System Fields in Read Operations
|
||
```python
|
||
# Read operations can access system fields
|
||
workflow = db_connector.getRecordsetWithRBAC(ChatWorkflow, user, {"id": "workflow-123"})
|
||
# Result includes: {"id": "workflow-123", "_createdAt": 1640995200, "_createdBy": "user-456", ...}
|
||
```
|
||
|