Skip to content

Commit

Permalink
Redact sensitive data in alexa debug logging (#107676)
Browse files Browse the repository at this point in the history
* Redact sensitive data in alexa debug logging

* Add wrappers to diagnostics module

* Test http api log is redacted
  • Loading branch information
jbouwh authored Jan 10, 2024
1 parent 956921a commit 5bdcbc4
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 14 deletions.
9 changes: 5 additions & 4 deletions homeassistant/components/alexa/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
from homeassistant.helpers.storage import Store
from homeassistant.util import dt as dt_util

from .const import STORAGE_ACCESS_TOKEN, STORAGE_REFRESH_TOKEN
from .diagnostics import async_redact_lwa_params

_LOGGER = logging.getLogger(__name__)

LWA_TOKEN_URI = "https://api.amazon.com/auth/o2/token"
Expand All @@ -24,8 +27,6 @@
STORAGE_KEY = "alexa_auth"
STORAGE_VERSION = 1
STORAGE_EXPIRE_TIME = "expire_time"
STORAGE_ACCESS_TOKEN = "access_token"
STORAGE_REFRESH_TOKEN = "refresh_token"


class Auth:
Expand Down Expand Up @@ -56,7 +57,7 @@ async def async_do_auth(self, accept_grant_code: str) -> str | None:
}
_LOGGER.debug(
"Calling LWA to get the access token (first time), with: %s",
json.dumps(lwa_params),
json.dumps(async_redact_lwa_params(lwa_params)),
)

return await self._async_request_new_token(lwa_params)
Expand Down Expand Up @@ -133,7 +134,7 @@ async def _async_request_new_token(self, lwa_params: dict[str, str]) -> str | No
return None

response_json = await response.json()
_LOGGER.debug("LWA response body : %s", response_json)
_LOGGER.debug("LWA response body : %s", async_redact_lwa_params(response_json))

access_token: str = response_json["access_token"]
refresh_token: str = response_json["refresh_token"]
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/alexa/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@
# we add PRESET_MODE_NA if a fan / humidifier has only one preset_mode
PRESET_MODE_NA = "-"

STORAGE_ACCESS_TOKEN = "access_token"
STORAGE_REFRESH_TOKEN = "refresh_token"


class Cause:
"""Possible causes for property changes.
Expand Down
34 changes: 34 additions & 0 deletions homeassistant/components/alexa/diagnostics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Diagnostics helpers for Alexa."""

from __future__ import annotations

from collections.abc import Mapping
from typing import Any

from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.core import callback

STORAGE_ACCESS_TOKEN = "access_token"
STORAGE_REFRESH_TOKEN = "refresh_token"

TO_REDACT_LWA = {
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
STORAGE_ACCESS_TOKEN,
STORAGE_REFRESH_TOKEN,
}

TO_REDACT_AUTH = {"correlationToken", "token"}


@callback
def async_redact_lwa_params(lwa_params: dict[str, str]) -> dict[str, str]:
"""Redact lwa_params."""
return async_redact_data(lwa_params, TO_REDACT_LWA)


@callback
def async_redact_auth_data(mapping: Mapping[Any, Any]) -> dict[str, str]:
"""React auth data."""
return async_redact_data(mapping, TO_REDACT_AUTH)
1 change: 0 additions & 1 deletion homeassistant/components/alexa/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ async def async_api_accept_grant(
Async friendly.
"""
auth_code: str = directive.payload["grant"]["code"]
_LOGGER.debug("AcceptGrant code: %s", auth_code)

if config.supports_auth:
await config.async_accept_grant(auth_code)
Expand Down
14 changes: 12 additions & 2 deletions homeassistant/components/alexa/smart_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
CONF_LOCALE,
EVENT_ALEXA_SMART_HOME,
)
from .diagnostics import async_redact_auth_data
from .errors import AlexaBridgeUnreachableError, AlexaError
from .handlers import HANDLERS
from .state_report import AlexaDirective
Expand Down Expand Up @@ -149,12 +150,21 @@ async def post(self, request: HomeAssistantRequest) -> web.Response | bytes:
user: User = request["hass_user"]
message: dict[str, Any] = await request.json()

_LOGGER.debug("Received Alexa Smart Home request: %s", message)
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug(
"Received Alexa Smart Home request: %s",
async_redact_auth_data(message),
)

response = await async_handle_message(
hass, self.smart_home_config, message, context=core.Context(user_id=user.id)
)
_LOGGER.debug("Sending Alexa Smart Home response: %s", response)
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug(
"Sending Alexa Smart Home response: %s",
async_redact_auth_data(response),
)

return b"" if response is None else self.json(response)


Expand Down
11 changes: 9 additions & 2 deletions homeassistant/components/alexa/state_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
DOMAIN,
Cause,
)
from .diagnostics import async_redact_auth_data
from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id
from .errors import AlexaInvalidEndpointError, NoTokenAvailable, RequireRelink

Expand All @@ -43,6 +44,8 @@
_LOGGER = logging.getLogger(__name__)
DEFAULT_TIMEOUT = 10

TO_REDACT = {"correlationToken", "token"}


class AlexaDirective:
"""An incoming Alexa directive."""
Expand Down Expand Up @@ -379,7 +382,9 @@ async def async_send_changereport_message(
response_text = await response.text()

if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug("Sent: %s", json.dumps(message_serialized))
_LOGGER.debug(
"Sent: %s", json.dumps(async_redact_auth_data(message_serialized))
)
_LOGGER.debug("Received (%s): %s", response.status, response_text)

if response.status == HTTPStatus.ACCEPTED:
Expand Down Expand Up @@ -533,7 +538,9 @@ async def async_send_doorbell_event_message(
response_text = await response.text()

if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug("Sent: %s", json.dumps(message_serialized))
_LOGGER.debug(
"Sent: %s", json.dumps(async_redact_auth_data(message_serialized))
)
_LOGGER.debug("Received (%s): %s", response.status, response_text)

if response.status == HTTPStatus.ACCEPTED:
Expand Down
15 changes: 10 additions & 5 deletions tests/components/alexa/test_smart_home_http.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Test Smart Home HTTP endpoints."""
from http import HTTPStatus
import json
import logging
from typing import Any

import pytest
Expand Down Expand Up @@ -44,11 +45,16 @@ async def do_http_discovery(config, hass, hass_client):
],
)
async def test_http_api(
hass: HomeAssistant, hass_client: ClientSessionGenerator, config: dict[str, Any]
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
hass_client: ClientSessionGenerator,
config: dict[str, Any],
) -> None:
"""With `smart_home:` HTTP API is exposed."""
response = await do_http_discovery(config, hass, hass_client)
response_data = await response.json()
"""With `smart_home:` HTTP API is exposed and debug log is redacted."""
with caplog.at_level(logging.DEBUG):
response = await do_http_discovery(config, hass, hass_client)
response_data = await response.json()
assert "'correlationToken': '**REDACTED**'" in caplog.text

# Here we're testing just the HTTP view glue -- details of discovery are
# covered in other tests.
Expand All @@ -61,5 +67,4 @@ async def test_http_api_disabled(
"""Without `smart_home:`, the HTTP API is disabled."""
config = {"alexa": {}}
response = await do_http_discovery(config, hass, hass_client)

assert response.status == HTTPStatus.NOT_FOUND

0 comments on commit 5bdcbc4

Please sign in to comment.