mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 07:05:24 +00:00
feat: convert secret-sources into a plugin (B20)
This commit is contained in:
1 parent
2ac7779eda
commit
f4f26a9b8b
72 files changed
+18843
-933
No files matched your search
@@ -1,15 +1,13 @@
|
||||
import { createCurrentSecretSourceRepository } from "../database/repositories/factory.js";
|
||||
import type { SecretSourceRecord } from "../database/repositories/secret-source-repository.js";
|
||||
import {
|
||||
isSecretReference,
|
||||
parseSecretReference,
|
||||
resolveConnectReference,
|
||||
} from "../utils/onepassword-connect.js";
|
||||
import { readSecretSourcePrivateAllowlist } from "../utils/secret-source-egress.js";
|
||||
getSecretResolver,
|
||||
requireSecretResolver,
|
||||
} from "./connect/secret-resolver-registry.js";
|
||||
|
||||
/**
|
||||
* Expands "op://vault/item/field" references in a resolved host's secret
|
||||
* fields into the actual secrets, fetched from the user's secret source.
|
||||
* Expands "<scheme>://..." references ("op://vault/item/field" for 1Password
|
||||
* Connect, and so on) in a resolved host's secret fields into the actual
|
||||
* secrets, resolved by whichever plugin registered that scheme through
|
||||
* ctx.credentials.registerSecretResolver.
|
||||
*
|
||||
* Runs once per host resolution, at the single point where every subsystem
|
||||
* gets its plaintext credentials - so terminal, SFTP, Docker, metrics and
|
||||
@@ -30,6 +28,8 @@ export const SECRET_FIELDS = [
|
||||
"telnetPassword",
|
||||
] as const;
|
||||
|
||||
const REFERENCE_PATTERN = /^([a-z][a-z0-9+.-]*):\/\//i;
|
||||
|
||||
const CACHE_TTL_MS = 60_000;
|
||||
const cache = new Map<string, { value: string; expiresAt: number }>();
|
||||
|
||||
@@ -38,37 +38,27 @@ export function clearExternalSecretCache(): void {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
export function isSecretReference(value: unknown): value is string {
|
||||
return typeof value === "string" && REFERENCE_PATTERN.test(value.trim());
|
||||
}
|
||||
|
||||
function referenceScheme(reference: string): string {
|
||||
const match = REFERENCE_PATTERN.exec(reference.trim());
|
||||
return (match?.[1] ?? "").toLowerCase();
|
||||
}
|
||||
|
||||
export type SecretResolver = (
|
||||
source: SecretSourceRecord,
|
||||
userId: string,
|
||||
reference: string,
|
||||
) => Promise<string>;
|
||||
|
||||
async function defaultResolver(
|
||||
source: SecretSourceRecord,
|
||||
userId: string,
|
||||
reference: string,
|
||||
): Promise<string> {
|
||||
const ref = parseSecretReference(reference);
|
||||
if (!ref) throw new Error(`Invalid secret reference: ${reference}`);
|
||||
const repository = createCurrentSecretSourceRepository();
|
||||
return resolveConnectReference(
|
||||
{
|
||||
baseUrl: source.baseUrl,
|
||||
token: repository.decryptToken(source),
|
||||
allowedPrivateHosts: await readSecretSourcePrivateAllowlist(),
|
||||
},
|
||||
ref,
|
||||
);
|
||||
}
|
||||
|
||||
/** The user's own source first, else a shared one. */
|
||||
export async function pickSecretSource(
|
||||
userId: string,
|
||||
): Promise<SecretSourceRecord | null> {
|
||||
const sources =
|
||||
await createCurrentSecretSourceRepository().listVisibleToUser(userId);
|
||||
return (
|
||||
sources.find((source) => source.userId === userId) ?? sources[0] ?? null
|
||||
);
|
||||
const scheme = referenceScheme(reference);
|
||||
const resolver = requireSecretResolver(scheme);
|
||||
return resolver.resolve(userId, reference);
|
||||
}
|
||||
|
||||
export async function resolveSecretReference(
|
||||
@@ -76,22 +66,15 @@ export async function resolveSecretReference(
|
||||
reference: string,
|
||||
deps: {
|
||||
resolver?: SecretResolver;
|
||||
pickSource?: (userId: string) => Promise<SecretSourceRecord | null>;
|
||||
now?: () => number;
|
||||
} = {},
|
||||
): Promise<string> {
|
||||
const now = deps.now ?? Date.now;
|
||||
const source = await (deps.pickSource ?? pickSecretSource)(userId);
|
||||
if (!source) {
|
||||
throw new Error(
|
||||
"This host uses a secret reference but no secret source is configured",
|
||||
);
|
||||
}
|
||||
const cacheKey = `${source.id}:${reference.trim()}`;
|
||||
const cacheKey = `${userId}:${reference.trim()}`;
|
||||
const cached = cache.get(cacheKey);
|
||||
if (cached && cached.expiresAt > now()) return cached.value;
|
||||
|
||||
const value = await (deps.resolver ?? defaultResolver)(source, reference);
|
||||
const value = await (deps.resolver ?? defaultResolver)(userId, reference);
|
||||
cache.set(cacheKey, { value, expiresAt: now() + CACHE_TTL_MS });
|
||||
return value;
|
||||
}
|
||||
@@ -108,3 +91,8 @@ export async function resolveExternalSecretRefs(
|
||||
host[field] = await resolveSecretReference(userId, value, deps);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether any plugin currently resolves this reference's scheme. */
|
||||
export function hasSecretResolverFor(reference: string): boolean {
|
||||
return !!getSecretResolver(referenceScheme(reference));
|
||||
}
|
||||
Reference in new issue
Block a user