mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Fix ProxmoxVE entities allowed without permissions (#176360)
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user