mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-18 06:36:30 +00:00
Improve /auth API request sanitation (#5843)
* Add basic test coverage for /auth API * Check /auth API is called from an add-on Currently the /auth API is only available for add-ons. Return 403 for calls not originating from an add-on. * Handle bad json in auth API Use the API specific JSON load helper which raises an APIError. This causes the API to return a 400 error instead of a 500 error when the JSON is invalid. * Avoid redefining name 'mock_check_login' * Update tests/api/test_auth.py
This commit is contained in:
parent
0e55e6e67b
commit
9470f44840
@ -14,7 +14,6 @@ from ..addons.addon import Addon
|
|||||||
from ..const import ATTR_NAME, ATTR_PASSWORD, ATTR_USERNAME, REQUEST_FROM
|
from ..const import ATTR_NAME, ATTR_PASSWORD, ATTR_USERNAME, REQUEST_FROM
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..exceptions import APIForbidden
|
from ..exceptions import APIForbidden
|
||||||
from ..utils.json import json_loads
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_GROUP_IDS,
|
ATTR_GROUP_IDS,
|
||||||
ATTR_IS_ACTIVE,
|
ATTR_IS_ACTIVE,
|
||||||
@ -24,7 +23,7 @@ from .const import (
|
|||||||
CONTENT_TYPE_JSON,
|
CONTENT_TYPE_JSON,
|
||||||
CONTENT_TYPE_URL,
|
CONTENT_TYPE_URL,
|
||||||
)
|
)
|
||||||
from .utils import api_process, api_validate
|
from .utils import api_process, api_validate, json_loads
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -68,7 +67,7 @@ class APIAuth(CoreSysAttributes):
|
|||||||
"""Process login request."""
|
"""Process login request."""
|
||||||
addon = request[REQUEST_FROM]
|
addon = request[REQUEST_FROM]
|
||||||
|
|
||||||
if not addon.access_auth_api:
|
if not isinstance(addon, Addon) or not addon.access_auth_api:
|
||||||
raise APIForbidden("Can't use Home Assistant auth!")
|
raise APIForbidden("Can't use Home Assistant auth!")
|
||||||
|
|
||||||
# BasicAuth
|
# BasicAuth
|
||||||
|
@ -6,8 +6,11 @@ from unittest.mock import AsyncMock, patch
|
|||||||
from aiohttp.test_utils import TestClient
|
from aiohttp.test_utils import TestClient
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from supervisor.addons.addon import Addon
|
||||||
from supervisor.coresys import CoreSys
|
from supervisor.coresys import CoreSys
|
||||||
|
|
||||||
|
from tests.const import TEST_ADDON_SLUG
|
||||||
|
|
||||||
LIST_USERS_RESPONSE = [
|
LIST_USERS_RESPONSE = [
|
||||||
{
|
{
|
||||||
"id": "a1d90e114a3b4da4a487fe327918dcef",
|
"id": "a1d90e114a3b4da4a487fe327918dcef",
|
||||||
@ -67,6 +70,13 @@ LIST_USERS_RESPONSE = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="mock_check_login")
|
||||||
|
def fixture_mock_check_login(coresys: CoreSys):
|
||||||
|
"""Patch sys_auth.check_login."""
|
||||||
|
with patch.object(coresys.auth, "check_login", new_callable=AsyncMock) as mock:
|
||||||
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
async def test_password_reset(
|
async def test_password_reset(
|
||||||
api_client: TestClient, coresys: CoreSys, caplog: pytest.LogCaptureFixture
|
api_client: TestClient, coresys: CoreSys, caplog: pytest.LogCaptureFixture
|
||||||
):
|
):
|
||||||
@ -106,3 +116,124 @@ async def test_list_users(
|
|||||||
"group_ids": ["system-admin"],
|
"group_ids": ["system-admin"],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("api_client", [TEST_ADDON_SLUG], indirect=True)
|
||||||
|
async def test_auth_json_success(
|
||||||
|
api_client: TestClient, mock_check_login: AsyncMock, install_addon_ssh: Addon
|
||||||
|
):
|
||||||
|
"""Test successful JSON auth."""
|
||||||
|
mock_check_login.return_value = True
|
||||||
|
resp = await api_client.post("/auth", json={"username": "test", "password": "pass"})
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("api_client", [TEST_ADDON_SLUG], indirect=True)
|
||||||
|
async def test_auth_json_invalid_credentials(
|
||||||
|
api_client: TestClient, mock_check_login: AsyncMock, install_addon_ssh: Addon
|
||||||
|
):
|
||||||
|
"""Test failed JSON auth due to invalid credentials."""
|
||||||
|
mock_check_login.return_value = False
|
||||||
|
resp = await api_client.post(
|
||||||
|
"/auth", json={"username": "test", "password": "wrong"}
|
||||||
|
)
|
||||||
|
# Do we really want the API to return 400 here?
|
||||||
|
assert resp.status == 400
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("api_client", [TEST_ADDON_SLUG], indirect=True)
|
||||||
|
async def test_auth_json_empty_body(api_client: TestClient, install_addon_ssh: Addon):
|
||||||
|
"""Test JSON auth with empty body."""
|
||||||
|
resp = await api_client.post(
|
||||||
|
"/auth", data="", headers={"Content-Type": "application/json"}
|
||||||
|
)
|
||||||
|
assert resp.status == 400
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("api_client", [TEST_ADDON_SLUG], indirect=True)
|
||||||
|
async def test_auth_json_invalid_json(api_client: TestClient, install_addon_ssh: Addon):
|
||||||
|
"""Test JSON auth with malformed JSON."""
|
||||||
|
resp = await api_client.post(
|
||||||
|
"/auth", data="{not json}", headers={"Content-Type": "application/json"}
|
||||||
|
)
|
||||||
|
assert resp.status == 400
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("api_client", [TEST_ADDON_SLUG], indirect=True)
|
||||||
|
async def test_auth_urlencoded_success(
|
||||||
|
api_client: TestClient, mock_check_login: AsyncMock, install_addon_ssh: Addon
|
||||||
|
):
|
||||||
|
"""Test successful URL-encoded auth."""
|
||||||
|
mock_check_login.return_value = True
|
||||||
|
resp = await api_client.post(
|
||||||
|
"/auth",
|
||||||
|
data="username=test&password=pass",
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
|
)
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("api_client", [TEST_ADDON_SLUG], indirect=True)
|
||||||
|
async def test_auth_urlencoded_failure(
|
||||||
|
api_client: TestClient, mock_check_login: AsyncMock, install_addon_ssh: Addon
|
||||||
|
):
|
||||||
|
"""Test URL-encoded auth with invalid credentials."""
|
||||||
|
mock_check_login.return_value = False
|
||||||
|
resp = await api_client.post(
|
||||||
|
"/auth",
|
||||||
|
data="username=test&password=fail",
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
|
)
|
||||||
|
# Do we really want the API to return 400 here?
|
||||||
|
assert resp.status == 400
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("api_client", [TEST_ADDON_SLUG], indirect=True)
|
||||||
|
async def test_auth_unsupported_content_type(
|
||||||
|
api_client: TestClient, install_addon_ssh: Addon
|
||||||
|
):
|
||||||
|
"""Test auth with unsupported content type."""
|
||||||
|
resp = await api_client.post(
|
||||||
|
"/auth", data="something", headers={"Content-Type": "text/plain"}
|
||||||
|
)
|
||||||
|
# This probably should be 400 here for better consistency
|
||||||
|
assert resp.status == 401
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("api_client", [TEST_ADDON_SLUG], indirect=True)
|
||||||
|
async def test_auth_basic_auth(
|
||||||
|
api_client: TestClient, mock_check_login: AsyncMock, install_addon_ssh: Addon
|
||||||
|
):
|
||||||
|
"""Test auth with BasicAuth header."""
|
||||||
|
mock_check_login.return_value = True
|
||||||
|
resp = await api_client.post(
|
||||||
|
"/auth", headers={"Authorization": "Basic dGVzdDpwYXNz"}
|
||||||
|
)
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("api_client", [TEST_ADDON_SLUG], indirect=True)
|
||||||
|
async def test_auth_basic_auth_failure(
|
||||||
|
api_client: TestClient, mock_check_login: AsyncMock, install_addon_ssh: Addon
|
||||||
|
):
|
||||||
|
"""Test auth with BasicAuth header and failure."""
|
||||||
|
mock_check_login.return_value = False
|
||||||
|
resp = await api_client.post(
|
||||||
|
"/auth", headers={"Authorization": "Basic dGVzdDpwYXNz"}
|
||||||
|
)
|
||||||
|
assert resp.status == 401
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("api_client", ["local_example"], indirect=True)
|
||||||
|
async def test_auth_addon_no_auth_access(
|
||||||
|
api_client: TestClient, install_addon_example: Addon
|
||||||
|
):
|
||||||
|
"""Test auth where add-on is not allowed to access auth API."""
|
||||||
|
resp = await api_client.post("/auth", json={"username": "test", "password": "pass"})
|
||||||
|
assert resp.status == 403
|
||||||
|
|
||||||
|
|
||||||
|
async def test_non_addon_token_no_auth_access(api_client: TestClient):
|
||||||
|
"""Test auth where add-on is not allowed to access auth API."""
|
||||||
|
resp = await api_client.post("/auth", json={"username": "test", "password": "pass"})
|
||||||
|
assert resp.status == 403
|
||||||
|
1
tests/fixtures/addons/local/ssh/config.yaml
vendored
1
tests/fixtures/addons/local/ssh/config.yaml
vendored
@ -18,6 +18,7 @@ panel_title: Terminal
|
|||||||
hassio_api: true
|
hassio_api: true
|
||||||
hassio_role: manager
|
hassio_role: manager
|
||||||
homeassistant_api: true
|
homeassistant_api: true
|
||||||
|
auth_api: true
|
||||||
audio: true
|
audio: true
|
||||||
uart: true
|
uart: true
|
||||||
ports:
|
ports:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user