mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 22:55:24 +00:00
203 lines
6.0 KiB
TypeScript
203 lines
6.0 KiB
TypeScript
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<HostResolutionHostRecord, "credentialId" | "authType">,
|
|
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<HostResolutionHostRecord, "shareSshAuth">,
|
|
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<RecipientSharedHostAuthResolution> {
|
|
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<number | null> {
|
|
const roleIds = await createCurrentRoleRepository().listUserRoleIds(userId);
|
|
const access = await createCurrentRbacAccessRepository().findActiveHostAccess(
|
|
hostId,
|
|
userId,
|
|
roleIds,
|
|
);
|
|
return access?.id ?? null;
|
|
}
|