From e4fc76ac2c4dd338039d6d8297031cc26eebbff1 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Mon, 28 Jun 2021 03:48:18 -0500 Subject: [PATCH] Add re-authentication support to cloudflare (#51787) --- .../components/cloudflare/__init__.py | 7 ++-- .../components/cloudflare/config_flow.py | 41 ++++++++++++++++++- .../components/cloudflare/strings.json | 7 ++++ .../components/cloudflare/test_config_flow.py | 36 +++++++++++++++- tests/components/cloudflare/test_init.py | 31 +++++++++++++- 5 files changed, 114 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index ed4c54966cf..2d0d3145ead 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -14,7 +14,7 @@ from pycfdns.exceptions import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_TOKEN, CONF_ZONE from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval @@ -37,9 +37,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: zone_id = await cfupdate.get_zone_id() - except CloudflareAuthenticationException: - _LOGGER.error("API access forbidden. Please reauthenticate") - return False + except CloudflareAuthenticationException as error: + raise ConfigEntryAuthFailed from error except CloudflareConnectionException as error: raise ConfigEntryNotReady from error diff --git a/homeassistant/components/cloudflare/config_flow.py b/homeassistant/components/cloudflare/config_flow.py index 364700427da..2a369fe65e0 100644 --- a/homeassistant/components/cloudflare/config_flow.py +++ b/homeassistant/components/cloudflare/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pycfdns import CloudflareUpdater from pycfdns.exceptions import ( @@ -12,9 +13,10 @@ from pycfdns.exceptions import ( import voluptuous as vol from homeassistant.components import persistent_notification -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_API_TOKEN, CONF_ZONE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -85,12 +87,49 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + entry: ConfigEntry | None = None + def __init__(self): """Initialize the Cloudflare config flow.""" self.cloudflare_config = {} self.zones = None self.records = None + async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: + """Handle initiation of re-authentication with Cloudflare.""" + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle re-authentication with Cloudflare.""" + errors = {} + + if user_input is not None and self.entry: + _, errors = await self._async_validate_or_error(user_input) + + if not errors: + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_API_TOKEN: user_input[CONF_API_TOKEN], + }, + ) + + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.entry.entry_id) + ) + + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=DATA_SCHEMA, + errors=errors, + ) + async def async_step_user(self, user_input: dict | None = None): """Handle a flow initiated by the user.""" if self._async_current_entries(): diff --git a/homeassistant/components/cloudflare/strings.json b/homeassistant/components/cloudflare/strings.json index bdadfde4800..31df9a62341 100644 --- a/homeassistant/components/cloudflare/strings.json +++ b/homeassistant/components/cloudflare/strings.json @@ -20,6 +20,12 @@ "data": { "records": "Records" } + }, + "reauth_confirm": { + "data": { + "description": "Re-authenticate with your Cloudflare account.", + "api_token": "[%key:common::config_flow::data::api_token%]" + } } }, "error": { @@ -28,6 +34,7 @@ "invalid_zone": "Invalid zone" }, "abort": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "unknown": "[%key:common::config_flow::error::unknown%]" } diff --git a/tests/components/cloudflare/test_config_flow.py b/tests/components/cloudflare/test_config_flow.py index 00dbb5e47df..230f4c3647f 100644 --- a/tests/components/cloudflare/test_config_flow.py +++ b/tests/components/cloudflare/test_config_flow.py @@ -6,7 +6,7 @@ from pycfdns.exceptions import ( ) from homeassistant.components.cloudflare.const import CONF_RECORDS, DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_TOKEN, CONF_SOURCE, CONF_ZONE from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -162,3 +162,37 @@ async def test_user_form_single_instance_allowed(hass): ) assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "single_instance_allowed" + + +async def test_reauth_flow(hass, cfupdate_flow): + """Test the reauthentication configuration flow.""" + entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + with _patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_TOKEN: "other_token"}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + + assert entry.data[CONF_API_TOKEN] == "other_token" + assert entry.data[CONF_ZONE] == ENTRY_CONFIG[CONF_ZONE] + assert entry.data[CONF_RECORDS] == ENTRY_CONFIG[CONF_RECORDS] + + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/cloudflare/test_init.py b/tests/components/cloudflare/test_init.py index 5a42ca9f09c..ab7dbdab78e 100644 --- a/tests/components/cloudflare/test_init.py +++ b/tests/components/cloudflare/test_init.py @@ -1,8 +1,11 @@ """Test the Cloudflare integration.""" -from pycfdns.exceptions import CloudflareConnectionException +from pycfdns.exceptions import ( + CloudflareAuthenticationException, + CloudflareConnectionException, +) from homeassistant.components.cloudflare.const import DOMAIN, SERVICE_UPDATE_RECORDS -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from . import ENTRY_CONFIG, init_integration @@ -36,6 +39,30 @@ async def test_async_setup_raises_entry_not_ready(hass, cfupdate): assert entry.state is ConfigEntryState.SETUP_RETRY +async def test_async_setup_raises_entry_auth_failed(hass, cfupdate): + """Test that it throws ConfigEntryAuthFailed when exception occurs during setup.""" + instance = cfupdate.return_value + + entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG) + entry.add_to_hass(hass) + + instance.get_zone_id.side_effect = CloudflareAuthenticationException() + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + + assert "context" in flow + assert flow["context"]["source"] == SOURCE_REAUTH + assert flow["context"]["entry_id"] == entry.entry_id + + async def test_integration_services(hass, cfupdate): """Test integration services.""" instance = cfupdate.return_value