Skip to content

Commit

Permalink
Allow blocking remote enabling of HA Cloud remote
Browse files Browse the repository at this point in the history
  • Loading branch information
emontnemery committed Feb 5, 2024
1 parent ebda047 commit 74739cc
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 @@ -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(
Expand All @@ -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,
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 @@ -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
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 @@ -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,
Expand Down Expand Up @@ -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}
Expand All @@ -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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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,
}
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 == {

Check failure on line 379 in tests/components/cloud/test_client.py

View workflow job for this annotation

GitHub Actions / Run tests Python 3.11 (2)

test_cloud_connection_info AssertionError: assert {'instance_id...024.3.0.dev0'} == {'instance_id...024.3.0.dev0'} Omitting 2 identical items, use -vv to show Differing items: {'remote': {'alias': None, 'can_enable': True, 'connected': False, 'enabled': False, ...}} != {'remote': {'alias': None, 'can_activate': True, 'connected': False, 'enabled': False, ...}} Full diff: { 'instance_id': '12345678901234567890', 'remote': {'alias': None, - 'can_activate': True, ? ^^^^^^ + 'can_enable': True, ? ++ ^^ 'connected': False, 'enabled': False, 'instance_domain': None}, 'version': '2024.3.0.dev0', }

Check failure on line 379 in tests/components/cloud/test_client.py

View workflow job for this annotation

GitHub Actions / Run tests Python 3.12 (2)

test_cloud_connection_info AssertionError: assert {'instance_id...024.3.0.dev0'} == {'instance_id...024.3.0.dev0'} Omitting 2 identical items, use -vv to show Differing items: {'remote': {'alias': None, 'can_enable': True, 'connected': False, 'enabled': False, ...}} != {'remote': {'alias': None, 'can_activate': True, 'connected': False, 'enabled': False, ...}} Full diff: { 'instance_id': '12345678901234567890', 'remote': {'alias': None, - 'can_activate': True, ? ^^^^^^ + 'can_enable': True, ? ++ ^^ 'connected': False, 'enabled': False, 'instance_domain': None}, 'version': '2024.3.0.dev0', }
"instance_id": "12345678901234567890",
"remote": {
"alias": None,
"can_activate": True,
"connected": False,
"enabled": False,
"instance_domain": None,
"alias": None,
},
"version": HA_VERSION,
"instance_id": "12345678901234567890",
}


Expand Down Expand Up @@ -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()
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 74739cc

Please sign in to comment.