38 KiB
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
- Role Management: Define roles with mandate scope (users can have multiple roles)
- Access Rules: Matrix of permissions per role/context/item
- Context Types: DATA (database tables/fields), UI (interface elements), RESOURCE (system resources)
- Database Integration: Native filtering in SQL queries for DATA context
- UI Integration: Component visibility and feature access control
- 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:
- Detect that it's a string (not a list)
- Fetch options from
/api/options/{optionsName} - 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 formatstr: 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
valueandlabelkeys - 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:
- Generic Rules (
item = null): Apply to all tables and fields - Table Rules (
item = "UserInDB"): Apply to all fields in a specific table - Field Rules (
item = "UserInDB.email"): Apply to a specific field in a specific table
For UI Context:
- Generic Rules (
item = null): Apply to all UI elements - Component Rules (
item = "playground"): Apply to entire component - Feature Rules (
item = "playground.voice"): Apply to feature within component - Element Rules (
item = "playground.voice.settings"): Apply to specific UI element
For RESOURCE Context:
- Generic Rules (
item = null): Apply to all resources - Category Rules (
item = "ai"): Apply to resource category - Type Rules (
item = "ai.model"): Apply to resource type - Specific Rules (
item = "ai.model.anthropic"): Apply to specific resource
Rule Resolution Order (most specific wins within a role):
- Most specific item match (longest matching prefix) - This overrides generic rules
- 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: trueare shown. - For RESOURCE Context: Controls whether resources are accessible. Only resources with
view: trueare 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: truebut specific rule hasview: false, thenview: falseapplies (specific overrides generic) - If generic rule has
view: falsebut specific rule hasview: true, thenview: trueapplies (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
idfields 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) hasview: truebut specific rule (item = "playground.voice.settings") hasview: false, thenview: falseapplies - 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:
- For each role: Find the most specific matching rule (Level 1)
- 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
- "user" role → most specific rule for "playground" →
- Level 2 Resolution: Union logic →
view: trueORview: false→view: 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
- Create
AccessRuletable withcontext,item,viewfields - Add
roleLabelscolumn (array) toUsertable - Create indexes for performance (roleLabel, context, item)
- Migrate existing
tableName/fieldNametoitemformat
Phase 2: Data Migration
- Create default access rules for each role label across all contexts (DATA, UI, RESOURCE)
- Migrate existing users to appropriate role labels (convert single roleLabel to roleLabels array)
- Create access rules based on current UAM logic
- Migrate tableName/fieldName to item format for DATA context
- Create UI and RESOURCE access rules as needed
Phase 3: Code Migration
- Update database connector with RBAC support
- Replace
_uam()calls withgetRecordsetWithRBAC() - Update all interface methods
Phase 4: Testing & Optimization
- Performance testing
- Security validation
- 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:
- Within "user" role: Most specific rule for "playground" is
view: false - Within "viewer" role: Most specific rule for "playground" is
view: true - Across roles: Union logic applies - if ANY role has
view: true, thenview: 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:
- For item "playground.voice.settings": Find most specific matching rule
- Specific rule found:
item: "playground.voice.settings"withview: false - 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,deleteaccess levels - View Attribute: Controls table/field accessibility
- Opening Rights: Read permission is prerequisite for CUD operations
- System Field Protection: Automatic protection for
idand_*fields
UI Context
- Item Format: Cascading string starting with UI name (e.g.,
"playground","playground.voice","playground.voice.settings","chatbot.search") - Permissions: Only uses
viewattribute (boolean) - View Attribute: Controls visibility - only objects with
view: trueare 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
viewattribute (boolean) - View Attribute: Controls accessibility - only resources with
view: trueare 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
- Clear Separation: DATA, UI, and RESOURCE contexts are clearly separated
- Unified Model: Same rule structure for all contexts
- Flexible Item Naming: Cascading item names support hierarchical access control
- View-Based UI Control: Simple boolean
viewattribute controls UI visibility
Common Use Cases for Generic Rules
- Default Permissions: Set baseline permissions for all items in a context
- New Item Support: Automatically apply permissions to new tables/UI elements/resources
- Bulk Permission Changes: Update permissions across all items with one rule
- Role Templates: Create role templates that work out-of-the-box
- UI Feature Flags: Use view attribute to enable/disable features per role
Rule Management Best Practices
- Start with Generic Rules: Define broad permissions first
- Override Selectively: Add specific rules only where needed
- Use Descriptive Names: Make role labels clear and meaningful
- Test Incrementally: Add rules one at a time and test
- 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", ...}