validate secret provider config before save

This commit is contained in:
jarek committed 2026-08-14 14:01:36 +02:00
1 parent 344ad4aadc
commit 5fef45e003
8 files changed
+106 -31

No files matched your search

@@ -3,9 +3,17 @@ import type { RequestHandler } from './$types';
import { getSecretProviderById } from '$lib/server/db';
import { authorize } from '$lib/server/authorize';
import { testProviderConnection } from '$lib/server/secretproviders';
import { mergeProviderConfigForWrite } from '$lib/server/secretproviders/shared';
import type { SecretProviderConfig } from '$lib/server/secretproviders/shared';
/** Test a stored provider using its persisted (decrypted) config. */
export const POST: RequestHandler = async ({ params, cookies }) => {
/**
* Test a stored provider. With no body it tests the persisted config. With an optional
* `{ config }` override (the edit form's typed non-secret fields) it tests exactly what a
* Save would persist: the incoming config merged over the stored one, with a blank token
* falling back to the stored secret. The secret is NEVER supplied by the client - it comes
* from storage server-side - so this stays a `secrets:view` operation.
*/
export const POST: RequestHandler = async ({ params, cookies, request }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !(await auth.can('secrets', 'view'))) {
return json({ error: 'Permission denied' }, { status: 403 });
@@ -21,7 +29,20 @@ export const POST: RequestHandler = async ({ params, cookies }) => {
return json({ error: 'Secret provider not found' }, { status: 404 });
}
const result = await testProviderConnection(provider.type, provider.config);
let configToTest: SecretProviderConfig = provider.config;
try {
const body = await request.json();
if (body && typeof body.config === 'object' && body.config !== null) {
configToTest = mergeProviderConfigForWrite(
body.config as Record<string, unknown>,
provider.config as unknown as Record<string, unknown>
) as unknown as SecretProviderConfig;
}
} catch {
// No/invalid body: test the stored config as-is.
}
const result = await testProviderConnection(provider.type, configToTest);
if (!result.ok) {
return json({ ok: false, error: result.error }, { status: 200 });
}
@@ -212,23 +212,24 @@
formError = '';
try {
const config = collectConfig();
// On edit the token is server-side (blank field = keep stored), so if the
// user hasn't typed a new one, test the STORED config; the pre-filled
// non-secret fields aren't enough to re-test from the client.
const hasNewSecret = fields.some((f) => f.type === 'password' && (config[f.key] ?? '') !== '');
const missing = missingRequired(config, isEditing);
if (missing) {
formError = missing;
return;
}
let response: Response;
if (isEditing && !hasNewSecret) {
// Test the stored (server-side) config.
if (isEditing) {
// Test EXACTLY what a Save would persist: the typed non-secret fields, merged
// server-side over the stored config (a blank token keeps the stored one). This
// makes an edited address/mount/namespace actually get tested - not the old
// stored config.
response = await fetch(`/api/secret-providers/${provider!.id}/test`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ config }),
});
} else {
const missing = missingRequired(config, isEditing);
if (missing) {
formError = missing;
return;
}
response = await fetch('/api/secret-providers/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },