""" Migration script to convert UAM (User Access Management) to RBAC (Role-Based Access Control). This script: 1. Creates AccessRule table if it doesn't exist 2. Adds roleLabels column to User table if it doesn't exist 3. Converts User.privilege to User.roleLabels 4. Creates initial RBAC rules based on bootstrap logic """ import logging from typing import List, Dict, Any from modules.connectors.connectorDbPostgre import DatabaseConnector from modules.shared.configuration import APP_CONFIG from modules.datamodels.datamodelUam import UserInDB, UserPrivilege from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext from modules.datamodels.datamodelUam import AccessLevel from modules.interfaces.interfaceBootstrap import initRbacRules logger = logging.getLogger(__name__) def migrateUamToRbac(db: DatabaseConnector, dryRun: bool = False) -> Dict[str, Any]: """ Migrate from UAM to RBAC system. Args: db: Database connector instance dryRun: If True, only report what would be done without making changes Returns: Dictionary with migration results """ results = { "schemaChanges": [], "dataMigrations": [], "rulesCreated": 0, "usersUpdated": 0, "errors": [] } try: # Step 1: Ensure AccessRule table exists logger.info("Step 1: Ensuring AccessRule table exists") if not dryRun: db._ensureTableExists(AccessRule) results["schemaChanges"].append("AccessRule table ensured") else: results["schemaChanges"].append("Would ensure AccessRule table") # Step 2: Add roleLabels column to UserInDB table if it doesn't exist logger.info("Step 2: Adding roleLabels column to UserInDB table") if not dryRun: try: with db.connection.cursor() as cursor: # Check if column exists cursor.execute(""" SELECT column_name FROM information_schema.columns WHERE table_name = 'UserInDB' AND column_name = 'roleLabels' """) columnExists = cursor.fetchone() is not None if not columnExists: cursor.execute('ALTER TABLE "UserInDB" ADD COLUMN "roleLabels" JSONB DEFAULT \'[]\'::jsonb') db.connection.commit() results["schemaChanges"].append("Added roleLabels column to UserInDB") logger.info("Added roleLabels column to UserInDB table") else: results["schemaChanges"].append("roleLabels column already exists") logger.info("roleLabels column already exists in UserInDB table") except Exception as e: logger.error(f"Error adding roleLabels column: {e}") results["errors"].append(f"Error adding roleLabels column: {e}") db.connection.rollback() else: results["schemaChanges"].append("Would add roleLabels column to UserInDB") # Step 3: Convert User.privilege to User.roleLabels logger.info("Step 3: Converting User.privilege to User.roleLabels") if not dryRun: try: users = db.getRecordset(UserInDB) updatedCount = 0 for user in users: privilege = user.get("privilege") roleLabels = user.get("roleLabels", []) # Skip if already has roleLabels if roleLabels and isinstance(roleLabels, list) and len(roleLabels) > 0: logger.debug(f"User {user.get('id')} already has roleLabels: {roleLabels}") continue # Convert privilege to roleLabels if privilege == UserPrivilege.SYSADMIN.value: newRoleLabels = ["sysadmin"] elif privilege == UserPrivilege.ADMIN.value: newRoleLabels = ["admin"] elif privilege == UserPrivilege.USER.value: newRoleLabels = ["user"] else: # Default to user if privilege is unknown newRoleLabels = ["user"] logger.warning(f"Unknown privilege '{privilege}' for user {user.get('id')}, defaulting to 'user'") # Update user user["roleLabels"] = newRoleLabels db.recordModify(UserInDB, user["id"], user) updatedCount += 1 logger.info(f"Updated user {user.get('id')} ({user.get('username')}): {privilege} -> {newRoleLabels}") results["usersUpdated"] = updatedCount logger.info(f"Updated {updatedCount} users with roleLabels") except Exception as e: logger.error(f"Error converting user privileges: {e}") results["errors"].append(f"Error converting user privileges: {e}") else: # Dry run: count users that would be updated users = db.getRecordset(UserInDB) wouldUpdate = 0 for user in users: roleLabels = user.get("roleLabels", []) if not roleLabels or not isinstance(roleLabels, list) or len(roleLabels) == 0: wouldUpdate += 1 results["usersUpdated"] = wouldUpdate logger.info(f"Would update {wouldUpdate} users with roleLabels") # Step 4: Create RBAC rules if they don't exist logger.info("Step 4: Creating RBAC rules") if not dryRun: try: existingRules = db.getRecordset(AccessRule) if existingRules: results["rulesCreated"] = len(existingRules) results["dataMigrations"].append(f"RBAC rules already exist ({len(existingRules)} rules)") logger.info(f"RBAC rules already exist ({len(existingRules)} rules)") else: # Initialize RBAC rules using bootstrap logic initRbacRules(db) newRules = db.getRecordset(AccessRule) results["rulesCreated"] = len(newRules) results["dataMigrations"].append(f"Created {len(newRules)} RBAC rules") logger.info(f"Created {len(newRules)} RBAC rules") except Exception as e: logger.error(f"Error creating RBAC rules: {e}") results["errors"].append(f"Error creating RBAC rules: {e}") else: existingRules = db.getRecordset(AccessRule) if existingRules: results["rulesCreated"] = len(existingRules) results["dataMigrations"].append(f"RBAC rules already exist ({len(existingRules)} rules)") else: results["dataMigrations"].append("Would create RBAC rules") logger.info("Migration completed successfully") return results except Exception as e: logger.error(f"Migration failed: {e}") results["errors"].append(f"Migration failed: {e}") return results def validateMigration(db: DatabaseConnector) -> Dict[str, Any]: """ Validate that migration was successful. Args: db: Database connector instance Returns: Dictionary with validation results """ validation = { "valid": True, "issues": [] } try: # Check that AccessRule table exists try: rules = db.getRecordset(AccessRule) if not rules: validation["valid"] = False validation["issues"].append("AccessRule table exists but has no rules") except Exception as e: validation["valid"] = False validation["issues"].append(f"AccessRule table does not exist or is not accessible: {e}") # Check that all users have roleLabels users = db.getRecordset(UserInDB) usersWithoutRoles = [] for user in users: roleLabels = user.get("roleLabels", []) if not roleLabels or not isinstance(roleLabels, list) or len(roleLabels) == 0: usersWithoutRoles.append({ "id": user.get("id"), "username": user.get("username"), "privilege": user.get("privilege") }) if usersWithoutRoles: validation["valid"] = False validation["issues"].append(f"{len(usersWithoutRoles)} users without roleLabels: {[u['username'] for u in usersWithoutRoles]}") return validation except Exception as e: validation["valid"] = False validation["issues"].append(f"Validation error: {e}") return validation