From 0527dcd49592f094d45eb43880571066724cd7fa Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Sat, 19 Sep 2026 11:52:42 +0200 Subject: [PATCH] Reuse an existing Supervisor system user instead of creating a duplicate (#182304) Co-authored-by: Claude Fable 5.1 --- homeassistant/components/hassio/__init__.py | 23 +++++++-- tests/components/hassio/test_init.py | 56 +++++++++++++++++++++ 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 59c14de4460e..5285a5f2f411 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -186,15 +186,28 @@ async def _async_get_or_create_supervisor_user( if user is None and legacy_user_id is not None: user = await hass.auth.async_get_user(legacy_user_id) + if user is None: + # The storage naming the Supervisor user may have been lost. Reuse an + # existing Supervisor system user instead of creating a duplicate. + user = next( + ( + existing + for existing in await hass.auth.async_get_users() + if existing.system_generated and existing.name == HASSIO_USER_NAME + ), + None, + ) + if user is None: user = await hass.auth.async_create_system_user( HASSIO_USER_NAME, group_ids=[GROUP_ID_ADMIN] ) - if entry is not None: - hass.config_entries.async_update_entry( - entry, - data={**entry.data, ENTRY_DATA_USER: user.id}, - ) + + if entry is not None and entry.data.get(ENTRY_DATA_USER) != user.id: + hass.config_entries.async_update_entry( + entry, + data={**entry.data, ENTRY_DATA_USER: user.id}, + ) # Migrate old Hass.io users to be admin. if not user.is_admin: diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 933df318454c..7c57ef891cd8 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -33,6 +33,7 @@ from probatio import Invalid import pytest from homeassistant.auth.const import GROUP_ID_ADMIN +from homeassistant.auth.models import User from homeassistant.components import frontend, hassio from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.hassio import ( @@ -69,6 +70,7 @@ from homeassistant.components.homeassistant import ( ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import HASSIO_USER_NAME from homeassistant.core import CoreState, HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr, issue_registry as ir @@ -444,6 +446,60 @@ async def test_setup_api_existing_hassio_user( assert hass.auth.async_validate_access_token(access_token) is None +async def _async_assert_supervisor_user_adopted( + hass: HomeAssistant, user: User, human: User +) -> None: + """Assert setup adopted the existing Supervisor system user.""" + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data[ENTRY_DATA_USER] == user.id + assert entry.data[ENTRY_DATA_USER] != human.id + assert [ + existing + for existing in await hass.auth.async_get_users() + if existing.system_generated and existing.name == HASSIO_USER_NAME + ] == [user] + + +async def test_setup_adopts_existing_supervisor_user(hass: HomeAssistant) -> None: + """Test setup reuses an existing Supervisor system user. + + When the config entry data (and legacy store) naming the Supervisor user + is lost, an existing Supervisor system user must be adopted instead of + creating a duplicate. A regular user named alike must not be picked up. + """ + human = await hass.auth.async_create_user(HASSIO_USER_NAME) + user = await hass.auth.async_create_system_user( + HASSIO_USER_NAME, group_ids=[GROUP_ID_ADMIN] + ) + MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN).add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + assert await async_setup_component(hass, DOMAIN, {"hassio": {}}) + await hass.async_block_till_done() + + await _async_assert_supervisor_user_adopted(hass, user, human) + + +async def test_setup_adopts_existing_supervisor_user_without_entry( + hass: HomeAssistant, +) -> None: + """Test setup reuses an existing Supervisor system user without config entry. + + Same as above, but with the config entry itself gone: the entry created by + the system flow must name the adopted user. + """ + human = await hass.auth.async_create_user(HASSIO_USER_NAME) + user = await hass.auth.async_create_system_user( + HASSIO_USER_NAME, group_ids=[GROUP_ID_ADMIN] + ) + + with patch.dict(os.environ, MOCK_ENVIRON): + assert await async_setup_component(hass, DOMAIN, {"hassio": {}}) + await hass.async_block_till_done() + + await _async_assert_supervisor_user_adopted(hass, user, human) + + async def test_setup_migrates_legacy_hassio_store_to_config_entry( hass: HomeAssistant, hass_storage: dict[str, Any],