Admin allowlist storage seeded with the setup user (Feature 03, task 4)

- admin_allowlist.py: list_admin_ids / set_admin_ids (atomic sync of
  the allowlist inside one transaction under the connection lock;
  refuses empty targets so the last admin can never be removed and
  the allowlist can never be emptied — fail closed) and
  seed_setup_admin (seeds ONLY an empty allowlist: stored entry-data
  copy first — disaster recovery for a lost database — else the HA
  user who completed the config flow from the entry context;
  non-empty lists are never resurrected).
- __init__.py: both setup paths (first setup and the reopened-record
  path) seed after migrations; a failed seed closes the database and
  re-raises through the existing error handling.
- options_flow.py: the admin picker is a multi-select of EXISTING HA
  users (hass.auth.async_get_users), defaulted to the current
  database allowlist, membership-checked at the schema level and
  backed up on the raw-input path; empty selections are refused
  (no_admins), unknown ids rejected (invalid_admin); the selection is
  applied to the DB when the runtime record is reachable and always
  stored on the entry as the recovery copy. Submissions without the
  key (legacy raw payloads) keep the three-key behaviour untouched.
- const.CONF_ADMIN_USER_IDS; strings + translations for the field and
  the two errors; conftest entries gain a context dict (HA's entry
  context carries the flow user).
- Tests: 20 allowlist business tests (round-trips, refusal matrix,
  seed precedence and narrowed-list persistence), 5 picker flow tests,
  3 setup-seeding lifecycle tests. Suite: 901 passed.
This commit is contained in:
Joshua King
2026-09-15 17:03:53 -04:00
parent b7f7e092a0
commit 0d61ea2c88
9 changed files with 828 additions and 39 deletions
+29 -1
View File
@@ -13,10 +13,11 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN, LOGGER
from .const import CONF_ADMIN_USER_IDS, DOMAIN, LOGGER
from .db import NestQuestDatabase
from .migrations import apply_migrations
from .store import async_get_db_path
from .admin_allowlist import seed_setup_admin
_LOGGER = LOGGER
@@ -245,6 +246,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
db = await _async_open_database(
hass, await async_get_db_path(hass)
)
context = getattr(entry, "context", None)
context_user_id = (
context.get("user_id")
if isinstance(context, dict)
else None
)
await seed_setup_admin(
db,
stored_admin_ids=entry.data.get(CONF_ADMIN_USER_IDS),
context_user_id=context_user_id,
)
existing.database = db
entry.runtime_data = existing
return True
@@ -257,6 +269,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
database = await _async_open_database(hass, await async_get_db_path(hass))
try:
# Seed the admin allowlist on first setup so the owner is never
# locked out: an empty database takes the config entry's stored
# admin copy (disaster recovery), else the HA user who
# completed the config flow. Non-empty allowlists are never
# touched (fail-closed narrowing is deliberate).
context = getattr(entry, "context", None)
context_user_id = (
context.get("user_id")
if isinstance(context, dict)
else None
)
await seed_setup_admin(
database,
stored_admin_ids=entry.data.get(CONF_ADMIN_USER_IDS),
context_user_id=context_user_id,
)
remove_update_listener = entry.add_update_listener(_async_update_listener)
except BaseException:
await database.close()