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:
Jason Hu 2019-03-10 19:55:36 -07:00 committed by Paulus Schoutsen
parent 7ec7e51f70
commit fe1840f901
27 changed files with 304 additions and 324 deletions

View file

@ -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)

View file

@ -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)\

View file

@ -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)

View file

@ -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 {})

View file

@ -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__,
}) })

View file

@ -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)

View file

@ -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'

View file

@ -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()

View file

@ -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,
} }

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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)

View file

@ -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)

View file

@ -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(

View file

@ -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()

View file

@ -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',

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()))

View file

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

View file

@ -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

View file

@ -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):