Remove homeassistant auth legacy mode (#175613)

This commit is contained in:
G Johansson
2026-08-30 22:27:06 +02:00
committed by GitHub
parent 4ae28fc35c
commit 342c9d4858
3 changed files with 2 additions and 300 deletions
+2 -61
View File
@@ -3,7 +3,6 @@
import asyncio
import base64
from collections.abc import Mapping
import logging
from typing import Any, cast, override
import bcrypt
@@ -12,7 +11,6 @@ import voluptuous as vol
from homeassistant.const import CONF_ID
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.storage import Store
from ..models import AuthFlowContext, AuthFlowResult, Credentials, UserMeta
@@ -85,19 +83,10 @@ class Data:
hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True
)
self._data: dict[str, list[dict[str, str]]] | None = None
# Legacy mode will allow usernames to start/end with whitespace
# and will compare usernames case-insensitive.
# Deprecated in June 2019 and will be removed in 2026.7
self.is_legacy = False
@callback
def normalize_username(
self, username: str, *, force_normalize: bool = False
) -> str:
def normalize_username(self, username: str) -> str:
"""Normalize a username based on the mode."""
if self.is_legacy and not force_normalize:
return username
return username.strip().casefold()
async def async_load(self) -> None:
@@ -105,53 +94,8 @@ class Data:
if (data := await self._store.async_load()) is None:
data = cast(dict[str, list[dict[str, str]]], {"users": []})
self._async_check_for_not_normalized_usernames(data)
self._data = data
@callback
def _async_check_for_not_normalized_usernames(
self, data: dict[str, list[dict[str, str]]]
) -> None:
not_normalized_usernames: set[str] = set()
for user in data["users"]:
username = user["username"]
if self.normalize_username(username, force_normalize=True) != username:
logging.getLogger(__name__).warning(
(
"Home Assistant auth provider is running in"
" legacy mode because we detected usernames"
" that are normalized (lowercase and without"
" spaces). Please change the username: '%s'."
),
username,
)
not_normalized_usernames.add(username)
if not_normalized_usernames:
self.is_legacy = True
ir.async_create_issue(
self.hass,
"auth",
"homeassistant_provider_not_normalized_usernames",
breaks_in_ha_version="2026.7.0",
is_fixable=False,
severity=ir.IssueSeverity.WARNING,
translation_key="homeassistant_provider_not_normalized_usernames",
translation_placeholders={
"usernames": (
f'- "{'"\n- "'.join(sorted(not_normalized_usernames))}"'
)
},
learn_more_url="homeassistant://config/users",
)
else:
self.is_legacy = False
ir.async_delete_issue(
self.hass, "auth", "homeassistant_provider_not_normalized_usernames"
)
@property
def users(self) -> list[dict[str, str]]:
"""Return users."""
@@ -247,9 +191,7 @@ class Data:
Raises InvalidUsername if the new username is invalid.
"""
normalized_username = self.normalize_username(
new_username, force_normalize=True
)
normalized_username = self.normalize_username(new_username)
if normalized_username != new_username:
raise InvalidUsername(
translation_key="username_not_normalized",
@@ -279,7 +221,6 @@ class Data:
if self.normalize_username(user["username"]) == username:
user["username"] = new_username
assert self._data is not None
self._async_check_for_not_normalized_usernames(self._data)
break
else:
raise InvalidUser(translation_key="user_not_found")
@@ -10,12 +10,6 @@
"message": "Username \"{new_username}\" is not normalized. Please make sure the username is lowercase and does not contain any whitespace."
}
},
"issues": {
"homeassistant_provider_not_normalized_usernames": {
"description": "The Home Assistant auth provider is running in legacy mode because we detected not normalized usernames. The legacy mode is deprecated and will be removed. Please change the following usernames:\n\n{usernames}\n\nNormalized usernames are case folded (lower case) and stripped of whitespaces.",
"title": "Not normalized usernames detected"
}
},
"mfa_setup": {
"notify": {
"abort": {
-233
View File
@@ -1,7 +1,6 @@
"""Test the Home Assistant local auth provider."""
import asyncio
from typing import Any
from unittest.mock import Mock, patch
import pytest
@@ -14,7 +13,6 @@ from homeassistant.auth.providers import (
homeassistant as hass_auth,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
@@ -26,15 +24,6 @@ async def data(hass: HomeAssistant) -> hass_auth.Data:
return data
@pytest.fixture
async def legacy_data(hass: HomeAssistant) -> hass_auth.Data:
"""Create a loaded legacy data class."""
data = hass_auth.Data(hass)
await data.async_load()
data.is_legacy = True
return data
@pytest.fixture
async def load_auth_component(hass: HomeAssistant) -> None:
"""Load the auth component for translations."""
@@ -209,118 +198,6 @@ async def test_get_or_create_credentials(
assert credentials1 is credentials2
# Legacy mode
async def test_legacy_adding_user(legacy_data: hass_auth.Data) -> None:
"""Test in legacy mode adding a user."""
legacy_data.add_auth("test-user", "test-pass")
legacy_data.validate_login("test-user", "test-pass")
async def test_legacy_validating_password_invalid_password(
legacy_data: hass_auth.Data,
) -> None:
"""Test in legacy mode validating an invalid password."""
legacy_data.add_auth("test-user", "test-pass")
with pytest.raises(hass_auth.InvalidAuth):
legacy_data.validate_login("test-user", "invalid-pass")
async def test_legacy_changing_password(legacy_data: hass_auth.Data) -> None:
"""Test in legacy mode adding a user."""
user = "test-user"
legacy_data.add_auth(user, "test-pass")
legacy_data.change_password(user, "new-pass")
with pytest.raises(hass_auth.InvalidAuth):
legacy_data.validate_login(user, "test-pass")
legacy_data.validate_login(user, "new-pass")
async def test_legacy_changing_password_raises_invalid_user(
legacy_data: hass_auth.Data,
) -> None:
"""Test in legacy mode that we initialize an empty config."""
with pytest.raises(hass_auth.InvalidUser):
legacy_data.change_password("non-existing", "pw")
async def test_legacy_login_flow_validates(
legacy_data: hass_auth.Data, hass: HomeAssistant
) -> None:
"""Test in legacy mode login flow."""
legacy_data.add_auth("test-user", "test-pass")
await legacy_data.async_save()
provider = hass_auth.HassAuthProvider(
hass, auth_store.AuthStore(hass), {"type": "homeassistant"}
)
flow = await provider.async_login_flow({})
result = await flow.async_step_init()
assert result["type"] is data_entry_flow.FlowResultType.FORM
result = await flow.async_step_init(
{"username": "incorrect-user", "password": "test-pass"}
)
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["errors"]["base"] == "invalid_auth"
result = await flow.async_step_init(
{"username": "test-user", "password": "incorrect-pass"}
)
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["errors"]["base"] == "invalid_auth"
result = await flow.async_step_init(
{"username": "test-user", "password": "test-pass"}
)
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["data"]["username"] == "test-user"
async def test_legacy_saving_loading(
legacy_data: hass_auth.Data, hass: HomeAssistant
) -> None:
"""Test in legacy mode saving and loading JSON."""
legacy_data.add_auth("test-user", "test-pass")
legacy_data.add_auth("second-user", "second-pass")
await legacy_data.async_save()
legacy_data = hass_auth.Data(hass)
await legacy_data.async_load()
legacy_data.is_legacy = True
legacy_data.validate_login("test-user", "test-pass")
legacy_data.validate_login("second-user", "second-pass")
with pytest.raises(hass_auth.InvalidAuth):
legacy_data.validate_login("test-user ", "test-pass")
async def test_legacy_get_or_create_credentials(
hass: HomeAssistant, legacy_data: hass_auth.Data
) -> None:
"""Test in legacy mode that we can get or create credentials."""
manager = await auth_manager_from_config(hass, [{"type": "homeassistant"}], [])
provider = manager.auth_providers[0]
provider.data = legacy_data
credentials1 = await provider.async_get_or_create_credentials({"username": "hello"})
with patch.object(provider, "async_credentials", return_value=[credentials1]):
credentials2 = await provider.async_get_or_create_credentials(
{"username": "hello"}
)
assert credentials1 is credentials2
with patch.object(provider, "async_credentials", return_value=[credentials1]):
credentials3 = await provider.async_get_or_create_credentials(
{"username": "hello "}
)
assert credentials1 is not credentials3
async def test_race_condition_in_data_loading(hass: HomeAssistant) -> None:
"""Test race condition in the hass_auth.Data loading.
@@ -361,28 +238,6 @@ def test_change_username(data: hass_auth.Data) -> None:
assert users[0]["username"] == "new-user"
@pytest.mark.parametrize("username", ["test-user ", "TEST-USER"])
def test_change_username_legacy(legacy_data: hass_auth.Data, username: str) -> None:
"""Test changing username."""
# Cannot use add_auth as it normalizes username
legacy_data.users.append(
{
"username": username,
"password": legacy_data.hash_password("test-pass", True).decode(),
}
)
users = legacy_data.users
assert len(users) == 1
assert users[0]["username"] == username
legacy_data.change_username(username, "test-user")
users = legacy_data.users
assert len(users) == 1
assert users[0]["username"] == "test-user"
def test_change_username_invalid_user(data: hass_auth.Data) -> None:
"""Test changing username raises on invalid user."""
data.add_auth("test-user", "test-pass")
@@ -409,91 +264,3 @@ async def test_change_username_not_normalized(
hass_auth.InvalidUsername, match='Username "TEST-user " is not normalized'
):
data.change_username("test-user", "TEST-user ")
@pytest.mark.parametrize(
("usernames_in_storage", "usernames_in_repair"),
[
(["Uppercase"], '- "Uppercase"'),
([" leading"], '- " leading"'),
(["trailing "], '- "trailing "'),
(["Test", "test", "Fritz "], '- "Fritz "\n- "Test"'),
],
)
async def test_create_repair_on_legacy_usernames(
hass: HomeAssistant,
hass_storage: dict[str, Any],
issue_registry: ir.IssueRegistry,
usernames_in_storage: list[str],
usernames_in_repair: str,
) -> None:
"""Test that we create a repair issue for legacy usernames."""
assert not issue_registry.issues.get(
("auth", "homeassistant_provider_not_normalized_usernames")
), "Repair issue already exists"
hass_storage[hass_auth.STORAGE_KEY] = {
"version": 1,
"minor_version": 1,
"key": "auth_provider.homeassistant",
"data": {
"users": [
{
"username": username,
"password": "onlyherebecauseweneedapasswordstring",
}
for username in usernames_in_storage
]
},
}
data = hass_auth.Data(hass)
await data.async_load()
issue = issue_registry.issues.get(
("auth", "homeassistant_provider_not_normalized_usernames")
)
assert issue, "Repair issue not created"
assert issue.translation_placeholders == {"usernames": usernames_in_repair}
async def test_delete_repair_after_fixing_usernames(
hass: HomeAssistant,
hass_storage: dict[str, Any],
issue_registry: ir.IssueRegistry,
) -> None:
"""Test that the repair is deleted after fixing the usernames."""
hass_storage[hass_auth.STORAGE_KEY] = {
"version": 1,
"minor_version": 1,
"key": "auth_provider.homeassistant",
"data": {
"users": [
{
"username": "Test",
"password": "onlyherebecauseweneedapasswordstring",
},
{
"username": "bla ",
"password": "onlyherebecauseweneedapasswordstring",
},
]
},
}
data = hass_auth.Data(hass)
await data.async_load()
issue = issue_registry.issues.get(
("auth", "homeassistant_provider_not_normalized_usernames")
)
assert issue, "Repair issue not created"
assert issue.translation_placeholders == {"usernames": '- "Test"\n- "bla "'}
data.change_username("Test", "test")
issue = issue_registry.issues.get(
("auth", "homeassistant_provider_not_normalized_usernames")
)
assert issue
assert issue.translation_placeholders == {"usernames": '- "bla "'}
data.change_username("bla ", "bla")
assert not issue_registry.issues.get(
("auth", "homeassistant_provider_not_normalized_usernames")
), "Repair issue should be deleted"