Skip to content

Commit

Permalink
Validate slug in addon services (#99232)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mdegat01 committed Aug 29, 2023
1 parent e2dd7f2 commit e0eb63c
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 41 deletions.
20 changes: 17 additions & 3 deletions homeassistant/components/hassio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
HassJob,
HomeAssistant,
ServiceCall,
async_get_hass,
callback,
)
from homeassistant.exceptions import HomeAssistantError
Expand Down Expand Up @@ -149,9 +150,22 @@
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)}
Expand All @@ -174,7 +188,7 @@
{
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]),
}
)

Expand All @@ -189,7 +203,7 @@
{
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]),
}
)

Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/hassio/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing import Any

import aiohttp
from yarl import URL

from homeassistant.components.http import (
CONF_SERVER_HOST,
Expand Down Expand Up @@ -530,6 +531,11 @@ async def send_command(
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,
Expand Down
7 changes: 7 additions & 0 deletions tests/components/hassio/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
103 changes: 65 additions & 38 deletions tests/components/hassio/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from unittest.mock import patch

import pytest
from voluptuous import Invalid

from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.components import frontend
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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"]
Expand All @@ -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"]
Expand All @@ -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"]
Expand Down Expand Up @@ -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
Expand All @@ -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"):
Expand All @@ -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"


Expand Down Expand Up @@ -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"})
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -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"],
Expand All @@ -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",
Expand All @@ -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:
Expand Down Expand Up @@ -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


Expand Down

0 comments on commit e0eb63c

Please sign in to comment.