mirror of
https://github.com/home-assistant/core.git
synced 2026-08-21 13:50:31 +01:00
Remove Supervisor refresh tokens (#179219)
This commit is contained in:
@@ -15,7 +15,7 @@ from aiohasupervisor.models import (
|
||||
)
|
||||
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
from homeassistant.auth.models import RefreshToken, User
|
||||
from homeassistant.auth.models import User
|
||||
from homeassistant.components import frontend
|
||||
from homeassistant.components.homeassistant import async_set_stop_handler
|
||||
from homeassistant.components.onboarding import async_is_onboarded
|
||||
@@ -412,11 +412,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
translation_key="supervisor_update_pending",
|
||||
)
|
||||
|
||||
# Get or create a refresh token for the Supervisor user
|
||||
if user.refresh_tokens:
|
||||
refresh_token = list(user.refresh_tokens.values())[0]
|
||||
else:
|
||||
refresh_token = await hass.auth.async_create_refresh_token(user)
|
||||
# Supervisor authenticates through its dedicated Unix socket.
|
||||
for refresh_token in list(user.refresh_tokens.values()):
|
||||
hass.auth.async_remove_refresh_token(refresh_token)
|
||||
|
||||
# Set up coordinators — these can raise ConfigEntryNotReady.
|
||||
# Register listeners only after all refreshes succeed to avoid accumulation
|
||||
@@ -492,7 +490,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
entry.async_on_unload(hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, push_config))
|
||||
|
||||
async def update_hass_api(refresh_token: RefreshToken) -> None:
|
||||
async def update_hass_api() -> None:
|
||||
"""Update Home Assistant API data on Hass.io."""
|
||||
# hass.config.api is always set here: hassio depends on http, and the
|
||||
# http integration assigns hass.config.api during its async_setup.
|
||||
@@ -500,7 +498,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
options = HomeAssistantOptions(
|
||||
ssl=hass.config.api.use_ssl,
|
||||
port=hass.config.api.port,
|
||||
refresh_token=refresh_token.token,
|
||||
refresh_token=None,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -512,7 +510,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
# Push initial config to Supervisor and refresh issues state
|
||||
await asyncio.gather(
|
||||
update_hass_api(refresh_token),
|
||||
update_hass_api(),
|
||||
push_config(None),
|
||||
issues_coordinator.async_refresh(),
|
||||
)
|
||||
|
||||
@@ -10,7 +10,6 @@ from aiohasupervisor.models import AddonsStats, AddonState, InstalledAddonComple
|
||||
from aiohttp.test_utils import TestClient
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.hassio.const import DATA_HASSIO_SUPERVISOR_USER
|
||||
from homeassistant.components.hassio.handler import HassIO
|
||||
from homeassistant.components.http.config import _DEFAULT_CONFIG as HTTP_DEFAULT_CONFIG
|
||||
from homeassistant.components.http.const import CONF_SERVER_PORT
|
||||
@@ -71,16 +70,23 @@ async def hassio_client_supervisor(
|
||||
hass: HomeAssistant,
|
||||
aiohttp_client: ClientSessionGenerator,
|
||||
hassio_stubs: None,
|
||||
) -> TestClient:
|
||||
) -> AsyncGenerator[TestClient]:
|
||||
"""Return an authenticated HTTP client."""
|
||||
hassio_user = hass.data[DATA_HASSIO_SUPERVISOR_USER]
|
||||
assert hassio_user.refresh_tokens
|
||||
refresh_token = next(iter(hassio_user.refresh_tokens.values()))
|
||||
access_token = hass.auth.async_create_access_token(refresh_token)
|
||||
return await aiohttp_client(
|
||||
hass.http.app,
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.hassio.auth.is_supervisor_unix_socket_request",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.http.auth.is_supervisor_unix_socket_request",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.http.ban.is_supervisor_unix_socket_request",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
yield await aiohttp_client(hass.http.app)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -91,11 +97,21 @@ def hass_supervisor_ws_client(
|
||||
"""Return a websocket client authenticated as the Supervisor user."""
|
||||
|
||||
async def create_client() -> WebSocketGenerator:
|
||||
hassio_user = hass.data[DATA_HASSIO_SUPERVISOR_USER]
|
||||
assert hassio_user.refresh_tokens
|
||||
refresh_token = next(iter(hassio_user.refresh_tokens.values()))
|
||||
access_token = hass.auth.async_create_access_token(refresh_token)
|
||||
return await hass_ws_client(hass, access_token=access_token)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.http.auth.is_supervisor_unix_socket_request",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.http.ban.is_supervisor_unix_socket_request",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.websocket_api.http.is_supervisor_unix_socket_request",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
return await hass_ws_client(hass, supervisor_unix_socket=True)
|
||||
|
||||
return create_client
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from datetime import timedelta
|
||||
import os
|
||||
from pathlib import PurePath
|
||||
from typing import Any
|
||||
from unittest.mock import ANY, AsyncMock, Mock, call, patch
|
||||
from unittest.mock import AsyncMock, Mock, call, patch
|
||||
from uuid import uuid4
|
||||
|
||||
from aiohasupervisor import SupervisorBadRequestError, SupervisorError
|
||||
@@ -312,7 +312,7 @@ async def test_setup_api_push_api_data(
|
||||
assert result
|
||||
assert len(supervisor_client.mock_calls) == 17
|
||||
supervisor_client.homeassistant.set_options.assert_called_once_with(
|
||||
HomeAssistantOptions(ssl=False, port=9999, refresh_token=ANY)
|
||||
HomeAssistantOptions(ssl=False, port=9999, refresh_token=None)
|
||||
)
|
||||
|
||||
|
||||
@@ -349,7 +349,7 @@ async def test_setup_api_push_api_data_server_host(
|
||||
assert result
|
||||
assert len(supervisor_client.mock_calls) == 17
|
||||
supervisor_client.homeassistant.set_options.assert_called_once_with(
|
||||
HomeAssistantOptions(ssl=False, port=9999, refresh_token=ANY)
|
||||
HomeAssistantOptions(ssl=False, port=9999, refresh_token=None)
|
||||
)
|
||||
|
||||
|
||||
@@ -364,21 +364,14 @@ async def test_setup_api_push_api_data_default(
|
||||
assert result
|
||||
assert len(supervisor_client.mock_calls) == 17
|
||||
supervisor_client.homeassistant.set_options.assert_called_once_with(
|
||||
HomeAssistantOptions(ssl=False, port=80, refresh_token=ANY)
|
||||
)
|
||||
refresh_token = (
|
||||
supervisor_client.homeassistant.set_options.mock_calls[0].args[0].refresh_token
|
||||
HomeAssistantOptions(ssl=False, port=80, refresh_token=None)
|
||||
)
|
||||
hassio_user = hass.data[DATA_HASSIO_SUPERVISOR_USER]
|
||||
assert hassio_user.system_generated
|
||||
assert len(hassio_user.groups) == 1
|
||||
assert hassio_user.groups[0].id == GROUP_ID_ADMIN
|
||||
assert hassio_user.name == "Supervisor"
|
||||
for token in hassio_user.refresh_tokens.values():
|
||||
if token.token == refresh_token:
|
||||
break
|
||||
else:
|
||||
pytest.fail("refresh token not found")
|
||||
assert not hassio_user.refresh_tokens
|
||||
|
||||
|
||||
async def test_setup_adds_admin_group_to_user(hass: HomeAssistant) -> None:
|
||||
@@ -399,6 +392,7 @@ async def test_setup_adds_admin_group_to_user(hass: HomeAssistant) -> None:
|
||||
assert result
|
||||
|
||||
assert user.is_admin
|
||||
assert not user.refresh_tokens
|
||||
|
||||
|
||||
async def test_setup_migrate_user_name(hass: HomeAssistant) -> None:
|
||||
@@ -418,6 +412,7 @@ async def test_setup_migrate_user_name(hass: HomeAssistant) -> None:
|
||||
assert result
|
||||
|
||||
assert user.name == "Supervisor"
|
||||
assert not user.refresh_tokens
|
||||
|
||||
|
||||
async def test_setup_api_existing_hassio_user(
|
||||
@@ -425,7 +420,10 @@ async def test_setup_api_existing_hassio_user(
|
||||
) -> None:
|
||||
"""Test setup uses the user from config entry data."""
|
||||
user = await hass.auth.async_create_system_user("Hass.io test")
|
||||
token = await hass.auth.async_create_refresh_token(user)
|
||||
refresh_tokens = [
|
||||
await hass.auth.async_create_refresh_token(user) for _ in range(2)
|
||||
]
|
||||
access_token = hass.auth.async_create_access_token(refresh_tokens[0])
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={ENTRY_DATA_USER: user.id},
|
||||
@@ -440,8 +438,10 @@ async def test_setup_api_existing_hassio_user(
|
||||
assert result
|
||||
assert len(supervisor_client.mock_calls) == 17
|
||||
supervisor_client.homeassistant.set_options.assert_called_once_with(
|
||||
HomeAssistantOptions(ssl=False, port=80, refresh_token=token.token)
|
||||
HomeAssistantOptions(ssl=False, port=80, refresh_token=None)
|
||||
)
|
||||
assert not user.refresh_tokens
|
||||
assert hass.auth.async_validate_access_token(access_token) is None
|
||||
|
||||
|
||||
async def test_setup_migrates_legacy_hassio_store_to_config_entry(
|
||||
@@ -451,7 +451,7 @@ async def test_setup_migrates_legacy_hassio_store_to_config_entry(
|
||||
) -> None:
|
||||
"""Test setup migrates legacy hassio store user/options into config entry."""
|
||||
user = await hass.auth.async_create_system_user("Hass.io test")
|
||||
token = await hass.auth.async_create_refresh_token(user)
|
||||
await hass.auth.async_create_refresh_token(user)
|
||||
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data={}, options={}, unique_id=DOMAIN)
|
||||
config_entry.add_to_hass(hass)
|
||||
@@ -485,8 +485,9 @@ async def test_setup_migrates_legacy_hassio_store_to_config_entry(
|
||||
|
||||
assert len(supervisor_client.mock_calls) == 17
|
||||
supervisor_client.homeassistant.set_options.assert_called_once_with(
|
||||
HomeAssistantOptions(ssl=False, port=80, refresh_token=token.token)
|
||||
HomeAssistantOptions(ssl=False, port=80, refresh_token=None)
|
||||
)
|
||||
assert not user.refresh_tokens
|
||||
|
||||
|
||||
async def test_setup_migrates_legacy_options_over_default_entry_options(
|
||||
@@ -496,7 +497,7 @@ async def test_setup_migrates_legacy_options_over_default_entry_options(
|
||||
) -> None:
|
||||
"""Test legacy update options override default config entry options."""
|
||||
user = await hass.auth.async_create_system_user("Hass.io test")
|
||||
token = await hass.auth.async_create_refresh_token(user)
|
||||
await hass.auth.async_create_refresh_token(user)
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
@@ -533,8 +534,9 @@ async def test_setup_migrates_legacy_options_over_default_entry_options(
|
||||
assert entry.options[OPTION_CORE_BACKUP_BEFORE_UPDATE] is True
|
||||
|
||||
supervisor_client.homeassistant.set_options.assert_called_once_with(
|
||||
HomeAssistantOptions(ssl=False, port=80, refresh_token=token.token)
|
||||
HomeAssistantOptions(ssl=False, port=80, refresh_token=None)
|
||||
)
|
||||
assert not user.refresh_tokens
|
||||
|
||||
|
||||
async def test_setup_core_push_config(
|
||||
@@ -2040,6 +2042,15 @@ async def test_supervisor_issues_not_set_on_coordinator_failure(
|
||||
If a coordinator first-refresh raises ConfigEntryNotReady the issues
|
||||
listener must not be registered, preventing accumulation across retries.
|
||||
"""
|
||||
user = await hass.auth.async_create_system_user("Hass.io test")
|
||||
refresh_token = await hass.auth.async_create_refresh_token(user)
|
||||
access_token = hass.auth.async_create_access_token(refresh_token)
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={ENTRY_DATA_USER: user.id},
|
||||
unique_id=DOMAIN,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
supervisor_root_info.side_effect = SupervisorError()
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
result = await async_setup_component(hass, DOMAIN, {})
|
||||
@@ -2048,3 +2059,5 @@ async def test_supervisor_issues_not_set_on_coordinator_failure(
|
||||
entry = hass.config_entries.async_entries("hassio")[0]
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
assert DATA_KEY_SUPERVISOR_ISSUES not in hass.data
|
||||
assert not user.refresh_tokens
|
||||
assert hass.auth.async_validate_access_token(access_token) is None
|
||||
|
||||
+18
-9
@@ -959,22 +959,31 @@ def hass_ws_client(
|
||||
"""Websocket client fixture connected to websocket server."""
|
||||
|
||||
async def create_client(
|
||||
hass: HomeAssistant = hass, access_token: str | None = hass_access_token
|
||||
hass: HomeAssistant = hass,
|
||||
access_token: str | None = hass_access_token,
|
||||
supervisor_unix_socket: bool = False,
|
||||
) -> MockHAClientWebSocket:
|
||||
"""Create a websocket client."""
|
||||
"""Create a client, skipping token auth for Supervisor Unix sockets."""
|
||||
assert await async_setup_component(hass, "websocket_api", {})
|
||||
client = await aiohttp_client(hass.http.app)
|
||||
websocket = await client.ws_connect(URL)
|
||||
auth_resp = await websocket.receive_json()
|
||||
assert auth_resp["type"] == TYPE_AUTH_REQUIRED
|
||||
|
||||
if access_token is None:
|
||||
await websocket.send_json({"type": TYPE_AUTH, "access_token": "incorrect"})
|
||||
if supervisor_unix_socket:
|
||||
assert auth_resp["type"] == TYPE_AUTH_OK
|
||||
else:
|
||||
await websocket.send_json({"type": TYPE_AUTH, "access_token": access_token})
|
||||
assert auth_resp["type"] == TYPE_AUTH_REQUIRED
|
||||
|
||||
auth_ok = await websocket.receive_json()
|
||||
assert auth_ok["type"] == TYPE_AUTH_OK
|
||||
if access_token is None:
|
||||
await websocket.send_json(
|
||||
{"type": TYPE_AUTH, "access_token": "incorrect"}
|
||||
)
|
||||
else:
|
||||
await websocket.send_json(
|
||||
{"type": TYPE_AUTH, "access_token": access_token}
|
||||
)
|
||||
|
||||
auth_ok = await websocket.receive_json()
|
||||
assert auth_ok["type"] == TYPE_AUTH_OK
|
||||
|
||||
def _get_next_id() -> Generator[int]:
|
||||
i = 0
|
||||
|
||||
Reference in New Issue
Block a user