Drop hass argument from verify_domain_control

This commit is contained in:
epenet
2025-07-02 10:49:25 +00:00
parent 0eaea13e8d
commit 0b18349b0e
3 changed files with 135 additions and 23 deletions

View File

@@ -138,6 +138,32 @@ def deprecated_function[**_P, _R](
return deprecated_decorator
def deprecate_hass_binding[**_P, _T](
breaks_in_ha_version: str | None = None,
) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]:
"""Decorate function to indicate that first argument hass will be ignored."""
def _decorator(func: Callable[_P, _T]) -> Callable[_P, _T]:
@functools.wraps(func)
def _inner(*args: _P.args, **kwargs: _P.kwargs) -> _T:
from homeassistant.core import HomeAssistant # noqa: PLC0415
if isinstance(args[0], HomeAssistant):
_print_deprecation_warning(
func,
"without hass",
"argument",
"called with hass as the first argument",
breaks_in_ha_version,
)
args = args[1:] # type: ignore[assignment]
return func(*args, **kwargs)
return _inner
return _decorator
def _print_deprecation_warning(
obj: Any,
replacement: str,

View File

@@ -10,7 +10,7 @@ from functools import cache, partial
import inspect
import logging
from types import ModuleType
from typing import TYPE_CHECKING, Any, TypedDict, cast, override
from typing import TYPE_CHECKING, Any, TypedDict, cast, overload, override
import voluptuous as vol
@@ -60,7 +60,7 @@ from . import (
template,
translation,
)
from .deprecation import deprecated_class, deprecated_function
from .deprecation import deprecate_hass_binding, deprecated_class, deprecated_function
from .selector import TargetSelector
from .typing import ConfigType, TemplateVarsType, VolDictType, VolSchemaType
@@ -990,10 +990,22 @@ def async_register_admin_service(
)
@bind_hass
# Overloads can be dropped when all core calls have been updated to drop hass argument
@overload
def verify_domain_control(
domain: str,
) -> Callable[[Callable[[ServiceCall], Any]], Callable[[ServiceCall], Any]]: ...
@overload
def verify_domain_control(
hass: HomeAssistant,
domain: str,
) -> Callable[[Callable[[ServiceCall], Any]], Callable[[ServiceCall], Any]]: ...
@deprecate_hass_binding(breaks_in_ha_version="2026.2") # type: ignore[misc]
@callback
def verify_domain_control(
hass: HomeAssistant, domain: str
domain: str,
) -> Callable[[Callable[[ServiceCall], Any]], Callable[[ServiceCall], Any]]:
"""Ensure permission to access any entity under domain in service call."""
@@ -1009,6 +1021,7 @@ def verify_domain_control(
if not call.context.user_id:
return await service_handler(call)
hass = call.hass
user = await hass.auth.async_get_user(call.context.user_id)
if user is None:

View File

@@ -1,7 +1,7 @@
"""Test service helpers."""
import asyncio
from collections.abc import Iterable
from collections.abc import Callable, Iterable
from copy import deepcopy
import dataclasses
import io
@@ -1753,7 +1753,27 @@ async def test_register_admin_service_return_response(
assert result == {"test-reply": "test-value1"}
async def test_domain_control_not_async(hass: HomeAssistant, mock_entities) -> None:
_DEPRECATED_VERIFY_DOMAIN_CONTROL_MESSAGE = (
"verify_domain_control is a deprecated argument which will be removed"
" in HA Core 2026.2. Use without hass instead"
)
@pytest.mark.parametrize(
# Check that with or without hass behaves the same
("decorator", "in_caplog"),
[
(service.verify_domain_control, True), # deprecated with hass
(lambda _, domain: service.verify_domain_control(domain), False),
],
)
async def test_domain_control_not_async(
hass: HomeAssistant,
mock_entities,
decorator: Callable[[HomeAssistant, str], Any],
in_caplog: bool,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test domain verification in a service call with an unknown user."""
calls = []
@@ -1762,10 +1782,26 @@ async def test_domain_control_not_async(hass: HomeAssistant, mock_entities) -> N
calls.append(call)
with pytest.raises(exceptions.HomeAssistantError):
service.verify_domain_control(hass, "test_domain")(mock_service_log)
decorator(hass, "test_domain")(mock_service_log)
assert (_DEPRECATED_VERIFY_DOMAIN_CONTROL_MESSAGE in caplog.text) == in_caplog
async def test_domain_control_unknown(hass: HomeAssistant, mock_entities) -> None:
@pytest.mark.parametrize(
# Check that with or without hass behaves the same
("decorator", "in_caplog"),
[
(service.verify_domain_control, True), # deprecated with hass
(lambda _, domain: service.verify_domain_control(domain), False),
],
)
async def test_domain_control_unknown(
hass: HomeAssistant,
mock_entities,
decorator: Callable[[HomeAssistant, str], Any],
in_caplog: bool,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test domain verification in a service call with an unknown user."""
calls = []
@@ -1777,9 +1813,7 @@ async def test_domain_control_unknown(hass: HomeAssistant, mock_entities) -> Non
"homeassistant.helpers.entity_registry.async_get",
return_value=Mock(entities=mock_entities),
):
protected_mock_service = service.verify_domain_control(hass, "test_domain")(
mock_service_log
)
protected_mock_service = decorator(hass, "test_domain")(mock_service_log)
hass.services.async_register(
"test_domain", "test_service", protected_mock_service, schema=None
@@ -1795,9 +1829,23 @@ async def test_domain_control_unknown(hass: HomeAssistant, mock_entities) -> Non
)
assert len(calls) == 0
assert (_DEPRECATED_VERIFY_DOMAIN_CONTROL_MESSAGE in caplog.text) == in_caplog
@pytest.mark.parametrize(
# Check that with or without hass behaves the same
("decorator", "in_caplog"),
[
(service.verify_domain_control, True), # deprecated with hass
(lambda _, domain: service.verify_domain_control(domain), False),
],
)
async def test_domain_control_unauthorized(
hass: HomeAssistant, hass_read_only_user: MockUser
hass: HomeAssistant,
hass_read_only_user: MockUser,
decorator: Callable[[HomeAssistant, str], Any],
in_caplog: bool,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test domain verification in a service call with an unauthorized user."""
mock_registry(
@@ -1817,9 +1865,7 @@ async def test_domain_control_unauthorized(
"""Define a protected service."""
calls.append(call)
protected_mock_service = service.verify_domain_control(hass, "test_domain")(
mock_service_log
)
protected_mock_service = decorator(hass, "test_domain")(mock_service_log)
hass.services.async_register(
"test_domain", "test_service", protected_mock_service, schema=None
@@ -1836,9 +1882,23 @@ async def test_domain_control_unauthorized(
assert len(calls) == 0
assert (_DEPRECATED_VERIFY_DOMAIN_CONTROL_MESSAGE in caplog.text) == in_caplog
@pytest.mark.parametrize(
# Check that with or without hass behaves the same
("decorator", "in_caplog"),
[
(service.verify_domain_control, True), # deprecated with hass
(lambda _, domain: service.verify_domain_control(domain), False),
],
)
async def test_domain_control_admin(
hass: HomeAssistant, hass_admin_user: MockUser
hass: HomeAssistant,
hass_admin_user: MockUser,
decorator: Callable[[HomeAssistant, str], Any],
in_caplog: bool,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test domain verification in a service call with an admin user."""
mock_registry(
@@ -1858,9 +1918,7 @@ async def test_domain_control_admin(
"""Define a protected service."""
calls.append(call)
protected_mock_service = service.verify_domain_control(hass, "test_domain")(
mock_service_log
)
protected_mock_service = decorator(hass, "test_domain")(mock_service_log)
hass.services.async_register(
"test_domain", "test_service", protected_mock_service, schema=None
@@ -1876,8 +1934,23 @@ async def test_domain_control_admin(
assert len(calls) == 1
assert (_DEPRECATED_VERIFY_DOMAIN_CONTROL_MESSAGE in caplog.text) == in_caplog
async def test_domain_control_no_user(hass: HomeAssistant) -> None:
@pytest.mark.parametrize(
# Check that with or without hass behaves the same
("decorator", "in_caplog"),
[
(service.verify_domain_control, True), # deprecated with hass
(lambda _, domain: service.verify_domain_control(domain), False),
],
)
async def test_domain_control_no_user(
hass: HomeAssistant,
decorator: Callable[[HomeAssistant, str], Any],
in_caplog: bool,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test domain verification in a service call with no user."""
mock_registry(
hass,
@@ -1896,9 +1969,7 @@ async def test_domain_control_no_user(hass: HomeAssistant) -> None:
"""Define a protected service."""
calls.append(call)
protected_mock_service = service.verify_domain_control(hass, "test_domain")(
mock_service_log
)
protected_mock_service = decorator(hass, "test_domain")(mock_service_log)
hass.services.async_register(
"test_domain", "test_service", protected_mock_service, schema=None
@@ -1914,6 +1985,8 @@ async def test_domain_control_no_user(hass: HomeAssistant) -> None:
assert len(calls) == 1
assert (_DEPRECATED_VERIFY_DOMAIN_CONTROL_MESSAGE in caplog.text) == in_caplog
async def test_extract_from_service_available_device(hass: HomeAssistant) -> None:
"""Test the extraction of entity from service and device is available."""