From a3b4956d74f2ada1bc14274f12a26302815d15f4 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Fri, 2 Oct 2026 16:39:09 +0100 Subject: [PATCH] Check only_supervisor against the Supervisor user provided by hassio (#184041) Co-authored-by: Claude Fable 5.1 --- .../components/websocket_api/decorators.py | 7 ++- .../websocket_api/test_decorators.py | 45 ++++++++++++++++++- tests/conftest.py | 11 ++++- 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index 557f2b287604..d5df7f88646d 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any import probatio -from homeassistant.const import HASSIO_USER_NAME +from homeassistant.components.http.const import DATA_SUPERVISOR_USER from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import Unauthorized from homeassistant.helpers.typing import VolDictType @@ -117,7 +117,10 @@ def ws_require_user( output_error("only_inactive_user", "Not allowed as active user") return None - if only_supervisor and connection.user.name != HASSIO_USER_NAME: + if only_supervisor and ( + (supervisor_user := hass.data.get(DATA_SUPERVISOR_USER)) is None + or connection.user.id != supervisor_user.id + ): output_error("only_supervisor", "Only allowed as Supervisor") return None diff --git a/tests/components/websocket_api/test_decorators.py b/tests/components/websocket_api/test_decorators.py index f343543735e5..75908cf518fd 100644 --- a/tests/components/websocket_api/test_decorators.py +++ b/tests/components/websocket_api/test_decorators.py @@ -3,10 +3,15 @@ from typing import Any import probatio +import pytest from homeassistant.components import http, websocket_api +from homeassistant.const import HASSIO_USER_NAME from homeassistant.core import HomeAssistant +from tests.common import MockUser +from tests.typing import MockHAClientWebSocket, WebSocketGenerator + async def test_async_response_request_context( hass: HomeAssistant, websocket_client @@ -156,8 +161,18 @@ async def test_async_response_request_context( ) -async def test_supervisor_only(hass: HomeAssistant, websocket_client) -> None: - """Test that only the Supervisor can make requests.""" +@pytest.mark.usefixtures("hass_supervisor_user") +async def test_supervisor_only( + hass: HomeAssistant, + websocket_client: MockHAClientWebSocket, + hass_admin_user: MockUser, +) -> None: + """Test that only the Supervisor can make requests. + + With the Supervisor user registered, an admin that is merely named like + the Supervisor user must still be rejected. + """ + await hass.auth.async_update_user(hass_admin_user, name=HASSIO_USER_NAME) @websocket_api.ws_require_user(only_supervisor=True) @websocket_api.websocket_command({"type": "test-require-supervisor-user"}) @@ -181,3 +196,29 @@ async def test_supervisor_only(hass: HomeAssistant, websocket_client) -> None: assert msg["id"] == 5 assert not msg["success"] assert msg["error"]["code"] == "only_supervisor" + + +async def test_supervisor_only_allows_supervisor( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_supervisor_access_token: str, +) -> None: + """Test that the Supervisor user can make Supervisor-only requests.""" + + @websocket_api.ws_require_user(only_supervisor=True) + @websocket_api.websocket_command({"type": "test-require-supervisor-user"}) + def require_supervisor_request( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], + ) -> None: + connection.send_result(msg["id"]) + + websocket_api.async_register_command(hass, require_supervisor_request) + + client = await hass_ws_client(hass, hass_supervisor_access_token) + await client.send_json({"id": 5, "type": "test-require-supervisor-user"}) + + msg = await client.receive_json() + assert msg["id"] == 5 + assert msg["success"] diff --git a/tests/conftest.py b/tests/conftest.py index 44a26cc2e078..5db5dacdd04d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -60,6 +60,7 @@ from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY from homeassistant.auth.models import Credentials from homeassistant.auth.providers import homeassistant from homeassistant.components.device_tracker.legacy import Device +from homeassistant.components.http.const import DATA_SUPERVISOR_USER # pylint: disable-next=home-assistant-component-root-import from homeassistant.components.websocket_api.auth import ( @@ -902,11 +903,17 @@ async def hass_read_only_access_token( async def hass_supervisor_user( hass: HomeAssistant, local_auth: homeassistant.HassAuthProvider ) -> MockUser: - """Return the Home Assistant Supervisor user.""" + """Return the Home Assistant Supervisor user. + + The user is published the same way the hassio integration does, so + commands restricted to the Supervisor accept it. + """ admin_group = await hass.auth.async_get_group(GROUP_ID_ADMIN) - return MockUser( + user = MockUser( name=HASSIO_USER_NAME, groups=[admin_group], system_generated=True ).add_to_hass(hass) + hass.data[DATA_SUPERVISOR_USER] = user + return user @pytest.fixture