fix uploading and more RBAC refinements for auto deploy
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user