From a953c51c10b9fa15825e24731752b5c0c14f23b9 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Sun, 7 Dec 2025 13:48:52 +0100 Subject: [PATCH] refactored uam to rbac --- appdoc/doc_gateway_import_map.drawio | 2 +- appdoc/doc_security_role_based_access.md | 768 +++++++++++++----- ...y_role_based_access_implementation_plan.md | 574 +++++++++++++ 3 files changed, 1150 insertions(+), 194 deletions(-) create mode 100644 appdoc/doc_security_role_based_access_implementation_plan.md diff --git a/appdoc/doc_gateway_import_map.drawio b/appdoc/doc_gateway_import_map.drawio index 5d0f1ad..b79bd96 100644 --- a/appdoc/doc_gateway_import_map.drawio +++ b/appdoc/doc_gateway_import_map.drawio @@ -1 +1 @@ -5Vldk5owFP01Pm4nEBT3cVf3ozPdJ6fT9jGGi7BG4sSg0l/fsCaaALYda8FRXuCem5DknOTey9DDo8X2RZBl8sYjYD0fRdseHvd83wuDgbqVSLFD/BDf75CZSCPd6gBM0p+gQaTRPI1g5TSUnDOZLl2Q8iwDKh2MCME3brOYM3fUJZlBDZhQwurotzSSiUYDhA6OV0hniRkaGc+CmNYaWCUk4hsLwk89PBKcy93TYjsCVtJniNn1ez7i3c9MQCb/pgNJKRew67UmLAcXftazlIVZu+B5FkHZ2+vhx02SSpgsCS29GyW3whK5YNodp4yNOOPioy+O4xgDUvhKCj4HywODviJp7zGs+uU7eCYnenzP2Lst4QXKrq9Yk7AGIWFrQZqBF+ALkKJQTYzXG2o59I7EWNsbS14jWWIpO9AY0Ttqtn/3gXP1oGlvlmAFYp1StZ+rIhwc55UhGiIU4iYZHvoIBafJUNqu1Oo6jzz3yFXHb1OdNJMgYtKkj+26bYWC0FUoHLSo0PwtGMsB+fw6HxZfKY2meZzf9Wt6QKRiuDYznqnboysRFzLhM54R9oXzpQbfQcpCs0hyyV3ZFGOi+K4MZIwfpfGpb8zx1naOC225skYQk5zJj1bRQ5mfFDhlnM4bdVaNnlNmprBbZ7m4oxlAQyueC7oPLTQXqSx08iRiBrIejep6C2BEpmt3rH8LfvZEnOBnHOfOQTAFaDpa09CjHu0qB/X77hHyUItHKAYic9EQ4g6O2w5wQ89V5y5oUZ0NF/OYlVVrVR7Lc159AMV+7DXpo9SJTtTnf1RqrR4TxahsOCQGPnOgohAAbZJgOEQQxF1JEODKUfDblKBMdIOaAn8g/oTM3ph2q7nZzcTv+WJpuCeC/o5tNxXb5b+diu1vs0qCbyEzlyMOb4TpapHfDdued1V0V0m9rK3tXdvePlbQX8beNuxeCd1uwXzs+6kzsoMbIbtSGXfEdv+q2K5wellR2w9vhetLiCOm0r8Stu3PN5tqN750RPV11X7HqL6MYgRfVzFyjGy3JuyIanwjVLcarZV5+EX84bN+teOnXw== \ No newline at end of file +7Zpbc9o6EMc/DXOe2pHxBfKYQC+Z0860k/Zc+tIR0hq7CIvKcoB++koggS+iTVrHcOA4D0G7siz/f9JqZbvnj+arVwIvkrecAuv1EV31/HGv3/e8KFD/tGVtLeHV1jIVKTW2veEu/QbGiIy1SCnklYqScybTRdVIeJYBkRUbFoIvq9VizqpXXeApNAx3BLOm9e+UysRYA4T2jteQThN7aWQ9c2xrG0OeYMqXJZP/ouePBOdy+2u+GgHT8llhtue9PODd9UxAJh9ywugr//yJfi7e/3kl3/8lrpMP3tdn/W0r95gV5o6Nklzkuqltz+Xa6iF4kVHQLXo9/2aZpBLuFpho71INAWVL5JwZd5wyNuKMi825Png0hIGy51LwGZQ8V9HAx9HOY5Xu6zZ4Ju/M9T1b3g4TL1Bl038QElYHhfF2cquRCnwOUqxVFXPCDqYZpEFgyssScUsxKcGOjA2bQTbdNb3HoH4YEo+g4jWofLz9I28VBsUwjIkLRkSGMIlPBUa/QxY4JVxAQ3trbnc2xDFMAFwAJgOPeOQ3ADTUdjA5CGAfwdY1Il0gyEHcpwTyBoS9o+Wg5JOhukEHhht0FZY8j8Ggy6WW0OZoB8+wPj/8DumkmQQRYxefsqtdQhFGKI5dhPwBQqNRG4TizdEOofCY82f2NhjLCN++ng3XHwmhkyIunoUNHkBVlmOKGc90GKoi4kImfMozzN5wvjDGLyDl2qiIC8mr2JRiYv2PKiBb+FcXnoe2OF6VneO1KVWxUohxweSmFr3WGZwyThgnMydnVellymwXDtLLeSHILpKQQqRyKyuSWExBuoKP1uiHvAUwLNP7aqb4e8Gv3LNK8LOOy1iDomMGuRiwLIQjxO0dlx3gGhlC1CGdJRezmOl9XR1PyXPZKUKDT6fTRyktHZPHmttFg1CIwDl1EPLGNyc3deq5QZczR69nUYPLT3D8Qh7gXKTrK3l13f5SzBeWCBbk4St5eb0ur+TlnVz367i+4vBClK5vCY6jttd8YvJflrsu6mkNbe/cxvah/cBpjG2r7pnIXU2vT2n7tRE7uBCxa3n0kdQOz0rtmqanFbX7g0vR+hTiiN1xnYna5U1dWepqfDmS1OeV+x2S+jSSEf+8kpFDYldzwiNJ7V+I1MeN1s4X8E3pKZZ4rr9vaf0ZVhzqP9czrGhztPEMa3v0nuQNfeh4RT/o9HOJZs6eJ+p26P+k6k8bj03qAfm+aiVd5KC1TPBCGwnjBf05rCcQLHAM7cihV/BUejVz9ut3tz+QrAOJhrXZ31QodCjkP14hVdx/HLfxlT4y9F98Bw== \ No newline at end of file diff --git a/appdoc/doc_security_role_based_access.md b/appdoc/doc_security_role_based_access.md index 34864c1..f68f194 100644 --- a/appdoc/doc_security_role_based_access.md +++ b/appdoc/doc_security_role_based_access.md @@ -4,6 +4,13 @@ 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. +The RBAC system extends beyond data access control to include: +- **Data Access Control**: Table and field-level permissions for database operations +- **UI Access Control**: Component and feature visibility management +- **Resource Access Control**: System resource availability (AI models, actions, etc.) + +Users can have multiple roles that are combined using opening (union) logic, where permissions from all roles are combined and the most permissive access is granted. + ## Problem Statement The current User Access Management (UAM) system has significant performance bottlenecks: @@ -17,7 +24,9 @@ The current User Access Management (UAM) system has significant performance bott The new RBAC system implements a **matrix-based access model** with database-level filtering, providing: -- **Granular Permissions**: Table and field-level access control +- **Granular Permissions**: Table and field-level access control for data +- **UI Access Control**: Fine-grained control over UI elements and features +- **Resource Access Control**: Manage access to system resources (e.g., AI models, actions) - **Role-Based Security**: Centralized permission management - **Database Optimization**: Filtering at database level - **Mandate Isolation**: Secure multi-tenant access @@ -27,16 +36,52 @@ The new RBAC system implements a **matrix-based access model** with database-lev ### 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 +1. **Role Management**: Define roles with mandate scope (users can have multiple roles) +2. **Access Rules**: Matrix of permissions per role/context/item +3. **Context Types**: DATA (database tables/fields), UI (interface elements), RESOURCE (system resources) +4. **Database Integration**: Native filtering in SQL queries for DATA context +5. **UI Integration**: Component visibility and feature access control +6. **Migration Strategy**: Seamless transition from current UAM ## Data Models +### Frontend Options Format + +The `frontend_options` attribute in Field definitions supports two formats: + +1. **Static List** (for basic data types): A list of option dictionaries + ```python + frontend_options=[ + {"value": "a", "label": {"en": "All Records", "fr": "Tous les enregistrements"}}, + {"value": "m", "label": {"en": "My Records", "fr": "Mes enregistrements"}} + ] + ``` + +2. **String Reference** (for custom types): A string identifier that references dynamic options + ```python + frontend_options="user.role" # Frontend fetches from /api/options/user.role + ``` + +**Dynamic Options API**: When `frontend_options` is a string reference, the frontend must fetch options from `/api/options/{optionsName}`. This allows for: +- Database-driven options (e.g., user connections loaded from database) +- Context-aware options (e.g., options filtered by current user's permissions) +- Centralized option management (e.g., role definitions managed in one place) + +**Available Option Names**: +- `"user.role"` - User role options (sysadmin, admin, user, viewer) +- `"user.connection"` - User connection types +- `"auth.authority"` - Authentication authorities +- `"connection.status"` - Connection statuses + ### Access Rule Model ```python +class AccessRuleContext(str, Enum): + """Context type enumeration""" + DATA = "DATA" # Database tables and fields + UI = "UI" # UI elements and features + RESOURCE = "RESOURCE" # System resources (AI models, actions, etc.) + class AccessRule(BaseModel, ModelMixin): """Data model for access control rules""" id: str = Field( @@ -51,72 +96,82 @@ class AccessRule(BaseModel, ModelMixin): frontend_type="select", frontend_readonly=False, frontend_required=True, + frontend_options="user.role" + ) + context: AccessRuleContext = Field( + description="Context type: DATA (database), UI (interface), RESOURCE (system resources)", + 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"}} + {"value": "DATA", "label": {"en": "Data", "fr": "Données"}}, + {"value": "UI", "label": {"en": "UI", "fr": "Interface"}}, + {"value": "RESOURCE", "label": {"en": "Resource", "fr": "Ressource"}} ] ) - tableName: Optional[str] = Field( + item: Optional[str] = Field( None, - description="Name of the database table (null = all tables)", + description="Item identifier (null = all items in context). Format: DATA: '' or '
.', UI: cascading string (e.g., 'playground.voice.settings'), RESOURCE: cascading string (e.g., 'ai.model.anthropic')", frontend_type="text", frontend_readonly=False, frontend_required=False ) - fieldName: Optional[str] = Field( + view: bool = Field( + False, + description="View permission: if true, item is visible/enabled. Only objects with view=true are shown.", + frontend_type="checkbox", + frontend_readonly=False, + frontend_required=True + ) + read: Optional[AccessLevel] = 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", + description="Read permission level (only for DATA context)", frontend_type="select", frontend_readonly=False, - frontend_required=True, + frontend_required=False, 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": "g", "label": {"en": "Group Records", "fr": "Enregistrements du groupe"}}, {"value": "n", "label": {"en": "No Access", "fr": "Aucun accès"}} ] ) - create: AccessLevel = Field( - description="Create permission level", + create: Optional[AccessLevel] = Field( + None, + description="Create permission level (only for DATA context)", frontend_type="select", frontend_readonly=False, - frontend_required=True, + frontend_required=False, 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": "g", "label": {"en": "Group Records", "fr": "Enregistrements du groupe"}}, {"value": "n", "label": {"en": "No Access", "fr": "Aucun accès"}} ] ) - update: AccessLevel = Field( - description="Update permission level", + update: Optional[AccessLevel] = Field( + None, + description="Update permission level (only for DATA context)", frontend_type="select", frontend_readonly=False, - frontend_required=True, + frontend_required=False, 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": "g", "label": {"en": "Group Records", "fr": "Enregistrements du groupe"}}, {"value": "n", "label": {"en": "No Access", "fr": "Aucun accès"}} ] ) - delete: AccessLevel = Field( - description="Delete permission level", + delete: Optional[AccessLevel] = Field( + None, + description="Delete permission level (only for DATA context)", frontend_type="select", frontend_readonly=False, - frontend_required=True, + frontend_required=False, 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": "g", "label": {"en": "Group Records", "fr": "Enregistrements du groupe"}}, {"value": "n", "label": {"en": "No Access", "fr": "Aucun accès"}} ] ) @@ -129,64 +184,130 @@ class AccessLevel(str, Enum): """Access level enumeration""" ALL = "a" # All records MY = "m" # My records (created by me) - MANDATE = "g" # Mandate records (my mandate) + GROUP = "g" # Group records (group context is the mandate) NONE = "n" # No access ``` +### User Permissions Model + +```python +class UserPermissions(BaseModel): + """User permissions model for RBAC""" + view: bool = Field( + default=False, + description="View permission: if true, item is visible/enabled" + ) + read: AccessLevel = Field( + default=AccessLevel.NONE, + description="Read permission level" + ) + create: AccessLevel = Field( + default=AccessLevel.NONE, + description="Create permission level" + ) + update: AccessLevel = Field( + default=AccessLevel.NONE, + description="Update permission level" + ) + delete: AccessLevel = Field( + default=AccessLevel.NONE, + description="Delete permission level" + ) +``` + ### Updated User Model ```python class User(BaseModel, ModelMixin): # ... existing fields ... - roleLabel: str = Field( - description="Role label assigned to this user", - frontend_type="select", + roleLabels: List[str] = Field( + default_factory=list, + description="List of role labels assigned to this user. All roles are opening roles (union) - if one role enables something, it is enabled.", + frontend_type="multiselect", 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"}} - ] + frontend_options="user.role" ) # ... rest of existing fields ... ``` +**Note on `frontend_options`**: The `frontend_options` attribute can be either: +- **Static list** (for basic data types): A list of option dictionaries with `value` and `label` keys +- **String reference** (for custom types): A string identifier (e.g., `"user.role"`, `"user.connection"`) that references dynamic options loaded from the API + +For custom types like `"user.role"` or `"user.connection"`, the frontend must fetch the options from the API endpoint `/api/options/{optionsName}`. The API will return the options list dynamically, allowing for: +- Database-driven options (e.g., user connections loaded from database) +- Context-aware options (e.g., options filtered by current user's permissions) +- Centralized option management (e.g., role definitions managed in one place) + ## Access Control Logic ### Rule Hierarchy and Specificity -The AccessRule system supports a three-level hierarchy for maximum flexibility: +The AccessRule system supports a cascading hierarchy based on the `item` field: -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 +#### For DATA Context: +1. **Generic Rules** (`item = null`): Apply to all tables and fields +2. **Table Rules** (`item = "UserInDB"`): Apply to all fields in a specific table +3. **Field Rules** (`item = "UserInDB.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 +#### For UI Context: +1. **Generic Rules** (`item = null`): Apply to all UI elements +2. **Component Rules** (`item = "playground"`): Apply to entire component +3. **Feature Rules** (`item = "playground.voice"`): Apply to feature within component +4. **Element Rules** (`item = "playground.voice.settings"`): Apply to specific UI element + +#### For RESOURCE Context: +1. **Generic Rules** (`item = null`): Apply to all resources +2. **Category Rules** (`item = "ai"`): Apply to resource category +3. **Type Rules** (`item = "ai.model"`): Apply to resource type +4. **Specific Rules** (`item = "ai.model.anthropic"`): Apply to specific resource + +**Rule Resolution Order** (most specific wins within a role): +1. Most specific item match (longest matching prefix) - **This overrides generic rules** +2. Generic rule for the context (`item = null`) - **Only applies if no specific rule exists** + +**Important Logic**: Within a single role, the most specific rule always wins. If a generic rule has `view: true` but a specific rule has `view: false`, then `view: false` applies (the specific rule overrides the generic). **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 +- DATA Generic: `{roleLabel: "viewer", context: "DATA", item: null, read: "g"}` - Viewers can read group records in all tables (group context is the mandate) +- DATA Table: `{roleLabel: "admin", context: "DATA", item: "UserInDB", read: "a"}` - Admins can read all users (overrides generic rule for UserInDB table) +- DATA Field: `{roleLabel: "user", context: "DATA", item: "UserInDB.email", read: "m"}` - Users can only read their own email (overrides table-level rule for email field) +- UI Component: `{roleLabel: "user", context: "UI", item: "playground", view: true}` - Users can view playground component +- UI Feature Override: `{roleLabel: "user", context: "UI", item: null, view: true}` + `{roleLabel: "user", context: "UI", item: "playground.voice.settings", view: false}` - Generic allows all UI, but specific rule hides voice settings (specific wins: `view: false`) +- RESOURCE: `{roleLabel: "user", context: "RESOURCE", item: "ai.model.anthropic", view: true}` - Users can access Anthropic AI model -### Opening Rights Principle +### View Attribute -The system implements **opening rights** where read permission (`R`) is a prerequisite for create/update/delete operations (`CUD`): +The `view` attribute controls visibility and enablement across all contexts: + +- **For DATA Context**: Controls whether the table/field is accessible (in addition to read/create/update/delete permissions) +- **For UI Context**: Controls whether UI elements are visible/enabled. Only objects with `view: true` are shown. +- **For RESOURCE Context**: Controls whether resources are accessible. Only resources with `view: true` are available. + +**Key Rule**: Only objects with `view: true` are enabled/visible. If `view: false`, the item is hidden regardless of other permissions. + +**Rule Specificity Within a Role**: +- Within a single role, the most specific rule wins +- If generic rule has `view: true` but specific rule has `view: false`, then `view: false` applies (specific overrides generic) +- If generic rule has `view: false` but specific rule has `view: true`, then `view: true` applies (specific overrides generic) + +**Multiple Roles**: If a user has multiple roles, `view` uses opening (union) logic - if ANY role has `view: true` (after applying rule specificity within that role), the item is visible. + +### Opening Rights Principle (DATA Context Only) + +For DATA context, 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. +**Key Rule**: You can ONLY create/update/delete (CUD) if you have read (R) right. This principle applies only to DATA context. -### System Field Protection +### System Field Protection (DATA Context Only) -**Critical Security Rule**: System fields are automatically protected and cannot be modified by users: +**Critical Security Rule**: System fields are automatically protected and cannot be modified by users (applies only to DATA context): - **ID Fields**: All `id` fields are read-only for all roles - **System Fields**: All fields starting with `_` (underscore) are read-only for all roles @@ -203,40 +324,139 @@ The system implements **opening rights** where read permission (`R`) is a prereq **Access Rule Override**: Even if an AccessRule grants CUD permissions to system fields, the database connector will automatically restrict these operations to read-only. +**Item Format for System Fields**: When referencing system fields in DATA context, use format `"
."` (e.g., `"UserInDB._createdAt"`). + ### Role-Based Access Logic -Each user has **one role label** that directly maps to a set of access rules: +The RBAC system uses a **two-level resolution** approach: -- **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 +#### Level 1: Rule Specificity Within a Role (Most Specific Wins) +- **Within a single role**, the most specific rule always wins +- If generic rule (`item = null`) has `view: true` but specific rule (`item = "playground.voice.settings"`) has `view: false`, then `view: false` applies +- Specific rules override generic rules within the same role +- Resolution: Find longest matching prefix for the item, or use generic rule if no match + +#### Level 2: Multiple Roles (Opening/Union Logic) +- **Across multiple roles**, opening (union) logic applies +- If ANY role enables something (after Level 1 resolution), it is enabled +- Permissions from all roles are combined (OR logic) +- Most permissive access level wins for DATA context + +**Resolution Process:** +1. **For each role**: Find the most specific matching rule (Level 1) +2. **Across all roles**: Combine using union logic (Level 2) + +**Example:** +- User has roles: `["user", "viewer"]` +- Rule 1: `{roleLabel: "user", context: "UI", item: "playground", view: false}` - User role hides playground +- Rule 2: `{roleLabel: "viewer", context: "UI", item: "playground", view: true}` - Viewer role shows playground +- **Level 1 Resolution**: + - "user" role → most specific rule for "playground" → `view: false` + - "viewer" role → most specific rule for "playground" → `view: true` +- **Level 2 Resolution**: Union logic → `view: true` OR `view: false` → `view: true` +- **Result**: Playground is visible (viewer role enables it) ### 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) +def validateAccessRule(rule: AccessRule) -> bool: + """Validate that CUD permissions are allowed by read permission level (only for DATA context)""" + if rule.context != AccessRuleContext.DATA: + # For UI and RESOURCE contexts, only view is relevant + return True + + if rule.read is None: + return False # DATA context requires read permission + + readLevel = AccessLevel(rule.read) # CUD operations are only allowed if read permission exists for operation in [rule.create, rule.update, rule.delete]: - if operation == "n": + if operation is None or operation == "n": continue # No access is always valid - if read_level == AccessLevel.NONE: + if readLevel == AccessLevel.NONE: return False # No CUD allowed if no read access - if read_level == AccessLevel.MY and operation not in ["n", "m"]: + if readLevel == AccessLevel.MY and operation not in ["n", "m"]: return False - if read_level == AccessLevel.MANDATE and operation not in ["n", "m", "g"]: + if readLevel == AccessLevel.GROUP 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 +def getUserPermissions(user: User, context: AccessRuleContext, item: str) -> UserPermissions: + """Get combined permissions for a user across all their roles""" + permissions = UserPermissions( + view=False, + read=AccessLevel.NONE, + create=AccessLevel.NONE, + update=AccessLevel.NONE, + delete=AccessLevel.NONE + ) + + # Step 1: For each role, find the most specific matching rule (most specific wins within role) + rolePermissions = {} + for roleLabel in user.roleLabels: + # Get all rules for this role and context + allRules = getRulesForRole(roleLabel, context) + + # Find most specific rule for this item (longest matching prefix) + mostSpecificRule = findMostSpecificRule(allRules, item) + + if mostSpecificRule: + rolePermissions[roleLabel] = mostSpecificRule + + # Step 2: Combine permissions across roles using opening (union) logic + for roleLabel, rule in rolePermissions.items(): + # View: union logic - if ANY role has view=true, then view=true + if rule.view: + permissions.view = True + + if context == AccessRuleContext.DATA: + # For DATA context, use most permissive access level across roles + if rule.read and _isMorePermissive(rule.read, permissions.read): + permissions.read = rule.read + if rule.create and _isMorePermissive(rule.create, permissions.create): + permissions.create = rule.create + if rule.update and _isMorePermissive(rule.update, permissions.update): + permissions.update = rule.update + if rule.delete and _isMorePermissive(rule.delete, permissions.delete): + permissions.delete = rule.delete + + return permissions + +def findMostSpecificRule(rules: List[AccessRule], item: str) -> Optional[AccessRule]: + """Find the most specific rule for an item (longest matching prefix wins)""" + if not item: + # If no item specified, return generic rule (item = null) + genericRules = [r for r in rules if r.item is None] + return genericRules[0] if genericRules else None + + # Find longest matching prefix + itemParts = item.split(".") + bestMatch = None + bestMatchLength = -1 + + for rule in rules: + if rule.item is None: + # Generic rule - use as fallback if no specific match found + if bestMatch is None: + bestMatch = rule + elif rule.item == item: + # Exact match - most specific + return rule + elif item.startswith(rule.item + "."): + # Prefix match - check if it's longer than current best + matchLength = len(rule.item.split(".")) + if matchLength > bestMatchLength: + bestMatch = rule + bestMatchLength = matchLength + + return bestMatch + +def _isMorePermissive(level1: AccessLevel, level2: AccessLevel) -> bool: + """Check if level1 is more permissive than level2""" + hierarchy = {AccessLevel.NONE: 0, AccessLevel.MY: 1, AccessLevel.GROUP: 2, AccessLevel.ALL: 3} + return hierarchy.get(level1, 0) > hierarchy.get(level2, 0) ``` ## Database Integration @@ -248,77 +468,102 @@ 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, + modelClass: Type[BaseModel], + currentUser: User, + recordFilter: Dict = None, + orderBy: 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__) + # Get access rules for user's roles + accessRules = self.getAccessRulesForRoles( + currentUser.roleLabels, + AccessRuleContext.DATA, + modelClass.__tablename__ + ) # Build SQL query with RBAC WHERE clause - where_clause = self.build_rbac_where_clause(access_rules, current_user) + whereClause = self.buildRbacWhereClause(accessRules, currentUser) # Execute optimized query - return self.execute_query_with_rbac( - model_class, - record_filter, - where_clause, - order_by, + return self.executeQueryWithRbac( + modelClass, + recordFilter, + whereClause, + orderBy, 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 getAccessRulesForRoles(self, roleLabels: List[str], context: AccessRuleContext, item: str) -> List[AccessRule]: + """Get access rules for multiple roles, context, and item""" + # Get most specific matching rules for each role + allRules = [] + + for roleLabel in roleLabels: + # Get rules matching the role and context + rules = self.getRecordset(AccessRule, + recordFilter={ + "roleLabel": roleLabel, + "context": context.value + } + ) + + # Find most specific match for item (longest matching prefix) + matchingRules = [] + if item: + # Try to find exact match or longest prefix match + itemParts = item.split(".") + for i in range(len(itemParts), 0, -1): + prefix = ".".join(itemParts[:i]) + matching = [r for r in rules if r.item == prefix] + if matching: + matchingRules.extend(matching) + break + # Also include generic rules (item = null) + matchingRules.extend([r for r in rules if r.item is None]) + else: + # Include all rules for this role/context + matchingRules.extend(rules) + + allRules.extend(matchingRules) + + return allRules - def is_system_field(self, field_name: str) -> bool: + def isSystemField(self, fieldName: str) -> bool: """Check if a field is a protected system field""" - return field_name == "id" or field_name.startswith("_") + return fieldName == "id" or fieldName.startswith("_") - def enforce_system_field_protection(self, data: Dict, operation: str) -> Dict: + def enforceSystemFieldProtection(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 = {} + protectedData = {} for key, value in data.items(): - if not self.is_system_field(key): - protected_data[key] = value + if not self.isSystemField(key): + protectedData[key] = value - return protected_data + return protectedData ``` ### SQL Query Generation ```sql --- Example: Get workflows with RBAC filtering for single role +-- Example: Get workflows with RBAC filtering for multiple roles SELECT w.* FROM "ChatWorkflow" w -JOIN "AccessRule" ar ON ar.roleLabel = %s AND ar.tableName = 'ChatWorkflow' +JOIN "AccessRule" ar ON + ar.roleLabel = ANY(%s) -- Array of role labels + AND ar.context = 'DATA' + AND (ar.item = 'ChatWorkflow' OR ar.item IS NULL) WHERE - -- User access control based on role permissions + -- User access control based on role permissions (opening/union logic) ( -- All records access (ar.read = 'a') OR - -- Mandate records access + -- Group records access (group context is the mandate) (ar.read = 'g' AND w.mandateId = %s) OR -- My records access @@ -333,14 +578,17 @@ LIMIT 100; ## Migration Strategy ### Phase 1: Database Schema -1. Create `AccessRule` table -2. Add `roleLabel` column to `User` table -3. Create indexes for performance +1. Create `AccessRule` table with `context`, `item`, `view` fields +2. Add `roleLabels` column (array) to `User` table +3. Create indexes for performance (roleLabel, context, item) +4. Migrate existing `tableName`/`fieldName` to `item` format ### Phase 2: Data Migration -1. Create default access rules for each role label -2. Migrate existing users to appropriate role labels +1. Create default access rules for each role label across all contexts (DATA, UI, RESOURCE) +2. Migrate existing users to appropriate role labels (convert single roleLabel to roleLabels array) 3. Create access rules based on current UAM logic +4. Migrate tableName/fieldName to item format for DATA context +5. Create UI and RESOURCE access rules as needed ### Phase 3: Code Migration 1. Update database connector with RBAC support @@ -388,28 +636,6 @@ LIMIT 100; - 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 @@ -424,18 +650,25 @@ LIMIT 100; ## Role Management -### Simple Role Assignment +### Multiple Role Assignment -Users have a single role label that directly maps to access rules: +Users can have multiple role labels that are combined using opening (union) logic: ```json { "userId": "user-123", - "roleLabel": "admin", + "roleLabels": ["admin", "viewer"], "mandateId": "mandate-456" } ``` +**Role Combination Logic:** +- All roles are **opening roles** (union) +- If ANY role enables something, it is enabled +- Permissions from all roles are combined +- Most permissive access level wins for DATA context +- View permission is true if ANY role has view=true + ### Role Label Options - **sysadmin**: System administrator with full access - **admin**: Administrator with mandate-level access @@ -444,30 +677,30 @@ Users have a single role label that directly maps to access rules: ## Access Rule Examples -### Generic Rules (All Tables) - -Generic rules provide a powerful way to set default permissions that apply across all tables: +### DATA Context Examples #### Example 1: Viewer Role - Read-Only Access to All Tables ```json { "roleLabel": "viewer", - "tableName": null, - "fieldName": null, + "context": "DATA", + "item": null, + "view": true, "read": "g", "create": "n", "update": "n", "delete": "n" } ``` -**Effect**: Viewers can read mandate records in ALL tables, but cannot create, update, or delete anything. +**Effect**: Viewers can read group records in ALL tables (group context is the mandate), but cannot create, update, or delete anything. #### Example 2: SysAdmin Role - Full Access to All Tables ```json { "roleLabel": "sysadmin", - "tableName": null, - "fieldName": null, + "context": "DATA", + "item": null, + "view": true, "read": "a", "create": "a", "update": "a", @@ -480,8 +713,9 @@ Generic rules provide a powerful way to set default permissions that apply acros ```json { "roleLabel": "user", - "tableName": null, - "fieldName": null, + "context": "DATA", + "item": null, + "view": true, "read": "m", "create": "m", "update": "m", @@ -490,16 +724,13 @@ Generic rules provide a powerful way to set default permissions that apply acros ``` **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, + "context": "DATA", + "item": "UserInDB", + "view": true, "read": "g", "create": "g", "update": "g", @@ -511,8 +742,9 @@ Table-specific rules override generic rules for particular tables: ```json { "roleLabel": "user", - "tableName": "FileItem", - "fieldName": null, + "context": "DATA", + "item": "FileItem", + "view": true, "read": "g", "create": "g", "update": "g", @@ -521,30 +753,13 @@ Table-specific rules override generic rules for particular tables: ``` **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 +#### Example 6: User Role - Email Field Access ```json { "roleLabel": "user", - "tableName": "UserInDB", - "fieldName": "email", + "context": "DATA", + "item": "UserInDB.email", + "view": true, "read": "a", "create": "a", "update": "a", @@ -553,28 +768,194 @@ Field-specific rules provide the most granular control: ``` **Effect**: Users can read and modify email addresses of all users, but cannot delete them. -## Benefits of Generic Rules +### UI Context Examples + +#### Example 7: User Role - Playground Component Access +```json +{ + "roleLabel": "user", + "context": "UI", + "item": "playground", + "view": true +} +``` +**Effect**: Users can view the playground component. + +#### Example 8: Admin Role - Voice Settings Access +```json +{ + "roleLabel": "admin", + "context": "UI", + "item": "playground.voice.settings", + "view": true +} +``` +**Effect**: Admins can view voice settings in the playground. + +#### Example 9: Viewer Role - Hide Search Feature +```json +{ + "roleLabel": "viewer", + "context": "UI", + "item": "chatbot.search", + "view": false +} +``` +**Effect**: Viewers cannot see the search feature in the chatbot. + +#### Example 9b: Rule Specificity - Generic Allows, Specific Denies +```json +// Generic rule: Allow all UI +{ + "roleLabel": "user", + "context": "UI", + "item": null, + "view": true +} + +// Specific rule: Hide voice settings (overrides generic) +{ + "roleLabel": "user", + "context": "UI", + "item": "playground.voice.settings", + "view": false +} +``` +**Effect**: Users can view all UI elements EXCEPT `playground.voice.settings` (specific rule overrides generic - most specific wins). + +### RESOURCE Context Examples + +#### Example 10: User Role - Anthropic AI Model Access +```json +{ + "roleLabel": "user", + "context": "RESOURCE", + "item": "ai.model.anthropic", + "view": true +} +``` +**Effect**: Users can access the Anthropic AI model. + +#### Example 11: Admin Role - Jira Action Access +```json +{ + "roleLabel": "admin", + "context": "RESOURCE", + "item": "ai.action.jira", + "view": true +} +``` +**Effect**: Admins can access the Jira action resource. + +#### Example 12: Viewer Role - Hide All AI Models +```json +{ + "roleLabel": "viewer", + "context": "RESOURCE", + "item": "ai.model", + "view": false +} +``` +**Effect**: Viewers cannot access any AI models. + +### Multiple Roles Example + +**User Configuration:** +```json +{ + "userId": "user-123", + "roleLabels": ["user", "viewer"] +} +``` + +**Rules:** +- Rule 1: `{roleLabel: "user", context: "UI", item: "playground", view: false}` - User role hides playground +- Rule 2: `{roleLabel: "viewer", context: "UI", item: "playground", view: true}` - Viewer role shows playground + +**Resolution Process:** +1. **Within "user" role**: Most specific rule for "playground" is `view: false` +2. **Within "viewer" role**: Most specific rule for "playground" is `view: true` +3. **Across roles**: Union logic applies - if ANY role has `view: true`, then `view: true` + +**Result**: Playground is visible (viewer role enables it - opening/union logic across roles) + +### Rule Specificity Example + +**User Configuration:** +```json +{ + "userId": "user-456", + "roleLabels": ["user"] +} +``` + +**Rules for "user" role:** +- Generic: `{roleLabel: "user", context: "UI", item: null, view: true}` - Allow all UI +- Specific: `{roleLabel: "user", context: "UI", item: "playground.voice.settings", view: false}` - Hide voice settings + +**Resolution Process:** +1. **For item "playground.voice.settings"**: Find most specific matching rule +2. **Specific rule found**: `item: "playground.voice.settings"` with `view: false` +3. **Generic rule ignored**: Even though generic has `view: true`, specific rule wins + +**Result**: Voice settings are hidden (specific rule overrides generic - most specific wins within role) + +## Context-Specific Behavior + +### DATA Context + +- **Item Format**: `
` or `
.` (e.g., `"UserInDB"`, `"UserInDB.email"`) +- **Permissions**: Uses `read`, `create`, `update`, `delete` access levels +- **View Attribute**: Controls table/field accessibility +- **Opening Rights**: Read permission is prerequisite for CUD operations +- **System Field Protection**: Automatic protection for `id` and `_*` fields + +### UI Context + +- **Item Format**: Cascading string starting with UI name (e.g., `"playground"`, `"playground.voice"`, `"playground.voice.settings"`, `"chatbot.search"`) +- **Permissions**: Only uses `view` attribute (boolean) +- **View Attribute**: Controls visibility - only objects with `view: true` are shown +- **Hierarchical**: Supports component → feature → element hierarchy +- **No Opening Rights**: Not applicable to UI context + +### RESOURCE Context + +- **Item Format**: Cascading string (e.g., `"ai"`, `"ai.model"`, `"ai.model.anthropic"`, `"ai.action.jira"`) +- **Permissions**: Only uses `view` attribute (boolean) +- **View Attribute**: Controls accessibility - only resources with `view: true` are available +- **Hierarchical**: Supports category → type → specific resource hierarchy +- **No Opening Rights**: Not applicable to RESOURCE context + +## Benefits of Generic Rules and Context-Based System ### 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 +- Need separate rules for each table/UI element/resource +- 10 tables × 4 roles × 3 contexts = 120+ access rules - Difficult to maintain and prone to inconsistencies **With Generic Rules:** -- 1 generic rule per role = 4 access rules +- 1 generic rule per role per context = 12 access rules (4 roles × 3 contexts) - Override with specific rules only when needed - Much easier to maintain and understand +### Context Separation Benefits + +1. **Clear Separation**: DATA, UI, and RESOURCE contexts are clearly separated +2. **Unified Model**: Same rule structure for all contexts +3. **Flexible Item Naming**: Cascading item names support hierarchical access control +4. **View-Based UI Control**: Simple boolean `view` attribute controls UI visibility + ### 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 +1. **Default Permissions**: Set baseline permissions for all items in a context +2. **New Item Support**: Automatically apply permissions to new tables/UI elements/resources +3. **Bulk Permission Changes**: Update permissions across all items with one rule 4. **Role Templates**: Create role templates that work out-of-the-box +5. **UI Feature Flags**: Use view attribute to enable/disable features per role ### Rule Management Best Practices @@ -606,8 +987,9 @@ protected_data = db_connector.enforce_system_field_protection(user_data, "update ```json { "roleLabel": "admin", - "tableName": "UserInDB", - "fieldName": "id", + "context": "DATA", + "item": "UserInDB.id", + "view": true, "read": "a", "create": "a", // ❌ Ignored - system field protection "update": "a", // ❌ Ignored - system field protection diff --git a/appdoc/doc_security_role_based_access_implementation_plan.md b/appdoc/doc_security_role_based_access_implementation_plan.md new file mode 100644 index 0000000..0d0d5cc --- /dev/null +++ b/appdoc/doc_security_role_based_access_implementation_plan.md @@ -0,0 +1,574 @@ +# RBAC Implementation Plan + +## Overview + +This document outlines the implementation plan for migrating from the current User Access Management (UAM) system to the new Role-Based Access Control (RBAC) system as specified in `doc_security_role_based_access.md`. + +## Implementation Phases + +### Phase 1: Database Schema and Data Models + +#### 1.1 Create RBAC Data Models +**File**: `gateway/modules/datamodels/datamodelRbac.py` + +**New Models**: +- `AccessRuleContext` (Enum): DATA, UI, RESOURCE +- `AccessRule` (BaseModel): Complete RBAC rule model with context, item, view, read, create, update, delete +- `AccessLevel` (Enum): Already exists in `datamodelUam.py` - verify and ensure consistency + +**Dependencies**: +- Import from `datamodelUam.py`: `AccessLevel`, `User` +- Use `ModelMixin` pattern from existing models +- Register model labels using `registerModelLabels()` + +**Status**: ✅ `AccessLevel` already exists in `datamodelUam.py` +**Action**: Create `datamodelRbac.py` with `AccessRule` and `AccessRuleContext` + +#### 1.2 Update User Model +**File**: `gateway/modules/datamodels/datamodelUam.py` + +**Changes**: +- Replace `privilege: UserPrivilege` with `roleLabels: List[str]` +- Update `frontend_options` to use `"user.role"` string reference +- Keep `UserPrivilege` enum for backward compatibility during migration + +**Migration Strategy**: +- Add `roleLabels` field alongside `privilege` during transition +- Migration script will populate `roleLabels` from `privilege` +- After migration, `privilege` can be deprecated + +**Status**: ⚠️ Partial - `AccessLevel` exists, `roleLabels` needs to be added + +#### 1.3 Database Schema Migration +**File**: Database migration script (to be created) + +**Schema Changes**: +1. Create `AccessRule` table: + - `id` (UUID, primary key) + - `roleLabel` (string, indexed) + - `context` (enum: DATA, UI, RESOURCE, indexed) + - `item` (string, nullable, indexed) + - `view` (boolean) + - `read` (AccessLevel enum, nullable) + - `create` (AccessLevel enum, nullable) + - `update` (AccessLevel enum, nullable) + - `delete` (AccessLevel enum, nullable) + - Standard fields: `_createdAt`, `_createdBy`, `_updatedAt`, `_updatedBy` + +2. Update `User` table: + - Add `roleLabels` column (array of strings) + - Keep `privilege` column for backward compatibility + +3. Create indexes: + - `AccessRule(roleLabel, context, item)` - composite index for rule lookups + - `AccessRule(context, item)` - for context/item queries + +**Status**: 📝 To be implemented + +--- + +### Phase 2: RBAC Interface and Core Logic + +#### 2.1 Create RBAC Interface +**File**: `gateway/modules/interfaces/interfaceRbac.py` + +**Purpose**: Centralized RBAC logic and permission resolution + +**Key Functions**: +- `getUserPermissions(user: User, context: AccessRuleContext, item: str) -> UserPermissions` +- `findMostSpecificRule(rules: List[AccessRule], item: str) -> Optional[AccessRule]` +- `validateAccessRule(rule: AccessRule) -> bool` +- `_isMorePermissive(level1: AccessLevel, level2: AccessLevel) -> bool` + +**Dependencies**: +- `datamodelRbac.py`: `AccessRule`, `AccessRuleContext` +- `datamodelUam.py`: `User`, `UserPermissions`, `AccessLevel` +- `connectorDbPostgre.py`: Database connector for rule queries + +**References Check**: ✅ +- Can import from `datamodelUam.py` and `datamodelRbac.py` +- Can use database connector from `interfaceDbAppObjects.py` pattern +- Follows same pattern as `interfaceDbAppAccess.py` + +**Status**: 📝 To be implemented + +#### 2.2 Integrate RBAC CRUD into AppObjects Interface +**File**: `gateway/modules/interfaces/interfaceDbAppObjects.py` + +**New Methods** (camelCase): +- `createAccessRule(accessRule: AccessRule) -> AccessRule` +- `getAccessRule(ruleId: str) -> Optional[AccessRule]` +- `updateAccessRule(ruleId: str, accessRule: AccessRule) -> AccessRule` +- `deleteAccessRule(ruleId: str) -> bool` +- `getAccessRules(roleLabel: Optional[str] = None, context: Optional[AccessRuleContext] = None, item: Optional[str] = None) -> List[AccessRule]` +- `getAccessRulesForRoles(roleLabels: List[str], context: AccessRuleContext, item: str) -> List[AccessRule]` + +**Integration Points**: +- Use existing `self.db.recordCreate()`, `self.db.recordUpdate()`, `self.db.recordDelete()`, `self.db.getRecordset()` methods +- Follow same pattern as existing CRUD methods (e.g., `createUser()`, `updateUser()`) +- Add RBAC permission checks using `interfaceRbac.getUserPermissions()` + +**References Check**: ✅ +- Can use `self.db` database connector (already initialized) +- Can import `AccessRule` from `datamodelRbac.py` +- Follows existing interface pattern + +**Status**: 📝 To be implemented + +--- + +### Phase 3: Database Bootstrap and Initialization + +#### 3.1 Create Centralized Bootstrap Interface +**File**: `gateway/modules/interfaces/interfaceBootstrap.py` + +**Purpose**: Centralized bootstrap module containing all initialization logic, specific data (roles, user names, mandate profiles), and RBAC rules converted from existing UAM logic. + +**Key Functions**: +- `initBootstrap(db: DatabaseConnector) -> None` - Main bootstrap entry point +- `initRootMandate(db: DatabaseConnector) -> str` - Creates root mandate, returns mandateId +- `initAdminUser(db: DatabaseConnector, mandateId: str) -> str` - Creates admin user, returns userId +- `initEventUser(db: DatabaseConnector, mandateId: str) -> str` - Creates event user, returns userId +- `initRbacRules(db: DatabaseConnector) -> None` - Creates all RBAC rules from UAM logic +- `createDefaultRoleRules(db: DatabaseConnector) -> None` - Creates default role rules +- `createTableSpecificRules(db: DatabaseConnector) -> None` - Creates table-specific rules from UAM logic +- `assignInitialUserRoles(db: DatabaseConnector, adminUserId: str, eventUserId: str) -> None` - Assigns roles to initial users + +**Bootstrap Data Configuration**: +- **Root Mandate**: name="Root", language="en", enabled=True +- **Admin User**: username="admin", email="admin@example.com", fullName="Administrator", roleLabels=["sysadmin"] +- **Event User**: username="event", email="event@example.com", fullName="Event", roleLabels=["sysadmin"] +- **Roles**: sysadmin, admin, user, viewer + +**RBAC Rules to Create** (converted from `interfaceDbAppAccess.py` logic): + +1. **Generic Role Rules** (context: DATA, item: null): + - **sysadmin**: view=true, read="a", create="a", update="a", delete="a" + - **admin**: view=true, read="g", create="g", update="g", delete="n" + - **user**: view=true, read="m", create="m", update="m", delete="m" + - **viewer**: view=true, read="g", create="n", update="n", delete="n" + +2. **Table-Specific Rules** (context: DATA, item: `
`): + - **Mandate**: + - sysadmin: view=true, read="a", create="a", update="a", delete="a" + - admin/user/viewer: view=false (no access) + + - **UserInDB**: + - sysadmin: view=true, read="a", create="a", update="a", delete="a" + - admin: view=true, read="g", create="g", update="g", delete="g" (mandate scope) + - user/viewer: view=true, read="m", create="n", update="m", delete="n" (own records only) + + - **UserConnection**: + - sysadmin: view=true, read="a", create="a", update="a", delete="a" + - admin: view=true, read="g", create="g", update="g", delete="g" (users in mandate) + - user/viewer: view=true, read="m", create="m", update="m", delete="m" (own connections) + + - **DataNeutraliserConfig**: + - sysadmin: view=true, read="a", create="a", update="a", delete="a" + - admin: view=true, read="g", create="g", update="g", delete="g" (mandate scope) + - user/viewer: view=true, read="m", create="m", update="m", delete="m" (own configs) + + - **DataNeutralizerAttributes**: + - sysadmin: view=true, read="a", create="a", update="a", delete="a" + - admin: view=true, read="g", create="g", update="g", delete="g" (mandate scope) + - user/viewer: view=true, read="m", create="m", update="m", delete="m" (own attributes) + + - **AuthEvent**: + - sysadmin/admin: view=true, read="a", create="n", update="n", delete="a" + - user/viewer: view=true, read="m", create="n", update="n", delete="n" (own events only) + + - **Default Tables** (all other tables): + - sysadmin: view=true, read="a", create="a", update="a", delete="a" + - admin: view=true, read="g", create="g", update="g", delete="g" (mandate scope) + - user/viewer: view=true, read="m", create="m", update="m", delete="m" (own records) + +3. **UI Context Rules** (context: UI): + - Generic UI access for all roles (to be defined based on requirements) + - Component-specific rules as needed + +4. **RESOURCE Context Rules** (context: RESOURCE): + - AI model access rules (to be defined based on requirements) + - Action access rules (to be defined based on requirements) + +**Integration with AppObjects Interface**: +**File**: `gateway/modules/interfaces/interfaceDbAppObjects.py` + +**Replace `_initRecords()` method**: +```python +def _initRecords(self): + """Initialize standard records if they don't exist.""" + from modules.interfaces.interfaceBootstrap import initBootstrap + initBootstrap(self.db) +``` + +**Remove Methods** (moved to `interfaceBootstrap.py`): +- `_initRootMandate()` → `interfaceBootstrap.initRootMandate()` +- `_initAdminUser()` → `interfaceBootstrap.initAdminUser()` +- `_initEventUser()` → `interfaceBootstrap.initEventUser()` + +**Status**: 📝 To be implemented + +#### 3.2 UAM Logic to RBAC Rules Conversion + +**Source Files to Analyze**: +- `gateway/modules/interfaces/interfaceDbAppAccess.py` +- `gateway/modules/interfaces/interfaceDbChatAccess.py` +- `gateway/modules/interfaces/interfaceDbComponentAccess.py` + +**Conversion Mapping**: + +| UAM Logic (interfaceDbAppAccess.py) | RBAC Rule (context: DATA) | +|-------------------------------------|---------------------------| +| `table_name == "Mandate"` + `privilege == SYSADMIN` | `roleLabel="sysadmin"`, `item="Mandate"`, `view=true`, `read="a"`, `create="a"`, `update="a"`, `delete="a"` | +| `table_name == "UserInDB"` + `privilege == SYSADMIN` | `roleLabel="sysadmin"`, `item="UserInDB"`, `view=true`, `read="a"`, `create="a"`, `update="a"`, `delete="a"` | +| `table_name == "UserInDB"` + `privilege == ADMIN` | `roleLabel="admin"`, `item="UserInDB"`, `view=true`, `read="g"`, `create="g"`, `update="g"`, `delete="g"` | +| `table_name == "UserInDB"` + `privilege == USER` | `roleLabel="user"`, `item="UserInDB"`, `view=true`, `read="m"`, `create="n"`, `update="m"`, `delete="n"` | +| `table_name == "UserConnection"` + `privilege == SYSADMIN` | `roleLabel="sysadmin"`, `item="UserConnection"`, `view=true`, `read="a"`, `create="a"`, `update="a"`, `delete="a"` | +| `table_name == "UserConnection"` + `privilege == ADMIN` | `roleLabel="admin"`, `item="UserConnection"`, `view=true`, `read="g"`, `create="g"`, `update="g"`, `delete="g"` | +| `table_name == "UserConnection"` + `privilege == USER` | `roleLabel="user"`, `item="UserConnection"`, `view=true`, `read="m"`, `create="m"`, `update="m"`, `delete="m"` | +| `table_name == "DataNeutraliserConfig"` + `privilege == SYSADMIN` | `roleLabel="sysadmin"`, `item="DataNeutraliserConfig"`, `view=true`, `read="a"`, `create="a"`, `update="a"`, `delete="a"` | +| `table_name == "DataNeutraliserConfig"` + `privilege == ADMIN` | `roleLabel="admin"`, `item="DataNeutraliserConfig"`, `view=true`, `read="g"`, `create="g"`, `update="g"`, `delete="g"` | +| `table_name == "DataNeutraliserConfig"` + `privilege == USER` | `roleLabel="user"`, `item="DataNeutraliserConfig"`, `view=true`, `read="m"`, `create="m"`, `update="m"`, `delete="m"` | +| `table_name == "DataNeutralizerAttributes"` + `privilege == SYSADMIN` | `roleLabel="sysadmin"`, `item="DataNeutralizerAttributes"`, `view=true`, `read="a"`, `create="a"`, `update="a"`, `delete="a"` | +| `table_name == "DataNeutralizerAttributes"` + `privilege == ADMIN` | `roleLabel="admin"`, `item="DataNeutralizerAttributes"`, `view=true`, `read="g"`, `create="g"`, `update="g"`, `delete="g"` | +| `table_name == "DataNeutralizerAttributes"` + `privilege == USER` | `roleLabel="user"`, `item="DataNeutralizerAttributes"`, `view=true`, `read="m"`, `create="m"`, `update="m"`, `delete="m"` | +| `table_name == "AuthEvent"` + `privilege in [SYSADMIN, ADMIN]` | `roleLabel="sysadmin"`/`"admin"`, `item="AuthEvent"`, `view=true`, `read="a"`, `create="n"`, `update="n"`, `delete="a"` | +| `table_name == "AuthEvent"` + `privilege == USER` | `roleLabel="user"`, `item="AuthEvent"`, `view=true`, `read="m"`, `create="n"`, `update="n"`, `delete="n"` | +| Default tables + `privilege == SYSADMIN` | `roleLabel="sysadmin"`, `item=null`, `view=true`, `read="a"`, `create="a"`, `update="a"`, `delete="a"` | +| Default tables + `privilege == ADMIN` | `roleLabel="admin"`, `item=null`, `view=true`, `read="g"`, `create="g"`, `update="g"`, `delete="g"` | +| Default tables + `privilege == USER` | `roleLabel="user"`, `item=null`, `view=true`, `read="m"`, `create="m"`, `update="m"`, `delete="m"` | + +**Implementation Steps**: +1. Read `interfaceDbAppAccess.py` and extract all `uam()` logic +2. Read `interfaceDbChatAccess.py` and extract all `uam()` logic +3. Read `interfaceDbComponentAccess.py` and extract all `uam()` logic +4. Map each permission check to RBAC rule format +5. Create `AccessRule` entries in `interfaceBootstrap.createTableSpecificRules()` +6. Test that RBAC rules produce same results as UAM logic + +**Status**: 📝 To be implemented + +--- + +### Phase 4: Database Connector RBAC Support + +#### 4.1 Extend Database Connector +**File**: `gateway/modules/connectors/connectorDbPostgre.py` + +**New Methods**: +- `getRecordsetWithRBAC(modelClass: Type[BaseModel], currentUser: User, recordFilter: Dict = None, orderBy: str = None, limit: int = None) -> List[Dict]` +- `buildRbacWhereClause(accessRules: List[AccessRule], currentUser: User) -> str` +- `executeQueryWithRbac(...) -> List[Dict]` + +**SQL Query Enhancement**: +- Modify SQL generation to include RBAC WHERE clauses +- Support multiple roles with UNION logic +- Optimize queries with proper indexes + +**Status**: 📝 To be implemented + +--- + +### Phase 5: Migration from UAM to RBAC + +#### 5.1 Create Migration Script +**File**: `gateway/modules/migration/migrateUamToRbac.py` + +**Migration Steps**: +1. **Schema Migration**: + - Create `AccessRule` table + - Add `roleLabels` column to `User` table + - Create indexes + +2. **Data Migration**: + - Convert `User.privilege` → `User.roleLabels`: + - `UserPrivilege.SYSADMIN` → `["sysadmin"]` + - `UserPrivilege.ADMIN` → `["admin"]` + - `UserPrivilege.USER` → `["user"]` + - Create default access rules based on current UAM logic + - Map existing table-specific permissions to RBAC rules + +3. **Validation**: + - Verify all users have roleLabels assigned + - Verify access rules match current UAM behavior + - Test permission resolution + +**Status**: 📝 To be implemented + +#### 5.2 Update Interface Methods +**Files to Update**: +- `gateway/modules/interfaces/interfaceDbAppObjects.py` +- `gateway/modules/interfaces/interfaceDbChatObjects.py` +- `gateway/modules/interfaces/interfaceDbComponentObjects.py` + +**Changes**: +- Replace `_uam()` calls with `getRecordsetWithRBAC()` +- Replace `_canModify()` checks with RBAC permission checks +- Update all `getRecordset()` calls to use RBAC filtering + +**Status**: 📝 To be implemented + +--- + +### Phase 6: UI and Resource Access Control + +#### 6.1 UI Access Control Integration +**Files**: Frontend integration (out of scope for backend) + +**Backend Support**: +- Ensure `getUserPermissions()` works for UI context +- Create API endpoint: `GET /api/rbac/permissions?context=UI&item=playground.voice.settings` +- Return `UserPermissions` model with `view` attribute + +**Status**: 📝 To be implemented + +#### 6.2 Resource Access Control Integration +**Files**: Feature modules that use resources + +**Integration Points**: +- AI model selection: Check `getUserPermissions(context=RESOURCE, item="ai.model.anthropic")` +- Action execution: Check permissions before allowing action execution +- Create helper functions in feature modules + +**Status**: 📝 To be implemented + +--- + +### Phase 7: Testing and Validation + +#### 7.1 Unit Tests +**Files**: `gateway/tests/unit/rbac/` + +**Test Cases**: +- Permission resolution (single role, multiple roles) +- Rule specificity (generic vs specific) +- Opening rights principle +- System field protection +- Bootstrap initialization + +**Status**: 📝 To be implemented + +#### 7.2 Integration Tests +**Files**: `gateway/tests/integration/rbac/` + +**Test Cases**: +- Database queries with RBAC filtering +- User CRUD operations with RBAC +- Multi-role permission combination +- Migration from UAM to RBAC + +**Status**: 📝 To be implemented + +#### 7.3 Performance Tests +**Test Cases**: +- Query performance with RBAC (compare to current UAM) +- Memory usage reduction +- Database load reduction + +**Status**: 📝 To be implemented + +--- + +## Module Adaptation Summary + +### Modules to Create + +1. **`gateway/modules/datamodels/datamodelRbac.py`** + - `AccessRule` model + - `AccessRuleContext` enum + - Model label registration + +2. **`gateway/modules/interfaces/interfaceRbac.py`** + - RBAC core logic + - Permission resolution functions + - Rule validation functions + +3. **`gateway/modules/interfaces/interfaceBootstrap.py`** ⭐ **NEW** + - Centralized bootstrap interface + - All initialization logic (mandate, users, RBAC rules) + - Bootstrap data configuration (roles, user names, mandate profiles) + - RBAC rules converted from UAM logic (`interfaceDbAppAccess.py`, `interfaceDbChatAccess.py`, `interfaceDbComponentAccess.py`) + +4. **`gateway/modules/migration/migrateUamToRbac.py`** + - Migration script + - Data transformation logic + - Validation functions + +### Modules to Adapt + +1. **`gateway/modules/datamodels/datamodelUam.py`** ⚠️ **KEEP - Still Needed** + - ✅ Add `AccessLevel` enum (already done) + - ✅ Add `UserPermissions` model (already done) + - 📝 Add `roleLabels: List[str]` to `User` model + - 📝 Update `frontend_options` to use string references + - ⚠️ **Keep**: `User`, `Mandate`, `UserConnection` models (core data structures) + - ⚠️ **Deprecate**: `UserPrivilege` enum (replaced by `roleLabels` with RBAC) + +2. **`gateway/modules/interfaces/interfaceDbAppObjects.py`** + - 📝 Add RBAC CRUD methods + - 📝 Replace `_initRecords()` to call `interfaceBootstrap.initBootstrap()` + - 📝 Remove `_initRootMandate()`, `_initAdminUser()`, `_initEventUser()` (moved to `interfaceBootstrap.py`) + - 📝 Replace `_uam()` with RBAC-based filtering (Phase 5) + - 📝 Remove `self.access` initialization (no longer needed after RBAC migration) + +3. **`gateway/modules/connectors/connectorDbPostgre.py`** + - 📝 Add `getRecordsetWithRBAC()` method + - 📝 Add `buildRbacWhereClause()` method + - 📝 Add `executeQueryWithRbac()` method + - 📝 Enhance SQL generation for RBAC + +4. **`gateway/modules/interfaces/interfaceDbChatObjects.py`** + - 📝 Replace `_uam()` calls with RBAC filtering + - 📝 Update permission checks to use RBAC + +5. **`gateway/modules/interfaces/interfaceDbComponentObjects.py`** + - 📝 Replace `_uam()` calls with RBAC filtering + - 📝 Update permission checks to use RBAC + +6. **`gateway/modules/features/options/mainOptions.py`** (if created) + - 📝 Ensure `getOptions()` function exists for dynamic options + +### Modules to Remove (After Refactoring) + +1. **`gateway/modules/interfaces/interfaceDbAppAccess.py`** ❌ **REMOVE after Phase 5** + - ⚠️ **Convert all UAM logic to RBAC rules in `interfaceBootstrap.py`** + - Current UAM logic (`uam()`, `canModify()`) converted to AccessRule entries + - **Action**: Extract all permission logic from `uam()` and `canModify()` methods + - **Action**: Convert to RBAC rules in `interfaceBootstrap.createTableSpecificRules()` + - Remove after all interfaces migrated to RBAC and rules validated + +2. **`gateway/modules/interfaces/interfaceDbChatAccess.py`** ❌ **REMOVE after Phase 5** + - ⚠️ **Convert all UAM logic to RBAC rules in `interfaceBootstrap.py`** + - Similar to `interfaceDbAppAccess.py` + - Extract permission logic and convert to RBAC rules + - Remove after migration complete + +3. **`gateway/modules/interfaces/interfaceDbComponentAccess.py`** ❌ **REMOVE after Phase 5** + - ⚠️ **Convert all UAM logic to RBAC rules in `interfaceBootstrap.py`** + - Similar to `interfaceDbAppAccess.py` + - Extract permission logic and convert to RBAC rules + - Remove after migration complete + +**Migration Strategy for Access Modules**: +1. **Phase 3**: Analyze all `interface*Access.py` modules +2. **Phase 3**: Extract permission logic from `uam()` and `canModify()` methods +3. **Phase 3**: Convert to RBAC rules in `interfaceBootstrap.createTableSpecificRules()` +4. **Phase 5**: Replace all `_uam()` calls with RBAC filtering +5. **Phase 5**: Remove `self.access` initialization from interfaces +6. **Phase 9-10**: Delete `interface*Access.py` modules after validation + +**Note**: Keep these modules during migration for backward compatibility. Remove only after: +- All UAM logic converted to RBAC rules in bootstrap +- All interfaces use RBAC +- All tests pass +- Migration validation complete +- No references to old UAM methods remain + +### Database Schema Changes + +1. **New Table**: `AccessRule` +2. **Modified Table**: `User` (add `roleLabels` column) +3. **New Indexes**: Performance optimization for RBAC queries + +--- + +## Implementation Timeline + +### Week 1-2: Foundation +- ✅ Create `datamodelRbac.py` with `AccessRule` model +- ✅ Create `interfaceRbac.py` with core RBAC logic +- ✅ Create `interfaceBootstrap.py` with centralized bootstrap logic +- ✅ Extract bootstrap logic from `interfaceDbAppObjects.py` to `interfaceBootstrap.py` +- ✅ Analyze `interface*Access.py` modules and extract UAM logic +- ✅ Convert UAM logic to RBAC rules in `interfaceBootstrap.py` +- ✅ Update `datamodelUam.py` with `roleLabels` field +- ✅ Integrate `interfaceBootstrap.initBootstrap()` into `interfaceDbAppObjects.py` + +### Week 3-4: Database Integration +- 📝 Extend database connector with RBAC support +- 📝 Create migration script +- 📝 Test database schema changes +- 📝 Validate bootstrap initialization + +### Week 5-6: Interface Migration +- 📝 Add RBAC CRUD methods to `interfaceDbAppObjects.py` +- 📝 Update `interfaceDbChatObjects.py` to use RBAC +- 📝 Update `interfaceDbComponentObjects.py` to use RBAC +- 📝 Replace `_uam()` calls with RBAC filtering + +### Week 7-8: Testing & Optimization +- 📝 Write unit tests +- 📝 Write integration tests +- 📝 Performance testing +- 📝 Query optimization +- 📝 Documentation updates + +### Week 9-10: Cleanup & Deprecation +- 📝 Remove `interfaceDbAppAccess.py` (UAM logic converted to RBAC rules) +- 📝 Remove `interfaceDbChatAccess.py` (UAM logic converted to RBAC rules) +- 📝 Remove `interfaceDbComponentAccess.py` (UAM logic converted to RBAC rules) +- 📝 Deprecate `UserPrivilege` enum in `datamodelUam.py` (keep for backward compatibility, mark as deprecated) +- 📝 Final validation +- 📝 Production deployment + +--- + +## Risk Mitigation + +### Backward Compatibility +- Keep `UserPrivilege` enum during migration +- Maintain `_uam()` method alongside RBAC during transition +- Gradual migration allows rollback if needed + +### Data Integrity +- Migration script with validation +- Backup before migration +- Test migration on staging environment first + +### Performance +- Index optimization for RBAC queries +- Query performance testing before production +- Monitor database load after deployment + +--- + +## Success Criteria + +1. ✅ All users have `roleLabels` assigned +2. ✅ All access rules created and validated +3. ✅ RBAC filtering works for all data operations +4. ✅ Performance meets or exceeds current UAM system +5. ✅ All tests pass +6. ✅ No deprecated UAM code remains +7. ✅ Documentation updated + +--- + +## Notes + +- Follow camelCase naming convention for all functions and variables +- Internal functions use `_` prefix +- Use Pydantic models for type safety +- Maintain existing error handling patterns +- Follow existing logging patterns + +## Important Clarifications + +### `datamodelUam.py` Status +- ✅ **KEEP**: Core data models (`User`, `Mandate`, `UserConnection`) are still needed +- ✅ **KEEP**: `AccessLevel` enum (used by RBAC) +- ✅ **KEEP**: `UserPermissions` model (used by RBAC) +- ⚠️ **DEPRECATE**: `UserPrivilege` enum (replaced by `roleLabels` with RBAC rules) +- 📝 **ADD**: `roleLabels: List[str]` field to `User` model + +### `interface*Access.py` Modules Status +- ❌ **REMOVE**: All `interface*Access.py` modules after migration +- ⚠️ **CONVERT**: All UAM logic from these modules to RBAC rules in `interfaceBootstrap.py` +- 📝 **ACTION**: Extract permission logic from `uam()` and `canModify()` methods +- 📝 **ACTION**: Create corresponding `AccessRule` entries in bootstrap + +### Bootstrap Strategy +- ⭐ **CENTRALIZE**: All bootstrap logic in `interfaceBootstrap.py` +- 📝 **INCLUDE**: Mandate creation, user creation, RBAC rule initialization +- 📝 **INCLUDE**: All bootstrap data (roles, user names, mandate profiles) +- 📝 **INCLUDE**: RBAC rules converted from UAM logic