Browse Source

Add PERM_ACL_REGION_MGR role for delegated region management

Introduces a fourth ACL role (value 4) that can manage the region map
without full admin privileges. The role is intended for trusted users
who curate regions on a repeater but should not have access to general
admin commands.

ClientACL:
  - Widen PERM_ACL_ROLE_MASK from 2 to 3 bits so the new value fits.
  - Add PERM_ACL_REGION_MGR and ClientInfo::isRegionMgr().
  - Exempt region_mgr entries from least-recently-active eviction in
    putClient(), same as admins.

simple_repeater:
  - Phones may still gate UI on the legacy is_admin byte (reply_data[6]),
    so report region_mgr as admin there. Without this, the phone CLI
    falls back to guest view.
  - Allow region_mgr to send TXT_MSG CLI commands. handleCommand()
    gates non-whitelisted commands with "Err - not permitted". The
    whitelist covers region.* (read+write) plus a small set of
    read-only queries (get, ver, board, neighbors, clock, sensor
    get/list).
  - Pass the ClientInfo* through to handleCommand and drop the
    redundant sender_timestamp parameter (derived from
    sender->last_timestamp; NULL means Serial CLI).
  - Use ~PERM_ACL_ROLE_MASK instead of ~0x03 when clearing role bits
    on login, so the wider mask is honored.
pull/2438/head
hank 2 months ago
parent
commit
65a28582dd
  1. 39
      examples/simple_repeater/MyMesh.cpp
  2. 2
      examples/simple_repeater/MyMesh.h
  3. 2
      examples/simple_repeater/main.cpp
  4. 3
      src/helpers/ClientACL.cpp
  5. 4
      src/helpers/ClientACL.h

39
examples/simple_repeater/MyMesh.cpp

@ -119,7 +119,7 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr
MESH_DEBUG_PRINTLN("Login success!"); MESH_DEBUG_PRINTLN("Login success!");
client->last_timestamp = sender_timestamp; client->last_timestamp = sender_timestamp;
client->last_activity = getRTCClock()->getCurrentTime(); client->last_activity = getRTCClock()->getCurrentTime();
client->permissions &= ~0x03; client->permissions &= ~PERM_ACL_ROLE_MASK;
client->permissions |= perms; client->permissions |= perms;
memcpy(client->shared_secret, secret, PUB_KEY_SIZE); memcpy(client->shared_secret, secret, PUB_KEY_SIZE);
@ -136,7 +136,7 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
reply_data[4] = RESP_SERVER_LOGIN_OK; reply_data[4] = RESP_SERVER_LOGIN_OK;
reply_data[5] = 0; // Legacy: was recommended keep-alive interval (secs / 16) reply_data[5] = 0; // Legacy: was recommended keep-alive interval (secs / 16)
reply_data[6] = client->isAdmin() ? 1 : 0; reply_data[6] = (client->isAdmin() || client->isRegionMgr()) ? 1 : 0;
reply_data[7] = client->permissions; reply_data[7] = client->permissions;
getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness
reply_data[12] = FIRMWARE_VER_LEVEL; // New field reply_data[12] = FIRMWARE_VER_LEVEL; // New field
@ -682,7 +682,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
} else { } else {
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected"); MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
} }
} else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && client->isAdmin()) { // a CLI command } else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && (client->isAdmin() || client->isRegionMgr())) { // a CLI command
uint32_t sender_timestamp; uint32_t sender_timestamp;
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
uint8_t flags = (data[4] >> 2); // message attempt number, and other flags uint8_t flags = (data[4] >> 2); // message attempt number, and other flags
@ -719,7 +719,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
if (is_retry) { if (is_retry) {
*reply = 0; *reply = 0;
} else { } else {
handleCommand(sender_timestamp, command, reply); handleCommand(client, command, reply);
} }
int text_len = strlen(reply); int text_len = strlen(reply);
if (text_len > 0) { if (text_len > 0) {
@ -1165,7 +1165,27 @@ void MyMesh::clearStats() {
((SimpleMeshTables *)getTables())->resetStats(); ((SimpleMeshTables *)getTables())->resetStats();
} }
void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) { // Whitelist helper for region manager command perms
static bool isRegionMgrAllowed(const char* cmd) {
while(*cmd == ' ') cmd++; // skip leading spaces
// region commands (read + write region map)
if (memcmp(cmd, "region", 6) == 0) return true;
// read-only getters / status
if (memcmp(cmd, "get ", 4) == 0) return true;
if (memcmp(cmd, "ver", 3) == 0) return true;
if (memcmp(cmd, "board", 5) == 0) return true;
// "neighbors" (plural) is read-only; reject "neighbor.remove" by checking next char
if (memcmp(cmd, "neighbors", 9) == 0) return true;
// bare "clock" is read-only; "clock sync" must be denied
if (memcmp(cmd, "clock", 5) == 0 && memcmp(cmd, "clock sync", 10) != 0) return true;
// sensor reads only
if (memcmp(cmd, "sensor get ", 11) == 0) return true;
if (memcmp(cmd, "sensor list", 11) == 0) return true;
return false;
}
void MyMesh::handleCommand(ClientInfo* sender, char *command, char *reply) {
uint32_t sender_timestamp = sender ? sender->last_timestamp : 0; // Serial CLI passes NULL
if (region_load_active) { if (region_load_active) {
if (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation if (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation
region_map = temp_map; // copy over the temp instance as new current map region_map = temp_map; // copy over the temp instance as new current map
@ -1208,6 +1228,15 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
command += 3; command += 3;
} }
// Region managers are limited to read-only queries and region commands
// Admins are unrestricted
if (sender && !sender->isAdmin() && sender->isRegionMgr()) {
if (!isRegionMgrAllowed(command)) {
strcpy(reply, "Err - not permitted");
return;
}
}
// handle ACL related commands // handle ACL related commands
if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int8} if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int8}
char* hex = &command[8]; char* hex = &command[8];

2
examples/simple_repeater/MyMesh.h

@ -223,7 +223,7 @@ public:
void saveIdentity(const mesh::LocalIdentity& new_id) override; void saveIdentity(const mesh::LocalIdentity& new_id) override;
void clearStats() override; void clearStats() override;
void handleCommand(uint32_t sender_timestamp, char* command, char* reply); void handleCommand(ClientInfo* sender, char* command, char* reply);
void loop(); void loop();
#if defined(WITH_BRIDGE) #if defined(WITH_BRIDGE)

2
examples/simple_repeater/main.cpp

@ -124,7 +124,7 @@ void loop() {
Serial.print('\n'); Serial.print('\n');
command[len - 1] = 0; // replace newline with C string null terminator command[len - 1] = 0; // replace newline with C string null terminator
char reply[160]; char reply[160];
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! the_mesh.handleCommand(NULL, command, reply); // NOTE: sender is NULL via serial
if (reply[0]) { if (reply[0]) {
Serial.print(" -> "); Serial.println(reply); Serial.print(" -> "); Serial.println(reply);
} }

3
src/helpers/ClientACL.cpp

@ -99,7 +99,8 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
ClientInfo* oldest = &clients[MAX_CLIENTS - 1]; ClientInfo* oldest = &clients[MAX_CLIENTS - 1];
for (int i = 0; i < num_clients; i++) { for (int i = 0; i < num_clients; i++) {
if (id.matches(clients[i].id)) return &clients[i]; // already known if (id.matches(clients[i].id)) return &clients[i]; // already known
if (!clients[i].isAdmin() && clients[i].last_activity < min_time) { if ( (!clients[i].isAdmin() && !clients[i].isRegionMgr())
&& clients[i].last_activity < min_time) {
oldest = &clients[i]; oldest = &clients[i];
min_time = oldest->last_activity; min_time = oldest->last_activity;
} }

4
src/helpers/ClientACL.h

@ -4,11 +4,12 @@
#include <Mesh.h> #include <Mesh.h>
#include <helpers/IdentityStore.h> #include <helpers/IdentityStore.h>
#define PERM_ACL_ROLE_MASK 3 // lower 2 bits #define PERM_ACL_ROLE_MASK 7 // lower 3 bits
#define PERM_ACL_GUEST 0 #define PERM_ACL_GUEST 0
#define PERM_ACL_READ_ONLY 1 #define PERM_ACL_READ_ONLY 1
#define PERM_ACL_READ_WRITE 2 #define PERM_ACL_READ_WRITE 2
#define PERM_ACL_ADMIN 3 #define PERM_ACL_ADMIN 3
#define PERM_ACL_REGION_MGR 4
#define OUT_PATH_UNKNOWN 0xFF #define OUT_PATH_UNKNOWN 0xFF
@ -31,6 +32,7 @@ struct ClientInfo {
} extra; } extra;
bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; } bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; }
bool isRegionMgr() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_REGION_MGR; }
}; };
#ifndef MAX_CLIENTS #ifndef MAX_CLIENTS

Loading…
Cancel
Save