1025 lines
38 KiB
Markdown
1025 lines
38 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.
|
||
|
||
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:
|
||
|
||
- **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 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
|
||
- **Performance**: 80-95% reduction in data transfer and memory usage
|
||
|
||
## System Architecture
|
||
|
||
### Core Components
|
||
|
||
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 for static, predefined options:
|
||
```python
|
||
frontend_options=[
|
||
{"value": "a", "label": {"en": "All Records", "fr": "Tous les enregistrements"}},
|
||
{"value": "m", "label": {"en": "My Records", "fr": "Mes enregistrements"}}
|
||
]
|
||
```
|
||
|
||
**Use static lists when:**
|
||
- Options are fixed and don't change based on user context
|
||
- Options are simple enums or constants
|
||
- Options don't require database queries
|
||
|
||
#### 2. String Reference (for dynamic/custom types)
|
||
A string identifier that references dynamic options from the Options API:
|
||
```python
|
||
frontend_options="user.role" # Frontend fetches from /api/options/user.role
|
||
```
|
||
|
||
**Use string references when:**
|
||
- Options come from the database (e.g., user connections)
|
||
- Options are context-aware (filtered by current user's permissions)
|
||
- Options need centralized management (e.g., role definitions)
|
||
- Options may change frequently
|
||
|
||
**Dynamic Options API**: When `frontend_options` is a string reference, the frontend must:
|
||
1. Detect that it's a string (not a list)
|
||
2. Fetch options from `/api/options/{optionsName}`
|
||
3. Use the returned options for the select/multiselect field
|
||
|
||
**Available Option Names**:
|
||
- `"user.role"` - User role options (sysadmin, admin, user, viewer)
|
||
- `"user.connection"` - User connection types (context-aware, requires currentUser)
|
||
- `"auth.authority"` - Authentication authorities (local, google, msft)
|
||
- `"connection.status"` - Connection statuses (active, inactive, expired, error)
|
||
|
||
**Type Definition**: The `frontend_options` attribute is typed as `Union[List[Dict[str, Any]], str]`:
|
||
- `List[Dict[str, Any]]`: Static list format
|
||
- `str`: String reference format
|
||
|
||
See `gateway/modules/shared/frontendOptionsTypes.py` for type definitions and utility functions.
|
||
|
||
### 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(
|
||
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="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": "DATA", "label": {"en": "Data", "fr": "Données"}},
|
||
{"value": "UI", "label": {"en": "UI", "fr": "Interface"}},
|
||
{"value": "RESOURCE", "label": {"en": "Resource", "fr": "Ressource"}}
|
||
]
|
||
)
|
||
item: Optional[str] = Field(
|
||
None,
|
||
description="Item identifier (null = all items in context). Format: DATA: '<table>' or '<table>.<field>', 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
|
||
)
|
||
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="Read permission level (only for DATA context)",
|
||
frontend_type="select",
|
||
frontend_readonly=False,
|
||
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": "Group Records", "fr": "Enregistrements du groupe"}},
|
||
{"value": "n", "label": {"en": "No Access", "fr": "Aucun accès"}}
|
||
]
|
||
)
|
||
create: Optional[AccessLevel] = Field(
|
||
None,
|
||
description="Create permission level (only for DATA context)",
|
||
frontend_type="select",
|
||
frontend_readonly=False,
|
||
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": "Group Records", "fr": "Enregistrements du groupe"}},
|
||
{"value": "n", "label": {"en": "No Access", "fr": "Aucun accès"}}
|
||
]
|
||
)
|
||
update: Optional[AccessLevel] = Field(
|
||
None,
|
||
description="Update permission level (only for DATA context)",
|
||
frontend_type="select",
|
||
frontend_readonly=False,
|
||
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": "Group Records", "fr": "Enregistrements du groupe"}},
|
||
{"value": "n", "label": {"en": "No Access", "fr": "Aucun accès"}}
|
||
]
|
||
)
|
||
delete: Optional[AccessLevel] = Field(
|
||
None,
|
||
description="Delete permission level (only for DATA context)",
|
||
frontend_type="select",
|
||
frontend_readonly=False,
|
||
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": "Group Records", "fr": "Enregistrements du groupe"}},
|
||
{"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)
|
||
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 ...
|
||
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="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 cascading hierarchy based on the `item` field:
|
||
|
||
#### 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
|
||
|
||
#### 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:**
|
||
- 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
|
||
|
||
### View Attribute
|
||
|
||
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. This principle applies only to DATA context.
|
||
|
||
### System Field Protection (DATA Context Only)
|
||
|
||
**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
|
||
- **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.
|
||
|
||
**Item Format for System Fields**: When referencing system fields in DATA context, use format `"<table>.<field>"` (e.g., `"UserInDB._createdAt"`).
|
||
|
||
### Role-Based Access Logic
|
||
|
||
The RBAC system uses a **two-level resolution** approach:
|
||
|
||
#### 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 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 is None or operation == "n":
|
||
continue # No access is always valid
|
||
if readLevel == AccessLevel.NONE:
|
||
return False # No CUD allowed if no read access
|
||
if readLevel == AccessLevel.MY and operation not in ["n", "m"]:
|
||
return False
|
||
if readLevel == AccessLevel.GROUP and operation not in ["n", "m", "g"]:
|
||
return False
|
||
|
||
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
|
||
|
||
### Enhanced Database Connector
|
||
|
||
The database connector will be extended to support RBAC filtering:
|
||
|
||
```python
|
||
class DatabaseConnector:
|
||
def getRecordsetWithRBAC(self,
|
||
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 roles
|
||
accessRules = self.getAccessRulesForRoles(
|
||
currentUser.roleLabels,
|
||
AccessRuleContext.DATA,
|
||
modelClass.__tablename__
|
||
)
|
||
|
||
# Build SQL query with RBAC WHERE clause
|
||
whereClause = self.buildRbacWhereClause(accessRules, currentUser)
|
||
|
||
# Execute optimized query
|
||
return self.executeQueryWithRbac(
|
||
modelClass,
|
||
recordFilter,
|
||
whereClause,
|
||
orderBy,
|
||
limit
|
||
)
|
||
|
||
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 isSystemField(self, fieldName: str) -> bool:
|
||
"""Check if a field is a protected system field"""
|
||
return fieldName == "id" or fieldName.startswith("_")
|
||
|
||
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
|
||
protectedData = {}
|
||
for key, value in data.items():
|
||
if not self.isSystemField(key):
|
||
protectedData[key] = value
|
||
|
||
return protectedData
|
||
```
|
||
|
||
### SQL Query Generation
|
||
|
||
```sql
|
||
-- Example: Get workflows with RBAC filtering for multiple roles
|
||
SELECT w.*
|
||
FROM "ChatWorkflow" w
|
||
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 (opening/union logic)
|
||
(
|
||
-- All records access
|
||
(ar.read = 'a')
|
||
OR
|
||
-- Group records access (group context is the mandate)
|
||
(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 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 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
|
||
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
|
||
|
||
|
||
## 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
|
||
|
||
### Multiple Role Assignment
|
||
|
||
Users can have multiple role labels that are combined using opening (union) logic:
|
||
|
||
```json
|
||
{
|
||
"userId": "user-123",
|
||
"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
|
||
- **user**: Regular user with own-record access
|
||
- **viewer**: Read-only access within mandate
|
||
|
||
## Access Rule Examples
|
||
|
||
### DATA Context Examples
|
||
|
||
#### Example 1: Viewer Role - Read-Only Access to All Tables
|
||
```json
|
||
{
|
||
"roleLabel": "viewer",
|
||
"context": "DATA",
|
||
"item": null,
|
||
"view": true,
|
||
"read": "g",
|
||
"create": "n",
|
||
"update": "n",
|
||
"delete": "n"
|
||
}
|
||
```
|
||
**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",
|
||
"context": "DATA",
|
||
"item": null,
|
||
"view": true,
|
||
"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",
|
||
"context": "DATA",
|
||
"item": null,
|
||
"view": true,
|
||
"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).
|
||
|
||
#### Example 4: Admin Role - User Table Access
|
||
```json
|
||
{
|
||
"roleLabel": "admin",
|
||
"context": "DATA",
|
||
"item": "UserInDB",
|
||
"view": true,
|
||
"read": "g",
|
||
"create": "g",
|
||
"update": "g",
|
||
"delete": "n"
|
||
}
|
||
```
|
||
|
||
#### Example 5: User Role - File Access (Override Generic Rule)
|
||
```json
|
||
{
|
||
"roleLabel": "user",
|
||
"context": "DATA",
|
||
"item": "FileItem",
|
||
"view": true,
|
||
"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: User Role - Email Field Access
|
||
```json
|
||
{
|
||
"roleLabel": "user",
|
||
"context": "DATA",
|
||
"item": "UserInDB.email",
|
||
"view": true,
|
||
"read": "a",
|
||
"create": "a",
|
||
"update": "a",
|
||
"delete": "n"
|
||
}
|
||
```
|
||
**Effect**: Users can read and modify email addresses of all users, but cannot delete them.
|
||
|
||
### 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**: `<table>` or `<table>.<field>` (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/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 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 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
|
||
|
||
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",
|
||
"context": "DATA",
|
||
"item": "UserInDB.id",
|
||
"view": true,
|
||
"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", ...}
|
||
```
|
||
|