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 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) \
-> 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)
async def async_get_users(self) -> List[models.User]:
@ -113,6 +125,11 @@ class AuthManager:
"""Retrieve a user."""
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]:
"""Retrieve all groups."""
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
"""
import hmac
from typing import Any, Dict, Optional, cast, TYPE_CHECKING
from typing import Any, Dict, Optional, cast
import voluptuous as vol
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from .. import AuthManager
from ..models import Credentials, UserMeta, User
if TYPE_CHECKING:
from homeassistant.components.http import HomeAssistantHTTP # noqa: F401
USER_SCHEMA = vol.Schema({
vol.Required('username'): str,
})
AUTH_PROVIDER_TYPE = 'legacy_api_password'
CONF_API_PASSWORD = 'api_password'
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
vol.Required(CONF_API_PASSWORD): cv.string,
}, extra=vol.PREVENT_EXTRA)
LEGACY_USER_NAME = 'Legacy API password user'
@ -34,40 +30,45 @@ class InvalidAuthError(HomeAssistantError):
"""Raised when submitting invalid authentication."""
async def async_get_user(hass: HomeAssistant) -> User:
"""Return the legacy API password user."""
async def async_validate_password(hass: HomeAssistant, password: str)\
-> Optional[User]:
"""Return a user if password is valid. None if not."""
auth = cast(AuthManager, hass.auth) # type: ignore
found = None
for prv in auth.auth_providers:
if prv.type == 'legacy_api_password':
found = prv
break
if found is None:
providers = auth.get_auth_providers(AUTH_PROVIDER_TYPE)
if not providers:
raise ValueError('Legacy API password provider not found')
return await auth.async_get_or_create_user(
await found.async_get_or_create_credentials({})
)
try:
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):
"""Example auth provider based on hardcoded usernames and passwords."""
"""An auth provider support 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:
"""Return a flow to login."""
return LegacyLoginFlow(self)
@callback
def async_validate_login(self, password: str) -> None:
"""Validate a username and password."""
hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP
"""Validate password."""
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')):
raise InvalidAuthError
@ -99,12 +100,6 @@ class LegacyLoginFlow(LoginFlow):
"""Handle the step of the form."""
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:
try:
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")
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')
try:
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:
conf_util.async_log_exception(
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)
return
# auth only processed during startup
await conf_util.async_process_ha_core_config(
hass, conf.get(ha.DOMAIN) or {})

View file

@ -168,11 +168,11 @@ class APIDiscoveryView(HomeAssistantView):
def get(self, request):
"""Get discovery information."""
hass = request.app['hass']
needs_auth = hass.config.api.api_password is not None
return self.json({
ATTR_BASE_URL: hass.config.api.base_url,
ATTR_LOCATION_NAME: hass.config.location_name,
ATTR_REQUIRES_API_PASSWORD: needs_auth,
# always needs authentication
ATTR_REQUIRES_API_PASSWORD: True,
ATTR_VERSION: __version__,
})

View file

@ -11,13 +11,11 @@ from datetime import timedelta
import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_MODE, \
HTTP_HEADER_HA_AUTH
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_MODE
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.util.async_ import run_coroutine_threadsafe
import homeassistant.util.dt as dt_util
from homeassistant.components.camera import async_get_still_stream
REQUIREMENTS = ['pillow==5.4.1']
@ -209,9 +207,6 @@ class ProxyCamera(Camera):
or config.get(CONF_CACHE_IMAGES))
self._last_image_time = dt_util.utc_from_timestamp(0)
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)
def camera_image(self):
@ -252,7 +247,7 @@ class ProxyCamera(Camera):
return await self.hass.components.camera.async_get_mjpeg_stream(
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,
self.content_type, self.frame_interval)

View file

@ -407,7 +407,7 @@ class IndexView(HomeAssistantView):
})
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
no_auth = '0'

View file

@ -57,9 +57,9 @@ class HassIOAuth(HomeAssistantView):
def _get_provider(self):
"""Return Homeassistant auth provider."""
for prv in self.hass.auth.auth_providers:
if prv.type == 'homeassistant':
return prv
prv = self.hass.auth.get_auth_provider('homeassistant', None)
if prv is not None:
return prv
_LOGGER.error("Can't find Home Assistant auth.")
raise HTTPNotFound()

View file

@ -7,8 +7,10 @@ import aiohttp
import async_timeout
from homeassistant.components.http import (
CONF_API_PASSWORD, CONF_SERVER_HOST, CONF_SERVER_PORT,
CONF_SSL_CERTIFICATE)
CONF_SERVER_HOST,
CONF_SERVER_PORT,
CONF_SSL_CERTIFICATE,
)
from homeassistant.const import CONF_TIME_ZONE, SERVER_PORT
from .const import X_HASSIO
@ -125,7 +127,6 @@ class HassIO:
options = {
'ssl': CONF_SSL_CERTIFICATE in http_config,
'port': port,
'password': http_config.get(CONF_API_PASSWORD),
'watchdog': True,
'refresh_token': refresh_token,
}

View file

@ -18,7 +18,12 @@ from homeassistant.util.logging import HideSensitiveDataFilter
from .auth import setup_auth
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 .real_ip import setup_real_ip
from .static import CACHE_HEADERS, CachingStaticResource
@ -66,8 +71,22 @@ def trusted_networks_deprecated(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({
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_PORT, default=SERVER_PORT): cv.port,
vol.Optional(CONF_BASE_URL): cv.string,
@ -98,12 +117,10 @@ class ApiConfig:
"""Configuration settings for API server."""
def __init__(self, host: str, port: Optional[int] = SERVER_PORT,
use_ssl: bool = False,
api_password: Optional[str] = None) -> None:
use_ssl: bool = False) -> None:
"""Initialize a new API config object."""
self.host = host
self.port = port
self.api_password = api_password
host = host.rstrip('/')
if host.startswith(("http://", "https://")):
@ -133,7 +150,6 @@ async def async_setup(hass, config):
cors_origins = conf[CONF_CORS_ORIGINS]
use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False)
trusted_proxies = conf.get(CONF_TRUSTED_PROXIES, [])
trusted_networks = conf[CONF_TRUSTED_NETWORKS]
is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
ssl_profile = conf[CONF_SSL_PROFILE]
@ -146,14 +162,12 @@ async def async_setup(hass, config):
hass,
server_host=server_host,
server_port=server_port,
api_password=api_password,
ssl_certificate=ssl_certificate,
ssl_peer_certificate=ssl_peer_certificate,
ssl_key=ssl_key,
cors_origins=cors_origins,
use_x_forwarded_for=use_x_forwarded_for,
trusted_proxies=trusted_proxies,
trusted_networks=trusted_networks,
login_threshold=login_threshold,
is_ban_enabled=is_ban_enabled,
ssl_profile=ssl_profile,
@ -183,8 +197,7 @@ async def async_setup(hass, config):
host = hass_util.get_local_ip()
port = server_port
hass.config.api = ApiConfig(host, port, ssl_certificate is not None,
api_password)
hass.config.api = ApiConfig(host, port, ssl_certificate is not None)
return True
@ -192,13 +205,14 @@ async def async_setup(hass, config):
class HomeAssistantHTTP:
"""HTTP server for Home Assistant."""
def __init__(self, hass, api_password,
def __init__(self, hass,
ssl_certificate, ssl_peer_certificate,
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):
"""Initialize the HTTP Home Assistant server."""
app = self.app = web.Application(middlewares=[])
app[KEY_HASS] = hass
# This order matters
setup_real_ip(app, use_x_forwarded_for, trusted_proxies)
@ -206,34 +220,16 @@ class HomeAssistantHTTP:
if is_ban_enabled:
setup_bans(hass, app, login_threshold)
if hass.auth.support_legacy:
_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_auth(hass, app)
setup_cors(app, cors_origins)
app['hass'] = hass
self.hass = hass
self.api_password = api_password
self.ssl_certificate = ssl_certificate
self.ssl_peer_certificate = ssl_peer_certificate
self.ssl_key = ssl_key
self.server_host = server_host
self.server_port = server_port
self.trusted_networks = trusted_networks
self.is_ban_enabled = is_ban_enabled
self.ssl_profile = ssl_profile
self._handler = None

View file

@ -1,6 +1,5 @@
"""Authentication for HTTP component."""
import base64
import hmac
import logging
from aiohttp import hdrs
@ -13,7 +12,11 @@ from homeassistant.const import HTTP_HEADER_HA_AUTH
from homeassistant.core import callback
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__)
@ -40,10 +43,125 @@ def async_sign_path(hass, refresh_token_id, path, expiration):
@callback
def setup_auth(app, trusted_networks, api_password):
def setup_auth(hass, app):
"""Create auth middleware for the app."""
old_auth_warning = set()
support_legacy = hass.auth.support_legacy
if support_legacy:
_LOGGER.warning("legacy_api_password support has been enabled.")
trusted_networks = []
for prv in hass.auth.auth_providers:
if prv.type == 'trusted_networks':
trusted_networks += prv.trusted_networks
async def async_validate_auth_header(request):
"""
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
async def auth_middleware(request, handler):
"""Authenticate as middleware."""
@ -53,13 +171,14 @@ def setup_auth(app, trusted_networks, api_password):
DATA_API_PASSWORD in request.query):
if request.path not in old_auth_warning:
_LOGGER.log(
logging.INFO if api_password else logging.WARNING,
'You need to use a bearer token to access %s from %s',
logging.INFO if support_legacy else logging.WARNING,
'api_password is going to deprecate. You need to use a'
' bearer token to access %s from %s',
request.path, request[KEY_REAL_IP])
old_auth_warning.add(request.path)
if (hdrs.AUTHORIZATION in request.headers and
await async_validate_auth_header(request, api_password)):
await async_validate_auth_header(request)):
# it included both use_auth and api_password Basic auth
authenticated = True
@ -69,133 +188,21 @@ def setup_auth(app, trusted_networks, api_password):
await async_validate_signed_request(request)):
authenticated = True
elif (api_password and HTTP_HEADER_HA_AUTH in request.headers and
hmac.compare_digest(
api_password.encode('utf-8'),
request.headers[HTTP_HEADER_HA_AUTH].encode('utf-8'))):
# A valid auth header has been set
elif (trusted_networks and
await async_validate_trusted_networks(request)):
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
hmac.compare_digest(
api_password.encode('utf-8'),
request.query[DATA_API_PASSWORD].encode('utf-8'))):
elif (support_legacy and HTTP_HEADER_HA_AUTH in request.headers and
await async_validate_legacy_api_password(
request, request.headers[HTTP_HEADER_HA_AUTH])):
authenticated = True
request['hass_user'] = await legacy_api_password.async_get_user(
app['hass'])
elif _is_trusted_ip(request, trusted_networks):
users = await app['hass'].auth.async_get_users()
for user in users:
if user.is_owner:
request['hass_user'] = user
authenticated = True
break
elif (support_legacy and DATA_API_PASSWORD in request.query and
await async_validate_legacy_api_password(
request, request.query[DATA_API_PASSWORD])):
authenticated = True
request[KEY_AUTHENTICATED] = authenticated
return await handler(request)
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."""
KEY_AUTHENTICATED = 'ha_authenticated'
KEY_HASS = 'hass'
KEY_HASS_USER = 'hass_user'
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.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__)
@ -91,7 +91,7 @@ def request_handler_factory(view, handler):
async def handle(request):
"""Handle incoming request."""
if not request.app['hass'].is_running:
if not request.app[KEY_HASS].is_running:
return web.Response(status=503)
authenticated = request.get(KEY_AUTHENTICATED, False)

View file

@ -399,14 +399,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
conf = dict(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)

View file

@ -2,11 +2,12 @@
import voluptuous as vol
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.components.http.ban import (
process_wrong_login,
process_success_login,
)
from homeassistant.const import __version__
from .connection import ActiveConnection
from .error import Disconnect
@ -80,9 +81,15 @@ class AuthPhase:
refresh_token.user, refresh_token)
elif self._hass.auth.support_legacy and 'api_password' in msg:
self._logger.debug("Received api_password")
if validate_password(self._request, msg['api_password']):
user = await legacy_api_password.async_get_user(self._hass)
self._logger.info(
"Received api_password, it is going to deprecate, please use"
" access_token instead. For instructions, see https://"
"developers.home-assistant.io/docs/en/external_api_websocket"
".html#authentication-phase"
)
user = await legacy_api_password.async_validate_password(
self._hass, msg['api_password'])
if user is not None:
return await self._async_finish_auth(user, None)
self._send_message(auth_invalid_message(

View file

@ -30,11 +30,11 @@ def setup(hass, config):
zeroconf_name = '{}.{}'.format(hass.config.location_name, ZEROCONF_TYPE)
requires_api_password = hass.config.api.api_password is not None
params = {
'version': __version__,
'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()

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(
hass: HomeAssistant, config: Dict,
has_api_password: bool = False,
api_password: Optional[str] = None,
trusted_networks: Optional[Any] = None) -> None:
"""Process the [homeassistant] section from the configuration.
@ -444,8 +444,11 @@ async def async_process_ha_core_config(
auth_conf = [
{'type': 'homeassistant'}
]
if has_api_password:
auth_conf.append({'type': 'legacy_api_password'})
if api_password:
auth_conf.append({
'type': 'legacy_api_password',
'api_password': api_password,
})
if trusted_networks:
auth_conf.append({
'type': 'trusted_networks',

View file

@ -1180,7 +1180,7 @@ class Config:
# List of loaded components
self.components = set() # type: set
# API (HTTP) server configuration
# API (HTTP) server configuration, see components.http.ApiConfig
self.api = None # type: Optional[Any]
# Directory that holds the configuration

View file

@ -1,6 +1,4 @@
"""Tests for the legacy_api_password auth provider."""
from unittest.mock import Mock
import pytest
from homeassistant import auth, data_entry_flow
@ -19,6 +17,7 @@ def provider(hass, store):
"""Mock provider."""
return legacy_api_password.LegacyApiPasswordAuthProvider(hass, store, {
'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):
"""Test login using legacy api password auth provider."""
hass.http = Mock(api_password='test-password')
provider.async_validate_login('test-password')
hass.http = Mock(api_password='test-password')
with pytest.raises(legacy_api_password.InvalidAuthError):
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):
"""Test wrong config."""
hass.http = Mock(api_password='hello')
result = await manager.login_flow.async_init(
handler=('legacy_api_password', None)
)
@ -94,7 +74,7 @@ async def test_login_flow_works(hass, manager):
result = await manager.login_flow.async_configure(
flow_id=result['flow_id'],
user_input={
'password': 'hello'
'password': 'test-password'
}
)
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,
hass_admin_user, legacy_auth):
hass_admin_user):
"""Test if we can fetch the error log."""
hass.data[DATA_LOGGING] = '/some/path'
await async_setup_component(hass, 'api', {
'http': {
'api_password': 'yolo'
}
})
await async_setup_component(hass, 'api', {})
client = await aiohttp_client(hass.http.app)
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):
result = yield from async_setup_component(hass, 'hassio', {
'http': {
'api_password': "123456",
'server_port': 9999
},
'hassio': {}
@ -60,7 +59,6 @@ def test_setup_api_push_api_data(hass, aioclient_mock):
assert aioclient_mock.call_count == 3
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]['watchdog']
@ -71,7 +69,6 @@ def test_setup_api_push_api_data_server_host(hass, aioclient_mock):
with patch.dict(os.environ, MOCK_ENVIRON):
result = yield from async_setup_component(hass, 'hassio', {
'http': {
'api_password': "123456",
'server_port': 9999,
'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 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 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 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
refresh_token = aioclient_mock.mock_calls[1][2]['refresh_token']
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 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]['refresh_token'] == token.token

View file

@ -7,7 +7,7 @@ import pytest
from aiohttp import BasicAuth, web
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.const import KEY_AUTHENTICATED
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
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
TRUSTED_NETWORKS = [
@ -35,17 +35,22 @@ async def mock_handler(request):
if not request[KEY_AUTHENTICATED]:
raise HTTPUnauthorized
token = request.get('hass_refresh_token')
token_id = token.id if token else None
user = request.get('hass_user')
user_id = user.id if user else None
return web.json_response(status=200, data={
'refresh_token_id': token_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
def app(hass):
"""Fixture to set up a web.Application."""
@ -65,6 +70,19 @@ def app2(hass):
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):
"""Test accessing to server from banned IP when feature is off."""
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,
legacy_auth, hass):
"""Test access with password in header."""
setup_auth(app, [], api_password=API_PASSWORD)
setup_auth(hass, 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(
'/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
assert req.status == 200
assert await req.json() == {
'refresh_token_id': None,
'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,
hass):
"""Test access with password in URL."""
setup_auth(app, [], api_password=API_PASSWORD)
setup_auth(hass, 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={
'api_password': API_PASSWORD
})
assert resp.status == 200
assert await resp.json() == {
'refresh_token_id': None,
'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):
"""Test access with basic authentication."""
setup_auth(app, [], api_password=API_PASSWORD)
setup_auth(hass, 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(
'/',
auth=BasicAuth('homeassistant', API_PASSWORD))
assert req.status == 200
assert await req.json() == {
'refresh_token_id': None,
'user_id': user.id,
}
@ -153,9 +168,11 @@ async def test_basic_auth_works(app, aiohttp_client, hass, legacy_auth):
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."""
setup_auth(app2, TRUSTED_NETWORKS, api_password='some-pass')
setup_auth(hass, app2)
set_mock_ip = mock_real_ip(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, \
"{} should be trusted".format(remote_addr)
assert await resp.json() == {
'refresh_token_id': None,
'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):
"""Test access with access token in header."""
token = hass_access_token
setup_auth(app, [], api_password=None)
setup_auth(hass, app)
client = await aiohttp_client(app)
refresh_token = await hass.auth.async_validate_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)})
assert req.status == 200
assert await req.json() == {
'refresh_token_id': refresh_token.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)})
assert req.status == 200
assert await req.json() == {
'refresh_token_id': refresh_token.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)})
assert req.status == 200
assert await req.json() == {
'refresh_token_id': refresh_token.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
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):
"""Test access with an untrusted ip address."""
setup_auth(app2, TRUSTED_NETWORKS, None)
setup_auth(hass, app2)
set_mock_ip = mock_real_ip(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, \
"{} should be trusted".format(remote_addr)
assert await resp.json() == {
'refresh_token_id': None,
'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(
app, aiohttp_client, legacy_auth, hass):
"""Test access using api_password if auth.support_legacy."""
setup_auth(app, [], API_PASSWORD)
setup_auth(hass, 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(
'/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
assert req.status == 200
assert await req.json() == {
'refresh_token_id': None,
'user_id': user.id,
}
@ -271,7 +284,6 @@ async def test_auth_legacy_support_api_password_access(
})
assert resp.status == 200
assert await resp.json() == {
'refresh_token_id': None,
'user_id': user.id,
}
@ -280,7 +292,6 @@ async def test_auth_legacy_support_api_password_access(
auth=BasicAuth('homeassistant', API_PASSWORD))
assert req.status == 200
assert await req.json() == {
'refresh_token_id': None,
'user_id': user.id,
}
@ -290,7 +301,7 @@ async def test_auth_access_signed_path(
"""Test access with signed url."""
app.router.add_post('/', mock_handler)
app.router.add_get('/another_path', mock_handler)
setup_auth(app, [], None)
setup_auth(hass, app)
client = await aiohttp_client(app)
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)
assert req.status == 200
data = await req.json()
assert data['refresh_token_id'] == refresh_token.id
assert data['user_id'] == refresh_token.user.id
# 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):
"""Test access with password doesn't get logged."""
assert await async_setup_component(hass, 'api', {
'http': {
http.CONF_API_PASSWORD: 'some-pass'
}
'http': {}
})
client = await aiohttp_client(hass.http.app)
logging.getLogger('aiohttp.access').setLevel(logging.INFO)
resp = await client.get('/api/', params={
'api_password': 'some-pass'
'api_password': 'test-password'
})
assert resp.status == 200

View file

@ -19,24 +19,6 @@ class TestMQTT:
"""Stop everything that was started."""
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('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock()))
@patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock()))

View file

@ -1,2 +1,2 @@
"""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."""
prv = legacy_api_password.LegacyApiPasswordAuthProvider(
hass, hass.auth._store, {
'type': 'legacy_api_password'
'type': 'legacy_api_password',
'api_password': 'test-password',
}
)
hass.auth._providers[(prv.type, prv.id)] = prv
return prv
@pytest.fixture
@ -168,6 +170,7 @@ def local_auth(hass):
}
)
hass.auth._providers[(prv.type, prv.id)] = prv
return prv
@pytest.fixture

View file

@ -822,7 +822,7 @@ async def test_auth_provider_config(hass):
'time_zone': 'GMT',
CONF_AUTH_PROVIDERS: [
{'type': 'homeassistant'},
{'type': 'legacy_api_password'},
{'type': 'legacy_api_password', 'api_password': 'some-pass'},
],
CONF_AUTH_MFA_MODULES: [
{'type': 'totp'},
@ -873,11 +873,12 @@ async def test_auth_provider_config_default_api_password(hass):
}
if hasattr(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 hass.auth.auth_providers[0].type == 'homeassistant'
assert hass.auth.auth_providers[1].type == 'legacy_api_password'
assert hass.auth.auth_providers[1].api_password == 'pass'
async def test_auth_provider_config_default_trusted_networks(hass):