History API entity_id validation (#90067)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Harvey 2023-04-14 20:41:54 +01:00 committed by GitHub
parent f5911bcad6
commit bf4559719a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 412 additions and 5 deletions

View file

@ -12,7 +12,7 @@ from homeassistant.components import frontend
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.recorder import get_instance, history
from homeassistant.components.recorder.util import session_scope
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, valid_entity_id
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA
from homeassistant.helpers.typing import ConfigType
@ -73,6 +73,14 @@ class HistoryPeriodView(HomeAssistantView):
"filter_entity_id is missing", HTTPStatus.BAD_REQUEST
)
hass = request.app["hass"]
for entity_id in entity_ids:
if not hass.states.get(entity_id) and not valid_entity_id(entity_id):
return self.json_message(
"Invalid filter_entity_id", HTTPStatus.BAD_REQUEST
)
now = dt_util.utcnow()
if datetime_:
start_time = dt_util.as_utc(datetime_)
@ -96,8 +104,6 @@ class HistoryPeriodView(HomeAssistantView):
minimal_response = "minimal_response" in request.query
no_attributes = "no_attributes" in request.query
hass = request.app["hass"]
if (
not include_start_time_state
and entity_ids

View file

@ -27,6 +27,7 @@ from homeassistant.core import (
State,
callback,
is_callback,
valid_entity_id,
)
from homeassistant.helpers.event import (
async_track_point_in_utc_time,
@ -95,7 +96,7 @@ def _ws_get_significant_states(
vol.Required("type"): "history/history_during_period",
vol.Required("start_time"): str,
vol.Optional("end_time"): str,
vol.Optional("entity_ids"): [str],
vol.Required("entity_ids"): [str],
vol.Optional("include_start_time_state", default=True): bool,
vol.Optional("significant_changes_only", default=True): bool,
vol.Optional("minimal_response", default=False): bool,
@ -129,7 +130,12 @@ async def ws_get_history_during_period(
connection.send_result(msg["id"], {})
return
entity_ids = msg.get("entity_ids")
entity_ids: list[str] = msg["entity_ids"]
for entity_id in entity_ids:
if not hass.states.get(entity_id) and not valid_entity_id(entity_id):
connection.send_error(msg["id"], "invalid_entity_ids", "Invalid entity_ids")
return
include_start_time_state = msg["include_start_time_state"]
no_attributes = msg["no_attributes"]
@ -428,6 +434,11 @@ async def ws_stream(
return
entity_ids: list[str] = msg["entity_ids"]
for entity_id in entity_ids:
if not hass.states.get(entity_id) and not valid_entity_id(entity_id):
connection.send_error(msg["id"], "invalid_entity_ids", "Invalid entity_ids")
return
include_start_time_state = msg["include_start_time_state"]
significant_changes_only = msg["significant_changes_only"]
no_attributes = msg["no_attributes"]

View file

@ -696,3 +696,72 @@ async def test_fetch_period_api_with_no_entity_ids(
assert response.status == HTTPStatus.BAD_REQUEST
response_json = await response.json()
assert response_json == {"message": "filter_entity_id is missing"}
@pytest.mark.parametrize(
("filter_entity_id", "status_code", "response_contains1", "response_contains2"),
[
("light.kitchen,light.cow", HTTPStatus.OK, "light.kitchen", "light.cow"),
(
"light.kitchen,light.cow&",
HTTPStatus.BAD_REQUEST,
"message",
"Invalid filter_entity_id",
),
(
"light.kitchen,li-ght.cow",
HTTPStatus.BAD_REQUEST,
"message",
"Invalid filter_entity_id",
),
(
"light.kit!chen",
HTTPStatus.BAD_REQUEST,
"message",
"Invalid filter_entity_id",
),
(
"lig+ht.kitchen,light.cow",
HTTPStatus.BAD_REQUEST,
"message",
"Invalid filter_entity_id",
),
(
"light.kitchenlight.cow",
HTTPStatus.BAD_REQUEST,
"message",
"Invalid filter_entity_id",
),
("cow", HTTPStatus.BAD_REQUEST, "message", "Invalid filter_entity_id"),
],
)
async def test_history_with_invalid_entity_ids(
filter_entity_id,
status_code,
response_contains1,
response_contains2,
recorder_mock: Recorder,
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
) -> None:
"""Test sending valid and invalid entity_ids to the API."""
await async_setup_component(
hass,
"history",
{"history": {}},
)
hass.states.async_set("light.kitchen", "on")
hass.states.async_set("light.cow", "on")
await async_wait_recording_done(hass)
now = dt_util.utcnow().isoformat()
client = await hass_client()
response = await client.get(
f"/api/history/period/{now}",
params={"filter_entity_id": filter_entity_id},
)
assert response.status == status_code
response_json = await response.json()
assert response_contains1 in str(response_json)
assert response_contains2 in str(response_json)

View file

@ -978,6 +978,7 @@ async def test_history_during_period_bad_start_time(
{
"id": 1,
"type": "history/history_during_period",
"entity_ids": ["sensor.pet"],
"start_time": "cats",
}
)
@ -1004,6 +1005,7 @@ async def test_history_during_period_bad_end_time(
{
"id": 1,
"type": "history/history_during_period",
"entity_ids": ["sensor.pet"],
"start_time": now.isoformat(),
"end_time": "dogs",
}

View file

@ -417,6 +417,7 @@ async def test_history_during_period_bad_start_time(
{
"id": 1,
"type": "history/history_during_period",
"entity_ids": ["sensor.pet"],
"start_time": "cats",
}
)
@ -442,6 +443,7 @@ async def test_history_during_period_bad_end_time(
{
"id": 1,
"type": "history/history_during_period",
"entity_ids": ["sensor.pet"],
"start_time": now.isoformat(),
"end_time": "dogs",
}
@ -1524,3 +1526,320 @@ async def test_overflow_queue(
assert listeners_without_writes(
hass.bus.async_listeners()
) == listeners_without_writes(init_listeners)
async def test_history_during_period_for_invalid_entity_ids(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history_during_period for valid and invalid entity ids."""
now = dt_util.utcnow()
await async_setup_component(hass, "history", {})
await async_setup_component(hass, "sensor", {})
await async_recorder_block_till_done(hass)
hass.states.async_set("sensor.one", "on", attributes={"any": "attr"})
sensor_one_last_updated = hass.states.get("sensor.one").last_updated
await async_recorder_block_till_done(hass)
hass.states.async_set("sensor.two", "off", attributes={"any": "attr"})
sensor_two_last_updated = hass.states.get("sensor.two").last_updated
await async_recorder_block_till_done(hass)
hass.states.async_set("sensor.three", "off", attributes={"any": "again"})
await async_recorder_block_till_done(hass)
await async_wait_recording_done(hass)
await async_wait_recording_done(hass)
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "history/history_during_period",
"start_time": now.isoformat(),
"entity_ids": ["sensor.one"],
"include_start_time_state": True,
"significant_changes_only": False,
"no_attributes": True,
}
)
response = await client.receive_json()
assert response["success"]
assert response == {
"result": {
"sensor.one": [
{"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"}
],
},
"id": 1,
"type": "result",
"success": True,
}
await client.send_json(
{
"id": 2,
"type": "history/history_during_period",
"start_time": now.isoformat(),
"entity_ids": ["sensor.one", "sensor.two"],
"include_start_time_state": True,
"significant_changes_only": False,
"no_attributes": True,
}
)
response = await client.receive_json()
assert response["success"]
assert response == {
"result": {
"sensor.one": [
{"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"}
],
"sensor.two": [
{"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"}
],
},
"id": 2,
"type": "result",
"success": True,
}
await client.send_json(
{
"id": 3,
"type": "history/history_during_period",
"start_time": now.isoformat(),
"entity_ids": ["sens!or.one", "two"],
"include_start_time_state": True,
"significant_changes_only": False,
"no_attributes": True,
}
)
response = await client.receive_json()
assert response["success"] is False
assert response == {
"error": {
"code": "invalid_entity_ids",
"message": "Invalid entity_ids",
},
"id": 3,
"type": "result",
"success": False,
}
await client.send_json(
{
"id": 4,
"type": "history/history_during_period",
"start_time": now.isoformat(),
"entity_ids": ["sensor.one", "sensortwo."],
"include_start_time_state": True,
"significant_changes_only": False,
"no_attributes": True,
}
)
response = await client.receive_json()
assert response["success"] is False
assert response == {
"error": {
"code": "invalid_entity_ids",
"message": "Invalid entity_ids",
},
"id": 4,
"type": "result",
"success": False,
}
await client.send_json(
{
"id": 5,
"type": "history/history_during_period",
"start_time": now.isoformat(),
"entity_ids": ["one", ".sensortwo"],
"include_start_time_state": True,
"significant_changes_only": False,
"no_attributes": True,
}
)
response = await client.receive_json()
assert response["success"] is False
assert response == {
"error": {
"code": "invalid_entity_ids",
"message": "Invalid entity_ids",
},
"id": 5,
"type": "result",
"success": False,
}
async def test_history_stream_for_invalid_entity_ids(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history stream for invalid and valid entity ids."""
now = dt_util.utcnow()
await async_setup_component(
hass,
"history",
{history.DOMAIN: {}},
)
await async_setup_component(hass, "sensor", {})
await async_recorder_block_till_done(hass)
hass.states.async_set("sensor.one", "on", attributes={"any": "attr"})
sensor_one_last_updated = hass.states.get("sensor.one").last_updated
await async_recorder_block_till_done(hass)
hass.states.async_set("sensor.two", "off", attributes={"any": "attr"})
sensor_two_last_updated = hass.states.get("sensor.two").last_updated
await async_recorder_block_till_done(hass)
hass.states.async_set("sensor.three", "off", attributes={"any": "again"})
await async_recorder_block_till_done(hass)
await async_wait_recording_done(hass)
await async_wait_recording_done(hass)
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "history/stream",
"start_time": now.isoformat(),
"entity_ids": ["sensor.one"],
"include_start_time_state": True,
"significant_changes_only": False,
"no_attributes": True,
"minimal_response": True,
}
)
response = await client.receive_json()
assert response["success"]
assert response["id"] == 1
assert response["type"] == "result"
response = await client.receive_json()
assert response == {
"event": {
"end_time": sensor_one_last_updated.timestamp(),
"start_time": now.timestamp(),
"states": {
"sensor.one": [
{"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"}
],
},
},
"id": 1,
"type": "event",
}
await client.send_json(
{
"id": 2,
"type": "history/stream",
"start_time": now.isoformat(),
"entity_ids": ["sensor.one", "sensor.two"],
"include_start_time_state": True,
"significant_changes_only": False,
"no_attributes": True,
"minimal_response": True,
}
)
response = await client.receive_json()
assert response["success"]
assert response["id"] == 2
assert response["type"] == "result"
response = await client.receive_json()
assert response == {
"event": {
"end_time": sensor_two_last_updated.timestamp(),
"start_time": now.timestamp(),
"states": {
"sensor.one": [
{"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"}
],
"sensor.two": [
{"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"}
],
},
},
"id": 2,
"type": "event",
}
await client.send_json(
{
"id": 3,
"type": "history/stream",
"start_time": now.isoformat(),
"entity_ids": ["sens!or.one", "two"],
"include_start_time_state": True,
"significant_changes_only": False,
"no_attributes": True,
"minimal_response": True,
}
)
response = await client.receive_json()
assert response["success"] is False
assert response["id"] == 3
assert response["type"] == "result"
assert response == {
"error": {
"code": "invalid_entity_ids",
"message": "Invalid entity_ids",
},
"id": 3,
"type": "result",
"success": False,
}
await client.send_json(
{
"id": 4,
"type": "history/stream",
"start_time": now.isoformat(),
"entity_ids": ["sensor.one", "sensortwo."],
"include_start_time_state": True,
"significant_changes_only": False,
"no_attributes": True,
"minimal_response": True,
}
)
response = await client.receive_json()
assert response["success"] is False
assert response["id"] == 4
assert response["type"] == "result"
assert response == {
"error": {
"code": "invalid_entity_ids",
"message": "Invalid entity_ids",
},
"id": 4,
"type": "result",
"success": False,
}
await client.send_json(
{
"id": 5,
"type": "history/stream",
"start_time": now.isoformat(),
"entity_ids": ["one", ".sensortwo"],
"include_start_time_state": True,
"significant_changes_only": False,
"no_attributes": True,
"minimal_response": True,
}
)
response = await client.receive_json()
assert response["success"] is False
assert response["id"] == 5
assert response["type"] == "result"
assert response == {
"error": {
"code": "invalid_entity_ids",
"message": "Invalid entity_ids",
},
"id": 5,
"type": "result",
"success": False,
}