Remove direct authentication via trusted networks or API password (#27656)

* Remove direct authentication via trusted networks and API password

* Fix tests
This commit is contained in:
Paulus Schoutsen 2019-10-14 14:56:45 -07:00 committed by GitHub
parent 97478d1ef4
commit 3231e22ddf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 114 additions and 423 deletions

View file

@ -86,18 +86,6 @@ class AuthManager:
hass, self._async_create_login_flow, self._async_finish_login_flow
)
@property
def support_legacy(self) -> bool:
"""
Return if legacy_api_password auth providers are registered.
Should be removed when we removed legacy_api_password auth providers.
"""
for provider_type, _ in self._providers:
if provider_type == "legacy_api_password":
return True
return False
@property
def auth_providers(self) -> List[AuthProvider]:
"""Return a list of available auth providers."""

View file

@ -64,13 +64,9 @@ async def async_from_config_dict(
)
core_config = config.get(core.DOMAIN, {})
api_password = config.get("http", {}).get("api_password")
trusted_networks = config.get("http", {}).get("trusted_networks")
try:
await conf_util.async_process_ha_core_config(
hass, core_config, api_password, trusted_networks
)
await conf_util.async_process_ha_core_config(hass, core_config)
except vol.Invalid as config_err:
conf_util.async_log_exception(config_err, "homeassistant", core_config, hass)
return None

View file

@ -72,7 +72,11 @@ import voluptuous_serialize
from homeassistant import data_entry_flow
from homeassistant.components.http import KEY_REAL_IP
from homeassistant.components.http.ban import process_wrong_login, log_invalid_auth
from homeassistant.components.http.ban import (
process_wrong_login,
process_success_login,
log_invalid_auth,
)
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView
from . import indieauth
@ -185,6 +189,7 @@ class LoginFlowIndexView(HomeAssistantView):
return self.json_message("Handler does not support init", 400)
if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
await process_success_login(request)
result.pop("data")
result["result"] = self._store_result(data["client_id"], result["result"])
return self.json(result)

View file

@ -17,7 +17,6 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
import homeassistant.util as hass_util
from homeassistant.util import ssl as ssl_util
from homeassistant.util.logging import HideSensitiveDataFilter
from .auth import setup_auth
from .ban import setup_bans
@ -32,7 +31,6 @@ from .view import HomeAssistantView # noqa
DOMAIN = "http"
CONF_API_PASSWORD = "api_password"
CONF_SERVER_HOST = "server_host"
CONF_SERVER_PORT = "server_port"
CONF_BASE_URL = "base_url"
@ -42,7 +40,6 @@ CONF_SSL_KEY = "ssl_key"
CONF_CORS_ORIGINS = "cors_allowed_origins"
CONF_USE_X_FORWARDED_FOR = "use_x_forwarded_for"
CONF_TRUSTED_PROXIES = "trusted_proxies"
CONF_TRUSTED_NETWORKS = "trusted_networks"
CONF_LOGIN_ATTEMPTS_THRESHOLD = "login_attempts_threshold"
CONF_IP_BAN_ENABLED = "ip_ban_enabled"
CONF_SSL_PROFILE = "ssl_profile"
@ -59,37 +56,8 @@ DEFAULT_CORS = "https://cast.home-assistant.io"
NO_LOGIN_ATTEMPT_THRESHOLD = -1
def trusted_networks_deprecated(value):
"""Warn user trusted_networks config is deprecated."""
if not value:
return value
_LOGGER.warning(
"Configuring trusted_networks via the http integration has been"
" deprecated. Use the trusted networks auth provider instead."
" For instructions, see https://www.home-assistant.io/docs/"
"authentication/providers/#trusted-networks"
)
return value
def api_password_deprecated(value):
"""Warn user api_password config is deprecated."""
if not value:
return value
_LOGGER.warning(
"Configuring api_password via the http integration has been"
" deprecated. Use the legacy api password auth provider instead."
" For instructions, see https://www.home-assistant.io/docs/"
"authentication/providers/#legacy-api-password"
)
return value
HTTP_SCHEMA = vol.Schema(
{
vol.Optional(CONF_API_PASSWORD): vol.All(cv.string, api_password_deprecated),
vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string,
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port,
vol.Optional(CONF_BASE_URL): cv.string,
@ -103,9 +71,6 @@ HTTP_SCHEMA = vol.Schema(
vol.Inclusive(CONF_TRUSTED_PROXIES, "proxy"): vol.All(
cv.ensure_list, [ip_network]
),
vol.Optional(CONF_TRUSTED_NETWORKS, default=[]): vol.All(
cv.ensure_list, [ip_network], trusted_networks_deprecated
),
vol.Optional(
CONF_LOGIN_ATTEMPTS_THRESHOLD, default=NO_LOGIN_ATTEMPT_THRESHOLD
): vol.Any(cv.positive_int, NO_LOGIN_ATTEMPT_THRESHOLD),
@ -149,7 +114,6 @@ async def async_setup(hass, config):
if conf is None:
conf = HTTP_SCHEMA({})
api_password = conf.get(CONF_API_PASSWORD)
server_host = conf[CONF_SERVER_HOST]
server_port = conf[CONF_SERVER_PORT]
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
@ -162,11 +126,6 @@ async def async_setup(hass, config):
login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
ssl_profile = conf[CONF_SSL_PROFILE]
if api_password is not None:
logging.getLogger("aiohttp.access").addFilter(
HideSensitiveDataFilter(api_password)
)
server = HomeAssistantHTTP(
hass,
server_host=server_host,

View file

@ -1,14 +1,11 @@
"""Authentication for HTTP component."""
import base64
import logging
from aiohttp import hdrs
from aiohttp.web import middleware
import jwt
from homeassistant.auth.providers import legacy_api_password
from homeassistant.auth.util import generate_secret
from homeassistant.const import HTTP_HEADER_HA_AUTH
from homeassistant.core import callback
from homeassistant.util import dt as dt_util
@ -52,16 +49,6 @@ def async_sign_path(hass, refresh_token_id, path, expiration):
@callback
def setup_auth(hass, app):
"""Create auth middleware for the app."""
old_auth_warning = set()
support_legacy = hass.auth.support_legacy
if support_legacy:
_LOGGER.warning("legacy_api_password support has been enabled.")
trusted_networks = []
for prv in hass.auth.auth_providers:
if prv.type == "trusted_networks":
trusted_networks += prv.trusted_networks
async def async_validate_auth_header(request):
"""
@ -75,40 +62,16 @@ def setup_auth(hass, app):
# If no space in authorization header
return False
if auth_type == "Bearer":
refresh_token = await hass.auth.async_validate_access_token(auth_val)
if refresh_token is None:
return False
if auth_type != "Bearer":
return False
request[KEY_HASS_USER] = refresh_token.user
return True
refresh_token = await hass.auth.async_validate_access_token(auth_val)
if auth_type == "Basic" and support_legacy:
decoded = base64.b64decode(auth_val).decode("utf-8")
try:
username, password = decoded.split(":", 1)
except ValueError:
# If no ':' in decoded
return False
if refresh_token is None:
return False
if username != "homeassistant":
return False
user = await legacy_api_password.async_validate_password(hass, password)
if user is None:
return False
request[KEY_HASS_USER] = user
_LOGGER.info(
"Basic auth with api_password is going to deprecate,"
" please use a bearer token to access %s from %s",
request.path,
request[KEY_REAL_IP],
)
old_auth_warning.add(request.path)
return True
return False
request[KEY_HASS_USER] = refresh_token.user
return True
async def async_validate_signed_request(request):
"""Validate a signed request."""
@ -140,50 +103,16 @@ def setup_auth(hass, app):
request[KEY_HASS_USER] = refresh_token.user
return True
async def async_validate_trusted_networks(request):
"""Test if request is from a trusted ip."""
ip_addr = request[KEY_REAL_IP]
if not any(ip_addr in trusted_network for trusted_network in trusted_networks):
return False
user = await hass.auth.async_get_owner()
if user is None:
return False
request[KEY_HASS_USER] = user
return True
async def async_validate_legacy_api_password(request, password):
"""Validate api_password."""
user = await legacy_api_password.async_validate_password(hass, password)
if user is None:
return False
request[KEY_HASS_USER] = user
return True
@middleware
async def auth_middleware(request, handler):
"""Authenticate as middleware."""
authenticated = False
if HTTP_HEADER_HA_AUTH in request.headers or DATA_API_PASSWORD in request.query:
if request.path not in old_auth_warning:
_LOGGER.log(
logging.INFO if support_legacy else logging.WARNING,
"api_password is going to deprecate. You need to use a"
" bearer token to access %s from %s",
request.path,
request[KEY_REAL_IP],
)
old_auth_warning.add(request.path)
if hdrs.AUTHORIZATION in request.headers and await async_validate_auth_header(
request
):
# it included both use_auth and api_password Basic auth
authenticated = True
auth_type = "bearer token"
# We first start with a string check to avoid parsing query params
# for every request.
@ -193,39 +122,15 @@ def setup_auth(hass, app):
and await async_validate_signed_request(request)
):
authenticated = True
auth_type = "signed request"
elif trusted_networks and await async_validate_trusted_networks(request):
if request.path not in old_auth_warning:
# When removing this, don't forget to remove the print logic
# in http/view.py
request["deprecate_warning_message"] = (
"Access from trusted networks without auth token is "
"going to be removed in Home Assistant 0.96. Configure "
"the trusted networks auth provider or use long-lived "
"access tokens to access {} from {}".format(
request.path, request[KEY_REAL_IP]
)
)
old_auth_warning.add(request.path)
authenticated = True
elif (
support_legacy
and HTTP_HEADER_HA_AUTH in request.headers
and await async_validate_legacy_api_password(
request, request.headers[HTTP_HEADER_HA_AUTH]
if authenticated:
_LOGGER.debug(
"Authenticated %s for %s using %s",
request[KEY_REAL_IP],
request.path,
auth_type,
)
):
authenticated = True
elif (
support_legacy
and DATA_API_PASSWORD in request.query
and await async_validate_legacy_api_password(
request, request.query[DATA_API_PASSWORD]
)
):
authenticated = True
request[KEY_AUTHENTICATED] = authenticated
return await handler(request)

View file

@ -3,7 +3,7 @@ import aiohttp_cors
from aiohttp.web_urldispatcher import Resource, ResourceRoute, StaticResource
from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN, AUTHORIZATION
from homeassistant.const import HTTP_HEADER_HA_AUTH, HTTP_HEADER_X_REQUESTED_WITH
from homeassistant.const import HTTP_HEADER_X_REQUESTED_WITH
from homeassistant.core import callback
@ -14,7 +14,6 @@ ALLOWED_CORS_HEADERS = [
ACCEPT,
HTTP_HEADER_X_REQUESTED_WITH,
CONTENT_TYPE,
HTTP_HEADER_HA_AUTH,
AUTHORIZATION,
]
VALID_CORS_TYPES = (Resource, ResourceRoute, StaticResource)

View file

@ -17,7 +17,6 @@ from homeassistant.const import CONTENT_TYPE_JSON
from homeassistant.core import Context, is_callback
from homeassistant.helpers.json import JSONEncoder
from .ban import process_success_login
from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_REAL_IP
_LOGGER = logging.getLogger(__name__)
@ -106,13 +105,8 @@ def request_handler_factory(view, handler):
authenticated = request.get(KEY_AUTHENTICATED, False)
if view.requires_auth:
if authenticated:
if "deprecate_warning_message" in request:
_LOGGER.warning(request["deprecate_warning_message"])
await process_success_login(request)
else:
raise HTTPUnauthorized()
if view.requires_auth and not authenticated:
raise HTTPUnauthorized()
_LOGGER.debug(
"Serving %s to %s (auth: %s)",

View file

@ -3,7 +3,6 @@ import voluptuous as vol
from voluptuous.humanize import humanize_error
from homeassistant.auth.models import RefreshToken, User
from homeassistant.auth.providers import legacy_api_password
from homeassistant.components.http.ban import process_wrong_login, process_success_login
from homeassistant.const import __version__
@ -74,19 +73,6 @@ class AuthPhase:
if refresh_token is not None:
return await self._async_finish_auth(refresh_token.user, refresh_token)
elif self._hass.auth.support_legacy and "api_password" in msg:
self._logger.info(
"Received api_password, it is going to deprecate, please use"
" access_token instead. For instructions, see https://"
"developers.home-assistant.io/docs/en/external_api_websocket"
".html#authentication-phase"
)
user = await legacy_api_password.async_validate_password(
self._hass, msg["api_password"]
)
if user is not None:
return await self._async_finish_auth(user, None)
self._send_message(auth_invalid_message("Invalid access token or password"))
await process_wrong_login(self._request)
raise Disconnect

View file

@ -468,12 +468,7 @@ def _format_config_error(ex: Exception, domain: str, config: Dict) -> str:
return message
async def async_process_ha_core_config(
hass: HomeAssistant,
config: Dict,
api_password: Optional[str] = None,
trusted_networks: Optional[Any] = None,
) -> None:
async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> None:
"""Process the [homeassistant] section from the configuration.
This method is a coroutine.
@ -486,14 +481,6 @@ async def async_process_ha_core_config(
if auth_conf is None:
auth_conf = [{"type": "homeassistant"}]
if api_password:
auth_conf.append(
{"type": "legacy_api_password", "api_password": api_password}
)
if trusted_networks:
auth_conf.append(
{"type": "trusted_networks", "trusted_networks": trusted_networks}
)
mfa_conf = config.get(
CONF_AUTH_MFA_MODULES,

View file

@ -451,7 +451,6 @@ HTTP_SERVICE_UNAVAILABLE = 503
HTTP_BASIC_AUTHENTICATION = "basic"
HTTP_DIGEST_AUTHENTICATION = "digest"
HTTP_HEADER_HA_AUTH = "X-HA-access"
HTTP_HEADER_X_REQUESTED_WITH = "X-Requested-With"
CONTENT_TYPE_JSON = "application/json"

View file

@ -30,7 +30,7 @@ async def async_setup_auth(
hass, provider_configs, module_configs
)
ensure_auth_manager_loaded(hass.auth)
await async_setup_component(hass, "auth", {"http": {"api_password": "bla"}})
await async_setup_component(hass, "auth", {})
if setup_api:
await async_setup_component(hass, "api", {})
return await aiohttp_client(hass.http.app)

View file

@ -103,7 +103,7 @@ def test_auth_code_store_expiration():
async def test_ws_current_user(hass, hass_ws_client, hass_access_token):
"""Test the current user command with homeassistant creds."""
assert await async_setup_component(hass, "auth", {"http": {"api_password": "bla"}})
assert await async_setup_component(hass, "auth", {})
refresh_token = await hass.auth.async_validate_access_token(hass_access_token)
user = refresh_token.user

View file

@ -40,7 +40,9 @@ def hass_ws_client(aiohttp_client, hass_access_token):
assert auth_resp["type"] == TYPE_AUTH_REQUIRED
if access_token is None:
await websocket.send_json({"type": TYPE_AUTH, "api_password": "bla"})
await websocket.send_json(
{"type": TYPE_AUTH, "access_token": "incorrect"}
)
else:
await websocket.send_json(
{"type": TYPE_AUTH, "access_token": access_token}

View file

@ -3,7 +3,7 @@
import asyncio
import json
from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION
from aiohttp.hdrs import AUTHORIZATION
import pytest
from homeassistant import core, const, setup
@ -24,11 +24,6 @@ from . import DEMO_DEVICES
API_PASSWORD = "test1234"
HA_HEADERS = {
const.HTTP_HEADER_HA_AUTH: API_PASSWORD,
CONTENT_TYPE: const.CONTENT_TYPE_JSON,
}
PROJECT_ID = "hasstest-1234"
CLIENT_ID = "helloworld"
ACCESS_TOKEN = "superdoublesecret"

View file

@ -1,4 +1,3 @@
"""Tests for Hassio component."""
API_PASSWORD = "pass1234"
HASSIO_TOKEN = "123456"

View file

@ -9,7 +9,7 @@ from homeassistant.setup import async_setup_component
from homeassistant.components.hassio.handler import HassIO, HassioAPIError
from tests.common import mock_coro
from . import API_PASSWORD, HASSIO_TOKEN
from . import HASSIO_TOKEN
@pytest.fixture
@ -39,23 +39,19 @@ def hassio_stubs(hassio_env, hass, hass_client, aioclient_mock):
side_effect=HassioAPIError(),
):
hass.state = CoreState.starting
hass.loop.run_until_complete(
async_setup_component(
hass, "hassio", {"http": {"api_password": API_PASSWORD}}
)
)
hass.loop.run_until_complete(async_setup_component(hass, "hassio", {}))
@pytest.fixture
def hassio_client(hassio_stubs, hass, hass_client):
"""Return a Hass.io HTTP client."""
yield hass.loop.run_until_complete(hass_client())
return hass.loop.run_until_complete(hass_client())
@pytest.fixture
def hassio_noauth_client(hassio_stubs, hass, aiohttp_client):
"""Return a Hass.io HTTP client without auth."""
yield hass.loop.run_until_complete(aiohttp_client(hass.http.app))
return hass.loop.run_until_complete(aiohttp_client(hass.http.app))
@pytest.fixture

View file

@ -4,10 +4,8 @@ from unittest.mock import patch, Mock
import pytest
from homeassistant.setup import async_setup_component
from homeassistant.const import HTTP_HEADER_HA_AUTH
from tests.common import mock_coro
from . import API_PASSWORD
@pytest.fixture(autouse=True)
@ -53,9 +51,7 @@ async def test_hassio_addon_panel_startup(hass, aioclient_mock, hassio_env):
"homeassistant.components.hassio.addon_panel._register_panel",
Mock(return_value=mock_coro()),
) as mock_panel:
await async_setup_component(
hass, "hassio", {"http": {"api_password": API_PASSWORD}}
)
await async_setup_component(hass, "hassio", {})
await hass.async_block_till_done()
assert aioclient_mock.call_count == 3
@ -98,9 +94,7 @@ async def test_hassio_addon_panel_api(hass, aioclient_mock, hassio_env, hass_cli
"homeassistant.components.hassio.addon_panel._register_panel",
Mock(return_value=mock_coro()),
) as mock_panel:
await async_setup_component(
hass, "hassio", {"http": {"api_password": API_PASSWORD}}
)
await async_setup_component(hass, "hassio", {})
await hass.async_block_till_done()
assert aioclient_mock.call_count == 3
@ -113,14 +107,10 @@ async def test_hassio_addon_panel_api(hass, aioclient_mock, hassio_env, hass_cli
hass_client = await hass_client()
resp = await hass_client.post(
"/api/hassio_push/panel/test2", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}
)
resp = await hass_client.post("/api/hassio_push/panel/test2")
assert resp.status == 400
resp = await hass_client.post(
"/api/hassio_push/panel/test1", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}
)
resp = await hass_client.post("/api/hassio_push/panel/test1")
assert resp.status == 200
assert mock_panel.call_count == 2

View file

@ -1,11 +1,9 @@
"""The tests for the hassio component."""
from unittest.mock import patch, Mock
from homeassistant.const import HTTP_HEADER_HA_AUTH
from homeassistant.exceptions import HomeAssistantError
from tests.common import mock_coro
from . import API_PASSWORD
async def test_login_success(hass, hassio_client):
@ -18,7 +16,6 @@ async def test_login_success(hass, hassio_client):
resp = await hassio_client.post(
"/api/hassio_auth",
json={"username": "test", "password": "123456", "addon": "samba"},
headers={HTTP_HEADER_HA_AUTH: API_PASSWORD},
)
# Check we got right response
@ -36,7 +33,6 @@ async def test_login_error(hass, hassio_client):
resp = await hassio_client.post(
"/api/hassio_auth",
json={"username": "test", "password": "123456", "addon": "samba"},
headers={HTTP_HEADER_HA_AUTH: API_PASSWORD},
)
# Check we got right response
@ -51,9 +47,7 @@ async def test_login_no_data(hass, hassio_client):
"HassAuthProvider.async_validate_login",
Mock(side_effect=HomeAssistantError()),
) as mock_login:
resp = await hassio_client.post(
"/api/hassio_auth", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}
)
resp = await hassio_client.post("/api/hassio_auth")
# Check we got right response
assert resp.status == 400
@ -68,9 +62,7 @@ async def test_login_no_username(hass, hassio_client):
Mock(side_effect=HomeAssistantError()),
) as mock_login:
resp = await hassio_client.post(
"/api/hassio_auth",
json={"password": "123456", "addon": "samba"},
headers={HTTP_HEADER_HA_AUTH: API_PASSWORD},
"/api/hassio_auth", json={"password": "123456", "addon": "samba"}
)
# Check we got right response
@ -93,7 +85,6 @@ async def test_login_success_extra(hass, hassio_client):
"addon": "samba",
"path": "/share",
},
headers={HTTP_HEADER_HA_AUTH: API_PASSWORD},
)
# Check we got right response

View file

@ -3,10 +3,9 @@ from unittest.mock import patch, Mock
from homeassistant.setup import async_setup_component
from homeassistant.components.hassio.handler import HassioAPIError
from homeassistant.const import EVENT_HOMEASSISTANT_START, HTTP_HEADER_HA_AUTH
from homeassistant.const import EVENT_HOMEASSISTANT_START
from tests.common import mock_coro
from . import API_PASSWORD
async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client):
@ -101,9 +100,7 @@ async def test_hassio_discovery_startup_done(hass, aioclient_mock, hassio_client
Mock(return_value=mock_coro({"type": "abort"})),
) as mock_mqtt:
await hass.async_start()
await async_setup_component(
hass, "hassio", {"http": {"api_password": API_PASSWORD}}
)
await async_setup_component(hass, "hassio", {})
await hass.async_block_till_done()
assert aioclient_mock.call_count == 2
@ -151,7 +148,6 @@ async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client):
) as mock_mqtt:
resp = await hassio_client.post(
"/api/hassio_push/discovery/testuuid",
headers={HTTP_HEADER_HA_AUTH: API_PASSWORD},
json={"addon": "mosquitto", "service": "mqtt", "uuid": "testuuid"},
)
await hass.async_block_till_done()

View file

@ -4,19 +4,13 @@ from unittest.mock import patch
import pytest
from homeassistant.const import HTTP_HEADER_HA_AUTH
from . import API_PASSWORD
@asyncio.coroutine
def test_forward_request(hassio_client, aioclient_mock):
"""Test fetching normal path."""
aioclient_mock.post("http://127.0.0.1/beer", text="response")
resp = yield from hassio_client.post(
"/api/hassio/beer", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}
)
resp = yield from hassio_client.post("/api/hassio/beer")
# Check we got right response
assert resp.status == 200
@ -87,9 +81,7 @@ def test_forward_log_request(hassio_client, aioclient_mock):
"""Test fetching normal log path doesn't remove ANSI color escape codes."""
aioclient_mock.get("http://127.0.0.1/beer/logs", text="\033[32mresponse\033[0m")
resp = yield from hassio_client.get(
"/api/hassio/beer/logs", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}
)
resp = yield from hassio_client.get("/api/hassio/beer/logs")
# Check we got right response
assert resp.status == 200
@ -107,9 +99,7 @@ def test_bad_gateway_when_cannot_find_supervisor(hassio_client):
"homeassistant.components.hassio.http.async_timeout.timeout",
side_effect=asyncio.TimeoutError,
):
resp = yield from hassio_client.get(
"/api/hassio/addons/test/info", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}
)
resp = yield from hassio_client.get("/api/hassio/addons/test/info")
assert resp.status == 502

View file

@ -6,6 +6,10 @@ from aiohttp import web
from homeassistant.components.http.const import KEY_REAL_IP
# Relic from the past. Kept here so we can run negative tests.
HTTP_HEADER_HA_AUTH = "X-HA-access"
def mock_real_ip(app):
"""Inject middleware to mock real IP.

View file

@ -11,10 +11,8 @@ from homeassistant.auth.providers import trusted_networks
from homeassistant.components.http.auth import setup_auth, async_sign_path
from homeassistant.components.http.const import KEY_AUTHENTICATED
from homeassistant.components.http.real_ip import setup_real_ip
from homeassistant.const import HTTP_HEADER_HA_AUTH
from homeassistant.setup import async_setup_component
from . import mock_real_ip
from . import mock_real_ip, HTTP_HEADER_HA_AUTH
API_PASSWORD = "test-password"
@ -87,29 +85,29 @@ async def test_auth_middleware_loaded_by_default(hass):
assert len(mock_setup.mock_calls) == 1
async def test_access_with_password_in_header(app, aiohttp_client, legacy_auth, hass):
async def test_cant_access_with_password_in_header(
app, aiohttp_client, legacy_auth, hass
):
"""Test access with password in header."""
setup_auth(hass, app)
client = await aiohttp_client(app)
user = await get_legacy_user(hass.auth)
req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
assert req.status == 200
assert await req.json() == {"user_id": user.id}
assert req.status == 401
req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: "wrong-pass"})
assert req.status == 401
async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth, hass):
async def test_cant_access_with_password_in_query(
app, aiohttp_client, legacy_auth, hass
):
"""Test access with password in URL."""
setup_auth(hass, app)
client = await aiohttp_client(app)
user = await get_legacy_user(hass.auth)
resp = await client.get("/", params={"api_password": API_PASSWORD})
assert resp.status == 200
assert await resp.json() == {"user_id": user.id}
assert resp.status == 401
resp = await client.get("/")
assert resp.status == 401
@ -118,15 +116,13 @@ async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth, h
assert resp.status == 401
async def test_basic_auth_works(app, aiohttp_client, hass, legacy_auth):
async def test_basic_auth_does_not_work(app, aiohttp_client, hass, legacy_auth):
"""Test access with basic authentication."""
setup_auth(hass, app)
client = await aiohttp_client(app)
user = await get_legacy_user(hass.auth)
req = await client.get("/", auth=BasicAuth("homeassistant", API_PASSWORD))
assert req.status == 200
assert await req.json() == {"user_id": user.id}
assert req.status == 401
req = await client.get("/", auth=BasicAuth("wrong_username", API_PASSWORD))
assert req.status == 401
@ -138,7 +134,7 @@ async def test_basic_auth_works(app, aiohttp_client, hass, legacy_auth):
assert req.status == 401
async def test_access_with_trusted_ip(
async def test_cannot_access_with_trusted_ip(
hass, app2, trusted_networks_auth, aiohttp_client, hass_owner_user
):
"""Test access with an untrusted ip address."""
@ -155,8 +151,7 @@ async def test_access_with_trusted_ip(
for remote_addr in TRUSTED_ADDRESSES:
set_mock_ip(remote_addr)
resp = await client.get("/")
assert resp.status == 200, "{} should be trusted".format(remote_addr)
assert await resp.json() == {"user_id": hass_owner_user.id}
assert resp.status == 401, "{} shouldn't be trusted".format(remote_addr)
async def test_auth_active_access_with_access_token_in_header(
@ -209,29 +204,24 @@ async def test_auth_active_access_with_trusted_ip(
for remote_addr in TRUSTED_ADDRESSES:
set_mock_ip(remote_addr)
resp = await client.get("/")
assert resp.status == 200, "{} should be trusted".format(remote_addr)
assert await resp.json() == {"user_id": hass_owner_user.id}
assert resp.status == 401, "{} shouldn't be trusted".format(remote_addr)
async def test_auth_legacy_support_api_password_access(
async def test_auth_legacy_support_api_password_cannot_access(
app, aiohttp_client, legacy_auth, hass
):
"""Test access using api_password if auth.support_legacy."""
setup_auth(hass, app)
client = await aiohttp_client(app)
user = await get_legacy_user(hass.auth)
req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
assert req.status == 200
assert await req.json() == {"user_id": user.id}
assert req.status == 401
resp = await client.get("/", params={"api_password": API_PASSWORD})
assert resp.status == 200
assert await resp.json() == {"user_id": user.id}
assert resp.status == 401
req = await client.get("/", auth=BasicAuth("homeassistant", API_PASSWORD))
assert req.status == 200
assert await req.json() == {"user_id": user.id}
assert req.status == 401
async def test_auth_access_signed_path(hass, app, aiohttp_client, hass_access_token):

View file

@ -148,6 +148,8 @@ async def test_failed_login_attempts_counter(hass, aiohttp_client):
assert resp.status == 200
assert app[KEY_FAILED_LOGIN_ATTEMPTS][remote_ip] == 2
# This used to check that with trusted networks we reset login attempts
# We no longer support trusted networks.
resp = await client.get("/auth_true")
assert resp.status == 200
assert remote_ip not in app[KEY_FAILED_LOGIN_ATTEMPTS]
assert app[KEY_FAILED_LOGIN_ATTEMPTS][remote_ip] == 2

View file

@ -13,11 +13,12 @@ from aiohttp.hdrs import (
)
import pytest
from homeassistant.const import HTTP_HEADER_HA_AUTH
from homeassistant.setup import async_setup_component
from homeassistant.components.http.cors import setup_cors
from homeassistant.components.http.view import HomeAssistantView
from . import HTTP_HEADER_HA_AUTH
TRUSTED_ORIGIN = "https://home-assistant.io"
@ -91,13 +92,13 @@ async def test_cors_preflight_allowed(client):
headers={
ORIGIN: TRUSTED_ORIGIN,
ACCESS_CONTROL_REQUEST_METHOD: "GET",
ACCESS_CONTROL_REQUEST_HEADERS: "x-ha-access",
ACCESS_CONTROL_REQUEST_HEADERS: "x-requested-with",
},
)
assert req.status == 200
assert req.headers[ACCESS_CONTROL_ALLOW_ORIGIN] == TRUSTED_ORIGIN
assert req.headers[ACCESS_CONTROL_ALLOW_HEADERS] == HTTP_HEADER_HA_AUTH.upper()
assert req.headers[ACCESS_CONTROL_ALLOW_HEADERS] == "X-REQUESTED-WITH"
async def test_cors_middleware_with_cors_allowed_view(hass):

View file

@ -133,7 +133,7 @@ async def test_not_log_password(hass, aiohttp_client, caplog, legacy_auth):
resp = await client.get("/api/", params={"api_password": "test-password"})
assert resp.status == 200
assert resp.status == 401
logs = caplog.text
# Ensure we don't log API passwords

View file

@ -57,12 +57,7 @@ class TestMQTT:
self.hass.config.api = MagicMock(api_password="api_password")
assert setup_component(
self.hass,
mqtt.DOMAIN,
{
"http": {"api_password": "http_secret"},
mqtt.DOMAIN: {CONF_PASSWORD: password},
},
self.hass, mqtt.DOMAIN, {mqtt.DOMAIN: {CONF_PASSWORD: password}}
)
self.hass.block_till_done()
assert mock_mqtt.called

View file

@ -1,2 +1 @@
"""Tests for the websocket API."""
API_PASSWORD = "test-password"

View file

@ -5,8 +5,6 @@ from homeassistant.setup import async_setup_component
from homeassistant.components.websocket_api.http import URL
from homeassistant.components.websocket_api.auth import TYPE_AUTH_REQUIRED
from . import API_PASSWORD
@pytest.fixture
def websocket_client(hass, hass_ws_client, hass_access_token):
@ -17,11 +15,7 @@ def websocket_client(hass, hass_ws_client, hass_access_token):
@pytest.fixture
def no_auth_websocket_client(hass, loop, aiohttp_client):
"""Websocket connection that requires authentication."""
assert loop.run_until_complete(
async_setup_component(
hass, "websocket_api", {"http": {"api_password": API_PASSWORD}}
)
)
assert loop.run_until_complete(async_setup_component(hass, "websocket_api", {}))
client = loop.run_until_complete(aiohttp_client(hass.http.app))
ws = loop.run_until_complete(client.ws_connect(URL))

View file

@ -17,21 +17,10 @@ from homeassistant.setup import async_setup_component
from tests.common import mock_coro
from . import API_PASSWORD
async def test_auth_via_msg(no_auth_websocket_client, legacy_auth):
"""Test authenticating."""
await no_auth_websocket_client.send_json(
{"type": TYPE_AUTH, "api_password": API_PASSWORD}
)
msg = await no_auth_websocket_client.receive_json()
assert msg["type"] == TYPE_AUTH_OK
async def test_auth_events(hass, no_auth_websocket_client, legacy_auth):
async def test_auth_events(
hass, no_auth_websocket_client, legacy_auth, hass_access_token
):
"""Test authenticating."""
connected_evt = []
hass.helpers.dispatcher.async_dispatcher_connect(
@ -42,7 +31,7 @@ async def test_auth_events(hass, no_auth_websocket_client, legacy_auth):
SIGNAL_WEBSOCKET_DISCONNECTED, lambda: disconnected_evt.append(1)
)
await test_auth_via_msg(no_auth_websocket_client, legacy_auth)
await test_auth_active_with_token(hass, no_auth_websocket_client, hass_access_token)
assert len(connected_evt) == 1
assert not disconnected_evt
@ -60,7 +49,7 @@ async def test_auth_via_msg_incorrect_pass(no_auth_websocket_client):
return_value=mock_coro(),
) as mock_process_wrong_login:
await no_auth_websocket_client.send_json(
{"type": TYPE_AUTH, "api_password": API_PASSWORD + "wrong"}
{"type": TYPE_AUTH, "api_password": "wrong"}
)
msg = await no_auth_websocket_client.receive_json()
@ -110,31 +99,25 @@ async def test_pre_auth_only_auth_allowed(no_auth_websocket_client):
assert msg["message"].startswith("Auth message incorrectly formatted")
async def test_auth_active_with_token(hass, aiohttp_client, hass_access_token):
async def test_auth_active_with_token(
hass, no_auth_websocket_client, hass_access_token
):
"""Test authenticating with a token."""
assert await async_setup_component(
hass, "websocket_api", {"http": {"api_password": API_PASSWORD}}
assert await async_setup_component(hass, "websocket_api", {})
await no_auth_websocket_client.send_json(
{"type": TYPE_AUTH, "access_token": hass_access_token}
)
client = await aiohttp_client(hass.http.app)
async with client.ws_connect(URL) as ws:
auth_msg = await ws.receive_json()
assert auth_msg["type"] == TYPE_AUTH_REQUIRED
await ws.send_json({"type": TYPE_AUTH, "access_token": hass_access_token})
auth_msg = await ws.receive_json()
assert auth_msg["type"] == TYPE_AUTH_OK
auth_msg = await no_auth_websocket_client.receive_json()
assert auth_msg["type"] == TYPE_AUTH_OK
async def test_auth_active_user_inactive(hass, aiohttp_client, hass_access_token):
"""Test authenticating with a token."""
refresh_token = await hass.auth.async_validate_access_token(hass_access_token)
refresh_token.user.is_active = False
assert await async_setup_component(
hass, "websocket_api", {"http": {"api_password": API_PASSWORD}}
)
assert await async_setup_component(hass, "websocket_api", {})
client = await aiohttp_client(hass.http.app)
@ -150,9 +133,7 @@ async def test_auth_active_user_inactive(hass, aiohttp_client, hass_access_token
async def test_auth_active_with_password_not_allow(hass, aiohttp_client):
"""Test authenticating with a token."""
assert await async_setup_component(
hass, "websocket_api", {"http": {"api_password": API_PASSWORD}}
)
assert await async_setup_component(hass, "websocket_api", {})
client = await aiohttp_client(hass.http.app)
@ -160,7 +141,7 @@ async def test_auth_active_with_password_not_allow(hass, aiohttp_client):
auth_msg = await ws.receive_json()
assert auth_msg["type"] == TYPE_AUTH_REQUIRED
await ws.send_json({"type": TYPE_AUTH, "api_password": API_PASSWORD})
await ws.send_json({"type": TYPE_AUTH, "api_password": "some-password"})
auth_msg = await ws.receive_json()
assert auth_msg["type"] == TYPE_AUTH_INVALID
@ -168,28 +149,23 @@ async def test_auth_active_with_password_not_allow(hass, aiohttp_client):
async def test_auth_legacy_support_with_password(hass, aiohttp_client, legacy_auth):
"""Test authenticating with a token."""
assert await async_setup_component(
hass, "websocket_api", {"http": {"api_password": API_PASSWORD}}
)
assert await async_setup_component(hass, "websocket_api", {})
client = await aiohttp_client(hass.http.app)
async with client.ws_connect(URL) as ws:
with patch("homeassistant.auth.AuthManager.support_legacy", return_value=True):
auth_msg = await ws.receive_json()
assert auth_msg["type"] == TYPE_AUTH_REQUIRED
auth_msg = await ws.receive_json()
assert auth_msg["type"] == TYPE_AUTH_REQUIRED
await ws.send_json({"type": TYPE_AUTH, "api_password": API_PASSWORD})
await ws.send_json({"type": TYPE_AUTH, "api_password": "some-password"})
auth_msg = await ws.receive_json()
assert auth_msg["type"] == TYPE_AUTH_OK
auth_msg = await ws.receive_json()
assert auth_msg["type"] == TYPE_AUTH_INVALID
async def test_auth_with_invalid_token(hass, aiohttp_client):
"""Test authenticating with a token."""
assert await async_setup_component(
hass, "websocket_api", {"http": {"api_password": API_PASSWORD}}
)
assert await async_setup_component(hass, "websocket_api", {})
client = await aiohttp_client(hass.http.app)

View file

@ -14,8 +14,6 @@ from homeassistant.setup import async_setup_component
from tests.common import async_mock_service
from . import API_PASSWORD
async def test_call_service(hass, websocket_client):
"""Test call service command."""
@ -250,9 +248,7 @@ async def test_ping(websocket_client):
async def test_call_service_context_with_user(hass, aiohttp_client, hass_access_token):
"""Test that the user is set in the service call context."""
assert await async_setup_component(
hass, "websocket_api", {"http": {"api_password": API_PASSWORD}}
)
assert await async_setup_component(hass, "websocket_api", {})
calls = async_mock_service(hass, "domain_test", "test_service")
client = await aiohttp_client(hass.http.app)

View file

@ -3,10 +3,12 @@
from homeassistant.bootstrap import async_setup_component
from tests.common import assert_setup_component
from .test_auth import test_auth_via_msg
from .test_auth import test_auth_active_with_token
async def test_websocket_api(hass, no_auth_websocket_client, legacy_auth):
async def test_websocket_api(
hass, no_auth_websocket_client, hass_access_token, legacy_auth
):
"""Test API streams."""
with assert_setup_component(1):
await async_setup_component(
@ -16,7 +18,7 @@ async def test_websocket_api(hass, no_auth_websocket_client, legacy_auth):
state = hass.states.get("sensor.connected_clients")
assert state.state == "0"
await test_auth_via_msg(no_auth_websocket_client, legacy_auth)
await test_auth_active_with_token(hass, no_auth_websocket_client, hass_access_token)
state = hass.states.get("sensor.connected_clients")
assert state.state == "1"

View file

@ -92,8 +92,8 @@ def test_secrets(isfile_patch, loop):
files = {
get_test_config_dir(YAML_CONFIG_FILE): BASE_CONFIG
+ ("http:\n" " api_password: !secret http_pw"),
secrets_path: ("logger: debug\n" "http_pw: abc123"),
+ ("http:\n" " cors_allowed_origins: !secret http_pw"),
secrets_path: ("logger: debug\n" "http_pw: http://google.com"),
}
with patch_yaml_files(files):
@ -103,17 +103,15 @@ def test_secrets(isfile_patch, loop):
assert res["except"] == {}
assert res["components"].keys() == {"homeassistant", "http"}
assert res["components"]["http"] == {
"api_password": "abc123",
"cors_allowed_origins": ["https://cast.home-assistant.io"],
"cors_allowed_origins": ["http://google.com"],
"ip_ban_enabled": True,
"login_attempts_threshold": -1,
"server_host": "0.0.0.0",
"server_port": 8123,
"trusted_networks": [],
"ssl_profile": "modern",
}
assert res["secret_cache"] == {secrets_path: {"http_pw": "abc123"}}
assert res["secrets"] == {"http_pw": "abc123"}
assert res["secret_cache"] == {secrets_path: {"http_pw": "http://google.com"}}
assert res["secrets"] == {"http_pw": "http://google.com"}
assert normalize_yaml_files(res) == [
".../configuration.yaml",
".../secrets.yaml",

View file

@ -5,7 +5,6 @@ import copy
import os
import unittest.mock as mock
from collections import OrderedDict
from ipaddress import ip_network
import asynctest
import pytest
@ -876,48 +875,6 @@ async def test_auth_provider_config_default(hass):
assert hass.auth.auth_mfa_modules[0].id == "totp"
async def test_auth_provider_config_default_api_password(hass):
"""Test loading default auth provider config with api password."""
core_config = {
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
"time_zone": "GMT",
}
if hasattr(hass, "auth"):
del hass.auth
await config_util.async_process_ha_core_config(hass, core_config, "pass")
assert len(hass.auth.auth_providers) == 2
assert hass.auth.auth_providers[0].type == "homeassistant"
assert hass.auth.auth_providers[1].type == "legacy_api_password"
assert hass.auth.auth_providers[1].api_password == "pass"
async def test_auth_provider_config_default_trusted_networks(hass):
"""Test loading default auth provider config with trusted networks."""
core_config = {
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
"time_zone": "GMT",
}
if hasattr(hass, "auth"):
del hass.auth
await config_util.async_process_ha_core_config(
hass, core_config, trusted_networks=["192.168.0.1"]
)
assert len(hass.auth.auth_providers) == 2
assert hass.auth.auth_providers[0].type == "homeassistant"
assert hass.auth.auth_providers[1].type == "trusted_networks"
assert hass.auth.auth_providers[1].trusted_networks[0] == ip_network("192.168.0.1")
async def test_disallowed_auth_provider_config(hass):
"""Test loading insecure example auth provider is disallowed."""
core_config = {