Validate slug in addon services (#99232)

* Validate slug in addon services

* Move validator into hassio component

* Fixes from mypy

* Fix test for changes

* Adjust fixtures to current supervisor

* Fix call counts after fixture adjustment

* Increase coverage
This commit is contained in:
Mike Degatano 2023-08-29 13:57:41 -04:00 committed by GitHub
parent e2dd7f2069
commit e0eb63c588
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 95 additions and 41 deletions

View File

@ -32,6 +32,7 @@ from homeassistant.core import (
HassJob,
HomeAssistant,
ServiceCall,
async_get_hass,
callback,
)
from homeassistant.exceptions import HomeAssistantError
@ -149,9 +150,22 @@ SERVICE_RESTORE_FULL = "restore_full"
SERVICE_RESTORE_PARTIAL = "restore_partial"
def valid_addon(value: Any) -> str:
"""Validate value is a valid addon slug."""
value = cv.slug(value)
hass: HomeAssistant | None = None
with suppress(HomeAssistantError):
hass = async_get_hass()
if hass and (addons := get_addons_info(hass)) is not None and value not in addons:
raise vol.Invalid("Not a valid add-on slug")
return value
SCHEMA_NO_DATA = vol.Schema({})
SCHEMA_ADDON = vol.Schema({vol.Required(ATTR_ADDON): cv.string})
SCHEMA_ADDON = vol.Schema({vol.Required(ATTR_ADDON): valid_addon})
SCHEMA_ADDON_STDIN = SCHEMA_ADDON.extend(
{vol.Required(ATTR_INPUT): vol.Any(dict, cv.string)}
@ -174,7 +188,7 @@ SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend(
{
vol.Optional(ATTR_HOMEASSISTANT): cv.boolean,
vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.slug]),
}
)
@ -189,7 +203,7 @@ SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend(
{
vol.Optional(ATTR_HOMEASSISTANT): cv.boolean,
vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.slug]),
}
)

View File

@ -8,6 +8,7 @@ import os
from typing import Any
import aiohttp
from yarl import URL
from homeassistant.components.http import (
CONF_SERVER_HOST,
@ -530,6 +531,11 @@ class HassIO:
This method is a coroutine.
"""
url = f"http://{self._ip}{command}"
if url != str(URL(url)):
_LOGGER.error("Invalid request %s", command)
raise HassioAPIError()
try:
request = await self.websession.request(
method,

View File

@ -413,3 +413,10 @@ async def test_api_reboot_host(
assert await handler.async_reboot_host(hass) == {}
assert aioclient_mock.call_count == 1
async def test_send_command_invalid_command(hass: HomeAssistant, hassio_stubs) -> None:
"""Test send command fails when command is invalid."""
hassio: HassIO = hass.data["hassio"]
with pytest.raises(HassioAPIError):
await hassio.send_command("/test/../bad")

View File

@ -5,6 +5,7 @@ from typing import Any
from unittest.mock import patch
import pytest
from voluptuous import Invalid
from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.components import frontend
@ -100,29 +101,29 @@ def mock_all(aioclient_mock, request, os_info):
"version_latest": "1.0.0",
"version": "1.0.0",
"auto_update": True,
"addons": [
{
"name": "test",
"slug": "test",
"state": "stopped",
"update_available": False,
"version": "1.0.0",
"version_latest": "1.0.0",
"repository": "core",
"icon": False,
},
{
"name": "test2",
"slug": "test2",
"state": "stopped",
"update_available": False,
"version": "1.0.0",
"version_latest": "1.0.0",
"repository": "core",
"icon": False,
},
],
},
"addons": [
{
"name": "test",
"slug": "test",
"installed": True,
"update_available": False,
"version": "1.0.0",
"version_latest": "1.0.0",
"repository": "core",
"url": "https://github.com/home-assistant/addons/test",
},
{
"name": "test2",
"slug": "test2",
"installed": True,
"update_available": False,
"version": "1.0.0",
"version_latest": "1.0.0",
"repository": "core",
"url": "https://github.com",
},
],
},
)
aioclient_mock.get(
@ -243,7 +244,7 @@ async def test_setup_api_ping(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 18
assert aioclient_mock.call_count == 22
assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0"
assert hass.components.hassio.is_hassio()
@ -288,7 +289,7 @@ async def test_setup_api_push_api_data(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 18
assert aioclient_mock.call_count == 22
assert not aioclient_mock.mock_calls[1][2]["ssl"]
assert aioclient_mock.mock_calls[1][2]["port"] == 9999
assert aioclient_mock.mock_calls[1][2]["watchdog"]
@ -307,7 +308,7 @@ async def test_setup_api_push_api_data_server_host(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 18
assert aioclient_mock.call_count == 22
assert not aioclient_mock.mock_calls[1][2]["ssl"]
assert aioclient_mock.mock_calls[1][2]["port"] == 9999
assert not aioclient_mock.mock_calls[1][2]["watchdog"]
@ -324,7 +325,7 @@ async def test_setup_api_push_api_data_default(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 18
assert aioclient_mock.call_count == 22
assert not aioclient_mock.mock_calls[1][2]["ssl"]
assert aioclient_mock.mock_calls[1][2]["port"] == 8123
refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"]
@ -404,7 +405,7 @@ async def test_setup_api_existing_hassio_user(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 18
assert aioclient_mock.call_count == 22
assert not aioclient_mock.mock_calls[1][2]["ssl"]
assert aioclient_mock.mock_calls[1][2]["port"] == 8123
assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token
@ -421,7 +422,7 @@ async def test_setup_core_push_timezone(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 18
assert aioclient_mock.call_count == 22
assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone"
with patch("homeassistant.util.dt.set_default_time_zone"):
@ -441,7 +442,7 @@ async def test_setup_hassio_no_additional_data(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 18
assert aioclient_mock.call_count == 22
assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456"
@ -486,13 +487,17 @@ async def test_service_register(hassio_env, hass: HomeAssistant) -> None:
@pytest.mark.freeze_time("2021-11-13 11:48:00")
async def test_service_calls(
hassio_env,
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Call service and check the API calls behind that."""
assert await async_setup_component(hass, "hassio", {})
with patch.dict(os.environ, MOCK_ENVIRON), patch(
"homeassistant.components.hassio.HassIO.is_connected",
return_value=None,
):
assert await async_setup_component(hass, "hassio", {})
await hass.async_block_till_done()
aioclient_mock.post("http://127.0.0.1/addons/test/start", json={"result": "ok"})
aioclient_mock.post("http://127.0.0.1/addons/test/stop", json={"result": "ok"})
@ -519,14 +524,14 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 10
assert aioclient_mock.call_count == 26
assert aioclient_mock.mock_calls[-1][2] == "test"
await hass.services.async_call("hassio", "host_shutdown", {})
await hass.services.async_call("hassio", "host_reboot", {})
await hass.async_block_till_done()
assert aioclient_mock.call_count == 12
assert aioclient_mock.call_count == 28
await hass.services.async_call("hassio", "backup_full", {})
await hass.services.async_call(
@ -541,7 +546,7 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 14
assert aioclient_mock.call_count == 30
assert aioclient_mock.mock_calls[-1][2] == {
"name": "2021-11-13 11:48:00",
"homeassistant": True,
@ -566,7 +571,7 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 16
assert aioclient_mock.call_count == 32
assert aioclient_mock.mock_calls[-1][2] == {
"addons": ["test"],
"folders": ["ssl"],
@ -584,7 +589,7 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 17
assert aioclient_mock.call_count == 33
assert aioclient_mock.mock_calls[-1][2] == {
"name": "backup_name",
"location": "backup_share",
@ -599,13 +604,35 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 18
assert aioclient_mock.call_count == 34
assert aioclient_mock.mock_calls[-1][2] == {
"name": "2021-11-13 11:48:00",
"location": None,
}
async def test_invalid_service_calls(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Call service with invalid input and check that it raises."""
with patch.dict(os.environ, MOCK_ENVIRON), patch(
"homeassistant.components.hassio.HassIO.is_connected",
return_value=None,
):
assert await async_setup_component(hass, "hassio", {})
await hass.async_block_till_done()
with pytest.raises(Invalid):
await hass.services.async_call(
"hassio", "addon_start", {"addon": "does_not_exist"}
)
with pytest.raises(Invalid):
await hass.services.async_call(
"hassio", "addon_stdin", {"addon": "does_not_exist", "input": "test"}
)
async def test_service_calls_core(
hassio_env, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
@ -889,7 +916,7 @@ async def test_setup_hardware_integration(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 18
assert aioclient_mock.call_count == 22
assert len(mock_setup_entry.mock_calls) == 1