wiki/z-archive/appdoc/doc_security_role_based_access.md

38 KiB
Raw Permalink Blame History

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. 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:

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:

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

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

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

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

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: falseview: true
  • Result: Playground is visible (viewer role enables it)

Permission Validation

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:

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

-- 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:

{
  "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

{
  "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

{
  "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)

{
  "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

{
  "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)

{
  "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

{
  "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

{
  "roleLabel": "user",
  "context": "UI",
  "item": "playground",
  "view": true
}

Effect: Users can view the playground component.

Example 8: Admin Role - Voice Settings Access

{
  "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

{
  "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

// 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

{
  "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

{
  "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

{
  "roleLabel": "viewer",
  "context": "RESOURCE",
  "item": "ai.model",
  "view": false
}

Effect: Viewers cannot access any AI models.

Multiple Roles Example

User Configuration:

{
  "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:

{
  "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

# 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

{
  "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

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