diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 952bb3b8352..4ef8440de62 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -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) diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index a669f8bb5f0..603ca6ff3b1 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -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 diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index d8ec04e9072..370391d57cd 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -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 diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index c10964e2da3..2051359c0ba 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -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) diff --git a/homeassistant/config.py b/homeassistant/config.py index a799094c94d..d742e62660b 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -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), }) diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index 935c5e50dd5..84beb8cdd3f 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -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): diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index f724b40a71f..d9e7a50410f 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -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, diff --git a/tests/test_config.py b/tests/test_config.py index 3cfe67f70b1..e4a6798093f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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)