Files
ss-tools/backend/tests/core/test_permission_utils.py
busya f0d7b79523 fix(rbac): single admin authority, gate privileged surfaces, enforce session on dual-auth
- is_admin flag is the sole admin authority; remove role-name fallbacks (dependencies, reports, tasks, agent lifecycle, security badge, frontend)
- normalize (resource, ACTION) across REST/MCP/catalog; strict admin role/permission parsing (400 on unknown)
- gate LLM provider CRUD/status/test on admin:settings; require auth on agent llm-status; quarantine listing requires scenario:automation MANAGE
- enforce logical-session revocation/idle in require_api_key_or_jwt
- last-admin lockout guards for user/role CRUD; block non-admin from granting is_admin
- frontend: flag-only admin, route permission map from the nav registry, write-control gating, 403 dedupe
- tests: orthogonal edge coverage (flag-only admin, lockout boundaries, session states, normalization)
2026-09-22 21:14:35 +03:00

195 lines
10 KiB
Python

# #region Test.Core.PermissionUtils [C:3] [TYPE Module] [SEMANTICS test,auth,rbac,permission,predicate]
# @BRIEF Verify user_has_permission — the pure user->permission predicate used by
# load_testing/lineage conditional PROD checks — plus the shared normalization helper and the
# is_admin predicate (flag is the sole administrative authority).
# @RELATION BINDS_TO -> [Core.Auth.PermissionUtils]
# @TEST_EDGE: missing_roles_attribute -> Missing roles attribute evaluates to False
# @TEST_EDGE: admin_short_circuit -> Admin role grants regardless of permissions
# @TEST_EDGE: exact_match -> resource:action match grants
# @TEST_EDGE: resource_mismatch -> Wrong resource denies even with matching action
# @TEST_EDGE: action_mismatch -> Right resource with wrong action denies
# @TEST_EDGE: missing_permissions -> Role without permissions denies
# @TEST_EDGE: missing_perm_fields -> Permission without resource/action fields denies
# @TEST_EDGE: normalization -> action case/whitespace and resource whitespace are ignored
# @TEST_INVARIANT: pure_predicate_never_raises -> VERIFIED_BY: test_missing_roles_attribute_denies,
# test_missing_permissions_attribute_denies, test_missing_perm_fields_deny
from types import SimpleNamespace
from src.core.auth.permission_utils import user_has_permission
# ═══════════════════════════════════════════════════════════════════
# Hardcoded fixture builders
# ═══════════════════════════════════════════════════════════════════
def _user(*roles):
"""Build a user carrying the given role objects."""
return SimpleNamespace(roles=list(roles))
def _role(is_admin=False, permissions=None):
"""Build a role with explicit is_admin flag and permission list."""
return SimpleNamespace(is_admin=is_admin, permissions=list(permissions or []))
def _perm(resource, action):
"""Build a permission with explicit resource/action."""
return SimpleNamespace(resource=resource, action=action)
# ═══════════════════════════════════════════════════════════════════
# Grant paths
# ═══════════════════════════════════════════════════════════════════
class TestUserHasPermissionGrant:
"""user_has_permission returns True for admin or exact resource:action match."""
def test_admin_role_grants_anything(self):
assert user_has_permission(_user(_role(is_admin=True)), "dashboard", "read") is True
def test_admin_short_circuits_permission_check(self):
"""Admin returns True before iterating permissions."""
assert user_has_permission(
_user(_role(is_admin=True, permissions=[_perm("other", "other")])),
"dashboard", "read",
) is True
def test_exact_resource_action_match_grants(self):
user = _user(_role(is_admin=False, permissions=[_perm("dashboard", "read")]))
assert user_has_permission(user, "dashboard", "read") is True
def test_match_in_second_role_grants(self):
user = _user(
_role(is_admin=False, permissions=[_perm("chart", "write")]),
_role(is_admin=False, permissions=[_perm("dashboard", "read")]),
)
assert user_has_permission(user, "dashboard", "read") is True
def test_match_in_second_permission_grants(self):
user = _user(_role(is_admin=False, permissions=[
_perm("chart", "read"),
_perm("dashboard", "read"),
]))
assert user_has_permission(user, "dashboard", "read") is True
# ═══════════════════════════════════════════════════════════════════
# Deny paths
# ═══════════════════════════════════════════════════════════════════
class TestUserHasPermissionDeny:
"""user_has_permission returns False when nothing matches."""
def test_no_roles_denies(self):
assert user_has_permission(_user(), "dashboard", "read") is False
def test_roles_none_denies(self):
assert user_has_permission(SimpleNamespace(roles=None), "dashboard", "read") is False
def test_missing_roles_attribute_denies(self):
"""current_user without any roles attribute -> False (getattr default)."""
assert user_has_permission(object(), "dashboard", "read") is False
def test_non_admin_without_permissions_denies(self):
user = _user(_role(is_admin=False))
assert user_has_permission(user, "dashboard", "read") is False
def test_missing_permissions_attribute_denies(self):
"""Role object without a permissions attribute -> inner loop skipped."""
user = _user(SimpleNamespace(is_admin=False))
assert user_has_permission(user, "dashboard", "read") is False
def test_resource_mismatch_denies(self):
"""Right action, wrong resource -> denied (short-circuits action check)."""
user = _user(_role(is_admin=False, permissions=[_perm("chart", "read")]))
assert user_has_permission(user, "dashboard", "read") is False
def test_action_mismatch_denies(self):
"""Right resource, wrong action -> denied."""
user = _user(_role(is_admin=False, permissions=[_perm("dashboard", "write")]))
assert user_has_permission(user, "dashboard", "read") is False
def test_missing_perm_fields_deny(self):
"""Permission without resource/action attributes -> never matches."""
user = _user(_role(is_admin=False, permissions=[object()]))
assert user_has_permission(user, "dashboard", "read") is False
def test_no_matching_permission_in_list_denies(self):
user = _user(_role(is_admin=False, permissions=[
_perm("dashboard", "write"),
_perm("chart", "read"),
]))
assert user_has_permission(user, "dashboard", "read") is False
# ═══════════════════════════════════════════════════════════════════
# Normalization + is_admin predicate (RBAC-03 / RBAC-09 edge coverage)
# ═══════════════════════════════════════════════════════════════════
class TestNormalizePermissionPair:
"""normalize_permission_pair is the single comparison boundary for REST/MCP/catalog."""
def test_strips_and_uppercases(self):
from src.core.auth.permission_utils import normalize_permission_pair
assert normalize_permission_pair(" scenario ", "run_prod") == ("scenario", "RUN_PROD")
def test_none_becomes_empty(self):
from src.core.auth.permission_utils import normalize_permission_pair
assert normalize_permission_pair(None, None) == ("", "")
def test_non_string_is_coerced(self):
from src.core.auth.permission_utils import normalize_permission_pair
assert normalize_permission_pair(123, 456) == ("123", "456")
def test_resource_with_colons_is_preserved(self):
from src.core.auth.permission_utils import normalize_permission_pair
assert normalize_permission_pair("a:b:c", " view ") == ("a:b:c", "VIEW")
class TestIsAdminUser:
"""The is_admin flag is the sole administrative authority; role NAME is never consulted."""
def test_flag_is_the_only_authority(self):
from src.core.auth.permission_utils import is_admin_user
assert is_admin_user(_user(_role(is_admin=True))) is True
assert is_admin_user(_user(_role(is_admin=False, permissions=[]))) is False
def test_role_named_admin_without_flag_is_not_admin(self):
from src.core.auth.permission_utils import is_admin_user
named = SimpleNamespace(name="Admin", is_admin=False, permissions=[])
assert is_admin_user(_user(named)) is False
def test_role_without_flag_attribute_is_not_admin(self):
from src.core.auth.permission_utils import is_admin_user
assert is_admin_user(_user(SimpleNamespace(name="Admin", permissions=[]))) is False
def test_roles_none_or_missing_is_not_admin(self):
from src.core.auth.permission_utils import is_admin_user
assert is_admin_user(SimpleNamespace(roles=None)) is False
assert is_admin_user(object()) is False
def test_numeric_flag_follows_truthiness(self):
from src.core.auth.permission_utils import is_admin_user
assert is_admin_user(_user(_role(is_admin=1))) is True
assert is_admin_user(_user(_role(is_admin=0))) is False
class TestUserHasPermissionNormalization:
"""Permission comparison is case/whitespace insensitive; empty requirement is not a grant."""
def test_action_comparison_is_case_insensitive(self):
user = _user(_role(permissions=[_perm("scenario", "RUN_PROD")]))
assert user_has_permission(user, "scenario", "run_prod") is True
def test_resource_whitespace_is_ignored(self):
user = _user(_role(permissions=[SimpleNamespace(resource="scenario", action="RUN")]))
assert user_has_permission(user, " scenario ", "run") is True
def test_role_named_admin_without_flag_does_not_grant(self):
named = SimpleNamespace(name="Admin", is_admin=False, permissions=[])
assert user_has_permission(_user(named), "tasks", "READ") is False
def test_empty_requirement_is_not_a_grant(self):
user = _user(_role(permissions=[_perm("tasks", "READ")]))
assert user_has_permission(user, "", "") is False
# #endregion Test.Core.PermissionUtils