fix(wireguard): isolate user data by owner_user_id
This commit is contained in:
parent
d67081492d
commit
3f0d529d14
5 changed files with 158 additions and 40 deletions
1
.agents/skills/spawn-agent
Submodule
1
.agents/skills/spawn-agent
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 288a767d8b251004f7bd7999dcdf408cbbaa86c7
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
35
backend/migrations/20260310000000_wireguard_owner.js
Normal file
35
backend/migrations/20260310000000_wireguard_owner.js
Normal 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");
|
||||
});
|
||||
}
|
||||
|
|
@ -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" } });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue