Blow up startup if init auth providers or modules failed (#16240)

* Blow up startup if init auth providers or modules failed

* Delete core.entity_registry
This commit is contained in:
Jason Hu 2018-08-28 11:54:01 -07:00 committed by Paulus Schoutsen
parent 9a786e449b
commit 257b8b9b80
8 changed files with 194 additions and 72 deletions

View file

@ -24,7 +24,11 @@ async def auth_manager_from_config(
hass: HomeAssistant, hass: HomeAssistant,
provider_configs: List[Dict[str, Any]], provider_configs: List[Dict[str, Any]],
module_configs: List[Dict[str, Any]]) -> 'AuthManager': module_configs: List[Dict[str, Any]]) -> 'AuthManager':
"""Initialize an auth manager from config.""" """Initialize an auth manager from config.
CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or
mfa modules exist in configs.
"""
store = auth_store.AuthStore(hass) store = auth_store.AuthStore(hass)
if provider_configs: if provider_configs:
providers = await asyncio.gather( providers = await asyncio.gather(
@ -35,17 +39,7 @@ async def auth_manager_from_config(
# So returned auth providers are in same order as config # So returned auth providers are in same order as config
provider_hash = OrderedDict() # type: _ProviderDict provider_hash = OrderedDict() # type: _ProviderDict
for provider in providers: for provider in providers:
if provider is None:
continue
key = (provider.type, provider.id) key = (provider.type, provider.id)
if key in provider_hash:
_LOGGER.error(
'Found duplicate provider: %s. Please add unique IDs if you '
'want to have the same provider twice.', key)
continue
provider_hash[key] = provider provider_hash[key] = provider
if module_configs: if module_configs:
@ -57,15 +51,6 @@ async def auth_manager_from_config(
# So returned auth modules are in same order as config # So returned auth modules are in same order as config
module_hash = OrderedDict() # type: _MfaModuleDict module_hash = OrderedDict() # type: _MfaModuleDict
for module in modules: for module in modules:
if module is None:
continue
if module.id in module_hash:
_LOGGER.error(
'Found duplicate multi-factor module: %s. Please add unique '
'IDs if you want to have the same module twice.', module.id)
continue
module_hash[module.id] = module module_hash[module.id] = module
manager = AuthManager(hass, store, provider_hash, module_hash) manager = AuthManager(hass, store, provider_hash, module_hash)

View file

@ -11,6 +11,7 @@ from voluptuous.humanize import humanize_error
from homeassistant import requirements, data_entry_flow from homeassistant import requirements, data_entry_flow
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
MULTI_FACTOR_AUTH_MODULES = Registry() MULTI_FACTOR_AUTH_MODULES = Registry()
@ -127,26 +128,23 @@ class SetupFlow(data_entry_flow.FlowHandler):
async def auth_mfa_module_from_config( async def auth_mfa_module_from_config(
hass: HomeAssistant, config: Dict[str, Any]) \ hass: HomeAssistant, config: Dict[str, Any]) \
-> Optional[MultiFactorAuthModule]: -> MultiFactorAuthModule:
"""Initialize an auth module from a config.""" """Initialize an auth module from a config."""
module_name = config[CONF_TYPE] module_name = config[CONF_TYPE]
module = await _load_mfa_module(hass, module_name) module = await _load_mfa_module(hass, module_name)
if module is None:
return None
try: try:
config = module.CONFIG_SCHEMA(config) # type: ignore config = module.CONFIG_SCHEMA(config) # type: ignore
except vol.Invalid as err: except vol.Invalid as err:
_LOGGER.error('Invalid configuration for multi-factor module %s: %s', _LOGGER.error('Invalid configuration for multi-factor module %s: %s',
module_name, humanize_error(config, err)) module_name, humanize_error(config, err))
return None raise
return MULTI_FACTOR_AUTH_MODULES[module_name](hass, config) # type: ignore return MULTI_FACTOR_AUTH_MODULES[module_name](hass, config) # type: ignore
async def _load_mfa_module(hass: HomeAssistant, module_name: str) \ async def _load_mfa_module(hass: HomeAssistant, module_name: str) \
-> Optional[types.ModuleType]: -> types.ModuleType:
"""Load an mfa auth module.""" """Load an mfa auth module."""
module_path = 'homeassistant.auth.mfa_modules.{}'.format(module_name) module_path = 'homeassistant.auth.mfa_modules.{}'.format(module_name)
@ -154,7 +152,8 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) \
module = importlib.import_module(module_path) module = importlib.import_module(module_path)
except ImportError as err: except ImportError as err:
_LOGGER.error('Unable to load mfa module %s: %s', module_name, err) _LOGGER.error('Unable to load mfa module %s: %s', module_name, err)
return None raise HomeAssistantError('Unable to load mfa module {}: {}'.format(
module_name, err))
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'): if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
return module return module
@ -170,7 +169,9 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) \
hass, module_path, module.REQUIREMENTS) # type: ignore hass, module_path, module.REQUIREMENTS) # type: ignore
if not req_success: if not req_success:
return None raise HomeAssistantError(
'Unable to process requirements of mfa module {}'.format(
module_name))
processed.add(module_name) processed.add(module_name)
return module return module

View file

@ -10,6 +10,7 @@ from voluptuous.humanize import humanize_error
from homeassistant import data_entry_flow, requirements from homeassistant import data_entry_flow, requirements
from homeassistant.core import callback, HomeAssistant from homeassistant.core import callback, HomeAssistant
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
@ -110,33 +111,31 @@ class AuthProvider:
async def auth_provider_from_config( async def auth_provider_from_config(
hass: HomeAssistant, store: AuthStore, hass: HomeAssistant, store: AuthStore,
config: Dict[str, Any]) -> Optional[AuthProvider]: config: Dict[str, Any]) -> AuthProvider:
"""Initialize an auth provider from a config.""" """Initialize an auth provider from a config."""
provider_name = config[CONF_TYPE] provider_name = config[CONF_TYPE]
module = await load_auth_provider_module(hass, provider_name) module = await load_auth_provider_module(hass, provider_name)
if module is None:
return None
try: try:
config = module.CONFIG_SCHEMA(config) # type: ignore config = module.CONFIG_SCHEMA(config) # type: ignore
except vol.Invalid as err: except vol.Invalid as err:
_LOGGER.error('Invalid configuration for auth provider %s: %s', _LOGGER.error('Invalid configuration for auth provider %s: %s',
provider_name, humanize_error(config, err)) provider_name, humanize_error(config, err))
return None raise
return AUTH_PROVIDERS[provider_name](hass, store, config) # type: ignore return AUTH_PROVIDERS[provider_name](hass, store, config) # type: ignore
async def load_auth_provider_module( async def load_auth_provider_module(
hass: HomeAssistant, provider: str) -> Optional[types.ModuleType]: hass: HomeAssistant, provider: str) -> types.ModuleType:
"""Load an auth provider.""" """Load an auth provider."""
try: try:
module = importlib.import_module( module = importlib.import_module(
'homeassistant.auth.providers.{}'.format(provider)) 'homeassistant.auth.providers.{}'.format(provider))
except ImportError as err: except ImportError as err:
_LOGGER.error('Unable to load auth provider %s: %s', provider, err) _LOGGER.error('Unable to load auth provider %s: %s', provider, err)
return None raise HomeAssistantError('Unable to load auth provider {}: {}'.format(
provider, err))
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'): if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
return module return module
@ -154,7 +153,9 @@ async def load_auth_provider_module(
hass, 'auth provider {}'.format(provider), reqs) hass, 'auth provider {}'.format(provider), reqs)
if not req_success: if not req_success:
return None raise HomeAssistantError(
'Unable to process requirements of auth provider {}'.format(
provider))
processed.add(provider) processed.add(provider)
return module return module

View file

@ -61,7 +61,6 @@ def from_config_dict(config: Dict[str, Any],
config, hass, config_dir, enable_log, verbose, skip_pip, config, hass, config_dir, enable_log, verbose, skip_pip,
log_rotate_days, log_file, log_no_color) log_rotate_days, log_file, log_no_color)
) )
return hass return hass
@ -94,8 +93,13 @@ async def async_from_config_dict(config: Dict[str, Any],
try: try:
await conf_util.async_process_ha_core_config( await conf_util.async_process_ha_core_config(
hass, core_config, has_api_password, has_trusted_networks) hass, core_config, has_api_password, has_trusted_networks)
except vol.Invalid as ex: except vol.Invalid as config_err:
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass) conf_util.async_log_exception(
config_err, 'homeassistant', core_config, hass)
return None
except HomeAssistantError:
_LOGGER.error("Home Assistant core failed to initialize. "
"Further initialization aborted")
return None return None
await hass.async_add_executor_job( await hass.async_add_executor_job(
@ -130,7 +134,7 @@ async def async_from_config_dict(config: Dict[str, Any],
res = await core_components.async_setup(hass, config) res = await core_components.async_setup(hass, config)
if not res: if not res:
_LOGGER.error("Home Assistant core failed to initialize. " _LOGGER.error("Home Assistant core failed to initialize. "
"further initialization aborted") "Further initialization aborted")
return hass return hass
await persistent_notification.async_setup(hass, config) await persistent_notification.async_setup(hass, config)

View file

@ -8,7 +8,7 @@ import re
import shutil import shutil
# pylint: disable=unused-import # pylint: disable=unused-import
from typing import ( # noqa: F401 from typing import ( # noqa: F401
Any, Tuple, Optional, Dict, List, Union, Callable) Any, Tuple, Optional, Dict, List, Union, Callable, Sequence, Set)
from types import ModuleType from types import ModuleType
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
@ -23,7 +23,7 @@ from homeassistant.const import (
CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS,
__version__, CONF_CUSTOMIZE, CONF_CUSTOMIZE_DOMAIN, CONF_CUSTOMIZE_GLOB, __version__, CONF_CUSTOMIZE, CONF_CUSTOMIZE_DOMAIN, CONF_CUSTOMIZE_GLOB,
CONF_WHITELIST_EXTERNAL_DIRS, CONF_AUTH_PROVIDERS, CONF_AUTH_MFA_MODULES, CONF_WHITELIST_EXTERNAL_DIRS, CONF_AUTH_PROVIDERS, CONF_AUTH_MFA_MODULES,
CONF_TYPE) CONF_TYPE, CONF_ID)
from homeassistant.core import callback, DOMAIN as CONF_CORE, HomeAssistant from homeassistant.core import callback, DOMAIN as CONF_CORE, HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import get_component, get_platform from homeassistant.loader import get_component, get_platform
@ -128,6 +128,48 @@ some_password: welcome
""" """
def _no_duplicate_auth_provider(configs: Sequence[Dict[str, Any]]) \
-> Sequence[Dict[str, Any]]:
"""No duplicate auth provider config allowed in a list.
Each type of auth provider can only have one config without optional id.
Unique id is required if same type of auth provider used multiple times.
"""
config_keys = set() # type: Set[Tuple[str, Optional[str]]]
for config in configs:
key = (config[CONF_TYPE], config.get(CONF_ID))
if key in config_keys:
raise vol.Invalid(
'Duplicate auth provider {} found. Please add unique IDs if '
'you want to have the same auth provider twice'.format(
config[CONF_TYPE]
))
config_keys.add(key)
return configs
def _no_duplicate_auth_mfa_module(configs: Sequence[Dict[str, Any]]) \
-> Sequence[Dict[str, Any]]:
"""No duplicate auth mfa module item allowed in a list.
Each type of mfa module can only have one config without optional id.
A global unique id is required if same type of mfa module used multiple
times.
Note: this is different than auth provider
"""
config_keys = set() # type: Set[str]
for config in configs:
key = config.get(CONF_ID, config[CONF_TYPE])
if key in config_keys:
raise vol.Invalid(
'Duplicate mfa module {} found. Please add unique IDs if '
'you want to have the same mfa module twice'.format(
config[CONF_TYPE]
))
config_keys.add(key)
return configs
PACKAGES_CONFIG_SCHEMA = vol.Schema({ PACKAGES_CONFIG_SCHEMA = vol.Schema({
cv.slug: vol.Schema( # Package names are slugs cv.slug: vol.Schema( # Package names are slugs
{cv.slug: vol.Any(dict, list, None)}) # Only slugs for component names {cv.slug: vol.Any(dict, list, None)}) # Only slugs for component names
@ -166,10 +208,16 @@ CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend({
CONF_TYPE: vol.NotIn(['insecure_example'], CONF_TYPE: vol.NotIn(['insecure_example'],
'The insecure_example auth provider' 'The insecure_example auth provider'
' is for testing only.') ' is for testing only.')
})]), })],
_no_duplicate_auth_provider),
vol.Optional(CONF_AUTH_MFA_MODULES): vol.Optional(CONF_AUTH_MFA_MODULES):
vol.All(cv.ensure_list, vol.All(cv.ensure_list,
[auth_mfa_modules.MULTI_FACTOR_AUTH_MODULE_SCHEMA]), [auth_mfa_modules.MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
CONF_TYPE: vol.NotIn(['insecure_example'],
'The insecure_example mfa module'
' is for testing only.')
})],
_no_duplicate_auth_mfa_module),
}) })

View file

@ -3,6 +3,7 @@ from unittest.mock import Mock
import base64 import base64
import pytest import pytest
import voluptuous as vol
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.auth import auth_manager_from_config, auth_store from homeassistant.auth import auth_manager_from_config, auth_store
@ -111,11 +112,11 @@ async def test_saving_loading(data, hass):
async def test_not_allow_set_id(): async def test_not_allow_set_id():
"""Test we are not allowed to set an ID in config.""" """Test we are not allowed to set an ID in config."""
hass = Mock() hass = Mock()
provider = await auth_provider_from_config(hass, None, { with pytest.raises(vol.Invalid):
'type': 'homeassistant', await auth_provider_from_config(hass, None, {
'id': 'invalid', 'type': 'homeassistant',
}) 'id': 'invalid',
assert provider is None })
async def test_new_users_populate_values(hass, data): async def test_new_users_populate_values(hass, data):

View file

@ -3,6 +3,7 @@ from datetime import timedelta
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import pytest import pytest
import voluptuous as vol
from homeassistant import auth, data_entry_flow from homeassistant import auth, data_entry_flow
from homeassistant.auth import ( from homeassistant.auth import (
@ -21,33 +22,36 @@ def mock_hass(loop):
return hass return hass
async def test_auth_manager_from_config_validates_config_and_id(mock_hass): async def test_auth_manager_from_config_validates_config(mock_hass):
"""Test get auth providers.""" """Test get auth providers."""
with pytest.raises(vol.Invalid):
manager = await auth.auth_manager_from_config(mock_hass, [{
'name': 'Test Name',
'type': 'insecure_example',
'users': [],
}, {
'name': 'Invalid config because no users',
'type': 'insecure_example',
'id': 'invalid_config',
}], [])
manager = await auth.auth_manager_from_config(mock_hass, [{ manager = await auth.auth_manager_from_config(mock_hass, [{
'name': 'Test Name', 'name': 'Test Name',
'type': 'insecure_example', 'type': 'insecure_example',
'users': [], 'users': [],
}, {
'name': 'Invalid config because no users',
'type': 'insecure_example',
'id': 'invalid_config',
}, { }, {
'name': 'Test Name 2', 'name': 'Test Name 2',
'type': 'insecure_example', 'type': 'insecure_example',
'id': 'another', 'id': 'another',
'users': [], 'users': [],
}, {
'name': 'Wrong because duplicate ID',
'type': 'insecure_example',
'id': 'another',
'users': [],
}], []) }], [])
providers = [{ providers = [{
'name': provider.name, 'name': provider.name,
'id': provider.id, 'id': provider.id,
'type': provider.type, 'type': provider.type,
} for provider in manager.auth_providers] } for provider in manager.auth_providers]
assert providers == [{ assert providers == [{
'name': 'Test Name', 'name': 'Test Name',
'type': 'insecure_example', 'type': 'insecure_example',
@ -61,6 +65,26 @@ async def test_auth_manager_from_config_validates_config_and_id(mock_hass):
async def test_auth_manager_from_config_auth_modules(mock_hass): async def test_auth_manager_from_config_auth_modules(mock_hass):
"""Test get auth modules.""" """Test get auth modules."""
with pytest.raises(vol.Invalid):
manager = await auth.auth_manager_from_config(mock_hass, [{
'name': 'Test Name',
'type': 'insecure_example',
'users': [],
}, {
'name': 'Test Name 2',
'type': 'insecure_example',
'id': 'another',
'users': [],
}], [{
'name': 'Module 1',
'type': 'insecure_example',
'data': [],
}, {
'name': 'Invalid config because no data',
'type': 'insecure_example',
'id': 'another',
}])
manager = await auth.auth_manager_from_config(mock_hass, [{ manager = await auth.auth_manager_from_config(mock_hass, [{
'name': 'Test Name', 'name': 'Test Name',
'type': 'insecure_example', 'type': 'insecure_example',
@ -79,13 +103,7 @@ async def test_auth_manager_from_config_auth_modules(mock_hass):
'type': 'insecure_example', 'type': 'insecure_example',
'id': 'another', 'id': 'another',
'data': [], 'data': [],
}, {
'name': 'Duplicate ID',
'type': 'insecure_example',
'id': 'another',
'data': [],
}]) }])
providers = [{ providers = [{
'name': provider.name, 'name': provider.name,
'type': provider.type, 'type': provider.type,

View file

@ -895,9 +895,73 @@ async def test_disallowed_auth_provider_config(hass):
'name': 'Huis', 'name': 'Huis',
CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
'time_zone': 'GMT', 'time_zone': 'GMT',
CONF_AUTH_PROVIDERS: [ CONF_AUTH_PROVIDERS: [{
{'type': 'insecure_example'}, 'type': 'insecure_example',
] 'users': [{
'username': 'test-user',
'password': 'test-pass',
'name': 'Test Name'
}],
}]
}
with pytest.raises(Invalid):
await config_util.async_process_ha_core_config(hass, core_config)
async def test_disallowed_duplicated_auth_provider_config(hass):
"""Test loading insecure example auth provider is disallowed."""
core_config = {
'latitude': 60,
'longitude': 50,
'elevation': 25,
'name': 'Huis',
CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
'time_zone': 'GMT',
CONF_AUTH_PROVIDERS: [{
'type': 'homeassistant',
}, {
'type': 'homeassistant',
}]
}
with pytest.raises(Invalid):
await config_util.async_process_ha_core_config(hass, core_config)
async def test_disallowed_auth_mfa_module_config(hass):
"""Test loading insecure example auth mfa module is disallowed."""
core_config = {
'latitude': 60,
'longitude': 50,
'elevation': 25,
'name': 'Huis',
CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
'time_zone': 'GMT',
CONF_AUTH_MFA_MODULES: [{
'type': 'insecure_example',
'data': [{
'user_id': 'mock-user',
'pin': 'test-pin'
}]
}]
}
with pytest.raises(Invalid):
await config_util.async_process_ha_core_config(hass, core_config)
async def test_disallowed_duplicated_auth_mfa_module_config(hass):
"""Test loading insecure example auth mfa module is disallowed."""
core_config = {
'latitude': 60,
'longitude': 50,
'elevation': 25,
'name': 'Huis',
CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
'time_zone': 'GMT',
CONF_AUTH_MFA_MODULES: [{
'type': 'totp',
}, {
'type': 'totp',
}]
} }
with pytest.raises(Invalid): with pytest.raises(Invalid):
await config_util.async_process_ha_core_config(hass, core_config) await config_util.async_process_ha_core_config(hass, core_config)