From f796ae38072a44a94d01a1b805fbf1907ea863a1 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Mon, 23 Mar 2026 10:29:23 +0100 Subject: [PATCH] fix: RBAC bootstrap anthropic for user, FeatureAccess response, workspace UI repair, user access overview, RBAC tests Made-with: Cursor --- modules/features/workspace/mainWorkspace.py | 53 +++ modules/interfaces/interfaceBootstrap.py | 8 +- modules/routes/routeAdminFeatures.py | 2 +- .../routes/routeAdminUserAccessOverview.py | 4 +- tests/unit/rbac/test_rbac_bootstrap.py | 180 ++++--- tests/unit/rbac/test_rbac_permissions.py | 444 +++++++++--------- 6 files changed, 360 insertions(+), 331 deletions(-) diff --git a/modules/features/workspace/mainWorkspace.py b/modules/features/workspace/mainWorkspace.py index 353129cc..c502a82e 100644 --- a/modules/features/workspace/mainWorkspace.py +++ b/modules/features/workspace/mainWorkspace.py @@ -226,6 +226,8 @@ def _syncTemplateRolesToDb() -> int: if createdCount > 0: logger.info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles") + _repairWorkspaceUserInstanceUiNav(rootInterface) + return createdCount except Exception as e: @@ -233,6 +235,57 @@ def _syncTemplateRolesToDb() -> int: return 0 +def _repairWorkspaceUserInstanceUiNav(rootInterface) -> int: + """ + Ensure every instance-scoped workspace-user role grants UI view on Editor and Settings. + Covers older instance roles copied before template updates (bootstrap / app startup). + """ + from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext, Role + + workspaceNavObjectKeys = ( + "ui.feature.workspace.editor", + "ui.feature.workspace.settings", + ) + repairCount = 0 + try: + userRoleRecords = rootInterface.db.getRecordset( + Role, + recordFilter={"featureCode": FEATURE_CODE, "roleLabel": "workspace-user"}, + ) + for roleRec in userRoleRecords or []: + if not roleRec.get("featureInstanceId"): + continue + roleId = str(roleRec.get("id")) + accessRules = rootInterface.getAccessRulesByRole(roleId) + rulesByUiKey = {(r.context, r.item): r for r in accessRules} + for objectKey in workspaceNavObjectKeys: + uiKey = (AccessRuleContext.UI, objectKey) + existingRule = rulesByUiKey.get(uiKey) + if existingRule is None: + newRule = AccessRule( + roleId=roleId, + context=AccessRuleContext.UI, + item=objectKey, + view=True, + read=None, + create=None, + update=None, + delete=None, + ) + rootInterface.db.recordCreate(AccessRule, newRule.model_dump()) + repairCount += 1 + elif not existingRule.view: + rootInterface.db.recordModify(AccessRule, str(existingRule.id), {"view": True}) + repairCount += 1 + if repairCount: + logger.info( + f"Feature '{FEATURE_CODE}': Repaired {repairCount} UI AccessRules for instance workspace-user roles (Editor/Settings)" + ) + except Exception as e: + logger.warning(f"Feature '{FEATURE_CODE}': workspace-user UI nav repair skipped: {e}") + return repairCount + + def _ensureAccessRulesForRole(rootInterface, roleId: str, ruleTemplates: List[Dict[str, Any]]) -> int: """Ensure AccessRules exist for a role based on templates.""" from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext diff --git a/modules/interfaces/interfaceBootstrap.py b/modules/interfaces/interfaceBootstrap.py index 4a3881f5..89cf4126 100644 --- a/modules/interfaces/interfaceBootstrap.py +++ b/modules/interfaces/interfaceBootstrap.py @@ -1871,7 +1871,7 @@ def _createAicoreProviderRules(db: DatabaseConnector) -> None: Provider access per role: - admin: all providers allowed - - user: all providers EXCEPT anthropic (view=False) + - user: all providers allowed - viewer: NO provider access (viewer has no RESOURCE permissions) NOTE: Provider list is dynamically discovered from AICore model registry. @@ -1916,7 +1916,7 @@ def _createAicoreProviderRules(db: DatabaseConnector) -> None: read=None, create=None, update=None, delete=None, )) - # User: access to all providers EXCEPT anthropic + # User: access to all providers (same provider keys as admin) userId = _getRoleId(db, "user") if userId: for provider in providers: @@ -1930,13 +1930,11 @@ def _createAicoreProviderRules(db: DatabaseConnector) -> None: } ) if not existingRules: - # Anthropic is not allowed for user role - isAllowed = provider != "anthropic" providerRules.append(AccessRule( roleId=userId, context=AccessRuleContext.RESOURCE, item=resourceKey, - view=isAllowed, + view=True, read=None, create=None, update=None, delete=None, )) diff --git a/modules/routes/routeAdminFeatures.py b/modules/routes/routeAdminFeatures.py index 3e701548..c95c0b1b 100644 --- a/modules/routes/routeAdminFeatures.py +++ b/modules/routes/routeAdminFeatures.py @@ -1408,7 +1408,7 @@ def update_feature_instance_user_roles( "userId": userId, "featureInstanceId": instanceId, "roleIds": data.roleIds, - "enabled": data.enabled if data.enabled is not None else existingAccess[0].get("enabled", True) + "enabled": data.enabled if data.enabled is not None else bool(existingAccess.enabled), } except HTTPException: diff --git a/modules/routes/routeAdminUserAccessOverview.py b/modules/routes/routeAdminUserAccessOverview.py index 758eff65..9b19fc41 100644 --- a/modules/routes/routeAdminUserAccessOverview.py +++ b/modules/routes/routeAdminUserAccessOverview.py @@ -278,7 +278,8 @@ def getUserAccessOverview( # Get mandate name mandate = interface.getMandate(umMandateId) mandateName = mandate.name if mandate else umMandateId - + mandateLabel = (mandate.label or None) if mandate else None + # Get roles for this UserMandate using interface method umRoles = interface.getUserMandateRoles(umId) @@ -368,6 +369,7 @@ def getUserAccessOverview( mandatesInfo.append({ "id": umMandateId, "name": mandateName, + "label": mandateLabel, "roleIds": mandateRoleIds, "featureInstances": featureInstancesInfo, }) diff --git a/tests/unit/rbac/test_rbac_bootstrap.py b/tests/unit/rbac/test_rbac_bootstrap.py index e8b04f07..0e69b802 100644 --- a/tests/unit/rbac/test_rbac_bootstrap.py +++ b/tests/unit/rbac/test_rbac_bootstrap.py @@ -5,165 +5,155 @@ Unit tests for RBAC bootstrap initialization. Tests that bootstrap creates correct rules and initial data. """ -import pytest -from unittest.mock import Mock, MagicMock, patch +from unittest.mock import Mock, patch + from modules.interfaces.interfaceBootstrap import ( - initBootstrap, initRootMandate, initAdminUser, initEventUser, initRbacRules, - createDefaultRoleRules, - createTableSpecificRules + _createDefaultRoleRules, + _createTableSpecificRules, ) -from modules.datamodels.datamodelUam import UserInDB, Mandate, AuthAuthority +from modules.datamodels.datamodelUam import UserInDB, Mandate from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext from modules.datamodels.datamodelUam import AccessLevel class TestRbacBootstrap: """Test RBAC bootstrap initialization.""" - + def testInitRootMandateCreatesIfNotExists(self): """Test that initRootMandate creates mandate if it doesn't exist.""" db = Mock() - db.getRecordset = Mock(return_value=[]) # No existing mandates + db.getRecordset = Mock(return_value=[]) db.recordCreate = Mock(return_value={"id": "mandate1", "name": "Root"}) - + mandateId = initRootMandate(db) - + assert mandateId == "mandate1" db.recordCreate.assert_called_once() callArgs = db.recordCreate.call_args assert isinstance(callArgs[0][1], Mandate) - assert callArgs[0][1].name == "Root" - + assert callArgs[0][1].name == "root" + assert callArgs[0][1].label == "Root" + def testInitRootMandateReturnsExisting(self): """Test that initRootMandate returns existing mandate ID.""" db = Mock() db.getRecordset = Mock(return_value=[{"id": "existing_mandate"}]) - + mandateId = initRootMandate(db) - + assert mandateId == "existing_mandate" db.recordCreate.assert_not_called() - + def testInitAdminUserCreatesWithSysadminRole(self): - """Test that initAdminUser creates user with sysadmin role.""" + """Test that initAdminUser creates user with isSysAdmin=True.""" db = Mock() - db.getRecordset = Mock(return_value=[]) # No existing users + db.getRecordset = Mock(return_value=[]) db.recordCreate = Mock(return_value={"id": "admin1", "username": "admin"}) - - with patch('modules.interfaces.interfaceBootstrap._getPasswordHash', return_value="hashed"): + + with patch("modules.interfaces.interfaceBootstrap._getPasswordHash", return_value="hashed"): userId = initAdminUser(db, "mandate1") - + assert userId == "admin1" db.recordCreate.assert_called_once() callArgs = db.recordCreate.call_args user = callArgs[0][1] assert isinstance(user, UserInDB) assert user.username == "admin" - assert "sysadmin" in user.roleLabels - + assert user.isSysAdmin is True + def testInitEventUserCreatesWithSysadminRole(self): - """Test that initEventUser creates user with sysadmin role.""" + """Test that initEventUser creates user with isSysAdmin=True.""" db = Mock() - db.getRecordset = Mock(return_value=[]) # No existing users + db.getRecordset = Mock(return_value=[]) db.recordCreate = Mock(return_value={"id": "event1", "username": "event"}) - - with patch('modules.interfaces.interfaceBootstrap._getPasswordHash', return_value="hashed"): + + with patch("modules.interfaces.interfaceBootstrap._getPasswordHash", return_value="hashed"): userId = initEventUser(db, "mandate1") - + assert userId == "event1" db.recordCreate.assert_called_once() callArgs = db.recordCreate.call_args user = callArgs[0][1] assert isinstance(user, UserInDB) assert user.username == "event" - assert "sysadmin" in user.roleLabels - + assert user.isSysAdmin is True + def testCreateDefaultRoleRules(self): - """Test that createDefaultRoleRules creates correct default rules.""" + """Test that _createDefaultRoleRules creates admin + viewer generic DATA rules.""" db = Mock() db.recordCreate = Mock() - - createDefaultRoleRules(db) - - # Should create 4 default rules (sysadmin, admin, user, viewer) - assert db.recordCreate.call_count == 4 - - # Check sysadmin rule - sysadminCall = [call for call in db.recordCreate.call_args_list - if call[0][1].roleLabel == "sysadmin"][0] - sysadminRule = sysadminCall[0][1] - assert sysadminRule.context == AccessRuleContext.DATA - assert sysadminRule.item is None - assert sysadminRule.view == True - assert sysadminRule.read == AccessLevel.ALL - assert sysadminRule.create == AccessLevel.ALL - - # Check user rule - userCall = [call for call in db.recordCreate.call_args_list - if call[0][1].roleLabel == "user"][0] - userRule = userCall[0][1] - assert userRule.read == AccessLevel.MY - assert userRule.create == AccessLevel.MY - + + with patch( + "modules.interfaces.interfaceBootstrap._getRoleId", + side_effect=lambda d, label: f"rid-{label}", + ): + _createDefaultRoleRules(db) + + assert db.recordCreate.call_count == 2 + created = [c[0][1] for c in db.recordCreate.call_args_list] + byRoleId = {r.roleId: r for r in created} + + adminRule = byRoleId["rid-admin"] + assert adminRule.context == AccessRuleContext.DATA + assert adminRule.item is None + assert adminRule.view is True + assert adminRule.read == AccessLevel.GROUP + assert adminRule.create == AccessLevel.GROUP + + viewerRule = byRoleId["rid-viewer"] + assert viewerRule.read == AccessLevel.GROUP + assert viewerRule.create == AccessLevel.NONE + def testCreateTableSpecificRules(self): - """Test that createTableSpecificRules creates table-specific rules.""" + """Test that _createTableSpecificRules creates table-specific rules.""" db = Mock() db.recordCreate = Mock() - - createTableSpecificRules(db) - - # Should create multiple rules for different tables + + with patch( + "modules.interfaces.interfaceBootstrap._getRoleId", + side_effect=lambda d, label: f"rid-{label}", + ): + _createTableSpecificRules(db) + assert db.recordCreate.call_count > 0 - - # Check that Mandate table rules are created with full objectKey (UAM namespace) - mandateCalls = [call for call in db.recordCreate.call_args_list - if call[0][1].item == "data.uam.Mandate"] + + mandateCalls = [ + call for call in db.recordCreate.call_args_list if call[0][1].item == "data.uam.Mandate" + ] assert len(mandateCalls) > 0 - - # Check that all roles have view=False and no access for Mandate - # (SysAdmin bypasses RBAC via isSysAdmin flag, not via roles) + for call in mandateCalls: rule = call[0][1] - assert rule.view == False + assert rule.view is False assert rule.read == AccessLevel.NONE - + def testInitRbacRulesSkipsIfExists(self): - """Test that initRbacRules skips default rule creation if rules already exist, but adds missing table-specific rules.""" + """When AccessRules already exist, init skips full init; ensure-* hooks are not exercised here.""" db = Mock() - # Mock existing rules - include rules for ChatWorkflow and AutomationDefinition to prevent adding missing rules - # Need rules for all required roles to fully prevent creation - # Using semantic namespace format: data.chat.{TableName}, data.automation.{TableName} - existingRules = [] - for table in ["data.chat.ChatWorkflow", "data.automation.AutomationDefinition"]: - for role in ["admin", "user", "viewer"]: - existingRules.append({ - "id": f"rule_{table}_{role}", - "item": table, - "context": AccessRuleContext.DATA.value, - "roleLabel": role - }) - db.getRecordset = Mock(return_value=existingRules) + db.getRecordset = Mock(return_value=[{"id": "existing_rule"}]) db.recordCreate = Mock() - - initRbacRules(db) - - # Should not create new rules since all required tables already have rules for all roles + + with patch("modules.interfaces.interfaceBootstrap._ensureUiContextRules"): + with patch("modules.interfaces.interfaceBootstrap._ensureDataContextRules"): + initRbacRules(db) + db.recordCreate.assert_not_called() - + def testInitRbacRulesCreatesIfNotExists(self): - """Test that initRbacRules creates rules if they don't exist.""" + """Test that initRbacRules creates rules when the AccessRule table is empty.""" db = Mock() - db.getRecordset = Mock(side_effect=[ - [], # No existing rules - [] # After creating default rules - ]) db.recordCreate = Mock() - - initRbacRules(db) - - # Should create rules + db.recordModify = Mock() + db.getRecordset = Mock(return_value=[]) + + with patch( + "modules.interfaces.interfaceBootstrap._getRoleId", + side_effect=lambda d, label: f"rid-{label}", + ): + initRbacRules(db) + assert db.recordCreate.call_count > 0 diff --git a/tests/unit/rbac/test_rbac_permissions.py b/tests/unit/rbac/test_rbac_permissions.py index b40bebe3..49458367 100644 --- a/tests/unit/rbac/test_rbac_permissions.py +++ b/tests/unit/rbac/test_rbac_permissions.py @@ -5,411 +5,397 @@ 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 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 -from unittest.mock import Mock, MagicMock + +_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.""" - # 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" + 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 + + 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" + "SomeTable", ) - - assert permissions.view == True + + 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" + 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="data.uam.UserInDB", # Specific rule with UAM namespace - 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 - # Using UAM namespace: data.uam.UserInDB + + 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" + "data.uam.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.""" + """Test that multiple roles use union (opening) logic for view.""" 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" + 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 + + 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" + "playground", ) - - # Union logic: if ANY role has view=true, then view=true - assert permissions.view == True - + + 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" + 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 + + 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" + "playground.voice.settings", ) - - # Specific rule should override generic - assert permissions.view == False - + + assert permissions.view is False + def testNoRolesReturnsNoAccess(self): - """Test that user with no roles gets no access.""" + """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=[], # No roles - mandateId="mandate1" + roleLabels=[], + mandateId="mandate1", ) - + permissions = rbac.getUserPermissions( user, AccessRuleContext.DATA, - "SomeTable" + "SomeTable", ) - - assert permissions.view == False + + 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( - roleLabel="user", + roleId=_TEST_ROLE_USER, context=AccessRuleContext.DATA, - item=None, # Generic + item=None, view=True, - read=AccessLevel.GROUP + read=AccessLevel.GROUP, ), AccessRule( - roleLabel="user", + roleId=_TEST_ROLE_USER, context=AccessRuleContext.DATA, - item="data.uam.UserInDB", # Table-level with UAM namespace + item="data.uam.UserInDB", view=True, - read=AccessLevel.MY + read=AccessLevel.MY, ), AccessRule( - roleLabel="user", + roleId=_TEST_ROLE_USER, context=AccessRuleContext.DATA, - item="data.uam.UserInDB.email", # Field-level - most specific + item="data.uam.UserInDB.email", view=True, - read=AccessLevel.NONE - ) + read=AccessLevel.NONE, + ), ] - - # Test exact match + 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 - - # Test table-level match + rule = rbac.findMostSpecificRule(rules, "data.uam.UserInDB") assert rule is not None assert rule.item == "data.uam.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", + 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 + delete=AccessLevel.MY, ) - assert rbac.validateAccessRule(rule1) == True - - # Invalid: Read=MY, Create=GROUP (not allowed - GROUP > MY) + assert rbac.validateAccessRule(rule1) is True + rule2 = AccessRule( - roleLabel="user", + roleId=_TEST_ROLE_USER, context=AccessRuleContext.DATA, item="data.uam.UserInDB", view=True, read=AccessLevel.MY, - create=AccessLevel.GROUP, # Not allowed + create=AccessLevel.GROUP, update=AccessLevel.MY, - delete=AccessLevel.MY + delete=AccessLevel.MY, ) - assert rbac.validateAccessRule(rule2) == False - - # Valid: Read=GROUP, Create=GROUP (allowed) + assert rbac.validateAccessRule(rule2) is False + rule3 = AccessRule( - roleLabel="admin", + 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 + delete=AccessLevel.GROUP, ) - assert rbac.validateAccessRule(rule3) == True - - # Invalid: Read=NONE, Create=MY (not allowed - no read access) + assert rbac.validateAccessRule(rule3) is True + rule4 = AccessRule( - roleLabel="user", + roleId=_TEST_ROLE_USER, context=AccessRuleContext.DATA, item="data.uam.UserInDB", view=True, read=AccessLevel.NONE, - create=AccessLevel.MY, # Not allowed without read + create=AccessLevel.MY, update=AccessLevel.MY, - delete=AccessLevel.MY + delete=AccessLevel.MY, ) - assert rbac.validateAccessRule(rule4) == False - + 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" + 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 - + + 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" + "playground", ) - - assert permissions.view == True - # Other permissions don't matter for UI context - + + 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" + 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 - + + 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" + "ai.model.anthropic", ) - - assert permissions.view == True + + assert permissions.view is True