Skip to content

Commit

Permalink
Improved permissions repairs
Browse files Browse the repository at this point in the history
  • Loading branch information
dougiteixeira committed Nov 12, 2023
1 parent 0a838ae commit 2b4d570
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 36 deletions.
143 changes: 138 additions & 5 deletions custom_components/proxmoxve/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import CONF_NODE, DOMAIN, LOGGER, UPDATE_INTERVAL, ProxmoxType
from .models import ProxmoxLXCData, ProxmoxNodeData, ProxmoxStorageData, ProxmoxVMData
from .models import ProxmoxLXCData, ProxmoxNodeData, ProxmoxStorageData, ProxmoxUpdateData, ProxmoxVMData


class ProxmoxCoordinator(
Expand Down Expand Up @@ -89,6 +89,7 @@ def poll_api() -> dict[str, Any] | None:
translation_placeholders={
"resource": f"Node {self.resource_id}",
"user": self.config_entry.data[CONF_USERNAME],
"permission": f"['perm','/nodes/{self.resource_id}',['Sys.Audit']]"
},
)
raise UpdateFailed(
Expand Down Expand Up @@ -200,6 +201,7 @@ def poll_api() -> dict[str, Any] | None:
translation_placeholders={
"resource": f"QEMU {self.resource_id}",
"user": self.config_entry.data[CONF_USERNAME],
"permission": "['perm','/vms/{self.resource_id}',['VM.Audit']]"
},
)
raise UpdateFailed(
Expand Down Expand Up @@ -310,6 +312,7 @@ def poll_api() -> dict[str, Any] | None:
translation_placeholders={
"resource": f"LXC {self.resource_id}",
"user": self.config_entry.data[CONF_USERNAME],
"permission": "['perm','/vms/{self.resource_id}',['VM.Audit']]"
},
)
raise UpdateFailed(
Expand Down Expand Up @@ -422,6 +425,7 @@ def poll_api() -> dict[str, Any] | None:
translation_placeholders={
"resource": f"Storage {self.resource_id}",
"user": self.config_entry.data[CONF_USERNAME],
"permission": f"['perm','/storage/{self.resource_id}',['Datastore.Audit'],'any',1]"
},
)
raise UpdateFailed(
Expand All @@ -439,8 +443,8 @@ def poll_api() -> dict[str, Any] | None:

api_status = await self.hass.async_add_executor_job(poll_api)

#if api_status is None or "content" not in api_status:
# raise UpdateFailed(f"Storage {self.resource_id} unable to be found")
if api_status is None or "content" not in api_status:
raise UpdateFailed(f"Storage {self.resource_id} unable to be found")

update_device_via(self, ProxmoxType.Storage)
return ProxmoxStorageData(
Expand All @@ -452,6 +456,114 @@ def poll_api() -> dict[str, Any] | None:
content=api_status["content"]
)

class ProxmoxUpdateCoordinator(ProxmoxCoordinator):
"""Proxmox VE Update data update coordinator."""

def __init__(
self,
hass: HomeAssistant,
proxmox: ProxmoxAPI,
api_category: str,
node_name: int,
) -> None:
"""Initialize the Proxmox LXC coordinator."""

super().__init__(
hass,
LOGGER,
name=f"proxmox_coordinator_{api_category}_{node_name}",
update_interval=timedelta(seconds=UPDATE_INTERVAL),
)

self.hass = hass
self.config_entry: ConfigEntry = self.config_entry
self.proxmox = proxmox
self.node_name = node_name
self.resource_id = f"{api_category}_{node_name}"

async def _async_update_data(self) -> ProxmoxLXCData:
"""Update data for Proxmox LXC."""

def poll_api() -> dict[str, Any] | None:
"""Return data from the Proxmox LXC API."""
try:
api_status = None

if self.node_name is not None:
api_status = (
self.proxmox.nodes(self.node_name)
.apt
.update.get()
)

if self.node_name is None:
raise UpdateFailed(f"{self.resource_id} node not found")

except (
AuthenticationError,
SSLError,
ConnectTimeout,
) as error:
raise UpdateFailed(error) from error
except ResourceException as error:
if error.status_code == 403:
async_create_issue(
self.hass,
DOMAIN,
f"{self.config_entry.entry_id}_{self.resource_id}_forbiden",
is_fixable=False,
severity=IssueSeverity.ERROR,
translation_key="resource_exception_forbiden",
translation_placeholders={
"resource": f"Update {self.node_name}",
"user": self.config_entry.data[CONF_USERNAME],
"permission": f"['perm','/nodes/{self.node_name}',['Sys.Modify']]"
},
)
raise UpdateFailed(
"User not allowed to access the resource, check user permissions as per the documentation."
) from error

async_delete_issue(
self.hass,
DOMAIN,
f"{self.config_entry.entry_id}_{self.resource_id}_forbiden",
)

LOGGER.debug("API Response - Update: %s", api_status)
return api_status

api_status = await self.hass.async_add_executor_job(poll_api)

if api_status is None:
return ProxmoxUpdateData(
type=ProxmoxType.Update,
node=self.node_name,
total=0,
updates_list=[],
update=False,
)

list = []
total = 0
for update in api_status:
list.append(f"{update['Title']} - {update['Version']}")
total += 1

list.sort()

update_avail=False
if total >0:
update_avail=True

return ProxmoxUpdateData(
type=ProxmoxType.Update,
node=self.node_name,
total=total,
updates_list=list,
update=update_avail,
)


def update_device_via(
self,
Expand Down Expand Up @@ -505,19 +617,39 @@ async def verify_permissions_error(
except ResourceException as error:
if error.status_code == 403:
permissions = True
permission_check = "['perm','/nodes/{resource}',['Sys.Audit']]"

if resource_type == ProxmoxType.QEMU:
try:
self.proxmox.nodes(resource_node).qemu(resource).get()
self.proxmox.nodes(resource_node).qemu(resource).status.current.get()
except ResourceException as error:
if error.status_code == 403:
permissions = True
permission_check = "['perm','/vms/{resource}',['VM.Audit']]"

if resource_type == ProxmoxType.LXC:
try:
self.proxmox.nodes(resource_node).lxc(resource).get()
self.proxmox.nodes(resource_node).lxc(resource).status.current.get()
except ResourceException as error:
if error.status_code == 403:
permissions = True
permission_check = "['perm','/vms/{resource}',['VM.Audit']]"

if resource_type == ProxmoxType.Storage:
try:
self.proxmox.nodes(resource_node).storage(resource).get()
except ResourceException as error:
if error.status_code == 403:
permissions = True
permission_check = f"['perm','/storage/{resource}',['Datastore.Audit'],'any',1]"

if resource_type == ProxmoxType.Update:
try:
self.proxmox.nodes(resource_node).apt.update.get()
except ResourceException as error:
if error.status_code == 403:
permissions = True
permission_check = f"['perm','/nodes/{resource_node}',['Sys.Modify']]"

if permissions:
async_create_issue(
Expand All @@ -530,6 +662,7 @@ async def verify_permissions_error(
translation_placeholders={
"resource": f"{resource_type.upper()} {resource}",
"user": self.config_entry.data[CONF_USERNAME],
"permission": permission_check,
},
)
return permissions
63 changes: 53 additions & 10 deletions custom_components/proxmoxve/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import Any
from attr import asdict

from proxmoxer import ProxmoxAPI
from proxmoxer.core import ResourceException

from homeassistant.components.diagnostics.util import async_redact_data
from homeassistant.config_entries import ConfigEntry
Expand All @@ -16,7 +16,7 @@
from homeassistant.helpers import device_registry as dr, entity_registry as er

from .const import COORDINATORS, DOMAIN, LOGGER, PROXMOX_CLIENT
from .coordinator import ProxmoxNodeCoordinator, ProxmoxQEMUCoordinator, ProxmoxLXCCoordinator
from .coordinator import ProxmoxNodeCoordinator, ProxmoxQEMUCoordinator, ProxmoxLXCCoordinator, ProxmoxStorageCoordinator, ProxmoxUpdateCoordinator

TO_REDACT_CONFIG = ["host", "username", "password"]

Expand All @@ -32,21 +32,64 @@ async def async_get_config_entry_diagnostics(
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""

coordinators: dict[str, ProxmoxNodeCoordinator | ProxmoxQEMUCoordinator | ProxmoxLXCCoordinator] = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS]
coordinators: dict[str, ProxmoxNodeCoordinator | ProxmoxQEMUCoordinator | ProxmoxLXCCoordinator | ProxmoxStorageCoordinator | ProxmoxUpdateCoordinator] = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS]

proxmox_client = hass.data[DOMAIN][config_entry.entry_id][PROXMOX_CLIENT]

proxmox = proxmox_client.get_api_client()

resources = await hass.async_add_executor_job(proxmox.cluster.resources.get)
try:
resources = await hass.async_add_executor_job(proxmox.cluster.resources.get)
except ResourceException as error:
if error.status_code == 403:
resources = "403 Forbidden: Permission check failed"
else:
resources = "Error"


nodes = {}
nodes_api= await hass.async_add_executor_job(proxmox.nodes().get)
try:
nodes_api= await hass.async_add_executor_job(proxmox.nodes().get)
except ResourceException as error:
if error.status_code == 403:
nodes_api = "403 Forbidden: Permission check failed"
else:
nodes_api = "Error"

for node in nodes_api:
nodes[node["node"]] = node
nodes[node["node"]]["qemu"] = await hass.async_add_executor_job(proxmox.nodes(node["node"]).qemu.get)
nodes[node["node"]]["lxc"] = await hass.async_add_executor_job(proxmox.nodes(node["node"]).lxc.get)

try:
nodes[node["node"]]["qemu"] = await hass.async_add_executor_job(proxmox.nodes(node["node"]).qemu.get)
except ResourceException as error:
if error.status_code == 403:
nodes[node["node"]]["qemu"] = "403 Forbidden: Permission check failed"
else:
nodes[node["node"]]["qemu"] = "Error"

try:
nodes[node["node"]]["lxc"] = await hass.async_add_executor_job(proxmox.nodes(node["node"]).lxc.get)
except ResourceException as error:
if error.status_code == 403:
nodes[node["node"]]["lxc"] = "403 Forbidden: Permission check failed"
else:
nodes[node["node"]]["lxc"] = "Error"

try:
nodes[node["node"]]["storage"] = await hass.async_add_executor_job(proxmox.nodes(node["node"]).storage.get)
except ResourceException as error:
if error.status_code == 403:
nodes[node["node"]]["storage"] = "403 Forbidden: Permission check failed"
else:
nodes[node["node"]]["storage"] = "Error"

try:
nodes[node["node"]]["updates"] = await hass.async_add_executor_job(proxmox.nodes(node["node"]).apt.update.get)
except ResourceException as error:
if error.status_code == 403:
nodes[node["node"]]["updates"] = "403 Forbidden: Permission check failed"
else:
nodes[node["node"]]["updates"] = "Error"

api_data = {
"resources": resources,
Expand Down Expand Up @@ -90,11 +133,11 @@ async def async_get_config_entry_diagnostics(
coordinator.name: {
"data": async_redact_data(
coordinator.data.__dict__, TO_REDACT_COORD
),
) if coordinator.data is not None else {}
}
for i, coordinator in enumerate(coordinators.values())
for coordinator in coordinators.values()
},
"api_response": async_redact_data(api_data, TO_REDACT_API),
"api_response": async_redact_data(api_data, TO_REDACT_API) if api_data is not None else {},
}

async def async_get_device_diagnostics(
Expand Down
4 changes: 2 additions & 2 deletions custom_components/proxmoxve/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,15 @@
"description": "Configuration of the {integration} (`{platform}`) in YAML is deprecated and should be removed in {version}.\n\nResolve the import issues and remove the YAML configuration from your `configuration.yaml` file."
},
"resource_nonexistent": {
"description": "{resource_type} {resource} does not exist on ({host}:{port}), remove it in integration options.\n\nThis can also be caused if the user doesn't have enough permission to access the resource.",
"description": "{resource_type} {resource} does not exist on ({host}:{port}), remove it in integration options.\n\nThis can also be caused if the user doesn't have enough permission to access the resource.\n\nTip on required permissions:\n* `{permission}`",
"title": "{resource_type} {resource} does not exist"
},
"no_permissions": {
"description": "The user `{user}` does not have the required permissions for all features.\n\nThe following features are not accessible by the user:\n`{errors}`\n\nCheck the user permissions as described in the documentation.",
"title": "User `{user}` does not have the required permissions"
},
"resource_exception_forbiden": {
"description": "User {user} does not have sufficient permissions to access resource {resource}.\n\nPlease check documentation and user permissions.",
"description": "User `{user}` does not have sufficient permissions to access resource `{resource}`.\n\nTip on required permissions:\n* `{permission}`\n\nPlease check documentation and user permissions.",
"title": "Permissions error for `{resource}`"
}
},
Expand Down
4 changes: 2 additions & 2 deletions custom_components/proxmoxve/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,15 @@
"description": "Configuration of the {integration} (`{platform}`) in YAML is deprecated and should be removed in {version}.\n\nResolve the import issues and remove the YAML configuration from your `configuration.yaml` file."
},
"resource_nonexistent": {
"description": "{resource_type} {resource} does not exist on ({host}:{port}), remove it in integration options.\n\nThis can also be caused if the user doesn't have enough permission to access the resource.",
"description": "{resource_type} {resource} does not exist on ({host}:{port}), remove it in integration options.\n\nThis can also be caused if the user doesn't have enough permission to access the resource.\n\nTip on required permissions:\n* `{permission}`",
"title": "{resource_type} {resource} does not exist"
},
"no_permissions": {
"description": "The user `{user}` does not have the required permissions for all features.\n\nThe following features are not accessible by the user:\n`{errors}`\n\nCheck the user permissions as described in the documentation.",
"title": "User `{user}` does not have the required permissions"
},
"resource_exception_forbiden": {
"description": "User {user} does not have sufficient permissions to access resource {resource}.\n\nPlease check documentation and user permissions.",
"description": "User `{user}` does not have sufficient permissions to access resource `{resource}`.\n\nTip on required permissions:\n* `{permission}`\n\nPlease check documentation and user permissions.",
"title": "Permissions error for `{resource}`"
}
},
Expand Down
Loading

0 comments on commit 2b4d570

Please sign in to comment.