RBAC 3.0
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user