Feature: filter device visibility by allowed groups for web user

This commit is contained in:
yuanyuanxiang
2026-05-02 00:30:08 +02:00
parent 56419f8ecb
commit fd3838a151
3 changed files with 207 additions and 16 deletions

View File

@@ -11,6 +11,7 @@
#include <filesystem>
#include <fstream>
#include <shlobj.h>
#include <set>
// Algorithm constants (same as ScreenSpyDlg.cpp)
#define ALGORITHM_H264 2
@@ -363,6 +364,8 @@ void CWebService::ServerThread(int port) {
HandleDeleteUser(ws_ptr, msg);
} else if (cmd == "list_users") {
HandleListUsers(ws_ptr, token);
} else if (cmd == "get_groups") {
HandleGetGroups(ws_ptr, token);
}
}
});
@@ -566,7 +569,7 @@ void CWebService::HandleGetDevices(void* ws_ptr, const std::string& token) {
return;
}
SendText(ws_ptr, BuildDeviceListJson());
SendText(ws_ptr, BuildDeviceListJson(username));
}
void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id) {
@@ -588,6 +591,32 @@ void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t
return;
}
// Check group permission (admin can access all devices)
if (username != "admin") {
std::string deviceGroup = ctx->GetGroupName();
if (deviceGroup.empty()) deviceGroup = "default";
bool hasAccess = false;
{
std::lock_guard<std::mutex> lock(m_UsersMutex);
for (const auto& u : m_Users) {
if (u.username == username) {
for (const auto& g : u.allowed_groups) {
if (g == deviceGroup) {
hasAccess = true;
break;
}
}
break;
}
}
}
if (!hasAccess) {
SendText(ws_ptr, BuildJsonResponse("connect_result", false, "Permission denied"));
return;
}
}
// Check max clients per device
int current_count = GetWebClientCount(device_id);
if (current_count >= m_nMaxClientsPerDevice) {
@@ -954,12 +983,23 @@ void CWebService::HandleCreateUser(void* ws_ptr, const std::string& msg) {
std::string newPassword = root.get("password", "").asString();
std::string newRole = root.get("role", "viewer").asString();
// Parse allowed_groups array
std::vector<std::string> allowedGroups;
const Json::Value& groups = root["allowed_groups"];
if (groups.isArray()) {
for (const auto& g : groups) {
if (g.isString() && !g.asString().empty()) {
allowedGroups.push_back(g.asString());
}
}
}
if (newUsername.empty() || newPassword.empty()) {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Username and password required"));
return;
}
if (CreateUser(newUsername, newPassword, newRole)) {
if (CreateUser(newUsername, newPassword, newRole, allowedGroups)) {
SendText(ws_ptr, BuildJsonResponse("create_user_result", true));
} else {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Failed to create user (may already exist)"));
@@ -1009,18 +1049,27 @@ void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) {
return;
}
auto users = ListUsers();
Json::Value res;
res["cmd"] = "list_users_result";
res["ok"] = true;
Json::Value usersArray(Json::arrayValue);
for (const auto& u : users) {
Json::Value user;
user["username"] = u.first;
user["role"] = u.second;
usersArray.append(user);
{
std::lock_guard<std::mutex> lock(m_UsersMutex);
for (const auto& u : m_Users) {
Json::Value user;
user["username"] = u.username;
user["role"] = u.role;
// Include allowed_groups
Json::Value groups(Json::arrayValue);
for (const auto& g : u.allowed_groups) {
groups.append(g);
}
user["allowed_groups"] = groups;
usersArray.append(user);
}
}
res["users"] = usersArray;
@@ -1030,6 +1079,48 @@ void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) {
SendText(ws_ptr, json);
}
void CWebService::HandleGetGroups(void* ws_ptr, const std::string& token) {
std::string username, role;
if (!ValidateToken(token, username, role)) {
SendText(ws_ptr, BuildJsonResponse("groups", false, "Invalid token"));
return;
}
// Only admin can get groups list (for user management)
if (role != "admin") {
SendText(ws_ptr, BuildJsonResponse("groups", false, "Permission denied"));
return;
}
// Collect all unique groups from online devices
std::set<std::string> groups;
groups.insert("default"); // Always include default group
if (m_pParentDlg) {
EnterCriticalSection(&m_pParentDlg->m_cs);
for (context* ctx : m_pParentDlg->m_HostList) {
if (!ctx || !ctx->IsLogin()) continue;
std::string g = ctx->GetGroupName();
groups.insert(g.empty() ? "default" : g);
}
LeaveCriticalSection(&m_pParentDlg->m_cs);
}
// Build response
Json::Value res;
res["cmd"] = "groups";
res["ok"] = true;
res["groups"] = Json::Value(Json::arrayValue);
for (const auto& g : groups) {
res["groups"].append(g);
}
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
std::string json = Json::writeString(builder, res);
SendText(ws_ptr, json);
}
//////////////////////////////////////////////////////////////////////////
// Token Management (delegated to WebServiceAuth module)
//////////////////////////////////////////////////////////////////////////
@@ -1170,6 +1261,14 @@ void CWebService::LoadUsers() {
user.salt = u.get("salt", "").asString();
user.role = u.get("role", "viewer").asString();
// Load allowed_groups
const Json::Value& groups = u["allowed_groups"];
if (groups.isArray()) {
for (const auto& g : groups) {
user.allowed_groups.push_back(g.asString());
}
}
if (!user.password_hash.empty()) {
m_Users.push_back(user);
loaded++;
@@ -1197,6 +1296,14 @@ void CWebService::SaveUsers() {
user["password_hash"] = u.password_hash;
user["salt"] = u.salt;
user["role"] = u.role;
// Save allowed_groups
Json::Value groups(Json::arrayValue);
for (const auto& g : u.allowed_groups) {
groups.append(g);
}
user["allowed_groups"] = groups;
users.append(user);
}
@@ -1217,7 +1324,8 @@ void CWebService::SaveUsers() {
Mprintf("[WebService] Saved %d users to users.json\n", (int)users.size());
}
bool CWebService::CreateUser(const std::string& username, const std::string& password, const std::string& role) {
bool CWebService::CreateUser(const std::string& username, const std::string& password, const std::string& role,
const std::vector<std::string>& allowed_groups) {
if (username.empty() || password.empty()) return false;
if (username == "admin") return false; // Cannot create user named "admin"
if (role != "admin" && role != "viewer") return false;
@@ -1236,9 +1344,11 @@ bool CWebService::CreateUser(const std::string& username, const std::string& pas
user.salt = GenerateSalt();
user.password_hash = WSAuth::ComputeSHA256(password + user.salt);
user.role = role;
user.allowed_groups = allowed_groups;
m_Users.push_back(user);
Mprintf("[WebService] Created user: %s (role: %s)\n", username.c_str(), role.c_str());
Mprintf("[WebService] Created user: %s (role: %s, groups: %d)\n",
username.c_str(), role.c_str(), (int)allowed_groups.size());
}
// Save to file (outside lock scope since SaveUsers acquires its own lock)
@@ -1295,17 +1405,47 @@ std::string CWebService::BuildJsonResponse(const std::string& cmd, bool ok, cons
return Json::writeString(builder, res);
}
std::string CWebService::BuildDeviceListJson() {
std::string CWebService::BuildDeviceListJson(const std::string& username) {
Json::Value res;
res["cmd"] = "device_list";
res["devices"] = Json::Value(Json::arrayValue);
// Get user's allowed groups for filtering (skip for admin or empty username)
std::vector<std::string> allowedGroups;
bool filterByGroup = false;
if (!username.empty() && username != "admin") {
std::lock_guard<std::mutex> lock(m_UsersMutex);
for (const auto& u : m_Users) {
if (u.username == username) {
allowedGroups = u.allowed_groups;
filterByGroup = true;
break;
}
}
}
if (m_pParentDlg) {
// Access device list with lock
EnterCriticalSection(&m_pParentDlg->m_cs);
for (context* ctx : m_pParentDlg->m_HostList) {
if (!ctx || !ctx->IsLogin()) continue;
// Get device group (empty = "default")
std::string deviceGroup = ctx->GetGroupName();
if (deviceGroup.empty()) deviceGroup = "default";
// Filter by allowed groups if user is not admin
if (filterByGroup) {
bool allowed = false;
for (const auto& g : allowedGroups) {
if (g == deviceGroup) {
allowed = true;
break;
}
}
if (!allowed) continue; // Skip device not in allowed groups
}
Json::Value device;
// Use string for ID to avoid JavaScript number precision loss
device["id"] = std::to_string(ctx->GetClientID());
@@ -1332,6 +1472,9 @@ std::string CWebService::BuildDeviceListJson() {
device["activeWindow"] = AnsiToUtf8(activeWindow);
device["online"] = true;
// Add device group to response
device["group"] = deviceGroup;
// Get screen info from client's reported resolution
// Format: "n:MxN" where n=monitor count, M=width, N=height
CString resolution = ctx->GetAdditionalData(RES_RESOLUTION);