From 65216df3a5615ca05a3f4b43ca3a30ed54ff020a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 26 Aug 2024 09:40:25 +0200 Subject: [PATCH] Auto recover mqtt config entry secret if Mosquitto add-on was re-installed (#124514) Auto recover mqtt config entry secret if Mosquitto add-on is re-installed --- homeassistant/components/mqtt/config_flow.py | 30 ++++++ tests/components/mqtt/test_config_flow.py | 102 +++++++++++++++++++ 2 files changed, 132 insertions(+) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index a27e34c364c..ca799ff3653 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -401,6 +401,36 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Handle re-authentication with MQTT broker.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + if is_hassio(self.hass): + # Check if entry setup matches the add-on discovery config + addon_manager = get_addon_manager(self.hass) + try: + addon_discovery_config = ( + await addon_manager.async_get_addon_discovery_info() + ) + except AddonError: + # Follow manual flow if we have an error + pass + else: + # Check if the addon secrets need to be renewed. + # This will repair the config entry, + # in case the official Mosquitto Broker addon was re-installed. + if ( + entry_data[CONF_BROKER] == addon_discovery_config[CONF_HOST] + and entry_data[CONF_PORT] == addon_discovery_config[CONF_PORT] + and entry_data.get(CONF_USERNAME) + == (username := addon_discovery_config.get(CONF_USERNAME)) + and entry_data.get(CONF_PASSWORD) + != (password := addon_discovery_config.get(CONF_PASSWORD)) + ): + _LOGGER.info( + "Executing autorecovery %s add-on secrets", + addon_manager.addon_name, + ) + return await self.async_step_reauth_confirm( + user_input={CONF_USERNAME: username, CONF_PASSWORD: password} + ) + return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 8e29658e017..907e3ef9946 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -1587,6 +1587,108 @@ async def test_step_reauth( await hass.async_block_till_done() +@pytest.mark.parametrize("discovery_info", [{"config": ADD_ON_DISCOVERY_INFO.copy()}]) +@pytest.mark.usefixtures( + "mqtt_client_mock", "mock_reload_after_entry_update", "supervisor", "addon_running" +) +async def test_step_hassio_reauth( + hass: HomeAssistant, mock_try_connection: MagicMock, addon_info: AsyncMock +) -> None: + """Test that the reauth step works in case the Mosquitto broker add-on was re-installed.""" + + # Set up entry data based on the discovery data, but with a stale password + entry_data = { + mqtt.CONF_BROKER: "core-mosquitto", + CONF_PORT: 1883, + CONF_USERNAME: "mock-user", + CONF_PASSWORD: "stale-secret", + } + + addon_info["hostname"] = "core-mosquitto" + + # Prepare the config entry + config_entry = MockConfigEntry(domain=mqtt.DOMAIN, data=entry_data) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + + assert config_entry.data.get(CONF_PASSWORD) == "stale-secret" + + # Start reauth flow + mock_try_connection.reset_mock() + mock_try_connection.return_value = True + config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 0 + + # Assert the entry is updated automatically + assert config_entry.data.get(CONF_PASSWORD) == "mock-pass" + mock_try_connection.assert_called_once_with( + { + "broker": "core-mosquitto", + "port": 1883, + "username": "mock-user", + "password": "mock-pass", + } + ) + + +@pytest.mark.parametrize( + ("discovery_info", "discovery_info_side_effect", "broker"), + [ + ({"config": ADD_ON_DISCOVERY_INFO.copy()}, AddonError, "core-mosquitto"), + ({"config": ADD_ON_DISCOVERY_INFO.copy()}, None, "broker-not-addon"), + ], +) +@pytest.mark.usefixtures( + "mqtt_client_mock", "mock_reload_after_entry_update", "supervisor", "addon_running" +) +async def test_step_hassio_reauth_no_discovery_info( + hass: HomeAssistant, + mock_try_connection: MagicMock, + addon_info: AsyncMock, + broker: str, +) -> None: + """Test hassio reauth flow defaults to manual flow. + + Test that the reauth step defaults to + normal reauth flow if fetching add-on discovery info failed, + or the broker is not the add-on. + """ + + # Set up entry data based on the discovery data, but with a stale password + entry_data = { + mqtt.CONF_BROKER: broker, + CONF_PORT: 1883, + CONF_USERNAME: "mock-user", + CONF_PASSWORD: "wrong-pass", + } + + addon_info["hostname"] = "core-mosquitto" + + # Prepare the config entry + config_entry = MockConfigEntry(domain=mqtt.DOMAIN, data=entry_data) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + + assert config_entry.data.get(CONF_PASSWORD) == "wrong-pass" + + # Start reauth flow + mock_try_connection.reset_mock() + mock_try_connection.return_value = True + config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + result = flows[0] + assert result["step_id"] == "reauth_confirm" + assert result["context"]["source"] == "reauth" + + # Assert the entry is not updated + assert config_entry.data.get(CONF_PASSWORD) == "wrong-pass" + mock_try_connection.assert_not_called() + + async def test_options_user_connection_fails( hass: HomeAssistant, mock_try_connection_time_out: MagicMock ) -> None: