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