Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions apps/dokploy/__test__/env/vault.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ vi.mock("@dokploy/server/db", () => ({
},
}));

const getVariables = vi.fn();
const createClient = vi.fn();

vi.mock("@1password/sdk", () => ({
createClient: (...args: unknown[]) => createClient(...args),
}));

import { prepareEnvironmentVariables } from "@dokploy/server/utils/docker/utils";
import {
resolveVaultReferences,
Expand All @@ -21,6 +28,7 @@ import { azureClient } from "@dokploy/server/utils/vault/azure";
import { dopplerClient } from "@dokploy/server/utils/vault/doppler";
import { hashicorpClient } from "@dokploy/server/utils/vault/hashicorp";
import { infisicalClient } from "@dokploy/server/utils/vault/infisical";
import { onePasswordClient } from "@dokploy/server/utils/vault/onepassword";
import { phaseClient } from "@dokploy/server/utils/vault/phase";
import { scalewayClient } from "@dokploy/server/utils/vault/scaleway";

Expand All @@ -37,6 +45,13 @@ const jsonResponse = (body: unknown, ok = true, status = 200) =>
beforeEach(() => {
findMany.mockReset();
mockFetch.mockReset();
getVariables.mockReset();
createClient.mockReset();
createClient.mockResolvedValue({
environments: {
getVariables: (...args: unknown[]) => getVariables(...args),
},
});
});

const scope = {
Expand Down Expand Up @@ -961,3 +976,103 @@ describe("phase client", () => {
expect(result).toBe("DB_PASSWORD=s3cret");
});
});

describe("onepassword client", () => {
const config = {
providerType: "onepassword" as const,
serviceAccountToken: "ops_service-account-token",
environmentId: "env-123",
};

const variablesResponse = (
variables: Array<{ name: string; value: string }>,
) => ({
variables: variables.map((variable) => ({ ...variable, masked: false })),
});

it("authenticates with the service account token and reads variables by name", async () => {
getVariables.mockResolvedValue(
variablesResponse([{ name: "DB_PASSWORD", value: "op-secret" }]),
);

const result = await onePasswordClient.getSecrets(config, ["DB_PASSWORD"]);

expect(result).toEqual({ DB_PASSWORD: "op-secret" });
expect(createClient).toHaveBeenCalledWith(
expect.objectContaining({ auth: config.serviceAccountToken }),
);
expect(getVariables).toHaveBeenCalledWith(config.environmentId);
});

it("wraps authentication failures with a clear message", async () => {
createClient.mockRejectedValue(new Error("invalid token"));

await expect(
onePasswordClient.getSecrets(config, ["DB_PASSWORD"]),
).rejects.toThrow("1Password: authentication failed (invalid token)");
});

it("wraps environment read failures with a clear message", async () => {
getVariables.mockRejectedValue(new Error("environment not found"));

await expect(
onePasswordClient.getSecrets(config, ["DB_PASSWORD"]),
).rejects.toThrow(
'1Password: failed to read environment "env-123" (environment not found)',
);
});

it("throws a clear error for a missing variable", async () => {
getVariables.mockResolvedValue(
variablesResponse([{ name: "OTHER", value: "x" }]),
);

await expect(
onePasswordClient.getSecrets(config, ["MISSING"]),
).rejects.toThrow(
'1Password: variable "MISSING" not found in environment "env-123"',
);
});

it("tests the connection by fetching variables", async () => {
getVariables.mockResolvedValue(variablesResponse([]));

await onePasswordClient.testConnection(config);

expect(getVariables).toHaveBeenCalledWith(config.environmentId);
});

it("lists variable names from the environment", async () => {
getVariables.mockResolvedValue(
variablesResponse([
{ name: "DB_PASSWORD", value: "a" },
{ name: "API_KEY", value: "b" },
]),
);

const names = await onePasswordClient.listSecretNames?.(config);

expect(names).toEqual(["DB_PASSWORD", "API_KEY"]);
});

it("resolves env refs end to end through a onepassword provider", async () => {
findMany.mockResolvedValue([
{
name: "op-prod",
providerType: "onepassword",
config,
assignments: assignedEverywhere,
},
]);
getVariables.mockResolvedValue(
variablesResponse([{ name: "DB_PASSWORD", value: "s3cret" }]),
);

const result = await resolveVaultReferences(
"DB_PASSWORD=${{vault.op-prod.DB_PASSWORD}}",
scope,
);

expect(result).toBe("DB_PASSWORD=s3cret");
});
});
4 changes: 4 additions & 0 deletions apps/dokploy/__test__/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export default defineConfig({
__dirname,
"../../../packages/server/src",
),
"@1password/sdk": path.resolve(
__dirname,
"../../../packages/server/node_modules/@1password/sdk",
),
},
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const providerLabels = {
azure: "Azure Key Vault",
scaleway: "Scaleway Secret Manager",
phase: "Phase",
onepassword: "1Password",
} as const;

type ProviderType = keyof typeof providerLabels;
Expand All @@ -67,6 +68,7 @@ const VaultProviderSchema = z
"azure",
"scaleway",
"phase",
"onepassword",
]),
url: z.string(),
token: z.string(),
Expand Down Expand Up @@ -99,6 +101,8 @@ const VaultProviderSchema = z
phaseEnv: z.string(),
phasePath: z.string(),
phaseApiUrl: z.string(),
onePasswordServiceAccountToken: z.string(),
onePasswordEnvironmentId: z.string(),
assignments: z.array(
z.object({
projectId: z.string(),
Expand Down Expand Up @@ -237,6 +241,10 @@ const VaultProviderSchema = z
["phaseAppId", "App ID is required"],
["phaseEnv", "Environment is required"],
],
onepassword: [
["onePasswordServiceAccountToken", "Service Account Token is required"],
["onePasswordEnvironmentId", "Environment ID is required"],
],
};

for (const [field, message] of required[data.providerType] ?? []) {
Expand Down Expand Up @@ -303,6 +311,8 @@ const defaultValues: VaultProviderForm = {
phaseEnv: "",
phasePath: "/",
phaseApiUrl: "https://api.phase.dev",
onePasswordServiceAccountToken: "",
onePasswordEnvironmentId: "",
assignments: [],
};

Expand Down Expand Up @@ -375,6 +385,12 @@ const buildConfig = (data: VaultProviderForm) => {
path: data.phasePath || "/",
apiUrl: data.phaseApiUrl || "https://api.phase.dev",
};
case "onepassword":
return {
providerType: "onepassword" as const,
serviceAccountToken: data.onePasswordServiceAccountToken,
environmentId: data.onePasswordEnvironmentId,
};
}
};

Expand Down Expand Up @@ -507,6 +523,10 @@ export const HandleVaultProvider = ({ vaultProviderId }: Props) => {
phasePath: provider.config.path,
phaseApiUrl: provider.config.apiUrl,
}),
...(provider.config.providerType === "onepassword" && {
onePasswordServiceAccountToken: provider.config.serviceAccountToken,
onePasswordEnvironmentId: provider.config.environmentId,
}),
});
} else if (!vaultProviderId) {
form.reset(defaultValues);
Expand Down Expand Up @@ -1195,6 +1215,51 @@ export const HandleVaultProvider = ({ vaultProviderId }: Props) => {
</>
)}

{providerType === "onepassword" && (
<>
<FormField
control={form.control}
name="onePasswordServiceAccountToken"
render={({ field }) => (
<FormItem>
<FormLabel>Service Account Token</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="onePasswordEnvironmentId"
render={({ field }) => (
<FormItem>
<FormLabel>Environment ID</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormDescription>
Create a service account with read access to the{" "}
<a
className="text-primary"
href="https://www.1password.dev/environments"
target="_blank"
rel="noopener noreferrer"
>
1Password Environment
</a>{" "}
you want to use, then paste its token and the Environment ID
above. Reference format:{" "}
<code>{"${{vault.<name>.VARIABLE_NAME}}"}</code>
</FormDescription>
</>
)}

<div className="flex flex-col gap-2 rounded-lg border p-3">
<div className="flex flex-row items-center justify-between">
<FormLabel>Access</FormLabel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const providerLabels: Record<string, string> = {
azure: "Azure Key Vault",
scaleway: "Scaleway Secret Manager",
phase: "Phase",
onepassword: "1Password",
};

export const ShowVaultProviders = () => {
Expand Down
17 changes: 17 additions & 0 deletions apps/dokploy/components/icons/vault-provider-icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,22 @@ export const PhaseIcon = ({ className }: Props) => (
</svg>
);

export const OnePasswordIcon = ({ className }: Props) => (
<svg
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M32 16C32 7.164 24.836 0 16 0C7.164 0 0 7.164 0 16C0 24.837 7.164 32 16 32C24.836 32 32 24.837 32 16ZM13.063 6.989C12.962 7.188 12.962 7.448 12.962 7.969V12.076C12.962 12.206 12.962 12.271 12.979 12.332C12.9934 12.3851 13.0171 12.4352 13.049 12.48C13.085 12.53 13.136 12.572 13.237 12.654L14.316 13.53C14.437 13.629 14.498 13.678 14.52 13.738C14.54 13.79 14.54 13.848 14.52 13.9C14.498 13.959 14.437 14.008 14.316 14.107L13.236 14.984C13.136 15.066 13.086 15.107 13.049 15.157C13.017 15.2021 12.9932 15.2526 12.979 15.306C12.962 15.366 12.962 15.431 12.962 15.561V24.031C12.962 24.552 12.962 24.813 13.063 25.011C13.1522 25.1863 13.2947 25.3288 13.47 25.418C13.668 25.519 13.929 25.519 14.45 25.519H17.55C18.071 25.519 18.331 25.519 18.53 25.418C18.7053 25.3288 18.8478 25.1863 18.937 25.011C19.038 24.813 19.038 24.552 19.038 24.031V19.924C19.038 19.794 19.038 19.729 19.022 19.669C19.0074 19.6155 18.9834 19.565 18.951 19.52C18.8959 19.4543 18.8327 19.3959 18.763 19.346L17.684 18.47C17.563 18.371 17.502 18.322 17.48 18.262C17.4608 18.2101 17.4608 18.1529 17.48 18.101C17.502 18.041 17.563 17.991 17.684 17.893L18.764 17.016C18.864 16.935 18.914 16.894 18.951 16.843C18.983 16.7979 19.0067 16.7474 19.021 16.694C19.038 16.634 19.038 16.569 19.038 16.439V7.97C19.038 7.449 19.038 7.189 18.937 6.99C18.8478 6.81471 18.7053 6.67221 18.53 6.583C18.331 6.482 18.071 6.482 17.55 6.482H14.45C13.929 6.482 13.669 6.482 13.47 6.583C13.2947 6.67221 13.1522 6.81371 13.063 6.989Z"
fill="currentColor"
/>
</svg>
);

export const vaultProviderIcons = {
hashicorp: HashicorpVaultIcon,
infisical: InfisicalIcon,
Expand All @@ -613,4 +629,5 @@ export const vaultProviderIcons = {
azure: AzureIcon,
scaleway: ScalewayIcon,
phase: PhaseIcon,
onepassword: OnePasswordIcon,
} as const;
1 change: 1 addition & 0 deletions apps/dokploy/drizzle/0197_youthful_vector.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TYPE "public"."VaultProviderType" ADD VALUE 'onepassword';
Loading