Skip to content

Commit

Permalink
Add option to block remote enabling of HA Cloud remote (#109700)
Browse files Browse the repository at this point in the history
* Allow blocking remote enabling of HA Cloud remote

* Fix test
  • Loading branch information
emontnemery authored Feb 15, 2024
1 parent 619e7fb commit 3526fd6
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 5 deletions.
5 changes: 4 additions & 1 deletion homeassistant/components/cloud/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -234,6 +234,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(
Expand All @@ -242,6 +244,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,
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/cloud/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
PREF_GOOGLE_SETTINGS_VERSION = "google_settings_version"
PREF_TTS_DEFAULT_VOICE = "tts_default_voice"
PREF_GOOGLE_CONNECTED = "google_connected"
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
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/cloud/http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions homeassistant/components/cloud/prefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,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,
Expand Down Expand Up @@ -153,6 +154,7 @@ async def async_update(
alexa_settings_version: int | UndefinedType = UNDEFINED,
google_settings_version: int | UndefinedType = UNDEFINED,
google_connected: bool | UndefinedType = UNDEFINED,
remote_allow_remote_enable: bool | UndefinedType = UNDEFINED,
) -> None:
"""Update user preferences."""
prefs = {**self._prefs}
Expand All @@ -171,6 +173,7 @@ async def async_update(
(PREF_TTS_DEFAULT_VOICE, tts_default_voice),
(PREF_REMOTE_DOMAIN, remote_domain),
(PREF_GOOGLE_CONNECTED, google_connected),
(PREF_REMOTE_ALLOW_REMOTE_ENABLE, remote_allow_remote_enable),
):
if value is not UNDEFINED:
prefs[key] = value
Expand Down Expand Up @@ -212,9 +215,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."""
Expand Down Expand Up @@ -375,5 +385,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,
}
23 changes: 19 additions & 4 deletions tests/components/cloud/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import aiohttp
from aiohttp import web
from hass_nabucasa.client import RemoteActivationNotAllowed
import pytest

from homeassistant.components.cloud import DOMAIN
Expand Down Expand Up @@ -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_enable": True,
"connected": False,
"enabled": False,
"instance_domain": None,
"alias": None,
},
"version": HA_VERSION,
"instance_id": "12345678901234567890",
}


Expand Down Expand Up @@ -481,6 +483,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()
33 changes: 33 additions & 0 deletions tests/components/cloud/test_http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
},
Expand Down Expand Up @@ -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)

Expand All @@ -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()
Expand All @@ -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")


Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 3526fd6

Please sign in to comment.