278 files
This commit is contained in:
@@ -18,6 +18,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.service import async_set_service_schema
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import authz
|
||||
from .const import (
|
||||
ATTR_AS_PARENT,
|
||||
ATTR_AWARDED_BADGE_ID,
|
||||
@@ -62,7 +63,6 @@ from .const import (
|
||||
ATTR_REWARD_ID,
|
||||
ATTR_SOUND,
|
||||
BADGE_TIERS,
|
||||
COMPLETION_SOUND_OPTIONS,
|
||||
CONF_TASK_GROUP_CHORE_IDS,
|
||||
CONF_TASK_GROUP_ID,
|
||||
CONF_TASK_GROUP_NAME,
|
||||
@@ -384,6 +384,19 @@ async def _async_record_service_audit(hass: HomeAssistant, call: ServiceCall) ->
|
||||
_LOGGER.debug("Failed to record service audit for %s", call.service, exc_info=True)
|
||||
|
||||
|
||||
def _valid_sound(value: str) -> str:
|
||||
"""Validate a preview_sound name: a built-in, or an uploaded custom sound (#856).
|
||||
|
||||
Replaces a plain ``vol.In(COMPLETION_SOUND_OPTIONS)``, which could not see
|
||||
custom sounds because they are created at runtime.
|
||||
"""
|
||||
from .const import is_valid_completion_sound
|
||||
|
||||
if is_valid_completion_sound(value):
|
||||
return value
|
||||
raise vol.Invalid(f"Unknown completion sound: {value}")
|
||||
|
||||
|
||||
def _safe(handler):
|
||||
"""Surface coordinator validation errors as clean service errors.
|
||||
|
||||
@@ -433,6 +446,13 @@ async def _async_require_linked_child(hass: HomeAssistant, call: ServiceCall, co
|
||||
others = coordinator.storage.get_children() or []
|
||||
if any(getattr(c, "linked_user_id", "") == user_id for c in others):
|
||||
raise Unauthorized(context=call.context)
|
||||
# Strict mode: households where every child has their own HA login can
|
||||
# require a link, so an unlinked child profile is no longer an open door
|
||||
# for any authenticated user. Parents keep acting on any child's behalf.
|
||||
if coordinator.storage.get_require_linked_child():
|
||||
if user_id in (coordinator.storage.get_parent_user_ids() or []):
|
||||
return
|
||||
raise Unauthorized(context=call.context)
|
||||
|
||||
|
||||
async def _async_register_services(hass: HomeAssistant) -> None:
|
||||
@@ -452,6 +472,23 @@ async def _async_register_services(hass: HomeAssistant) -> None:
|
||||
# Unauthorized (not ValueError), so it is unaffected and still 401s.
|
||||
return _safe(wrapped)
|
||||
|
||||
def _audited(handler):
|
||||
"""Wrap a child-facing handler so its mutation is recorded too.
|
||||
|
||||
Only the admin and parent wrappers used to audit. Everything a child
|
||||
can drive — completing a chore, claiming a reward, allocating points —
|
||||
therefore left no trail at all, which is exactly the set of actions
|
||||
worth being able to review. The guards stay inside each handler; this
|
||||
only adds the record.
|
||||
"""
|
||||
|
||||
@wraps(handler)
|
||||
async def wrapped(call: ServiceCall) -> None:
|
||||
await handler(call)
|
||||
await _async_record_service_audit(hass, call)
|
||||
|
||||
return _safe(wrapped)
|
||||
|
||||
def _parent(handler):
|
||||
"""Like _admin, but also allows non-admin users in parent_user_ids (#661).
|
||||
|
||||
@@ -651,12 +688,20 @@ async def _async_register_services(hass: HomeAssistant) -> None:
|
||||
_LOGGER.error("No TaskMate coordinator available")
|
||||
return
|
||||
await _async_require_linked_child(hass, call, coordinator, call.data[ATTR_CHILD_ID])
|
||||
# A child may have their own chore list read out; choosing the words is
|
||||
# a parent action. Otherwise any account that can act as a child could
|
||||
# make a speaker in the house say anything, at any hour.
|
||||
message = call.data.get("message", "")
|
||||
if message and not await authz.async_context_is_parent(hass, coordinator, call.context):
|
||||
_LOGGER.warning("Ignoring caller-supplied read_aloud message from a non-parent user")
|
||||
message = ""
|
||||
try:
|
||||
await coordinator.async_read_aloud(
|
||||
child_id=call.data[ATTR_CHILD_ID],
|
||||
media_player=call.data.get("media_player", ""),
|
||||
tts_entity=call.data.get("tts_entity", ""),
|
||||
message=call.data.get("message", ""),
|
||||
message=message,
|
||||
context=call.context,
|
||||
)
|
||||
except ValueError as err:
|
||||
raise ServiceValidationError(str(err)) from err
|
||||
@@ -1075,7 +1120,7 @@ async def _async_register_services(hass: HomeAssistant) -> None:
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_COMPLETE_CHORE,
|
||||
handle_complete_chore,
|
||||
_audited(handle_complete_chore),
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CHORE_ID): cv.string,
|
||||
@@ -1089,7 +1134,7 @@ async def _async_register_services(hass: HomeAssistant) -> None:
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_COMPLETE_BONUS_SUBTASK,
|
||||
_safe(handle_complete_bonus_subtask),
|
||||
_audited(handle_complete_bonus_subtask),
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CHORE_ID): cv.string,
|
||||
@@ -1102,7 +1147,7 @@ async def _async_register_services(hass: HomeAssistant) -> None:
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_START_TIMED_TASK,
|
||||
_safe(handle_start_timed_task),
|
||||
_audited(handle_start_timed_task),
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CHORE_ID): cv.string,
|
||||
@@ -1114,7 +1159,7 @@ async def _async_register_services(hass: HomeAssistant) -> None:
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_PAUSE_TIMED_TASK,
|
||||
_safe(handle_pause_timed_task),
|
||||
_audited(handle_pause_timed_task),
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CHORE_ID): cv.string,
|
||||
@@ -1126,7 +1171,7 @@ async def _async_register_services(hass: HomeAssistant) -> None:
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_STOP_TIMED_TASK,
|
||||
_safe(handle_stop_timed_task),
|
||||
_audited(handle_stop_timed_task),
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CHORE_ID): cv.string,
|
||||
@@ -1249,7 +1294,7 @@ async def _async_register_services(hass: HomeAssistant) -> None:
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_REQUEST_SWAP,
|
||||
_safe(handle_request_swap),
|
||||
_audited(handle_request_swap),
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required("chore_id"): cv.string,
|
||||
@@ -1265,9 +1310,11 @@ async def _async_register_services(hass: HomeAssistant) -> None:
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CHILD_ID): cv.string,
|
||||
vol.Optional("media_player", default=""): cv.string,
|
||||
vol.Optional("tts_entity", default=""): cv.string,
|
||||
vol.Optional("message", default=""): cv.string,
|
||||
# Blank means "use the configured default"; anything else has to
|
||||
# be an entity in the right domain, not an arbitrary string.
|
||||
vol.Optional("media_player", default=""): vol.Any("", cv.entity_domain("media_player")),
|
||||
vol.Optional("tts_entity", default=""): vol.Any("", cv.entity_domain("tts")),
|
||||
vol.Optional("message", default=""): vol.All(cv.string, vol.Length(max=500)),
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -1275,14 +1322,14 @@ async def _async_register_services(hass: HomeAssistant) -> None:
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SPIN_ROULETTE,
|
||||
_safe(handle_spin_roulette),
|
||||
_audited(handle_spin_roulette),
|
||||
schema=vol.Schema({vol.Required(ATTR_CHILD_ID): cv.string}),
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_CHOOSE_AVATAR,
|
||||
_safe(handle_choose_avatar),
|
||||
_audited(handle_choose_avatar),
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CHILD_ID): cv.string,
|
||||
@@ -1294,7 +1341,7 @@ async def _async_register_services(hass: HomeAssistant) -> None:
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_CLAIM_REWARD,
|
||||
_safe(handle_claim_reward),
|
||||
_audited(handle_claim_reward),
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_REWARD_ID): cv.string,
|
||||
@@ -1324,7 +1371,7 @@ async def _async_register_services(hass: HomeAssistant) -> None:
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_ALLOCATE_POINTS_TO_POOL,
|
||||
_safe(handle_allocate_points_to_pool),
|
||||
_audited(handle_allocate_points_to_pool),
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CHILD_ID): cv.string,
|
||||
@@ -1366,7 +1413,7 @@ async def _async_register_services(hass: HomeAssistant) -> None:
|
||||
_safe(handle_preview_sound),
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_SOUND): vol.In(COMPLETION_SOUND_OPTIONS),
|
||||
vol.Required(ATTR_SOUND): vol.All(str, _valid_sound),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user