gateway/tests/unit/rbac/test_rbac_permissions.py
2025-12-07 13:48:39 +01:00

403 lines
13 KiB
Python

"""
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 modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
from modules.security.rbac import RbacClass
from modules.connectors.connectorDbPostgre import DatabaseConnector
from unittest.mock import Mock, MagicMock
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)
# Create RBAC interface
rbac = RbacClass(db)
# Create user with single role
user = User(
id="user1",
username="testuser",
roleLabels=["user"],
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
permissions = rbac.getUserPermissions(
user,
AccessRuleContext.DATA,
"SomeTable"
)
assert permissions.view == 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)
rbac = RbacClass(db)
user = User(
id="user1",
username="testuser",
roleLabels=["user"],
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="UserInDB", # Specific rule
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
permissions = rbac.getUserPermissions(
user,
AccessRuleContext.DATA,
"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."""
db = Mock(spec=DatabaseConnector)
rbac = RbacClass(db)
# User with multiple roles
user = User(
id="user1",
username="testuser",
roleLabels=["user", "viewer"],
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
permissions = rbac.getUserPermissions(
user,
AccessRuleContext.UI,
"playground"
)
# Union logic: if ANY role has view=true, then view=true
assert permissions.view == True
def testViewFalseOverridesGeneric(self):
"""Test that specific view=false overrides generic view=true."""
db = Mock(spec=DatabaseConnector)
rbac = RbacClass(db)
user = User(
id="user1",
username="testuser",
roleLabels=["user"],
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
permissions = rbac.getUserPermissions(
user,
AccessRuleContext.UI,
"playground.voice.settings"
)
# Specific rule should override generic
assert permissions.view == False
def testNoRolesReturnsNoAccess(self):
"""Test that user with no roles gets no access."""
db = Mock(spec=DatabaseConnector)
rbac = RbacClass(db)
user = User(
id="user1",
username="testuser",
roleLabels=[], # No roles
mandateId="mandate1"
)
permissions = rbac.getUserPermissions(
user,
AccessRuleContext.DATA,
"SomeTable"
)
assert permissions.view == 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)
rbac = RbacClass(db)
rules = [
AccessRule(
roleLabel="user",
context=AccessRuleContext.DATA,
item=None, # Generic
view=True,
read=AccessLevel.GROUP
),
AccessRule(
roleLabel="user",
context=AccessRuleContext.DATA,
item="UserInDB", # Table-level
view=True,
read=AccessLevel.MY
),
AccessRule(
roleLabel="user",
context=AccessRuleContext.DATA,
item="UserInDB.email", # Field-level - most specific
view=True,
read=AccessLevel.NONE
)
]
# Test exact match
rule = rbac.findMostSpecificRule(rules, "UserInDB.email")
assert rule is not None
assert rule.item == "UserInDB.email"
assert rule.read == AccessLevel.NONE
# Test table-level match
rule = rbac.findMostSpecificRule(rules, "UserInDB")
assert rule is not None
assert rule.item == "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)
rbac = RbacClass(db)
# Valid: Read=MY, Create=MY (allowed)
rule1 = AccessRule(
roleLabel="user",
context=AccessRuleContext.DATA,
item="UserInDB",
view=True,
read=AccessLevel.MY,
create=AccessLevel.MY,
update=AccessLevel.MY,
delete=AccessLevel.MY
)
assert rbac.validateAccessRule(rule1) == True
# Invalid: Read=MY, Create=GROUP (not allowed - GROUP > MY)
rule2 = AccessRule(
roleLabel="user",
context=AccessRuleContext.DATA,
item="UserInDB",
view=True,
read=AccessLevel.MY,
create=AccessLevel.GROUP, # Not allowed
update=AccessLevel.MY,
delete=AccessLevel.MY
)
assert rbac.validateAccessRule(rule2) == False
# Valid: Read=GROUP, Create=GROUP (allowed)
rule3 = AccessRule(
roleLabel="admin",
context=AccessRuleContext.DATA,
item="UserInDB",
view=True,
read=AccessLevel.GROUP,
create=AccessLevel.GROUP,
update=AccessLevel.GROUP,
delete=AccessLevel.GROUP
)
assert rbac.validateAccessRule(rule3) == True
# Invalid: Read=NONE, Create=MY (not allowed - no read access)
rule4 = AccessRule(
roleLabel="user",
context=AccessRuleContext.DATA,
item="UserInDB",
view=True,
read=AccessLevel.NONE,
create=AccessLevel.MY, # Not allowed without read
update=AccessLevel.MY,
delete=AccessLevel.MY
)
assert rbac.validateAccessRule(rule4) == False
def testUiContextOnlyViewMatters(self):
"""Test that UI context only checks view permission."""
db = Mock(spec=DatabaseConnector)
rbac = RbacClass(db)
user = User(
id="user1",
username="testuser",
roleLabels=["user"],
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
permissions = rbac.getUserPermissions(
user,
AccessRuleContext.UI,
"playground"
)
assert permissions.view == True
# Other permissions don't matter for UI context
def testResourceContextOnlyViewMatters(self):
"""Test that RESOURCE context only checks view permission."""
db = Mock(spec=DatabaseConnector)
rbac = RbacClass(db)
user = User(
id="user1",
username="testuser",
roleLabels=["user"],
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
permissions = rbac.getUserPermissions(
user,
AccessRuleContext.RESOURCE,
"ai.model.anthropic"
)
assert permissions.view == True