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:
parent
619e7fbbce
commit
3526fd66df
6 changed files with 70 additions and 5 deletions
|
@ -9,7 +9,7 @@ from pathlib import Path
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
import aiohttp
|
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 import google_assistant, persistent_notification, webhook
|
||||||
from homeassistant.components.alexa import (
|
from homeassistant.components.alexa import (
|
||||||
|
@ -234,6 +234,8 @@ class CloudClient(Interface):
|
||||||
|
|
||||||
async def async_cloud_connect_update(self, connect: bool) -> None:
|
async def async_cloud_connect_update(self, connect: bool) -> None:
|
||||||
"""Process cloud remote message to client."""
|
"""Process cloud remote message to client."""
|
||||||
|
if not self._prefs.remote_allow_remote_enable:
|
||||||
|
raise RemoteActivationNotAllowed
|
||||||
await self._prefs.async_update(remote_enabled=connect)
|
await self._prefs.async_update(remote_enabled=connect)
|
||||||
|
|
||||||
async def async_cloud_connection_info(
|
async def async_cloud_connection_info(
|
||||||
|
@ -242,6 +244,7 @@ class CloudClient(Interface):
|
||||||
"""Process cloud connection info message to client."""
|
"""Process cloud connection info message to client."""
|
||||||
return {
|
return {
|
||||||
"remote": {
|
"remote": {
|
||||||
|
"can_enable": self._prefs.remote_allow_remote_enable,
|
||||||
"connected": self.cloud.remote.is_connected,
|
"connected": self.cloud.remote.is_connected,
|
||||||
"enabled": self._prefs.remote_enabled,
|
"enabled": self._prefs.remote_enabled,
|
||||||
"instance_domain": self.cloud.remote.instance_domain,
|
"instance_domain": self.cloud.remote.instance_domain,
|
||||||
|
|
|
@ -31,6 +31,7 @@ PREF_ALEXA_SETTINGS_VERSION = "alexa_settings_version"
|
||||||
PREF_GOOGLE_SETTINGS_VERSION = "google_settings_version"
|
PREF_GOOGLE_SETTINGS_VERSION = "google_settings_version"
|
||||||
PREF_TTS_DEFAULT_VOICE = "tts_default_voice"
|
PREF_TTS_DEFAULT_VOICE = "tts_default_voice"
|
||||||
PREF_GOOGLE_CONNECTED = "google_connected"
|
PREF_GOOGLE_CONNECTED = "google_connected"
|
||||||
|
PREF_REMOTE_ALLOW_REMOTE_ENABLE = "remote_allow_remote_enable"
|
||||||
DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "female")
|
DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "female")
|
||||||
DEFAULT_DISABLE_2FA = False
|
DEFAULT_DISABLE_2FA = False
|
||||||
DEFAULT_ALEXA_REPORT_STATE = True
|
DEFAULT_ALEXA_REPORT_STATE = True
|
||||||
|
|
|
@ -44,6 +44,7 @@ from .const import (
|
||||||
PREF_ENABLE_GOOGLE,
|
PREF_ENABLE_GOOGLE,
|
||||||
PREF_GOOGLE_REPORT_STATE,
|
PREF_GOOGLE_REPORT_STATE,
|
||||||
PREF_GOOGLE_SECURE_DEVICES_PIN,
|
PREF_GOOGLE_SECURE_DEVICES_PIN,
|
||||||
|
PREF_REMOTE_ALLOW_REMOTE_ENABLE,
|
||||||
PREF_TTS_DEFAULT_VOICE,
|
PREF_TTS_DEFAULT_VOICE,
|
||||||
REQUEST_TIMEOUT,
|
REQUEST_TIMEOUT,
|
||||||
)
|
)
|
||||||
|
@ -408,6 +409,7 @@ async def websocket_subscription(
|
||||||
vol.Optional(PREF_TTS_DEFAULT_VOICE): vol.All(
|
vol.Optional(PREF_TTS_DEFAULT_VOICE): vol.All(
|
||||||
vol.Coerce(tuple), vol.In(MAP_VOICE)
|
vol.Coerce(tuple), vol.In(MAP_VOICE)
|
||||||
),
|
),
|
||||||
|
vol.Optional(PREF_REMOTE_ALLOW_REMOTE_ENABLE): bool,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
|
|
|
@ -39,6 +39,7 @@ from .const import (
|
||||||
PREF_GOOGLE_SECURE_DEVICES_PIN,
|
PREF_GOOGLE_SECURE_DEVICES_PIN,
|
||||||
PREF_GOOGLE_SETTINGS_VERSION,
|
PREF_GOOGLE_SETTINGS_VERSION,
|
||||||
PREF_INSTANCE_ID,
|
PREF_INSTANCE_ID,
|
||||||
|
PREF_REMOTE_ALLOW_REMOTE_ENABLE,
|
||||||
PREF_REMOTE_DOMAIN,
|
PREF_REMOTE_DOMAIN,
|
||||||
PREF_TTS_DEFAULT_VOICE,
|
PREF_TTS_DEFAULT_VOICE,
|
||||||
PREF_USERNAME,
|
PREF_USERNAME,
|
||||||
|
@ -153,6 +154,7 @@ class CloudPreferences:
|
||||||
alexa_settings_version: int | UndefinedType = UNDEFINED,
|
alexa_settings_version: int | UndefinedType = UNDEFINED,
|
||||||
google_settings_version: int | UndefinedType = UNDEFINED,
|
google_settings_version: int | UndefinedType = UNDEFINED,
|
||||||
google_connected: bool | UndefinedType = UNDEFINED,
|
google_connected: bool | UndefinedType = UNDEFINED,
|
||||||
|
remote_allow_remote_enable: bool | UndefinedType = UNDEFINED,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update user preferences."""
|
"""Update user preferences."""
|
||||||
prefs = {**self._prefs}
|
prefs = {**self._prefs}
|
||||||
|
@ -171,6 +173,7 @@ class CloudPreferences:
|
||||||
(PREF_TTS_DEFAULT_VOICE, tts_default_voice),
|
(PREF_TTS_DEFAULT_VOICE, tts_default_voice),
|
||||||
(PREF_REMOTE_DOMAIN, remote_domain),
|
(PREF_REMOTE_DOMAIN, remote_domain),
|
||||||
(PREF_GOOGLE_CONNECTED, google_connected),
|
(PREF_GOOGLE_CONNECTED, google_connected),
|
||||||
|
(PREF_REMOTE_ALLOW_REMOTE_ENABLE, remote_allow_remote_enable),
|
||||||
):
|
):
|
||||||
if value is not UNDEFINED:
|
if value is not UNDEFINED:
|
||||||
prefs[key] = value
|
prefs[key] = value
|
||||||
|
@ -212,9 +215,16 @@ class CloudPreferences:
|
||||||
PREF_GOOGLE_DEFAULT_EXPOSE: self.google_default_expose,
|
PREF_GOOGLE_DEFAULT_EXPOSE: self.google_default_expose,
|
||||||
PREF_GOOGLE_REPORT_STATE: self.google_report_state,
|
PREF_GOOGLE_REPORT_STATE: self.google_report_state,
|
||||||
PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin,
|
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,
|
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
|
@property
|
||||||
def remote_enabled(self) -> bool:
|
def remote_enabled(self) -> bool:
|
||||||
"""Return if remote is enabled on start."""
|
"""Return if remote is enabled on start."""
|
||||||
|
@ -375,5 +385,6 @@ class CloudPreferences:
|
||||||
PREF_INSTANCE_ID: uuid.uuid4().hex,
|
PREF_INSTANCE_ID: uuid.uuid4().hex,
|
||||||
PREF_GOOGLE_SECURE_DEVICES_PIN: None,
|
PREF_GOOGLE_SECURE_DEVICES_PIN: None,
|
||||||
PREF_REMOTE_DOMAIN: None,
|
PREF_REMOTE_DOMAIN: None,
|
||||||
|
PREF_REMOTE_ALLOW_REMOTE_ENABLE: True,
|
||||||
PREF_USERNAME: username,
|
PREF_USERNAME: username,
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
from hass_nabucasa.client import RemoteActivationNotAllowed
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.cloud import DOMAIN
|
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({})
|
response = await cloud.client.async_cloud_connection_info({})
|
||||||
|
|
||||||
assert response == {
|
assert response == {
|
||||||
|
"instance_id": "12345678901234567890",
|
||||||
"remote": {
|
"remote": {
|
||||||
|
"alias": None,
|
||||||
|
"can_enable": True,
|
||||||
"connected": False,
|
"connected": False,
|
||||||
"enabled": False,
|
"enabled": False,
|
||||||
"instance_domain": None,
|
"instance_domain": None,
|
||||||
"alias": None,
|
|
||||||
},
|
},
|
||||||
"version": HA_VERSION,
|
"version": HA_VERSION,
|
||||||
"instance_id": "12345678901234567890",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -481,6 +483,19 @@ async def test_remote_enable(hass: HomeAssistant) -> None:
|
||||||
client = CloudClient(hass, prefs, None, {}, {})
|
client = CloudClient(hass, prefs, None, {}, {})
|
||||||
client.cloud = MagicMock(is_logged_in=True, username="mock-username")
|
client.cloud = MagicMock(is_logged_in=True, username="mock-username")
|
||||||
|
|
||||||
result = await client.async_cloud_connect_update(True)
|
await client.async_cloud_connect_update(True)
|
||||||
assert result is None
|
|
||||||
prefs.async_update.assert_called_once_with(remote_enabled=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()
|
||||||
|
|
|
@ -734,6 +734,7 @@ async def test_websocket_status(
|
||||||
"alexa_default_expose": DEFAULT_EXPOSED_DOMAINS,
|
"alexa_default_expose": DEFAULT_EXPOSED_DOMAINS,
|
||||||
"alexa_report_state": True,
|
"alexa_report_state": True,
|
||||||
"google_report_state": True,
|
"google_report_state": True,
|
||||||
|
"remote_allow_remote_enable": True,
|
||||||
"remote_enabled": False,
|
"remote_enabled": False,
|
||||||
"tts_default_voice": ["en-US", "female"],
|
"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.google_enabled
|
||||||
assert cloud.client.prefs.alexa_enabled
|
assert cloud.client.prefs.alexa_enabled
|
||||||
assert cloud.client.prefs.google_secure_devices_pin is None
|
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)
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
@ -864,6 +866,7 @@ async def test_websocket_update_preferences(
|
||||||
"google_enabled": False,
|
"google_enabled": False,
|
||||||
"google_secure_devices_pin": "1234",
|
"google_secure_devices_pin": "1234",
|
||||||
"tts_default_voice": ["en-GB", "male"],
|
"tts_default_voice": ["en-GB", "male"],
|
||||||
|
"remote_allow_remote_enable": False,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
response = await client.receive_json()
|
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.google_enabled
|
||||||
assert not cloud.client.prefs.alexa_enabled
|
assert not cloud.client.prefs.alexa_enabled
|
||||||
assert cloud.client.prefs.google_secure_devices_pin == "1234"
|
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")
|
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
|
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(
|
async def test_list_google_entities(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entity_registry: er.EntityRegistry,
|
entity_registry: er.EntityRegistry,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue