import type { AuthOverrideProtocol } from "../../types/auth-protocols.js"; import { isAuthOverrideProtocol } from "../hosts/protocol-auth/registry.js"; import { createCurrentHostResolutionRepository, createCurrentRbacAccessRepository, createCurrentRoleRepository, createCurrentSharedFolderAuthOverrideRepository, createCurrentSharedHostAuthOverrideRepository, } from "../database/repositories/factory.js"; 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 { findUsableCredential } from "../hosts/usable-credential.js"; import { SharedHostSecretsManager, type SharedSecretData, } from "./shared-host-secrets-manager.js"; export type RecipientSharedHostAuthResolution = | { source: "personal-override"; credentialId: number; credential: HostResolutionCredentialRecord; /** Set when the login comes from the folder, not the host itself. */ folder?: string; } | { source: "owner-shared"; authType: string; secret: SharedSecretData | null; } | { source: "secretless" } | { source: "required" }; export function requiresPersonalHostAuthentication( host: Pick, protocol: AuthOverrideProtocol, ): boolean { // Owner auth for a plugin protocol is snapshotted for every recipient, so // only SSH, which sits behind shareSshAuth, can leave one without auth. if (protocol !== "ssh") return false; if (host.credentialId) return true; ensureCoreSshAuthProviders(); return !!getSshAuthProvider(host.authType ?? "none")?.requiresSecret; } /** Whether the owner's auth for this protocol is available to recipients. */ function isOwnerAuthShared( host: Pick, protocol: AuthOverrideProtocol, ): boolean { return protocol === "ssh" ? !!host.shareSshAuth : true; } /** * Applies the shared-host authentication precedence independently from any * transport: recipient override on the host, then on its folder, shared * owner auth, secretless auth, then "required". */ export async function resolveRecipientSharedHostAuthentication( host: HostResolutionHostRecord, hostId: number, userId: string, protocol: AuthOverrideProtocol, ): Promise { if (!isAuthOverrideProtocol(protocol)) { throw new Error(`No plugin declares the ${String(protocol)} protocol`); } const repository = createCurrentHostResolutionRepository(); let overrideCredentialId: number | null = null; try { overrideCredentialId = await createCurrentSharedHostAuthOverrideRepository().findCredentialId( hostId, userId, protocol, ); } catch { // A missing/deleted override behaves like no personal credential. } if (overrideCredentialId) { const credential = await findUsableCredential(overrideCredentialId, userId); if (credential) { return { source: "personal-override", credentialId: overrideCredentialId, credential, }; } } try { const folderOverride = await createCurrentSharedFolderAuthOverrideRepository().findClosest( userId, host.folder, protocol, ); if (folderOverride) { const credential = await findUsableCredential( folderOverride.credentialId, userId, ); if (credential) { return { source: "personal-override", credentialId: folderOverride.credentialId, credential, folder: folderOverride.folder, }; } } } catch { // Same as a host override: an unreadable one counts as none. } if (isOwnerAuthShared(host, protocol)) { if (protocol === "ssh" && host.authType === "agent") { return { source: "owner-shared", authType: "agent", secret: null, }; } try { const secretsManager = SharedHostSecretsManager.getInstance(); let secret = await secretsManager.getSecretForUser( hostId, userId, protocol, ); if (!secret) { // No snapshot yet -- most often the grant was created while this // recipient's data key was unavailable (snapshotForUser silently // skips in that case). Mirrors the self-heal in findUsableCredential. const hostAccessId = await findActiveHostAccessId(hostId, userId); if (hostAccessId !== null) { const ownerId = await repository.findHostOwnerId(hostId); if (ownerId) { await secretsManager.snapshotForUser( hostAccessId, hostId, userId, ownerId, ); secret = await secretsManager.getSecretForUser( hostId, userId, protocol, ); } } } if (secret) { return { source: "owner-shared", authType: secret.authType, secret, }; } } catch { // An unreadable owner snapshot cannot expose the owner's auth. } } if (protocol === "ssh" && host.credentialId) { const credential = await findUsableCredential(host.credentialId, userId); if (credential) { return { source: "personal-override", credentialId: host.credentialId, credential, }; } } return requiresPersonalHostAuthentication(host, protocol) ? { source: "required" } : { source: "secretless" }; } /** The grant id for this user's access to the host, direct or via a role. */ async function findActiveHostAccessId( hostId: number, userId: string, ): Promise { const roleIds = await createCurrentRoleRepository().listUserRoleIds(userId); const access = await createCurrentRbacAccessRepository().findActiveHostAccess( hostId, userId, roleIds, ); return access?.id ?? null; }