fix(acl): reload a room given its first password; log a refused restore

Sixth review round (opencode: design still clean):
- A room whose passwords were cleared, then set again in a settings-only
  update, was told no reload was needed, yet had no login handler or ACL
  until a restart. Turning a password on counts as a reload.
- A restore refused because the new entry already holds the hash is logged.
- The no-login-helper move path says why it keeps its own store guard.
This commit is contained in:
agessaman committed 2026-09-30 16:59:45 -07:00
1 parent 99ff0f0963
commit 8274638ede
2 files changed
+32 -3

No files matched your search

+18 -3
View File
@@ -6872,11 +6872,17 @@ class APIEndpoints:
needs_reload = data.get("identity_key") or "new_name" in data
# A room with no password is not registered for logins, so a live
# reload would leave its login ACL under the old name; it waits
# for a restart instead.
# for a restart instead. Giving such a room a password is a
# reload too: it has no login handler or ACL until it registers.
room_settings = identity.get("settings") or {}
can_log_in = bool(
room_settings.get("admin_password") or room_settings.get("guest_password")
)
old_settings = snapshot.get("settings") or {}
could_log_in = bool(
old_settings.get("admin_password") or old_settings.get("guest_password")
)
needs_reload = needs_reload or (can_log_in and not could_log_in)
if needs_reload and can_log_in and self.daemon_instance:
try:
@@ -6931,9 +6937,15 @@ class APIEndpoints:
)
finally:
if not registration_success and previous is not None:
identity_manager.register_identity(
# Refused only when the new entry already holds
# the hash, i.e. it did register before failing.
if not identity_manager.register_identity(
old_name, previous[0], previous[1], previous[2]
)
):
logger.warning(
f"Hot reload: could not restore '{old_name}'; "
f"it stays registered as '{final_name}'"
)
if registration_success:
logger.info(
f"Hot reload: Re-registered identity '{final_name}' with all systems"
@@ -7302,6 +7314,9 @@ class APIEndpoints:
if login_helper is not None:
login_helper.move_room_acl(old_name, old_pubkey, new_pubkey, new_name, commit)
return
# No login helper (a daemon still starting, or a test double): move the
# rows in the store directly. LoginHelper.move_room_acl holds the same
# guard for the usual path, against the store its ACLs were built with.
storage = getattr(getattr(self.daemon_instance, "repeater_handler", None), "storage", None)
sqlite_handler = getattr(storage, "sqlite_handler", None)
if sqlite_handler is None and old_pubkey != new_pubkey:
+14
View File
@@ -554,3 +554,17 @@ def test_delete_identity_frees_the_name_and_hash(db, request_ctx):
identity = LocalIdentity(seed=bytes.fromhex("11" * 32))
# Re-creating the room registers at once instead of conflicting until a restart.
assert daemon.identity_manager.registration_error("room-a", identity, "room_server") is None
def test_giving_a_passwordless_room_a_password_registers_it_live(db, request_ctx):
daemon = _LiveDaemon(db, "room-a", "11" * 32)
api = _api(daemon, daemon.config)
request_ctx.method = "PUT"
request_ctx.json = {"name": "room-a", "settings": {"admin_password": "", "guest_password": ""}}
api.update_identity()
request_ctx.method = "PUT"
request_ctx.json = {"name": "room-a", "settings": {"admin_password": "new-admin"}}
result = api.update_identity()
assert "no reload needed" not in result["message"]
assert daemon.login_helper.get_acl_by_name("room-a").admin_password == "new-admin"