mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
134 lines
3.7 KiB
TypeScript
134 lines
3.7 KiB
TypeScript
import type { AuthOverrideProtocol } from "../../types/auth-protocols.js";
|
|
import { isAuthOverrideProtocol } from "../hosts/protocol-auth/registry.js";
|
|
import {
|
|
createCurrentCredentialRepository,
|
|
createCurrentSharedHostAuthOverrideRepository,
|
|
createCurrentUserRepository,
|
|
} from "../database/repositories/factory.js";
|
|
import { logAudit } from "./audit-logger.js";
|
|
import { PermissionManager } from "./permission-manager.js";
|
|
|
|
export interface SharedHostAuthOverrideAuditContext {
|
|
ipAddress?: string;
|
|
userAgent?: string;
|
|
}
|
|
|
|
export class SharedHostAuthOverrideServiceError extends Error {
|
|
constructor(
|
|
message: string,
|
|
readonly statusCode: number,
|
|
) {
|
|
super(message);
|
|
this.name = "SharedHostAuthOverrideServiceError";
|
|
}
|
|
}
|
|
|
|
export class SharedHostAuthOverrideService {
|
|
private static instance: SharedHostAuthOverrideService;
|
|
|
|
private constructor() {}
|
|
|
|
static getInstance(): SharedHostAuthOverrideService {
|
|
if (!this.instance) {
|
|
this.instance = new SharedHostAuthOverrideService();
|
|
}
|
|
return this.instance;
|
|
}
|
|
|
|
async getCredentialId(
|
|
hostId: number,
|
|
userId: string,
|
|
protocol: AuthOverrideProtocol,
|
|
): Promise<number | null> {
|
|
this.requireSupportedProtocol(protocol);
|
|
await this.requireSharedHostAccess(hostId, userId);
|
|
return createCurrentSharedHostAuthOverrideRepository().findCredentialId(
|
|
hostId,
|
|
userId,
|
|
protocol,
|
|
);
|
|
}
|
|
|
|
async setCredentialId(
|
|
hostId: number,
|
|
userId: string,
|
|
protocol: AuthOverrideProtocol,
|
|
credentialId: number | null,
|
|
auditContext: SharedHostAuthOverrideAuditContext = {},
|
|
): Promise<void> {
|
|
this.requireSupportedProtocol(protocol);
|
|
await this.requireSharedHostAccess(hostId, userId);
|
|
|
|
if (credentialId !== null) {
|
|
const credential =
|
|
await createCurrentCredentialRepository().findByIdForUser(
|
|
userId,
|
|
credentialId,
|
|
);
|
|
if (!credential) {
|
|
throw new SharedHostAuthOverrideServiceError(
|
|
"Credential not found",
|
|
404,
|
|
);
|
|
}
|
|
}
|
|
|
|
const repository = createCurrentSharedHostAuthOverrideRepository();
|
|
if (credentialId === null) {
|
|
await repository.clearCredential(hostId, userId, protocol);
|
|
} else {
|
|
await repository.setCredential(hostId, userId, protocol, credentialId);
|
|
}
|
|
|
|
try {
|
|
const actor = await createCurrentUserRepository().findById(userId);
|
|
await logAudit({
|
|
userId,
|
|
username: actor?.username ?? userId,
|
|
action:
|
|
credentialId === null
|
|
? "clear_shared_host_auth_override"
|
|
: "set_shared_host_auth_override",
|
|
resourceType: "host",
|
|
resourceId: String(hostId),
|
|
details: JSON.stringify({
|
|
protocol,
|
|
credentialId,
|
|
}),
|
|
ipAddress: auditContext.ipAddress,
|
|
userAgent: auditContext.userAgent,
|
|
success: true,
|
|
});
|
|
} catch {
|
|
// Audit bookkeeping must never turn a successful override write into a
|
|
// failed API response.
|
|
}
|
|
}
|
|
|
|
private requireSupportedProtocol(protocol: AuthOverrideProtocol): void {
|
|
if (!isAuthOverrideProtocol(protocol)) {
|
|
throw new SharedHostAuthOverrideServiceError(
|
|
`No plugin declares the ${protocol} protocol`,
|
|
400,
|
|
);
|
|
}
|
|
}
|
|
|
|
private async requireSharedHostAccess(
|
|
hostId: number,
|
|
userId: string,
|
|
): Promise<void> {
|
|
const access = await PermissionManager.getInstance().canAccessHost(
|
|
userId,
|
|
hostId,
|
|
"connect",
|
|
);
|
|
if (!access.hasAccess || !access.isShared || access.isAdminBypass) {
|
|
throw new SharedHostAuthOverrideServiceError(
|
|
"Authentication overrides require active shared host access",
|
|
403,
|
|
);
|
|
}
|
|
}
|
|
}
|