Add option to block remote enabling of HA Cloud remote (#109700)

* Allow blocking remote enabling of HA Cloud remote

* Fix test
This commit is contained in:
Erik Montnemery 2024-02-15 17:26:06 +01:00 committed by GitHub
parent 619e7fbbce
commit 3526fd66df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 70 additions and 5 deletions

View file

@ -9,7 +9,7 @@ from pathlib import Path
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 (
@ -234,6 +234,8 @@ class CloudClient(Interface):
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(
@ -242,6 +244,7 @@ class CloudClient(Interface):
"""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,

View file

@ -31,6 +31,7 @@ PREF_ALEXA_SETTINGS_VERSION = "alexa_settings_version"
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

View file

@ -44,6 +44,7 @@ from .const import (
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

View file

@ -39,6 +39,7 @@ from .const import (
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,
@ -153,6 +154,7 @@ class CloudPreferences:
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}
@ -171,6 +173,7 @@ class CloudPreferences:
(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
@ -212,9 +215,16 @@ class CloudPreferences:
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."""
@ -375,5 +385,6 @@ class CloudPreferences:
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,
}

View file

@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch
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_enable": True,
"connected": False,
"enabled": False,
"instance_domain": None,
"alias": None,
},
"version": HA_VERSION,
"instance_id": "12345678901234567890",
}
@ -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()

View file

@ -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,