401 lines
12 KiB
Python
401 lines
12 KiB
Python
# 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
|