Deprecate http.api_password (#21884)
* Deprecated http.api_password * Deprecated ApiConfig.api_password GitHub Drafted PR would trigger CI after changed it to normal PR. I have to commit a comment change to trigger it * Trigger CI * Adjust if- elif chain in auth middleware
This commit is contained in:
parent
7ec7e51f70
commit
fe1840f901
27 changed files with 304 additions and 324 deletions
|
@ -100,9 +100,21 @@ class AuthManager:
|
||||||
"""Return a list of available auth modules."""
|
"""Return a list of available auth modules."""
|
||||||
return list(self._mfa_modules.values())
|
return list(self._mfa_modules.values())
|
||||||
|
|
||||||
|
def get_auth_provider(self, provider_type: str, provider_id: str) \
|
||||||
|
-> Optional[AuthProvider]:
|
||||||
|
"""Return an auth provider, None if not found."""
|
||||||
|
return self._providers.get((provider_type, provider_id))
|
||||||
|
|
||||||
|
def get_auth_providers(self, provider_type: str) \
|
||||||
|
-> List[AuthProvider]:
|
||||||
|
"""Return a List of auth provider of one type, Empty if not found."""
|
||||||
|
return [provider
|
||||||
|
for (p_type, _), provider in self._providers.items()
|
||||||
|
if p_type == provider_type]
|
||||||
|
|
||||||
def get_auth_mfa_module(self, module_id: str) \
|
def get_auth_mfa_module(self, module_id: str) \
|
||||||
-> Optional[MultiFactorAuthModule]:
|
-> Optional[MultiFactorAuthModule]:
|
||||||
"""Return an multi-factor auth module, None if not found."""
|
"""Return a multi-factor auth module, None if not found."""
|
||||||
return self._mfa_modules.get(module_id)
|
return self._mfa_modules.get(module_id)
|
||||||
|
|
||||||
async def async_get_users(self) -> List[models.User]:
|
async def async_get_users(self) -> List[models.User]:
|
||||||
|
@ -113,6 +125,11 @@ class AuthManager:
|
||||||
"""Retrieve a user."""
|
"""Retrieve a user."""
|
||||||
return await self._store.async_get_user(user_id)
|
return await self._store.async_get_user(user_id)
|
||||||
|
|
||||||
|
async def async_get_owner(self) -> Optional[models.User]:
|
||||||
|
"""Retrieve the owner."""
|
||||||
|
users = await self.async_get_users()
|
||||||
|
return next((user for user in users if user.is_owner), None)
|
||||||
|
|
||||||
async def async_get_group(self, group_id: str) -> Optional[models.Group]:
|
async def async_get_group(self, group_id: str) -> Optional[models.Group]:
|
||||||
"""Retrieve all groups."""
|
"""Retrieve all groups."""
|
||||||
return await self._store.async_get_group(group_id)
|
return await self._store.async_get_group(group_id)
|
||||||
|
|
|
@ -4,27 +4,23 @@ Support Legacy API password auth provider.
|
||||||
It will be removed when auth system production ready
|
It will be removed when auth system production ready
|
||||||
"""
|
"""
|
||||||
import hmac
|
import hmac
|
||||||
from typing import Any, Dict, Optional, cast, TYPE_CHECKING
|
from typing import Any, Dict, Optional, cast
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
||||||
from .. import AuthManager
|
from .. import AuthManager
|
||||||
from ..models import Credentials, UserMeta, User
|
from ..models import Credentials, UserMeta, User
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
AUTH_PROVIDER_TYPE = 'legacy_api_password'
|
||||||
from homeassistant.components.http import HomeAssistantHTTP # noqa: F401
|
CONF_API_PASSWORD = 'api_password'
|
||||||
|
|
||||||
|
|
||||||
USER_SCHEMA = vol.Schema({
|
|
||||||
vol.Required('username'): str,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
|
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_API_PASSWORD): cv.string,
|
||||||
}, extra=vol.PREVENT_EXTRA)
|
}, extra=vol.PREVENT_EXTRA)
|
||||||
|
|
||||||
LEGACY_USER_NAME = 'Legacy API password user'
|
LEGACY_USER_NAME = 'Legacy API password user'
|
||||||
|
@ -34,40 +30,45 @@ class InvalidAuthError(HomeAssistantError):
|
||||||
"""Raised when submitting invalid authentication."""
|
"""Raised when submitting invalid authentication."""
|
||||||
|
|
||||||
|
|
||||||
async def async_get_user(hass: HomeAssistant) -> User:
|
async def async_validate_password(hass: HomeAssistant, password: str)\
|
||||||
"""Return the legacy API password user."""
|
-> Optional[User]:
|
||||||
|
"""Return a user if password is valid. None if not."""
|
||||||
auth = cast(AuthManager, hass.auth) # type: ignore
|
auth = cast(AuthManager, hass.auth) # type: ignore
|
||||||
found = None
|
providers = auth.get_auth_providers(AUTH_PROVIDER_TYPE)
|
||||||
|
if not providers:
|
||||||
for prv in auth.auth_providers:
|
|
||||||
if prv.type == 'legacy_api_password':
|
|
||||||
found = prv
|
|
||||||
break
|
|
||||||
|
|
||||||
if found is None:
|
|
||||||
raise ValueError('Legacy API password provider not found')
|
raise ValueError('Legacy API password provider not found')
|
||||||
|
|
||||||
return await auth.async_get_or_create_user(
|
try:
|
||||||
await found.async_get_or_create_credentials({})
|
provider = cast(LegacyApiPasswordAuthProvider, providers[0])
|
||||||
)
|
provider.async_validate_login(password)
|
||||||
|
return await auth.async_get_or_create_user(
|
||||||
|
await provider.async_get_or_create_credentials({})
|
||||||
|
)
|
||||||
|
except InvalidAuthError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@AUTH_PROVIDERS.register('legacy_api_password')
|
@AUTH_PROVIDERS.register(AUTH_PROVIDER_TYPE)
|
||||||
class LegacyApiPasswordAuthProvider(AuthProvider):
|
class LegacyApiPasswordAuthProvider(AuthProvider):
|
||||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
"""An auth provider support legacy api_password."""
|
||||||
|
|
||||||
DEFAULT_TITLE = 'Legacy API Password'
|
DEFAULT_TITLE = 'Legacy API Password'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def api_password(self) -> str:
|
||||||
|
"""Return api_password."""
|
||||||
|
return str(self.config[CONF_API_PASSWORD])
|
||||||
|
|
||||||
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
|
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
|
||||||
"""Return a flow to login."""
|
"""Return a flow to login."""
|
||||||
return LegacyLoginFlow(self)
|
return LegacyLoginFlow(self)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_validate_login(self, password: str) -> None:
|
def async_validate_login(self, password: str) -> None:
|
||||||
"""Validate a username and password."""
|
"""Validate password."""
|
||||||
hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP
|
api_password = str(self.config[CONF_API_PASSWORD])
|
||||||
|
|
||||||
if not hmac.compare_digest(hass_http.api_password.encode('utf-8'),
|
if not hmac.compare_digest(api_password.encode('utf-8'),
|
||||||
password.encode('utf-8')):
|
password.encode('utf-8')):
|
||||||
raise InvalidAuthError
|
raise InvalidAuthError
|
||||||
|
|
||||||
|
@ -99,12 +100,6 @@ class LegacyLoginFlow(LoginFlow):
|
||||||
"""Handle the step of the form."""
|
"""Handle the step of the form."""
|
||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
hass_http = getattr(self.hass, 'http', None)
|
|
||||||
if hass_http is None or not hass_http.api_password:
|
|
||||||
return self.async_abort(
|
|
||||||
reason='no_api_password_set'
|
|
||||||
)
|
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
try:
|
try:
|
||||||
cast(LegacyApiPasswordAuthProvider, self._auth_provider)\
|
cast(LegacyApiPasswordAuthProvider, self._auth_provider)\
|
||||||
|
|
|
@ -99,12 +99,12 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||||
"This may cause issues")
|
"This may cause issues")
|
||||||
|
|
||||||
core_config = config.get(core.DOMAIN, {})
|
core_config = config.get(core.DOMAIN, {})
|
||||||
has_api_password = bool(config.get('http', {}).get('api_password'))
|
api_password = config.get('http', {}).get('api_password')
|
||||||
trusted_networks = config.get('http', {}).get('trusted_networks')
|
trusted_networks = config.get('http', {}).get('trusted_networks')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await conf_util.async_process_ha_core_config(
|
await conf_util.async_process_ha_core_config(
|
||||||
hass, core_config, has_api_password, trusted_networks)
|
hass, core_config, api_password, trusted_networks)
|
||||||
except vol.Invalid as config_err:
|
except vol.Invalid as config_err:
|
||||||
conf_util.async_log_exception(
|
conf_util.async_log_exception(
|
||||||
config_err, 'homeassistant', core_config, hass)
|
config_err, 'homeassistant', core_config, hass)
|
||||||
|
|
|
@ -166,6 +166,7 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
||||||
_LOGGER.error(err)
|
_LOGGER.error(err)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# auth only processed during startup
|
||||||
await conf_util.async_process_ha_core_config(
|
await conf_util.async_process_ha_core_config(
|
||||||
hass, conf.get(ha.DOMAIN) or {})
|
hass, conf.get(ha.DOMAIN) or {})
|
||||||
|
|
||||||
|
|
|
@ -168,11 +168,11 @@ class APIDiscoveryView(HomeAssistantView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get discovery information."""
|
"""Get discovery information."""
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
needs_auth = hass.config.api.api_password is not None
|
|
||||||
return self.json({
|
return self.json({
|
||||||
ATTR_BASE_URL: hass.config.api.base_url,
|
ATTR_BASE_URL: hass.config.api.base_url,
|
||||||
ATTR_LOCATION_NAME: hass.config.location_name,
|
ATTR_LOCATION_NAME: hass.config.location_name,
|
||||||
ATTR_REQUIRES_API_PASSWORD: needs_auth,
|
# always needs authentication
|
||||||
|
ATTR_REQUIRES_API_PASSWORD: True,
|
||||||
ATTR_VERSION: __version__,
|
ATTR_VERSION: __version__,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -11,13 +11,11 @@ from datetime import timedelta
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
|
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
|
||||||
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_MODE, \
|
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_MODE
|
||||||
HTTP_HEADER_HA_AUTH
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.components.camera import async_get_still_stream
|
|
||||||
|
|
||||||
REQUIREMENTS = ['pillow==5.4.1']
|
REQUIREMENTS = ['pillow==5.4.1']
|
||||||
|
|
||||||
|
@ -209,9 +207,6 @@ class ProxyCamera(Camera):
|
||||||
or config.get(CONF_CACHE_IMAGES))
|
or config.get(CONF_CACHE_IMAGES))
|
||||||
self._last_image_time = dt_util.utc_from_timestamp(0)
|
self._last_image_time = dt_util.utc_from_timestamp(0)
|
||||||
self._last_image = None
|
self._last_image = None
|
||||||
self._headers = (
|
|
||||||
{HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password}
|
|
||||||
if self.hass.config.api.api_password is not None else None)
|
|
||||||
self._mode = config.get(CONF_MODE)
|
self._mode = config.get(CONF_MODE)
|
||||||
|
|
||||||
def camera_image(self):
|
def camera_image(self):
|
||||||
|
@ -252,7 +247,7 @@ class ProxyCamera(Camera):
|
||||||
return await self.hass.components.camera.async_get_mjpeg_stream(
|
return await self.hass.components.camera.async_get_mjpeg_stream(
|
||||||
request, self._proxied_camera)
|
request, self._proxied_camera)
|
||||||
|
|
||||||
return await async_get_still_stream(
|
return await self.hass.components.camera.async_get_still_stream(
|
||||||
request, self._async_stream_image,
|
request, self._async_stream_image,
|
||||||
self.content_type, self.frame_interval)
|
self.content_type, self.frame_interval)
|
||||||
|
|
||||||
|
|
|
@ -407,7 +407,7 @@ class IndexView(HomeAssistantView):
|
||||||
})
|
})
|
||||||
|
|
||||||
no_auth = '1'
|
no_auth = '1'
|
||||||
if hass.config.api.api_password and not request[KEY_AUTHENTICATED]:
|
if not request[KEY_AUTHENTICATED]:
|
||||||
# do not try to auto connect on load
|
# do not try to auto connect on load
|
||||||
no_auth = '0'
|
no_auth = '0'
|
||||||
|
|
||||||
|
|
|
@ -57,9 +57,9 @@ class HassIOAuth(HomeAssistantView):
|
||||||
|
|
||||||
def _get_provider(self):
|
def _get_provider(self):
|
||||||
"""Return Homeassistant auth provider."""
|
"""Return Homeassistant auth provider."""
|
||||||
for prv in self.hass.auth.auth_providers:
|
prv = self.hass.auth.get_auth_provider('homeassistant', None)
|
||||||
if prv.type == 'homeassistant':
|
if prv is not None:
|
||||||
return prv
|
return prv
|
||||||
|
|
||||||
_LOGGER.error("Can't find Home Assistant auth.")
|
_LOGGER.error("Can't find Home Assistant auth.")
|
||||||
raise HTTPNotFound()
|
raise HTTPNotFound()
|
||||||
|
|
|
@ -7,8 +7,10 @@ import aiohttp
|
||||||
import async_timeout
|
import async_timeout
|
||||||
|
|
||||||
from homeassistant.components.http import (
|
from homeassistant.components.http import (
|
||||||
CONF_API_PASSWORD, CONF_SERVER_HOST, CONF_SERVER_PORT,
|
CONF_SERVER_HOST,
|
||||||
CONF_SSL_CERTIFICATE)
|
CONF_SERVER_PORT,
|
||||||
|
CONF_SSL_CERTIFICATE,
|
||||||
|
)
|
||||||
from homeassistant.const import CONF_TIME_ZONE, SERVER_PORT
|
from homeassistant.const import CONF_TIME_ZONE, SERVER_PORT
|
||||||
|
|
||||||
from .const import X_HASSIO
|
from .const import X_HASSIO
|
||||||
|
@ -125,7 +127,6 @@ class HassIO:
|
||||||
options = {
|
options = {
|
||||||
'ssl': CONF_SSL_CERTIFICATE in http_config,
|
'ssl': CONF_SSL_CERTIFICATE in http_config,
|
||||||
'port': port,
|
'port': port,
|
||||||
'password': http_config.get(CONF_API_PASSWORD),
|
|
||||||
'watchdog': True,
|
'watchdog': True,
|
||||||
'refresh_token': refresh_token,
|
'refresh_token': refresh_token,
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,12 @@ from homeassistant.util.logging import HideSensitiveDataFilter
|
||||||
|
|
||||||
from .auth import setup_auth
|
from .auth import setup_auth
|
||||||
from .ban import setup_bans
|
from .ban import setup_bans
|
||||||
from .const import KEY_AUTHENTICATED, KEY_REAL_IP # noqa
|
from .const import ( # noqa
|
||||||
|
KEY_AUTHENTICATED,
|
||||||
|
KEY_HASS,
|
||||||
|
KEY_HASS_USER,
|
||||||
|
KEY_REAL_IP,
|
||||||
|
)
|
||||||
from .cors import setup_cors
|
from .cors import setup_cors
|
||||||
from .real_ip import setup_real_ip
|
from .real_ip import setup_real_ip
|
||||||
from .static import CACHE_HEADERS, CachingStaticResource
|
from .static import CACHE_HEADERS, CachingStaticResource
|
||||||
|
@ -66,8 +71,22 @@ def trusted_networks_deprecated(value):
|
||||||
return value
|
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 component 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({
|
HTTP_SCHEMA = vol.Schema({
|
||||||
vol.Optional(CONF_API_PASSWORD): cv.string,
|
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_HOST, default=DEFAULT_SERVER_HOST): cv.string,
|
||||||
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port,
|
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port,
|
||||||
vol.Optional(CONF_BASE_URL): cv.string,
|
vol.Optional(CONF_BASE_URL): cv.string,
|
||||||
|
@ -98,12 +117,10 @@ class ApiConfig:
|
||||||
"""Configuration settings for API server."""
|
"""Configuration settings for API server."""
|
||||||
|
|
||||||
def __init__(self, host: str, port: Optional[int] = SERVER_PORT,
|
def __init__(self, host: str, port: Optional[int] = SERVER_PORT,
|
||||||
use_ssl: bool = False,
|
use_ssl: bool = False) -> None:
|
||||||
api_password: Optional[str] = None) -> None:
|
|
||||||
"""Initialize a new API config object."""
|
"""Initialize a new API config object."""
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
self.api_password = api_password
|
|
||||||
|
|
||||||
host = host.rstrip('/')
|
host = host.rstrip('/')
|
||||||
if host.startswith(("http://", "https://")):
|
if host.startswith(("http://", "https://")):
|
||||||
|
@ -133,7 +150,6 @@ async def async_setup(hass, config):
|
||||||
cors_origins = conf[CONF_CORS_ORIGINS]
|
cors_origins = conf[CONF_CORS_ORIGINS]
|
||||||
use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False)
|
use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False)
|
||||||
trusted_proxies = conf.get(CONF_TRUSTED_PROXIES, [])
|
trusted_proxies = conf.get(CONF_TRUSTED_PROXIES, [])
|
||||||
trusted_networks = conf[CONF_TRUSTED_NETWORKS]
|
|
||||||
is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
|
is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
|
||||||
login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
|
login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
|
||||||
ssl_profile = conf[CONF_SSL_PROFILE]
|
ssl_profile = conf[CONF_SSL_PROFILE]
|
||||||
|
@ -146,14 +162,12 @@ async def async_setup(hass, config):
|
||||||
hass,
|
hass,
|
||||||
server_host=server_host,
|
server_host=server_host,
|
||||||
server_port=server_port,
|
server_port=server_port,
|
||||||
api_password=api_password,
|
|
||||||
ssl_certificate=ssl_certificate,
|
ssl_certificate=ssl_certificate,
|
||||||
ssl_peer_certificate=ssl_peer_certificate,
|
ssl_peer_certificate=ssl_peer_certificate,
|
||||||
ssl_key=ssl_key,
|
ssl_key=ssl_key,
|
||||||
cors_origins=cors_origins,
|
cors_origins=cors_origins,
|
||||||
use_x_forwarded_for=use_x_forwarded_for,
|
use_x_forwarded_for=use_x_forwarded_for,
|
||||||
trusted_proxies=trusted_proxies,
|
trusted_proxies=trusted_proxies,
|
||||||
trusted_networks=trusted_networks,
|
|
||||||
login_threshold=login_threshold,
|
login_threshold=login_threshold,
|
||||||
is_ban_enabled=is_ban_enabled,
|
is_ban_enabled=is_ban_enabled,
|
||||||
ssl_profile=ssl_profile,
|
ssl_profile=ssl_profile,
|
||||||
|
@ -183,8 +197,7 @@ async def async_setup(hass, config):
|
||||||
host = hass_util.get_local_ip()
|
host = hass_util.get_local_ip()
|
||||||
port = server_port
|
port = server_port
|
||||||
|
|
||||||
hass.config.api = ApiConfig(host, port, ssl_certificate is not None,
|
hass.config.api = ApiConfig(host, port, ssl_certificate is not None)
|
||||||
api_password)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -192,13 +205,14 @@ async def async_setup(hass, config):
|
||||||
class HomeAssistantHTTP:
|
class HomeAssistantHTTP:
|
||||||
"""HTTP server for Home Assistant."""
|
"""HTTP server for Home Assistant."""
|
||||||
|
|
||||||
def __init__(self, hass, api_password,
|
def __init__(self, hass,
|
||||||
ssl_certificate, ssl_peer_certificate,
|
ssl_certificate, ssl_peer_certificate,
|
||||||
ssl_key, server_host, server_port, cors_origins,
|
ssl_key, server_host, server_port, cors_origins,
|
||||||
use_x_forwarded_for, trusted_proxies, trusted_networks,
|
use_x_forwarded_for, trusted_proxies,
|
||||||
login_threshold, is_ban_enabled, ssl_profile):
|
login_threshold, is_ban_enabled, ssl_profile):
|
||||||
"""Initialize the HTTP Home Assistant server."""
|
"""Initialize the HTTP Home Assistant server."""
|
||||||
app = self.app = web.Application(middlewares=[])
|
app = self.app = web.Application(middlewares=[])
|
||||||
|
app[KEY_HASS] = hass
|
||||||
|
|
||||||
# This order matters
|
# This order matters
|
||||||
setup_real_ip(app, use_x_forwarded_for, trusted_proxies)
|
setup_real_ip(app, use_x_forwarded_for, trusted_proxies)
|
||||||
|
@ -206,34 +220,16 @@ class HomeAssistantHTTP:
|
||||||
if is_ban_enabled:
|
if is_ban_enabled:
|
||||||
setup_bans(hass, app, login_threshold)
|
setup_bans(hass, app, login_threshold)
|
||||||
|
|
||||||
if hass.auth.support_legacy:
|
setup_auth(hass, app)
|
||||||
_LOGGER.warning(
|
|
||||||
"legacy_api_password support has been enabled. If you don't "
|
|
||||||
"require it, remove the 'api_password' from your http config.")
|
|
||||||
|
|
||||||
for prv in hass.auth.auth_providers:
|
|
||||||
if prv.type == 'trusted_networks':
|
|
||||||
# auth_provider.trusted_networks will override
|
|
||||||
# http.trusted_networks, http.trusted_networks will be
|
|
||||||
# removed from future release
|
|
||||||
trusted_networks = prv.trusted_networks
|
|
||||||
break
|
|
||||||
|
|
||||||
setup_auth(app, trusted_networks,
|
|
||||||
api_password if hass.auth.support_legacy else None)
|
|
||||||
|
|
||||||
setup_cors(app, cors_origins)
|
setup_cors(app, cors_origins)
|
||||||
|
|
||||||
app['hass'] = hass
|
|
||||||
|
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.api_password = api_password
|
|
||||||
self.ssl_certificate = ssl_certificate
|
self.ssl_certificate = ssl_certificate
|
||||||
self.ssl_peer_certificate = ssl_peer_certificate
|
self.ssl_peer_certificate = ssl_peer_certificate
|
||||||
self.ssl_key = ssl_key
|
self.ssl_key = ssl_key
|
||||||
self.server_host = server_host
|
self.server_host = server_host
|
||||||
self.server_port = server_port
|
self.server_port = server_port
|
||||||
self.trusted_networks = trusted_networks
|
|
||||||
self.is_ban_enabled = is_ban_enabled
|
self.is_ban_enabled = is_ban_enabled
|
||||||
self.ssl_profile = ssl_profile
|
self.ssl_profile = ssl_profile
|
||||||
self._handler = None
|
self._handler = None
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
"""Authentication for HTTP component."""
|
"""Authentication for HTTP component."""
|
||||||
import base64
|
import base64
|
||||||
import hmac
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiohttp import hdrs
|
from aiohttp import hdrs
|
||||||
|
@ -13,7 +12,11 @@ from homeassistant.const import HTTP_HEADER_HA_AUTH
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import KEY_AUTHENTICATED, KEY_REAL_IP
|
from .const import (
|
||||||
|
KEY_AUTHENTICATED,
|
||||||
|
KEY_HASS_USER,
|
||||||
|
KEY_REAL_IP,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -40,10 +43,125 @@ def async_sign_path(hass, refresh_token_id, path, expiration):
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def setup_auth(app, trusted_networks, api_password):
|
def setup_auth(hass, app):
|
||||||
"""Create auth middleware for the app."""
|
"""Create auth middleware for the app."""
|
||||||
old_auth_warning = set()
|
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):
|
||||||
|
"""
|
||||||
|
Test authorization header against access token.
|
||||||
|
|
||||||
|
Basic auth_type is legacy code, should be removed with api_password.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
auth_type, auth_val = \
|
||||||
|
request.headers.get(hdrs.AUTHORIZATION).split(' ', 1)
|
||||||
|
except ValueError:
|
||||||
|
# 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
|
||||||
|
|
||||||
|
request[KEY_HASS_USER] = refresh_token.user
|
||||||
|
return True
|
||||||
|
|
||||||
|
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 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
|
||||||
|
|
||||||
|
async def async_validate_signed_request(request):
|
||||||
|
"""Validate a signed request."""
|
||||||
|
secret = hass.data.get(DATA_SIGN_SECRET)
|
||||||
|
|
||||||
|
if secret is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
signature = request.query.get(SIGN_QUERY_PARAM)
|
||||||
|
|
||||||
|
if signature is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
claims = jwt.decode(
|
||||||
|
signature,
|
||||||
|
secret,
|
||||||
|
algorithms=['HS256'],
|
||||||
|
options={'verify_iss': False}
|
||||||
|
)
|
||||||
|
except jwt.InvalidTokenError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if claims['path'] != request.path:
|
||||||
|
return False
|
||||||
|
|
||||||
|
refresh_token = await hass.auth.async_get_refresh_token(claims['iss'])
|
||||||
|
|
||||||
|
if refresh_token is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
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
|
@middleware
|
||||||
async def auth_middleware(request, handler):
|
async def auth_middleware(request, handler):
|
||||||
"""Authenticate as middleware."""
|
"""Authenticate as middleware."""
|
||||||
|
@ -53,13 +171,14 @@ def setup_auth(app, trusted_networks, api_password):
|
||||||
DATA_API_PASSWORD in request.query):
|
DATA_API_PASSWORD in request.query):
|
||||||
if request.path not in old_auth_warning:
|
if request.path not in old_auth_warning:
|
||||||
_LOGGER.log(
|
_LOGGER.log(
|
||||||
logging.INFO if api_password else logging.WARNING,
|
logging.INFO if support_legacy else logging.WARNING,
|
||||||
'You need to use a bearer token to access %s from %s',
|
'api_password is going to deprecate. You need to use a'
|
||||||
|
' bearer token to access %s from %s',
|
||||||
request.path, request[KEY_REAL_IP])
|
request.path, request[KEY_REAL_IP])
|
||||||
old_auth_warning.add(request.path)
|
old_auth_warning.add(request.path)
|
||||||
|
|
||||||
if (hdrs.AUTHORIZATION in request.headers and
|
if (hdrs.AUTHORIZATION in request.headers and
|
||||||
await async_validate_auth_header(request, api_password)):
|
await async_validate_auth_header(request)):
|
||||||
# it included both use_auth and api_password Basic auth
|
# it included both use_auth and api_password Basic auth
|
||||||
authenticated = True
|
authenticated = True
|
||||||
|
|
||||||
|
@ -69,133 +188,21 @@ def setup_auth(app, trusted_networks, api_password):
|
||||||
await async_validate_signed_request(request)):
|
await async_validate_signed_request(request)):
|
||||||
authenticated = True
|
authenticated = True
|
||||||
|
|
||||||
elif (api_password and HTTP_HEADER_HA_AUTH in request.headers and
|
elif (trusted_networks and
|
||||||
hmac.compare_digest(
|
await async_validate_trusted_networks(request)):
|
||||||
api_password.encode('utf-8'),
|
|
||||||
request.headers[HTTP_HEADER_HA_AUTH].encode('utf-8'))):
|
|
||||||
# A valid auth header has been set
|
|
||||||
authenticated = True
|
authenticated = True
|
||||||
request['hass_user'] = await legacy_api_password.async_get_user(
|
|
||||||
app['hass'])
|
|
||||||
|
|
||||||
elif (api_password and DATA_API_PASSWORD in request.query and
|
elif (support_legacy and HTTP_HEADER_HA_AUTH in request.headers and
|
||||||
hmac.compare_digest(
|
await async_validate_legacy_api_password(
|
||||||
api_password.encode('utf-8'),
|
request, request.headers[HTTP_HEADER_HA_AUTH])):
|
||||||
request.query[DATA_API_PASSWORD].encode('utf-8'))):
|
|
||||||
authenticated = True
|
authenticated = True
|
||||||
request['hass_user'] = await legacy_api_password.async_get_user(
|
|
||||||
app['hass'])
|
|
||||||
|
|
||||||
elif _is_trusted_ip(request, trusted_networks):
|
elif (support_legacy and DATA_API_PASSWORD in request.query and
|
||||||
users = await app['hass'].auth.async_get_users()
|
await async_validate_legacy_api_password(
|
||||||
for user in users:
|
request, request.query[DATA_API_PASSWORD])):
|
||||||
if user.is_owner:
|
authenticated = True
|
||||||
request['hass_user'] = user
|
|
||||||
authenticated = True
|
|
||||||
break
|
|
||||||
|
|
||||||
request[KEY_AUTHENTICATED] = authenticated
|
request[KEY_AUTHENTICATED] = authenticated
|
||||||
return await handler(request)
|
return await handler(request)
|
||||||
|
|
||||||
app.middlewares.append(auth_middleware)
|
app.middlewares.append(auth_middleware)
|
||||||
|
|
||||||
|
|
||||||
def _is_trusted_ip(request, trusted_networks):
|
|
||||||
"""Test if request is from a trusted ip."""
|
|
||||||
ip_addr = request[KEY_REAL_IP]
|
|
||||||
|
|
||||||
return any(
|
|
||||||
ip_addr in trusted_network for trusted_network
|
|
||||||
in trusted_networks)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_password(request, api_password):
|
|
||||||
"""Test if password is valid."""
|
|
||||||
return hmac.compare_digest(
|
|
||||||
api_password.encode('utf-8'),
|
|
||||||
request.app['hass'].http.api_password.encode('utf-8'))
|
|
||||||
|
|
||||||
|
|
||||||
async def async_validate_auth_header(request, api_password=None):
|
|
||||||
"""
|
|
||||||
Test authorization header against access token.
|
|
||||||
|
|
||||||
Basic auth_type is legacy code, should be removed with api_password.
|
|
||||||
"""
|
|
||||||
if hdrs.AUTHORIZATION not in request.headers:
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
auth_type, auth_val = \
|
|
||||||
request.headers.get(hdrs.AUTHORIZATION).split(' ', 1)
|
|
||||||
except ValueError:
|
|
||||||
# If no space in authorization header
|
|
||||||
return False
|
|
||||||
|
|
||||||
hass = request.app['hass']
|
|
||||||
|
|
||||||
if auth_type == 'Bearer':
|
|
||||||
refresh_token = await hass.auth.async_validate_access_token(auth_val)
|
|
||||||
if refresh_token is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
request['hass_refresh_token'] = refresh_token
|
|
||||||
request['hass_user'] = refresh_token.user
|
|
||||||
return True
|
|
||||||
|
|
||||||
if auth_type == 'Basic' and api_password is not None:
|
|
||||||
decoded = base64.b64decode(auth_val).decode('utf-8')
|
|
||||||
try:
|
|
||||||
username, password = decoded.split(':', 1)
|
|
||||||
except ValueError:
|
|
||||||
# If no ':' in decoded
|
|
||||||
return False
|
|
||||||
|
|
||||||
if username != 'homeassistant':
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not hmac.compare_digest(api_password.encode('utf-8'),
|
|
||||||
password.encode('utf-8')):
|
|
||||||
return False
|
|
||||||
|
|
||||||
request['hass_user'] = await legacy_api_password.async_get_user(hass)
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
async def async_validate_signed_request(request):
|
|
||||||
"""Validate a signed request."""
|
|
||||||
hass = request.app['hass']
|
|
||||||
secret = hass.data.get(DATA_SIGN_SECRET)
|
|
||||||
|
|
||||||
if secret is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
signature = request.query.get(SIGN_QUERY_PARAM)
|
|
||||||
|
|
||||||
if signature is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
claims = jwt.decode(
|
|
||||||
signature,
|
|
||||||
secret,
|
|
||||||
algorithms=['HS256'],
|
|
||||||
options={'verify_iss': False}
|
|
||||||
)
|
|
||||||
except jwt.InvalidTokenError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if claims['path'] != request.path:
|
|
||||||
return False
|
|
||||||
|
|
||||||
refresh_token = await hass.auth.async_get_refresh_token(claims['iss'])
|
|
||||||
|
|
||||||
if refresh_token is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
request['hass_refresh_token'] = refresh_token
|
|
||||||
request['hass_user'] = refresh_token.user
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
"""HTTP specific constants."""
|
"""HTTP specific constants."""
|
||||||
KEY_AUTHENTICATED = 'ha_authenticated'
|
KEY_AUTHENTICATED = 'ha_authenticated'
|
||||||
|
KEY_HASS = 'hass'
|
||||||
|
KEY_HASS_USER = 'hass_user'
|
||||||
KEY_REAL_IP = 'ha_real_ip'
|
KEY_REAL_IP = 'ha_real_ip'
|
||||||
|
|
|
@ -14,7 +14,7 @@ from homeassistant.const import CONTENT_TYPE_JSON
|
||||||
from homeassistant.core import Context, is_callback
|
from homeassistant.core import Context, is_callback
|
||||||
from homeassistant.helpers.json import JSONEncoder
|
from homeassistant.helpers.json import JSONEncoder
|
||||||
|
|
||||||
from .const import KEY_AUTHENTICATED, KEY_REAL_IP
|
from .const import KEY_AUTHENTICATED, KEY_REAL_IP, KEY_HASS
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ def request_handler_factory(view, handler):
|
||||||
|
|
||||||
async def handle(request):
|
async def handle(request):
|
||||||
"""Handle incoming request."""
|
"""Handle incoming request."""
|
||||||
if not request.app['hass'].is_running:
|
if not request.app[KEY_HASS].is_running:
|
||||||
return web.Response(status=503)
|
return web.Response(status=503)
|
||||||
|
|
||||||
authenticated = request.get(KEY_AUTHENTICATED, False)
|
authenticated = request.get(KEY_AUTHENTICATED, False)
|
||||||
|
|
|
@ -399,14 +399,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
||||||
conf = dict(conf)
|
conf = dict(conf)
|
||||||
|
|
||||||
if CONF_EMBEDDED in conf or CONF_BROKER not in conf:
|
if CONF_EMBEDDED in conf or CONF_BROKER not in conf:
|
||||||
if (conf.get(CONF_PASSWORD) is None and
|
|
||||||
config.get('http', {}).get('api_password') is not None):
|
|
||||||
_LOGGER.error(
|
|
||||||
"Starting from release 0.76, the embedded MQTT broker does not"
|
|
||||||
" use api_password as default password anymore. Please set"
|
|
||||||
" password configuration. See https://home-assistant.io/docs/"
|
|
||||||
"mqtt/broker#embedded-broker for details")
|
|
||||||
return False
|
|
||||||
|
|
||||||
broker_config = await _async_setup_server(hass, config)
|
broker_config = await _async_setup_server(hass, config)
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,12 @@
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from voluptuous.humanize import humanize_error
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
from homeassistant.const import __version__
|
|
||||||
from homeassistant.components.http.auth import validate_password
|
|
||||||
from homeassistant.components.http.ban import process_wrong_login, \
|
|
||||||
process_success_login
|
|
||||||
from homeassistant.auth.providers import legacy_api_password
|
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__
|
||||||
|
|
||||||
from .connection import ActiveConnection
|
from .connection import ActiveConnection
|
||||||
from .error import Disconnect
|
from .error import Disconnect
|
||||||
|
@ -80,9 +81,15 @@ class AuthPhase:
|
||||||
refresh_token.user, refresh_token)
|
refresh_token.user, refresh_token)
|
||||||
|
|
||||||
elif self._hass.auth.support_legacy and 'api_password' in msg:
|
elif self._hass.auth.support_legacy and 'api_password' in msg:
|
||||||
self._logger.debug("Received api_password")
|
self._logger.info(
|
||||||
if validate_password(self._request, msg['api_password']):
|
"Received api_password, it is going to deprecate, please use"
|
||||||
user = await legacy_api_password.async_get_user(self._hass)
|
" 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)
|
return await self._async_finish_auth(user, None)
|
||||||
|
|
||||||
self._send_message(auth_invalid_message(
|
self._send_message(auth_invalid_message(
|
||||||
|
|
|
@ -30,11 +30,11 @@ def setup(hass, config):
|
||||||
|
|
||||||
zeroconf_name = '{}.{}'.format(hass.config.location_name, ZEROCONF_TYPE)
|
zeroconf_name = '{}.{}'.format(hass.config.location_name, ZEROCONF_TYPE)
|
||||||
|
|
||||||
requires_api_password = hass.config.api.api_password is not None
|
|
||||||
params = {
|
params = {
|
||||||
'version': __version__,
|
'version': __version__,
|
||||||
'base_url': hass.config.api.base_url,
|
'base_url': hass.config.api.base_url,
|
||||||
'requires_api_password': requires_api_password,
|
# always needs authentication
|
||||||
|
'requires_api_password': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
host_ip = util.get_local_ip()
|
host_ip = util.get_local_ip()
|
||||||
|
|
|
@ -428,7 +428,7 @@ def _format_config_error(ex: vol.Invalid, domain: str, config: Dict) -> str:
|
||||||
|
|
||||||
async def async_process_ha_core_config(
|
async def async_process_ha_core_config(
|
||||||
hass: HomeAssistant, config: Dict,
|
hass: HomeAssistant, config: Dict,
|
||||||
has_api_password: bool = False,
|
api_password: Optional[str] = None,
|
||||||
trusted_networks: Optional[Any] = None) -> None:
|
trusted_networks: Optional[Any] = None) -> None:
|
||||||
"""Process the [homeassistant] section from the configuration.
|
"""Process the [homeassistant] section from the configuration.
|
||||||
|
|
||||||
|
@ -444,8 +444,11 @@ async def async_process_ha_core_config(
|
||||||
auth_conf = [
|
auth_conf = [
|
||||||
{'type': 'homeassistant'}
|
{'type': 'homeassistant'}
|
||||||
]
|
]
|
||||||
if has_api_password:
|
if api_password:
|
||||||
auth_conf.append({'type': 'legacy_api_password'})
|
auth_conf.append({
|
||||||
|
'type': 'legacy_api_password',
|
||||||
|
'api_password': api_password,
|
||||||
|
})
|
||||||
if trusted_networks:
|
if trusted_networks:
|
||||||
auth_conf.append({
|
auth_conf.append({
|
||||||
'type': 'trusted_networks',
|
'type': 'trusted_networks',
|
||||||
|
|
|
@ -1180,7 +1180,7 @@ class Config:
|
||||||
# List of loaded components
|
# List of loaded components
|
||||||
self.components = set() # type: set
|
self.components = set() # type: set
|
||||||
|
|
||||||
# API (HTTP) server configuration
|
# API (HTTP) server configuration, see components.http.ApiConfig
|
||||||
self.api = None # type: Optional[Any]
|
self.api = None # type: Optional[Any]
|
||||||
|
|
||||||
# Directory that holds the configuration
|
# Directory that holds the configuration
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
"""Tests for the legacy_api_password auth provider."""
|
"""Tests for the legacy_api_password auth provider."""
|
||||||
from unittest.mock import Mock
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant import auth, data_entry_flow
|
from homeassistant import auth, data_entry_flow
|
||||||
|
@ -19,6 +17,7 @@ def provider(hass, store):
|
||||||
"""Mock provider."""
|
"""Mock provider."""
|
||||||
return legacy_api_password.LegacyApiPasswordAuthProvider(hass, store, {
|
return legacy_api_password.LegacyApiPasswordAuthProvider(hass, store, {
|
||||||
'type': 'legacy_api_password',
|
'type': 'legacy_api_password',
|
||||||
|
'api_password': 'test-password',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,32 +50,13 @@ async def test_only_one_credentials(manager, provider):
|
||||||
|
|
||||||
async def test_verify_login(hass, provider):
|
async def test_verify_login(hass, provider):
|
||||||
"""Test login using legacy api password auth provider."""
|
"""Test login using legacy api password auth provider."""
|
||||||
hass.http = Mock(api_password='test-password')
|
|
||||||
provider.async_validate_login('test-password')
|
provider.async_validate_login('test-password')
|
||||||
hass.http = Mock(api_password='test-password')
|
|
||||||
with pytest.raises(legacy_api_password.InvalidAuthError):
|
with pytest.raises(legacy_api_password.InvalidAuthError):
|
||||||
provider.async_validate_login('invalid-password')
|
provider.async_validate_login('invalid-password')
|
||||||
|
|
||||||
|
|
||||||
async def test_login_flow_abort(hass, manager):
|
|
||||||
"""Test wrong config."""
|
|
||||||
for http in (
|
|
||||||
None,
|
|
||||||
Mock(api_password=None),
|
|
||||||
Mock(api_password=''),
|
|
||||||
):
|
|
||||||
hass.http = http
|
|
||||||
|
|
||||||
result = await manager.login_flow.async_init(
|
|
||||||
handler=('legacy_api_password', None)
|
|
||||||
)
|
|
||||||
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
|
|
||||||
assert result['reason'] == 'no_api_password_set'
|
|
||||||
|
|
||||||
|
|
||||||
async def test_login_flow_works(hass, manager):
|
async def test_login_flow_works(hass, manager):
|
||||||
"""Test wrong config."""
|
"""Test wrong config."""
|
||||||
hass.http = Mock(api_password='hello')
|
|
||||||
result = await manager.login_flow.async_init(
|
result = await manager.login_flow.async_init(
|
||||||
handler=('legacy_api_password', None)
|
handler=('legacy_api_password', None)
|
||||||
)
|
)
|
||||||
|
@ -94,7 +74,7 @@ async def test_login_flow_works(hass, manager):
|
||||||
result = await manager.login_flow.async_configure(
|
result = await manager.login_flow.async_configure(
|
||||||
flow_id=result['flow_id'],
|
flow_id=result['flow_id'],
|
||||||
user_input={
|
user_input={
|
||||||
'password': 'hello'
|
'password': 'test-password'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
|
|
@ -407,14 +407,10 @@ def _listen_count(hass):
|
||||||
|
|
||||||
|
|
||||||
async def test_api_error_log(hass, aiohttp_client, hass_access_token,
|
async def test_api_error_log(hass, aiohttp_client, hass_access_token,
|
||||||
hass_admin_user, legacy_auth):
|
hass_admin_user):
|
||||||
"""Test if we can fetch the error log."""
|
"""Test if we can fetch the error log."""
|
||||||
hass.data[DATA_LOGGING] = '/some/path'
|
hass.data[DATA_LOGGING] = '/some/path'
|
||||||
await async_setup_component(hass, 'api', {
|
await async_setup_component(hass, 'api', {})
|
||||||
'http': {
|
|
||||||
'api_password': 'yolo'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
client = await aiohttp_client(hass.http.app)
|
client = await aiohttp_client(hass.http.app)
|
||||||
|
|
||||||
resp = await client.get(const.URL_API_ERROR_LOG)
|
resp = await client.get(const.URL_API_ERROR_LOG)
|
||||||
|
|
|
@ -51,7 +51,6 @@ def test_setup_api_push_api_data(hass, aioclient_mock):
|
||||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||||
result = yield from async_setup_component(hass, 'hassio', {
|
result = yield from async_setup_component(hass, 'hassio', {
|
||||||
'http': {
|
'http': {
|
||||||
'api_password': "123456",
|
|
||||||
'server_port': 9999
|
'server_port': 9999
|
||||||
},
|
},
|
||||||
'hassio': {}
|
'hassio': {}
|
||||||
|
@ -60,7 +59,6 @@ def test_setup_api_push_api_data(hass, aioclient_mock):
|
||||||
|
|
||||||
assert aioclient_mock.call_count == 3
|
assert aioclient_mock.call_count == 3
|
||||||
assert not aioclient_mock.mock_calls[1][2]['ssl']
|
assert not aioclient_mock.mock_calls[1][2]['ssl']
|
||||||
assert aioclient_mock.mock_calls[1][2]['password'] == "123456"
|
|
||||||
assert aioclient_mock.mock_calls[1][2]['port'] == 9999
|
assert aioclient_mock.mock_calls[1][2]['port'] == 9999
|
||||||
assert aioclient_mock.mock_calls[1][2]['watchdog']
|
assert aioclient_mock.mock_calls[1][2]['watchdog']
|
||||||
|
|
||||||
|
@ -71,7 +69,6 @@ def test_setup_api_push_api_data_server_host(hass, aioclient_mock):
|
||||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||||
result = yield from async_setup_component(hass, 'hassio', {
|
result = yield from async_setup_component(hass, 'hassio', {
|
||||||
'http': {
|
'http': {
|
||||||
'api_password': "123456",
|
|
||||||
'server_port': 9999,
|
'server_port': 9999,
|
||||||
'server_host': "127.0.0.1"
|
'server_host': "127.0.0.1"
|
||||||
},
|
},
|
||||||
|
@ -81,7 +78,6 @@ def test_setup_api_push_api_data_server_host(hass, aioclient_mock):
|
||||||
|
|
||||||
assert aioclient_mock.call_count == 3
|
assert aioclient_mock.call_count == 3
|
||||||
assert not aioclient_mock.mock_calls[1][2]['ssl']
|
assert not aioclient_mock.mock_calls[1][2]['ssl']
|
||||||
assert aioclient_mock.mock_calls[1][2]['password'] == "123456"
|
|
||||||
assert aioclient_mock.mock_calls[1][2]['port'] == 9999
|
assert aioclient_mock.mock_calls[1][2]['port'] == 9999
|
||||||
assert not aioclient_mock.mock_calls[1][2]['watchdog']
|
assert not aioclient_mock.mock_calls[1][2]['watchdog']
|
||||||
|
|
||||||
|
@ -98,7 +94,6 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock,
|
||||||
|
|
||||||
assert aioclient_mock.call_count == 3
|
assert aioclient_mock.call_count == 3
|
||||||
assert not aioclient_mock.mock_calls[1][2]['ssl']
|
assert not aioclient_mock.mock_calls[1][2]['ssl']
|
||||||
assert aioclient_mock.mock_calls[1][2]['password'] is None
|
|
||||||
assert aioclient_mock.mock_calls[1][2]['port'] == 8123
|
assert aioclient_mock.mock_calls[1][2]['port'] == 8123
|
||||||
refresh_token = aioclient_mock.mock_calls[1][2]['refresh_token']
|
refresh_token = aioclient_mock.mock_calls[1][2]['refresh_token']
|
||||||
hassio_user = await hass.auth.async_get_user(
|
hassio_user = await hass.auth.async_get_user(
|
||||||
|
@ -159,7 +154,6 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock,
|
||||||
|
|
||||||
assert aioclient_mock.call_count == 3
|
assert aioclient_mock.call_count == 3
|
||||||
assert not aioclient_mock.mock_calls[1][2]['ssl']
|
assert not aioclient_mock.mock_calls[1][2]['ssl']
|
||||||
assert aioclient_mock.mock_calls[1][2]['password'] is None
|
|
||||||
assert aioclient_mock.mock_calls[1][2]['port'] == 8123
|
assert aioclient_mock.mock_calls[1][2]['port'] == 8123
|
||||||
assert aioclient_mock.mock_calls[1][2]['refresh_token'] == token.token
|
assert aioclient_mock.mock_calls[1][2]['refresh_token'] == token.token
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import pytest
|
||||||
from aiohttp import BasicAuth, web
|
from aiohttp import BasicAuth, web
|
||||||
from aiohttp.web_exceptions import HTTPUnauthorized
|
from aiohttp.web_exceptions import HTTPUnauthorized
|
||||||
|
|
||||||
from homeassistant.auth.providers import legacy_api_password
|
from homeassistant.auth.providers import trusted_networks
|
||||||
from homeassistant.components.http.auth import setup_auth, async_sign_path
|
from homeassistant.components.http.auth import setup_auth, async_sign_path
|
||||||
from homeassistant.components.http.const import KEY_AUTHENTICATED
|
from homeassistant.components.http.const import KEY_AUTHENTICATED
|
||||||
from homeassistant.components.http.real_ip import setup_real_ip
|
from homeassistant.components.http.real_ip import setup_real_ip
|
||||||
|
@ -16,7 +16,7 @@ from homeassistant.setup import async_setup_component
|
||||||
from . import mock_real_ip
|
from . import mock_real_ip
|
||||||
|
|
||||||
|
|
||||||
API_PASSWORD = 'test1234'
|
API_PASSWORD = 'test-password'
|
||||||
|
|
||||||
# Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases
|
# Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases
|
||||||
TRUSTED_NETWORKS = [
|
TRUSTED_NETWORKS = [
|
||||||
|
@ -35,17 +35,22 @@ async def mock_handler(request):
|
||||||
if not request[KEY_AUTHENTICATED]:
|
if not request[KEY_AUTHENTICATED]:
|
||||||
raise HTTPUnauthorized
|
raise HTTPUnauthorized
|
||||||
|
|
||||||
token = request.get('hass_refresh_token')
|
|
||||||
token_id = token.id if token else None
|
|
||||||
user = request.get('hass_user')
|
user = request.get('hass_user')
|
||||||
user_id = user.id if user else None
|
user_id = user.id if user else None
|
||||||
|
|
||||||
return web.json_response(status=200, data={
|
return web.json_response(status=200, data={
|
||||||
'refresh_token_id': token_id,
|
|
||||||
'user_id': user_id,
|
'user_id': user_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
async def get_legacy_user(auth):
|
||||||
|
"""Get the user in legacy_api_password auth provider."""
|
||||||
|
provider = auth.get_auth_provider('legacy_api_password', None)
|
||||||
|
return await auth.async_get_or_create_user(
|
||||||
|
await provider.async_get_or_create_credentials({})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def app(hass):
|
def app(hass):
|
||||||
"""Fixture to set up a web.Application."""
|
"""Fixture to set up a web.Application."""
|
||||||
|
@ -65,6 +70,19 @@ def app2(hass):
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def trusted_networks_auth(hass):
|
||||||
|
"""Load trusted networks auth provider."""
|
||||||
|
prv = trusted_networks.TrustedNetworksAuthProvider(
|
||||||
|
hass, hass.auth._store, {
|
||||||
|
'type': 'trusted_networks',
|
||||||
|
'trusted_networks': TRUSTED_NETWORKS,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
hass.auth._providers[(prv.type, prv.id)] = prv
|
||||||
|
return prv
|
||||||
|
|
||||||
|
|
||||||
async def test_auth_middleware_loaded_by_default(hass):
|
async def test_auth_middleware_loaded_by_default(hass):
|
||||||
"""Test accessing to server from banned IP when feature is off."""
|
"""Test accessing to server from banned IP when feature is off."""
|
||||||
with patch('homeassistant.components.http.setup_auth') as mock_setup:
|
with patch('homeassistant.components.http.setup_auth') as mock_setup:
|
||||||
|
@ -78,15 +96,14 @@ async def test_auth_middleware_loaded_by_default(hass):
|
||||||
async def test_access_with_password_in_header(app, aiohttp_client,
|
async def test_access_with_password_in_header(app, aiohttp_client,
|
||||||
legacy_auth, hass):
|
legacy_auth, hass):
|
||||||
"""Test access with password in header."""
|
"""Test access with password in header."""
|
||||||
setup_auth(app, [], api_password=API_PASSWORD)
|
setup_auth(hass, app)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
user = await legacy_api_password.async_get_user(hass)
|
user = await get_legacy_user(hass.auth)
|
||||||
|
|
||||||
req = await client.get(
|
req = await client.get(
|
||||||
'/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
|
'/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
|
||||||
assert req.status == 200
|
assert req.status == 200
|
||||||
assert await req.json() == {
|
assert await req.json() == {
|
||||||
'refresh_token_id': None,
|
|
||||||
'user_id': user.id,
|
'user_id': user.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,16 +115,15 @@ async def test_access_with_password_in_header(app, aiohttp_client,
|
||||||
async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth,
|
async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth,
|
||||||
hass):
|
hass):
|
||||||
"""Test access with password in URL."""
|
"""Test access with password in URL."""
|
||||||
setup_auth(app, [], api_password=API_PASSWORD)
|
setup_auth(hass, app)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
user = await legacy_api_password.async_get_user(hass)
|
user = await get_legacy_user(hass.auth)
|
||||||
|
|
||||||
resp = await client.get('/', params={
|
resp = await client.get('/', params={
|
||||||
'api_password': API_PASSWORD
|
'api_password': API_PASSWORD
|
||||||
})
|
})
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
assert await resp.json() == {
|
assert await resp.json() == {
|
||||||
'refresh_token_id': None,
|
|
||||||
'user_id': user.id,
|
'user_id': user.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,16 +138,15 @@ async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth,
|
||||||
|
|
||||||
async def test_basic_auth_works(app, aiohttp_client, hass, legacy_auth):
|
async def test_basic_auth_works(app, aiohttp_client, hass, legacy_auth):
|
||||||
"""Test access with basic authentication."""
|
"""Test access with basic authentication."""
|
||||||
setup_auth(app, [], api_password=API_PASSWORD)
|
setup_auth(hass, app)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
user = await legacy_api_password.async_get_user(hass)
|
user = await get_legacy_user(hass.auth)
|
||||||
|
|
||||||
req = await client.get(
|
req = await client.get(
|
||||||
'/',
|
'/',
|
||||||
auth=BasicAuth('homeassistant', API_PASSWORD))
|
auth=BasicAuth('homeassistant', API_PASSWORD))
|
||||||
assert req.status == 200
|
assert req.status == 200
|
||||||
assert await req.json() == {
|
assert await req.json() == {
|
||||||
'refresh_token_id': None,
|
|
||||||
'user_id': user.id,
|
'user_id': user.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,9 +168,11 @@ async def test_basic_auth_works(app, aiohttp_client, hass, legacy_auth):
|
||||||
assert req.status == 401
|
assert req.status == 401
|
||||||
|
|
||||||
|
|
||||||
async def test_access_with_trusted_ip(app2, aiohttp_client, hass_owner_user):
|
async def test_access_with_trusted_ip(hass, app2, trusted_networks_auth,
|
||||||
|
aiohttp_client,
|
||||||
|
hass_owner_user):
|
||||||
"""Test access with an untrusted ip address."""
|
"""Test access with an untrusted ip address."""
|
||||||
setup_auth(app2, TRUSTED_NETWORKS, api_password='some-pass')
|
setup_auth(hass, app2)
|
||||||
|
|
||||||
set_mock_ip = mock_real_ip(app2)
|
set_mock_ip = mock_real_ip(app2)
|
||||||
client = await aiohttp_client(app2)
|
client = await aiohttp_client(app2)
|
||||||
|
@ -172,7 +189,6 @@ async def test_access_with_trusted_ip(app2, aiohttp_client, hass_owner_user):
|
||||||
assert resp.status == 200, \
|
assert resp.status == 200, \
|
||||||
"{} should be trusted".format(remote_addr)
|
"{} should be trusted".format(remote_addr)
|
||||||
assert await resp.json() == {
|
assert await resp.json() == {
|
||||||
'refresh_token_id': None,
|
|
||||||
'user_id': hass_owner_user.id,
|
'user_id': hass_owner_user.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,7 +197,7 @@ async def test_auth_active_access_with_access_token_in_header(
|
||||||
hass, app, aiohttp_client, hass_access_token):
|
hass, app, aiohttp_client, hass_access_token):
|
||||||
"""Test access with access token in header."""
|
"""Test access with access token in header."""
|
||||||
token = hass_access_token
|
token = hass_access_token
|
||||||
setup_auth(app, [], api_password=None)
|
setup_auth(hass, app)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
refresh_token = await hass.auth.async_validate_access_token(
|
refresh_token = await hass.auth.async_validate_access_token(
|
||||||
hass_access_token)
|
hass_access_token)
|
||||||
|
@ -190,7 +206,6 @@ async def test_auth_active_access_with_access_token_in_header(
|
||||||
'/', headers={'Authorization': 'Bearer {}'.format(token)})
|
'/', headers={'Authorization': 'Bearer {}'.format(token)})
|
||||||
assert req.status == 200
|
assert req.status == 200
|
||||||
assert await req.json() == {
|
assert await req.json() == {
|
||||||
'refresh_token_id': refresh_token.id,
|
|
||||||
'user_id': refresh_token.user.id,
|
'user_id': refresh_token.user.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,7 +213,6 @@ async def test_auth_active_access_with_access_token_in_header(
|
||||||
'/', headers={'AUTHORIZATION': 'Bearer {}'.format(token)})
|
'/', headers={'AUTHORIZATION': 'Bearer {}'.format(token)})
|
||||||
assert req.status == 200
|
assert req.status == 200
|
||||||
assert await req.json() == {
|
assert await req.json() == {
|
||||||
'refresh_token_id': refresh_token.id,
|
|
||||||
'user_id': refresh_token.user.id,
|
'user_id': refresh_token.user.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,7 +220,6 @@ async def test_auth_active_access_with_access_token_in_header(
|
||||||
'/', headers={'authorization': 'Bearer {}'.format(token)})
|
'/', headers={'authorization': 'Bearer {}'.format(token)})
|
||||||
assert req.status == 200
|
assert req.status == 200
|
||||||
assert await req.json() == {
|
assert await req.json() == {
|
||||||
'refresh_token_id': refresh_token.id,
|
|
||||||
'user_id': refresh_token.user.id,
|
'user_id': refresh_token.user.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,10 +239,12 @@ async def test_auth_active_access_with_access_token_in_header(
|
||||||
assert req.status == 401
|
assert req.status == 401
|
||||||
|
|
||||||
|
|
||||||
async def test_auth_active_access_with_trusted_ip(app2, aiohttp_client,
|
async def test_auth_active_access_with_trusted_ip(hass, app2,
|
||||||
|
trusted_networks_auth,
|
||||||
|
aiohttp_client,
|
||||||
hass_owner_user):
|
hass_owner_user):
|
||||||
"""Test access with an untrusted ip address."""
|
"""Test access with an untrusted ip address."""
|
||||||
setup_auth(app2, TRUSTED_NETWORKS, None)
|
setup_auth(hass, app2)
|
||||||
|
|
||||||
set_mock_ip = mock_real_ip(app2)
|
set_mock_ip = mock_real_ip(app2)
|
||||||
client = await aiohttp_client(app2)
|
client = await aiohttp_client(app2)
|
||||||
|
@ -246,7 +261,6 @@ async def test_auth_active_access_with_trusted_ip(app2, aiohttp_client,
|
||||||
assert resp.status == 200, \
|
assert resp.status == 200, \
|
||||||
"{} should be trusted".format(remote_addr)
|
"{} should be trusted".format(remote_addr)
|
||||||
assert await resp.json() == {
|
assert await resp.json() == {
|
||||||
'refresh_token_id': None,
|
|
||||||
'user_id': hass_owner_user.id,
|
'user_id': hass_owner_user.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,15 +268,14 @@ async def test_auth_active_access_with_trusted_ip(app2, aiohttp_client,
|
||||||
async def test_auth_legacy_support_api_password_access(
|
async def test_auth_legacy_support_api_password_access(
|
||||||
app, aiohttp_client, legacy_auth, hass):
|
app, aiohttp_client, legacy_auth, hass):
|
||||||
"""Test access using api_password if auth.support_legacy."""
|
"""Test access using api_password if auth.support_legacy."""
|
||||||
setup_auth(app, [], API_PASSWORD)
|
setup_auth(hass, app)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
user = await legacy_api_password.async_get_user(hass)
|
user = await get_legacy_user(hass.auth)
|
||||||
|
|
||||||
req = await client.get(
|
req = await client.get(
|
||||||
'/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
|
'/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
|
||||||
assert req.status == 200
|
assert req.status == 200
|
||||||
assert await req.json() == {
|
assert await req.json() == {
|
||||||
'refresh_token_id': None,
|
|
||||||
'user_id': user.id,
|
'user_id': user.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,7 +284,6 @@ async def test_auth_legacy_support_api_password_access(
|
||||||
})
|
})
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
assert await resp.json() == {
|
assert await resp.json() == {
|
||||||
'refresh_token_id': None,
|
|
||||||
'user_id': user.id,
|
'user_id': user.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,7 +292,6 @@ async def test_auth_legacy_support_api_password_access(
|
||||||
auth=BasicAuth('homeassistant', API_PASSWORD))
|
auth=BasicAuth('homeassistant', API_PASSWORD))
|
||||||
assert req.status == 200
|
assert req.status == 200
|
||||||
assert await req.json() == {
|
assert await req.json() == {
|
||||||
'refresh_token_id': None,
|
|
||||||
'user_id': user.id,
|
'user_id': user.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,7 +301,7 @@ async def test_auth_access_signed_path(
|
||||||
"""Test access with signed url."""
|
"""Test access with signed url."""
|
||||||
app.router.add_post('/', mock_handler)
|
app.router.add_post('/', mock_handler)
|
||||||
app.router.add_get('/another_path', mock_handler)
|
app.router.add_get('/another_path', mock_handler)
|
||||||
setup_auth(app, [], None)
|
setup_auth(hass, app)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
refresh_token = await hass.auth.async_validate_access_token(
|
refresh_token = await hass.auth.async_validate_access_token(
|
||||||
|
@ -303,7 +314,6 @@ async def test_auth_access_signed_path(
|
||||||
req = await client.get(signed_path)
|
req = await client.get(signed_path)
|
||||||
assert req.status == 200
|
assert req.status == 200
|
||||||
data = await req.json()
|
data = await req.json()
|
||||||
assert data['refresh_token_id'] == refresh_token.id
|
|
||||||
assert data['user_id'] == refresh_token.user.id
|
assert data['user_id'] == refresh_token.user.id
|
||||||
|
|
||||||
# Use signature on other path
|
# Use signature on other path
|
||||||
|
|
|
@ -143,15 +143,13 @@ async def test_api_base_url_removes_trailing_slash(hass):
|
||||||
async def test_not_log_password(hass, aiohttp_client, caplog, legacy_auth):
|
async def test_not_log_password(hass, aiohttp_client, caplog, legacy_auth):
|
||||||
"""Test access with password doesn't get logged."""
|
"""Test access with password doesn't get logged."""
|
||||||
assert await async_setup_component(hass, 'api', {
|
assert await async_setup_component(hass, 'api', {
|
||||||
'http': {
|
'http': {}
|
||||||
http.CONF_API_PASSWORD: 'some-pass'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
client = await aiohttp_client(hass.http.app)
|
client = await aiohttp_client(hass.http.app)
|
||||||
logging.getLogger('aiohttp.access').setLevel(logging.INFO)
|
logging.getLogger('aiohttp.access').setLevel(logging.INFO)
|
||||||
|
|
||||||
resp = await client.get('/api/', params={
|
resp = await client.get('/api/', params={
|
||||||
'api_password': 'some-pass'
|
'api_password': 'test-password'
|
||||||
})
|
})
|
||||||
|
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
|
|
|
@ -19,24 +19,6 @@ class TestMQTT:
|
||||||
"""Stop everything that was started."""
|
"""Stop everything that was started."""
|
||||||
self.hass.stop()
|
self.hass.stop()
|
||||||
|
|
||||||
@patch('passlib.apps.custom_app_context', Mock(return_value=''))
|
|
||||||
@patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock()))
|
|
||||||
@patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock()))
|
|
||||||
@patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro()))
|
|
||||||
@patch('homeassistant.components.mqtt.MQTT')
|
|
||||||
def test_creating_config_with_http_pass_only(self, mock_mqtt):
|
|
||||||
"""Test if the MQTT server failed starts.
|
|
||||||
|
|
||||||
Since 0.77, MQTT server has to set up its own password.
|
|
||||||
If user has api_password but don't have mqtt.password, MQTT component
|
|
||||||
will fail to start
|
|
||||||
"""
|
|
||||||
mock_mqtt().async_connect.return_value = mock_coro(True)
|
|
||||||
self.hass.bus.listen_once = MagicMock()
|
|
||||||
assert not setup_component(self.hass, mqtt.DOMAIN, {
|
|
||||||
'http': {'api_password': 'http_secret'}
|
|
||||||
})
|
|
||||||
|
|
||||||
@patch('passlib.apps.custom_app_context', Mock(return_value=''))
|
@patch('passlib.apps.custom_app_context', Mock(return_value=''))
|
||||||
@patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock()))
|
@patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock()))
|
||||||
@patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock()))
|
@patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock()))
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
"""Tests for the websocket API."""
|
"""Tests for the websocket API."""
|
||||||
API_PASSWORD = 'test1234'
|
API_PASSWORD = 'test-password'
|
||||||
|
|
|
@ -153,10 +153,12 @@ def legacy_auth(hass):
|
||||||
"""Load legacy API password provider."""
|
"""Load legacy API password provider."""
|
||||||
prv = legacy_api_password.LegacyApiPasswordAuthProvider(
|
prv = legacy_api_password.LegacyApiPasswordAuthProvider(
|
||||||
hass, hass.auth._store, {
|
hass, hass.auth._store, {
|
||||||
'type': 'legacy_api_password'
|
'type': 'legacy_api_password',
|
||||||
|
'api_password': 'test-password',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
hass.auth._providers[(prv.type, prv.id)] = prv
|
hass.auth._providers[(prv.type, prv.id)] = prv
|
||||||
|
return prv
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -168,6 +170,7 @@ def local_auth(hass):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
hass.auth._providers[(prv.type, prv.id)] = prv
|
hass.auth._providers[(prv.type, prv.id)] = prv
|
||||||
|
return prv
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
@ -822,7 +822,7 @@ async def test_auth_provider_config(hass):
|
||||||
'time_zone': 'GMT',
|
'time_zone': 'GMT',
|
||||||
CONF_AUTH_PROVIDERS: [
|
CONF_AUTH_PROVIDERS: [
|
||||||
{'type': 'homeassistant'},
|
{'type': 'homeassistant'},
|
||||||
{'type': 'legacy_api_password'},
|
{'type': 'legacy_api_password', 'api_password': 'some-pass'},
|
||||||
],
|
],
|
||||||
CONF_AUTH_MFA_MODULES: [
|
CONF_AUTH_MFA_MODULES: [
|
||||||
{'type': 'totp'},
|
{'type': 'totp'},
|
||||||
|
@ -873,11 +873,12 @@ async def test_auth_provider_config_default_api_password(hass):
|
||||||
}
|
}
|
||||||
if hasattr(hass, 'auth'):
|
if hasattr(hass, 'auth'):
|
||||||
del hass.auth
|
del hass.auth
|
||||||
await config_util.async_process_ha_core_config(hass, core_config, True)
|
await config_util.async_process_ha_core_config(hass, core_config, 'pass')
|
||||||
|
|
||||||
assert len(hass.auth.auth_providers) == 2
|
assert len(hass.auth.auth_providers) == 2
|
||||||
assert hass.auth.auth_providers[0].type == 'homeassistant'
|
assert hass.auth.auth_providers[0].type == 'homeassistant'
|
||||||
assert hass.auth.auth_providers[1].type == 'legacy_api_password'
|
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):
|
async def test_auth_provider_config_default_trusted_networks(hass):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue