mirror of
https://github.com/home-assistant/core.git
synced 2026-04-02 08:26:41 +01:00
Verify Proxmox permissions when creating snapshots (#166547)
This commit is contained in:
@@ -23,7 +23,7 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, ProxmoxPermission
|
||||
from .coordinator import ProxmoxConfigEntry, ProxmoxCoordinator, ProxmoxNodeData
|
||||
from .entity import ProxmoxContainerEntity, ProxmoxNodeEntity, ProxmoxVMEntity
|
||||
from .helpers import is_granted
|
||||
@@ -34,6 +34,8 @@ class ProxmoxNodeButtonNodeEntityDescription(ButtonEntityDescription):
|
||||
"""Class to hold Proxmox node button description."""
|
||||
|
||||
press_action: Callable[[ProxmoxCoordinator, str], None]
|
||||
permission: ProxmoxPermission = ProxmoxPermission.POWER
|
||||
permission_raise: str = "no_permission_node_power"
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -41,6 +43,8 @@ class ProxmoxVMButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Class to hold Proxmox VM button description."""
|
||||
|
||||
press_action: Callable[[ProxmoxCoordinator, str, int], None]
|
||||
permission: ProxmoxPermission = ProxmoxPermission.POWER
|
||||
permission_raise: str = "no_permission_vm_lxc_power"
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -48,6 +52,8 @@ class ProxmoxContainerButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Class to hold Proxmox container button description."""
|
||||
|
||||
press_action: Callable[[ProxmoxCoordinator, str, int], None]
|
||||
permission: ProxmoxPermission = ProxmoxPermission.POWER
|
||||
permission_raise: str = "no_permission_vm_lxc_power"
|
||||
|
||||
|
||||
NODE_BUTTONS: tuple[ProxmoxNodeButtonNodeEntityDescription, ...] = (
|
||||
@@ -156,6 +162,8 @@ VM_BUTTONS: tuple[ProxmoxVMButtonEntityDescription, ...] = (
|
||||
)
|
||||
)
|
||||
),
|
||||
permission=ProxmoxPermission.SNAPSHOT,
|
||||
permission_raise="no_permission_snapshot",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
)
|
||||
@@ -199,6 +207,8 @@ CONTAINER_BUTTONS: tuple[ProxmoxContainerButtonEntityDescription, ...] = (
|
||||
)
|
||||
)
|
||||
),
|
||||
permission=ProxmoxPermission.SNAPSHOT,
|
||||
permission_raise="no_permission_snapshot",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
)
|
||||
@@ -315,10 +325,15 @@ class ProxmoxNodeButtonEntity(ProxmoxNodeEntity, ProxmoxBaseButton):
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Execute the node button action via executor."""
|
||||
node_id = self._node_data.node["node"]
|
||||
if not is_granted(self.coordinator.permissions, p_type="nodes", p_id=node_id):
|
||||
if not is_granted(
|
||||
self.coordinator.permissions,
|
||||
p_type="nodes",
|
||||
p_id=node_id,
|
||||
permission=self.entity_description.permission,
|
||||
):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_permission_node_power",
|
||||
translation_key=self.entity_description.permission_raise,
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
self.entity_description.press_action,
|
||||
@@ -335,10 +350,15 @@ class ProxmoxVMButtonEntity(ProxmoxVMEntity, ProxmoxBaseButton):
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Execute the VM button action via executor."""
|
||||
vmid = self.vm_data["vmid"]
|
||||
if not is_granted(self.coordinator.permissions, p_type="vms", p_id=vmid):
|
||||
if not is_granted(
|
||||
self.coordinator.permissions,
|
||||
p_type="vms",
|
||||
p_id=vmid,
|
||||
permission=self.entity_description.permission,
|
||||
):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_permission_vm_lxc_power",
|
||||
translation_key=self.entity_description.permission_raise,
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
self.entity_description.press_action,
|
||||
@@ -357,10 +377,15 @@ class ProxmoxContainerButtonEntity(ProxmoxContainerEntity, ProxmoxBaseButton):
|
||||
"""Execute the container button action via executor."""
|
||||
vmid = self.container_data["vmid"]
|
||||
# Container power actions fall under vms
|
||||
if not is_granted(self.coordinator.permissions, p_type="vms", p_id=vmid):
|
||||
if not is_granted(
|
||||
self.coordinator.permissions,
|
||||
p_type="vms",
|
||||
p_id=vmid,
|
||||
permission=self.entity_description.permission,
|
||||
):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_permission_vm_lxc_power",
|
||||
translation_key=self.entity_description.permission_raise,
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
self.entity_description.press_action,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Constants for ProxmoxVE."""
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
DOMAIN = "proxmoxve"
|
||||
CONF_AUTH_METHOD = "auth_method"
|
||||
CONF_REALM = "realm"
|
||||
@@ -33,4 +35,9 @@ TYPE_VM = 0
|
||||
TYPE_CONTAINER = 1
|
||||
UPDATE_INTERVAL = 60
|
||||
|
||||
PERM_POWER = "VM.PowerMgmt"
|
||||
|
||||
class ProxmoxPermission(StrEnum):
|
||||
"""Proxmox permissions."""
|
||||
|
||||
POWER = "VM.PowerMgmt"
|
||||
SNAPSHOT = "VM.Snapshot"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""Helpers for Proxmox VE."""
|
||||
|
||||
from .const import PERM_POWER
|
||||
from .const import ProxmoxPermission
|
||||
|
||||
|
||||
def is_granted(
|
||||
permissions: dict[str, dict[str, int]],
|
||||
p_type: str = "vms",
|
||||
p_id: str | int | None = None, # can be str for nodes
|
||||
permission: str = PERM_POWER,
|
||||
permission: ProxmoxPermission = ProxmoxPermission.POWER,
|
||||
) -> bool:
|
||||
"""Validate user permissions for the given type and permission."""
|
||||
paths = [f"/{p_type}/{p_id}", f"/{p_type}", "/"]
|
||||
|
||||
@@ -315,6 +315,9 @@
|
||||
"no_permission_node_power": {
|
||||
"message": "The configured Proxmox VE user does not have permission to manage the power state of nodes. Please grant the user the 'VM.PowerMgmt' permission and try again."
|
||||
},
|
||||
"no_permission_snapshot": {
|
||||
"message": "The configured Proxmox VE user does not have permission to create snapshots of VMs and containers. Please grant the user the 'VM.Snapshot' permission and try again."
|
||||
},
|
||||
"no_permission_vm_lxc_power": {
|
||||
"message": "The configured Proxmox VE user does not have permission to manage the power state of VMs and containers. Please grant the user the 'VM.PowerMgmt' permission and try again."
|
||||
},
|
||||
|
||||
@@ -31,9 +31,20 @@ POWER_PERMISSIONS = {
|
||||
"/vms/101": {"VM.PowerMgmt": 0},
|
||||
}
|
||||
|
||||
SNAPSHOT_PERMISSIONS = {
|
||||
"/vms": {"VM.Snapshot": 1},
|
||||
"/vms/101": {"VM.Snapshot": 0},
|
||||
}
|
||||
|
||||
MERGED_PERMISSIONS = {
|
||||
key: {**AUDIT_PERMISSIONS.get(key, {}), **POWER_PERMISSIONS.get(key, {})}
|
||||
for key in set(AUDIT_PERMISSIONS) | set(POWER_PERMISSIONS)
|
||||
key: {
|
||||
**AUDIT_PERMISSIONS.get(key, {}),
|
||||
**POWER_PERMISSIONS.get(key, {}),
|
||||
**SNAPSHOT_PERMISSIONS.get(key, {}),
|
||||
}
|
||||
for key in set(AUDIT_PERMISSIONS)
|
||||
| set(POWER_PERMISSIONS)
|
||||
| set(SNAPSHOT_PERMISSIONS)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -370,6 +370,7 @@ async def test_container_buttons_exceptions(
|
||||
("button.pve1_start_all", "no_permission_node_power"),
|
||||
("button.ct_nginx_start", "no_permission_vm_lxc_power"),
|
||||
("button.vm_web_start", "no_permission_vm_lxc_power"),
|
||||
("button.vm_web_create_snapshot", "no_permission_snapshot"),
|
||||
],
|
||||
)
|
||||
async def test_node_buttons_permission_denied_for_auditor_role(
|
||||
@@ -394,19 +395,29 @@ async def test_node_buttons_permission_denied_for_auditor_role(
|
||||
assert exc_info.value.translation_key == translation_key
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "translation_key"),
|
||||
[
|
||||
("button.vm_db_start", "no_permission_vm_lxc_power"),
|
||||
("button.vm_db_create_snapshot", "no_permission_snapshot"),
|
||||
],
|
||||
)
|
||||
async def test_vm_buttons_denied_for_specific_vm(
|
||||
hass: HomeAssistant,
|
||||
mock_proxmox_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_id: str,
|
||||
translation_key: str,
|
||||
) -> None:
|
||||
"""Test that button only works on actual permissions."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
mock_proxmox_client._node_mock.qemu(101)
|
||||
|
||||
with pytest.raises(ServiceValidationError):
|
||||
with pytest.raises(ServiceValidationError) as exc_info:
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
{ATTR_ENTITY_ID: "button.vm_db_start"},
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
assert exc_info.value.translation_key == translation_key
|
||||
|
||||
Reference in New Issue
Block a user