Redact sensitive data in alexa debug logging (#107676)

* Redact sensitive data in alexa debug logging

* Add wrappers to diagnostics module

* Test http api log is redacted
This commit is contained in:
Jan Bouwhuis 2024-01-10 16:20:47 +01:00 committed by Franck Nijhof
parent f993e923a3
commit 7f8a157788
No known key found for this signature in database
GPG key ID: D62583BA8AB11CA3
7 changed files with 73 additions and 14 deletions

View file

@ -15,6 +15,9 @@ from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
from homeassistant.util import dt as dt_util 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__) _LOGGER = logging.getLogger(__name__)
LWA_TOKEN_URI = "https://api.amazon.com/auth/o2/token" LWA_TOKEN_URI = "https://api.amazon.com/auth/o2/token"
@ -24,8 +27,6 @@ PREEMPTIVE_REFRESH_TTL_IN_SECONDS = 300
STORAGE_KEY = "alexa_auth" STORAGE_KEY = "alexa_auth"
STORAGE_VERSION = 1 STORAGE_VERSION = 1
STORAGE_EXPIRE_TIME = "expire_time" STORAGE_EXPIRE_TIME = "expire_time"
STORAGE_ACCESS_TOKEN = "access_token"
STORAGE_REFRESH_TOKEN = "refresh_token"
class Auth: class Auth:
@ -56,7 +57,7 @@ class Auth:
} }
_LOGGER.debug( _LOGGER.debug(
"Calling LWA to get the access token (first time), with: %s", "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) return await self._async_request_new_token(lwa_params)
@ -133,7 +134,7 @@ class Auth:
return None return None
response_json = await response.json() 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"] access_token: str = response_json["access_token"]
refresh_token: str = response_json["refresh_token"] refresh_token: str = response_json["refresh_token"]

View file

@ -90,6 +90,9 @@ API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"}
# we add PRESET_MODE_NA if a fan / humidifier has only one preset_mode # we add PRESET_MODE_NA if a fan / humidifier has only one preset_mode
PRESET_MODE_NA = "-" PRESET_MODE_NA = "-"
STORAGE_ACCESS_TOKEN = "access_token"
STORAGE_REFRESH_TOKEN = "refresh_token"
class Cause: class Cause:
"""Possible causes for property changes. """Possible causes for property changes.

View file

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

View file

@ -144,7 +144,6 @@ async def async_api_accept_grant(
Async friendly. Async friendly.
""" """
auth_code: str = directive.payload["grant"]["code"] auth_code: str = directive.payload["grant"]["code"]
_LOGGER.debug("AcceptGrant code: %s", auth_code)
if config.supports_auth: if config.supports_auth:
await config.async_accept_grant(auth_code) await config.async_accept_grant(auth_code)

View file

@ -25,6 +25,7 @@ from .const import (
CONF_LOCALE, CONF_LOCALE,
EVENT_ALEXA_SMART_HOME, EVENT_ALEXA_SMART_HOME,
) )
from .diagnostics import async_redact_auth_data
from .errors import AlexaBridgeUnreachableError, AlexaError from .errors import AlexaBridgeUnreachableError, AlexaError
from .handlers import HANDLERS from .handlers import HANDLERS
from .state_report import AlexaDirective from .state_report import AlexaDirective
@ -149,12 +150,21 @@ class SmartHomeView(HomeAssistantView):
user: User = request["hass_user"] user: User = request["hass_user"]
message: dict[str, Any] = await request.json() 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( response = await async_handle_message(
hass, self.smart_home_config, message, context=core.Context(user_id=user.id) 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) return b"" if response is None else self.json(response)

View file

@ -34,6 +34,7 @@ from .const import (
DOMAIN, DOMAIN,
Cause, Cause,
) )
from .diagnostics import async_redact_auth_data
from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id
from .errors import AlexaInvalidEndpointError, NoTokenAvailable, RequireRelink from .errors import AlexaInvalidEndpointError, NoTokenAvailable, RequireRelink
@ -43,6 +44,8 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_TIMEOUT = 10 DEFAULT_TIMEOUT = 10
TO_REDACT = {"correlationToken", "token"}
class AlexaDirective: class AlexaDirective:
"""An incoming Alexa directive.""" """An incoming Alexa directive."""
@ -379,7 +382,9 @@ async def async_send_changereport_message(
response_text = await response.text() response_text = await response.text()
if _LOGGER.isEnabledFor(logging.DEBUG): 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) _LOGGER.debug("Received (%s): %s", response.status, response_text)
if response.status == HTTPStatus.ACCEPTED: if response.status == HTTPStatus.ACCEPTED:
@ -533,7 +538,9 @@ async def async_send_doorbell_event_message(
response_text = await response.text() response_text = await response.text()
if _LOGGER.isEnabledFor(logging.DEBUG): 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) _LOGGER.debug("Received (%s): %s", response.status, response_text)
if response.status == HTTPStatus.ACCEPTED: if response.status == HTTPStatus.ACCEPTED:

View file

@ -1,6 +1,7 @@
"""Test Smart Home HTTP endpoints.""" """Test Smart Home HTTP endpoints."""
from http import HTTPStatus from http import HTTPStatus
import json import json
import logging
from typing import Any from typing import Any
import pytest import pytest
@ -44,11 +45,16 @@ async def do_http_discovery(config, hass, hass_client):
], ],
) )
async def test_http_api( 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: ) -> None:
"""With `smart_home:` HTTP API is exposed.""" """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 = await do_http_discovery(config, hass, hass_client)
response_data = await response.json() response_data = await response.json()
assert "'correlationToken': '**REDACTED**'" in caplog.text
# Here we're testing just the HTTP view glue -- details of discovery are # Here we're testing just the HTTP view glue -- details of discovery are
# covered in other tests. # covered in other tests.
@ -61,5 +67,4 @@ async def test_http_api_disabled(
"""Without `smart_home:`, the HTTP API is disabled.""" """Without `smart_home:`, the HTTP API is disabled."""
config = {"alexa": {}} config = {"alexa": {}}
response = await do_http_discovery(config, hass, hass_client) response = await do_http_discovery(config, hass, hass_client)
assert response.status == HTTPStatus.NOT_FOUND assert response.status == HTTPStatus.NOT_FOUND