Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cde256e2a | ||
|
|
7a1cef1e54 |
@@ -91,6 +91,23 @@ async def set_admin_ids(
|
|||||||
await dao.add(user_id, _now_stamp())
|
await dao.add(user_id, _now_stamp())
|
||||||
|
|
||||||
|
|
||||||
|
async def is_admin(
|
||||||
|
database: NestQuestDatabase, user_id: object
|
||||||
|
) -> bool:
|
||||||
|
"""Resolve an HA user ID against the allowlist — FAILS CLOSED.
|
||||||
|
|
||||||
|
True only for an allowlisted user id. Everything else is False:
|
||||||
|
a non-allowlisted id, an unknown id, a None or non-string value
|
||||||
|
(a service call with no user context), and an empty allowlist
|
||||||
|
(nobody is admin rather than everybody). The Feature 09 gate
|
||||||
|
wraps every admin-only service with this verdict; it never raises
|
||||||
|
for malformed input — malformed means not admin.
|
||||||
|
"""
|
||||||
|
if not isinstance(user_id, str) or not user_id:
|
||||||
|
return False
|
||||||
|
return await AdminUsersDao(database).exists(user_id)
|
||||||
|
|
||||||
|
|
||||||
async def seed_setup_admin(
|
async def seed_setup_admin(
|
||||||
database: NestQuestDatabase,
|
database: NestQuestDatabase,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -415,3 +415,57 @@ async def test_reopened_setup_failure_closes_database(
|
|||||||
# The reopen path opened AND closed a connection; the failure is
|
# The reopen path opened AND closed a connection; the failure is
|
||||||
# the last thing observed.
|
# the last thing observed.
|
||||||
assert close_calls[-1] is True
|
assert close_calls[-1] is True
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# is_admin: the fail-closed resolver (Feature 09's verdict function)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_admin_verdict_matrix(tmp_path) -> None:
|
||||||
|
async def _body(database):
|
||||||
|
from custom_components.nestquest.admin_allowlist import is_admin
|
||||||
|
|
||||||
|
await set_admin_ids(database, ["user-owner"])
|
||||||
|
assert await is_admin(database, "user-owner") is True
|
||||||
|
assert await is_admin(database, "user-other") is False
|
||||||
|
# Unknown, malformed, and absent identities all fail closed.
|
||||||
|
assert await is_admin(database, "") is False
|
||||||
|
assert await is_admin(database, " user-owner ") is False, (
|
||||||
|
"ids are stored trimmed; an untrimmed lookup is a different "
|
||||||
|
"id and must not match"
|
||||||
|
)
|
||||||
|
assert await is_admin(database, None) is False
|
||||||
|
assert await is_admin(database, 42) is False
|
||||||
|
assert await is_admin(database, ["user-owner"]) is False
|
||||||
|
assert await is_admin(database, True) is False
|
||||||
|
return None
|
||||||
|
|
||||||
|
_with_db(tmp_path, "resolver-matrix.db")(_body)
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_admin_empty_allowlist_fails_closed(tmp_path) -> None:
|
||||||
|
"""An empty allowlist returns False for EVERYONE, not True."""
|
||||||
|
async def _body(database):
|
||||||
|
from custom_components.nestquest.admin_allowlist import is_admin
|
||||||
|
|
||||||
|
for candidate in ("user-owner", "", None, 1):
|
||||||
|
assert await is_admin(database, candidate) is False
|
||||||
|
return None
|
||||||
|
|
||||||
|
_with_db(tmp_path, "resolver-empty.db")(_body)
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_admin_false_after_last_admin_removal_attempt(tmp_path) -> None:
|
||||||
|
"""The refusal keeps the verdict sound: a refused removal leaves
|
||||||
|
the admin in place, so is_admin still resolves True."""
|
||||||
|
async def _body(database):
|
||||||
|
from custom_components.nestquest.admin_allowlist import is_admin
|
||||||
|
|
||||||
|
await set_admin_ids(database, ["user-owner"])
|
||||||
|
with pytest.raises(ValueError, match="at least one admin"):
|
||||||
|
await set_admin_ids(database, [])
|
||||||
|
assert await is_admin(database, "user-owner") is True
|
||||||
|
return None
|
||||||
|
|
||||||
|
_with_db(tmp_path, "resolver-refusal.db")(_body)
|
||||||
|
|||||||
Reference in New Issue
Block a user