# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Unit tests for RBAC permission resolution. Tests rule specificity, multiple roles, and permission combination logic. """ import pytest from modules.datamodels.datamodelUam import User, AccessLevel, UserPermissions from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext from modules.security.rbac import RbacClass from modules.connectors.connectorDbPostgre import DatabaseConnector from unittest.mock import Mock, MagicMock class TestRbacPermissionResolution: """Test RBAC permission resolution logic.""" def testSingleRoleGenericRule(self): """Test permission resolution with a single role and generic rule.""" # Mock database connector db = Mock(spec=DatabaseConnector) dbApp = Mock(spec=DatabaseConnector) # Create RBAC interface rbac = RbacClass(db, dbApp=dbApp) # Create user with single role user = User( id="user1", username="testuser", roleLabels=["user"], mandateId="mandate1" ) # Mock rules for "user" role def mockGetRulesForRole(roleLabel, context): if roleLabel == "user" and context == AccessRuleContext.DATA: return [ AccessRule( roleLabel="user", context=AccessRuleContext.DATA, item=None, # Generic rule view=True, read=AccessLevel.MY, create=AccessLevel.MY, update=AccessLevel.MY, delete=AccessLevel.MY ) ] return [] rbac._getRulesForRole = mockGetRulesForRole # Get permissions for generic table permissions = rbac.getUserPermissions( user, AccessRuleContext.DATA, "SomeTable" ) assert permissions.view == True assert permissions.read == AccessLevel.MY assert permissions.create == AccessLevel.MY assert permissions.update == AccessLevel.MY assert permissions.delete == AccessLevel.MY def testRuleSpecificityMostSpecificWins(self): """Test that most specific rule wins within a single role.""" db = Mock(spec=DatabaseConnector) dbApp = Mock(spec=DatabaseConnector) rbac = RbacClass(db, dbApp=dbApp) user = User( id="user1", username="testuser", roleLabels=["user"], mandateId="mandate1" ) def mockGetRulesForRole(roleLabel, context): if roleLabel == "user" and context == AccessRuleContext.DATA: return [ AccessRule( roleLabel="user", context=AccessRuleContext.DATA, item=None, # Generic rule view=True, read=AccessLevel.GROUP, create=AccessLevel.GROUP, update=AccessLevel.GROUP, delete=AccessLevel.GROUP ), AccessRule( roleLabel="user", context=AccessRuleContext.DATA, item="UserInDB", # Specific rule view=True, read=AccessLevel.MY, create=AccessLevel.NONE, update=AccessLevel.MY, delete=AccessLevel.NONE ) ] return [] rbac._getRulesForRole = mockGetRulesForRole # Get permissions for UserInDB table - should use specific rule permissions = rbac.getUserPermissions( user, AccessRuleContext.DATA, "UserInDB" ) # Most specific rule should win assert permissions.read == AccessLevel.MY assert permissions.create == AccessLevel.NONE assert permissions.update == AccessLevel.MY assert permissions.delete == AccessLevel.NONE def testMultipleRolesUnionLogic(self): """Test that multiple roles use union (opening) logic.""" db = Mock(spec=DatabaseConnector) dbApp = Mock(spec=DatabaseConnector) rbac = RbacClass(db, dbApp=dbApp) # User with multiple roles user = User( id="user1", username="testuser", roleLabels=["user", "viewer"], mandateId="mandate1" ) def mockGetRulesForRole(roleLabel, context): if context == AccessRuleContext.UI: if roleLabel == "user": return [ AccessRule( roleLabel="user", context=AccessRuleContext.UI, item="playground", view=False # User role hides playground ) ] elif roleLabel == "viewer": return [ AccessRule( roleLabel="viewer", context=AccessRuleContext.UI, item="playground", view=True # Viewer role shows playground ) ] return [] rbac._getRulesForRole = mockGetRulesForRole # Get permissions - union logic should make playground visible permissions = rbac.getUserPermissions( user, AccessRuleContext.UI, "playground" ) # Union logic: if ANY role has view=true, then view=true assert permissions.view == True def testViewFalseOverridesGeneric(self): """Test that specific view=false overrides generic view=true.""" db = Mock(spec=DatabaseConnector) dbApp = Mock(spec=DatabaseConnector) rbac = RbacClass(db, dbApp=dbApp) user = User( id="user1", username="testuser", roleLabels=["user"], mandateId="mandate1" ) def mockGetRulesForRole(roleLabel, context): if roleLabel == "user" and context == AccessRuleContext.UI: return [ AccessRule( roleLabel="user", context=AccessRuleContext.UI, item=None, # Generic: view all UI view=True ), AccessRule( roleLabel="user", context=AccessRuleContext.UI, item="playground.voice.settings", # Specific: hide this view=False ) ] return [] rbac._getRulesForRole = mockGetRulesForRole # Get permissions for specific UI element permissions = rbac.getUserPermissions( user, AccessRuleContext.UI, "playground.voice.settings" ) # Specific rule should override generic assert permissions.view == False def testNoRolesReturnsNoAccess(self): """Test that user with no roles gets no access.""" db = Mock(spec=DatabaseConnector) dbApp = Mock(spec=DatabaseConnector) rbac = RbacClass(db, dbApp=dbApp) user = User( id="user1", username="testuser", roleLabels=[], # No roles mandateId="mandate1" ) permissions = rbac.getUserPermissions( user, AccessRuleContext.DATA, "SomeTable" ) assert permissions.view == False assert permissions.read == AccessLevel.NONE assert permissions.create == AccessLevel.NONE assert permissions.update == AccessLevel.NONE assert permissions.delete == AccessLevel.NONE def testFindMostSpecificRule(self): """Test findMostSpecificRule method.""" db = Mock(spec=DatabaseConnector) dbApp = Mock(spec=DatabaseConnector) rbac = RbacClass(db, dbApp=dbApp) rules = [ AccessRule( roleLabel="user", context=AccessRuleContext.DATA, item=None, # Generic view=True, read=AccessLevel.GROUP ), AccessRule( roleLabel="user", context=AccessRuleContext.DATA, item="UserInDB", # Table-level view=True, read=AccessLevel.MY ), AccessRule( roleLabel="user", context=AccessRuleContext.DATA, item="UserInDB.email", # Field-level - most specific view=True, read=AccessLevel.NONE ) ] # Test exact match rule = rbac.findMostSpecificRule(rules, "UserInDB.email") assert rule is not None assert rule.item == "UserInDB.email" assert rule.read == AccessLevel.NONE # Test table-level match rule = rbac.findMostSpecificRule(rules, "UserInDB") assert rule is not None assert rule.item == "UserInDB" assert rule.read == AccessLevel.MY # Test generic fallback rule = rbac.findMostSpecificRule(rules, "OtherTable") assert rule is not None assert rule.item is None assert rule.read == AccessLevel.GROUP def testValidateAccessRuleOpeningRights(self): """Test that CUD permissions respect read permission level.""" db = Mock(spec=DatabaseConnector) dbApp = Mock(spec=DatabaseConnector) rbac = RbacClass(db, dbApp=dbApp) # Valid: Read=MY, Create=MY (allowed) rule1 = AccessRule( roleLabel="user", context=AccessRuleContext.DATA, item="UserInDB", view=True, read=AccessLevel.MY, create=AccessLevel.MY, update=AccessLevel.MY, delete=AccessLevel.MY ) assert rbac.validateAccessRule(rule1) == True # Invalid: Read=MY, Create=GROUP (not allowed - GROUP > MY) rule2 = AccessRule( roleLabel="user", context=AccessRuleContext.DATA, item="UserInDB", view=True, read=AccessLevel.MY, create=AccessLevel.GROUP, # Not allowed update=AccessLevel.MY, delete=AccessLevel.MY ) assert rbac.validateAccessRule(rule2) == False # Valid: Read=GROUP, Create=GROUP (allowed) rule3 = AccessRule( roleLabel="admin", context=AccessRuleContext.DATA, item="UserInDB", view=True, read=AccessLevel.GROUP, create=AccessLevel.GROUP, update=AccessLevel.GROUP, delete=AccessLevel.GROUP ) assert rbac.validateAccessRule(rule3) == True # Invalid: Read=NONE, Create=MY (not allowed - no read access) rule4 = AccessRule( roleLabel="user", context=AccessRuleContext.DATA, item="UserInDB", view=True, read=AccessLevel.NONE, create=AccessLevel.MY, # Not allowed without read update=AccessLevel.MY, delete=AccessLevel.MY ) assert rbac.validateAccessRule(rule4) == False def testUiContextOnlyViewMatters(self): """Test that UI context only checks view permission.""" db = Mock(spec=DatabaseConnector) dbApp = Mock(spec=DatabaseConnector) rbac = RbacClass(db, dbApp=dbApp) user = User( id="user1", username="testuser", roleLabels=["user"], mandateId="mandate1" ) def mockGetRulesForRole(roleLabel, context): if roleLabel == "user" and context == AccessRuleContext.UI: return [ AccessRule( roleLabel="user", context=AccessRuleContext.UI, item="playground", view=True # No read/create/update/delete for UI context ) ] return [] rbac._getRulesForRole = mockGetRulesForRole permissions = rbac.getUserPermissions( user, AccessRuleContext.UI, "playground" ) assert permissions.view == True # Other permissions don't matter for UI context def testResourceContextOnlyViewMatters(self): """Test that RESOURCE context only checks view permission.""" db = Mock(spec=DatabaseConnector) dbApp = Mock(spec=DatabaseConnector) rbac = RbacClass(db, dbApp=dbApp) user = User( id="user1", username="testuser", roleLabels=["user"], mandateId="mandate1" ) def mockGetRulesForRole(roleLabel, context): if roleLabel == "user" and context == AccessRuleContext.RESOURCE: return [ AccessRule( roleLabel="user", context=AccessRuleContext.RESOURCE, item="ai.model.anthropic", view=True ) ] return [] rbac._getRulesForRole = mockGetRulesForRole permissions = rbac.getUserPermissions( user, AccessRuleContext.RESOURCE, "ai.model.anthropic" ) assert permissions.view == True