From 29131a655dc740287a0d9e8443ca2cc078e4bf63 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 17 Mar 2019 19:13:06 -0700 Subject: [PATCH] Allow non-admins to listen to certain events (#22137) --- .../components/websocket_api/commands.py | 39 +++++++--- .../components/websocket_api/permissions.py | 23 ++++++ .../components/websocket_api/test_commands.py | 73 +++++++++++++++++++ 3 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/websocket_api/permissions.py diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index b64fac0ed51..32bbd90aad1 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -1,7 +1,9 @@ """Commands part of Websocket API.""" import voluptuous as vol -from homeassistant.const import MATCH_ALL, EVENT_TIME_CHANGED +from homeassistant.auth.permissions.const import POLICY_READ +from homeassistant.const import ( + MATCH_ALL, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED) from homeassistant.core import callback, DOMAIN as HASS_DOMAIN from homeassistant.exceptions import Unauthorized, ServiceNotFound, \ HomeAssistantError @@ -42,20 +44,37 @@ def handle_subscribe_events(hass, connection, msg): Async friendly. """ - if not connection.user.is_admin: + from .permissions import SUBSCRIBE_WHITELIST + + event_type = msg['event_type'] + + if (event_type not in SUBSCRIBE_WHITELIST and + not connection.user.is_admin): raise Unauthorized - async def forward_events(event): - """Forward events to websocket.""" - if event.event_type == EVENT_TIME_CHANGED: - return + if event_type == EVENT_STATE_CHANGED: + @callback + def forward_events(event): + """Forward state changed events to websocket.""" + if not connection.user.permissions.check_entity( + event.data['entity_id'], POLICY_READ): + return - connection.send_message(messages.event_message( - msg['id'], event.as_dict() - )) + connection.send_message(messages.event_message(msg['id'], event)) + + else: + @callback + def forward_events(event): + """Forward events to websocket.""" + if event.event_type == EVENT_TIME_CHANGED: + return + + connection.send_message(messages.event_message( + msg['id'], event.as_dict() + )) connection.subscriptions[msg['id']] = hass.bus.async_listen( - msg['event_type'], forward_events) + event_type, forward_events) connection.send_message(messages.result_message(msg['id'])) diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py new file mode 100644 index 00000000000..b98b21d184e --- /dev/null +++ b/homeassistant/components/websocket_api/permissions.py @@ -0,0 +1,23 @@ +"""Permission constants for the websocket API. + +Separate file to avoid circular imports. +""" +from homeassistant.const import ( + EVENT_COMPONENT_LOADED, + EVENT_SERVICE_REGISTERED, + EVENT_SERVICE_REMOVED, + EVENT_STATE_CHANGED, + EVENT_THEMES_UPDATED) +from homeassistant.components.persistent_notification import ( + EVENT_PERSISTENT_NOTIFICATIONS_UPDATED) + +# These are events that do not contain any sensitive data +# Except for state_changed, which is handled accordingly. +SUBSCRIBE_WHITELIST = { + EVENT_COMPONENT_LOADED, + EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, + EVENT_SERVICE_REGISTERED, + EVENT_SERVICE_REMOVED, + EVENT_STATE_CHANGED, + EVENT_THEMES_UPDATED, +} diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 8e0f751abed..4f3be31b22c 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -333,3 +333,76 @@ async def test_get_states_not_allows_nan(hass, websocket_client): msg = await websocket_client.receive_json() assert not msg['success'] assert msg['error']['code'] == const.ERR_UNKNOWN_ERROR + + +async def test_subscribe_unsubscribe_events_whitelist( + hass, websocket_client, hass_admin_user): + """Test subscribe/unsubscribe events on whitelist.""" + hass_admin_user.groups = [] + + await websocket_client.send_json({ + 'id': 5, + 'type': 'subscribe_events', + 'event_type': 'not-in-whitelist' + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 5 + assert msg['type'] == const.TYPE_RESULT + assert not msg['success'] + assert msg['error']['code'] == 'unauthorized' + + await websocket_client.send_json({ + 'id': 6, + 'type': 'subscribe_events', + 'event_type': 'themes_updated' + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 6 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] + + hass.bus.async_fire('themes_updated') + + with timeout(3, loop=hass.loop): + msg = await websocket_client.receive_json() + + assert msg['id'] == 6 + assert msg['type'] == 'event' + event = msg['event'] + assert event['event_type'] == 'themes_updated' + assert event['origin'] == 'LOCAL' + + +async def test_subscribe_unsubscribe_events_state_changed( + hass, websocket_client, hass_admin_user): + """Test subscribe/unsubscribe state_changed events.""" + hass_admin_user.groups = [] + hass_admin_user.mock_policy({ + 'entities': { + 'entity_ids': { + 'light.permitted': True + } + } + }) + + await websocket_client.send_json({ + 'id': 7, + 'type': 'subscribe_events', + 'event_type': 'state_changed' + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 7 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] + + hass.states.async_set('light.not_permitted', 'on') + hass.states.async_set('light.permitted', 'on') + + msg = await websocket_client.receive_json() + assert msg['id'] == 7 + assert msg['type'] == 'event' + assert msg['event']['event_type'] == 'state_changed' + assert msg['event']['data']['entity_id'] == 'light.permitted'