feat: drop the 2.8 columns, moves and aliases kept for a downgrade

This commit is contained in:
LukeGus committed 2026-10-05 10:21:11 -05:00
1 parent d3d8269cb7
commit 61f46d2517
196 files changed
+29909 -10511

No files matched your search

+10 -28
View File
@@ -34,14 +34,13 @@ export function pickResolvedPassword(
return undefined;
}
const USERNAME_PLACEHOLDER =
/\$(?:external\.username|oidc\.preferred_username)/g;
const USERNAME_PLACEHOLDER = /\$external\.username/g;
/**
* Expands `$external.username` (or its 2.8 spelling `$oidc.preferred_username`)
* in an SSH username to the name the connecting user signed in with through
* SSO or LDAP. Returns the username unchanged if it has no placeholder or the
* user has no external sign-in.
* Expands `$external.username` in an SSH username to the name the connecting
* user signed in with through SSO or LDAP: the subject of their first
* external identity. Returns the username unchanged if it has no placeholder
* or the user has no external sign-in.
*/
export async function expandExternalUsername(
username: string | undefined,
@@ -52,31 +51,14 @@ export async function expandExternalUsername(
}
try {
const { createCurrentUserRepository } =
const { createCurrentUserAuthRepository } =
await import("../database/repositories/factory.js");
const user = await createCurrentUserRepository().findById(userId);
let externalName = user?.oidcIdentifier;
const identities =
await createCurrentUserAuthRepository().listIdentitiesForUser(userId);
const externalName = [...identities].sort((a, b) => a.id - b.id)[0]
?.subject;
if (!externalName) return username;
const match = /^ldap:(\d+):(.+)$/.exec(externalName);
if (match) {
// Only strip the prefix for a real LDAP identity, to prevent spoofing
// through an SSO subject that happens to look like one.
const { createCurrentUserAuthRepository } =
await import("../database/repositories/factory.js");
const identities =
await createCurrentUserAuthRepository().listIdentitiesForUser(userId);
if (
identities.some(
(identity) =>
identity.providerId === `ldap:${match[1]}` &&
identity.subject === match[2],
)
) {
externalName = match[2];
}
}
return username.replace(USERNAME_PLACEHOLDER, () => externalName);
} catch {
return username;
+29
View File
@@ -258,6 +258,35 @@ export async function resolveForEditor(input: {
return resolveAll(catalog, levels, input.hostId ?? null);
}
/** Each folder's credential, from its auth default, keyed by folder id. */
export async function folderCredentialIds(
userId: string,
): Promise<Map<number, number>> {
const result = new Map<number, number>();
const rows = await createCurrentHostDefaultsRepository().listOwnedBy(userId);
for (const row of rows) {
if (
row.level !== "folder" ||
row.folderId == null ||
row.namespace !== CORE_NAMESPACE ||
row.key !== "auth"
) {
continue;
}
const auth = decodeDefaultValue(row.value) as {
authType?: string;
credentialId?: unknown;
} | null;
if (
auth?.authType === "credential" &&
typeof auth.credentialId === "number"
) {
result.set(row.folderId, auth.credentialId);
}
}
return result;
}
/**
* A folder's credential is its auth default: hosts in the folder that follow
* their defaults sign in with it. Null clears it.
+3 -46
View File
@@ -106,37 +106,11 @@ export async function resolveHostById(
host.jumpHosts = [];
}
}
if (typeof host.terminalConfig === "string" && host.terminalConfig) {
try {
host.terminalConfig = JSON.parse(host.terminalConfig as string);
} catch {
host.terminalConfig = undefined;
}
}
if (
host.terminalConfig &&
typeof host.terminalConfig === "object" &&
!Array.isArray(host.terminalConfig)
) {
// 2.8 editors kept the sudo password inside terminal_config. It is only
// ever handed out as sudoPassword, and only to the owner.
const { sudoPassword: legacySudo, ...rest } = host.terminalConfig as Record<
string,
unknown
>;
if (ownerEquivalent && !host.sudoPassword && legacySudo) {
host.sudoPassword = legacySudo;
}
host.terminalConfig = rest;
}
// Stored as the text "true"/"false"; the string "false" is truthy and would
// make the password provider skip password auth.
host.forceKeyboardInteractive =
String(host.forceKeyboardInteractive) === "true";
// A row the boot copy has not reached yet still has them in terminal_config.
host.sshOptions = parseSshOptions(
host.sshOptions != null ? host.sshOptions : host.terminalConfig,
);
host.sshOptions = parseSshOptions(host.sshOptions);
if (typeof host.socks5ProxyChain === "string" && host.socks5ProxyChain) {
try {
host.socks5ProxyChain = JSON.parse(host.socks5ProxyChain as string);
@@ -157,25 +131,8 @@ export async function resolveHostById(
sharedAuthResolution = await resolveRecipientSshAuth(host, hostId, userId);
if (!sharedAuthResolution) return null;
} else {
let effectiveCredentialId = host.credentialId as number | null | undefined;
if (
!effectiveCredentialId &&
host.authType === "credential" &&
host.folder
) {
try {
effectiveCredentialId = await repository.findFolderCredentialId(
ownerId,
host.folder as string,
);
} catch (e) {
sshLogger.warn("Failed to resolve folder credential for host", {
operation: "host_resolver_folder_credential",
hostId,
error: getErrorMessage(e, "Unknown"),
});
}
}
const effectiveCredentialId = host.credentialId as
number | null | undefined;
if (effectiveCredentialId) {
try {
+1 -2
View File
@@ -50,8 +50,7 @@ function optionalString(value: unknown): string | null | undefined {
/**
* The SSH options in a value (a JSON string or an object), typed and with
* anything else dropped. A terminal_config value works too, which is how the
* 2.8 keys are read.
* anything else dropped. A 2.8 client's terminalConfig works too.
*/
export function parseSshOptions(value: unknown): HostSshOptions {
const source = asObject(value);