Files
HaloKeymind/test/fixtures/client_acl_cli/test_client_acl_cli.cpp
T
mikecarper b847b46ef7 Retry identity startup and harden replay state and permissions
Read identities up to three times before allowing replacement, publish only complete reads, and require new startup keys to be saved with bounded retries.

Use authoritative replay-file metadata throughout persistence and clock recovery. Validate setperm input before narrowing or mutating roles, and add production-path regression coverage.
2026-09-15 17:33:50 -07:00

306 lines
11 KiB
C++

#include <helpers/ClientACL.cpp>
#include <helpers/ClientACLCLI.h>
#include <helpers/CLICommandUtils.h>
#include <helpers/LazyPersistence.h>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>
#define CHECK(condition) do { if (!(condition)) { \
std::fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #condition); std::exit(1); \
} } while (0)
class Stream {
public:
std::string output;
void print(char c) { output += c; }
void println(const char* text) { output += text; output += '\n'; }
void printf(const char* format, ...) {
char row[256];
va_list args;
va_start(args, format);
const int len = vsnprintf(row, sizeof(row), format, args);
va_end(args);
CHECK(len >= 0 && size_t(len) < sizeof(row));
output += row;
}
};
namespace mesh {
static Stream console;
Stream& usbConsolePort() { return console; }
}
static mesh::LocalIdentity self_id;
static unsigned long dirty_contacts_expiry = 0;
static uint8_t contacts_save_failures = 0;
static const unsigned long LAZY_CONTACTS_WRITE_DELAY = 5000;
static unsigned long futureMillis(unsigned long delay) { return delay + 1; }
static unsigned gps_updates = 0;
static void updateGpsTelemetryPolicy() { ++gps_updates; }
#include "production.h"
static ClientInfo* add(ClientACL& acl, unsigned index, uint8_t permissions) {
uint8_t key[PUB_KEY_SIZE];
for (size_t i = 0; i < sizeof(key); ++i) key[i] = uint8_t(i * 7);
// Deliberately share the first 12 hex characters between every entry.
key[30] = uint8_t(index >> 8);
key[31] = uint8_t(index);
auto* client = acl.putClient(mesh::Identity(key), permissions);
CHECK(client != nullptr);
memset(client->shared_secret, 0xFA, sizeof(client->shared_secret));
client->last_activity = index + 123;
client->last_timestamp = index + 456;
return client;
}
static std::string row(const ClientInfo* client) {
char output[68];
snprintf(output, sizeof(output), "%02X ", unsigned(client->permissions));
for (size_t i = 0; i < PUB_KEY_SIZE; ++i) {
snprintf(output + 3 + 2 * i, sizeof(output) - 3 - 2 * i,
"%02X", unsigned(client->id.pub_key[i]));
}
return output;
}
static std::string query(const ClientACL& acl, const char* command, bool local = false) {
char reply[160];
memset(reply, '#', sizeof(reply));
CHECK(mesh::cli::handleACLGet(acl, command, reply, 157, local));
CHECK(reply[157] == '#' && reply[158] == '#' && reply[159] == '#');
CHECK(strlen(reply) < 157);
return reply;
}
static void empty_and_single_entry() {
FakeFilesystem fs; mesh::LocalIdentity self;
ClientACL acl;
acl.load(&fs, self);
CHECK(query(acl, "get acl") == "ACL: empty");
CHECK(query(acl, "get acl 1") == "ACL: empty");
CHECK(query(acl, "get acl 2") == "Err - ACL page range: 1-1");
add(acl, 0, 0);
CHECK(query(acl, "get acl") == "ACL: empty");
const auto* client = add(acl, 1, 0xA3);
CHECK(query(acl, "get acl") == "ACL 1/1\n" + row(client));
}
static void skips_inactive_and_preserves_full_keys() {
FakeFilesystem fs; mesh::LocalIdentity self;
ClientACL acl;
acl.load(&fs, self);
std::vector<std::string> active;
for (unsigned i = 0; i < 9; ++i) {
auto* client = add(acl, i, i % 2 == 0 ? uint8_t(i + 1) : 0);
if (client->permissions) active.push_back(row(client));
}
CHECK(query(acl, "get acl") == "ACL 1/3\n" + active[0] + "\n" + active[1]);
CHECK(query(acl, "get acl 2") == "ACL 2/3\n" + active[2] + "\n" + active[3]);
CHECK(query(acl, "get acl 3") == "ACL 3/3\n" + active[4]);
CHECK(query(acl, "get acl 4") == "Err - ACL page range: 1-3");
}
static void full_table_pages() {
FakeFilesystem fs; mesh::LocalIdentity self;
ClientACL acl;
acl.load(&fs, self);
for (unsigned i = 0; i < MAX_CLIENTS; ++i) add(acl, i, PERM_ACL_ADMIN);
const unsigned pages = (MAX_CLIENTS + 1) / 2;
for (unsigned page = 1; page <= pages; ++page) {
char command[32], header[32];
snprintf(command, sizeof(command), "get acl %u", page);
snprintf(header, sizeof(header), "ACL %u/%u", page, pages);
std::string expected(header);
for (unsigned i = (page - 1) * 2; i < page * 2 && i < MAX_CLIENTS; ++i) {
expected += "\n" + row(acl.getClientByIdx(i));
}
CHECK(query(acl, command) == expected);
}
}
static void rejects_bad_pages() {
FakeFilesystem fs; mesh::LocalIdentity self;
ClientACL acl;
acl.load(&fs, self);
add(acl, 0, 3);
for (const char* command : {
"get acl 0", "get acl -1", "get acl +1", "get acl 1x", "get acl 1 2",
"get acl 1.0", "get acl page 1", "get acl 65536", "get acl 4294967296",
"get acl 9999999999999999999999999999999999", "get acl 1\nsetperm key 3"}) {
CHECK(query(acl, command) == "Err - usage: get acl [page]");
}
CHECK(query(acl, "get acl 65535") == "Err - ACL page range: 1-1");
const auto expected = query(acl, "get acl");
for (const char* command : {"get acl ", "get acl\r\n", "get acl\t01 \r\n", "get acl 00000000000001"}) {
CHECK(query(acl, command) == expected);
}
}
static void matching_and_local_fallback() {
FakeFilesystem fs; mesh::LocalIdentity self;
ClientACL acl;
acl.load(&fs, self);
char reply[157] = "unchanged";
for (const char* command : {"", "get", "get ac", "get aclx", "get acl.other", "set acl"}) {
CHECK(!mesh::cli::handleACLGet(acl, command, reply, sizeof(reply), false));
CHECK(strcmp(reply, "unchanged") == 0);
}
CHECK(!mesh::cli::handleACLGet(acl, "get acl", reply, sizeof(reply), true));
CHECK(strcmp(reply, "unchanged") == 0);
CHECK(query(acl, "get acl 1", true) == "ACL: empty");
}
static void bounds_never_truncate_keys() {
FakeFilesystem fs; mesh::LocalIdentity self;
ClientACL acl;
acl.load(&fs, self);
add(acl, 0, 3);
add(acl, 1, 3);
const auto expected = query(acl, "get acl");
for (size_t capacity = 1; capacity <= 160; ++capacity) {
char reply[161];
memset(reply, '#', sizeof(reply));
CHECK(mesh::cli::handleACLGet(acl, "get acl", reply, capacity, false));
CHECK(reply[capacity] == '#');
CHECK(strlen(reply) < capacity);
if (capacity > expected.size()) CHECK(reply == expected);
else CHECK(strstr(reply, "ACL 1/1") == nullptr);
}
char byte = '#';
CHECK(mesh::cli::handleACLGet(acl, "get acl", &byte, 0, false));
CHECK(byte == '#');
}
static void listing_does_not_mutate_acl() {
FakeFilesystem fs;
ClientACL acl;
mesh::LocalIdentity self;
acl.load(&fs, self);
for (unsigned i = 0; i < 5; ++i) add(acl, i, uint8_t(i));
std::vector<ClientInfo> before;
for (int i = 0; i < acl.getNumClients(); ++i) before.push_back(*acl.getClientByIdx(i));
const auto writes = fs.bytes_written;
for (const char* command : {"get acl", "get acl 2", "get acl 3", "get acl 0"}) {
query(acl, command);
}
CHECK(acl.getNumClients() == int(before.size()));
for (int i = 0; i < acl.getNumClients(); ++i) {
CHECK(memcmp(acl.getClientByIdx(i), &before[i], sizeof(ClientInfo)) == 0);
}
CHECK(fs.bytes_written == writes);
}
static void actual_role_dispatch_handles_radio_and_local() {
FakeFilesystem fs; mesh::LocalIdentity self;
ClientACL acl;
acl.load(&fs, self);
auto* admin = add(acl, 0, PERM_ACL_ADMIN);
add(acl, 1, PERM_ACL_READ_WRITE);
const auto expected = query(acl, "get acl");
for (auto handler : {repeaterCommand, roomCommand, sensorCommand}) {
for (uint32_t timestamp : {0U, 12345U}) {
for (const char* input : {"get acl", "get acl 1", "ab|GET acl", "ab|get acl 1"}) {
char command[80], reply[161];
strcpy(command, input);
memset(reply, '#', sizeof(reply));
mesh::console.output.clear();
handler(acl, admin, timestamp, command, reply);
CHECK(mesh::console.output.empty());
CHECK(reply[160] == '#');
CHECK(strlen(reply) < 160);
CHECK(std::string(reply) == (input[2] == '|' ? "ab|" + expected : expected));
}
}
char command[] = "get acl", reply[160] = {};
mesh::console.output.clear();
handler(acl, nullptr, 0, command, reply);
CHECK(reply[0] == 0);
CHECK(mesh::console.output.find("ACL:") == 0);
CHECK(mesh::console.output.find(row(admin)) != std::string::npos);
strcpy(command, "get acl");
char paged[] = "get acl 1";
mesh::console.output.clear();
handler(acl, nullptr, 0, paged, reply);
CHECK(mesh::console.output.empty());
CHECK(reply == expected);
}
}
static void repeater_delegation_stays_denied() {
FakeFilesystem fs; mesh::LocalIdentity self;
ClientACL acl;
acl.load(&fs, self);
auto* sender = add(acl, 0, 3);
for (unsigned permissions = 0; permissions <= 255; ++permissions) {
sender->permissions = uint8_t(permissions);
for (const char* input : {"get acl", "get acl 2", "xy|get acl", "xy|get acl 2"}) {
char command[32], reply[160] = {};
strcpy(command, input);
mesh::console.output.clear();
repeaterCommand(acl, sender, 0, command, reply);
CHECK(mesh::console.output.empty());
const char* body = input[2] == '|' ? reply + 3 : reply;
if ((permissions & PERM_ACL_ROLE_MASK) != PERM_ACL_ADMIN) {
CHECK(strcmp(body, "Err - not permitted") == 0);
} else {
CHECK(strstr(body, "not permitted") == nullptr);
}
}
}
}
static void permissions_validate_before_mutation() {
for (auto handler : {repeaterCommand, roomCommand, sensorCommand}) {
for (uint32_t timestamp : {0U, 12345U}) {
FakeFilesystem fs;
ClientACL acl;
acl.load(&fs, self_id);
auto* admin = add(acl, 0, PERM_ACL_ADMIN);
auto* target = add(acl, 1, PERM_ACL_ADMIN);
uint8_t target_key[PUB_KEY_SIZE];
memcpy(target_key, target->id.pub_key, sizeof(target_key));
char key[65]; mesh::Utils::toHex(key, target_key, sizeof(target_key));
CHECK(acl.save(&fs));
const auto durable = fs.files;
for (const char* value : {"", "invalid", "-1", "256", "259", "3bad", "3.0",
"+", "4294967296", "99999999999999999999999"}) {
char command[140], reply[160] = {};
snprintf(command, sizeof(command), "setperm %s %s", key, value);
dirty_contacts_expiry = 0;
gps_updates = 0;
handler(acl, timestamp ? admin : nullptr, timestamp, command, reply);
CHECK(strncmp(reply, "Err", 3) == 0);
CHECK(acl.getNumClients() == 2 && acl.getClient(target_key, PUB_KEY_SIZE)->isAdmin());
CHECK(dirty_contacts_expiry == 0 && fs.files == durable);
CHECK(gps_updates == 0);
}
for (unsigned value : {1U, 2U, 3U, 5U, 255U, 0U}) {
char command[140], reply[160] = {};
snprintf(command, sizeof(command), "setperm %s %u", key, value);
dirty_contacts_expiry = 0;
handler(acl, timestamp ? admin : nullptr, timestamp, command, reply);
CHECK(strcmp(reply, "OK") == 0 && dirty_contacts_expiry != 0);
auto* changed = acl.getClient(target_key, PUB_KEY_SIZE);
CHECK(value == 0 ? changed == nullptr : changed && changed->permissions == value);
}
}
}
}
int main() {
empty_and_single_entry();
skips_inactive_and_preserves_full_keys();
full_table_pages();
rejects_bad_pages();
matching_and_local_fallback();
bounds_never_truncate_keys();
listing_does_not_mutate_acl();
actual_role_dispatch_handles_radio_and_local();
repeater_delegation_stays_denied();
permissions_validate_before_mutation();
puts("10 ACL CLI checks passed");
}