Files
MeshCore/test/test_room_acl_persistence.py
mikecarper d031609f26 Improve room retention, moderation and local browser access
Add opt-in bounded chat history, persist explicit room ACL roles, enforce full-key bans and per-user quotas, and provide versioned information-board reads plus an ESP32 local room page. Preserve stored evidence on faults and withhold room delivery acknowledgments when a Companion cannot retain the message.

Exercise production handlers and filesystem fault recovery, pinned official-app JavaScript, real Chromium interactions, and representative ESP32, nRF52, RP2040 and STM32 builds. Add regression suites and bounded browser-workspace RAM reservations to CI.
2026-10-09 23:44:00 -07:00

153 lines
7.6 KiB
Python

#!/usr/bin/env python3
"""Run the actual room ACL filter and ClientACL storage/eviction policy."""
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
FIXTURE = ROOT / "test/fixtures/client_acl_spiffs"
HARNESS = r'''
#define main existing_acl_fixture_main
#include "test_client_acl_spiffs.cpp"
#undef main
#include <helpers/RoomAccessPolicy.h>
struct MyMesh { static bool saveFilter(ClientInfo*); };
@SAVE_FILTER@
static mesh::Identity keyed(unsigned n) {
uint8_t key[PUB_KEY_SIZE] = {};
key[0] = 0x49; key[29] = uint8_t(n >> 16); key[30] = uint8_t(n >> 8); key[31] = uint8_t(n);
return mesh::Identity(key);
}
static void roomAssignedAclSurvivesReboot() {
FakeFilesystem fs;
ClientACL acl; acl.load(&fs, SELF); acl.protectExplicitPermissions();
for (unsigned role = 1; role <= 7; ++role) {
const auto id = keyed(role);
CHECK(acl.applyPermissions(SELF, id.pub_key, PUB_KEY_SIZE, uint8_t(role | 0x80)));
auto* client = acl.getClient(id.pub_key, PUB_KEY_SIZE);
CHECK(client && client->permissions_are_explicit && MyMesh::saveFilter(client));
client->last_activity = 1000 + role;
CHECK(acl.authorizeLoginTimestamp(id.pub_key, 100 + role, 0, uint8_t(role)));
client->last_timestamp = 100 + role;
client->extra.room.sync_since = 40 + role;
#if !MESH_CLIENT_REPEATER_ONLY
client->extra.room.post_quota_used = 6; client->extra.room.poll_quota_used = 5;
#endif
}
const auto guest = keyed(20), writer = keyed(21), admin = keyed(22);
CHECK(acl.putClient(guest, PERM_ACL_GUEST));
auto* auto_rw = acl.putClient(writer, PERM_ACL_READ_WRITE);
auto* auto_admin = acl.putClient(admin, PERM_ACL_ADMIN);
CHECK(auto_rw && auto_admin);
CHECK(!auto_rw->permissions_are_explicit && !MyMesh::saveFilter(auto_rw));
CHECK(!auto_admin->permissions_are_explicit && MyMesh::saveFilter(auto_admin));
SELF.calcSharedSecret(auto_admin->shared_secret, admin.pub_key);
CHECK(acl.save(&fs, MyMesh::saveFilter));
const auto contacts = fs.files["/s_contacts"];
ClientACL restored; restored.load(&fs, SELF); restored.protectExplicitPermissions();
CHECK(restored.getNumClients() == 8);
CHECK(!restored.getClient(guest.pub_key, PUB_KEY_SIZE));
CHECK(!restored.getClient(writer.pub_key, PUB_KEY_SIZE));
for (unsigned role = 1; role <= 7; ++role) {
const auto id = keyed(role);
auto* client = restored.getClient(id.pub_key, PUB_KEY_SIZE);
CHECK(client && client->permissions == uint8_t(role | 0x80));
CHECK(client->permissions_are_explicit && client->extra.room.sync_since == 40 + role);
if (role == PERM_ACL_READ_ONLY) {
// Existing RO-only sessions deliberately do not allocate durable login ceilings.
CHECK(client->last_timestamp == 0);
CHECK(!restored.authorizeLoginTimestamp(id.pub_key, 100 + role, 100 + role, uint8_t(role)));
} else {
CHECK(client->last_timestamp >= 160 + role);
CHECK(!restored.authorizeLoginTimestamp(id.pub_key, 100 + role, client->last_timestamp, uint8_t(role)));
}
#if !MESH_CLIENT_REPEATER_ONLY
CHECK(client->extra.room.post_quota_used == 0 && client->extra.room.poll_quota_used == 0);
#endif
}
CHECK(restored.getClient(admin.pub_key, PUB_KEY_SIZE)->permissions_are_explicit);
// The marker is not a new wire/storage flag and an unchanged save is byte-identical.
CHECK(restored.save(&fs, MyMesh::saveFilter));
CHECK(fs.files["/s_contacts"] == contacts);
}
static void roomPinsOnlyExplicitAssignments() {
FakeFilesystem fs; ClientACL acl; acl.load(&fs, SELF); acl.protectExplicitPermissions();
for (unsigned i = 0; i < MAX_CLIENTS; ++i) {
const auto id = keyed(i + 1);
CHECK(acl.applyPermissions(SELF, id.pub_key, PUB_KEY_SIZE, i % 2 ? PERM_ACL_READ_ONLY : PERM_ACL_READ_WRITE));
acl.getClient(id.pub_key, PUB_KEY_SIZE)->last_activity = i + 1;
}
CHECK(!acl.putClient(keyed(100), PERM_ACL_GUEST));
CHECK(acl.getNumClients() == MAX_CLIENTS);
// setperm0 stays removal, creating one ephemeral login-cache slot.
const auto removed = keyed(1);
CHECK(acl.applyPermissions(SELF, removed.pub_key, PUB_KEY_SIZE, 0));
CHECK(!acl.getClient(removed.pub_key, PUB_KEY_SIZE));
auto* ephemeral = acl.putClient(keyed(100), PERM_ACL_READ_WRITE);
CHECK(ephemeral && !ephemeral->permissions_are_explicit);
#if !MESH_CLIENT_REPEATER_ONLY
ephemeral->extra.room.post_quota_used = ephemeral->extra.room.poll_quota_used = 9;
#endif
CHECK(acl.putClient(keyed(101), PERM_ACL_GUEST));
CHECK(!acl.getClient(keyed(100).pub_key, PUB_KEY_SIZE));
for (unsigned i = 2; i <= MAX_CLIENTS; ++i) CHECK(acl.getClient(keyed(i).pub_key, PUB_KEY_SIZE));
#if !MESH_CLIENT_REPEATER_ONLY
CHECK(acl.getClient(keyed(101).pub_key, PUB_KEY_SIZE)->extra.room.post_quota_used == 0);
#endif
// Other roles keep the existing default policy: explicit RO is still evictable.
FakeFilesystem other_fs; ClientACL other; other.load(&other_fs, SELF);
for (unsigned i = 0; i < MAX_CLIENTS; ++i) {
const auto id = keyed(i + 1);
CHECK(other.applyPermissions(SELF, id.pub_key, PUB_KEY_SIZE, PERM_ACL_READ_ONLY));
other.getClient(id.pub_key, PUB_KEY_SIZE)->last_activity = i + 1;
}
CHECK(other.putClient(keyed(999), PERM_ACL_GUEST));
CHECK(!other.getClient(keyed(1).pub_key, PUB_KEY_SIZE));
// Opt-out is deliberate and reversible; no persistence-format migration.
acl.protectExplicitPermissions(false);
CHECK(acl.putClient(keyed(102), PERM_ACL_GUEST));
}
int main() {
roomAssignedAclSurvivesReboot(); roomPinsOnlyExplicitAssignments();
puts("actual room assigned ACL persistence, replay, eviction, and other-role policy passed");
}
'''
class RoomAclPersistenceTests(unittest.TestCase):
def test_actual_acl_filter_storage_and_opt_in_pressure_protection(self):
compiler = shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler)
room = (ROOT / "examples/simple_room_server/MyMesh.cpp").read_text()
generated = HARNESS.replace("@SAVE_FILTER@", extract_braced(room, "bool MyMesh::saveFilter("))
with tempfile.TemporaryDirectory(prefix="room-assigned-acl-") as directory:
work = Path(directory)
(work / "test.cpp").write_text(generated)
for compact in (0, 1):
with self.subTest(repeater_only=compact):
binary = work / ("acl-" + str(compact))
command = [compiler, "-std=c++17", "-O1", "-Wall", "-Wextra", "-Werror",
"-DESP32=1", "-DESP32_PLATFORM=1", f"-DMESH_CLIENT_REPEATER_ONLY={compact}",
f"-I{FIXTURE / 'mocks'}", f"-I{FIXTURE}", f"-I{ROOT / 'src'}",
str(work / "test.cpp"), "-o", str(binary)]
if "g++" in Path(compiler).name:
# The existing fixture identity has a test-only nontrivial constructor.
command.insert(1, "-Wno-class-memaccess")
if sys.platform.startswith("linux"):
command[1:1] = ["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
built = subprocess.run(command, capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
checked = subprocess.run([str(binary)], capture_output=True, text=True, timeout=20)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
self.assertIn("actual room assigned ACL persistence, replay, eviction, and other-role policy passed", checked.stdout)
if __name__ == "__main__":
unittest.main()