mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
feat: store rdp, vnc and telnet logins in a generic per-protocol table
This commit is contained in:
1 parent
e0c30a4e6f
commit
bba06f7ff6
107 files changed
+18750
-1224
No files matched your search
@@ -3,6 +3,11 @@ import {
|
||||
remapImportedJumpHosts,
|
||||
} from "./host-import-order.js";
|
||||
import { sshOptionsForWrite } from "../../hosts/ssh-options.js";
|
||||
import {
|
||||
keepUsableProtocolCredentials,
|
||||
readProtocolAuthPayload,
|
||||
writeProtocolAuth,
|
||||
} from "../../hosts/protocol-auth/protocol-auth.js";
|
||||
import { getErrorMessage } from "../../utils/error-message.js";
|
||||
import type { AuthenticatedRequest } from "../../../types/index.js";
|
||||
import type { Request, RequestHandler, Response, Router } from "express";
|
||||
@@ -707,13 +712,6 @@ export function registerHostBulkRoutes(
|
||||
sshDataObj.key = null;
|
||||
sshDataObj.keyPassword = null;
|
||||
sshDataObj.keyType = null;
|
||||
sshDataObj.rdpUser = hostData.rdpUser || null;
|
||||
sshDataObj.rdpPassword = hostData.rdpPassword || null;
|
||||
sshDataObj.rdpDomain = hostData.rdpDomain || null;
|
||||
sshDataObj.vncUser = hostData.vncUser || null;
|
||||
sshDataObj.vncPassword = hostData.vncPassword || null;
|
||||
sshDataObj.telnetUser = hostData.telnetUser || null;
|
||||
sshDataObj.telnetPassword = hostData.telnetPassword || null;
|
||||
} else {
|
||||
sshDataObj.password =
|
||||
hostData.authType === "password" ? hostData.password : null;
|
||||
@@ -725,7 +723,6 @@ export function registerHostBulkRoutes(
|
||||
hostData.authType === "key" ? hostData.keyPassword || null : null;
|
||||
sshDataObj.keyType =
|
||||
hostData.authType === "key" ? hostData.keyType || "auto" : null;
|
||||
sshDataObj.domain = null;
|
||||
}
|
||||
|
||||
const lookupKey = `${hostData.ip}:${hostData.port}:${hostData.username}`;
|
||||
@@ -753,6 +750,18 @@ export function registerHostBulkRoutes(
|
||||
results.success++;
|
||||
}
|
||||
|
||||
const protocolAuth = readProtocolAuthPayload(
|
||||
hostData as Record<string, unknown>,
|
||||
);
|
||||
if (protocolAuth) {
|
||||
await writeProtocolAuth(
|
||||
userId,
|
||||
savedHostId,
|
||||
await keepUsableProtocolCredentials(protocolAuth, userId),
|
||||
{ isOwner: true },
|
||||
);
|
||||
}
|
||||
|
||||
// Every enabled plugin that declares host-scope settings and
|
||||
// registered a hostImportNormalizer validates and writes its own
|
||||
// fields here, so this loop does not need to know which plugins
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { AuthOverrideProtocol } from "../../../types/auth-protocols.js";
|
||||
import {
|
||||
parseSshOptions,
|
||||
type HostSshOptions,
|
||||
} from "../../hosts/ssh-options.js";
|
||||
import { listHostProtocols } from "../../hosts/protocol-auth/registry.js";
|
||||
import { sanitizeProtocolAuthForRecipient } from "../../hosts/protocol-auth/summary.js";
|
||||
|
||||
export function isNonEmptyString(value: unknown): value is string {
|
||||
return typeof value === "string" && value.trim().length > 0;
|
||||
@@ -41,6 +42,10 @@ export function normalizeProtocolEnableFields(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The owner's SSH authentication, which a shared editor may not change. A
|
||||
* plugin protocol's login is guarded in writeProtocolAuth instead.
|
||||
*/
|
||||
export const OWNER_PRIVATE_AUTH_FIELDS = {
|
||||
ssh: [
|
||||
"authType",
|
||||
@@ -54,21 +59,7 @@ export const OWNER_PRIVATE_AUTH_FIELDS = {
|
||||
"keyType",
|
||||
"sudoPassword",
|
||||
],
|
||||
rdp: [
|
||||
"rdpAuthType",
|
||||
"rdpCredentialId",
|
||||
"rdpUser",
|
||||
"rdpPassword",
|
||||
"rdpDomain",
|
||||
],
|
||||
vnc: ["vncAuthType", "vncCredentialId", "vncUser", "vncPassword"],
|
||||
telnet: [
|
||||
"telnetAuthType",
|
||||
"telnetCredentialId",
|
||||
"telnetUser",
|
||||
"telnetPassword",
|
||||
],
|
||||
} as const satisfies Record<AuthOverrideProtocol, readonly string[]>;
|
||||
} as const;
|
||||
|
||||
export const OWNER_PRIVATE_TERMINAL_CONFIG_FIELDS = [
|
||||
"sudoPassword",
|
||||
@@ -80,7 +71,7 @@ export const OWNER_PRIVATE_SSH_OPTION_FIELDS = ["agentSocketPath"] as const;
|
||||
|
||||
export function containsOwnerPrivateAuthUpdate(
|
||||
hostData: Record<string, unknown>,
|
||||
protocol: AuthOverrideProtocol,
|
||||
protocol: keyof typeof OWNER_PRIVATE_AUTH_FIELDS,
|
||||
): boolean {
|
||||
return OWNER_PRIVATE_AUTH_FIELDS[protocol].some((field) =>
|
||||
Object.prototype.hasOwnProperty.call(hostData, field),
|
||||
@@ -193,34 +184,41 @@ export type NormalizedImportedHost = Record<string, unknown> & {
|
||||
socks5ProxyChain?: unknown;
|
||||
portKnockSequence?: unknown;
|
||||
overrideCredentialUsername?: unknown;
|
||||
domain?: unknown;
|
||||
enableSsh: boolean;
|
||||
};
|
||||
|
||||
function legacyPrefix(protocolId: string): string {
|
||||
return protocolId.replace(/-([a-z0-9])/g, (_, c: string) => c.toUpperCase());
|
||||
}
|
||||
|
||||
function legacyKey(protocolId: string): string {
|
||||
const prefix = legacyPrefix(protocolId);
|
||||
return prefix.charAt(0).toUpperCase() + prefix.slice(1);
|
||||
}
|
||||
|
||||
export function normalizeImportedHost(
|
||||
hostData: Record<string, unknown>,
|
||||
): NormalizedImportedHost {
|
||||
const credentialAlias =
|
||||
asString(hostData.credentialAlias) || asString(hostData.credentialName);
|
||||
// A row with no connectionType names its protocol the way a 2.8 export
|
||||
// did: an "enable<Protocol>" flag and a "<protocol>Port".
|
||||
const flagged = listHostProtocols().find((protocol) =>
|
||||
asBoolean(hostData[`enable${legacyKey(protocol.id)}`]),
|
||||
);
|
||||
const connectionType =
|
||||
asString(hostData.connectionType) ||
|
||||
(asBoolean(hostData.enableRdp)
|
||||
? "rdp"
|
||||
: asBoolean(hostData.enableVnc)
|
||||
? "vnc"
|
||||
: asBoolean(hostData.enableTelnet)
|
||||
? "telnet"
|
||||
: "ssh");
|
||||
asString(hostData.connectionType) || flagged?.id || "ssh";
|
||||
const protocol = listHostProtocols().find(
|
||||
(entry) => entry.id === connectionType,
|
||||
);
|
||||
|
||||
const port =
|
||||
asPort(hostData.port) ||
|
||||
(connectionType === "rdp"
|
||||
? asPort(hostData.rdpPort) || 3389
|
||||
: connectionType === "vnc"
|
||||
? asPort(hostData.vncPort) || 5900
|
||||
: connectionType === "telnet"
|
||||
? asPort(hostData.telnetPort) || 23
|
||||
: asPort(hostData.sshPort) || 22);
|
||||
(protocol
|
||||
? asPort(hostData[`${legacyPrefix(protocol.id)}Port`]) ||
|
||||
protocol.defaultPort ||
|
||||
22
|
||||
: asPort(hostData.sshPort) || 22);
|
||||
|
||||
return {
|
||||
...hostData,
|
||||
@@ -260,9 +258,6 @@ const SENSITIVE_FIELDS = [
|
||||
"password",
|
||||
"sudoPassword",
|
||||
"socks5Password",
|
||||
"rdpPassword",
|
||||
"vncPassword",
|
||||
"telnetPassword",
|
||||
"autostartPassword",
|
||||
];
|
||||
|
||||
@@ -282,9 +277,6 @@ export function stripSensitiveFields(
|
||||
// 2.8 editors kept the sudo password inside terminal_config.
|
||||
result.hasSudoPassword =
|
||||
!!host.sudoPassword || !!terminalConfigForSudo?.sudoPassword;
|
||||
result.hasRdpPassword = !!host.rdpPassword;
|
||||
result.hasVncPassword = !!host.vncPassword;
|
||||
result.hasTelnetPassword = !!host.telnetPassword;
|
||||
for (const field of SENSITIVE_FIELDS) {
|
||||
delete result[field];
|
||||
}
|
||||
@@ -326,7 +318,7 @@ const CONNECT_LEVEL_FIELDS = new Set([
|
||||
"statusCheckInterval",
|
||||
"enableSsh",
|
||||
"sshPort",
|
||||
"rdpAuthType",
|
||||
"protocolAuth",
|
||||
"jumpHosts",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
@@ -368,6 +360,12 @@ export function sanitizeHostForRecipient(
|
||||
!Array.isArray(authOverrides.ssh)
|
||||
? (authOverrides.ssh as Record<string, unknown>)
|
||||
: undefined;
|
||||
if (stripped.protocolAuth !== undefined) {
|
||||
stripped.protocolAuth = sanitizeProtocolAuthForRecipient(
|
||||
stripped.protocolAuth,
|
||||
permissionLevel,
|
||||
);
|
||||
}
|
||||
if (!sshOverride?.credentialId) {
|
||||
stripped.hasPassword = false;
|
||||
stripped.hasKey = false;
|
||||
@@ -468,10 +466,6 @@ export function transformHostResponse(
|
||||
shareSshAuth: !!host.shareSshAuth,
|
||||
enableSsh: !!host.enableSsh,
|
||||
sshPort: host.sshPort ?? host.port ?? 22,
|
||||
rdpUser: host.rdpUser || undefined,
|
||||
rdpDomain: host.rdpDomain || undefined,
|
||||
vncUser: host.vncUser || undefined,
|
||||
telnetUser: host.telnetUser || undefined,
|
||||
jumpHosts: host.jumpHosts ? JSON.parse(host.jumpHosts as string) : [],
|
||||
// Moved into a plugin's host settings; its hostPayloadLegacy puts it
|
||||
// back for 2.8 clients.
|
||||
@@ -490,6 +484,5 @@ export function transformHostResponse(
|
||||
portKnockSequence: host.portKnockSequence
|
||||
? JSON.parse(host.portKnockSequence as string)
|
||||
: [],
|
||||
domain: host.domain || undefined,
|
||||
};
|
||||
}
|
||||
+139
-146
@@ -71,6 +71,24 @@ import {
|
||||
} from "../../utils/shared-host-auth-resolver.js";
|
||||
import { rejectSharedCopyWrites } from "../../sync/shared-copy-guard.js";
|
||||
import { sshOptionsForWrite } from "../../hosts/ssh-options.js";
|
||||
import {
|
||||
applyProtocolAuthPlan,
|
||||
attachProtocolAuth,
|
||||
firstProtocolUsername,
|
||||
listProtocolLogins,
|
||||
loadProtocolAuthSummaries,
|
||||
planProtocolAuthWrite,
|
||||
ProtocolAuthWriteError,
|
||||
readProtocolAuthPayload,
|
||||
toPortableLogins,
|
||||
withProtocolAuth,
|
||||
writeProtocolAuth,
|
||||
type PlannedProtocolAuth,
|
||||
} from "../../hosts/protocol-auth/protocol-auth.js";
|
||||
import {
|
||||
findHostProtocol,
|
||||
listHostProtocols,
|
||||
} from "../../hosts/protocol-auth/registry.js";
|
||||
import {
|
||||
mergeStoredTerminalFields,
|
||||
parseTerminalConfig,
|
||||
@@ -199,7 +217,6 @@ router.post(
|
||||
terminalConfig,
|
||||
sshOptions,
|
||||
forceKeyboardInteractive,
|
||||
domain,
|
||||
notes,
|
||||
useSocks5,
|
||||
socks5Host,
|
||||
@@ -213,20 +230,8 @@ router.post(
|
||||
overrideCredentialUsername,
|
||||
enableSsh,
|
||||
sshPort,
|
||||
rdpAuthType,
|
||||
rdpCredentialId,
|
||||
rdpUser,
|
||||
rdpPassword,
|
||||
rdpDomain,
|
||||
vncAuthType,
|
||||
vncCredentialId,
|
||||
vncPassword,
|
||||
vncUser,
|
||||
telnetAuthType,
|
||||
telnetCredentialId,
|
||||
telnetUser,
|
||||
telnetPassword,
|
||||
} = hostData;
|
||||
const protocolAuthPatch = readProtocolAuthPayload(hostData);
|
||||
databaseLogger.info("Creating SSH host", {
|
||||
operation: "host_create",
|
||||
userId,
|
||||
@@ -274,7 +279,7 @@ router.post(
|
||||
authMethod ||
|
||||
(effectiveConnectionType !== "ssh" ? "password" : undefined);
|
||||
const effectiveUsername =
|
||||
username || rdpUser || vncUser || telnetUser || "";
|
||||
username || firstProtocolUsername(protocolAuthPatch);
|
||||
const effectiveName =
|
||||
name || (effectiveUsername ? `${effectiveUsername}@${ip}` : String(ip));
|
||||
const sshDataObj: Record<string, unknown> = {
|
||||
@@ -304,7 +309,6 @@ router.post(
|
||||
: null,
|
||||
sshOptions: sshOptionsForWrite({ sshOptions, terminalConfig }) ?? null,
|
||||
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
||||
domain: domain || null,
|
||||
notes: notes || null,
|
||||
sudoPassword: sudoPassword || null,
|
||||
useSocks5: useSocks5 ? 1 : 0,
|
||||
@@ -325,28 +329,9 @@ router.post(
|
||||
: null,
|
||||
...normalizeProtocolEnableFields(hostData),
|
||||
sshPort: sshPort || port || 22,
|
||||
rdpAuthType: rdpAuthType || null,
|
||||
rdpCredentialId:
|
||||
rdpAuthType === "credential" && rdpCredentialId
|
||||
? rdpCredentialId
|
||||
: null,
|
||||
rdpUser: rdpUser || null,
|
||||
rdpDomain: rdpDomain || null,
|
||||
vncAuthType: vncAuthType || null,
|
||||
vncCredentialId:
|
||||
vncAuthType === "credential" && vncCredentialId
|
||||
? vncCredentialId
|
||||
: null,
|
||||
vncUser: vncUser || null,
|
||||
telnetAuthType: telnetAuthType || null,
|
||||
telnetCredentialId:
|
||||
telnetAuthType === "credential" && telnetCredentialId
|
||||
? telnetCredentialId
|
||||
: null,
|
||||
telnetUser: telnetUser || null,
|
||||
};
|
||||
|
||||
// For non-SSH hosts (RDP, VNC, Telnet), always save password if provided
|
||||
// A host whose main protocol is a plugin's keeps any password it is given.
|
||||
if (effectiveConnectionType !== "ssh") {
|
||||
sshDataObj.password = password || null;
|
||||
sshDataObj.key = null;
|
||||
@@ -399,10 +384,6 @@ router.post(
|
||||
sshDataObj.keyType = null;
|
||||
}
|
||||
|
||||
sshDataObj.rdpPassword = rdpPassword || null;
|
||||
sshDataObj.vncPassword = vncPassword || null;
|
||||
sshDataObj.telnetPassword = telnetPassword || null;
|
||||
|
||||
try {
|
||||
const result = await createCurrentHostRepository().createEncryptedForUser(
|
||||
userId,
|
||||
@@ -421,6 +402,14 @@ router.post(
|
||||
}
|
||||
|
||||
const createdHost = result;
|
||||
if (protocolAuthPatch) {
|
||||
await writeProtocolAuth(
|
||||
userId,
|
||||
createdHost.id as number,
|
||||
protocolAuthPatch,
|
||||
{ isOwner: true },
|
||||
);
|
||||
}
|
||||
|
||||
// Standing folder shares apply to the newcomer.
|
||||
try {
|
||||
@@ -464,7 +453,7 @@ router.post(
|
||||
name: String(name ?? ip),
|
||||
});
|
||||
|
||||
res.json(stripSensitiveFields(resolvedHost));
|
||||
res.json(await withProtocolAuth(stripSensitiveFields(resolvedHost)));
|
||||
notifyStatsHostUpdated(createdHost.id as number, userId, "host_create");
|
||||
} catch (err) {
|
||||
sshLogger.error("Failed to save SSH host to database", err, {
|
||||
@@ -794,7 +783,6 @@ router.put(
|
||||
terminalConfig,
|
||||
sshOptions,
|
||||
forceKeyboardInteractive,
|
||||
domain,
|
||||
notes,
|
||||
useSocks5,
|
||||
socks5Host,
|
||||
@@ -808,20 +796,8 @@ router.put(
|
||||
overrideCredentialUsername,
|
||||
enableSsh,
|
||||
sshPort,
|
||||
rdpAuthType,
|
||||
rdpCredentialId,
|
||||
rdpUser,
|
||||
rdpPassword,
|
||||
rdpDomain,
|
||||
vncAuthType,
|
||||
vncCredentialId,
|
||||
vncPassword,
|
||||
vncUser,
|
||||
telnetAuthType,
|
||||
telnetCredentialId,
|
||||
telnetUser,
|
||||
telnetPassword,
|
||||
} = hostData;
|
||||
const protocolAuthPatch = readProtocolAuthPayload(hostData);
|
||||
databaseLogger.info("Updating SSH host", {
|
||||
operation: "host_update",
|
||||
userId,
|
||||
@@ -871,7 +847,7 @@ router.put(
|
||||
|
||||
const effectiveAuthType = authType || authMethod;
|
||||
const effectiveUsername =
|
||||
username || rdpUser || vncUser || telnetUser || "";
|
||||
username || firstProtocolUsername(protocolAuthPatch);
|
||||
const effectiveName =
|
||||
name || (effectiveUsername ? `${effectiveUsername}@${ip}` : String(ip));
|
||||
const sshDataObj: Record<string, unknown> = {
|
||||
@@ -899,7 +875,6 @@ router.put(
|
||||
: JSON.stringify(terminalConfig)
|
||||
: null,
|
||||
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
||||
domain: domain || null,
|
||||
notes: notes || null,
|
||||
useSocks5: useSocks5 ? 1 : 0,
|
||||
socks5Host: socks5Host || null,
|
||||
@@ -919,25 +894,6 @@ router.put(
|
||||
: null,
|
||||
...normalizeProtocolEnableFields(hostData),
|
||||
sshPort: sshPort || port || 22,
|
||||
rdpAuthType: rdpAuthType || null,
|
||||
rdpCredentialId:
|
||||
rdpAuthType === "credential" && rdpCredentialId
|
||||
? rdpCredentialId
|
||||
: null,
|
||||
rdpUser: rdpUser || null,
|
||||
rdpDomain: rdpDomain || null,
|
||||
vncAuthType: vncAuthType || null,
|
||||
vncCredentialId:
|
||||
vncAuthType === "credential" && vncCredentialId
|
||||
? vncCredentialId
|
||||
: null,
|
||||
vncUser: vncUser || null,
|
||||
telnetAuthType: telnetAuthType || null,
|
||||
telnetCredentialId:
|
||||
telnetAuthType === "credential" && telnetCredentialId
|
||||
? telnetCredentialId
|
||||
: null,
|
||||
telnetUser: telnetUser || null,
|
||||
};
|
||||
|
||||
const nextSshOptions = sshOptionsForWrite({ sshOptions, terminalConfig });
|
||||
@@ -947,7 +903,7 @@ router.put(
|
||||
sshDataObj.sudoPassword = sudoPassword || null;
|
||||
}
|
||||
|
||||
// For non-SSH hosts (RDP, VNC, Telnet), always save password if provided
|
||||
// A host whose main protocol is a plugin's keeps any password it is given.
|
||||
if ((connectionType || "ssh") !== "ssh") {
|
||||
if (password) {
|
||||
sshDataObj.password = password;
|
||||
@@ -1007,10 +963,6 @@ router.put(
|
||||
sshDataObj.keyType = null;
|
||||
}
|
||||
|
||||
if (rdpPassword) sshDataObj.rdpPassword = rdpPassword;
|
||||
if (vncPassword) sshDataObj.vncPassword = vncPassword;
|
||||
if (telnetPassword) sshDataObj.telnetPassword = telnetPassword;
|
||||
|
||||
if (validatedParentHostId !== undefined) {
|
||||
sshDataObj.parentHostId = validatedParentHostId;
|
||||
} else if (folder !== undefined) {
|
||||
@@ -1087,37 +1039,28 @@ router.put(
|
||||
});
|
||||
}
|
||||
|
||||
const referenceViolations: Array<[unknown, number | null, string]> = [
|
||||
[
|
||||
hostData.rdpCredentialId,
|
||||
hostRecord.rdpCredentialId,
|
||||
"RDP credential",
|
||||
],
|
||||
[
|
||||
hostData.vncCredentialId,
|
||||
hostRecord.vncCredentialId,
|
||||
"VNC credential",
|
||||
],
|
||||
[
|
||||
hostData.telnetCredentialId,
|
||||
hostRecord.telnetCredentialId,
|
||||
"Telnet credential",
|
||||
],
|
||||
];
|
||||
|
||||
for (const [incoming, current, label] of referenceViolations) {
|
||||
if (incoming !== undefined && (incoming ?? null) !== current) {
|
||||
return res.status(403).json({
|
||||
error: `Only the host owner can change the ${label}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const field of OWNER_PRIVATE_AUTH_FIELDS.ssh) {
|
||||
delete sshDataObj[field];
|
||||
}
|
||||
}
|
||||
|
||||
let protocolAuthPlan: PlannedProtocolAuth | null = null;
|
||||
if (protocolAuthPatch) {
|
||||
try {
|
||||
protocolAuthPlan = await planProtocolAuthWrite(
|
||||
ownerId,
|
||||
Number(hostId),
|
||||
protocolAuthPatch,
|
||||
{ isOwner: accessInfo.isOwner },
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof ProtocolAuthWriteError) {
|
||||
return res.status(error.status).json({ error: error.message });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const terminalFieldsError = await mergeStoredTerminalFields(
|
||||
sshDataObj,
|
||||
hostData,
|
||||
@@ -1134,6 +1077,9 @@ router.put(
|
||||
Number(hostId),
|
||||
sshDataObj,
|
||||
);
|
||||
if (protocolAuthPlan) {
|
||||
await applyProtocolAuthPlan(ownerId, Number(hostId), protocolAuthPlan);
|
||||
}
|
||||
|
||||
// A host that moved into a folder inherits that folder's standing shares.
|
||||
try {
|
||||
@@ -1202,7 +1148,7 @@ router.put(
|
||||
success: true,
|
||||
});
|
||||
|
||||
res.json(stripSensitiveFields(resolvedHost));
|
||||
res.json(await withProtocolAuth(stripSensitiveFields(resolvedHost)));
|
||||
notifyStatsHostUpdated(parseInt(hostId), userId, "host_update");
|
||||
} catch (err) {
|
||||
sshLogger.error("Failed to update SSH host in database", err, {
|
||||
@@ -1338,6 +1284,7 @@ router.get(
|
||||
}),
|
||||
);
|
||||
|
||||
attachProtocolAuth(result, await loadProtocolAuthSummaries(result));
|
||||
const sanitized = result.map((host) =>
|
||||
host.isShared
|
||||
? sanitizeHostForRecipient(
|
||||
@@ -1425,7 +1372,9 @@ router.get(
|
||||
(await resolveHostCredentials(result, userId)) || result;
|
||||
|
||||
return res.json(
|
||||
await withHostPluginSettings(stripSensitiveFields(resolved)),
|
||||
await withHostPluginSettings(
|
||||
await withProtocolAuth(stripSensitiveFields(resolved)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1473,8 +1422,9 @@ router.get(
|
||||
sharedExpiresAt: accessInfo.expiresAt || undefined,
|
||||
ownerUsername,
|
||||
};
|
||||
const resolvedSharedResult =
|
||||
(await resolveHostCredentials(sharedResult, userId)) || sharedResult;
|
||||
const resolvedSharedResult = await withProtocolAuth(
|
||||
(await resolveHostCredentials(sharedResult, userId)) || sharedResult,
|
||||
);
|
||||
|
||||
res.json(
|
||||
await withHostPluginSettings(
|
||||
@@ -1585,7 +1535,12 @@ router.get(
|
||||
* name: field
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [password, sudoPassword, rdpPassword, vncPassword, telnetPassword, key, keyPassword]
|
||||
* enum: [password, sudoPassword, key, keyPassword]
|
||||
* - in: query
|
||||
* name: protocol
|
||||
* schema:
|
||||
* type: string
|
||||
* description: A plugin protocol id; returns that protocol's saved password instead of field.
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The requested password value.
|
||||
@@ -1600,18 +1555,22 @@ router.get(
|
||||
const hostId = Number(req.params.id);
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const field = (req.query.field as string) || "password";
|
||||
const coreField = [
|
||||
"password",
|
||||
"sudoPassword",
|
||||
"key",
|
||||
"keyPassword",
|
||||
].includes(field);
|
||||
// 2.8 clients ask for "<protocol>Password".
|
||||
const protocol =
|
||||
typeof req.query.protocol === "string"
|
||||
? req.query.protocol
|
||||
: coreField
|
||||
? undefined
|
||||
: /^([a-z][a-zA-Z0-9]*)Password$/.exec(field)?.[1];
|
||||
const protocolId = protocol ? findHostProtocol(protocol)?.id : undefined;
|
||||
|
||||
if (
|
||||
![
|
||||
"password",
|
||||
"sudoPassword",
|
||||
"rdpPassword",
|
||||
"vncPassword",
|
||||
"telnetPassword",
|
||||
"key",
|
||||
"keyPassword",
|
||||
].includes(field)
|
||||
) {
|
||||
if (!protocolId && (protocol || !coreField)) {
|
||||
return res.status(400).json({ error: "Invalid field" });
|
||||
}
|
||||
|
||||
@@ -1626,6 +1585,16 @@ router.get(
|
||||
return res.status(404).json({ error: "Host not found" });
|
||||
}
|
||||
|
||||
if (protocolId) {
|
||||
const login = (await listProtocolLogins(hostId, userId)).find(
|
||||
(entry) => entry.protocol === protocolId,
|
||||
);
|
||||
if (!login?.password) {
|
||||
return res.status(404).json({ error: "No password set" });
|
||||
}
|
||||
return res.json({ value: login.password });
|
||||
}
|
||||
|
||||
const resolved = (await resolveHostCredentials(host, userId)) || host;
|
||||
let value = resolved[field];
|
||||
|
||||
@@ -1724,25 +1693,14 @@ router.get(
|
||||
notes: resolvedHost.notes || null,
|
||||
// Every plugin's host settings, secrets redacted, for import to hand back.
|
||||
pluginSettings: hostPluginSettings ?? {},
|
||||
// Each plugin protocol's login, secrets included like the SSH ones.
|
||||
protocolAuth: toPortableLogins(
|
||||
await listProtocolLogins(Number(hostId), userId),
|
||||
),
|
||||
};
|
||||
|
||||
const exportData = isRemoteDesktop
|
||||
? {
|
||||
...baseExportData,
|
||||
rdpAuthType: resolvedHost.rdpAuthType || null,
|
||||
rdpCredentialId: resolvedHost.rdpCredentialId || null,
|
||||
rdpUser: resolvedHost.rdpUser || null,
|
||||
rdpPassword: resolvedHost.rdpPassword || null,
|
||||
rdpDomain: resolvedHost.rdpDomain || null,
|
||||
vncAuthType: resolvedHost.vncAuthType || null,
|
||||
vncCredentialId: resolvedHost.vncCredentialId || null,
|
||||
vncUser: resolvedHost.vncUser || null,
|
||||
vncPassword: resolvedHost.vncPassword || null,
|
||||
telnetAuthType: resolvedHost.telnetAuthType || null,
|
||||
telnetCredentialId: resolvedHost.telnetCredentialId || null,
|
||||
telnetUser: resolvedHost.telnetUser || null,
|
||||
telnetPassword: resolvedHost.telnetPassword || null,
|
||||
}
|
||||
? baseExportData
|
||||
: {
|
||||
...baseExportData,
|
||||
authType: resolvedHost.authType,
|
||||
@@ -1867,13 +1825,16 @@ router.get(
|
||||
notes: resolvedHost.notes || null,
|
||||
// Every plugin's host settings, secrets redacted, for import to hand back.
|
||||
pluginSettings: hostPluginSettings ?? {},
|
||||
protocolAuth: shareableLogins(
|
||||
toPortableLogins(
|
||||
await listProtocolLogins(host.id as number, userId),
|
||||
),
|
||||
shareMode,
|
||||
),
|
||||
};
|
||||
|
||||
const exportData = isRemoteDesktop
|
||||
? {
|
||||
...baseExportData,
|
||||
domain: resolvedHost.domain || null,
|
||||
}
|
||||
? baseExportData
|
||||
: {
|
||||
...baseExportData,
|
||||
authType: resolvedHost.authType,
|
||||
@@ -2091,6 +2052,38 @@ router.delete(
|
||||
// the file-manager plugin, under /plugin-api/file-manager/, and command
|
||||
// history to the ssh-terminal plugin, under /plugin-api/ssh-terminal/.
|
||||
|
||||
/**
|
||||
* A share export leaves out every secret and credential link, the same as
|
||||
* it does for SSH.
|
||||
*/
|
||||
function shareableLogins(
|
||||
logins: ReturnType<typeof toPortableLogins>,
|
||||
shareMode: boolean,
|
||||
): ReturnType<typeof toPortableLogins> {
|
||||
if (!shareMode) return logins;
|
||||
const out: ReturnType<typeof toPortableLogins> = {};
|
||||
for (const [protocol, login] of Object.entries(logins)) {
|
||||
const declared = findHostProtocol(protocol);
|
||||
const secret = new Set(
|
||||
(declared?.credentialFields ?? [])
|
||||
.filter((field) => field.secret)
|
||||
.map((field) => field.key),
|
||||
);
|
||||
out[protocol] = {
|
||||
authType: login.authType === "credential" ? "direct" : login.authType,
|
||||
credentialId: null,
|
||||
username: login.username,
|
||||
password: null,
|
||||
fields: Object.fromEntries(
|
||||
Object.entries(login.fields).filter(
|
||||
([key]) => declared && !secret.has(key),
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function resolveHostCredentials(
|
||||
host: Record<string, unknown>,
|
||||
requestingUserId?: string,
|
||||
@@ -2128,9 +2121,9 @@ async function resolveHostCredentials(
|
||||
host.id,
|
||||
requestingUserId,
|
||||
);
|
||||
// Whether a protocol is on is the remote-desktop plugin's host
|
||||
// setting; the client only offers an override for one that is.
|
||||
for (const protocol of ["rdp", "vnc", "telnet"] as const) {
|
||||
// Whether a plugin protocol is on is its plugin's host setting; the
|
||||
// client only offers an override for one that is.
|
||||
for (const { id: protocol } of listHostProtocols()) {
|
||||
authOverrides[protocol] = {
|
||||
credentialId: overrideCredentialIds[protocol],
|
||||
required: false,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
getAuditUsername,
|
||||
logAudit,
|
||||
} from "../../utils/audit-logger.js";
|
||||
import { isAuthOverrideProtocol } from "../../../types/auth-protocols.js";
|
||||
import { isAuthOverrideProtocol } from "../../hosts/protocol-auth/registry.js";
|
||||
import {
|
||||
SharedHostAuthOverrideService,
|
||||
SharedHostAuthOverrideServiceError,
|
||||
@@ -1945,7 +1945,7 @@ router.delete(
|
||||
* /rbac/host-access/{hostId}/auth/{protocol}:
|
||||
* put:
|
||||
* summary: Set personal authentication for a shared host protocol
|
||||
* description: Selects one of the authenticated recipient's own credentials, or clears the selection with null. Only SSH is currently supported.
|
||||
* description: Selects one of the authenticated recipient's own credentials, or clears the selection with null. The protocol is "ssh" or one a plugin declares.
|
||||
* tags: [RBAC]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
|
||||
@@ -346,7 +346,7 @@ export function registerUserSettingsRoutes(
|
||||
* /users/session-sharing-enabled:
|
||||
* get:
|
||||
* summary: Get session sharing globally enabled setting
|
||||
* description: Returns whether live session sharing (terminal/RDP/VNC/Telnet share links and in-app joins) is allowed instance-wide. Overrides every per-host toggle when false.
|
||||
* description: Returns whether live session sharing (terminal and remote desktop share links and in-app joins) is allowed instance-wide. Overrides every per-host toggle when false.
|
||||
* tags:
|
||||
* - Users
|
||||
* responses:
|
||||
|
||||
Reference in new issue
Block a user