feat: convert secret-sources into a plugin (B20)

This commit is contained in:
LukeGus committed 2026-09-24 16:20:20 -05:00
1 parent 2ac7779eda
commit f4f26a9b8b
72 files changed
+18843 -933

No files matched your search

@@ -1,46 +1,38 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../database/repositories/factory.js", () => ({
createCurrentSecretSourceRepository: () => ({
listVisibleToUser: async () => [],
decryptToken: () => "",
}),
createCurrentSettingsRepository: () => ({ get: async () => null }),
}));
const {
clearExternalSecretCache,
isSecretReference,
resolveExternalSecretRefs,
resolveSecretReference,
} = await import("../../hosts/external-secrets.js");
const { setSecretResolverOwnerSource, resetSecretResolverRegistryForTests } =
await import("../../hosts/connect/secret-resolver-registry.js");
const source = {
id: "src-1",
userId: "alice",
name: "1P",
kind: "onepassword-connect",
baseUrl: "https://connect.internal",
token: "enc",
shared: false,
createdAt: "",
updatedAt: "",
};
describe("isSecretReference", () => {
it("recognises a scheme:// reference without confusing it with a secret", () => {
expect(isSecretReference("op://v/i/f")).toBe(true);
expect(isSecretReference(" op://v/i/f")).toBe(true);
expect(isSecretReference("vault+https://x")).toBe(true);
expect(isSecretReference("hunter2")).toBe(false);
expect(isSecretReference(undefined)).toBe(false);
});
});
describe("external secret references", () => {
beforeEach(() => clearExternalSecretCache());
it("replaces references in the host's secret fields and leaves plain secrets alone", async () => {
const resolver = vi.fn(async (_s, ref: string) => `resolved:${ref}`);
const resolver = vi.fn(
async (_userId: string, ref: string) => `resolved:${ref}`,
);
const host: Record<string, unknown> = {
password: "op://Infra/box/password",
key: "-----BEGIN OPENSSH PRIVATE KEY-----",
sudoPassword: "op://Infra/box/sudo",
username: "op://not-a-secret-field",
};
await resolveExternalSecretRefs(host, "alice", {
resolver,
pickSource: async () => source,
});
await resolveExternalSecretRefs(host, "alice", { resolver });
expect(host.password).toBe("resolved:op://Infra/box/password");
expect(host.sudoPassword).toBe("resolved:op://Infra/box/sudo");
expect(host.key).toBe("-----BEGIN OPENSSH PRIVATE KEY-----");
@@ -48,10 +40,10 @@ describe("external secret references", () => {
expect(resolver).toHaveBeenCalledTimes(2);
});
it("caches a resolved reference per source for a minute", async () => {
it("caches a resolved reference per user for a minute", async () => {
const resolver = vi.fn(async () => "s3cret");
let clock = 1_000_000;
const deps = { resolver, pickSource: async () => source, now: () => clock };
const deps = { resolver, now: () => clock };
await resolveSecretReference("alice", "op://v/i/f", deps);
await resolveSecretReference("alice", "op://v/i/f", deps);
expect(resolver).toHaveBeenCalledTimes(1);
@@ -60,12 +52,26 @@ describe("external secret references", () => {
expect(resolver).toHaveBeenCalledTimes(2);
});
it("fails clearly when the user has no secret source", async () => {
it("fails clearly when no plugin resolves the scheme", async () => {
await expect(
resolveSecretReference("bob", "op://v/i/f", {
resolver: async () => "x",
pickSource: async () => null,
}),
).rejects.toThrow(/no secret source/);
resolveSecretReference("bob", "op://v/i/f", {}),
).rejects.toThrow(/needs the .* plugin|no enabled plugin resolves/);
});
it("names the plugin that would resolve a disabled scheme", async () => {
setSecretResolverOwnerSource(() => [
{
scheme: "op",
pluginId: "secret-sources",
pluginName: "Secret Sources",
},
]);
try {
await expect(
resolveSecretReference("bob", "op://v/i/f", {}),
).rejects.toThrow("needs the Secret Sources plugin");
} finally {
resetSecretResolverRegistryForTests();
}
});
});