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,
provider_configs: List[Dict[str, Any]],
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)
if provider_configs:
providers = await asyncio.gather(
@ -35,17 +39,7 @@ async def auth_manager_from_config(
# So returned auth providers are in same order as config
provider_hash = OrderedDict() # type: _ProviderDict
for provider in providers:
if provider is None:
continue
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
if module_configs:
@ -57,15 +51,6 @@ async def auth_manager_from_config(
# So returned auth modules are in same order as config
module_hash = OrderedDict() # type: _MfaModuleDict
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
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.const import CONF_ID, CONF_NAME, CONF_TYPE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.decorator import Registry
MULTI_FACTOR_AUTH_MODULES = Registry()
@ -127,26 +128,23 @@ class SetupFlow(data_entry_flow.FlowHandler):
async def auth_mfa_module_from_config(
hass: HomeAssistant, config: Dict[str, Any]) \
-> Optional[MultiFactorAuthModule]:
-> MultiFactorAuthModule:
"""Initialize an auth module from a config."""
module_name = config[CONF_TYPE]
module = await _load_mfa_module(hass, module_name)
if module is None:
return None
try:
config = module.CONFIG_SCHEMA(config) # type: ignore
except vol.Invalid as err:
_LOGGER.error('Invalid configuration for multi-factor module %s: %s',
module_name, humanize_error(config, err))
return None
raise
return MULTI_FACTOR_AUTH_MODULES[module_name](hass, config) # type: ignore
async def _load_mfa_module(hass: HomeAssistant, module_name: str) \
-> Optional[types.ModuleType]:
-> types.ModuleType:
"""Load an mfa auth module."""
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)
except ImportError as 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'):
return module
@ -170,7 +169,9 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) \
hass, module_path, module.REQUIREMENTS) # type: ignore
if not req_success:
return None
raise HomeAssistantError(
'Unable to process requirements of mfa module {}'.format(
module_name))
processed.add(module_name)
return module

View file

@ -10,6 +10,7 @@ from voluptuous.humanize import humanize_error
from homeassistant import data_entry_flow, requirements
from homeassistant.core import callback, HomeAssistant
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.decorator import Registry
@ -110,33 +111,31 @@ class AuthProvider:
async def auth_provider_from_config(
hass: HomeAssistant, store: AuthStore,
config: Dict[str, Any]) -> Optional[AuthProvider]:
config: Dict[str, Any]) -> AuthProvider:
"""Initialize an auth provider from a config."""
provider_name = config[CONF_TYPE]
module = await load_auth_provider_module(hass, provider_name)
if module is None:
return None
try:
config = module.CONFIG_SCHEMA(config) # type: ignore
except vol.Invalid as err:
_LOGGER.error('Invalid configuration for auth provider %s: %s',
provider_name, humanize_error(config, err))
return None
raise
return AUTH_PROVIDERS[provider_name](hass, store, config) # type: ignore
async def load_auth_provider_module(
hass: HomeAssistant, provider: str) -> Optional[types.ModuleType]:
hass: HomeAssistant, provider: str) -> types.ModuleType:
"""Load an auth provider."""
try:
module = importlib.import_module(
'homeassistant.auth.providers.{}'.format(provider))
except ImportError as 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'):
return module
@ -154,7 +153,9 @@ async def load_auth_provider_module(
hass, 'auth provider {}'.format(provider), reqs)
if not req_success:
return None
raise HomeAssistantError(
'Unable to process requirements of auth provider {}'.format(
provider))
processed.add(provider)
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,
log_rotate_days, log_file, log_no_color)
)
return hass
@ -94,8 +93,13 @@ async def async_from_config_dict(config: Dict[str, Any],
try:
await conf_util.async_process_ha_core_config(
hass, core_config, has_api_password, has_trusted_networks)
except vol.Invalid as ex:
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
except vol.Invalid as config_err:
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
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)
if not res:
_LOGGER.error("Home Assistant core failed to initialize. "
"further initialization aborted")
"Further initialization aborted")
return hass
await persistent_notification.async_setup(hass, config)

View file

@ -8,7 +8,7 @@ import re
import shutil
# pylint: disable=unused-import
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
import voluptuous as vol
from voluptuous.humanize import humanize_error
@ -23,7 +23,7 @@ from homeassistant.const import (
CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS,
__version__, CONF_CUSTOMIZE, CONF_CUSTOMIZE_DOMAIN, CONF_CUSTOMIZE_GLOB,
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.exceptions import HomeAssistantError
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({
cv.slug: vol.Schema( # Package names are slugs
{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'],
'The insecure_example auth provider'
' is for testing only.')
})]),
})],
_no_duplicate_auth_provider),
vol.Optional(CONF_AUTH_MFA_MODULES):
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 pytest
import voluptuous as vol
from homeassistant import data_entry_flow
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():
"""Test we are not allowed to set an ID in config."""
hass = Mock()
provider = await auth_provider_from_config(hass, None, {
'type': 'homeassistant',
'id': 'invalid',
})
assert provider is None
with pytest.raises(vol.Invalid):
await auth_provider_from_config(hass, None, {
'type': 'homeassistant',
'id': 'invalid',
})
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
import pytest
import voluptuous as vol
from homeassistant import auth, data_entry_flow
from homeassistant.auth import (
@ -21,33 +22,36 @@ def mock_hass(loop):
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."""
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, [{
'name': 'Test Name',
'type': 'insecure_example',
'users': [],
}, {
'name': 'Invalid config because no users',
'type': 'insecure_example',
'id': 'invalid_config',
}, {
'name': 'Test Name 2',
'type': 'insecure_example',
'id': 'another',
'users': [],
}, {
'name': 'Wrong because duplicate ID',
'type': 'insecure_example',
'id': 'another',
'users': [],
}], [])
providers = [{
'name': provider.name,
'id': provider.id,
'type': provider.type,
} for provider in manager.auth_providers]
'name': provider.name,
'id': provider.id,
'type': provider.type,
} for provider in manager.auth_providers]
assert providers == [{
'name': 'Test Name',
'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):
"""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, [{
'name': 'Test Name',
'type': 'insecure_example',
@ -79,13 +103,7 @@ async def test_auth_manager_from_config_auth_modules(mock_hass):
'type': 'insecure_example',
'id': 'another',
'data': [],
}, {
'name': 'Duplicate ID',
'type': 'insecure_example',
'id': 'another',
'data': [],
}])
providers = [{
'name': provider.name,
'type': provider.type,

View file

@ -895,9 +895,73 @@ async def test_disallowed_auth_provider_config(hass):
'name': 'Huis',
CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
'time_zone': 'GMT',
CONF_AUTH_PROVIDERS: [
{'type': 'insecure_example'},
]
CONF_AUTH_PROVIDERS: [{
'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):
await config_util.async_process_ha_core_config(hass, core_config)