diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 98eadead101..c5f62c0034f 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -40,7 +40,7 @@ from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.typing import ConfigType from . import config_flow -from .api import ApiAuthImpl, DeviceAuth +from .api import ApiAuthImpl, DeviceAuth, get_feature_access from .const import ( CONF_CALENDAR_ACCESS, DATA_CONFIG, @@ -172,7 +172,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # a ConfigEntry managed by home assistant. storage = Storage(hass.config.path(TOKEN_FILE)) creds = await hass.async_add_executor_job(storage.get) - if creds and conf[CONF_CALENDAR_ACCESS].scope in creds.scopes: + if creds and get_feature_access(hass).scope in creds.scopes: _LOGGER.debug("Importing configuration entry with credentials") hass.async_create_task( hass.config_entries.flow.async_init( @@ -210,8 +210,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except aiohttp.ClientError as err: raise ConfigEntryNotReady from err - required_scope = hass.data[DOMAIN][DATA_CONFIG][CONF_CALENDAR_ACCESS].scope - if required_scope not in session.token.get("scope", []): + access = get_feature_access(hass) + if access.scope not in session.token.get("scope", []): raise ConfigEntryAuthFailed( "Required scopes are not available, reauth required" ) @@ -220,7 +220,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data[DOMAIN][DATA_SERVICE] = calendar_service - await async_setup_services(hass, hass.data[DOMAIN][DATA_CONFIG], calendar_service) + track_new = hass.data[DOMAIN][DATA_CONFIG].get(CONF_TRACK_NEW, True) + await async_setup_services(hass, track_new, calendar_service) + # Only expose the add event service if we have the correct permissions + if access is FeatureAccess.read_write: + await async_setup_add_event_service(hass, calendar_service) hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -234,7 +238,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_services( hass: HomeAssistant, - config: ConfigType, + track_new: bool, calendar_service: GoogleCalendarService, ) -> None: """Set up the service listeners.""" @@ -274,7 +278,7 @@ async def async_setup_services( tasks = [] for calendar_item in result.items: calendar = calendar_item.dict(exclude_unset=True) - calendar[CONF_TRACK] = config[CONF_TRACK_NEW] + calendar[CONF_TRACK] = track_new tasks.append( hass.services.async_call(DOMAIN, SERVICE_FOUND_CALENDARS, calendar) ) @@ -282,6 +286,13 @@ async def async_setup_services( hass.services.async_register(DOMAIN, SERVICE_SCAN_CALENDARS, _scan_for_calendars) + +async def async_setup_add_event_service( + hass: HomeAssistant, + calendar_service: GoogleCalendarService, +) -> None: + """Add the service to add events.""" + async def _add_event(call: ServiceCall) -> None: """Add a new event to calendar.""" start: DateOrDatetime | None = None @@ -333,11 +344,9 @@ async def async_setup_services( ), ) - # Only expose the add event service if we have the correct permissions - if config.get(CONF_CALENDAR_ACCESS) is FeatureAccess.read_write: - hass.services.async_register( - DOMAIN, SERVICE_ADD_EVENT, _add_event, schema=ADD_EVENT_SERVICE_SCHEMA - ) + hass.services.async_register( + DOMAIN, SERVICE_ADD_EVENT, _add_event, schema=ADD_EVENT_SERVICE_SCHEMA + ) def get_calendar_info( diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index 5de563393e1..bb32d46f0e4 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -19,18 +19,25 @@ from oauth2client.client import ( OAuth2WebServerFlow, ) -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import dt -from .const import CONF_CALENDAR_ACCESS, DATA_CONFIG, DEVICE_AUTH_IMPL, DOMAIN +from .const import ( + CONF_CALENDAR_ACCESS, + DATA_CONFIG, + DEFAULT_FEATURE_ACCESS, + DEVICE_AUTH_IMPL, + DOMAIN, + FeatureAccess, +) _LOGGER = logging.getLogger(__name__) EVENT_PAGE_SIZE = 100 EXCHANGE_TIMEOUT_SECONDS = 60 +DEVICE_AUTH_CREDS = "creds" class OAuthError(Exception): @@ -53,7 +60,7 @@ class DeviceAuth(config_entry_oauth2_flow.LocalOAuth2Implementation): async def async_resolve_external_data(self, external_data: Any) -> dict: """Resolve a Google API Credentials object to Home Assistant token.""" - creds: Credentials = external_data["creds"] + creds: Credentials = external_data[DEVICE_AUTH_CREDS] return { "access_token": creds.access_token, "refresh_token": creds.refresh_token, @@ -132,13 +139,25 @@ class DeviceFlow: ) -async def async_create_device_flow(hass: HomeAssistant) -> DeviceFlow: +def get_feature_access(hass: HomeAssistant) -> FeatureAccess: + """Return the desired calendar feature access.""" + # This may be called during config entry setup without integration setup running when there + # is no google entry in configuration.yaml + return ( + hass.data.get(DOMAIN, {}) + .get(DATA_CONFIG, {}) + .get(CONF_CALENDAR_ACCESS, DEFAULT_FEATURE_ACCESS) + ) + + +async def async_create_device_flow( + hass: HomeAssistant, client_id: str, client_secret: str, access: FeatureAccess +) -> DeviceFlow: """Create a new Device flow.""" - conf = hass.data[DOMAIN][DATA_CONFIG] oauth_flow = OAuth2WebServerFlow( - client_id=conf[CONF_CLIENT_ID], - client_secret=conf[CONF_CLIENT_SECRET], - scope=conf[CONF_CALENDAR_ACCESS].scope, + client_id=client_id, + client_secret=client_secret, + scope=access.scope, redirect_uri="", ) try: diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index 8bbd2a6c2b1..f5e567a5272 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -9,7 +9,14 @@ from oauth2client.client import Credentials from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow -from .api import DeviceFlow, OAuthError, async_create_device_flow +from .api import ( + DEVICE_AUTH_CREDS, + DeviceAuth, + DeviceFlow, + OAuthError, + async_create_device_flow, + get_feature_access, +) from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -67,15 +74,28 @@ class OAuth2FlowHandler( if not self._device_flow: _LOGGER.debug("Creating DeviceAuth flow") + if not isinstance(self.flow_impl, DeviceAuth): + _LOGGER.error( + "Unexpected OAuth implementation does not support device auth: %s", + self.flow_impl, + ) + return self.async_abort(reason="oauth_error") try: - device_flow = await async_create_device_flow(self.hass) + device_flow = await async_create_device_flow( + self.hass, + self.flow_impl.client_id, + self.flow_impl.client_secret, + get_feature_access(self.hass), + ) except OAuthError as err: _LOGGER.error("Error initializing device flow: %s", str(err)) return self.async_abort(reason="oauth_error") self._device_flow = device_flow async def _exchange_finished(creds: Credentials | None) -> None: - self.external_data = {"creds": creds} # is None on timeout/expiration + self.external_data = { + DEVICE_AUTH_CREDS: creds + } # is None on timeout/expiration self.hass.async_create_task( self.hass.config_entries.flow.async_configure( flow_id=self.flow_id, user_input={} @@ -97,7 +117,7 @@ class OAuth2FlowHandler( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle external yaml configuration.""" - if self.external_data.get("creds") is None: + if self.external_data.get(DEVICE_AUTH_CREDS) is None: return self.async_abort(reason="code_expired") return await super().async_step_creation(user_input) diff --git a/homeassistant/components/google/const.py b/homeassistant/components/google/const.py index d5cdabb0638..c01ff1ea48b 100644 --- a/homeassistant/components/google/const.py +++ b/homeassistant/components/google/const.py @@ -28,3 +28,6 @@ class FeatureAccess(Enum): def scope(self) -> str: """Google calendar scope for the feature.""" return self._scope + + +DEFAULT_FEATURE_ACCESS = FeatureAccess.read_write diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index e96a4c3fd5f..0991f4e5194 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -13,6 +13,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.google.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util.dt import utcnow from .conftest import ComponentSetup, YieldFixture @@ -254,7 +255,7 @@ async def test_existing_config_entry( async def test_missing_configuration( hass: HomeAssistant, ) -> None: - """Test can't configure when config entry already exists.""" + """Test can't configure when no authentication source is available.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -262,6 +263,32 @@ async def test_missing_configuration( assert result.get("reason") == "missing_configuration" +async def test_wrong_configuration( + hass: HomeAssistant, +) -> None: + """Test can't use the wrong type of authentication.""" + + # Google calendar flow currently only supports device auth + config_entry_oauth2_flow.async_register_implementation( + hass, + DOMAIN, + config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + DOMAIN, + "client-id", + "client-secret", + "http://example/authorize", + "http://example/token", + ), + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "abort" + assert result.get("reason") == "oauth_error" + + async def test_import_config_entry_from_existing_token( hass: HomeAssistant, mock_token_read: None,