fix(wireguard): isolate user data by owner_user_id

This commit is contained in:
xtcnet 2026-03-10 10:39:46 +07:00
parent d67081492d
commit 3f0d529d14
5 changed files with 158 additions and 40 deletions

@ -0,0 +1 @@
Subproject commit 288a767d8b251004f7bd7999dcdf408cbbaa86c7

View file

@ -205,14 +205,21 @@ const internalWireguard = {
/** /**
* Get all clients with live status and interface name correlation * Get all clients with live status and interface name correlation
*/ */
async getClients(knex) { async getClients(knex, access) {
await this.getOrCreateInterface(knex); // Ensure structure exists await this.getOrCreateInterface(knex); // Ensure structure exists
const dbClients = await knex("wg_client") const query = knex("wg_client")
.join("wg_interface", "wg_client.interface_id", "=", "wg_interface.id") .join("wg_interface", "wg_client.interface_id", "=", "wg_interface.id")
.select("wg_client.*", "wg_interface.name as interface_name") .select("wg_client.*", "wg_interface.name as interface_name")
.orderBy("wg_client.created_on", "desc"); .orderBy("wg_client.created_on", "desc");
// Filter by owner if not admin
if (access && !access.token.hasScope("admin")) {
query.andWhere("wg_client.owner_user_id", access.token.getUserId(1));
}
const dbClients = await query;
const clients = dbClients.map((c) => ({ const clients = dbClients.map((c) => ({
id: c.id, id: c.id,
name: c.name, name: c.name,
@ -258,7 +265,7 @@ const internalWireguard = {
/** /**
* Create a new WireGuard client * Create a new WireGuard client
*/ */
async createClient(knex, data) { async createClient(knex, data, access) {
const iface = data.interface_id const iface = data.interface_id
? await knex("wg_interface").where("id", data.interface_id).first() ? await knex("wg_interface").where("id", data.interface_id).first()
: await this.getOrCreateInterface(knex); : await this.getOrCreateInterface(knex);
@ -284,6 +291,7 @@ const internalWireguard = {
persistent_keepalive: data.persistent_keepalive || WG_DEFAULT_PERSISTENT_KEEPALIVE, persistent_keepalive: data.persistent_keepalive || WG_DEFAULT_PERSISTENT_KEEPALIVE,
expires_at: data.expires_at || null, expires_at: data.expires_at || null,
interface_id: iface.id, interface_id: iface.id,
owner_user_id: access ? access.token.getUserId(1) : 1,
created_on: knex.fn.now(), created_on: knex.fn.now(),
modified_on: knex.fn.now(), modified_on: knex.fn.now(),
}; };
@ -299,8 +307,12 @@ const internalWireguard = {
/** /**
* Delete a WireGuard client * Delete a WireGuard client
*/ */
async deleteClient(knex, clientId) { async deleteClient(knex, clientId, access) {
const client = await knex("wg_client").where("id", clientId).first(); const query = knex("wg_client").where("id", clientId);
if (access && !access.token.hasScope("admin")) {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
const client = await query.first();
if (!client) { if (!client) {
throw new Error("Client not found"); throw new Error("Client not found");
} }
@ -314,8 +326,12 @@ const internalWireguard = {
/** /**
* Toggle a WireGuard client enabled/disabled * Toggle a WireGuard client enabled/disabled
*/ */
async toggleClient(knex, clientId, enabled) { async toggleClient(knex, clientId, enabled, access) {
const client = await knex("wg_client").where("id", clientId).first(); const query = knex("wg_client").where("id", clientId);
if (access && !access.token.hasScope("admin")) {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
const client = await query.first();
if (!client) { if (!client) {
throw new Error("Client not found"); throw new Error("Client not found");
} }
@ -333,8 +349,12 @@ const internalWireguard = {
/** /**
* Update a WireGuard client * Update a WireGuard client
*/ */
async updateClient(knex, clientId, data) { async updateClient(knex, clientId, data, access) {
const client = await knex("wg_client").where("id", clientId).first(); const query = knex("wg_client").where("id", clientId);
if (access && !access.token.hasScope("admin")) {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
const client = await query.first();
if (!client) { if (!client) {
throw new Error("Client not found"); throw new Error("Client not found");
} }
@ -392,7 +412,7 @@ const internalWireguard = {
/** /**
* Create a new WireGuard Interface Endpoint * Create a new WireGuard Interface Endpoint
*/ */
async createInterface(knex, data) { async createInterface(knex, data, access) {
const existingIfaces = await knex("wg_interface").select("name", "listen_port"); const existingIfaces = await knex("wg_interface").select("name", "listen_port");
const newIndex = existingIfaces.length; const newIndex = existingIfaces.length;
@ -416,6 +436,7 @@ const internalWireguard = {
dns: data.dns || WG_DEFAULT_DNS, dns: data.dns || WG_DEFAULT_DNS,
host: data.host || WG_HOST, host: data.host || WG_HOST,
isolate_clients: data.isolate_clients || false, isolate_clients: data.isolate_clients || false,
owner_user_id: access ? access.token.getUserId(1) : 1,
post_up: "iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE", post_up: "iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE",
post_down: "iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE", post_down: "iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE",
created_on: knex.fn.now(), created_on: knex.fn.now(),
@ -447,8 +468,12 @@ const internalWireguard = {
/** /**
* Update an existing Interface * Update an existing Interface
*/ */
async updateInterface(knex, id, data) { async updateInterface(knex, id, data, access) {
const iface = await knex("wg_interface").where("id", id).first(); const query = knex("wg_interface").where("id", id);
if (access && !access.token.hasScope("admin")) {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
const iface = await query.first();
if (!iface) throw new Error("Interface not found"); if (!iface) throw new Error("Interface not found");
const updateData = { modified_on: knex.fn.now() }; const updateData = { modified_on: knex.fn.now() };
@ -466,8 +491,12 @@ const internalWireguard = {
/** /**
* Delete an interface * Delete an interface
*/ */
async deleteInterface(knex, id) { async deleteInterface(knex, id, access) {
const iface = await knex("wg_interface").where("id", id).first(); const query = knex("wg_interface").where("id", id);
if (access && !access.token.hasScope("admin")) {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
const iface = await query.first();
if (!iface) throw new Error("Interface not found"); if (!iface) throw new Error("Interface not found");
try { try {
@ -487,7 +516,15 @@ const internalWireguard = {
/** /**
* Update Peering Links between WireGuard Interfaces * Update Peering Links between WireGuard Interfaces
*/ */
async updateInterfaceLinks(knex, id, linkedServers) { async updateInterfaceLinks(knex, id, linkedServers, access) {
// Verify ownership
const query = knex("wg_interface").where("id", id);
if (access && !access.token.hasScope("admin")) {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
const iface = await query.first();
if (!iface) throw new Error("Interface not found");
// Clean up existing links where this interface is involved // Clean up existing links where this interface is involved
await knex("wg_server_link").where("interface_id_1", id).orWhere("interface_id_2", id).del(); await knex("wg_server_link").where("interface_id_1", id).orWhere("interface_id_2", id).del();
@ -507,8 +544,13 @@ const internalWireguard = {
/** /**
* Get the WireGuard interfaces info * Get the WireGuard interfaces info
*/ */
async getInterfacesInfo(knex) { async getInterfacesInfo(knex, access) {
const ifaces = await knex("wg_interface").select("*"); const query = knex("wg_interface").select("*");
// Filter by owner if not admin
if (access && !access.token.hasScope("admin")) {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
const ifaces = await query;
const allLinks = await knex("wg_server_link").select("*"); const allLinks = await knex("wg_server_link").select("*");
return ifaces.map((i) => { return ifaces.map((i) => {

View file

@ -0,0 +1,35 @@
/**
* Migration to add owner_user_id to WireGuard tables for user-based data isolation
*/
export async function up(knex) {
// 1. Add owner_user_id to wg_interface
await knex.schema.alterTable("wg_interface", (table) => {
table.integer("owner_user_id").unsigned().nullable();
});
// 2. Add owner_user_id to wg_client
await knex.schema.alterTable("wg_client", (table) => {
table.integer("owner_user_id").unsigned().nullable();
});
// 3. Backfill existing rows with admin user (id=1)
await knex("wg_interface").whereNull("owner_user_id").update({ owner_user_id: 1 });
await knex("wg_client").whereNull("owner_user_id").update({ owner_user_id: 1 });
// 4. Make columns not nullable
await knex.schema.alterTable("wg_interface", (table) => {
table.integer("owner_user_id").unsigned().notNullable().defaultTo(1).alter();
});
await knex.schema.alterTable("wg_client", (table) => {
table.integer("owner_user_id").unsigned().notNullable().defaultTo(1).alter();
});
}
export async function down(knex) {
await knex.schema.alterTable("wg_client", (table) => {
table.dropColumn("owner_user_id");
});
await knex.schema.alterTable("wg_interface", (table) => {
table.dropColumn("owner_user_id");
});
}

View file

@ -21,7 +21,8 @@ router.use(jwtdecode());
router.get("/", async (_req, res, next) => { router.get("/", async (_req, res, next) => {
try { try {
const knex = db(); const knex = db();
const ifaces = await internalWireguard.getInterfacesInfo(knex); const access = res.locals.access;
const ifaces = await internalWireguard.getInterfacesInfo(knex, access);
res.status(200).json(ifaces); res.status(200).json(ifaces);
} catch (err) { } catch (err) {
next(err); next(err);
@ -35,8 +36,9 @@ router.get("/", async (_req, res, next) => {
router.post("/", async (req, res, next) => { router.post("/", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const iface = await internalWireguard.createInterface(knex, req.body); const access = res.locals.access;
await internalAuditLog.add(res.locals.access, { const iface = await internalWireguard.createInterface(knex, req.body, access);
await internalAuditLog.add(access, {
action: "created", action: "created",
object_type: "wireguard-server", object_type: "wireguard-server",
object_id: iface.id, object_id: iface.id,
@ -55,8 +57,9 @@ router.post("/", async (req, res, next) => {
router.put("/:id", async (req, res, next) => { router.put("/:id", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const iface = await internalWireguard.updateInterface(knex, req.params.id, req.body); const access = res.locals.access;
await internalAuditLog.add(res.locals.access, { const iface = await internalWireguard.updateInterface(knex, req.params.id, req.body, access);
await internalAuditLog.add(access, {
action: "updated", action: "updated",
object_type: "wireguard-server", object_type: "wireguard-server",
object_id: iface.id, object_id: iface.id,
@ -75,8 +78,9 @@ router.put("/:id", async (req, res, next) => {
router.delete("/:id", async (req, res, next) => { router.delete("/:id", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const result = await internalWireguard.deleteInterface(knex, req.params.id); const access = res.locals.access;
await internalAuditLog.add(res.locals.access, { const result = await internalWireguard.deleteInterface(knex, req.params.id, access);
await internalAuditLog.add(access, {
action: "deleted", action: "deleted",
object_type: "wireguard-server", object_type: "wireguard-server",
object_id: req.params.id, object_id: req.params.id,
@ -95,8 +99,9 @@ router.delete("/:id", async (req, res, next) => {
router.post("/:id/links", async (req, res, next) => { router.post("/:id/links", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const result = await internalWireguard.updateInterfaceLinks(knex, req.params.id, req.body.linked_servers || []); const access = res.locals.access;
await internalAuditLog.add(res.locals.access, { const result = await internalWireguard.updateInterfaceLinks(knex, req.params.id, req.body.linked_servers || [], access);
await internalAuditLog.add(access, {
action: "updated", action: "updated",
object_type: "wireguard-server-links", object_type: "wireguard-server-links",
object_id: req.params.id, object_id: req.params.id,
@ -115,7 +120,8 @@ router.post("/:id/links", async (req, res, next) => {
router.get("/client", async (_req, res, next) => { router.get("/client", async (_req, res, next) => {
try { try {
const knex = db(); const knex = db();
const clients = await internalWireguard.getClients(knex); const access = res.locals.access;
const clients = await internalWireguard.getClients(knex, access);
res.status(200).json(clients); res.status(200).json(clients);
} catch (err) { } catch (err) {
next(err); next(err);
@ -129,8 +135,9 @@ router.get("/client", async (_req, res, next) => {
router.post("/client", async (req, res, next) => { router.post("/client", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const client = await internalWireguard.createClient(knex, req.body); const access = res.locals.access;
await internalAuditLog.add(res.locals.access, { const client = await internalWireguard.createClient(knex, req.body, access);
await internalAuditLog.add(access, {
action: "created", action: "created",
object_type: "wireguard-client", object_type: "wireguard-client",
object_id: client.id, object_id: client.id,
@ -149,7 +156,12 @@ router.post("/client", async (req, res, next) => {
router.get("/client/:id", async (req, res, next) => { router.get("/client/:id", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const client = await knex("wg_client").where("id", req.params.id).first(); const access = res.locals.access;
const query = knex("wg_client").where("id", req.params.id);
if (!access.token.hasScope("admin")) {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
const client = await query.first();
if (!client) { if (!client) {
return res.status(404).json({ error: { message: "Client not found" } }); return res.status(404).json({ error: { message: "Client not found" } });
} }
@ -166,8 +178,9 @@ router.get("/client/:id", async (req, res, next) => {
router.put("/client/:id", async (req, res, next) => { router.put("/client/:id", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const client = await internalWireguard.updateClient(knex, req.params.id, req.body); const access = res.locals.access;
await internalAuditLog.add(res.locals.access, { const client = await internalWireguard.updateClient(knex, req.params.id, req.body, access);
await internalAuditLog.add(access, {
action: "updated", action: "updated",
object_type: "wireguard-client", object_type: "wireguard-client",
object_id: client.id, object_id: client.id,
@ -186,8 +199,9 @@ router.put("/client/:id", async (req, res, next) => {
router.delete("/client/:id", async (req, res, next) => { router.delete("/client/:id", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const result = await internalWireguard.deleteClient(knex, req.params.id); const access = res.locals.access;
await internalAuditLog.add(res.locals.access, { const result = await internalWireguard.deleteClient(knex, req.params.id, access);
await internalAuditLog.add(access, {
action: "deleted", action: "deleted",
object_type: "wireguard-client", object_type: "wireguard-client",
object_id: req.params.id, object_id: req.params.id,
@ -206,8 +220,9 @@ router.delete("/client/:id", async (req, res, next) => {
router.post("/client/:id/enable", async (req, res, next) => { router.post("/client/:id/enable", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const client = await internalWireguard.toggleClient(knex, req.params.id, true); const access = res.locals.access;
await internalAuditLog.add(res.locals.access, { const client = await internalWireguard.toggleClient(knex, req.params.id, true, access);
await internalAuditLog.add(access, {
action: "enabled", action: "enabled",
object_type: "wireguard-client", object_type: "wireguard-client",
object_id: client.id, object_id: client.id,
@ -226,8 +241,9 @@ router.post("/client/:id/enable", async (req, res, next) => {
router.post("/client/:id/disable", async (req, res, next) => { router.post("/client/:id/disable", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const client = await internalWireguard.toggleClient(knex, req.params.id, false); const access = res.locals.access;
await internalAuditLog.add(res.locals.access, { const client = await internalWireguard.toggleClient(knex, req.params.id, false, access);
await internalAuditLog.add(access, {
action: "disabled", action: "disabled",
object_type: "wireguard-client", object_type: "wireguard-client",
object_id: client.id, object_id: client.id,
@ -246,7 +262,12 @@ router.post("/client/:id/disable", async (req, res, next) => {
router.get("/client/:id/configuration", async (req, res, next) => { router.get("/client/:id/configuration", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const client = await knex("wg_client").where("id", req.params.id).first(); const access = res.locals.access;
const query = knex("wg_client").where("id", req.params.id);
if (!access.token.hasScope("admin")) {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
const client = await query.first();
if (!client) { if (!client) {
return res.status(404).json({ error: { message: "Client not found" } }); return res.status(404).json({ error: { message: "Client not found" } });
} }
@ -267,6 +288,15 @@ router.get("/client/:id/configuration", async (req, res, next) => {
router.get("/client/:id/qrcode.svg", async (req, res, next) => { router.get("/client/:id/qrcode.svg", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const access = res.locals.access;
const query = knex("wg_client").where("id", req.params.id);
if (!access.token.hasScope("admin")) {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
const client = await query.first();
if (!client) {
return res.status(404).json({ error: { message: "Client not found" } });
}
const svg = await internalWireguard.getClientQRCode(knex, req.params.id); const svg = await internalWireguard.getClientQRCode(knex, req.params.id);
res.set("Content-Type", "image/svg+xml"); res.set("Content-Type", "image/svg+xml");
res.status(200).send(svg); res.status(200).send(svg);
@ -282,7 +312,12 @@ router.get("/client/:id/qrcode.svg", async (req, res, next) => {
router.get("/client/:id/configuration.zip", async (req, res, next) => { router.get("/client/:id/configuration.zip", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const client = await knex("wg_client").where("id", req.params.id).first(); const access = res.locals.access;
const query = knex("wg_client").where("id", req.params.id);
if (!access.token.hasScope("admin")) {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
const client = await query.first();
if (!client) { if (!client) {
return res.status(404).json({ error: { message: "Client not found" } }); return res.status(404).json({ error: { message: "Client not found" } });
} }

View file

@ -2860,6 +2860,11 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
systeminformation@^5.31.3:
version "5.31.3"
resolved "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.3.tgz"
integrity sha512-vX0eeI7oGIr79NLiJRWnK8SyxDjyiNOEanaQnHRNyb5ep8QcpD8QMDvrukdrxV4pV4AKjwUDfaypXnWHMC/65A==
tar-fs@^2.0.0: tar-fs@^2.0.0:
version "2.1.4" version "2.1.4"
resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz" resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz"