Remove strict connection (#117933)

This commit is contained in:
Robert Resch 2024-05-24 15:50:22 +02:00 committed by GitHub
parent 6f81852eb4
commit cb62f4242e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 39 additions and 1816 deletions

View file

@ -1,28 +1,23 @@
"""The tests for the Home Assistant HTTP component."""
from collections.abc import Awaitable, Callable
from datetime import timedelta
from http import HTTPStatus
from ipaddress import ip_network
import logging
from unittest.mock import Mock, patch
from aiohttp import BasicAuth, ServerDisconnectedError, web
from aiohttp.test_utils import TestClient
from aiohttp import BasicAuth, web
from aiohttp.web_exceptions import HTTPUnauthorized
from aiohttp_session import get_session
import jwt
import pytest
import yarl
from yarl import URL
from homeassistant.auth.const import GROUP_ID_READ_ONLY
from homeassistant.auth.models import RefreshToken, User
from homeassistant.auth.models import User
from homeassistant.auth.providers import trusted_networks
from homeassistant.auth.providers.legacy_api_password import (
LegacyApiPasswordAuthProvider,
)
from homeassistant.auth.session import SESSION_ID, TEMP_TIMEOUT
from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_HASS
from homeassistant.components.http.auth import (
@ -30,12 +25,11 @@ from homeassistant.components.http.auth import (
DATA_SIGN_SECRET,
SIGN_QUERY_PARAM,
STORAGE_KEY,
STRICT_CONNECTION_GUARD_PAGE,
async_setup_auth,
async_sign_path,
async_user_not_allowed_do_auth,
)
from homeassistant.components.http.const import KEY_AUTHENTICATED, StrictConnectionMode
from homeassistant.components.http.const import KEY_AUTHENTICATED
from homeassistant.components.http.forwarded import async_setup_forwarded
from homeassistant.components.http.request_context import (
current_request,
@ -43,11 +37,10 @@ from homeassistant.components.http.request_context import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
from . import HTTP_HEADER_HA_AUTH
from tests.common import MockUser, async_fire_time_changed
from tests.common import MockUser
from tests.test_util import mock_real_ip
from tests.typing import ClientSessionGenerator, WebSocketGenerator
@ -137,7 +130,7 @@ async def test_cant_access_with_password_in_header(
hass: HomeAssistant,
) -> None:
"""Test access with password in header."""
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
await async_setup_auth(hass, app)
client = await aiohttp_client(app)
req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
@ -154,7 +147,7 @@ async def test_cant_access_with_password_in_query(
hass: HomeAssistant,
) -> None:
"""Test access with password in URL."""
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
await async_setup_auth(hass, app)
client = await aiohttp_client(app)
resp = await client.get("/", params={"api_password": API_PASSWORD})
@ -174,7 +167,7 @@ async def test_basic_auth_does_not_work(
legacy_auth: LegacyApiPasswordAuthProvider,
) -> None:
"""Test access with basic authentication."""
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
await async_setup_auth(hass, app)
client = await aiohttp_client(app)
req = await client.get("/", auth=BasicAuth("homeassistant", API_PASSWORD))
@ -198,7 +191,7 @@ async def test_cannot_access_with_trusted_ip(
hass_owner_user: MockUser,
) -> None:
"""Test access with an untrusted ip address."""
await async_setup_auth(hass, app2, StrictConnectionMode.DISABLED)
await async_setup_auth(hass, app2)
set_mock_ip = mock_real_ip(app2)
client = await aiohttp_client(app2)
@ -226,7 +219,7 @@ async def test_auth_active_access_with_access_token_in_header(
) -> None:
"""Test access with access token in header."""
token = hass_access_token
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
await async_setup_auth(hass, app)
client = await aiohttp_client(app)
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
@ -262,7 +255,7 @@ async def test_auth_active_access_with_trusted_ip(
hass_owner_user: MockUser,
) -> None:
"""Test access with an untrusted ip address."""
await async_setup_auth(hass, app2, StrictConnectionMode.DISABLED)
await async_setup_auth(hass, app2)
set_mock_ip = mock_real_ip(app2)
client = await aiohttp_client(app2)
@ -289,7 +282,7 @@ async def test_auth_legacy_support_api_password_cannot_access(
hass: HomeAssistant,
) -> None:
"""Test access using api_password if auth.support_legacy."""
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
await async_setup_auth(hass, app)
client = await aiohttp_client(app)
req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
@ -311,7 +304,7 @@ async def test_auth_access_signed_path_with_refresh_token(
"""Test access with signed url."""
app.router.add_post("/", mock_handler)
app.router.add_get("/another_path", mock_handler)
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
await async_setup_auth(hass, app)
client = await aiohttp_client(app)
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
@ -356,7 +349,7 @@ async def test_auth_access_signed_path_with_query_param(
"""Test access with signed url and query params."""
app.router.add_post("/", mock_handler)
app.router.add_get("/another_path", mock_handler)
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
await async_setup_auth(hass, app)
client = await aiohttp_client(app)
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
@ -386,7 +379,7 @@ async def test_auth_access_signed_path_with_query_param_order(
"""Test access with signed url and query params different order."""
app.router.add_post("/", mock_handler)
app.router.add_get("/another_path", mock_handler)
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
await async_setup_auth(hass, app)
client = await aiohttp_client(app)
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
@ -427,7 +420,7 @@ async def test_auth_access_signed_path_with_query_param_safe_param(
"""Test access with signed url and changing a safe param."""
app.router.add_post("/", mock_handler)
app.router.add_get("/another_path", mock_handler)
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
await async_setup_auth(hass, app)
client = await aiohttp_client(app)
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
@ -466,7 +459,7 @@ async def test_auth_access_signed_path_with_query_param_tamper(
"""Test access with signed url and query params that have been tampered with."""
app.router.add_post("/", mock_handler)
app.router.add_get("/another_path", mock_handler)
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
await async_setup_auth(hass, app)
client = await aiohttp_client(app)
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
@ -535,7 +528,7 @@ async def test_auth_access_signed_path_with_http(
)
app.router.add_get("/hello", mock_handler)
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
await async_setup_auth(hass, app)
client = await aiohttp_client(app)
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
@ -559,7 +552,7 @@ async def test_auth_access_signed_path_with_content_user(
hass: HomeAssistant, app, aiohttp_client: ClientSessionGenerator
) -> None:
"""Test access signed url uses content user."""
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
await async_setup_auth(hass, app)
signed_path = async_sign_path(hass, "/", timedelta(seconds=5))
signature = yarl.URL(signed_path).query["authSig"]
claims = jwt.decode(
@ -579,7 +572,7 @@ async def test_local_only_user_rejected(
) -> None:
"""Test access with access token in header."""
token = hass_access_token
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
await async_setup_auth(hass, app)
set_mock_ip = mock_real_ip(app)
client = await aiohttp_client(app)
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
@ -645,7 +638,7 @@ async def test_create_user_once(hass: HomeAssistant) -> None:
"""Test that we reuse the user."""
cur_users = len(await hass.auth.async_get_users())
app = web.Application()
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
await async_setup_auth(hass, app)
users = await hass.auth.async_get_users()
assert len(users) == cur_users + 1
@ -657,287 +650,7 @@ async def test_create_user_once(hass: HomeAssistant) -> None:
assert len(user.refresh_tokens) == 1
assert user.system_generated
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
await async_setup_auth(hass, app)
# test it did not create a user
assert len(await hass.auth.async_get_users()) == cur_users + 1
@pytest.fixture
def app_strict_connection(hass):
"""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_setup_forwarded(app, True, [])
return app
@pytest.mark.parametrize(
"strict_connection_mode", [e.value for e in StrictConnectionMode]
)
async def test_strict_connection_non_cloud_authenticated_requests(
hass: HomeAssistant,
app_strict_connection: web.Application,
aiohttp_client: ClientSessionGenerator,
hass_access_token: str,
strict_connection_mode: StrictConnectionMode,
) -> None:
"""Test authenticated requests with strict connection."""
token = hass_access_token
await async_setup_auth(hass, app_strict_connection, strict_connection_mode)
set_mock_ip = mock_real_ip(app_strict_connection)
client = await aiohttp_client(app_strict_connection)
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
assert refresh_token
assert hass.auth.session._strict_connection_sessions == {}
signed_path = async_sign_path(
hass, "/", timedelta(seconds=5), refresh_token_id=refresh_token.id
)
for remote_addr in (*LOCALHOST_ADDRESSES, *PRIVATE_ADDRESSES, *EXTERNAL_ADDRESSES):
set_mock_ip(remote_addr)
# authorized requests should work normally
req = await client.get("/", headers={"Authorization": f"Bearer {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}
@pytest.mark.parametrize(
"strict_connection_mode", [e.value for e in StrictConnectionMode]
)
async def test_strict_connection_non_cloud_local_unauthenticated_requests(
hass: HomeAssistant,
app_strict_connection: web.Application,
aiohttp_client: ClientSessionGenerator,
strict_connection_mode: StrictConnectionMode,
) -> None:
"""Test local unauthenticated requests with strict connection."""
await async_setup_auth(hass, app_strict_connection, strict_connection_mode)
set_mock_ip = mock_real_ip(app_strict_connection)
client = await aiohttp_client(app_strict_connection)
assert hass.auth.session._strict_connection_sessions == {}
for remote_addr in (*LOCALHOST_ADDRESSES, *PRIVATE_ADDRESSES):
set_mock_ip(remote_addr)
# local requests should work normally
req = await client.get("/")
assert req.status == HTTPStatus.OK
assert await req.json() == {"authenticated": False}
def _add_set_cookie_endpoint(app: web.Application, refresh_token: RefreshToken) -> None:
"""Add an endpoint to set a cookie."""
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)
async def _test_strict_connection_non_cloud_enabled_setup(
hass: HomeAssistant,
app: web.Application,
aiohttp_client: ClientSessionGenerator,
hass_access_token: str,
strict_connection_mode: StrictConnectionMode,
) -> tuple[TestClient, Callable[[str], None], RefreshToken]:
"""Test external unauthenticated requests with strict connection non cloud enabled."""
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 == {}
_add_set_cookie_endpoint(app, refresh_token)
await async_setup_auth(hass, app, strict_connection_mode)
set_mock_ip = mock_real_ip(app)
client = await aiohttp_client(app)
return (client, set_mock_ip, refresh_token)
async def _test_strict_connection_non_cloud_enabled_external_unauthenticated_requests(
hass: HomeAssistant,
app: web.Application,
aiohttp_client: ClientSessionGenerator,
hass_access_token: str,
perform_unauthenticated_request: Callable[
[HomeAssistant, TestClient], Awaitable[None]
],
strict_connection_mode: StrictConnectionMode,
) -> None:
"""Test external unauthenticated requests with strict connection non cloud enabled."""
client, set_mock_ip, _ = await _test_strict_connection_non_cloud_enabled_setup(
hass, app, aiohttp_client, hass_access_token, strict_connection_mode
)
for remote_addr in EXTERNAL_ADDRESSES:
set_mock_ip(remote_addr)
await perform_unauthenticated_request(hass, client)
async def _test_strict_connection_non_cloud_enabled_external_unauthenticated_requests_refresh_token(
hass: HomeAssistant,
app: web.Application,
aiohttp_client: ClientSessionGenerator,
hass_access_token: str,
perform_unauthenticated_request: Callable[
[HomeAssistant, TestClient], Awaitable[None]
],
strict_connection_mode: StrictConnectionMode,
) -> None:
"""Test external unauthenticated requests with strict connection non cloud enabled and refresh token cookie."""
(
client,
set_mock_ip,
refresh_token,
) = await _test_strict_connection_non_cloud_enabled_setup(
hass, app, aiohttp_client, hass_access_token, strict_connection_mode
)
session = hass.auth.session
# set strict connection cookie with refresh token
set_mock_ip(LOCALHOST_ADDRESSES[0])
session_id = await (await client.get("/test/cookie?token=refresh")).text()
assert session._strict_connection_sessions == {session_id: refresh_token.id}
for remote_addr in EXTERNAL_ADDRESSES:
set_mock_ip(remote_addr)
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 == {}
for remote_addr in EXTERNAL_ADDRESSES:
set_mock_ip(remote_addr)
await perform_unauthenticated_request(hass, client)
async def _test_strict_connection_non_cloud_enabled_external_unauthenticated_requests_temp_session(
hass: HomeAssistant,
app: web.Application,
aiohttp_client: ClientSessionGenerator,
hass_access_token: str,
perform_unauthenticated_request: Callable[
[HomeAssistant, TestClient], Awaitable[None]
],
strict_connection_mode: StrictConnectionMode,
) -> None:
"""Test external unauthenticated requests with strict connection non cloud enabled and temp cookie."""
client, set_mock_ip, _ = await _test_strict_connection_non_cloud_enabled_setup(
hass, app, aiohttp_client, hass_access_token, strict_connection_mode
)
session = hass.auth.session
# set strict connection cookie with temp session
assert session._temp_sessions == {}
set_mock_ip(LOCALHOST_ADDRESSES[0])
session_id = await (await client.get("/test/cookie?token=temp")).text()
assert client.session.cookie_jar.filter_cookies(URL("http://127.0.0.1"))
assert session_id in session._temp_sessions
for remote_addr in EXTERNAL_ADDRESSES:
set_mock_ip(remote_addr)
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 == {}
for remote_addr in EXTERNAL_ADDRESSES:
set_mock_ip(remote_addr)
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_non_cloud_enabled_external_unauthenticated_requests,
_test_strict_connection_non_cloud_enabled_external_unauthenticated_requests_refresh_token,
_test_strict_connection_non_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_non_cloud_external_unauthenticated_requests(
hass: HomeAssistant,
app_strict_connection: web.Application,
aiohttp_client: ClientSessionGenerator,
hass_access_token: str,
test_func: Callable[
[
HomeAssistant,
web.Application,
ClientSessionGenerator,
str,
Callable[[HomeAssistant, TestClient], Awaitable[None]],
StrictConnectionMode,
],
Awaitable[None],
],
strict_connection_mode: StrictConnectionMode,
request_func: Callable[[HomeAssistant, TestClient], Awaitable[None]],
) -> None:
"""Test external unauthenticated requests with strict connection non cloud."""
await test_func(
hass,
app_strict_connection,
aiohttp_client,
hass_access_token,
request_func,
strict_connection_mode,
)