This commit is contained in:
patrick-motsch 2026-02-08 14:00:08 +01:00
parent 1b55db4581
commit f15ed2e380
6 changed files with 135 additions and 23 deletions

View file

@ -119,13 +119,12 @@ class AutomationObjects:
def _enrichAutomationsWithUserAndMandate(self, automations: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Batch enrich automations with user names and mandate names for display.
Uses AppObjects interface to fetch users and mandates with proper access control.
Uses direct DB lookup (no RBAC) because this is purely cosmetic enrichment
the user already has RBAC-verified access to the automations themselves.
"""
if not automations:
return automations
from modules.interfaces.interfaceDbApp import getInterface as getAppInterface
# Collect all unique user IDs and mandate IDs
userIds = set()
mandateIds = set()
@ -139,22 +138,33 @@ class AutomationObjects:
if mandateId:
mandateIds.add(mandateId)
# Use AppObjects interface to fetch users (respects access control)
appInterface = getAppInterface(self.currentUser)
usersMap = {}
if userIds:
for userId in userIds:
user = appInterface.getUser(userId)
if user:
usersMap[userId] = user.username or user.email or userId
# Use root DB connector for display-only lookups (no RBAC needed)
try:
from modules.datamodels.datamodelUam import UserInDB, Mandate
from modules.security.rootAccess import getRootDbAppConnector
dbAppConn = getRootDbAppConnector()
# Use AppObjects interface to fetch mandates (respects access control)
mandatesMap = {}
if mandateIds:
for mandateId in mandateIds:
mandate = appInterface.getMandate(mandateId)
if mandate:
mandatesMap[mandateId] = mandate.name or mandateId
# Batch fetch user display names
usersMap = {}
if userIds:
for userId in userIds:
users = dbAppConn.getRecordset(UserInDB, {"id": userId})
if users:
user = users[0]
fullName = f"{user.get('firstName', '')} {user.get('lastName', '')}".strip()
usersMap[userId] = fullName or user.get("email") or user.get("username") or userId
# Batch fetch mandate display names
mandatesMap = {}
if mandateIds:
for mandateId in mandateIds:
mandates = dbAppConn.getRecordset(Mandate, {"id": mandateId})
if mandates:
mandatesMap[mandateId] = mandates[0].get("name") or mandateId
except Exception as e:
logger.warning(f"Could not enrich automations with user/mandate names: {e}")
usersMap = {}
mandatesMap = {}
# Enrich each automation with the fetched data
for automation in automations:

View file

@ -266,9 +266,10 @@ def _ensureAccessRulesForRole(rootInterface, roleId: str, ruleTemplates: List[Di
existingRules = rootInterface.getAccessRulesByRole(roleId)
# Create a set of existing rule signatures to avoid duplicates
# IMPORTANT: Use .value for enum comparison, not str() which gives "AccessRuleContext.DATA" in Python 3.11+
existingSignatures = set()
for rule in existingRules:
sig = (str(rule.context) if rule.context else None, rule.item)
sig = (rule.context.value if rule.context else None, rule.item)
existingSignatures.add(sig)
createdCount = 0

View file

@ -230,9 +230,10 @@ def _ensureAccessRulesForRole(rootInterface, roleId: str, ruleTemplates: List[Di
existingRules = rootInterface.getAccessRulesByRole(roleId)
# Create a set of existing rule signatures to avoid duplicates
# IMPORTANT: Use .value for enum comparison, not str() which gives "AccessRuleContext.DATA" in Python 3.11+
existingSignatures = set()
for rule in existingRules:
sig = (str(rule.context) if rule.context else None, rule.item)
sig = (rule.context.value if rule.context else None, rule.item)
existingSignatures.add(sig)
createdCount = 0

View file

@ -245,7 +245,8 @@ def _ensureAccessRulesForRole(rootInterface, roleId: str, ruleTemplates: list) -
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
existingRules = rootInterface.getAccessRulesByRole(roleId)
existingSignatures = {(str(r.context) if r.context else None, r.item) for r in existingRules}
# IMPORTANT: Use .value for enum comparison, not str() which gives "AccessRuleContext.DATA" in Python 3.11+
existingSignatures = {(r.context.value if r.context else None, r.item) for r in existingRules}
createdCount = 0
for template in ruleTemplates or []:

View file

@ -394,9 +394,10 @@ def _ensureAccessRulesForRole(rootInterface, roleId: str, ruleTemplates: List[Di
existingRules = rootInterface.getAccessRulesByRole(roleId)
# Create a set of existing rule signatures to avoid duplicates
# IMPORTANT: Use .value for enum comparison, not str() which gives "AccessRuleContext.DATA" in Python 3.11+
existingSignatures = set()
for rule in existingRules:
sig = (str(rule.context) if rule.context else None, rule.item)
sig = (rule.context.value if rule.context else None, rule.item)
existingSignatures.add(sig)
createdCount = 0

View file

@ -1192,3 +1192,101 @@ async def getCatalogStats(
status_code=500,
detail=f"Failed to get catalog stats: {str(e)}"
)
# =============================================================================
# CLEANUP: Remove duplicate AccessRules
# =============================================================================
@router.post("/cleanup/duplicate-rules", response_model=dict)
@limiter.limit("5/minute")
async def cleanup_duplicate_access_rules(
request: Request,
dryRun: bool = Query(True, description="If true, only report duplicates without deleting"),
currentUser: User = Depends(requireSysAdmin)
) -> dict:
"""
Find and remove duplicate AccessRules.
Duplicates are rules with the same (roleId, context, item) signature.
Only the first rule (oldest) is kept, all others are deleted.
Query Parameters:
- dryRun: If true (default), only report what would be deleted. Set to false to actually delete.
Returns:
- Summary with counts and details of duplicates found/removed
"""
try:
rootInterface = getRootInterface()
# Get ALL AccessRules from DB
allRules = rootInterface.db.getRecordset(AccessRule)
# Group by signature (roleId, context, item)
rulesBySignature: Dict[tuple, list] = {}
for rule in allRules:
context = rule.get("context", "")
# Normalize context enum value
if hasattr(context, 'value'):
context = context.value
sig = (rule.get("roleId"), str(context), rule.get("item"))
if sig not in rulesBySignature:
rulesBySignature[sig] = []
rulesBySignature[sig].append(rule)
# Find duplicates and collect IDs to delete
duplicateGroups = []
idsToDelete = []
for sig, rules in rulesBySignature.items():
if len(rules) > 1:
# Sort by creation time (keep oldest)
rules.sort(key=lambda r: r.get("_createdAt", 0))
keepRule = rules[0]
deleteRules = rules[1:]
duplicateGroups.append({
"roleId": sig[0],
"context": sig[1],
"item": sig[2] or "(global)",
"totalCount": len(rules),
"keepId": keepRule.get("id"),
"deleteCount": len(deleteRules),
"deleteIds": [r.get("id") for r in deleteRules]
})
idsToDelete.extend([r.get("id") for r in deleteRules])
# Perform deletion if not dry run
deletedCount = 0
if not dryRun and idsToDelete:
for ruleId in idsToDelete:
try:
rootInterface.db.recordDelete(AccessRule, ruleId)
deletedCount += 1
except Exception as e:
logger.warning(f"Failed to delete rule {ruleId}: {e}")
result = {
"dryRun": dryRun,
"totalRules": len(allRules),
"uniqueSignatures": len(rulesBySignature),
"duplicateGroups": len(duplicateGroups),
"duplicateRulesToDelete": len(idsToDelete),
"deletedCount": deletedCount,
"details": duplicateGroups[:50] # Limit details to 50 groups
}
logger.info(f"AccessRule cleanup: dryRun={dryRun}, total={len(allRules)}, "
f"duplicateGroups={len(duplicateGroups)}, toDelete={len(idsToDelete)}, "
f"deleted={deletedCount}")
return result
except Exception as e:
logger.error(f"Error during AccessRule cleanup: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"Failed to cleanup duplicate rules: {str(e)}"
)