"""Tests for the 4-role RBAC system.""" from __future__ import annotations import pytest from arautopilot.core.rbac import ( Capability, PermissionError, Role, capabilities_of, has, require, requires_dual_auth, ) # ---------------------------------------------------------------------------- # Capability matrix # ---------------------------------------------------------------------------- def test_super_admin_has_every_capability() -> None: super_admin_caps = capabilities_of(Role.SUPER_ADMIN) assert super_admin_caps == frozenset(Capability) @pytest.mark.parametrize("cap", [ Capability.FLASH_FIRMWARE, Capability.BUILD_FIRMWARE, Capability.EDIT_FIRMWARE_SOURCE, Capability.EDIT_COMMISSIONING, Capability.EDIT_OPERATIONAL, Capability.ENGAGE_PILOT, Capability.READ_TELEMETRY, Capability.ACK_ALARMS, Capability.VIEW_AUDIT_LOG_FULL, Capability.VIEW_AUDIT_LOG_VESSEL, ]) def test_engineer_can(cap: Capability) -> None: assert has(Role.ENGINEER, cap) @pytest.mark.parametrize("cap", [ Capability.EDIT_PYTHON_PROJECT, Capability.EDIT_BASE_GAINS, Capability.MANAGE_USERS, ]) def test_engineer_cannot(cap: Capability) -> None: assert not has(Role.ENGINEER, cap) @pytest.mark.parametrize("cap", [ Capability.EDIT_OPERATIONAL, Capability.MANAGE_USERS, Capability.ENGAGE_PILOT, Capability.READ_TELEMETRY, Capability.ACK_ALARMS, Capability.VIEW_AUDIT_LOG_VESSEL, ]) def test_owner_can(cap: Capability) -> None: assert has(Role.OWNER, cap) @pytest.mark.parametrize("cap", [ Capability.EDIT_PYTHON_PROJECT, Capability.EDIT_FIRMWARE_SOURCE, Capability.FLASH_FIRMWARE, Capability.BUILD_FIRMWARE, Capability.EDIT_BASE_GAINS, Capability.EDIT_COMMISSIONING, Capability.VIEW_AUDIT_LOG_FULL, ]) def test_owner_cannot(cap: Capability) -> None: assert not has(Role.OWNER, cap) @pytest.mark.parametrize("cap", [ Capability.ENGAGE_PILOT, Capability.READ_TELEMETRY, Capability.ACK_ALARMS, ]) def test_user_can(cap: Capability) -> None: assert has(Role.USER, cap) @pytest.mark.parametrize("cap", [ Capability.EDIT_PYTHON_PROJECT, Capability.EDIT_FIRMWARE_SOURCE, Capability.FLASH_FIRMWARE, Capability.BUILD_FIRMWARE, Capability.EDIT_BASE_GAINS, Capability.EDIT_COMMISSIONING, Capability.EDIT_OPERATIONAL, Capability.MANAGE_USERS, Capability.VIEW_AUDIT_LOG_FULL, Capability.VIEW_AUDIT_LOG_VESSEL, ]) def test_user_cannot(cap: Capability) -> None: assert not has(Role.USER, cap) # ---------------------------------------------------------------------------- # Dual-auth policy # ---------------------------------------------------------------------------- def test_engineer_flashing_requires_dual_auth() -> None: assert requires_dual_auth(Role.ENGINEER, Capability.FLASH_FIRMWARE) def test_super_admin_flashing_is_single_factor() -> None: assert not requires_dual_auth(Role.SUPER_ADMIN, Capability.FLASH_FIRMWARE) @pytest.mark.parametrize("role", [Role.SUPER_ADMIN, Role.ENGINEER, Role.OWNER, Role.USER]) @pytest.mark.parametrize("cap", [ Capability.ENGAGE_PILOT, Capability.READ_TELEMETRY, Capability.EDIT_OPERATIONAL, ]) def test_non_flash_actions_never_need_dual_auth(role: Role, cap: Capability) -> None: assert not requires_dual_auth(role, cap) # ---------------------------------------------------------------------------- # require() helper # ---------------------------------------------------------------------------- def test_require_passes_when_granted() -> None: require(Role.USER, Capability.ENGAGE_PILOT) # no raise def test_require_raises_on_denial() -> None: with pytest.raises(PermissionError): require(Role.USER, Capability.EDIT_BASE_GAINS) def test_no_privilege_escalation_via_unknown_capability() -> None: # Make sure that Owner / User can never grow capabilities by some # accidental code path -- we just snapshot the matrix here. assert capabilities_of(Role.USER) < capabilities_of(Role.OWNER) assert capabilities_of(Role.OWNER) < capabilities_of(Role.SUPER_ADMIN) assert capabilities_of(Role.ENGINEER) < capabilities_of(Role.SUPER_ADMIN) def test_engineer_and_owner_capabilities_overlap_but_neither_subsumes_other() -> None: eng = capabilities_of(Role.ENGINEER) own = capabilities_of(Role.OWNER) # Engineer can flash; Owner can manage users -- neither set is a subset # of the other. assert Capability.FLASH_FIRMWARE in eng and Capability.FLASH_FIRMWARE not in own assert Capability.MANAGE_USERS in own and Capability.MANAGE_USERS not in eng