This commit is contained in:
KingKDot
2026-05-19 00:46:30 -05:00
parent bbd61af672
commit 116beff515
31 changed files with 2170 additions and 521 deletions
@@ -24,6 +24,7 @@ import {
recordSuccessfulAttempt,
} from "../../rateLimit";
import { getUserById, canUserAccessClient, canUserAccessFeature, getUserFeaturePermissions, type FeatureName, ALL_FEATURES } from "../../users";
import { getUserPermissions, requirePermission } from "../../rbac";
import { makeAuthCookie, makeAuthCookieClear } from "./auth-cookie";
type RequestIpProvider = {
@@ -214,8 +215,11 @@ export async function handleAuthRoutes(
if (!user) {
return Response.json({ error: "Not authenticated" }, { status: 401 });
}
if (user.role !== "admin") {
return Response.json({ error: "Admin only" }, { status: 403 });
try {
requirePermission(user, "users:manage");
} catch (error) {
if (error instanceof Response) return error;
return Response.json({ error: "Forbidden" }, { status: 403 });
}
const targetUserId = parseInt(url.pathname.split("/")[3]);
@@ -274,8 +278,11 @@ export async function handleAuthRoutes(
if (!user) {
return Response.json({ error: "Not authenticated" }, { status: 401 });
}
if (user.role !== "admin") {
return Response.json({ error: "Admin only" }, { status: 403 });
try {
requirePermission(user, "users:manage");
} catch (error) {
if (error instanceof Response) return error;
return Response.json({ error: "Forbidden" }, { status: 403 });
}
const targetUserId = parseInt(url.pathname.split("/")[3]);
@@ -341,6 +348,7 @@ export async function handleAuthRoutes(
const dbUser = getUserById(user.userId);
const featurePermissions = getUserFeaturePermissions(user.userId);
const permissions = getUserPermissions(user.userId, user.role);
return new Response(
JSON.stringify({
@@ -351,6 +359,7 @@ export async function handleAuthRoutes(
canBuild: dbUser ? Boolean(dbUser.can_build) : false,
telegramChatId: dbUser?.telegram_chat_id || "",
featurePermissions,
permissions,
}),
{
headers: { "Content-Type": "application/json" },
@@ -50,8 +50,11 @@ export async function handleAutoDeployRoutes(
headers: { "Content-Type": "application/json" },
});
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "deploys:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let form: FormData;
@@ -123,8 +126,11 @@ export async function handleAutoDeployRoutes(
headers: { "Content-Type": "application/json" },
});
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "deploys:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
@@ -168,8 +174,11 @@ export async function handleAutoDeployRoutes(
headers: { "Content-Type": "application/json" },
});
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "deploys:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const deploy = getAutoDeploy(autoDeployMatch[1]);
@@ -40,8 +40,11 @@ export async function handleAutoScriptsRoutes(
headers: { "Content-Type": "application/json" },
});
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "scripts:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
@@ -95,8 +98,11 @@ export async function handleAutoScriptsRoutes(
headers: { "Content-Type": "application/json" },
});
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "scripts:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
@@ -147,8 +153,11 @@ export async function handleAutoScriptsRoutes(
headers: { "Content-Type": "application/json" },
});
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "scripts:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
deleteAutoScript(autoScriptMatch[1]);
@@ -852,10 +852,7 @@ export async function handleBuildRoutes(
}
if (req.method === "POST" && url.pathname === "/api/build/update-all") {
requirePermission(user, "clients:build");
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
requirePermission(user, "deploys:manage");
let body: any = {};
try {
@@ -1,7 +1,7 @@
import { authenticateRequest } from "../../auth";
import { getConfig } from "../../config";
import { getChatHistory, getOnlineClientCountForUser } from "../../db";
import { hasPermission } from "../../users";
import { hasPermission } from "../../rbac";
export async function handleChatRoutes(
req: Request,
@@ -29,7 +29,7 @@ import {
} from "../../db";
import { metrics } from "../../metrics";
import { encodeMessage } from "../../protocol";
import { requirePermission } from "../../rbac";
import { requireClientAccess, requireFeatureAccess, requirePermission } from "../../rbac";
import {
canUserAccessClient,
getUserClientAccessScope,
@@ -158,8 +158,11 @@ export async function handleClientRoutes(
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "network:manage-bans");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
if (req.method === "GET") {
@@ -208,8 +211,11 @@ export async function handleClientRoutes(
}
const targetId = banMatch[1];
if (!canUserAccessClient(user.userId, user.role, targetId)) {
return new Response("Forbidden: Client access denied", { status: 403 });
try {
requireClientAccess(user, targetId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const target = clientManager.getClient(targetId);
const targetIp = target?.ip || getClientIp(targetId);
@@ -247,8 +253,11 @@ export async function handleClientRoutes(
return new Response("Unauthorized", { status: 401 });
}
const clientId = thumbnailMatch[1];
if (!canUserAccessClient(user.userId, user.role, clientId)) {
return new Response("Forbidden: Client access denied", { status: 403 });
try {
requireClientAccess(user, clientId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const { requestThumbnailRegen, markThumbnailRequested, getThumbnailVersion, waitForThumbnail, clearThumbnailRequest } = await import("../../thumbnails");
markThumbnailRequested(clientId);
@@ -286,8 +295,11 @@ export async function handleClientRoutes(
return new Response("Unauthorized", { status: 401 });
}
const clientId = thumbnailMatch[1];
if (!canUserAccessClient(user.userId, user.role, clientId)) {
return new Response("Forbidden: Client access denied", { status: 403 });
try {
requireClientAccess(user, clientId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const { getThumbnailRecord } = await import("../../thumbnails");
const record = getThumbnailRecord(clientId);
@@ -349,8 +361,11 @@ export async function handleClientRoutes(
}
const targetId = clientDeleteMatch[1];
if (!canUserAccessClient(user.userId, user.role, targetId)) {
return new Response("Forbidden: Client access denied", { status: 403 });
try {
requireClientAccess(user, targetId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const target = clientManager.getClient(targetId);
const isOnlineInDb = getClientOnlineState(targetId);
@@ -399,15 +414,19 @@ export async function handleClientRoutes(
if (!user) return new Response("Unauthorized", { status: 401 });
try {
requirePermission(user, "clients:control");
requirePermission(user, "clients:metadata");
requireFeatureAccess(user, "client_metadata");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const targetId = clientNicknameMatch[1];
if (!canUserAccessClient(user.userId, user.role, targetId)) {
return new Response("Forbidden: Client access denied", { status: 403 });
try {
requireClientAccess(user, targetId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
if (!clientExists(targetId)) {
return Response.json({ error: "Client not found" }, { status: 404 });
@@ -456,15 +475,19 @@ export async function handleClientRoutes(
if (!user) return new Response("Unauthorized", { status: 401 });
try {
requirePermission(user, "clients:control");
requirePermission(user, "clients:metadata");
requireFeatureAccess(user, "client_metadata");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const targetId = clientTagMatch[1];
if (!canUserAccessClient(user.userId, user.role, targetId)) {
return new Response("Forbidden: Client access denied", { status: 403 });
try {
requireClientAccess(user, targetId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
if (!clientExists(targetId)) {
return Response.json({ error: "Client not found" }, { status: 404 });
@@ -517,7 +540,7 @@ export async function handleClientRoutes(
if (req.method === "PATCH" && url.pathname === "/api/clients/bulk-notifications-muted") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
try { requirePermission(user, "clients:control"); } catch (error) {
try { requirePermission(user, "clients:metadata"); requireFeatureAccess(user, "client_metadata"); } catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
@@ -555,14 +578,17 @@ export async function handleClientRoutes(
if (req.method === "PATCH" && muteMatch) {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
try { requirePermission(user, "clients:control"); } catch (error) {
try { requirePermission(user, "clients:metadata"); requireFeatureAccess(user, "client_metadata"); } catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const targetId = muteMatch[1];
if (!canUserAccessClient(user.userId, user.role, targetId)) {
return new Response("Forbidden: Client access denied", { status: 403 });
try {
requireClientAccess(user, targetId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
if (!clientExists(targetId)) {
return Response.json({ error: "Client not found" }, { status: 404 });
@@ -602,8 +628,11 @@ export async function handleClientRoutes(
if (!user) return new Response("Unauthorized", { status: 401 });
const targetId = bookmarkMatch[1];
if (!canUserAccessClient(user.userId, user.role, targetId)) {
return new Response("Forbidden: Client access denied", { status: 403 });
try {
requireClientAccess(user, targetId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
if (!clientExists(targetId)) {
return Response.json({ error: "Client not found" }, { status: 404 });
@@ -639,8 +668,11 @@ export async function handleClientRoutes(
}
const targetId = cmdMatch[1];
if (!canUserAccessClient(user.userId, user.role, targetId)) {
return new Response("Forbidden: Client access denied", { status: 403 });
try {
requireClientAccess(user, targetId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const target = clientManager.getClient(targetId);
const ip = server.requestIP(req)?.address || "unknown";
@@ -661,6 +693,13 @@ export async function handleClientRoutes(
for (let i = 0; i < count; i++) {
}
} else if (action === "disconnect") {
try {
requirePermission(user, "clients:disconnect");
requireFeatureAccess(user, "disconnect");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
target.ws.send(encodeMessage({ type: "command", commandType: "disconnect", id: uuidv4() }));
metrics.recordCommand("disconnect");
logAudit({
@@ -672,6 +711,13 @@ export async function handleClientRoutes(
success: true,
});
} else if (action === "reconnect") {
try {
requirePermission(user, "clients:reconnect");
requireFeatureAccess(user, "reconnect");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
target.ws.send(encodeMessage({ type: "command", commandType: "reconnect", id: uuidv4() }));
metrics.recordCommand("reconnect");
logAudit({
@@ -774,8 +820,11 @@ export async function handleClientRoutes(
return Response.json({ ok: false, error: error.message || "Voice capability probe failed" }, { status: 504 });
}
} else if (action === "silent_exec") {
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "clients:silent-exec");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const command = typeof body?.command === "string" ? body.command.trim() : "";
@@ -806,6 +855,13 @@ export async function handleClientRoutes(
details: JSON.stringify({ command, args, cwd }),
});
} else if (action === "uninstall") {
try {
requirePermission(user, "clients:uninstall");
requireFeatureAccess(user, "uninstall");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
target.ws.send(encodeMessage({ type: "command", commandType: "uninstall", id: uuidv4() }));
metrics.recordCommand("uninstall");
clientManager.deleteClient(targetId);
@@ -821,8 +877,11 @@ export async function handleClientRoutes(
success: true,
});
} else if (action === "elevate") {
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "clients:silent-exec");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const password = typeof body?.password === "string" ? body.password : "";
@@ -980,7 +1039,7 @@ export async function handleClientRoutes(
if (req.method === "PATCH" && url.pathname === "/api/clients/bulk-group") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
try { requirePermission(user, "clients:control"); } catch (error) {
try { requirePermission(user, "clients:metadata"); requireFeatureAccess(user, "client_metadata"); } catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
@@ -1019,14 +1078,17 @@ export async function handleClientRoutes(
if (req.method === "PATCH" && clientGroupMatch) {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
try { requirePermission(user, "clients:control"); } catch (error) {
try { requirePermission(user, "clients:metadata"); requireFeatureAccess(user, "client_metadata"); } catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const targetId = clientGroupMatch[1];
if (!canUserAccessClient(user.userId, user.role, targetId)) {
return new Response("Forbidden: Client access denied", { status: 403 });
try {
requireClientAccess(user, targetId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
if (!clientExists(targetId)) {
return Response.json({ error: "Client not found" }, { status: 404 });
@@ -7,6 +7,7 @@ import { AuditAction, logAudit } from "../../auditLog";
import * as clientManager from "../../clientManager";
import { metrics } from "../../metrics";
import { encodeMessage } from "../../protocol";
import { requirePermission } from "../../rbac";
import { createUploadPull } from "./file-download-routes";
type RequestIpProvider = {
@@ -44,8 +45,11 @@ export async function handleDeployRoutes(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "deploys:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let form: FormData;
@@ -87,8 +91,11 @@ export async function handleDeployRoutes(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "deploys:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
@@ -166,8 +173,11 @@ export async function handleDeployRoutes(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "deploys:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
@@ -289,8 +299,11 @@ export async function handleDeployRoutes(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "deploys:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
@@ -346,8 +346,11 @@ export async function handleEnrollmentRoutes(
if (req.method === "GET" && url.pathname === "/api/enrollment/banned-ips") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "network:manage-bans");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
return Response.json({ items: listBannedIps() });
@@ -357,8 +360,11 @@ export async function handleEnrollmentRoutes(
if (req.method === "DELETE" && url.pathname === "/api/enrollment/banned-ips") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "network:manage-bans");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const ipToUnban = (url.searchParams.get("ip") || "").trim();
@@ -384,8 +390,11 @@ export async function handleEnrollmentRoutes(
if (req.method === "POST" && url.pathname === "/api/enrollment/ban-ip") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "network:manage-bans");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any;
@@ -395,7 +395,7 @@ describe("file route feature_browser enforcement", () => {
expect(res).not.toBeNull();
expect(res!.status).toBe(403);
expect(await res!.text()).toContain("client access denied");
expect(await res!.text()).toContain("do not have access to this client");
} finally {
clientManager.deleteClient(clientId);
deleteUser(auth.userId);
@@ -8,7 +8,7 @@ import { logger } from "../../logger";
import { encodeMessage } from "../../protocol";
import { getConfig } from "../../config";
import { isAuthorizedAgentRequest } from "../agent-auth";
import { canUserAccessClient, canUserAccessFeature } from "../../users";
import { requireClientAccess, requireFeatureAccess, requirePermission } from "../../rbac";
type RequestIpProvider = {
requestIP: (req: Request) => { address?: string } | null | undefined;
@@ -156,12 +156,18 @@ async function serveDownloadById(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin" && user.role !== "operator") {
return new Response("Forbidden: Admin or operator access required", { status: 403 });
try {
requirePermission(user, "clients:control");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
if (!canUserAccessFeature(user.userId, user.role as any, "file_browser")) {
return new Response("Forbidden: feature access denied", { status: 403 });
try {
requireFeatureAccess(user, "file_browser");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
logger.debug("[filebrowser] http download request", {
@@ -345,8 +351,11 @@ export async function handleFileDownloadRoutes(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin" && user.role !== "operator") {
return new Response("Forbidden: Admin or operator access required", { status: 403 });
try {
requirePermission(user, "clients:control");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
@@ -363,12 +372,12 @@ export async function handleFileDownloadRoutes(
return new Response("Bad request", { status: 400 });
}
if (!canUserAccessClient(user.userId, user.role as any, clientId)) {
return new Response("Forbidden: client access denied", { status: 403 });
}
if (!canUserAccessFeature(user.userId, user.role as any, "file_browser")) {
return new Response("Forbidden: feature access denied", { status: 403 });
try {
requireClientAccess(user, clientId);
requireFeatureAccess(user, "file_browser");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
logger.debug("[filebrowser] http upload request", {
@@ -412,12 +421,18 @@ export async function handleFileDownloadRoutes(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin" && user.role !== "operator") {
return new Response("Forbidden: Admin or operator access required", { status: 403 });
try {
requirePermission(user, "clients:control");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
if (!canUserAccessFeature(user.userId, user.role as any, "file_browser")) {
return new Response("Forbidden: feature access denied", { status: 403 });
try {
requireFeatureAccess(user, "file_browser");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let uploadId = "";
@@ -642,8 +657,11 @@ export async function handleFileDownloadRoutes(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin" && user.role !== "operator") {
return new Response("Forbidden: Admin or operator access required", { status: 403 });
try {
requirePermission(user, "clients:control");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
@@ -659,12 +677,12 @@ export async function handleFileDownloadRoutes(
return new Response("Bad request", { status: 400 });
}
if (!canUserAccessClient(user.userId, user.role as any, clientId)) {
return new Response("Forbidden: client access denied", { status: 403 });
}
if (!canUserAccessFeature(user.userId, user.role as any, "file_browser")) {
return new Response("Forbidden: feature access denied", { status: 403 });
try {
requireClientAccess(user, clientId);
requireFeatureAccess(user, "file_browser");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const target = clientManager.getClient(clientId);
@@ -110,8 +110,11 @@ export async function handleMiscRoutes(
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "system:configure");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
if (req.method === "GET") {
@@ -157,8 +160,11 @@ export async function handleMiscRoutes(
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "system:configure");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
if (req.method === "GET") {
@@ -209,8 +215,11 @@ export async function handleMiscRoutes(
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "system:configure");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
@@ -447,8 +456,11 @@ export async function handleMiscRoutes(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "system:configure");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const exportData = getExportableConfig(deps.SERVER_VERSION);
@@ -477,8 +489,11 @@ export async function handleMiscRoutes(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "system:configure");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
@@ -526,8 +541,11 @@ export async function handleMiscRoutes(
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "system:configure");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
if (req.method === "GET") {
@@ -564,8 +582,11 @@ export async function handleMiscRoutes(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "system:configure");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
if (req.method === "GET") {
@@ -627,7 +648,7 @@ export async function handleMiscRoutes(
if (url.pathname === "/api/settings/registration") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role !== "admin") return new Response("Forbidden: Admin access required", { status: 403 });
try { requirePermission(user, "system:configure"); } catch (error) { if (error instanceof Response) return error; return new Response("Forbidden", { status: 403 }); }
if (req.method === "GET") {
return Response.json({ registration: getConfig().registration }, { headers: deps.CORS_HEADERS });
@@ -643,6 +664,7 @@ export async function handleMiscRoutes(
mode: body?.mode,
defaultRole: body?.defaultRole,
maxUsersTotal: body?.maxUsersTotal !== undefined ? Number(body.maxUsersTotal) : undefined,
defaultGroupIds: Array.isArray(body?.defaultGroupIds) ? body.defaultGroupIds : undefined,
});
logAudit({
@@ -662,7 +684,7 @@ export async function handleMiscRoutes(
if (url.pathname === "/api/settings/build-rate-limit") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role !== "admin") return new Response("Forbidden: Admin access required", { status: 403 });
try { requirePermission(user, "system:configure"); } catch (error) { if (error instanceof Response) return error; return new Response("Forbidden", { status: 403 }); }
if (req.method === "GET") {
return Response.json({ buildRateLimit: getConfig().buildRateLimit }, { headers: deps.CORS_HEADERS });
@@ -703,7 +725,7 @@ export async function handleMiscRoutes(
}
if (req.method === "PUT") {
if (user.role !== "admin") return new Response("Forbidden: Admin access required", { status: 403 });
try { requirePermission(user, "system:configure"); } catch (error) { if (error instanceof Response) return error; return new Response("Forbidden", { status: 403 }); }
let body: any = {};
try { body = await req.json(); } catch {
@@ -9,6 +9,7 @@ import {
getUserNotificationSettings,
updateUserNotificationSettings,
} from "../../users";
import { requirePermission } from "../../rbac";
import {
DEFAULT_WEBHOOK_TEMPLATE,
DEFAULT_TELEGRAM_TEMPLATE,
@@ -76,8 +77,11 @@ export async function handleNotificationsConfigRoutes(
headers: { "Content-Type": "application/json" },
});
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "system:configure");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
return Response.json({ notifications: getConfig().notifications });
}
@@ -90,10 +94,11 @@ export async function handleNotificationsConfigRoutes(
headers: { "Content-Type": "application/json" },
});
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", {
status: 403,
});
try {
requirePermission(user, "system:configure");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
@@ -0,0 +1,258 @@
import { authenticateRequest } from "../../auth";
import { AuditAction, logAudit } from "../../auditLog";
import {
createPermissionGroup,
deletePermissionGroup,
getPermissionGroup,
getUserById,
getUserExtraPermissions,
getUserGroupIds,
listPermissionGroups,
setUserExtraPermissions,
setUserGroups,
updatePermissionGroup,
} from "../../users";
import {
getPermissionDescription,
type Permission,
requirePermission,
} from "../../rbac";
const KNOWN_PERMISSIONS: Permission[] = [
"users:manage",
"clients:control",
"clients:build",
"clients:enroll",
"clients:silent-exec",
"clients:disconnect",
"clients:reconnect",
"clients:metadata",
"clients:uninstall",
"clients:winre",
"audit:view",
"chat:write",
"scripts:manage",
"deploys:manage",
"plugins:manage",
"plugins:configure",
"network:manage-bans",
"system:configure",
];
function sanitizePermissions(raw: unknown): string[] {
if (!Array.isArray(raw)) return [];
const known = new Set<string>(KNOWN_PERMISSIONS);
return Array.from(
new Set(
raw
.filter((p): p is string => typeof p === "string")
.map((p) => p.trim())
.filter((p) => known.has(p)),
),
);
}
type RequestIpProvider = {
requestIP: (req: Request) => { address?: string } | null | undefined;
};
export async function handlePermissionGroupsRoutes(
req: Request,
url: URL,
server: RequestIpProvider,
): Promise<Response | null> {
if (req.method === "GET" && url.pathname === "/api/permissions") {
const user = await authenticateRequest(req);
try {
requirePermission(user, "users:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
return Response.json({
permissions: KNOWN_PERMISSIONS.map((p) => ({
id: p,
description: getPermissionDescription(p),
})),
});
}
if (req.method === "GET" && url.pathname === "/api/permission-groups") {
const user = await authenticateRequest(req);
try {
requirePermission(user, "users:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
return Response.json({ groups: listPermissionGroups() });
}
if (req.method === "POST" && url.pathname === "/api/permission-groups") {
const user = await authenticateRequest(req);
let authed;
try {
authed = requirePermission(user, "users:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any;
try { body = await req.json(); } catch { return Response.json({ error: "Invalid JSON" }, { status: 400 }); }
const name = typeof body?.name === "string" ? body.name : "";
const description = typeof body?.description === "string" ? body.description : null;
const permissions = sanitizePermissions(body?.permissions);
const result = createPermissionGroup(name, description, permissions, authed.userId);
if (!result.success) return Response.json({ error: result.error }, { status: 400 });
logAudit({
timestamp: Date.now(),
username: authed.username,
ip: server.requestIP(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: `Created permission group "${result.group!.name}" with ${result.group!.permissions.length} permissions`,
success: true,
});
return Response.json({ group: result.group });
}
const singleGroupMatch = url.pathname.match(/^\/api\/permission-groups\/(\d+)$/);
if (singleGroupMatch) {
const groupId = Number(singleGroupMatch[1]);
const user = await authenticateRequest(req);
let authed;
try {
authed = requirePermission(user, "users:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
if (req.method === "GET") {
const group = getPermissionGroup(groupId);
if (!group) return Response.json({ error: "Not found" }, { status: 404 });
return Response.json({ group });
}
if (req.method === "PATCH" || req.method === "PUT") {
let body: any;
try { body = await req.json(); } catch { return Response.json({ error: "Invalid JSON" }, { status: 400 }); }
const updates: Parameters<typeof updatePermissionGroup>[1] = {};
if (body?.name !== undefined) updates.name = String(body.name);
if (body?.description !== undefined) updates.description = body.description === null ? null : String(body.description);
if (body?.permissions !== undefined) updates.permissions = sanitizePermissions(body.permissions);
const result = updatePermissionGroup(groupId, updates);
if (!result.success) return Response.json({ error: result.error }, { status: 400 });
logAudit({
timestamp: Date.now(),
username: authed.username,
ip: server.requestIP(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: `Updated permission group "${result.group!.name}"`,
success: true,
});
return Response.json({ group: result.group });
}
if (req.method === "DELETE") {
const existing = getPermissionGroup(groupId);
const result = deletePermissionGroup(groupId);
if (!result.success) return Response.json({ error: result.error }, { status: 404 });
logAudit({
timestamp: Date.now(),
username: authed.username,
ip: server.requestIP(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: `Deleted permission group "${existing?.name ?? groupId}"`,
success: true,
});
return Response.json({ ok: true });
}
}
const userGroupsMatch = url.pathname.match(/^\/api\/users\/(\d+)\/permission-groups$/);
if (userGroupsMatch) {
const targetUserId = Number(userGroupsMatch[1]);
const user = await authenticateRequest(req);
let authed;
try {
authed = requirePermission(user, "users:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
if (!getUserById(targetUserId)) {
return Response.json({ error: "User not found" }, { status: 404 });
}
if (req.method === "GET") {
return Response.json({ groupIds: getUserGroupIds(targetUserId) });
}
if (req.method === "PUT") {
let body: any;
try { body = await req.json(); } catch { return Response.json({ error: "Invalid JSON" }, { status: 400 }); }
const ids = Array.isArray(body?.groupIds)
? body.groupIds.map((g: any) => Number(g)).filter((g: number) => Number.isFinite(g))
: [];
const result = setUserGroups(targetUserId, ids);
if (!result.success) return Response.json({ error: result.error }, { status: 400 });
logAudit({
timestamp: Date.now(),
username: authed.username,
ip: server.requestIP(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: `Assigned ${ids.length} permission group(s) to user #${targetUserId}`,
success: true,
});
return Response.json({ ok: true, groupIds: ids });
}
}
const userExtrasMatch = url.pathname.match(/^\/api\/users\/(\d+)\/extra-permissions$/);
if (userExtrasMatch) {
const targetUserId = Number(userExtrasMatch[1]);
const user = await authenticateRequest(req);
let authed;
try {
authed = requirePermission(user, "users:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
if (!getUserById(targetUserId)) {
return Response.json({ error: "User not found" }, { status: 404 });
}
if (req.method === "GET") {
return Response.json({ permissions: Array.from(getUserExtraPermissions(targetUserId)).sort() });
}
if (req.method === "PUT") {
let body: any;
try { body = await req.json(); } catch { return Response.json({ error: "Invalid JSON" }, { status: 400 }); }
const perms = sanitizePermissions(body?.permissions);
const result = setUserExtraPermissions(targetUserId, perms);
if (!result.success) return Response.json({ error: result.error }, { status: 400 });
logAudit({
timestamp: Date.now(),
username: authed.username,
ip: server.requestIP(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: `Set ${perms.length} extra permission(s) on user #${targetUserId}`,
success: true,
});
return Response.json({ ok: true, permissions: perms });
}
}
return null;
}
@@ -4,8 +4,8 @@ import path from "path";
import AdmZip from "adm-zip";
import { v4 as uuidv4 } from "uuid";
import { authenticateRequest } from "../../auth";
import { requirePermission } from "../../rbac";
import { canUserAccessClient, canUserAccessPlugin } from "../../users";
import { requireClientAccess, requirePermission, requirePluginAccess } from "../../rbac";
import { canUserAccessPlugin } from "../../users";
import * as clientManager from "../../clientManager";
import { metrics } from "../../metrics";
import { encodeMessage, type PluginSignatureInfo } from "../../protocol";
@@ -146,8 +146,11 @@ export async function handlePluginRoutes(
if (req.method === "GET" && url.pathname === "/api/plugins/trusted-keys") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "plugins:configure");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const config = getConfig();
const allKeys = Array.from(new Set([...BUILTIN_TRUSTED_KEYS, ...config.plugins.trustedKeys]));
@@ -157,8 +160,11 @@ export async function handlePluginRoutes(
if (req.method === "POST" && url.pathname === "/api/plugins/trusted-keys") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "plugins:configure");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
try { body = await req.json(); } catch {}
@@ -179,8 +185,11 @@ export async function handlePluginRoutes(
if (req.method === "DELETE" && trustedKeyDeleteMatch) {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "plugins:configure");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const fingerprint = trustedKeyDeleteMatch[1].toLowerCase();
const config = getConfig();
@@ -203,8 +212,11 @@ export async function handlePluginRoutes(
}
const clientId = clientPluginsMatch[1];
if (!canUserAccessClient(user.userId, user.role, clientId)) {
return new Response("Forbidden: You do not have access to this client", { status: 403 });
try {
requireClientAccess(user, clientId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const loaded = deps.pluginLoadedByClient.get(clientId) || new Set<string>();
const manifests = await deps.listPluginManifests();
@@ -226,8 +238,11 @@ export async function handlePluginRoutes(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin" && user.role !== "operator") {
return new Response("Forbidden: Admin or operator access required", { status: 403 });
try {
requirePermission(user, "plugins:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let form: FormData;
@@ -321,8 +336,11 @@ export async function handlePluginRoutes(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin" && user.role !== "operator") {
return new Response("Forbidden: Admin or operator access required", { status: 403 });
try {
requirePermission(user, "plugins:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let pluginId = "";
try {
@@ -368,8 +386,11 @@ export async function handlePluginRoutes(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "plugins:configure");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let pluginId = "";
try {
@@ -460,8 +481,11 @@ export async function handlePluginRoutes(
try { requirePermission(user, "clients:control"); } catch (e) { if (e instanceof Response) return e; return new Response("Forbidden", { status: 403 }); }
let pluginId = "";
try { pluginId = deps.sanitizePluginId(pluginDataListMatch[1]); } catch { return new Response("Invalid plugin id", { status: 400 }); }
if (!canUserAccessPlugin(user.userId, user.role, pluginId)) {
return new Response("Forbidden: You do not have access to this plugin", { status: 403 });
try {
requirePluginAccess(user, pluginId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const dataDir = path.join(deps.PLUGIN_ROOT, pluginId, "data");
await fs.mkdir(dataDir, { recursive: true });
@@ -492,8 +516,11 @@ export async function handlePluginRoutes(
try { requirePermission(user, "clients:control"); } catch (e) { if (e instanceof Response) return e; return new Response("Forbidden", { status: 403 }); }
let pluginId = "";
try { pluginId = deps.sanitizePluginId(pluginDataReadMatch[1]); } catch { return new Response("Invalid plugin id", { status: 400 }); }
if (!canUserAccessPlugin(user.userId, user.role, pluginId)) {
return new Response("Forbidden: You do not have access to this plugin", { status: 403 });
try {
requirePluginAccess(user, pluginId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let relPath = pluginDataReadMatch[2];
try { relPath = decodeURIComponent(relPath); } catch { return new Response("Bad request", { status: 400 }); }
@@ -517,8 +544,11 @@ export async function handlePluginRoutes(
try { requirePermission(user, "clients:control"); } catch (e) { if (e instanceof Response) return e; return new Response("Forbidden", { status: 403 }); }
let pluginId = "";
try { pluginId = deps.sanitizePluginId(pluginDataWriteMatch[1]); } catch { return new Response("Invalid plugin id", { status: 400 }); }
if (!canUserAccessPlugin(user.userId, user.role, pluginId)) {
return new Response("Forbidden: You do not have access to this plugin", { status: 403 });
try {
requirePluginAccess(user, pluginId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let relPath = pluginDataWriteMatch[2];
try { relPath = decodeURIComponent(relPath); } catch { return new Response("Bad request", { status: 400 }); }
@@ -558,8 +588,11 @@ export async function handlePluginRoutes(
try { requirePermission(user, "clients:control"); } catch (e) { if (e instanceof Response) return e; return new Response("Forbidden", { status: 403 }); }
let pluginId = "";
try { pluginId = deps.sanitizePluginId(pluginDataDeleteMatch[1]); } catch { return new Response("Invalid plugin id", { status: 400 }); }
if (!canUserAccessPlugin(user.userId, user.role, pluginId)) {
return new Response("Forbidden: You do not have access to this plugin", { status: 403 });
try {
requirePluginAccess(user, pluginId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let relPath = pluginDataDeleteMatch[2];
try { relPath = decodeURIComponent(relPath); } catch { return new Response("Bad request", { status: 400 }); }
@@ -589,13 +622,19 @@ export async function handlePluginRoutes(
if (req.method === "POST" && pluginExecMatch) {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "plugins:configure");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let pluginId = "";
try { pluginId = deps.sanitizePluginId(pluginExecMatch[1]); } catch { return new Response("Invalid plugin id", { status: 400 }); }
if (!canUserAccessPlugin(user.userId, user.role, pluginId)) {
return new Response("Forbidden: You do not have access to this plugin", { status: 403 });
try {
requirePluginAccess(user, pluginId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
try { body = await req.json(); } catch { return new Response("Bad request", { status: 400 }); }
@@ -657,8 +696,11 @@ export async function handlePluginRoutes(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin" && user.role !== "operator") {
return new Response("Forbidden: Admin or operator access required", { status: 403 });
try {
requirePermission(user, "plugins:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let pluginId = "";
@@ -733,12 +775,18 @@ export async function handlePluginRoutes(
return new Response("Forbidden", { status: 403 });
}
const targetId = pluginLoadMatch[1];
if (!canUserAccessClient(user.userId, user.role, targetId)) {
return new Response("Forbidden: You do not have access to this client", { status: 403 });
try {
requireClientAccess(user, targetId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const pluginId = pluginLoadMatch[2];
if (!canUserAccessPlugin(user.userId, user.role, pluginId)) {
return new Response("Forbidden: You do not have access to this plugin", { status: 403 });
try {
requirePluginAccess(user, pluginId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const target = clientManager.getClient(targetId);
if (!target) return new Response("Not found", { status: 404 });
@@ -793,12 +841,18 @@ export async function handlePluginRoutes(
return new Response("Forbidden", { status: 403 });
}
const targetId = pluginEventsPollMatch[1];
if (!canUserAccessClient(user.userId, user.role, targetId)) {
return new Response("Forbidden: You do not have access to this client", { status: 403 });
try {
requireClientAccess(user, targetId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const pluginId = pluginEventsPollMatch[2];
if (!canUserAccessPlugin(user.userId, user.role, pluginId)) {
return new Response("Forbidden: You do not have access to this plugin", { status: 403 });
try {
requirePluginAccess(user, pluginId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const events = deps.drainPluginUIEvents(targetId, pluginId);
return Response.json({ events });
@@ -816,12 +870,18 @@ export async function handlePluginRoutes(
}
const targetId = pluginEventMatch[1];
if (!canUserAccessClient(user.userId, user.role, targetId)) {
return new Response("Forbidden: You do not have access to this client", { status: 403 });
try {
requireClientAccess(user, targetId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const pluginId = pluginEventMatch[2];
if (!canUserAccessPlugin(user.userId, user.role, pluginId)) {
return new Response("Forbidden: You do not have access to this plugin", { status: 403 });
try {
requirePluginAccess(user, pluginId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const target = clientManager.getClient(targetId);
if (!target) return new Response("Not found", { status: 404 });
@@ -881,12 +941,18 @@ export async function handlePluginRoutes(
}
const targetId = pluginUnloadMatch[1];
if (!canUserAccessClient(user.userId, user.role, targetId)) {
return new Response("Forbidden: You do not have access to this client", { status: 403 });
try {
requireClientAccess(user, targetId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const pluginId = pluginUnloadMatch[2];
if (!canUserAccessPlugin(user.userId, user.role, pluginId)) {
return new Response("Forbidden: You do not have access to this plugin", { status: 403 });
try {
requirePluginAccess(user, pluginId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const target = clientManager.getClient(targetId);
if (!target) return new Response("Not found", { status: 404 });
@@ -923,8 +989,11 @@ export async function handlePluginRoutes(
return new Response("Invalid plugin id", { status: 400 });
}
if (!canUserAccessPlugin(user.userId, user.role, pluginId)) {
return new Response("Forbidden: You do not have access to this plugin", { status: 403 });
try {
requirePluginAccess(user, pluginId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
if (deps.pluginState.enabled[pluginId] === false) {
@@ -976,8 +1045,11 @@ export async function handlePluginRoutes(
return new Response("Invalid plugin id", { status: 400 });
}
if (!canUserAccessPlugin(user.userId, user.role, pluginId)) {
return new Response("Forbidden: You do not have access to this plugin", { status: 403 });
try {
requirePluginAccess(user, pluginId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
if (deps.pluginState.enabled[pluginId] === false) {
@@ -1041,8 +1113,11 @@ export async function handlePluginRoutes(
return new Response("Invalid plugin id", { status: 400 });
}
if (!canUserAccessPlugin(user.userId, user.role, pluginId)) {
return new Response("Forbidden: You do not have access to this plugin", { status: 403 });
try {
requirePluginAccess(user, pluginId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const htmlFile = path.join(deps.PLUGIN_ROOT, pluginId, "assets", `${pluginId}.html`);
@@ -1085,13 +1160,21 @@ export async function handlePluginRoutes(
return new Response("Invalid plugin id", { status: 400 });
}
if (!canUserAccessPlugin(user.userId, user.role, pluginId)) {
return new Response("Forbidden: You do not have access to this plugin", { status: 403 });
try {
requirePluginAccess(user, pluginId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const clientId = url.searchParams.get("clientId") || "";
if (clientId && !canUserAccessClient(user.userId, user.role, clientId)) {
return new Response("Forbidden: Client access denied", { status: 403 });
if (clientId) {
try {
requireClientAccess(user, clientId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
}
const htmlFile = path.join(deps.PLUGIN_ROOT, pluginId, "assets", `${pluginId}.html`);
@@ -1180,8 +1263,11 @@ export async function handlePluginRoutes(
}
const [, pluginId, assetPath] = pluginAssetMatch;
if (!canUserAccessPlugin(user.userId, user.role, pluginId)) {
return new Response("Forbidden: You do not have access to this plugin", { status: 403 });
try {
requirePluginAccess(user, pluginId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let decodedPath = assetPath;
try {
@@ -2,6 +2,7 @@ import { authenticateRequest } from "../../auth";
import { AuditAction, logAudit } from "../../auditLog";
import { getConfig } from "../../config";
import { logger } from "../../logger";
import { requirePermission } from "../../rbac";
import {
registerUser,
deleteUser,
@@ -225,7 +226,12 @@ export async function handleRegistrationRoutes(
if (req.method === "GET" && url.pathname === "/api/registration/keys") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role !== "admin") return new Response("Forbidden", { status: 403 });
try {
requirePermission(user, "users:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const keys = listRegistrationKeys();
return Response.json({ keys });
@@ -234,7 +240,12 @@ export async function handleRegistrationRoutes(
if (req.method === "POST" && url.pathname === "/api/registration/keys") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role !== "admin") return new Response("Forbidden", { status: 403 });
try {
requirePermission(user, "users:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any;
try {
@@ -267,7 +278,12 @@ export async function handleRegistrationRoutes(
if (req.method === "DELETE" && url.pathname.match(/^\/api\/registration\/keys\/\d+$/)) {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role !== "admin") return new Response("Forbidden", { status: 403 });
try {
requirePermission(user, "users:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const keyId = Number(url.pathname.split("/").pop());
if (!keyId || !Number.isFinite(keyId)) {
@@ -296,7 +312,12 @@ export async function handleRegistrationRoutes(
if (req.method === "GET" && url.pathname === "/api/registration/keys/export") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role !== "admin") return new Response("Forbidden", { status: 403 });
try {
requirePermission(user, "users:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const keys = listRegistrationKeys();
const exportData = keys.map((k) => ({
@@ -319,7 +340,12 @@ export async function handleRegistrationRoutes(
if (req.method === "GET" && url.pathname === "/api/registration/pending") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role !== "admin") return new Response("Forbidden", { status: 403 });
try {
requirePermission(user, "users:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const pending = listPendingRegistrations();
return Response.json({
@@ -336,7 +362,12 @@ export async function handleRegistrationRoutes(
if (req.method === "POST" && url.pathname.match(/^\/api\/registration\/pending\/\d+\/approve$/)) {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role !== "admin") return new Response("Forbidden", { status: 403 });
try {
requirePermission(user, "users:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const pendingId = Number(url.pathname.split("/")[4]);
if (!pendingId || !Number.isFinite(pendingId)) {
@@ -371,7 +402,12 @@ export async function handleRegistrationRoutes(
if (req.method === "POST" && url.pathname.match(/^\/api\/registration\/pending\/\d+\/deny$/)) {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role !== "admin") return new Response("Forbidden", { status: 403 });
try {
requirePermission(user, "users:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const pendingId = Number(url.pathname.split("/")[4]);
if (!pendingId || !Number.isFinite(pendingId)) {
@@ -2,6 +2,7 @@ import { createHash, randomBytes, createCipheriv } from "crypto";
import { authenticateRequest } from "../../auth";
import { getConfig } from "../../config";
import { logger } from "../../logger";
import { requirePermission } from "../../rbac";
let _solana: typeof import("@solana/web3.js") | null = null;
async function getSolana() {
@@ -71,8 +72,11 @@ export async function handleSolRoutes(
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "system:configure");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
if (req.method === "POST" && url.pathname === "/api/sol/preview") {
@@ -1,6 +1,6 @@
import { randomBytes } from "node:crypto";
import { authenticateRequest } from "../../auth";
import { canUserAccessClient } from "../../users";
import { requireClientAccess } from "../../rbac";
import { logger } from "../../logger";
const MEDIAMTX_URL = (process.env.OVERLORD_MEDIAMTX_URL || "http://localhost:8889").replace(/\/+$/, "");
@@ -57,7 +57,10 @@ export async function handleWebrtcRoutes(req: Request, url: URL): Promise<Respon
} else {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (!canUserAccessClient(user.userId, user.role as any, clientId)) {
try {
requireClientAccess(user, clientId);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
}
@@ -6,6 +6,7 @@ import { AuditAction, logAudit } from "../../auditLog";
import * as clientManager from "../../clientManager";
import { metrics } from "../../metrics";
import { encodeMessage } from "../../protocol";
import { requirePermission } from "../../rbac";
import { createUploadPull } from "./file-download-routes";
type RequestIpProvider = {
@@ -39,8 +40,11 @@ export async function handleWinRERoutes(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "clients:winre");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let form: FormData;
@@ -80,8 +84,11 @@ export async function handleWinRERoutes(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "clients:winre");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
@@ -169,8 +176,11 @@ export async function handleWinRERoutes(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "clients:winre");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
@@ -232,8 +242,11 @@ export async function handleWinRERoutes(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
try {
requirePermission(user, "clients:winre");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
@@ -1,8 +1,8 @@
import { authenticateRequest } from "../../auth";
import { logger } from "../../logger";
import { isIpBanned } from "../../db";
import { canUserAccessClient, canUserAccessFeature, hasPermission } from "../../users";
import type { FeatureName } from "../../users";
import { hasPermission, requireClientAccess, requireFeatureAccess } from "../../rbac";
type RequestServer = {
requestIP: (req: Request) => { address?: string } | null | undefined;
@@ -47,11 +47,12 @@ function checkOperatorAccess(
clientId: string,
feature: FeatureName,
): Response | null {
if (!canUserAccessClient(user.userId, user.role as any, clientId)) {
return new Response("Forbidden: client access denied", { status: 403 });
}
if (!canUserAccessFeature(user.userId, user.role as any, feature)) {
return new Response("Forbidden: feature access denied", { status: 403 });
try {
requireClientAccess(user as any, clientId);
requireFeatureAccess(user as any, feature);
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
return null;
}