412 lines
14 KiB
Python
412 lines
14 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)
|
|
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"
|
|
)
|
|
|
|
# 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)
|
|
dbApp = Mock(spec=DatabaseConnector)
|
|
rbac = RbacClass(db, dbApp=dbApp)
|
|
|
|
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)
|
|
dbApp = Mock(spec=DatabaseConnector)
|
|
rbac = RbacClass(db, dbApp=dbApp)
|
|
|
|
# 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)
|
|
dbApp = Mock(spec=DatabaseConnector)
|
|
rbac = RbacClass(db, dbApp=dbApp)
|
|
|
|
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)
|
|
dbApp = Mock(spec=DatabaseConnector)
|
|
rbac = RbacClass(db, dbApp=dbApp)
|
|
|
|
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)
|
|
dbApp = Mock(spec=DatabaseConnector)
|
|
rbac = RbacClass(db, dbApp=dbApp)
|
|
|
|
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)
|
|
dbApp = Mock(spec=DatabaseConnector)
|
|
rbac = RbacClass(db, dbApp=dbApp)
|
|
|
|
# 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)
|
|
dbApp = Mock(spec=DatabaseConnector)
|
|
rbac = RbacClass(db, dbApp=dbApp)
|
|
|
|
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)
|
|
dbApp = Mock(spec=DatabaseConnector)
|
|
rbac = RbacClass(db, dbApp=dbApp)
|
|
|
|
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
|