diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index 81b0f670058..e8efa8a4752 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -17,6 +17,7 @@ from .const import ( CONF_ENTITY_CONFIG, CONF_FILTER, CONF_LOCALE, + CONF_PASSWORD, CONF_SUPPORTED_LOCALES, CONF_TEXT, CONF_TITLE, @@ -56,6 +57,7 @@ CONFIG_SCHEMA = vol.Schema( { DOMAIN: { CONF_FLASH_BRIEFINGS: { + vol.Required(CONF_PASSWORD): cv.string, cv.string: vol.All( cv.ensure_list, [ @@ -67,7 +69,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_DISPLAY_URL): cv.template, } ], - ) + ), }, # vol.Optional here would mean we couldn't distinguish between an empty # smart_home: and none at all. diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 50e3edb475c..a5a1cde2e15 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -19,6 +19,7 @@ CONF_FILTER = "filter" CONF_ENTITY_CONFIG = "entity_config" CONF_ENDPOINT = "endpoint" CONF_LOCALE = "locale" +CONF_PASSWORD = "password" ATTR_UID = "uid" ATTR_UPDATE_DATE = "updateDate" @@ -39,6 +40,7 @@ API_HEADER = "header" API_PAYLOAD = "payload" API_SCOPE = "scope" API_CHANGE = "change" +API_PASSWORD = "password" CONF_DESCRIPTION = "description" CONF_DISPLAY_CATEGORIES = "display_categories" diff --git a/homeassistant/components/alexa/flash_briefings.py b/homeassistant/components/alexa/flash_briefings.py index 1205fd58091..ed3da1d10be 100644 --- a/homeassistant/components/alexa/flash_briefings.py +++ b/homeassistant/components/alexa/flash_briefings.py @@ -1,15 +1,17 @@ """Support for Alexa skill service end point.""" import copy +import hmac import logging import uuid from homeassistant.components import http -from homeassistant.const import HTTP_NOT_FOUND +from homeassistant.const import HTTP_NOT_FOUND, HTTP_UNAUTHORIZED from homeassistant.core import callback from homeassistant.helpers import template import homeassistant.util.dt as dt_util from .const import ( + API_PASSWORD, ATTR_MAIN_TEXT, ATTR_REDIRECTION_URL, ATTR_STREAM_URL, @@ -18,6 +20,7 @@ from .const import ( ATTR_UPDATE_DATE, CONF_AUDIO, CONF_DISPLAY_URL, + CONF_PASSWORD, CONF_TEXT, CONF_TITLE, CONF_UID, @@ -39,6 +42,7 @@ class AlexaFlashBriefingView(http.HomeAssistantView): """Handle Alexa Flash Briefing skill requests.""" url = FLASH_BRIEFINGS_API_ENDPOINT + requires_auth = False name = "api:alexa:flash_briefings" def __init__(self, hass, flash_briefings): @@ -52,7 +56,20 @@ class AlexaFlashBriefingView(http.HomeAssistantView): """Handle Alexa Flash Briefing request.""" _LOGGER.debug("Received Alexa flash briefing request for: %s", briefing_id) - if self.flash_briefings.get(briefing_id) is None: + if request.query.get(API_PASSWORD) is None: + err = "No password provided for Alexa flash briefing: %s" + _LOGGER.error(err, briefing_id) + return b"", HTTP_UNAUTHORIZED + + if not hmac.compare_digest( + request.query[API_PASSWORD].encode("utf-8"), + self.flash_briefings[CONF_PASSWORD].encode("utf-8"), + ): + err = "Wrong password for Alexa flash briefing: %s" + _LOGGER.error(err, briefing_id) + return b"", HTTP_UNAUTHORIZED + + if not isinstance(self.flash_briefings.get(briefing_id), list): err = "No configured Alexa flash briefing was found for: %s" _LOGGER.error(err, briefing_id) return b"", HTTP_NOT_FOUND diff --git a/tests/components/alexa/test_flash_briefings.py b/tests/components/alexa/test_flash_briefings.py index 14dbe7336fb..7ab75d8c037 100644 --- a/tests/components/alexa/test_flash_briefings.py +++ b/tests/components/alexa/test_flash_briefings.py @@ -6,7 +6,7 @@ import pytest from homeassistant.components import alexa from homeassistant.components.alexa import const -from homeassistant.const import HTTP_NOT_FOUND +from homeassistant.const import HTTP_NOT_FOUND, HTTP_UNAUTHORIZED from homeassistant.core import callback from homeassistant.setup import async_setup_component @@ -39,6 +39,7 @@ def alexa_client(loop, hass, hass_client): "homeassistant": {}, "alexa": { "flash_briefings": { + "password": "pass/abc", "weather": [ { "title": "Weekly forecast", @@ -63,8 +64,11 @@ def alexa_client(loop, hass, hass_client): return loop.run_until_complete(hass_client()) -def _flash_briefing_req(client, briefing_id): - return client.get(f"/api/alexa/flash_briefings/{briefing_id}") +def _flash_briefing_req(client, briefing_id, password="pass%2Fabc"): + if password is None: + return client.get(f"/api/alexa/flash_briefings/{briefing_id}") + + return client.get(f"/api/alexa/flash_briefings/{briefing_id}?password={password}") async def test_flash_briefing_invalid_id(alexa_client): @@ -75,6 +79,30 @@ async def test_flash_briefing_invalid_id(alexa_client): assert text == "" +async def test_flash_briefing_no_password(alexa_client): + """Test for no Flash Briefing password.""" + req = await _flash_briefing_req(alexa_client, "weather", password=None) + assert req.status == HTTP_UNAUTHORIZED + text = await req.text() + assert text == "" + + +async def test_flash_briefing_invalid_password(alexa_client): + """Test an invalid Flash Briefing password.""" + req = await _flash_briefing_req(alexa_client, "weather", password="wrongpass") + assert req.status == HTTP_UNAUTHORIZED + text = await req.text() + assert text == "" + + +async def test_flash_briefing_request_for_password(alexa_client): + """Test for "password" Flash Briefing.""" + req = await _flash_briefing_req(alexa_client, "password") + assert req.status == HTTP_NOT_FOUND + text = await req.text() + assert text == "" + + async def test_flash_briefing_date_from_str(alexa_client): """Test the response has a valid date parsed from string.""" req = await _flash_briefing_req(alexa_client, "weather")