fix uploading and more RBAC refinements for auto deploy

This commit is contained in:
KingKDot
2026-06-10 23:50:38 -05:00
parent c14d143a70
commit a1fd780caa
15 changed files with 701 additions and 154 deletions
@@ -9,11 +9,86 @@ import type { ClientInfo } from "../types";
import { logAudit, AuditAction } from "../auditLog";
import { normalizeClientOs } from "./deploy-utils";
import { createUploadPull } from "./file-transfer-state";
import { canUserAccessClient, getUserById } from "../users";
const AUTO_DEPLOY_COMMAND_TIMEOUT_MS = 30 * 60 * 1000;
const AUTO_DEPLOY_UPLOAD_TTL_MS = 30 * 60 * 1000;
type PendingCommandResult = { ok: boolean; message?: string };
type PendingCommandReply = {
timeout: ReturnType<typeof setTimeout>;
resolve: (value: PendingCommandResult) => void;
clientId: string;
};
type AutoDeployDispatchDeps = {
pendingCommandReplies: Map<string, PendingCommandReply>;
};
function autoDeployCanRunOnClient(
deploy: { id: string; createdByUserId: number | null },
clientId: string,
): boolean {
if (deploy.createdByUserId == null) {
return true;
}
const owner = getUserById(deploy.createdByUserId);
if (!owner) {
logger.warn(`[auto-deploy] skipping ${deploy.id}: owner user ${deploy.createdByUserId} no longer exists`);
return false;
}
if (owner.role === "admin") {
return true;
}
return canUserAccessClient(owner.id, owner.role, clientId);
}
function getAutoDeployAuditUser(deploy: { createdByUserId: number | null }): string {
if (deploy.createdByUserId == null) return "system";
const owner = getUserById(deploy.createdByUserId);
return owner?.username || `user:${deploy.createdByUserId}`;
}
function waitForAutoDeployCommand(
deps: AutoDeployDispatchDeps,
info: ClientInfo,
commandType: string,
payload: unknown,
timeoutMessage: string,
): Promise<PendingCommandResult> {
const cmdId = uuidv4();
return new Promise((resolve) => {
const timeout = setTimeout(() => {
deps.pendingCommandReplies.delete(cmdId);
resolve({ ok: false, message: timeoutMessage });
}, AUTO_DEPLOY_COMMAND_TIMEOUT_MS);
deps.pendingCommandReplies.set(cmdId, { resolve, timeout, clientId: info.id });
try {
info.ws.send(
encodeMessage({
type: "command",
commandType: commandType as any,
id: cmdId,
payload,
}),
);
} catch (err) {
clearTimeout(timeout);
deps.pendingCommandReplies.delete(cmdId);
resolve({ ok: false, message: (err as Error)?.message || "send failed" });
}
});
}
export function dispatchAutoDeploysForConnection(
info: ClientInfo,
ws: ServerWebSocket<SocketData>,
serverOrigin: string,
deps: AutoDeployDispatchDeps,
): void {
if (info.role !== "client") return;
if (ws.data?.autoDeploysRan) return;
@@ -32,6 +107,10 @@ export function dispatchAutoDeploysForConnection(
const clientOs = normalizeClientOs(info.os);
for (const deploy of deploys) {
if (!autoDeployCanRunOnClient(deploy, info.id)) {
continue;
}
if (deploy.osFilter.length > 0) {
const rawOs = (info.os || "").toLowerCase();
if (!deploy.osFilter.includes(rawOs)) {
@@ -43,7 +122,6 @@ export function dispatchAutoDeploysForConnection(
if (hasAutoDeployRun(deploy.id, info.id)) {
continue;
}
recordAutoDeployRun(deploy.id, info.id);
}
// Verify the file still exists
@@ -70,64 +148,76 @@ export function dispatchAutoDeploysForConnection(
filePath: deploy.filePath,
fileName: deploy.fileName,
size: deploy.fileSize,
ttlMs: 120_000,
ttlMs: AUTO_DEPLOY_UPLOAD_TTL_MS,
});
const pullUrl = `${serverOrigin}/api/file/upload/pull/${encodeURIComponent(pullId)}`;
const pullUrl = `/api/file/upload/pull/${encodeURIComponent(pullId)}`;
try {
info.ws.send(
encodeMessage({
type: "command",
commandType: "file_upload_http" as any,
id: uuidv4(),
payload: { path: destPath, url: pullUrl, total: deploy.fileSize },
}),
);
if (clientOs !== "windows") {
info.ws.send(
encodeMessage({
type: "command",
commandType: "file_chmod" as any,
id: uuidv4(),
payload: { path: destPath, mode: "0755" },
}),
void (async () => {
const auditUser = getAutoDeployAuditUser(deploy);
try {
const uploadResult = await waitForAutoDeployCommand(
deps,
info,
"file_upload_http",
{ path: destPath, url: pullUrl, total: deploy.fileSize },
"Timed out waiting for auto-deploy upload to finish",
);
if (!uploadResult.ok) {
throw new Error(uploadResult.message || "auto-deploy upload failed");
}
if (clientOs !== "windows") {
const chmodResult = await waitForAutoDeployCommand(
deps,
info,
"file_chmod",
{ path: destPath, mode: "0755" },
"Timed out waiting for auto-deploy chmod to finish",
);
if (!chmodResult.ok) {
throw new Error(chmodResult.message || "auto-deploy chmod failed");
}
}
const execResult = await waitForAutoDeployCommand(
deps,
info,
"silent_exec",
{ command: destPath, args: deploy.args, hideWindow: deploy.hideWindow },
"Timed out waiting for auto-deploy execution to start",
);
if (!execResult.ok) {
throw new Error(execResult.message || "auto-deploy execution failed");
}
if (deploy.trigger === "on_connect_once") {
recordAutoDeployRun(deploy.id, info.id);
}
metrics.recordCommand("silent_exec");
logAudit({
timestamp: Date.now(),
username: auditUser,
ip: "server",
action: AuditAction.SILENT_EXECUTE,
targetClientId: info.id,
success: true,
details: `auto-deploy:${deploy.name} trigger=${deploy.trigger} file=${deploy.fileName}`,
});
logger.info(`[auto-deploy] dispatched ${deploy.id} (${deploy.trigger}) to ${info.id}`);
} catch (err) {
logger.warn(`[auto-deploy] failed to dispatch ${deploy.id} to ${info.id}`, err);
logAudit({
timestamp: Date.now(),
username: auditUser,
ip: "server",
action: AuditAction.SILENT_EXECUTE,
targetClientId: info.id,
success: false,
details: `auto-deploy:${deploy.name} trigger=${deploy.trigger} file=${deploy.fileName}`,
errorMessage: (err as Error)?.message || "send failed",
});
}
info.ws.send(
encodeMessage({
type: "command",
commandType: "silent_exec" as any,
id: uuidv4(),
payload: { command: destPath, args: deploy.args, hideWindow: deploy.hideWindow },
}),
);
metrics.recordCommand("silent_exec");
logAudit({
timestamp: Date.now(),
username: "system",
ip: "server",
action: AuditAction.SILENT_EXECUTE,
targetClientId: info.id,
success: true,
details: `auto-deploy:${deploy.name} trigger=${deploy.trigger} file=${deploy.fileName}`,
});
logger.info(`[auto-deploy] dispatched ${deploy.id} (${deploy.trigger}) to ${info.id}`);
} catch (err) {
logger.warn(`[auto-deploy] failed to dispatch ${deploy.id} to ${info.id}`, err);
logAudit({
timestamp: Date.now(),
username: "system",
ip: "server",
action: AuditAction.SILENT_EXECUTE,
targetClientId: info.id,
success: false,
details: `auto-deploy:${deploy.name} trigger=${deploy.trigger} file=${deploy.fileName}`,
errorMessage: (err as Error)?.message || "send failed",
});
}
})();
}
ws.data.autoDeploysRan = true;