mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 09:17:53 +00:00
Create repair when HA auth provider is running in legacy mode (#119975)
This commit is contained in:
parent
005c71a4a5
commit
9f4bf6f11a
@ -28,6 +28,7 @@ from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN, REFRESH_TOKEN_EXPIRA
|
||||
from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config
|
||||
from .models import AuthFlowResult
|
||||
from .providers import AuthProvider, LoginFlow, auth_provider_from_config
|
||||
from .providers.homeassistant import HassAuthProvider
|
||||
|
||||
EVENT_USER_ADDED = "user_added"
|
||||
EVENT_USER_UPDATED = "user_updated"
|
||||
@ -73,6 +74,13 @@ async def auth_manager_from_config(
|
||||
key = (provider.type, provider.id)
|
||||
provider_hash[key] = provider
|
||||
|
||||
if isinstance(provider, HassAuthProvider):
|
||||
# Can be removed in 2026.7 with the legacy mode of homeassistant auth provider
|
||||
# We need to initialize the provider to create the repair if needed as otherwise
|
||||
# the provider will be initialized on first use, which could be rare as users
|
||||
# don't frequently change auth settings
|
||||
await provider.async_initialize()
|
||||
|
||||
if module_configs:
|
||||
modules = await asyncio.gather(
|
||||
*(auth_mfa_module_from_config(hass, config) for config in module_configs)
|
||||
|
@ -14,6 +14,7 @@ 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 AuthFlowResult, Credentials, UserMeta
|
||||
@ -88,7 +89,7 @@ class Data:
|
||||
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.
|
||||
# Remove in 2020 or when we launch 1.0.
|
||||
# Deprecated in June 2019 and will be removed in 2026.7
|
||||
self.is_legacy = False
|
||||
|
||||
@callback
|
||||
@ -106,44 +107,49 @@ class Data:
|
||||
if (data := await self._store.async_load()) is None:
|
||||
data = cast(dict[str, list[dict[str, str]]], {"users": []})
|
||||
|
||||
seen: set[str] = set()
|
||||
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"]
|
||||
|
||||
# check if we have duplicates
|
||||
if (folded := username.casefold()) in seen:
|
||||
self.is_legacy = True
|
||||
|
||||
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 case-insensitive"
|
||||
"equivalent. Please change the username: '%s'."
|
||||
"because we detected usernames that are normalized (lowercase and without spaces)."
|
||||
" Please change the username: '%s'."
|
||||
),
|
||||
username,
|
||||
)
|
||||
not_normalized_usernames.add(username)
|
||||
|
||||
break
|
||||
|
||||
seen.add(folded)
|
||||
|
||||
# check if we have unstripped usernames
|
||||
if username != username.strip():
|
||||
self.is_legacy = True
|
||||
|
||||
logging.getLogger(__name__).warning(
|
||||
(
|
||||
"Home Assistant auth provider is running in legacy mode "
|
||||
"because we detected usernames that start or end in a "
|
||||
"space. Please change the username: '%s'."
|
||||
),
|
||||
username,
|
||||
)
|
||||
|
||||
break
|
||||
|
||||
self._data = data
|
||||
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]]:
|
||||
@ -228,6 +234,7 @@ class Data:
|
||||
else:
|
||||
raise InvalidUser
|
||||
|
||||
@callback
|
||||
def _validate_new_username(self, new_username: str) -> None:
|
||||
"""Validate that username is normalized and unique.
|
||||
|
||||
@ -251,6 +258,7 @@ class Data:
|
||||
translation_placeholders={"username": new_username},
|
||||
)
|
||||
|
||||
@callback
|
||||
def change_username(self, username: str, new_username: str) -> None:
|
||||
"""Update the username.
|
||||
|
||||
@ -263,6 +271,8 @@ class Data:
|
||||
for user in self.users:
|
||||
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
|
||||
@ -346,9 +356,7 @@ class HassAuthProvider(AuthProvider):
|
||||
await self.async_initialize()
|
||||
assert self.data is not None
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self.data.change_username, credential.data["username"], new_username
|
||||
)
|
||||
self.data.change_username(credential.data["username"], new_username)
|
||||
self.hass.auth.async_update_user_credentials_data(
|
||||
credential, {**credential.data, "username": new_username}
|
||||
)
|
||||
|
@ -39,5 +39,11 @@
|
||||
"username_not_normalized": {
|
||||
"message": "Username \"{new_username}\" is not normalized"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"homeassistant_provider_not_normalized_usernames": {
|
||||
"title": "Not normalized usernames detected",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Test the Home Assistant local auth provider."""
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
@ -13,6 +14,7 @@ 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
|
||||
|
||||
|
||||
@ -389,3 +391,91 @@ 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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user