mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: add auth extension points and one ssh connect pipeline
This commit is contained in:
1 parent
d3c209af13
commit
d774df8b41
159 files changed
+43243
-7451
No files matched your search
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Moves sign-in identities and 2FA enrolment into their own tables.
|
||||
*
|
||||
* users.oidc_identifier packed provider and subject into one string:
|
||||
* "ldap:<provider>:<id>", "github:<provider>:<id>", or a bare OIDC subject
|
||||
* whose provider was users.sso_provider_id. Each becomes a
|
||||
* user_external_identities row. users.totp_enabled becomes a
|
||||
* user_second_factors row for core's TOTP factor, so a login can tell a user
|
||||
* has a factor even after the plugin that runs it is gone.
|
||||
*
|
||||
* Lossless: the old column and flag are left as they are. Idempotent: rows
|
||||
* that already exist are skipped, so it runs on every boot.
|
||||
*/
|
||||
|
||||
import { databaseLogger } from "../logger.js";
|
||||
import {
|
||||
createCurrentUserAuthRepository,
|
||||
createCurrentUserRepository,
|
||||
} from "../../database/repositories/factory.js";
|
||||
|
||||
/** Matches LEGACY_OIDC_PROVIDER_ID in auth/legacy/oidc-login.ts. */
|
||||
const LEGACY_OIDC_PROVIDER_ID = "legacy-oidc";
|
||||
|
||||
export interface ExternalIdentityMigrationResult {
|
||||
identities: number;
|
||||
factors: number;
|
||||
skipped: number;
|
||||
}
|
||||
|
||||
/** Splits a stored identifier into provider and subject. */
|
||||
export function parseLegacyIdentifier(
|
||||
identifier: string,
|
||||
ssoProviderId: number | null | undefined,
|
||||
): { providerId: string; subject: string } | null {
|
||||
if (!identifier) return null;
|
||||
|
||||
const prefixed = /^(ldap|github):([^:]+):(.+)$/.exec(identifier);
|
||||
if (prefixed) {
|
||||
const provider = prefixed[2];
|
||||
return {
|
||||
providerId:
|
||||
provider === "null" || provider === "undefined"
|
||||
? LEGACY_OIDC_PROVIDER_ID
|
||||
: provider,
|
||||
subject: prefixed[3],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
providerId:
|
||||
ssoProviderId !== null && ssoProviderId !== undefined
|
||||
? String(ssoProviderId)
|
||||
: LEGACY_OIDC_PROVIDER_ID,
|
||||
subject: identifier,
|
||||
};
|
||||
}
|
||||
|
||||
export async function runExternalIdentityMigration(): Promise<ExternalIdentityMigrationResult> {
|
||||
const result: ExternalIdentityMigrationResult = {
|
||||
identities: 0,
|
||||
factors: 0,
|
||||
skipped: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
const users = await createCurrentUserRepository().listAll();
|
||||
const auth = createCurrentUserAuthRepository();
|
||||
|
||||
for (const user of users) {
|
||||
if (user.oidcIdentifier) {
|
||||
const parsed = parseLegacyIdentifier(
|
||||
user.oidcIdentifier,
|
||||
user.ssoProviderId,
|
||||
);
|
||||
if (parsed) {
|
||||
const existing = await auth.findIdentity(
|
||||
parsed.providerId,
|
||||
parsed.subject,
|
||||
);
|
||||
if (existing) {
|
||||
result.skipped++;
|
||||
} else {
|
||||
await auth.linkIdentity({
|
||||
userId: user.id,
|
||||
providerId: parsed.providerId,
|
||||
subject: parsed.subject,
|
||||
});
|
||||
result.identities++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (user.totpEnabled) {
|
||||
const enrolled = await auth.listSecondFactors(user.id);
|
||||
if (
|
||||
enrolled.some(
|
||||
(row) => row.pluginId === "core" && row.factorId === "totp",
|
||||
)
|
||||
) {
|
||||
result.skipped++;
|
||||
} else {
|
||||
await auth.recordSecondFactor(user.id, "core", "totp");
|
||||
result.factors++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.identities > 0 || result.factors > 0) {
|
||||
databaseLogger.info("Moved sign-in identities and 2FA enrolment", {
|
||||
operation: "external_identity_migration",
|
||||
...result,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
databaseLogger.error("External identity migration failed", error, {
|
||||
operation: "external_identity_migration",
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import type {
|
||||
HostResolutionCredentialRecord,
|
||||
HostResolutionHostRecord,
|
||||
} from "../database/repositories/host-resolution-repository.js";
|
||||
import { getSshAuthProvider } from "../hosts/connect/auth-provider-registry.js";
|
||||
import { ensureCoreSshAuthProviders } from "../hosts/connect/core-providers.js";
|
||||
import {
|
||||
SharedHostSecretsManager,
|
||||
type SharedSecretData,
|
||||
@@ -38,13 +40,9 @@ export function requiresPersonalHostAuthentication(
|
||||
// Owner auth for RDP/VNC/Telnet is snapshotted for every recipient, so only
|
||||
// SSH, which sits behind shareSshAuth, can leave a recipient without auth.
|
||||
if (protocol !== "ssh") return false;
|
||||
return (
|
||||
!!host.credentialId ||
|
||||
host.authType === "password" ||
|
||||
host.authType === "key" ||
|
||||
host.authType === "credential" ||
|
||||
host.authType === "agent"
|
||||
);
|
||||
if (host.credentialId) return true;
|
||||
ensureCoreSshAuthProviders();
|
||||
return !!getSshAuthProvider(host.authType ?? "none")?.requiresSecret;
|
||||
}
|
||||
|
||||
/** Whether the owner's auth for this protocol is available to recipients. */
|
||||
|
||||
Reference in new issue
Block a user