# 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", ...} ```