- 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)
195 lines
10 KiB
Python
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
|