Keep secret values out of configuration error messages (#183760)

This commit is contained in:
Franck Nijhof
2026-09-30 17:11:27 +02:00
committed by GitHub
parent 16d27b7def
commit 67c8312c03
23 changed files with 91 additions and 37 deletions
@@ -42,7 +42,9 @@ _LOGGER = logging.getLogger(__name__)
DEVICE_INPUT = "device_input"
INPUT_PIN_SCHEMA = probatio.Schema({probatio.Required(CONF_PIN, default=""): cv.string})
INPUT_PIN_SCHEMA = probatio.Schema(
{probatio.Required(probatio.Secret(CONF_PIN), default=""): cv.string}
)
DEFAULT_START_OFF = False
@@ -43,11 +43,13 @@ PIN_SCHEMA = selector.TextSelector(
PIN_ONLY_SCHEMA = probatio.Schema(
{
probatio.Required(CONF_PIN, default=DEFAULT_PIN): PIN_SCHEMA,
probatio.Required(probatio.Secret(CONF_PIN), default=DEFAULT_PIN): PIN_SCHEMA,
}
)
REAUTH_SCHEMA = probatio.Schema({probatio.Required(CONF_PIN): PIN_SCHEMA})
REAUTH_SCHEMA = probatio.Schema(
{probatio.Required(probatio.Secret(CONF_PIN)): PIN_SCHEMA}
)
def _user_schema(
@@ -63,7 +65,9 @@ def _user_schema(
for address, discovery in discoveries.items()
}
),
probatio.Required(CONF_PIN, default=DEFAULT_PIN): PIN_SCHEMA,
probatio.Required(
probatio.Secret(CONF_PIN), default=DEFAULT_PIN
): PIN_SCHEMA,
}
)
@@ -117,7 +117,11 @@ class BlinkConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="2fa",
data_schema=probatio.Schema(
{probatio.Optional(CONF_PIN): probatio.All(str, probatio.Length(min=1))}
{
probatio.Optional(probatio.Secret(CONF_PIN)): probatio.All(
str, probatio.Length(min=1)
)
}
),
errors=errors,
)
@@ -181,7 +181,7 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="pin",
data_schema=probatio.Schema(
{
probatio.Required(CONF_PIN): str,
probatio.Required(probatio.Secret(CONF_PIN)): str,
}
),
errors=errors,
@@ -210,7 +210,7 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="psk",
data_schema=probatio.Schema(
{
probatio.Required(CONF_PIN): str,
probatio.Required(probatio.Secret(CONF_PIN)): str,
}
),
errors=errors,
@@ -30,14 +30,14 @@ USER_SCHEMA = probatio.Schema(
{
probatio.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
probatio.Required(CONF_PORT, default=DEFAULT_PORT): probatio.Port(),
probatio.Optional(CONF_PIN, default=DEFAULT_PIN): cv.string,
probatio.Optional(probatio.Secret(CONF_PIN), default=DEFAULT_PIN): cv.string,
probatio.Required(CONF_TYPE, default=BRIDGE): probatio.In(DEVICE_TYPE_LIST),
probatio.Optional(CONF_VEDO_PIN): cv.string,
}
)
STEP_REAUTH_DATA_SCHEMA = probatio.Schema(
{
probatio.Required(CONF_PIN): cv.string,
probatio.Required(probatio.Secret(CONF_PIN)): cv.string,
probatio.Optional(CONF_VEDO_PIN): cv.string,
}
)
@@ -239,7 +239,7 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
probatio.Required(
CONF_PORT, default=reconfigure_entry.data[CONF_PORT]
): probatio.Port(),
probatio.Optional(CONF_PIN): cv.string,
probatio.Optional(probatio.Secret(CONF_PIN)): cv.string,
probatio.Optional(CONF_VEDO_PIN): cv.string,
}
)
@@ -43,7 +43,9 @@ CONFIG_SCHEMA = probatio.Schema(
probatio.Optional(
CONF_USER_AGENT, default=DEFAULT_USER_AGENT
): cv.string,
probatio.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int,
probatio.Optional(
probatio.Secret(CONF_PIN), default=DEFAULT_PIN
): cv.positive_int,
}
)
},
@@ -29,7 +29,7 @@ LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = probatio.Schema(
{
probatio.Required(CONF_PIN, default="000000"): probatio.All(
probatio.Required(probatio.Secret(CONF_PIN), default="000000"): probatio.All(
TextSelector(TextSelectorConfig(type=TextSelectorType.NUMBER)),
probatio.Length(min=6, max=6),
),
+1 -1
View File
@@ -48,7 +48,7 @@ PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
{
probatio.Required(CONF_BIN): cv.string,
probatio.Required(CONF_USERNAME): cv.string,
probatio.Required(CONF_PIN): cv.string,
probatio.Required(probatio.Secret(CONF_PIN)): cv.string,
probatio.Required(CONF_URL): cv.string,
probatio.Optional(CONF_NAME): cv.string,
probatio.Optional(CONF_ACCOUNTS, default=[]): probatio.EnsureList()(
@@ -67,7 +67,7 @@ class FumisFlowHandler(ConfigFlow, domain=DOMAIN):
step_id="dhcp_confirm",
data_schema=probatio.Schema(
{
probatio.Required(CONF_PIN): TextSelector(
probatio.Required(probatio.Secret(CONF_PIN)): TextSelector(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
}
@@ -104,7 +104,7 @@ class FumisFlowHandler(ConfigFlow, domain=DOMAIN):
probatio.Required(CONF_MAC): TextSelector(
TextSelectorConfig(autocomplete="off")
),
probatio.Required(CONF_PIN): TextSelector(
probatio.Required(probatio.Secret(CONF_PIN)): TextSelector(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
}
@@ -135,7 +135,7 @@ class FumisFlowHandler(ConfigFlow, domain=DOMAIN):
step_id="reconfigure",
data_schema=probatio.Schema(
{
probatio.Required(CONF_PIN): TextSelector(
probatio.Required(probatio.Secret(CONF_PIN)): TextSelector(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
}
@@ -170,7 +170,7 @@ class FumisFlowHandler(ConfigFlow, domain=DOMAIN):
step_id="reauth_confirm",
data_schema=probatio.Schema(
{
probatio.Required(CONF_PIN): TextSelector(
probatio.Required(probatio.Secret(CONF_PIN)): TextSelector(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
}
@@ -22,14 +22,14 @@ from .const import DOMAIN, LOGGER
BLUETOOTH_SCHEMA = probatio.Schema(
{
probatio.Required(CONF_PIN): str,
probatio.Required(probatio.Secret(CONF_PIN)): str,
}
)
USER_SCHEMA = probatio.Schema(
{
probatio.Required(CONF_ADDRESS): str,
probatio.Required(CONF_PIN): str,
probatio.Required(probatio.Secret(CONF_PIN)): str,
}
)
@@ -202,7 +202,7 @@ class MotionMountFlowHandler(ConfigFlow, domain=DOMAIN):
step_id="auth",
data_schema=probatio.Schema(
{
probatio.Required(CONF_PIN): probatio.All(
probatio.Required(probatio.Secret(CONF_PIN)): probatio.All(
int, probatio.Range(min=1, max=9999)
),
}
@@ -83,7 +83,7 @@ def async_setup_services(
probatio.Schema(
{
probatio.Required(ATTR_DEVICE_ID): cv.string,
probatio.Required(CONF_PIN): cv.string,
probatio.Required(probatio.Secret(CONF_PIN)): cv.string,
}
),
)
@@ -156,7 +156,9 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="pairing",
data_schema=probatio.Schema({probatio.Required(CONF_PIN): str}),
data_schema=probatio.Schema(
{probatio.Required(probatio.Secret(CONF_PIN)): str}
),
errors=errors,
)
@@ -125,7 +125,7 @@ class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
schema = probatio.Schema(
{
probatio.Required(CONF_PIN): str,
probatio.Required(probatio.Secret(CONF_PIN)): str,
}
)
@@ -46,14 +46,14 @@ CLOUD_SCHEMA = probatio.Schema(
{
probatio.Required(CONF_USERNAME): str,
probatio.Required(CONF_PASSWORD): str,
probatio.Required(CONF_PIN): str,
probatio.Required(probatio.Secret(CONF_PIN)): str,
}
)
LOCAL_SCHEMA = probatio.Schema(
{
probatio.Required(CONF_HOST): str,
probatio.Required(CONF_PORT, default=1000): int,
probatio.Required(CONF_PIN): str,
probatio.Required(probatio.Secret(CONF_PIN)): str,
}
)
HA_STATES = [
@@ -391,7 +391,9 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="encrypted_pairing",
errors=errors,
description_placeholders={"device": self._title},
data_schema=probatio.Schema({probatio.Required(CONF_PIN): str}),
data_schema=probatio.Schema(
{probatio.Required(probatio.Secret(CONF_PIN)): str}
),
)
@callback
@@ -679,5 +681,7 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="reauth_confirm_encrypted",
errors=errors,
description_placeholders={"device": reauth_entry.title},
data_schema=probatio.Schema({probatio.Required(CONF_PIN): str}),
data_schema=probatio.Schema(
{probatio.Required(probatio.Secret(CONF_PIN)): str}
),
)
@@ -31,7 +31,7 @@ from .coordinator import SubaruConfigEntry
_LOGGER = logging.getLogger(__name__)
CONF_CONTACT_METHOD = "contact_method"
CONF_VALIDATION_CODE = "validation_code"
PIN_SCHEMA = probatio.Schema({probatio.Required(CONF_PIN): str})
PIN_SCHEMA = probatio.Schema({probatio.Required(probatio.Secret(CONF_PIN)): str})
class SubaruConfigFlow(ConfigFlow, domain=DOMAIN):
+1 -1
View File
@@ -61,7 +61,7 @@ PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend(
probatio.Coerce(int), probatio.Range(min=1)
),
probatio.Optional(CONF_USERNAME): cv.string,
probatio.Optional(CONF_PIN): cv.string,
probatio.Optional(probatio.Secret(CONF_PIN)): cv.string,
}
)
@@ -78,7 +78,7 @@ class VenstarConfigFlow(ConfigFlow, domain=DOMAIN):
probatio.Required(CONF_HOST): str,
probatio.Optional(CONF_USERNAME): str,
probatio.Optional(CONF_PASSWORD): str,
probatio.Optional(CONF_PIN): str,
probatio.Optional(probatio.Secret(CONF_PIN)): str,
probatio.Optional(CONF_SSL, default=False): bool,
}
),
@@ -91,7 +91,11 @@ def _get_pairing_schema(input_dict: dict[str, Any] | None = None) -> probatio.Sc
input_dict = {}
return probatio.Schema(
{probatio.Required(CONF_PIN, default=input_dict.get(CONF_PIN, "")): str}
{
probatio.Required(
probatio.Secret(CONF_PIN), default=input_dict.get(CONF_PIN, "")
): str
}
)
@@ -27,7 +27,7 @@ DEFAULT_TIMEOUT = 5
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
{
probatio.Required(CONF_HOST): cv.string,
probatio.Required(CONF_PIN): cv.string,
probatio.Required(probatio.Secret(CONF_PIN)): cv.string,
probatio.Optional(CONF_ALLOW_UNREACHABLE, default=True): cv.boolean,
probatio.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
}
+9 -5
View File
@@ -25,6 +25,7 @@ from .core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
from .core_config import _PACKAGE_DEFINITION_SCHEMA, _PACKAGES_CONFIG_SCHEMA
from .exceptions import ConfigValidationError, HomeAssistantError
from .helpers import config_validation as cv
from .helpers.redact import REDACTED
from .helpers.translation import async_get_exception_message
from .helpers.typing import ConfigType
from .loader import ComponentProtocol, Integration, IntegrationNotFound
@@ -497,11 +498,14 @@ def stringify_invalid(
output = Exception.__str__(exc)
if error_type := exc.error_type:
output += " for " + error_type
offending_item_summary = repr(_get_by_path(config, exc.path))
if len(offending_item_summary) > max_sub_error_length:
offending_item_summary = (
f"{offending_item_summary[: max_sub_error_length - 3]}..."
)
if exc.secret:
offending_item_summary = REDACTED
else:
offending_item_summary = repr(_get_by_path(config, exc.path))
if len(offending_item_summary) > max_sub_error_length:
offending_item_summary = (
f"{offending_item_summary[: max_sub_error_length - 3]}..."
)
return (
f"{message_prefix}: {output} '{path}', got {offending_item_summary}"
f"{message_suffix}"
+28
View File
@@ -20,6 +20,7 @@ from homeassistant.const import CONF_PACKAGES, __version__
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.exceptions import ConfigValidationError, HomeAssistantError
from homeassistant.helpers import check_config, config_validation as cv
from homeassistant.helpers.redact import REDACTED
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import Integration, async_get_integration
from homeassistant.util.yaml import SECRET_YAML, load_yaml_dict
@@ -1532,6 +1533,33 @@ async def test_stringify_invalid_suggests_close_keys(
)
@pytest.mark.parametrize(
("key", "expected_value"),
[
pytest.param(probatio.Required("password"), "'hunter2'", id="plain"),
pytest.param(
probatio.Required(probatio.Secret("password")), REDACTED, id="secret"
),
],
)
async def test_stringify_invalid_redacts_a_secret(
hass: HomeAssistant, key: probatio.Marker, expected_value: str
) -> None:
"""Test a key marked secret reports the reason without its value."""
schema = probatio.Schema({key: probatio.All(str, probatio.Length(min=12))})
config = {"password": "hunter2"}
with pytest.raises(probatio.MultipleInvalid) as exc_info:
schema(config)
assert config_util.stringify_invalid(
hass, exc_info.value.errors[0], "demo", config, None, 500
) == (
"Invalid config for 'demo': length of value must be at least 12 for "
f"dictionary value 'password', got {expected_value}"
)
@pytest.mark.parametrize(
"config_dir",
["packages", "packages_include_dir_named"],