diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 972ff1a7..fef085a1 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -62,7 +62,8 @@ export enum ActionsEnum { deleteResourceRule = "deleteResourceRule", listResourceRules = "listResourceRules", updateResourceRule = "updateResourceRule", - createClient = "createClient" + createClient = "createClient", + deleteClient = "deleteClient" } export async function checkUserActionPermission( diff --git a/server/db/schema.ts b/server/db/schema.ts index 02602e32..449618f7 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -121,9 +121,11 @@ export const clients = sqliteTable("clients", { siteId: integer("siteId").references(() => sites.siteId, { onDelete: "cascade" }), - orgId: text("orgId").references(() => orgs.orgId, { - onDelete: "cascade" - }), + orgId: text("orgId") + .references(() => orgs.orgId, { + onDelete: "cascade" + }) + .notNull(), name: text("name").notNull(), pubKey: text("pubKey"), subnet: text("subnet").notNull(), diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index 03de18cb..a0f01d08 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts @@ -14,3 +14,4 @@ export * from "./verifyAdmin"; export * from "./verifySetResourceUsers"; export * from "./verifyUserInRole"; export * from "./verifyAccessTokenAccess"; +export * from "./verifyClientAccess"; diff --git a/server/middlewares/verifyClientAccess.ts b/server/middlewares/verifyClientAccess.ts new file mode 100644 index 00000000..b024d416 --- /dev/null +++ b/server/middlewares/verifyClientAccess.ts @@ -0,0 +1,131 @@ +import { Request, Response, NextFunction } from "express"; +import { db } from "@server/db"; +import { userOrgs, clients, roleClients, userClients } from "@server/db/schema"; +import { and, eq } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; + +export async function verifyClientAccess( + req: Request, + res: Response, + next: NextFunction +) { + const userId = req.user!.userId; // Assuming you have user information in the request + const clientId = parseInt( + req.params.clientId || req.body.clientId || req.query.clientId + ); + + if (!userId) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") + ); + } + + if (isNaN(clientId)) { + return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid client ID")); + } + + try { + // Get the client + const [client] = await db + .select() + .from(clients) + .where(eq(clients.clientId, clientId)) + .limit(1); + + if (!client) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Client with ID ${clientId} not found` + ) + ); + } + + if (!client.orgId) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + `Client with ID ${clientId} does not have an organization ID` + ) + ); + } + + if (!req.userOrg) { + // Get user's role ID in the organization + const userOrgRole = await db + .select() + .from(userOrgs) + .where( + and( + eq(userOrgs.userId, userId), + eq(userOrgs.orgId, client.orgId) + ) + ) + .limit(1); + req.userOrg = userOrgRole[0]; + } + + if (!req.userOrg) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "User does not have access to this organization" + ) + ); + } + + const userOrgRoleId = req.userOrg.roleId; + req.userOrgRoleId = userOrgRoleId; + req.userOrgId = client.orgId; + + // Check role-based site access first + const [roleClientAccess] = await db + .select() + .from(roleClients) + .where( + and( + eq(roleClients.clientId, clientId), + eq(roleClients.roleId, userOrgRoleId) + ) + ) + .limit(1); + + if (roleClientAccess) { + // User has access to the site through their role + return next(); + } + + // If role doesn't have access, check user-specific site access + const [userClientAccess] = await db + .select() + .from(userClients) + .where( + and( + eq(userClients.userId, userId), + eq(userClients.clientId, clientId) + ) + ) + .limit(1); + + if (userClientAccess) { + // User has direct access to the site + return next(); + } + + // If we reach here, the user doesn't have access to the site + return next( + createHttpError( + HttpCode.FORBIDDEN, + "User does not have access to this client" + ) + ); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying site access" + ) + ); + } +} diff --git a/server/routers/client/deleteClient.ts b/server/routers/client/deleteClient.ts index 6ee12734..2036a3ce 100644 --- a/server/routers/client/deleteClient.ts +++ b/server/routers/client/deleteClient.ts @@ -1,23 +1,21 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { newts, newtSessions, sites } from "@server/db/schema"; +import { clients, sites } from "@server/db/schema"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; -import { deletePeer } from "../gerbil/peers"; import { fromError } from "zod-validation-error"; -import { sendToClient } from "../ws"; const deleteClientSchema = z .object({ - siteId: z.string().transform(Number).pipe(z.number().int().positive()) + clientId: z.string().transform(Number).pipe(z.number().int().positive()) }) .strict(); -export async function deleteSite( +export async function deleteClient( req: Request, res: Response, next: NextFunction @@ -33,56 +31,30 @@ export async function deleteSite( ); } - const { siteId } = parsedParams.data; + const { clientId } = parsedParams.data; - const [site] = await db + const [client] = await db .select() - .from(sites) - .where(eq(sites.siteId, siteId)) + .from(clients) + .where(eq(clients.clientId, clientId)) .limit(1); - if (!site) { + if (!client) { return next( createHttpError( HttpCode.NOT_FOUND, - `Site with ID ${siteId} not found` + `Site with ID ${clientId} not found` ) ); } - await db.transaction(async (trx) => { - if (site.pubKey) { - if (site.type == "wireguard") { - await deletePeer(site.exitNodeId!, site.pubKey); - } else if (site.type == "newt") { - // get the newt on the site by querying the newt table for siteId - const [deletedNewt] = await trx - .delete(newts) - .where(eq(newts.siteId, siteId)) - .returning(); - if (deletedNewt) { - const payload = { - type: `newt/terminate`, - data: {} - }; - sendToClient(deletedNewt.newtId, payload); - - // delete all of the sessions for the newt - await trx - .delete(newtSessions) - .where(eq(newtSessions.newtId, deletedNewt.newtId)); - } - } - } - - await trx.delete(sites).where(eq(sites.siteId, siteId)); - }); + await db.delete(sites).where(eq(sites.siteId, clientId)); return response(res, { data: null, success: true, error: false, - message: "Site deleted successfully", + message: "Client deleted successfully", status: HttpCode.OK }); } catch (error) { diff --git a/server/routers/external.ts b/server/routers/external.ts index d2e680ec..8e064ab7 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -22,7 +22,8 @@ import { verifyRoleAccess, verifySetResourceUsers, verifyUserAccess, - getUserOrgs + getUserOrgs, + verifyClientAccess } from "@server/middlewares"; import { verifyUserHasAction } from "../middlewares/verifyUserHasAction"; import { ActionsEnum } from "@server/auth/actions"; @@ -98,7 +99,7 @@ authenticated.get( authenticated.get( "/site/:siteId/pick-client-defaults", - verifyOrgAccess, + verifySiteAccess, verifyUserHasAction(ActionsEnum.createClient), client.pickClientDefaults ); @@ -110,6 +111,14 @@ authenticated.put( client.createClient ); +authenticated.delete( + "/client/:clientId", + verifyClientAccess, + verifyUserHasAction(ActionsEnum.deleteClient), + client.deleteClient +); + + // authenticated.get( // "/site/:siteId/roles", // verifySiteAccess,