From 74739cc81a87c1565b41de20e8b730636a4275e4 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 5 Feb 2024 13:48:06 +0100 Subject: [PATCH] Allow blocking remote enabling of HA Cloud remote --- homeassistant/components/cloud/client.py | 5 +++- homeassistant/components/cloud/const.py | 1 + homeassistant/components/cloud/http_api.py | 2 ++ homeassistant/components/cloud/prefs.py | 11 ++++++++ tests/components/cloud/test_client.py | 23 ++++++++++++--- tests/components/cloud/test_http_api.py | 33 ++++++++++++++++++++++ 6 files changed, 70 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 8cf79d20c5d82d..85f7395fb7a482 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -9,7 +9,7 @@ from typing import Any, Literal import aiohttp -from hass_nabucasa.client import CloudClient as Interface +from hass_nabucasa.client import CloudClient as Interface, RemoteActivationNotAllowed from homeassistant.components import google_assistant, persistent_notification, webhook from homeassistant.components.alexa import ( @@ -230,6 +230,8 @@ def dispatcher_message(self, identifier: str, data: Any = None) -> None: async def async_cloud_connect_update(self, connect: bool) -> None: """Process cloud remote message to client.""" + if not self._prefs.remote_allow_remote_enable: + raise RemoteActivationNotAllowed await self._prefs.async_update(remote_enabled=connect) async def async_cloud_connection_info( @@ -238,6 +240,7 @@ async def async_cloud_connection_info( """Process cloud connection info message to client.""" return { "remote": { + "can_enable": self._prefs.remote_allow_remote_enable, "connected": self.cloud.remote.is_connected, "enabled": self._prefs.remote_enabled, "instance_domain": self.cloud.remote.instance_domain, diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 97d2345f16bb21..460c0c6859ba6c 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -30,6 +30,7 @@ PREF_ALEXA_SETTINGS_VERSION = "alexa_settings_version" PREF_GOOGLE_SETTINGS_VERSION = "google_settings_version" PREF_TTS_DEFAULT_VOICE = "tts_default_voice" +PREF_REMOTE_ALLOW_REMOTE_ENABLE = "remote_allow_remote_enable" DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "female") DEFAULT_DISABLE_2FA = False DEFAULT_ALEXA_REPORT_STATE = True diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index be3271a88a37dd..36a00fd1431459 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -44,6 +44,7 @@ PREF_ENABLE_GOOGLE, PREF_GOOGLE_REPORT_STATE, PREF_GOOGLE_SECURE_DEVICES_PIN, + PREF_REMOTE_ALLOW_REMOTE_ENABLE, PREF_TTS_DEFAULT_VOICE, REQUEST_TIMEOUT, ) @@ -408,6 +409,7 @@ async def websocket_subscription( vol.Optional(PREF_TTS_DEFAULT_VOICE): vol.All( vol.Coerce(tuple), vol.In(MAP_VOICE) ), + vol.Optional(PREF_REMOTE_ALLOW_REMOTE_ENABLE): bool, } ) @websocket_api.async_response diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index af5f9213e4dd78..3ce386863a1f61 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -35,6 +35,7 @@ PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_GOOGLE_SETTINGS_VERSION, PREF_INSTANCE_ID, + PREF_REMOTE_ALLOW_REMOTE_ENABLE, PREF_REMOTE_DOMAIN, PREF_TTS_DEFAULT_VOICE, PREF_USERNAME, @@ -131,6 +132,7 @@ async def async_update( remote_domain: str | None | UndefinedType = UNDEFINED, alexa_settings_version: int | UndefinedType = UNDEFINED, google_settings_version: int | UndefinedType = UNDEFINED, + remote_allow_remote_enable: bool | UndefinedType = UNDEFINED, ) -> None: """Update user preferences.""" prefs = {**self._prefs} @@ -148,6 +150,7 @@ async def async_update( (PREF_GOOGLE_SETTINGS_VERSION, google_settings_version), (PREF_TTS_DEFAULT_VOICE, tts_default_voice), (PREF_REMOTE_DOMAIN, remote_domain), + (PREF_REMOTE_ALLOW_REMOTE_ENABLE, remote_allow_remote_enable), ): if value is not UNDEFINED: prefs[key] = value @@ -189,9 +192,16 @@ def as_dict(self) -> dict[str, Any]: PREF_GOOGLE_DEFAULT_EXPOSE: self.google_default_expose, PREF_GOOGLE_REPORT_STATE: self.google_report_state, PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin, + PREF_REMOTE_ALLOW_REMOTE_ENABLE: self.remote_allow_remote_enable, PREF_TTS_DEFAULT_VOICE: self.tts_default_voice, } + @property + def remote_allow_remote_enable(self) -> bool: + """Return if it's allowed to remotely activate remote.""" + allowed: bool = self._prefs.get(PREF_REMOTE_ALLOW_REMOTE_ENABLE, True) + return allowed + @property def remote_enabled(self) -> bool: """Return if remote is enabled on start.""" @@ -345,5 +355,6 @@ def _empty_config(username: str) -> dict[str, Any]: PREF_INSTANCE_ID: uuid.uuid4().hex, PREF_GOOGLE_SECURE_DEVICES_PIN: None, PREF_REMOTE_DOMAIN: None, + PREF_REMOTE_ALLOW_REMOTE_ENABLE: True, PREF_USERNAME: username, } diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index c8c0e40a5bb2e3..a667802b28ddca 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -4,6 +4,7 @@ import aiohttp from aiohttp import web +from hass_nabucasa.client import RemoteActivationNotAllowed import pytest from homeassistant.components.cloud import DOMAIN @@ -376,14 +377,15 @@ async def test_cloud_connection_info(hass: HomeAssistant) -> None: response = await cloud.client.async_cloud_connection_info({}) assert response == { + "instance_id": "12345678901234567890", "remote": { + "alias": None, + "can_activate": True, "connected": False, "enabled": False, "instance_domain": None, - "alias": None, }, "version": HA_VERSION, - "instance_id": "12345678901234567890", } @@ -480,6 +482,19 @@ async def test_remote_enable(hass: HomeAssistant) -> None: client = CloudClient(hass, prefs, None, {}, {}) client.cloud = MagicMock(is_logged_in=True, username="mock-username") - result = await client.async_cloud_connect_update(True) - assert result is None + await client.async_cloud_connect_update(True) prefs.async_update.assert_called_once_with(remote_enabled=True) + + +async def test_remote_enable_not_allowed(hass: HomeAssistant) -> None: + """Test enabling remote UI.""" + prefs = MagicMock( + async_update=AsyncMock(return_value=None), + remote_allow_remote_enable=False, + ) + client = CloudClient(hass, prefs, None, {}, {}) + client.cloud = MagicMock(is_logged_in=True, username="mock-username") + + with pytest.raises(RemoteActivationNotAllowed): + await client.async_cloud_connect_update(True) + prefs.async_update.assert_not_called() diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 78b06874d6d9f7..8c93c1251d97d5 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -734,6 +734,7 @@ async def test_websocket_status( "alexa_default_expose": DEFAULT_EXPOSED_DOMAINS, "alexa_report_state": True, "google_report_state": True, + "remote_allow_remote_enable": True, "remote_enabled": False, "tts_default_voice": ["en-US", "female"], }, @@ -853,6 +854,7 @@ async def test_websocket_update_preferences( assert cloud.client.prefs.google_enabled assert cloud.client.prefs.alexa_enabled assert cloud.client.prefs.google_secure_devices_pin is None + assert cloud.client.prefs.remote_allow_remote_enable is True client = await hass_ws_client(hass) @@ -864,6 +866,7 @@ async def test_websocket_update_preferences( "google_enabled": False, "google_secure_devices_pin": "1234", "tts_default_voice": ["en-GB", "male"], + "remote_allow_remote_enable": False, } ) response = await client.receive_json() @@ -872,6 +875,7 @@ async def test_websocket_update_preferences( assert not cloud.client.prefs.google_enabled assert not cloud.client.prefs.alexa_enabled assert cloud.client.prefs.google_secure_devices_pin == "1234" + assert cloud.client.prefs.remote_allow_remote_enable is False assert cloud.client.prefs.tts_default_voice == ("en-GB", "male") @@ -1032,6 +1036,35 @@ async def test_enabling_remote( assert mock_disconnect.call_count == 1 +async def test_enabling_remote_remote_activation_not_allowed( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + cloud: MagicMock, + setup_cloud: None, +) -> None: + """Test we can enable remote UI locally when blocked remotely.""" + client = await hass_ws_client(hass) + mock_connect = cloud.remote.connect + assert not cloud.client.remote_autostart + cloud.client.prefs.async_update(remote_allow_remote_enable=False) + + await client.send_json({"id": 5, "type": "cloud/remote/connect"}) + response = await client.receive_json() + + assert response["success"] + assert cloud.client.remote_autostart + assert mock_connect.call_count == 1 + + mock_disconnect = cloud.remote.disconnect + + await client.send_json({"id": 6, "type": "cloud/remote/disconnect"}) + response = await client.receive_json() + + assert response["success"] + assert not cloud.client.remote_autostart + assert mock_disconnect.call_count == 1 + + async def test_list_google_entities( hass: HomeAssistant, entity_registry: er.EntityRegistry,