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
*/
async getClients(knex) {
async getClients(knex, access) {
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")
.select("wg_client.*", "wg_interface.name as interface_name")
.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) => ({
id: c.id,
name: c.name,
@ -258,7 +265,7 @@ const internalWireguard = {
/**
* Create a new WireGuard client
*/
async createClient(knex, data) {
async createClient(knex, data, access) {
const iface = data.interface_id
? await knex("wg_interface").where("id", data.interface_id).first()
: await this.getOrCreateInterface(knex);
@ -284,6 +291,7 @@ const internalWireguard = {
persistent_keepalive: data.persistent_keepalive || WG_DEFAULT_PERSISTENT_KEEPALIVE,
expires_at: data.expires_at || null,
interface_id: iface.id,
owner_user_id: access ? access.token.getUserId(1) : 1,
created_on: knex.fn.now(),
modified_on: knex.fn.now(),
};
@ -299,8 +307,12 @@ const internalWireguard = {
/**
* Delete a WireGuard client
*/
async deleteClient(knex, clientId) {
const client = await knex("wg_client").where("id", clientId).first();
async deleteClient(knex, clientId, access) {
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) {
throw new Error("Client not found");
}
@ -314,8 +326,12 @@ const internalWireguard = {
/**
* Toggle a WireGuard client enabled/disabled
*/
async toggleClient(knex, clientId, enabled) {
const client = await knex("wg_client").where("id", clientId).first();
async toggleClient(knex, clientId, enabled, access) {
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) {
throw new Error("Client not found");
}
@ -333,8 +349,12 @@ const internalWireguard = {
/**
* Update a WireGuard client
*/
async updateClient(knex, clientId, data) {
const client = await knex("wg_client").where("id", clientId).first();
async updateClient(knex, clientId, data, access) {
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) {
throw new Error("Client not found");
}
@ -392,7 +412,7 @@ const internalWireguard = {
/**
* 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 newIndex = existingIfaces.length;
@ -416,6 +436,7 @@ const internalWireguard = {
dns: data.dns || WG_DEFAULT_DNS,
host: data.host || WG_HOST,
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_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(),
@ -447,8 +468,12 @@ const internalWireguard = {
/**
* Update an existing Interface
*/
async updateInterface(knex, id, data) {
const iface = await knex("wg_interface").where("id", id).first();
async updateInterface(knex, id, data, access) {
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");
const updateData = { modified_on: knex.fn.now() };
@ -466,8 +491,12 @@ const internalWireguard = {
/**
* Delete an interface
*/
async deleteInterface(knex, id) {
const iface = await knex("wg_interface").where("id", id).first();
async deleteInterface(knex, id, access) {
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");
try {
@ -487,7 +516,15 @@ const internalWireguard = {
/**
* 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
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
*/
async getInterfacesInfo(knex) {
const ifaces = await knex("wg_interface").select("*");
async getInterfacesInfo(knex, access) {
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("*");
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) => {
try {
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);
} catch (err) {
next(err);
@ -35,8 +36,9 @@ router.get("/", async (_req, res, next) => {
router.post("/", async (req, res, next) => {
try {
const knex = db();
const iface = await internalWireguard.createInterface(knex, req.body);
await internalAuditLog.add(res.locals.access, {
const access = res.locals.access;
const iface = await internalWireguard.createInterface(knex, req.body, access);
await internalAuditLog.add(access, {
action: "created",
object_type: "wireguard-server",
object_id: iface.id,
@ -55,8 +57,9 @@ router.post("/", async (req, res, next) => {
router.put("/:id", async (req, res, next) => {
try {
const knex = db();
const iface = await internalWireguard.updateInterface(knex, req.params.id, req.body);
await internalAuditLog.add(res.locals.access, {
const access = res.locals.access;
const iface = await internalWireguard.updateInterface(knex, req.params.id, req.body, access);
await internalAuditLog.add(access, {
action: "updated",
object_type: "wireguard-server",
object_id: iface.id,
@ -75,8 +78,9 @@ router.put("/:id", async (req, res, next) => {
router.delete("/:id", async (req, res, next) => {
try {
const knex = db();
const result = await internalWireguard.deleteInterface(knex, req.params.id);
await internalAuditLog.add(res.locals.access, {
const access = res.locals.access;
const result = await internalWireguard.deleteInterface(knex, req.params.id, access);
await internalAuditLog.add(access, {
action: "deleted",
object_type: "wireguard-server",
object_id: req.params.id,
@ -95,8 +99,9 @@ router.delete("/:id", async (req, res, next) => {
router.post("/:id/links", async (req, res, next) => {
try {
const knex = db();
const result = await internalWireguard.updateInterfaceLinks(knex, req.params.id, req.body.linked_servers || []);
await internalAuditLog.add(res.locals.access, {
const access = res.locals.access;
const result = await internalWireguard.updateInterfaceLinks(knex, req.params.id, req.body.linked_servers || [], access);
await internalAuditLog.add(access, {
action: "updated",
object_type: "wireguard-server-links",
object_id: req.params.id,
@ -115,7 +120,8 @@ router.post("/:id/links", async (req, res, next) => {
router.get("/client", async (_req, res, next) => {
try {
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);
} catch (err) {
next(err);
@ -129,8 +135,9 @@ router.get("/client", async (_req, res, next) => {
router.post("/client", async (req, res, next) => {
try {
const knex = db();
const client = await internalWireguard.createClient(knex, req.body);
await internalAuditLog.add(res.locals.access, {
const access = res.locals.access;
const client = await internalWireguard.createClient(knex, req.body, access);
await internalAuditLog.add(access, {
action: "created",
object_type: "wireguard-client",
object_id: client.id,
@ -149,7 +156,12 @@ router.post("/client", async (req, res, next) => {
router.get("/client/:id", async (req, res, next) => {
try {
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) {
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) => {
try {
const knex = db();
const client = await internalWireguard.updateClient(knex, req.params.id, req.body);
await internalAuditLog.add(res.locals.access, {
const access = res.locals.access;
const client = await internalWireguard.updateClient(knex, req.params.id, req.body, access);
await internalAuditLog.add(access, {
action: "updated",
object_type: "wireguard-client",
object_id: client.id,
@ -186,8 +199,9 @@ router.put("/client/:id", async (req, res, next) => {
router.delete("/client/:id", async (req, res, next) => {
try {
const knex = db();
const result = await internalWireguard.deleteClient(knex, req.params.id);
await internalAuditLog.add(res.locals.access, {
const access = res.locals.access;
const result = await internalWireguard.deleteClient(knex, req.params.id, access);
await internalAuditLog.add(access, {
action: "deleted",
object_type: "wireguard-client",
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) => {
try {
const knex = db();
const client = await internalWireguard.toggleClient(knex, req.params.id, true);
await internalAuditLog.add(res.locals.access, {
const access = res.locals.access;
const client = await internalWireguard.toggleClient(knex, req.params.id, true, access);
await internalAuditLog.add(access, {
action: "enabled",
object_type: "wireguard-client",
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) => {
try {
const knex = db();
const client = await internalWireguard.toggleClient(knex, req.params.id, false);
await internalAuditLog.add(res.locals.access, {
const access = res.locals.access;
const client = await internalWireguard.toggleClient(knex, req.params.id, false, access);
await internalAuditLog.add(access, {
action: "disabled",
object_type: "wireguard-client",
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) => {
try {
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) {
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) => {
try {
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);
res.set("Content-Type", "image/svg+xml");
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) => {
try {
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) {
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"
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:
version "2.1.4"
resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz"