mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-09-14 11:06:15 +05:00
fix(vault): read Infisical secrets pulled in through an import
Fixes #5413. A secret brought into a folder with "Import Secrets" was reported as not found. Two reasons, and the first is easy to miss: The list endpoint takes the flag in **snake_case**. `includeImports` is silently ignored — the request still returns 200, with `imports` present but empty — so the imported secret looked like it simply did not exist. Measured against app.infisical.com with a registered import (confirmed via GET /api/v1/secret-imports): expandSecretReferences=true imports[] empty expandSecretReferences=true&includeImports=true imports[] empty expandSecretReferences=true&include_imports=true imports[] has the secret Second, imported secrets never appear in `secrets` — they come back in a separate `imports` array, one entry per source path, which the client did not read at all. Imported entries are merged before the folder's own, so a name defined in both resolves to the local value, matching how Infisical resolves it. Also measured, for whoever looks next: `/api/v4/secrets` returns imports with no flag at all, and single-secret reads (`/raw/{name}`) never see an imported key — 404 on v3, no such route on v4. So a folder listing is the only way to reach them. vault.test.ts: 57 passed. Reverting the fix fails the new merge test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
88118cdb3f
commit
0c1dfe978c
44
apps/dokploy/__test__/env/vault.test.ts
vendored
44
apps/dokploy/__test__/env/vault.test.ts
vendored
@ -472,6 +472,50 @@ describe("infisical client", () => {
|
||||
expect(params.get("secretPath")).toBe("/frontend");
|
||||
});
|
||||
|
||||
it("asks for imported secrets and merges them", async () => {
|
||||
mockFetch.mockResolvedValueOnce(loginResponse()).mockResolvedValueOnce(
|
||||
jsonResponse({
|
||||
secrets: [],
|
||||
imports: [
|
||||
{
|
||||
secretPath: "/shared",
|
||||
secrets: [
|
||||
{ secretKey: "DB_URL", secretValue: "postgres://imported" },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await infisicalClient.getSecrets(config, ["DB_URL"]);
|
||||
|
||||
expect(result).toEqual({ DB_URL: "postgres://imported" });
|
||||
const [listUrl] = mockFetch.mock.calls[1] as [string];
|
||||
// snake_case on purpose: `includeImports` is silently ignored and the
|
||||
// response comes back with an empty `imports` array.
|
||||
expect(new URL(listUrl).searchParams.get("include_imports")).toBe("true");
|
||||
});
|
||||
|
||||
it("lets a folder's own secret win over an imported one of the same name", async () => {
|
||||
mockFetch.mockResolvedValueOnce(loginResponse()).mockResolvedValueOnce(
|
||||
jsonResponse({
|
||||
secrets: [{ secretKey: "DB_URL", secretValue: "postgres://local" }],
|
||||
imports: [
|
||||
{
|
||||
secretPath: "/shared",
|
||||
secrets: [
|
||||
{ secretKey: "DB_URL", secretValue: "postgres://imported" },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await infisicalClient.getSecrets(config, ["DB_URL"]);
|
||||
|
||||
expect(result).toEqual({ DB_URL: "postgres://local" });
|
||||
});
|
||||
|
||||
it("throws a clear error for a missing secret", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(loginResponse())
|
||||
|
||||
@ -79,6 +79,12 @@ const readPath = async (
|
||||
// still reports success. Single secrets read via /raw/{name} expand by
|
||||
// default, which makes the difference easy to miss in the UI.
|
||||
expandSecretReferences: "true",
|
||||
// Secrets pulled in through "Import Secrets" are not part of `secrets`:
|
||||
// they come back in a separate `imports` array, and only when this is
|
||||
// asked for. Note the snake_case — `includeImports` is silently ignored
|
||||
// (the request still returns 200 with an empty `imports`), which is why
|
||||
// an imported secret looked like it simply did not exist.
|
||||
include_imports: "true",
|
||||
});
|
||||
const response = await vaultFetch(
|
||||
`${baseUrl(config)}/api/v3/secrets/raw?${params.toString()}`,
|
||||
@ -93,9 +99,17 @@ const readPath = async (
|
||||
|
||||
const body = (await response.json()) as {
|
||||
secrets?: { secretKey: string; secretValue: string }[];
|
||||
imports?: { secrets?: { secretKey: string; secretValue: string }[] }[];
|
||||
};
|
||||
|
||||
const secrets: Record<string, string> = {};
|
||||
// Imported first, then the folder's own: Infisical resolves a name defined
|
||||
// in both in favour of the local one, so writing local last preserves that.
|
||||
for (const imported of body.imports ?? []) {
|
||||
for (const secret of imported.secrets ?? []) {
|
||||
secrets[secret.secretKey] = secret.secretValue;
|
||||
}
|
||||
}
|
||||
for (const secret of body.secrets ?? []) {
|
||||
secrets[secret.secretKey] = secret.secretValue;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user