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:
parent
9a786e449b
commit
257b8b9b80
8 changed files with 194 additions and 72 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue