Remove strict connection (#117933)
This commit is contained in:
parent
6f81852eb4
commit
cb62f4242e
32 changed files with 39 additions and 1816 deletions
|
@ -24,7 +24,6 @@ from homeassistant.components.homeassistant.exposed_entities import (
|
|||
ExposedEntities,
|
||||
async_expose_entity,
|
||||
)
|
||||
from homeassistant.components.http.const import StrictConnectionMode
|
||||
from homeassistant.const import CONTENT_TYPE_JSON, __version__ as HA_VERSION
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
@ -388,7 +387,6 @@ async def test_cloud_connection_info(hass: HomeAssistant) -> None:
|
|||
"connected": False,
|
||||
"enabled": False,
|
||||
"instance_domain": None,
|
||||
"strict_connection": StrictConnectionMode.DISABLED,
|
||||
},
|
||||
"version": HA_VERSION,
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ from homeassistant.components.assist_pipeline.pipeline import STORAGE_KEY
|
|||
from homeassistant.components.cloud.const import DEFAULT_EXPOSED_DOMAINS, DOMAIN
|
||||
from homeassistant.components.google_assistant.helpers import GoogleEntity
|
||||
from homeassistant.components.homeassistant import exposed_entities
|
||||
from homeassistant.components.http.const import StrictConnectionMode
|
||||
from homeassistant.components.websocket_api import ERR_INVALID_FORMAT
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
@ -783,7 +782,6 @@ async def test_websocket_status(
|
|||
"google_report_state": True,
|
||||
"remote_allow_remote_enable": True,
|
||||
"remote_enabled": False,
|
||||
"strict_connection": "disabled",
|
||||
"tts_default_voice": ["en-US", "JennyNeural"],
|
||||
},
|
||||
"alexa_entities": {
|
||||
|
@ -903,7 +901,6 @@ async def test_websocket_update_preferences(
|
|||
assert cloud.client.prefs.alexa_enabled
|
||||
assert cloud.client.prefs.google_secure_devices_pin is None
|
||||
assert cloud.client.prefs.remote_allow_remote_enable is True
|
||||
assert cloud.client.prefs.strict_connection is StrictConnectionMode.DISABLED
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
|
@ -915,7 +912,6 @@ async def test_websocket_update_preferences(
|
|||
"google_secure_devices_pin": "1234",
|
||||
"tts_default_voice": ["en-GB", "RyanNeural"],
|
||||
"remote_allow_remote_enable": False,
|
||||
"strict_connection": StrictConnectionMode.DROP_CONNECTION,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
|
@ -926,7 +922,6 @@ async def test_websocket_update_preferences(
|
|||
assert cloud.client.prefs.google_secure_devices_pin == "1234"
|
||||
assert cloud.client.prefs.remote_allow_remote_enable is False
|
||||
assert cloud.client.prefs.tts_default_voice == ("en-GB", "RyanNeural")
|
||||
assert cloud.client.prefs.strict_connection is StrictConnectionMode.DROP_CONNECTION
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
from collections.abc import Callable, Coroutine
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from hass_nabucasa import Cloud
|
||||
import pytest
|
||||
|
@ -14,16 +13,11 @@ from homeassistant.components.cloud import (
|
|||
CloudNotConnected,
|
||||
async_get_or_create_cloudhook,
|
||||
)
|
||||
from homeassistant.components.cloud.const import (
|
||||
DOMAIN,
|
||||
PREF_CLOUDHOOKS,
|
||||
PREF_STRICT_CONNECTION,
|
||||
)
|
||||
from homeassistant.components.cloud.const import DOMAIN, PREF_CLOUDHOOKS
|
||||
from homeassistant.components.cloud.prefs import STORAGE_KEY
|
||||
from homeassistant.components.http.const import StrictConnectionMode
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError, Unauthorized
|
||||
from homeassistant.exceptions import Unauthorized
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry, MockUser
|
||||
|
@ -301,77 +295,3 @@ async def test_cloud_logout(
|
|||
await hass.async_block_till_done()
|
||||
|
||||
assert cloud.is_logged_in is False
|
||||
|
||||
|
||||
async def test_service_create_temporary_strict_connection_url_strict_connection_disabled(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test service create_temporary_strict_connection_url with strict_connection not enabled."""
|
||||
mock_config_entry = MockConfigEntry(domain=DOMAIN)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
assert await async_setup_component(hass, DOMAIN, {"cloud": {}})
|
||||
await hass.async_block_till_done()
|
||||
with pytest.raises(
|
||||
ServiceValidationError,
|
||||
match="Strict connection is not enabled for cloud requests",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
cloud.DOMAIN,
|
||||
"create_temporary_strict_connection_url",
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mode"),
|
||||
[
|
||||
StrictConnectionMode.DROP_CONNECTION,
|
||||
StrictConnectionMode.GUARD_PAGE,
|
||||
],
|
||||
)
|
||||
async def test_service_create_temporary_strict_connection(
|
||||
hass: HomeAssistant,
|
||||
set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]],
|
||||
mode: StrictConnectionMode,
|
||||
) -> None:
|
||||
"""Test service create_temporary_strict_connection_url."""
|
||||
mock_config_entry = MockConfigEntry(domain=DOMAIN)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
assert await async_setup_component(hass, DOMAIN, {"cloud": {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await set_cloud_prefs(
|
||||
{
|
||||
PREF_STRICT_CONNECTION: mode,
|
||||
}
|
||||
)
|
||||
|
||||
# No cloud url set
|
||||
with pytest.raises(ServiceValidationError, match="No cloud URL available"):
|
||||
await hass.services.async_call(
|
||||
cloud.DOMAIN,
|
||||
"create_temporary_strict_connection_url",
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
# Patch cloud url
|
||||
url = "https://example.com"
|
||||
with patch(
|
||||
"homeassistant.helpers.network._get_cloud_url",
|
||||
return_value=url,
|
||||
):
|
||||
response = await hass.services.async_call(
|
||||
cloud.DOMAIN,
|
||||
"create_temporary_strict_connection_url",
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert isinstance(response, dict)
|
||||
direct_url_prefix = f"{url}/auth/strict_connection/temp_token?authSig="
|
||||
assert response.pop("direct_url").startswith(direct_url_prefix)
|
||||
assert response.pop("url").startswith(
|
||||
f"https://login.home-assistant.io?u={quote_plus(direct_url_prefix)}"
|
||||
)
|
||||
assert response == {} # No more keys in response
|
||||
|
|
|
@ -6,13 +6,8 @@ from unittest.mock import ANY, MagicMock, patch
|
|||
import pytest
|
||||
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
from homeassistant.components.cloud.const import (
|
||||
DOMAIN,
|
||||
PREF_STRICT_CONNECTION,
|
||||
PREF_TTS_DEFAULT_VOICE,
|
||||
)
|
||||
from homeassistant.components.cloud.const import DOMAIN, PREF_TTS_DEFAULT_VOICE
|
||||
from homeassistant.components.cloud.prefs import STORAGE_KEY, CloudPreferences
|
||||
from homeassistant.components.http.const import StrictConnectionMode
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
|
@ -179,39 +174,3 @@ async def test_tts_default_voice_legacy_gender(
|
|||
await hass.async_block_till_done()
|
||||
|
||||
assert cloud.client.prefs.tts_default_voice == (expected_language, voice)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", list(StrictConnectionMode))
|
||||
async def test_strict_connection_convertion(
|
||||
hass: HomeAssistant,
|
||||
cloud: MagicMock,
|
||||
hass_storage: dict[str, Any],
|
||||
mode: StrictConnectionMode,
|
||||
) -> None:
|
||||
"""Test strict connection string value will be converted to the enum."""
|
||||
hass_storage[STORAGE_KEY] = {
|
||||
"version": 1,
|
||||
"data": {PREF_STRICT_CONNECTION: mode.value},
|
||||
}
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert cloud.client.prefs.strict_connection is mode
|
||||
|
||||
|
||||
@pytest.mark.parametrize("storage_data", [{}, {PREF_STRICT_CONNECTION: None}])
|
||||
async def test_strict_connection_default(
|
||||
hass: HomeAssistant,
|
||||
cloud: MagicMock,
|
||||
hass_storage: dict[str, Any],
|
||||
storage_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test strict connection default values."""
|
||||
hass_storage[STORAGE_KEY] = {
|
||||
"version": 1,
|
||||
"data": storage_data,
|
||||
}
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert cloud.client.prefs.strict_connection is StrictConnectionMode.DISABLED
|
||||
|
|
|
@ -1,294 +0,0 @@
|
|||
"""Test strict connection mode for cloud."""
|
||||
|
||||
from collections.abc import Awaitable, Callable, Coroutine, Generator
|
||||
from contextlib import contextmanager
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
from aiohttp import ServerDisconnectedError, web
|
||||
from aiohttp.test_utils import TestClient
|
||||
from aiohttp_session import get_session
|
||||
import pytest
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.auth.models import RefreshToken
|
||||
from homeassistant.auth.session import SESSION_ID, TEMP_TIMEOUT
|
||||
from homeassistant.components.cloud.const import PREF_STRICT_CONNECTION
|
||||
from homeassistant.components.http import KEY_HASS
|
||||
from homeassistant.components.http.auth import (
|
||||
STRICT_CONNECTION_GUARD_PAGE,
|
||||
async_setup_auth,
|
||||
async_sign_path,
|
||||
)
|
||||
from homeassistant.components.http.const import KEY_AUTHENTICATED, StrictConnectionMode
|
||||
from homeassistant.components.http.session import COOKIE_NAME, PREFIXED_COOKIE_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.network import is_cloud_connection
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def refresh_token(hass: HomeAssistant, hass_access_token: str) -> RefreshToken:
|
||||
"""Return a refresh token."""
|
||||
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
|
||||
assert refresh_token
|
||||
session = hass.auth.session
|
||||
assert session._strict_connection_sessions == {}
|
||||
assert session._temp_sessions == {}
|
||||
return refresh_token
|
||||
|
||||
|
||||
@contextmanager
|
||||
def simulate_cloud_request() -> Generator[None, None, None]:
|
||||
"""Simulate a cloud request."""
|
||||
with patch(
|
||||
"hass_nabucasa.remote.is_cloud_request", Mock(get=Mock(return_value=True))
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app_strict_connection(
|
||||
hass: HomeAssistant, refresh_token: RefreshToken
|
||||
) -> web.Application:
|
||||
"""Fixture to set up a web.Application."""
|
||||
|
||||
async def handler(request):
|
||||
"""Return if request was authenticated."""
|
||||
return web.json_response(data={"authenticated": request[KEY_AUTHENTICATED]})
|
||||
|
||||
app = web.Application()
|
||||
app[KEY_HASS] = hass
|
||||
app.router.add_get("/", handler)
|
||||
|
||||
async def set_cookie(request: web.Request) -> web.Response:
|
||||
hass = request.app[KEY_HASS]
|
||||
# Clear all sessions
|
||||
hass.auth.session._temp_sessions.clear()
|
||||
hass.auth.session._strict_connection_sessions.clear()
|
||||
|
||||
if request.query["token"] == "refresh":
|
||||
await hass.auth.session.async_create_session(request, refresh_token)
|
||||
else:
|
||||
await hass.auth.session.async_create_temp_unauthorized_session(request)
|
||||
session = await get_session(request)
|
||||
return web.Response(text=session[SESSION_ID])
|
||||
|
||||
app.router.add_get("/test/cookie", set_cookie)
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture(name="client")
|
||||
async def set_up_fixture(
|
||||
hass: HomeAssistant,
|
||||
aiohttp_client: ClientSessionGenerator,
|
||||
app_strict_connection: web.Application,
|
||||
cloud: MagicMock,
|
||||
socket_enabled: None,
|
||||
) -> TestClient:
|
||||
"""Set up the fixture."""
|
||||
|
||||
await async_setup_auth(hass, app_strict_connection, StrictConnectionMode.DISABLED)
|
||||
assert await async_setup_component(hass, "cloud", {"cloud": {}})
|
||||
await hass.async_block_till_done()
|
||||
return await aiohttp_client(app_strict_connection)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"strict_connection_mode", [e.value for e in StrictConnectionMode]
|
||||
)
|
||||
async def test_strict_connection_cloud_authenticated_requests(
|
||||
hass: HomeAssistant,
|
||||
client: TestClient,
|
||||
hass_access_token: str,
|
||||
set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]],
|
||||
refresh_token: RefreshToken,
|
||||
strict_connection_mode: StrictConnectionMode,
|
||||
) -> None:
|
||||
"""Test authenticated requests with strict connection."""
|
||||
assert hass.auth.session._strict_connection_sessions == {}
|
||||
|
||||
signed_path = async_sign_path(
|
||||
hass, "/", timedelta(seconds=5), refresh_token_id=refresh_token.id
|
||||
)
|
||||
|
||||
await set_cloud_prefs(
|
||||
{
|
||||
PREF_STRICT_CONNECTION: strict_connection_mode,
|
||||
}
|
||||
)
|
||||
|
||||
with simulate_cloud_request():
|
||||
assert is_cloud_connection(hass)
|
||||
req = await client.get(
|
||||
"/", headers={"Authorization": f"Bearer {hass_access_token}"}
|
||||
)
|
||||
assert req.status == HTTPStatus.OK
|
||||
assert await req.json() == {"authenticated": True}
|
||||
req = await client.get(signed_path)
|
||||
assert req.status == HTTPStatus.OK
|
||||
assert await req.json() == {"authenticated": True}
|
||||
|
||||
|
||||
async def _test_strict_connection_cloud_enabled_external_unauthenticated_requests(
|
||||
hass: HomeAssistant,
|
||||
client: TestClient,
|
||||
perform_unauthenticated_request: Callable[
|
||||
[HomeAssistant, TestClient], Awaitable[None]
|
||||
],
|
||||
_: RefreshToken,
|
||||
) -> None:
|
||||
"""Test external unauthenticated requests with strict connection cloud enabled."""
|
||||
with simulate_cloud_request():
|
||||
assert is_cloud_connection(hass)
|
||||
await perform_unauthenticated_request(hass, client)
|
||||
|
||||
|
||||
async def _test_strict_connection_cloud_enabled_external_unauthenticated_requests_refresh_token(
|
||||
hass: HomeAssistant,
|
||||
client: TestClient,
|
||||
perform_unauthenticated_request: Callable[
|
||||
[HomeAssistant, TestClient], Awaitable[None]
|
||||
],
|
||||
refresh_token: RefreshToken,
|
||||
) -> None:
|
||||
"""Test external unauthenticated requests with strict connection cloud enabled and refresh token cookie."""
|
||||
session = hass.auth.session
|
||||
|
||||
# set strict connection cookie with refresh token
|
||||
session_id = await _modify_cookie_for_cloud(client, "refresh")
|
||||
assert session._strict_connection_sessions == {session_id: refresh_token.id}
|
||||
with simulate_cloud_request():
|
||||
assert is_cloud_connection(hass)
|
||||
req = await client.get("/")
|
||||
assert req.status == HTTPStatus.OK
|
||||
assert await req.json() == {"authenticated": False}
|
||||
|
||||
# Invalidate refresh token, which should also invalidate session
|
||||
hass.auth.async_remove_refresh_token(refresh_token)
|
||||
assert session._strict_connection_sessions == {}
|
||||
|
||||
await perform_unauthenticated_request(hass, client)
|
||||
|
||||
|
||||
async def _test_strict_connection_cloud_enabled_external_unauthenticated_requests_temp_session(
|
||||
hass: HomeAssistant,
|
||||
client: TestClient,
|
||||
perform_unauthenticated_request: Callable[
|
||||
[HomeAssistant, TestClient], Awaitable[None]
|
||||
],
|
||||
_: RefreshToken,
|
||||
) -> None:
|
||||
"""Test external unauthenticated requests with strict connection cloud enabled and temp cookie."""
|
||||
session = hass.auth.session
|
||||
|
||||
# set strict connection cookie with temp session
|
||||
assert session._temp_sessions == {}
|
||||
session_id = await _modify_cookie_for_cloud(client, "temp")
|
||||
assert session_id in session._temp_sessions
|
||||
with simulate_cloud_request():
|
||||
assert is_cloud_connection(hass)
|
||||
resp = await client.get("/")
|
||||
assert resp.status == HTTPStatus.OK
|
||||
assert await resp.json() == {"authenticated": False}
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + TEMP_TIMEOUT + timedelta(minutes=1))
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert session._temp_sessions == {}
|
||||
|
||||
await perform_unauthenticated_request(hass, client)
|
||||
|
||||
|
||||
async def _drop_connection_unauthorized_request(
|
||||
_: HomeAssistant, client: TestClient
|
||||
) -> None:
|
||||
with pytest.raises(ServerDisconnectedError):
|
||||
# unauthorized requests should raise ServerDisconnectedError
|
||||
await client.get("/")
|
||||
|
||||
|
||||
async def _guard_page_unauthorized_request(
|
||||
hass: HomeAssistant, client: TestClient
|
||||
) -> None:
|
||||
req = await client.get("/")
|
||||
assert req.status == HTTPStatus.IM_A_TEAPOT
|
||||
|
||||
def read_guard_page() -> str:
|
||||
with open(STRICT_CONNECTION_GUARD_PAGE, encoding="utf-8") as file:
|
||||
return file.read()
|
||||
|
||||
assert await req.text() == await hass.async_add_executor_job(read_guard_page)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_func",
|
||||
[
|
||||
_test_strict_connection_cloud_enabled_external_unauthenticated_requests,
|
||||
_test_strict_connection_cloud_enabled_external_unauthenticated_requests_refresh_token,
|
||||
_test_strict_connection_cloud_enabled_external_unauthenticated_requests_temp_session,
|
||||
],
|
||||
ids=[
|
||||
"no cookie",
|
||||
"refresh token cookie",
|
||||
"temp session cookie",
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("strict_connection_mode", "request_func"),
|
||||
[
|
||||
(StrictConnectionMode.DROP_CONNECTION, _drop_connection_unauthorized_request),
|
||||
(StrictConnectionMode.GUARD_PAGE, _guard_page_unauthorized_request),
|
||||
],
|
||||
ids=["drop connection", "static page"],
|
||||
)
|
||||
async def test_strict_connection_cloud_external_unauthenticated_requests(
|
||||
hass: HomeAssistant,
|
||||
client: TestClient,
|
||||
refresh_token: RefreshToken,
|
||||
set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]],
|
||||
test_func: Callable[
|
||||
[
|
||||
HomeAssistant,
|
||||
TestClient,
|
||||
Callable[[HomeAssistant, TestClient], Awaitable[None]],
|
||||
RefreshToken,
|
||||
],
|
||||
Awaitable[None],
|
||||
],
|
||||
strict_connection_mode: StrictConnectionMode,
|
||||
request_func: Callable[[HomeAssistant, TestClient], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Test external unauthenticated requests with strict connection cloud."""
|
||||
await set_cloud_prefs(
|
||||
{
|
||||
PREF_STRICT_CONNECTION: strict_connection_mode,
|
||||
}
|
||||
)
|
||||
|
||||
await test_func(
|
||||
hass,
|
||||
client,
|
||||
request_func,
|
||||
refresh_token,
|
||||
)
|
||||
|
||||
|
||||
async def _modify_cookie_for_cloud(client: TestClient, token_type: str) -> str:
|
||||
"""Modify cookie for cloud."""
|
||||
# Cloud cookie has set secure=true and will not set on insecure connection
|
||||
# As we test with insecure connection, we need to set it manually
|
||||
# We get the session via http and modify the cookie name to the secure one
|
||||
session_id = await (await client.get(f"/test/cookie?token={token_type}")).text()
|
||||
cookie_jar = client.session.cookie_jar
|
||||
localhost = URL("http://127.0.0.1")
|
||||
cookie = cookie_jar.filter_cookies(localhost)[COOKIE_NAME].value
|
||||
assert cookie
|
||||
cookie_jar.clear()
|
||||
cookie_jar.update_cookies({PREFIXED_COOKIE_NAME: cookie}, localhost)
|
||||
return session_id
|
Loading…
Add table
Add a link
Reference in a new issue