2016-11-25 21:04:06 +00:00
|
|
|
"""The tests for the Home Assistant HTTP component."""
|
2018-10-25 14:44:57 +00:00
|
|
|
from datetime import timedelta
|
2018-02-15 21:06:14 +00:00
|
|
|
from ipaddress import ip_network
|
2016-11-25 21:04:06 +00:00
|
|
|
|
2018-02-15 21:06:14 +00:00
|
|
|
from aiohttp import BasicAuth, web
|
|
|
|
from aiohttp.web_exceptions import HTTPUnauthorized
|
2019-12-09 10:59:38 +00:00
|
|
|
import pytest
|
2016-11-25 21:04:06 +00:00
|
|
|
|
2019-03-11 02:55:36 +00:00
|
|
|
from homeassistant.auth.providers import trusted_networks
|
2019-12-09 10:59:38 +00:00
|
|
|
from homeassistant.components.http.auth import async_sign_path, setup_auth
|
2018-02-15 21:06:14 +00:00
|
|
|
from homeassistant.components.http.const import KEY_AUTHENTICATED
|
2020-08-11 20:57:50 +00:00
|
|
|
from homeassistant.components.http.forwarded import async_setup_forwarded
|
2018-07-01 02:31:36 +00:00
|
|
|
from homeassistant.setup import async_setup_component
|
2019-12-09 10:59:38 +00:00
|
|
|
|
|
|
|
from . import HTTP_HEADER_HA_AUTH, mock_real_ip
|
2018-07-01 02:31:36 +00:00
|
|
|
|
2020-05-03 18:27:19 +00:00
|
|
|
from tests.async_mock import patch
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
API_PASSWORD = "test-password"
|
2017-05-19 14:37:39 +00:00
|
|
|
|
2016-11-25 21:04:06 +00:00
|
|
|
# Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases
|
2018-02-15 21:06:14 +00:00
|
|
|
TRUSTED_NETWORKS = [
|
2019-07-31 19:25:30 +00:00
|
|
|
ip_network("192.0.2.0/24"),
|
|
|
|
ip_network("2001:DB8:ABCD::/48"),
|
|
|
|
ip_network("100.64.0.1"),
|
|
|
|
ip_network("FD01:DB8::1"),
|
2018-02-15 21:06:14 +00:00
|
|
|
]
|
2019-07-31 19:25:30 +00:00
|
|
|
TRUSTED_ADDRESSES = ["100.64.0.1", "192.0.2.100", "FD01:DB8::1", "2001:DB8:ABCD::1"]
|
|
|
|
UNTRUSTED_ADDRESSES = ["198.51.100.1", "2001:DB8:FA1::1", "127.0.0.1", "::1"]
|
2016-11-25 21:04:06 +00:00
|
|
|
|
|
|
|
|
2018-03-09 01:51:49 +00:00
|
|
|
async def mock_handler(request):
|
2018-02-15 21:06:14 +00:00
|
|
|
"""Return if request was authenticated."""
|
|
|
|
if not request[KEY_AUTHENTICATED]:
|
|
|
|
raise HTTPUnauthorized
|
2018-10-25 14:44:57 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
user = request.get("hass_user")
|
2018-10-25 14:44:57 +00:00
|
|
|
user_id = user.id if user else None
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
return web.json_response(status=200, data={"user_id": user_id})
|
2016-11-25 21:04:06 +00:00
|
|
|
|
|
|
|
|
2019-03-11 02:55:36 +00:00
|
|
|
async def get_legacy_user(auth):
|
|
|
|
"""Get the user in legacy_api_password auth provider."""
|
2019-07-31 19:25:30 +00:00
|
|
|
provider = auth.get_auth_provider("legacy_api_password", None)
|
2019-03-11 02:55:36 +00:00
|
|
|
return await auth.async_get_or_create_user(
|
|
|
|
await provider.async_get_or_create_credentials({})
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2018-02-15 21:06:14 +00:00
|
|
|
@pytest.fixture
|
2018-07-13 13:31:20 +00:00
|
|
|
def app(hass):
|
2018-08-19 20:29:08 +00:00
|
|
|
"""Fixture to set up a web.Application."""
|
2018-02-15 21:06:14 +00:00
|
|
|
app = web.Application()
|
2019-07-31 19:25:30 +00:00
|
|
|
app["hass"] = hass
|
|
|
|
app.router.add_get("/", mock_handler)
|
2020-08-11 20:57:50 +00:00
|
|
|
async_setup_forwarded(app, [])
|
2018-02-15 21:06:14 +00:00
|
|
|
return app
|
2016-11-25 21:04:06 +00:00
|
|
|
|
|
|
|
|
2018-07-01 02:31:36 +00:00
|
|
|
@pytest.fixture
|
2018-07-13 13:31:20 +00:00
|
|
|
def app2(hass):
|
2018-08-19 20:29:08 +00:00
|
|
|
"""Fixture to set up a web.Application without real_ip middleware."""
|
2018-07-01 02:31:36 +00:00
|
|
|
app = web.Application()
|
2019-07-31 19:25:30 +00:00
|
|
|
app["hass"] = hass
|
|
|
|
app.router.add_get("/", mock_handler)
|
2018-07-01 02:31:36 +00:00
|
|
|
return app
|
|
|
|
|
|
|
|
|
2019-03-11 02:55:36 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def trusted_networks_auth(hass):
|
|
|
|
"""Load trusted networks auth provider."""
|
|
|
|
prv = trusted_networks.TrustedNetworksAuthProvider(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass,
|
|
|
|
hass.auth._store,
|
|
|
|
{"type": "trusted_networks", "trusted_networks": TRUSTED_NETWORKS},
|
2019-03-11 02:55:36 +00:00
|
|
|
)
|
|
|
|
hass.auth._providers[(prv.type, prv.id)] = prv
|
|
|
|
return prv
|
|
|
|
|
|
|
|
|
2018-03-09 01:51:49 +00:00
|
|
|
async def test_auth_middleware_loaded_by_default(hass):
|
2018-02-15 21:06:14 +00:00
|
|
|
"""Test accessing to server from banned IP when feature is off."""
|
2019-07-31 19:25:30 +00:00
|
|
|
with patch("homeassistant.components.http.setup_auth") as mock_setup:
|
|
|
|
await async_setup_component(hass, "http", {"http": {}})
|
2016-11-25 21:04:06 +00:00
|
|
|
|
2018-02-15 21:06:14 +00:00
|
|
|
assert len(mock_setup.mock_calls) == 1
|
2016-11-25 21:04:06 +00:00
|
|
|
|
|
|
|
|
2019-10-14 21:56:45 +00:00
|
|
|
async def test_cant_access_with_password_in_header(
|
|
|
|
app, aiohttp_client, legacy_auth, hass
|
|
|
|
):
|
2018-07-01 02:31:36 +00:00
|
|
|
"""Test access with password in header."""
|
2019-03-11 02:55:36 +00:00
|
|
|
setup_auth(hass, app)
|
2018-03-15 20:49:49 +00:00
|
|
|
client = await aiohttp_client(app)
|
2016-11-25 21:04:06 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
|
2019-10-14 21:56:45 +00:00
|
|
|
assert req.status == 401
|
2016-11-25 21:04:06 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: "wrong-pass"})
|
2018-02-15 21:06:14 +00:00
|
|
|
assert req.status == 401
|
2016-11-25 21:04:06 +00:00
|
|
|
|
|
|
|
|
2019-10-14 21:56:45 +00:00
|
|
|
async def test_cant_access_with_password_in_query(
|
|
|
|
app, aiohttp_client, legacy_auth, hass
|
|
|
|
):
|
2018-07-01 02:31:36 +00:00
|
|
|
"""Test access with password in URL."""
|
2019-03-11 02:55:36 +00:00
|
|
|
setup_auth(hass, app)
|
2018-03-15 20:49:49 +00:00
|
|
|
client = await aiohttp_client(app)
|
2016-11-25 21:04:06 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
resp = await client.get("/", params={"api_password": API_PASSWORD})
|
2019-10-14 21:56:45 +00:00
|
|
|
assert resp.status == 401
|
2016-11-25 21:04:06 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
resp = await client.get("/")
|
2018-02-15 21:06:14 +00:00
|
|
|
assert resp.status == 401
|
2016-11-25 21:04:06 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
resp = await client.get("/", params={"api_password": "wrong-password"})
|
2018-02-15 21:06:14 +00:00
|
|
|
assert resp.status == 401
|
2017-09-28 07:49:35 +00:00
|
|
|
|
|
|
|
|
2019-10-14 21:56:45 +00:00
|
|
|
async def test_basic_auth_does_not_work(app, aiohttp_client, hass, legacy_auth):
|
2017-09-28 07:49:35 +00:00
|
|
|
"""Test access with basic authentication."""
|
2019-03-11 02:55:36 +00:00
|
|
|
setup_auth(hass, app)
|
2018-03-15 20:49:49 +00:00
|
|
|
client = await aiohttp_client(app)
|
2017-09-28 07:49:35 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
req = await client.get("/", auth=BasicAuth("homeassistant", API_PASSWORD))
|
2019-10-14 21:56:45 +00:00
|
|
|
assert req.status == 401
|
2017-09-28 07:49:35 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
req = await client.get("/", auth=BasicAuth("wrong_username", API_PASSWORD))
|
2018-02-15 21:06:14 +00:00
|
|
|
assert req.status == 401
|
2017-09-28 07:49:35 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
req = await client.get("/", auth=BasicAuth("homeassistant", "wrong password"))
|
2018-02-15 21:06:14 +00:00
|
|
|
assert req.status == 401
|
2017-09-28 07:49:35 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
req = await client.get("/", headers={"authorization": "NotBasic abcdefg"})
|
2017-09-28 07:49:35 +00:00
|
|
|
assert req.status == 401
|
|
|
|
|
|
|
|
|
2019-10-14 21:56:45 +00:00
|
|
|
async def test_cannot_access_with_trusted_ip(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass, app2, trusted_networks_auth, aiohttp_client, hass_owner_user
|
|
|
|
):
|
2018-02-15 21:06:14 +00:00
|
|
|
"""Test access with an untrusted ip address."""
|
2019-03-11 02:55:36 +00:00
|
|
|
setup_auth(hass, app2)
|
2017-09-28 07:49:35 +00:00
|
|
|
|
2018-07-01 02:31:36 +00:00
|
|
|
set_mock_ip = mock_real_ip(app2)
|
|
|
|
client = await aiohttp_client(app2)
|
2017-09-28 07:49:35 +00:00
|
|
|
|
2018-07-01 02:31:36 +00:00
|
|
|
for remote_addr in UNTRUSTED_ADDRESSES:
|
|
|
|
set_mock_ip(remote_addr)
|
2019-07-31 19:25:30 +00:00
|
|
|
resp = await client.get("/")
|
2020-04-04 22:33:07 +00:00
|
|
|
assert resp.status == 401, f"{remote_addr} shouldn't be trusted"
|
2018-07-01 02:31:36 +00:00
|
|
|
|
|
|
|
for remote_addr in TRUSTED_ADDRESSES:
|
|
|
|
set_mock_ip(remote_addr)
|
2019-07-31 19:25:30 +00:00
|
|
|
resp = await client.get("/")
|
2020-04-04 22:33:07 +00:00
|
|
|
assert resp.status == 401, f"{remote_addr} shouldn't be trusted"
|
2018-07-01 02:31:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
async def test_auth_active_access_with_access_token_in_header(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass, app, aiohttp_client, hass_access_token
|
|
|
|
):
|
2018-07-01 02:31:36 +00:00
|
|
|
"""Test access with access token in header."""
|
2018-08-14 19:14:12 +00:00
|
|
|
token = hass_access_token
|
2019-03-11 02:55:36 +00:00
|
|
|
setup_auth(hass, app)
|
2018-03-15 20:49:49 +00:00
|
|
|
client = await aiohttp_client(app)
|
2019-07-31 19:25:30 +00:00
|
|
|
refresh_token = await hass.auth.async_validate_access_token(hass_access_token)
|
2017-09-28 07:49:35 +00:00
|
|
|
|
2020-04-04 22:33:07 +00:00
|
|
|
req = await client.get("/", headers={"Authorization": f"Bearer {token}"})
|
2018-07-01 02:31:36 +00:00
|
|
|
assert req.status == 200
|
2019-07-31 19:25:30 +00:00
|
|
|
assert await req.json() == {"user_id": refresh_token.user.id}
|
2018-07-01 02:31:36 +00:00
|
|
|
|
2020-04-04 22:33:07 +00:00
|
|
|
req = await client.get("/", headers={"AUTHORIZATION": f"Bearer {token}"})
|
2018-07-01 02:31:36 +00:00
|
|
|
assert req.status == 200
|
2019-07-31 19:25:30 +00:00
|
|
|
assert await req.json() == {"user_id": refresh_token.user.id}
|
2018-07-01 02:31:36 +00:00
|
|
|
|
2020-04-04 22:33:07 +00:00
|
|
|
req = await client.get("/", headers={"authorization": f"Bearer {token}"})
|
2018-07-01 02:31:36 +00:00
|
|
|
assert req.status == 200
|
2019-07-31 19:25:30 +00:00
|
|
|
assert await req.json() == {"user_id": refresh_token.user.id}
|
2018-07-01 02:31:36 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
req = await client.get("/", headers={"Authorization": token})
|
2018-07-01 02:31:36 +00:00
|
|
|
assert req.status == 401
|
|
|
|
|
2020-04-04 22:33:07 +00:00
|
|
|
req = await client.get("/", headers={"Authorization": f"BEARER {token}"})
|
2018-07-01 02:31:36 +00:00
|
|
|
assert req.status == 401
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
refresh_token = await hass.auth.async_validate_access_token(hass_access_token)
|
2018-08-14 19:14:12 +00:00
|
|
|
refresh_token.user.is_active = False
|
2020-04-04 22:33:07 +00:00
|
|
|
req = await client.get("/", headers={"Authorization": f"Bearer {token}"})
|
2018-07-01 02:31:36 +00:00
|
|
|
assert req.status == 401
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def test_auth_active_access_with_trusted_ip(
|
|
|
|
hass, app2, trusted_networks_auth, aiohttp_client, hass_owner_user
|
|
|
|
):
|
2018-07-01 02:31:36 +00:00
|
|
|
"""Test access with an untrusted ip address."""
|
2019-03-11 02:55:36 +00:00
|
|
|
setup_auth(hass, app2)
|
2018-07-01 02:31:36 +00:00
|
|
|
|
|
|
|
set_mock_ip = mock_real_ip(app2)
|
|
|
|
client = await aiohttp_client(app2)
|
|
|
|
|
2018-02-15 21:06:14 +00:00
|
|
|
for remote_addr in UNTRUSTED_ADDRESSES:
|
|
|
|
set_mock_ip(remote_addr)
|
2019-07-31 19:25:30 +00:00
|
|
|
resp = await client.get("/")
|
2020-04-04 22:33:07 +00:00
|
|
|
assert resp.status == 401, f"{remote_addr} shouldn't be trusted"
|
2017-09-28 07:49:35 +00:00
|
|
|
|
2018-02-15 21:06:14 +00:00
|
|
|
for remote_addr in TRUSTED_ADDRESSES:
|
|
|
|
set_mock_ip(remote_addr)
|
2019-07-31 19:25:30 +00:00
|
|
|
resp = await client.get("/")
|
2020-04-04 22:33:07 +00:00
|
|
|
assert resp.status == 401, f"{remote_addr} shouldn't be trusted"
|
2018-07-01 02:31:36 +00:00
|
|
|
|
|
|
|
|
2019-10-14 21:56:45 +00:00
|
|
|
async def test_auth_legacy_support_api_password_cannot_access(
|
2019-07-31 19:25:30 +00:00
|
|
|
app, aiohttp_client, legacy_auth, hass
|
|
|
|
):
|
2018-07-01 02:31:36 +00:00
|
|
|
"""Test access using api_password if auth.support_legacy."""
|
2019-03-11 02:55:36 +00:00
|
|
|
setup_auth(hass, app)
|
2018-07-01 02:31:36 +00:00
|
|
|
client = await aiohttp_client(app)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
|
2019-10-14 21:56:45 +00:00
|
|
|
assert req.status == 401
|
2018-07-01 02:31:36 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
resp = await client.get("/", params={"api_password": API_PASSWORD})
|
2019-10-14 21:56:45 +00:00
|
|
|
assert resp.status == 401
|
2018-07-01 02:31:36 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
req = await client.get("/", auth=BasicAuth("homeassistant", API_PASSWORD))
|
2019-10-14 21:56:45 +00:00
|
|
|
assert req.status == 401
|
2018-10-25 14:44:57 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def test_auth_access_signed_path(hass, app, aiohttp_client, hass_access_token):
|
2018-10-25 14:44:57 +00:00
|
|
|
"""Test access with signed url."""
|
2019-07-31 19:25:30 +00:00
|
|
|
app.router.add_post("/", mock_handler)
|
|
|
|
app.router.add_get("/another_path", mock_handler)
|
2019-03-11 02:55:36 +00:00
|
|
|
setup_auth(hass, app)
|
2018-10-25 14:44:57 +00:00
|
|
|
client = await aiohttp_client(app)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
refresh_token = await hass.auth.async_validate_access_token(hass_access_token)
|
2018-10-25 14:44:57 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
signed_path = async_sign_path(hass, refresh_token.id, "/", timedelta(seconds=5))
|
2018-10-25 14:44:57 +00:00
|
|
|
|
|
|
|
req = await client.get(signed_path)
|
|
|
|
assert req.status == 200
|
|
|
|
data = await req.json()
|
2019-07-31 19:25:30 +00:00
|
|
|
assert data["user_id"] == refresh_token.user.id
|
2018-10-25 14:44:57 +00:00
|
|
|
|
|
|
|
# Use signature on other path
|
2019-07-31 19:25:30 +00:00
|
|
|
req = await client.get("/another_path?{}".format(signed_path.split("?")[1]))
|
2018-10-25 14:44:57 +00:00
|
|
|
assert req.status == 401
|
|
|
|
|
|
|
|
# We only allow GET
|
|
|
|
req = await client.post(signed_path)
|
|
|
|
assert req.status == 401
|
|
|
|
|
|
|
|
# Never valid as expired in the past.
|
|
|
|
expired_signed_path = async_sign_path(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass, refresh_token.id, "/", timedelta(seconds=-5)
|
2018-10-25 14:44:57 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
req = await client.get(expired_signed_path)
|
|
|
|
assert req.status == 401
|
|
|
|
|
|
|
|
# refresh token gone should also invalidate signature
|
|
|
|
await hass.auth.async_remove_refresh_token(refresh_token)
|
|
|
|
req = await client.get(signed_path)
|
|
|
|
assert req.status == 401
|