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.
This commit is contained in:
1 parent
d8f3d59d82
commit
65a28582dd
5 files changed
+41
-9
No files matched your search
@@ -119,7 +119,7 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr
|
||||
MESH_DEBUG_PRINTLN("Login success!");
|
||||
client->last_timestamp = sender_timestamp;
|
||||
client->last_activity = getRTCClock()->getCurrentTime();
|
||||
client->permissions &= ~0x03;
|
||||
client->permissions &= ~PERM_ACL_ROLE_MASK;
|
||||
client->permissions |= perms;
|
||||
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
|
||||
reply_data[4] = RESP_SERVER_LOGIN_OK;
|
||||
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;
|
||||
getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness
|
||||
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 {
|
||||
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;
|
||||
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
|
||||
@@ -719,7 +719,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
|
||||
if (is_retry) {
|
||||
*reply = 0;
|
||||
} else {
|
||||
handleCommand(sender_timestamp, command, reply);
|
||||
handleCommand(client, command, reply);
|
||||
}
|
||||
int text_len = strlen(reply);
|
||||
if (text_len > 0) {
|
||||
@@ -1165,7 +1165,27 @@ void MyMesh::clearStats() {
|
||||
((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 (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation
|
||||
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;
|
||||
}
|
||||
|
||||
// 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
|
||||
if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int8}
|
||||
char* hex = &command[8];
|
||||
|
||||
@@ -223,7 +223,7 @@ public:
|
||||
void saveIdentity(const mesh::LocalIdentity& new_id) override;
|
||||
void clearStats() override;
|
||||
|
||||
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
|
||||
void handleCommand(ClientInfo* sender, char* command, char* reply);
|
||||
void loop();
|
||||
|
||||
#if defined(WITH_BRIDGE)
|
||||
|
||||
@@ -124,7 +124,7 @@ void loop() {
|
||||
Serial.print('\n');
|
||||
command[len - 1] = 0; // replace newline with C string null terminator
|
||||
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]) {
|
||||
Serial.print(" -> "); Serial.println(reply);
|
||||
}
|
||||
|
||||
@@ -99,7 +99,8 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
|
||||
ClientInfo* oldest = &clients[MAX_CLIENTS - 1];
|
||||
for (int i = 0; i < num_clients; i++) {
|
||||
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];
|
||||
min_time = oldest->last_activity;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
#include <Mesh.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_READ_ONLY 1
|
||||
#define PERM_ACL_READ_WRITE 2
|
||||
#define PERM_ACL_ADMIN 3
|
||||
#define PERM_ACL_REGION_MGR 4
|
||||
|
||||
#define OUT_PATH_UNKNOWN 0xFF
|
||||
|
||||
@@ -31,6 +32,7 @@ struct ClientInfo {
|
||||
} extra;
|
||||
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user