diff --git a/repeater/handler_helpers/acl.py b/repeater/handler_helpers/acl.py index e2ad745..d863a9d 100644 --- a/repeater/handler_helpers/acl.py +++ b/repeater/handler_helpers/acl.py @@ -106,13 +106,27 @@ class ACL: if not password: client = self.clients.get(pub_key) if client is None: - if self.allow_read_only: - logger.info("Blank password, allowing read-only guest access") - return True, PERM_ACL_GUEST - else: + if not self.allow_read_only: logger.info("Blank password, sender not in ACL and read-only disabled") return False, 0 - logger.info(f"ACL-based login for {pub_key[:6].hex()}...") + if len(self.clients) >= self.max_clients: + logger.warning("ACL full, cannot add read-only guest") + return False, 0 + # Read-only guests must still land in the ACL: the room server's + # text handler and sync loop only see ACL members, so an absent + # entry means the client's posts are silently dropped (never + # decrypted or ACKed) and posts are never pushed to it. + client = ClientInfo(client_identity, PERM_ACL_GUEST) + self.clients[pub_key] = client + logger.info(f"Blank password, added read-only guest {pub_key[:6].hex()}...") + else: + logger.info(f"ACL-based login for {pub_key[:6].hex()}...") + client.last_activity = int(time.time()) + client.last_login_success = int(time.time()) + client.shared_secret = shared_secret + if sync_since is not None: + client.sync_since = sync_since + logger.debug(f"Stored sync_since={sync_since} for client") return True, client.permissions permissions = 0 diff --git a/tests/test_handler_helpers_acl_advert.py b/tests/test_handler_helpers_acl_advert.py index fecc80f..5e732f9 100644 --- a/tests/test_handler_helpers_acl_advert.py +++ b/tests/test_handler_helpers_acl_advert.py @@ -37,10 +37,42 @@ def test_acl_blank_password_guest_rules_and_room_server_password_requirements(): shared_secret=b"secret", password="", timestamp=10, + sync_since=42, ) assert ok is True assert perms == PERM_ACL_GUEST + # The read-only guest must land in the ACL with its shared secret: the + # room server's text handler and sync loop only see ACL members, so an + # absent entry means posts are dropped un-ACKed and pushes never happen. + guest = acl.get_client(b"A" * 32) + assert guest is not None + assert guest.is_guest() is True + assert guest.shared_secret == b"secret" + assert guest.sync_since == 42 + + # A repeat blank-password login reuses the existing entry. + ok_again, perms_again = acl.authenticate_client( + client_identity=identity, + shared_secret=b"secret", + password="", + timestamp=11, + ) + assert ok_again is True + assert perms_again == PERM_ACL_GUEST + assert acl.get_num_clients() == 1 + + # A full ACL rejects new blank-password guests. + acl_full = ACL(max_clients=0, allow_read_only=True) + full_ok, full_perms = acl_full.authenticate_client( + client_identity=identity, + shared_secret=b"secret", + password="", + timestamp=10, + ) + assert full_ok is False + assert full_perms == 0 + acl_ro_disabled = ACL(allow_read_only=False) ok2, perms2 = acl_ro_disabled.authenticate_client( client_identity=identity,