fix: RBAC bootstrap anthropic for user, FeatureAccess response, workspace UI repair, user access overview, RBAC tests
Made-with: Cursor
This commit is contained in:
parent
2345ff669a
commit
f796ae3807
6 changed files with 360 additions and 331 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
))
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -278,6 +278,7 @@ 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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,18 +5,17 @@ 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
|
||||
|
||||
|
|
@ -27,7 +26,7 @@ class TestRbacBootstrap:
|
|||
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)
|
||||
|
|
@ -36,7 +35,8 @@ class TestRbacBootstrap:
|
|||
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."""
|
||||
|
|
@ -49,12 +49,12 @@ class TestRbacBootstrap:
|
|||
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"
|
||||
|
|
@ -63,15 +63,15 @@ class TestRbacBootstrap:
|
|||
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"
|
||||
|
|
@ -80,90 +80,80 @@ class TestRbacBootstrap:
|
|||
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)
|
||||
with patch(
|
||||
"modules.interfaces.interfaceBootstrap._getRoleId",
|
||||
side_effect=lambda d, label: f"rid-{label}",
|
||||
):
|
||||
_createDefaultRoleRules(db)
|
||||
|
||||
# Should create 4 default rules (sysadmin, admin, user, viewer)
|
||||
assert db.recordCreate.call_count == 4
|
||||
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}
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
# 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
|
||||
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)
|
||||
with patch(
|
||||
"modules.interfaces.interfaceBootstrap._getRoleId",
|
||||
side_effect=lambda d, label: f"rid-{label}",
|
||||
):
|
||||
_createTableSpecificRules(db)
|
||||
|
||||
# Should create multiple rules for different tables
|
||||
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)
|
||||
with patch("modules.interfaces.interfaceBootstrap._ensureUiContextRules"):
|
||||
with patch("modules.interfaces.interfaceBootstrap._ensureDataContextRules"):
|
||||
initRbacRules(db)
|
||||
|
||||
# Should not create new rules since all required tables already have rules for all roles
|
||||
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()
|
||||
db.recordModify = Mock()
|
||||
db.getRecordset = Mock(return_value=[])
|
||||
|
||||
initRbacRules(db)
|
||||
with patch(
|
||||
"modules.interfaces.interfaceBootstrap._getRoleId",
|
||||
side_effect=lambda d, label: f"rid-{label}",
|
||||
):
|
||||
initRbacRules(db)
|
||||
|
||||
# Should create rules
|
||||
assert db.recordCreate.call_count > 0
|
||||
|
|
|
|||
|
|
@ -5,12 +5,21 @@ 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:
|
||||
|
|
@ -18,48 +27,41 @@ class TestRbacPermissionResolution:
|
|||
|
||||
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 []
|
||||
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)
|
||||
|
||||
rbac._getRulesForRole = mockGetRulesForRole
|
||||
|
||||
# Get permissions for generic table
|
||||
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
|
||||
|
|
@ -75,98 +77,92 @@ class TestRbacPermissionResolution:
|
|||
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 []
|
||||
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)
|
||||
|
||||
rbac._getRulesForRole = mockGetRulesForRole
|
||||
|
||||
# Get permissions for UserInDB table - should use specific rule
|
||||
# Using UAM namespace: data.uam.UserInDB
|
||||
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 []
|
||||
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)
|
||||
|
||||
rbac._getRulesForRole = mockGetRulesForRole
|
||||
|
||||
# Get permissions - union logic should make playground visible
|
||||
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."""
|
||||
|
|
@ -178,59 +174,60 @@ class TestRbacPermissionResolution:
|
|||
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 []
|
||||
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)
|
||||
|
||||
rbac._getRulesForRole = mockGetRulesForRole
|
||||
|
||||
# Get permissions for specific UI element
|
||||
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
|
||||
|
|
@ -244,41 +241,38 @@ class TestRbacPermissionResolution:
|
|||
|
||||
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
|
||||
|
|
@ -290,57 +284,53 @@ class TestRbacPermissionResolution:
|
|||
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
|
||||
assert rbac.validateAccessRule(rule1) is True
|
||||
|
||||
# Invalid: Read=MY, Create=GROUP (not allowed - GROUP > MY)
|
||||
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
|
||||
assert rbac.validateAccessRule(rule2) is False
|
||||
|
||||
# Valid: Read=GROUP, Create=GROUP (allowed)
|
||||
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
|
||||
assert rbac.validateAccessRule(rule3) is True
|
||||
|
||||
# Invalid: Read=NONE, Create=MY (not allowed - no read access)
|
||||
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."""
|
||||
|
|
@ -352,32 +342,29 @@ class TestRbacPermissionResolution:
|
|||
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."""
|
||||
|
|
@ -389,27 +376,26 @@ class TestRbacPermissionResolution:
|
|||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue