gateway/tests/unit/rbac/test_rbac_permissions.py

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