Fix ProxmoxVE entities allowed without permissions (#176360)

This commit is contained in:
Tom
2026-07-14 20:14:37 +02:00
committed by GitHub
parent 1bc5926ff1
commit de252d4b0d
11 changed files with 141 additions and 512 deletions
@@ -20,6 +20,7 @@ from .const import (
STORAGE_ENABLED,
STORAGE_SHARED,
VM_CONTAINER_RUNNING,
ProxmoxPermission,
)
from .coordinator import ProxmoxConfigEntry, ProxmoxNodeData
from .entity import (
@@ -28,6 +29,7 @@ from .entity import (
ProxmoxStorageEntity,
ProxmoxVMEntity,
)
from .helpers import is_granted
PARALLEL_UPDATES = 0
@@ -51,6 +53,8 @@ class ProxmoxNodeBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Class to hold Proxmox node binary sensor description."""
state_fn: Callable[[ProxmoxNodeData], bool | None]
permission: ProxmoxPermission = ProxmoxPermission.SYSAUDIT
permission_target: str = "nodes"
@dataclass(frozen=True, kw_only=True)
@@ -67,6 +71,8 @@ NODE_SENSORS: tuple[ProxmoxNodeBinarySensorEntityDescription, ...] = (
state_fn=lambda data: data.node["status"] == NODE_ONLINE,
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
permission=ProxmoxPermission.VMAUDIT, # PVEVMUsers are allowed this node, through "/vms"
permission_target="vms",
),
ProxmoxNodeBinarySensorEntityDescription(
key="node_backup_status",
@@ -132,10 +138,17 @@ async def async_setup_entry(
def _async_add_new_nodes(nodes: list[ProxmoxNodeData]) -> None:
"""Add new node binary sensors."""
async_add_entities(
ProxmoxNodeBinarySensor(coordinator, entity_description, node)
for node in nodes
for entity_description in NODE_SENSORS
if is_granted(
coordinator.permissions,
p_type=entity_description.permission_target,
p_id=node.node["node"],
permission=entity_description.permission,
)
)
def _async_add_new_vms(
+22 -48
View File
@@ -17,7 +17,7 @@ from homeassistant.components.button import (
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
@@ -28,8 +28,6 @@ from .helpers import is_granted
PARALLEL_UPDATES = 1
NO_PERM_VM_LXC_POWER = "no_permission_vm_lxc_power"
@dataclass(frozen=True, kw_only=True)
class ProxmoxNodeButtonNodeEntityDescription(ButtonEntityDescription):
@@ -37,7 +35,6 @@ class ProxmoxNodeButtonNodeEntityDescription(ButtonEntityDescription):
press_action: Callable[[ProxmoxCoordinator, str], None]
permission: ProxmoxPermission = ProxmoxPermission.SYSPOWER
permission_raise: str = "no_permission_node_power"
permission_target: str = "nodes"
@@ -47,7 +44,6 @@ class ProxmoxVMButtonEntityDescription(ButtonEntityDescription):
press_action: Callable[[ProxmoxCoordinator, str, int], None]
permission: ProxmoxPermission = ProxmoxPermission.POWER
permission_raise: str = NO_PERM_VM_LXC_POWER
permission_target: str = "vms"
@@ -57,7 +53,6 @@ class ProxmoxContainerButtonEntityDescription(ButtonEntityDescription):
press_action: Callable[[ProxmoxCoordinator, str, int], None]
permission: ProxmoxPermission = ProxmoxPermission.POWER
permission_raise: str = NO_PERM_VM_LXC_POWER
permission_target: str = "vms"
@@ -82,7 +77,6 @@ NODE_BUTTONS: tuple[ProxmoxNodeButtonNodeEntityDescription, ...] = (
key="start_all",
translation_key="start_all",
permission=ProxmoxPermission.POWER,
permission_raise=NO_PERM_VM_LXC_POWER,
permission_target="vms",
press_action=lambda coordinator, node: coordinator.proxmox.nodes(
node
@@ -93,7 +87,6 @@ NODE_BUTTONS: tuple[ProxmoxNodeButtonNodeEntityDescription, ...] = (
key="stop_all",
translation_key="stop_all",
permission=ProxmoxPermission.POWER,
permission_raise=NO_PERM_VM_LXC_POWER,
permission_target="vms",
press_action=lambda coordinator, node: coordinator.proxmox.nodes(
node
@@ -104,7 +97,6 @@ NODE_BUTTONS: tuple[ProxmoxNodeButtonNodeEntityDescription, ...] = (
key="suspend_all",
translation_key="suspend_all",
permission=ProxmoxPermission.POWER,
permission_raise=NO_PERM_VM_LXC_POWER,
permission_target="vms",
press_action=lambda coordinator, node: coordinator.proxmox.nodes(
node
@@ -185,7 +177,6 @@ VM_BUTTONS: tuple[ProxmoxVMButtonEntityDescription, ...] = (
)
),
permission=ProxmoxPermission.SNAPSHOT,
permission_raise="no_permission_snapshot",
entity_category=EntityCategory.CONFIG,
),
)
@@ -230,7 +221,6 @@ CONTAINER_BUTTONS: tuple[ProxmoxContainerButtonEntityDescription, ...] = (
)
),
permission=ProxmoxPermission.SNAPSHOT,
permission_raise="no_permission_snapshot",
entity_category=EntityCategory.CONFIG,
),
)
@@ -250,6 +240,12 @@ async def async_setup_entry(
ProxmoxNodeButtonEntity(coordinator, entity_description, node)
for node in nodes
for entity_description in NODE_BUTTONS
if is_granted(
coordinator.permissions,
p_type=entity_description.permission_target,
p_id=node.node["node"],
permission=entity_description.permission,
)
)
def _async_add_new_vms(
@@ -260,6 +256,12 @@ async def async_setup_entry(
ProxmoxVMButtonEntity(coordinator, entity_description, vm, node_data)
for (node_data, vm) in vms
for entity_description in VM_BUTTONS
if is_granted(
coordinator.permissions,
p_type=entity_description.permission_target,
p_id=vm["vmid"],
permission=entity_description.permission,
)
)
def _async_add_new_containers(
@@ -272,6 +274,12 @@ async def async_setup_entry(
)
for (node_data, container) in containers
for entity_description in CONTAINER_BUTTONS
if is_granted(
coordinator.permissions,
p_type=entity_description.permission_target,
p_id=container["vmid"],
permission=entity_description.permission,
)
)
coordinator.new_nodes_callbacks.append(_async_add_new_nodes)
@@ -351,21 +359,10 @@ class ProxmoxNodeButtonEntity(ProxmoxNodeEntity, ProxmoxBaseButton):
@override
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=self.entity_description.permission_target,
p_id=node_id,
permission=self.entity_description.permission,
):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key=self.entity_description.permission_raise,
)
await self.hass.async_add_executor_job(
self.entity_description.press_action,
self.coordinator,
node_id,
self._node_data.node["node"],
)
@@ -377,22 +374,11 @@ class ProxmoxVMButtonEntity(ProxmoxVMEntity, ProxmoxBaseButton):
@override
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=self.entity_description.permission_target,
p_id=vmid,
permission=self.entity_description.permission,
):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key=self.entity_description.permission_raise,
)
await self.hass.async_add_executor_job(
self.entity_description.press_action,
self.coordinator,
self._node_name,
vmid,
self.vm_data["vmid"],
)
@@ -404,21 +390,9 @@ class ProxmoxContainerButtonEntity(ProxmoxContainerEntity, ProxmoxBaseButton):
@override
async def _async_press_call(self) -> None:
"""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=self.entity_description.permission_target,
p_id=vmid,
permission=self.entity_description.permission,
):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key=self.entity_description.permission_raise,
)
await self.hass.async_add_executor_job(
self.entity_description.press_action,
self.coordinator,
self._node_name,
vmid,
self.container_data["vmid"],
)
@@ -41,4 +41,6 @@ class ProxmoxPermission(StrEnum):
POWER = "VM.PowerMgmt"
SNAPSHOT = "VM.Snapshot"
SYSAUDIT = "Sys.Audit"
SYSPOWER = "Sys.PowerMgmt"
VMAUDIT = "VM.Audit"
@@ -18,6 +18,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from .const import ProxmoxPermission
from .coordinator import ProxmoxConfigEntry, ProxmoxNodeData
from .entity import (
ProxmoxContainerEntity,
@@ -25,6 +26,7 @@ from .entity import (
ProxmoxStorageEntity,
ProxmoxVMEntity,
)
from .helpers import is_granted
PARALLEL_UPDATES = 0
@@ -34,6 +36,8 @@ class ProxmoxNodeSensorEntityDescription(SensorEntityDescription):
"""Class to hold Proxmox node sensor description."""
value_fn: Callable[[ProxmoxNodeData], StateType | datetime]
permission: ProxmoxPermission = ProxmoxPermission.SYSAUDIT
permission_target: str = "nodes"
@dataclass(frozen=True, kw_only=True)
@@ -147,6 +151,8 @@ NODE_SENSORS: tuple[ProxmoxNodeSensorEntityDescription, ...] = (
value_fn=lambda data: data.node["status"],
device_class=SensorDeviceClass.ENUM,
options=["online", "offline"],
permission=ProxmoxPermission.VMAUDIT,
permission_target="vms",
),
ProxmoxNodeSensorEntityDescription(
key="node_backup_last_backup",
@@ -474,6 +480,12 @@ async def async_setup_entry(
ProxmoxNodeSensor(coordinator, entity_description, node)
for node in nodes
for entity_description in NODE_SENSORS
if is_granted(
coordinator.permissions,
p_type=entity_description.permission_target,
p_id=node.node["node"],
permission=entity_description.permission,
)
)
def _async_add_new_vms(
@@ -308,15 +308,6 @@
"no_nodes_found": {
"message": "No active nodes were found on the Proxmox VE server."
},
"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 'Sys.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."
},
"no_vmlxc_found": {
"message": "No LXC or VM were found on the Proxmox VE server."
},
+7
View File
@@ -1,5 +1,7 @@
"""Tests for Proxmox VE integration."""
from copy import deepcopy
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -53,6 +55,11 @@ MERGED_PERMISSIONS = {
| set(SNAPSHOT_PERMISSIONS)
}
PVEVMUSER_PERMISSIONS = deepcopy(MERGED_PERMISSIONS)
# Remove node-level and root-level scopes entirely
PVEVMUSER_PERMISSIONS.pop("/", None)
PVEVMUSER_PERMISSIONS.pop("/nodes", None)
async def setup_integration(
hass: HomeAssistant,
+7 -2
View File
@@ -15,6 +15,7 @@ from homeassistant.components.proxmoxve.const import (
CONF_TOKEN_SECRET,
CONF_VMS,
DOMAIN,
ProxmoxPermission,
)
from homeassistant.const import (
CONF_HOST,
@@ -124,8 +125,12 @@ def mock_proxmox_client():
node_mock.storage.get.return_value = load_json_array_fixture(
"nodes/storage.json", DOMAIN
)
node_mock.tasks.get.return_value = load_json_array_fixture(
"nodes/tasks.json", DOMAIN
node_mock.tasks.get.side_effect = lambda **kwargs: (
[]
if ProxmoxPermission.SYSAUDIT
not in mock_instance.access.permissions.get.return_value.get("/nodes", [])
else load_json_array_fixture("nodes/tasks.json", DOMAIN)
)
qemu_by_vmid = {vm["vmid"]: vm for vm in qemu_list}
@@ -652,407 +652,6 @@
'state': 'unknown',
})
# ---
# name: test_all_button_entities[button.vm_db-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.vm_db',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'proxmoxve',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'resume',
'unique_id': '1234_101_resume',
'unit_of_measurement': None,
})
# ---
# name: test_all_button_entities[button.vm_db-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'vm-db',
}),
'context': <ANY>,
'entity_id': 'button.vm_db',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_all_button_entities[button.vm_db_create_snapshot-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.vm_db_create_snapshot',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Create snapshot',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Create snapshot',
'platform': 'proxmoxve',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'snapshot_create',
'unique_id': '1234_101_snapshot_create',
'unit_of_measurement': None,
})
# ---
# name: test_all_button_entities[button.vm_db_create_snapshot-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'vm-db Create snapshot',
}),
'context': <ANY>,
'entity_id': 'button.vm_db_create_snapshot',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_all_button_entities[button.vm_db_hibernate-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.vm_db_hibernate',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Hibernate',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Hibernate',
'platform': 'proxmoxve',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'hibernate',
'unique_id': '1234_101_hibernate',
'unit_of_measurement': None,
})
# ---
# name: test_all_button_entities[button.vm_db_hibernate-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'vm-db Hibernate',
}),
'context': <ANY>,
'entity_id': 'button.vm_db_hibernate',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_all_button_entities[button.vm_db_reset-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.vm_db_reset',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Reset',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Reset',
'platform': 'proxmoxve',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'reset',
'unique_id': '1234_101_reset',
'unit_of_measurement': None,
})
# ---
# name: test_all_button_entities[button.vm_db_reset-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'vm-db Reset',
}),
'context': <ANY>,
'entity_id': 'button.vm_db_reset',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_all_button_entities[button.vm_db_restart-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.vm_db_restart',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Restart',
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.RESTART: 'restart'>,
'original_icon': None,
'original_name': 'Restart',
'platform': 'proxmoxve',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '1234_101_restart',
'unit_of_measurement': None,
})
# ---
# name: test_all_button_entities[button.vm_db_restart-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'restart',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'vm-db Restart',
}),
'context': <ANY>,
'entity_id': 'button.vm_db_restart',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_all_button_entities[button.vm_db_shut_down-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.vm_db_shut_down',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Shut down',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Shut down',
'platform': 'proxmoxve',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'shutdown',
'unique_id': '1234_101_shutdown',
'unit_of_measurement': None,
})
# ---
# name: test_all_button_entities[button.vm_db_shut_down-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'vm-db Shut down',
}),
'context': <ANY>,
'entity_id': 'button.vm_db_shut_down',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_all_button_entities[button.vm_db_start-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.vm_db_start',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Start',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Start',
'platform': 'proxmoxve',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'start',
'unique_id': '1234_101_start',
'unit_of_measurement': None,
})
# ---
# name: test_all_button_entities[button.vm_db_start-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'vm-db Start',
}),
'context': <ANY>,
'entity_id': 'button.vm_db_start',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_all_button_entities[button.vm_db_stop-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.vm_db_stop',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Stop',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Stop',
'platform': 'proxmoxve',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'stop',
'unique_id': '1234_101_stop',
'unit_of_measurement': None,
})
# ---
# name: test_all_button_entities[button.vm_db_stop-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'vm-db Stop',
}),
'context': <ANY>,
'entity_id': 'button.vm_db_stop',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_all_button_entities[button.vm_web-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -16,7 +16,7 @@ from homeassistant.const import STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
import homeassistant.helpers.entity_registry as er
from . import setup_integration
from . import PVEVMUSER_PERMISSIONS, setup_integration
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@@ -80,3 +80,48 @@ async def test_refresh_exceptions(
state = hass.states.get("binary_sensor.ct_nginx_status")
assert state.state == STATE_UNAVAILABLE
async def test_binary_sensors_according_to_permissions(
hass: HomeAssistant,
mock_proxmox_client: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that binary_sensors are created when allowed."""
with patch(
"homeassistant.components.proxmoxve.PLATFORMS",
[Platform.BINARY_SENSOR],
):
await setup_integration(hass, mock_config_entry)
entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
assert "binary_sensor.pve1_status" in {e.entity_id for e in entries}
assert "binary_sensor.pve1_backup_status" in {e.entity_id for e in entries}
async def test_binary_sensors_absent_according_to_permissions(
hass: HomeAssistant,
mock_proxmox_client: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that binary_sensors are not created when not allowed."""
mock_proxmox_client.access.permissions.get.return_value = PVEVMUSER_PERMISSIONS
with patch(
"homeassistant.components.proxmoxve.PLATFORMS",
[Platform.BINARY_SENSOR],
):
await setup_integration(hass, mock_config_entry)
entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
assert "binary_sensor.pve1_status" in {e.entity_id for e in entries}
assert "binary_sensor.pve1_backup_status" not in {e.entity_id for e in entries}
+8 -50
View File
@@ -11,7 +11,7 @@ from syrupy.assertion import SnapshotAssertion
from homeassistant.components.button import SERVICE_PRESS
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import AUDIT_PERMISSIONS, setup_integration
@@ -362,61 +362,19 @@ async def test_container_buttons_exceptions(
)
@pytest.mark.parametrize(
("entity_id", "translation_key"),
[
("button.pve1_shut_down", "no_permission_node_power"),
("button.pve1_start_all", "no_permission_vm_lxc_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(
async def test_buttons_only_allowed_buttons(
hass: HomeAssistant,
mock_proxmox_client: MagicMock,
mock_config_entry: MockConfigEntry,
entity_id: str,
translation_key: str,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that buttons are raising accordingly for Auditor permissions."""
"""Test that ProxmoxVE button is not generated when not allowed."""
mock_proxmox_client.access.permissions.get.return_value = AUDIT_PERMISSIONS
await setup_integration(hass, mock_config_entry)
with pytest.raises(ServiceValidationError) as exc_info:
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
assert exc_info.value.translation_key == translation_key
entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
@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) as exc_info:
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
assert exc_info.value.translation_key == translation_key
assert all(not entry.entity_id.startswith("button.") for entry in entries)
+24 -1
View File
@@ -9,7 +9,7 @@ from homeassistant.const import STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from . import PVEVMUSER_PERMISSIONS, setup_integration
from tests.common import (
MockConfigEntry,
@@ -68,3 +68,26 @@ async def test_storage_missing_used_fraction(
state = hass.states.get("sensor.storage_local_storage_usage_percentage")
assert state.state == STATE_UNKNOWN
async def test_sensors_according_to_permissions(
hass: HomeAssistant,
mock_proxmox_client: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that sensors are not created when not allowed."""
mock_proxmox_client.access.permissions.get.return_value = PVEVMUSER_PERMISSIONS
with patch(
"homeassistant.components.proxmoxve.PLATFORMS",
[Platform.SENSOR],
):
await setup_integration(hass, mock_config_entry)
entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
assert "sensor.pve1_status" in {e.entity_id for e in entries}
assert "sensor.pve1_cpu" not in {e.entity_id for e in entries}