Skip to content
Merged
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
43 changes: 43 additions & 0 deletions apps/dokploy/__test__/ai/ai-settings-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,49 @@ describe("AI settings organization boundary", () => {
);
});

it("rejects a new API URL while keeping the stored API key", async () => {
mocks.aiFindFirst.mockResolvedValue({
...aiSettings,
organizationId: "org-1",
});

for (const apiKey of ["__DOKPLOY_REDACTED_SECRET__", "", undefined]) {
await expect(
saveAiSettings("org-1", {
aiId: "ai-1",
apiKey,
apiUrl: "https://collector.example.net/v1",
model: "gpt-4o-mini",
}),
).rejects.toMatchObject({
code: "BAD_REQUEST",
message: "Re-enter the API key to change apiUrl",
});
}
expect(mocks.insert).not.toHaveBeenCalled();

await saveAiSettings("org-1", {
aiId: "ai-1",
apiKey: "__DOKPLOY_REDACTED_SECRET__",
apiUrl: "https://api.openai.com/v1/",
model: "gpt-4o",
});
await saveAiSettings("org-1", {
aiId: "ai-1",
apiKey: "new-secret",
apiUrl: "https://api.mistral.ai/v1",
});
expect(mocks.onConflictDoUpdate).toHaveBeenCalledTimes(2);
expect(mocks.onConflictDoUpdate).toHaveBeenLastCalledWith(
expect.objectContaining({
set: expect.objectContaining({
apiKey: "new-secret",
apiUrl: "https://api.mistral.ai/v1",
}),
}),
);
});

it("rejects deleting another organization's AI settings by aiId", async () => {
await expect(deleteAiSettings("ai-1", "org-1")).rejects.toMatchObject({
code: "NOT_FOUND",
Expand Down
61 changes: 57 additions & 4 deletions apps/dokploy/__test__/dns/dns-provider-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,34 @@ describe("maskDnsProviderConfig", () => {
});
});

it("masks only the secretAccessKey for a route53 config, keeping accessKeyId visible", () => {
it("masks both the accessKeyId and the secretAccessKey for a route53 config", () => {
const masked = maskDnsProviderConfig({
providerType: "route53",
accessKeyId: "AKIA_VISIBLE",
accessKeyId: "AKIA_STORED",
secretAccessKey: "shh",
});

expect(masked).toEqual({
providerType: "route53",
accessKeyId: "AKIA_VISIBLE",
accessKeyId: DNS_SECRET_MASK,
secretAccessKey: DNS_SECRET_MASK,
});
});

it("masks both porkbun keys", () => {
const masked = maskDnsProviderConfig({
providerType: "porkbun",
apiKey: "pk1_stored",
secretApiKey: "sk1_stored",
});

expect(masked).toEqual({
providerType: "porkbun",
apiKey: DNS_SECRET_MASK,
secretApiKey: DNS_SECRET_MASK,
});
});

it("leaves an empty sensitive field untouched instead of masking a blank value", () => {
const masked = maskDnsProviderConfig({
providerType: "cloudflare",
Expand Down Expand Up @@ -95,7 +109,46 @@ describe("mergeDnsProviderConfig", () => {
);
});

it("does not require re-entry for fields that are not sensitive", () => {
it("restores masked route53 and porkbun key ids from the stored config", () => {
expect(
mergeDnsProviderConfig(
{
providerType: "route53",
accessKeyId: DNS_SECRET_MASK,
secretAccessKey: DNS_SECRET_MASK,
},
{
providerType: "route53",
accessKeyId: "AKIA_STORED",
secretAccessKey: "stored-secret",
},
),
).toEqual({
providerType: "route53",
accessKeyId: "AKIA_STORED",
secretAccessKey: "stored-secret",
});
expect(
mergeDnsProviderConfig(
{
providerType: "porkbun",
apiKey: DNS_SECRET_MASK,
secretApiKey: DNS_SECRET_MASK,
},
{
providerType: "porkbun",
apiKey: "pk1_stored",
secretApiKey: "sk1_stored",
},
),
).toEqual({
providerType: "porkbun",
apiKey: "pk1_stored",
secretApiKey: "sk1_stored",
});
});

it("keeps a newly entered key id next to a masked secret", () => {
const existing = {
providerType: "route53" as const,
accessKeyId: "AKIA_OLD",
Expand Down
218 changes: 218 additions & 0 deletions apps/dokploy/__test__/env/vault-provider-secret-binding.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

const mocks = vi.hoisted(() => ({
findFirst: vi.fn(),
findMany: vi.fn(),
update: vi.fn(),
}));

vi.mock("@dokploy/server/db", () => ({
db: {
query: {
vaultProvider: { findFirst: mocks.findFirst },
projects: { findMany: mocks.findMany },
},
update: mocks.update,
},
}));

const {
VAULT_SECRET_MASK,
maskVaultProviderConfig,
mergeVaultProviderConfig,
updateVaultProvider,
} = await import("@dokploy/server/services/vault-provider");

const storedHashicorp = {
providerType: "hashicorp" as const,
url: "https://vault.example.com",
token: "stored-vault-token",
mount: "secret",
};

describe("vault provider stored secret binding", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.findMany.mockResolvedValue([]);
});

it("masks the AWS access key id together with the secret key", () => {
for (const providerType of ["aws", "aws-parameter-store"] as const) {
expect(
maskVaultProviderConfig({
providerType,
region: "eu-west-1",
accessKeyId: "AKIA_STORED",
secretAccessKey: "stored-secret",
}),
).toMatchObject({
accessKeyId: VAULT_SECRET_MASK,
secretAccessKey: VAULT_SECRET_MASK,
});
}
});

it("restores masked AWS credentials when the endpoint is unchanged", () => {
const stored = {
providerType: "aws" as const,
region: "eu-west-1",
accessKeyId: "AKIA_STORED",
secretAccessKey: "stored-secret",
endpoint: "https://secrets.example.com",
};

expect(
mergeVaultProviderConfig(
{
...stored,
accessKeyId: VAULT_SECRET_MASK,
secretAccessKey: VAULT_SECRET_MASK,
},
stored,
),
).toEqual(stored);
});

it("rejects sending a kept HashiCorp token to a new URL", () => {
expect(() =>
mergeVaultProviderConfig(
{
...storedHashicorp,
url: "https://collector.example.net",
token: VAULT_SECRET_MASK,
},
storedHashicorp,
),
).toThrow(
expect.objectContaining({
code: "BAD_REQUEST",
message: "Re-enter the vault provider credentials to change url",
}),
);

expect(
mergeVaultProviderConfig(
{ ...storedHashicorp, token: VAULT_SECRET_MASK, mount: "kv" },
storedHashicorp,
),
).toEqual({ ...storedHashicorp, mount: "kv" });

const reentered = {
...storedHashicorp,
url: "https://vault-new.example.com",
token: "new-token",
};
expect(mergeVaultProviderConfig(reentered, storedHashicorp)).toEqual(
reentered,
);
});

it("rejects kept credentials with a new endpoint or identity for every URL-bearing provider", () => {
const cases = [
[
{
providerType: "infisical" as const,
siteUrl: "https://app.infisical.com",
clientId: "client",
clientSecret: "stored",
projectId: "p",
environmentSlug: "prod",
secretPath: "/",
},
{ siteUrl: "https://collector.example.net" },
"clientSecret",
],
[
{
providerType: "aws" as const,
region: "eu-west-1",
accessKeyId: "AKIA_STORED",
secretAccessKey: "stored",
},
{ endpoint: "https://collector.example.net" },
"secretAccessKey",
],
[
{
providerType: "aws-parameter-store" as const,
region: "eu-west-1",
accessKeyId: "AKIA_STORED",
secretAccessKey: "stored",
},
{ accessKeyId: "AKIA_OTHER" },
"secretAccessKey",
],
[
{
providerType: "azure" as const,
vaultUri: "https://vault.vault.azure.net",
tenantId: "tenant",
clientId: "client",
clientSecret: "stored",
},
{ vaultUri: "https://collector.example.net" },
"clientSecret",
],
[
{
providerType: "scaleway" as const,
region: "fr-par",
projectId: "p",
secretKey: "stored",
apiUrl: "https://api.scaleway.com",
},
{ apiUrl: "https://collector.example.net" },
"secretKey",
],
[
{
providerType: "phase" as const,
token: "stored",
appId: "app",
env: "prod",
path: "/",
apiUrl: "https://api.phase.dev",
},
{ apiUrl: "https://collector.example.net" },
"token",
],
] as const;

for (const [stored, change, secretField] of cases) {
const incoming = {
...stored,
...change,
[secretField]: VAULT_SECRET_MASK,
};
expect(() => mergeVaultProviderConfig(incoming as never, stored)).toThrow(
expect.objectContaining({ code: "BAD_REQUEST" }),
);
}
});

it("does not persist a provider update that redirects the stored token", async () => {
mocks.findFirst.mockResolvedValue({
vaultProviderId: "vault-1",
organizationId: "org-1",
name: "vault",
providerType: "hashicorp",
config: storedHashicorp,
assignments: [],
});

await expect(
updateVaultProvider(
"vault-1",
"vault",
{
...storedHashicorp,
url: "https://collector.example.net",
token: VAULT_SECRET_MASK,
},
[],
),
).rejects.toMatchObject({ code: "BAD_REQUEST" });

expect(mocks.update).not.toHaveBeenCalled();
});
});
Loading
Loading