# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Unit tests for RBAC permission resolution. Tests rule specificity, multiple roles, and permission combination logic. """ from unittest.mock import Mock from modules.datamodels.datamodelUam import User, AccessLevel from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext from modules.security.rbac import RbacClass from modules.connectors.connectorDbPostgre import DatabaseConnector _TEST_ROLE_USER = "test-rid-user" _TEST_ROLE_VIEWER = "test-rid-viewer" def _patchRbacResolution(rbac, roleIds, rulesWithPriority): """Stub multi-tenant role loading and rule fetch so tests exercise merge logic only.""" rbac._getRoleIdsForUser = Mock(return_value=roleIds) rbac._getRulesForRoleIds = Mock(return_value=rulesWithPriority) class TestRbacPermissionResolution: """Test RBAC permission resolution logic.""" def testSingleRoleGenericRule(self): """Test permission resolution with a single role and generic rule.""" db = Mock(spec=DatabaseConnector) dbApp = Mock(spec=DatabaseConnector) rbac = RbacClass(db, dbApp=dbApp) user = User( id="user1", username="testuser", roleLabels=["user"], mandateId="mandate1", ) rules = [ ( 1, AccessRule( roleId=_TEST_ROLE_USER, context=AccessRuleContext.DATA, item=None, view=True, read=AccessLevel.MY, create=AccessLevel.MY, update=AccessLevel.MY, delete=AccessLevel.MY, ), ) ] _patchRbacResolution(rbac, [_TEST_ROLE_USER], rules) permissions = rbac.getUserPermissions( user, AccessRuleContext.DATA, "SomeTable", ) assert permissions.view is 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", ) rules = [ ( 1, AccessRule( roleId=_TEST_ROLE_USER, context=AccessRuleContext.DATA, item=None, view=True, read=AccessLevel.GROUP, create=AccessLevel.GROUP, update=AccessLevel.GROUP, delete=AccessLevel.GROUP, ), ), ( 1, AccessRule( roleId=_TEST_ROLE_USER, context=AccessRuleContext.DATA, item="data.uam.UserInDB", view=True, read=AccessLevel.MY, create=AccessLevel.NONE, update=AccessLevel.MY, delete=AccessLevel.NONE, ), ), ] _patchRbacResolution(rbac, [_TEST_ROLE_USER], rules) permissions = rbac.getUserPermissions( user, AccessRuleContext.DATA, "data.uam.UserInDB", ) 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 for view.""" db = Mock(spec=DatabaseConnector) dbApp = Mock(spec=DatabaseConnector) rbac = RbacClass(db, dbApp=dbApp) user = User( id="user1", username="testuser", roleLabels=["user", "viewer"], mandateId="mandate1", ) rules = [ ( 1, AccessRule( roleId=_TEST_ROLE_USER, context=AccessRuleContext.UI, item="playground", view=False, ), ), ( 1, AccessRule( roleId=_TEST_ROLE_VIEWER, context=AccessRuleContext.UI, item="playground", view=True, ), ), ] _patchRbacResolution(rbac, [_TEST_ROLE_USER, _TEST_ROLE_VIEWER], rules) permissions = rbac.getUserPermissions( user, AccessRuleContext.UI, "playground", ) assert permissions.view is 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", ) rules = [ ( 1, AccessRule( roleId=_TEST_ROLE_USER, context=AccessRuleContext.UI, item=None, view=True, ), ), ( 1, AccessRule( roleId=_TEST_ROLE_USER, context=AccessRuleContext.UI, item="playground.voice.settings", view=False, ), ), ] _patchRbacResolution(rbac, [_TEST_ROLE_USER], rules) permissions = rbac.getUserPermissions( user, AccessRuleContext.UI, "playground.voice.settings", ) assert permissions.view is False def testNoRolesReturnsNoAccess(self): """Test that user with no resolved role IDs gets no access.""" db = Mock(spec=DatabaseConnector) dbApp = Mock(spec=DatabaseConnector) rbac = RbacClass(db, dbApp=dbApp) dbApp.getRecordset = Mock(return_value=[]) user = User( id="user1", username="testuser", roleLabels=[], mandateId="mandate1", ) permissions = rbac.getUserPermissions( user, AccessRuleContext.DATA, "SomeTable", ) assert permissions.view is 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( roleId=_TEST_ROLE_USER, context=AccessRuleContext.DATA, item=None, view=True, read=AccessLevel.GROUP, ), AccessRule( roleId=_TEST_ROLE_USER, context=AccessRuleContext.DATA, item="data.uam.UserInDB", view=True, read=AccessLevel.MY, ), AccessRule( roleId=_TEST_ROLE_USER, context=AccessRuleContext.DATA, item="data.uam.UserInDB.email", view=True, read=AccessLevel.NONE, ), ] rule = rbac.findMostSpecificRule(rules, "data.uam.UserInDB.email") assert rule is not None assert rule.item == "data.uam.UserInDB.email" assert rule.read == AccessLevel.NONE rule = rbac.findMostSpecificRule(rules, "data.uam.UserInDB") assert rule is not None assert rule.item == "data.uam.UserInDB" assert rule.read == AccessLevel.MY 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) rule1 = AccessRule( roleId=_TEST_ROLE_USER, context=AccessRuleContext.DATA, item="data.uam.UserInDB", view=True, read=AccessLevel.MY, create=AccessLevel.MY, update=AccessLevel.MY, delete=AccessLevel.MY, ) assert rbac.validateAccessRule(rule1) is True rule2 = AccessRule( roleId=_TEST_ROLE_USER, context=AccessRuleContext.DATA, item="data.uam.UserInDB", view=True, read=AccessLevel.MY, create=AccessLevel.GROUP, update=AccessLevel.MY, delete=AccessLevel.MY, ) assert rbac.validateAccessRule(rule2) is False rule3 = AccessRule( roleId="test-rid-admin", context=AccessRuleContext.DATA, item="data.uam.UserInDB", view=True, read=AccessLevel.GROUP, create=AccessLevel.GROUP, update=AccessLevel.GROUP, delete=AccessLevel.GROUP, ) assert rbac.validateAccessRule(rule3) is True rule4 = AccessRule( roleId=_TEST_ROLE_USER, context=AccessRuleContext.DATA, item="data.uam.UserInDB", view=True, read=AccessLevel.NONE, create=AccessLevel.MY, update=AccessLevel.MY, delete=AccessLevel.MY, ) assert rbac.validateAccessRule(rule4) is 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", ) rules = [ ( 1, AccessRule( roleId=_TEST_ROLE_USER, context=AccessRuleContext.UI, item="playground", view=True, ), ) ] _patchRbacResolution(rbac, [_TEST_ROLE_USER], rules) permissions = rbac.getUserPermissions( user, AccessRuleContext.UI, "playground", ) assert permissions.view is True 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", ) rules = [ ( 1, AccessRule( roleId=_TEST_ROLE_USER, context=AccessRuleContext.RESOURCE, item="ai.model.anthropic", view=True, ), ) ] _patchRbacResolution(rbac, [_TEST_ROLE_USER], rules) permissions = rbac.getUserPermissions( user, AccessRuleContext.RESOURCE, "ai.model.anthropic", ) assert permissions.view is True