stack secrets from external providers (1Password, HashiCorp Vault, Infisical and Doppler)
This commit is contained in:
11 files changed
+560
-137
No files matched your search
@@ -0,0 +1,83 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getSecretProviderById } from '$lib/server/db';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { getProvider } from '$lib/server/secretproviders';
|
||||
import { UnsupportedOperationError, parseProviderError } from '$lib/server/secretproviders/shared';
|
||||
import { probeBulkKeysCached } from '$lib/server/secretproviders/probe-cache';
|
||||
|
||||
/**
|
||||
* Live probe of a stored provider: given a bulk selector and/or inline op://
|
||||
* references, return which KEY NAMES currently exist in the provider. Only names
|
||||
* are returned - secret values never leave the server. Powers the editor's live
|
||||
* "IN VAULT" marker.
|
||||
*/
|
||||
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 });
|
||||
}
|
||||
|
||||
const id = Number.parseInt(params.id);
|
||||
if (Number.isNaN(id)) {
|
||||
return json({ error: 'Invalid secret provider ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
const row = await getSecretProviderById(id);
|
||||
if (!row) {
|
||||
return json({ error: 'Secret provider not found' }, { status: 404 });
|
||||
}
|
||||
const provider = getProvider(row.type);
|
||||
if (!provider) {
|
||||
return json({ error: `Unknown secret provider type: ${row.type}` }, { status: 400 });
|
||||
}
|
||||
|
||||
let selector: string | undefined;
|
||||
let refs: string[] = [];
|
||||
try {
|
||||
const body = await request.json();
|
||||
if (typeof body?.selector === 'string' && body.selector.trim()) {
|
||||
selector = body.selector.trim();
|
||||
}
|
||||
if (Array.isArray(body?.refs)) {
|
||||
refs = body.refs.filter((r: unknown): r is string => typeof r === 'string' && !!r.trim());
|
||||
}
|
||||
} catch {
|
||||
// empty/invalid body: nothing to probe
|
||||
}
|
||||
|
||||
let bulkKeys: string[] = [];
|
||||
let resolvedRefs: string[] = [];
|
||||
|
||||
// A provider missing a mode throws UnsupportedOperationError; that is not a
|
||||
// failure - it just means that mode contributes no keys. Any other throw
|
||||
// (network, bad token, bad path) is a real probe failure.
|
||||
if (selector && provider.supportsBulk) {
|
||||
try {
|
||||
bulkKeys = await probeBulkKeysCached(id, provider, row.config, selector);
|
||||
} catch (e) {
|
||||
if (!(e instanceof UnsupportedOperationError)) {
|
||||
return json({ ok: false, error: shortError(e) }, { status: 200 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (refs.length && provider.supportsReferences) {
|
||||
try {
|
||||
const resolved = await provider.resolveSecretReferences(row.config, refs);
|
||||
resolvedRefs = [...resolved.keys()];
|
||||
} catch (e) {
|
||||
if (!(e instanceof UnsupportedOperationError)) {
|
||||
return json({ ok: false, error: shortError(e) }, { status: 200 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return json({ ok: true, bulkKeys, resolvedRefs });
|
||||
};
|
||||
|
||||
/** A short, non-reflecting error message for the probe-failed UI line. */
|
||||
function shortError(e: unknown): string {
|
||||
const raw = e instanceof Error ? e.message : String(e);
|
||||
return parseProviderError(raw) ?? raw.slice(0, 200);
|
||||
}
|
||||
+11
-2
@@ -1,5 +1,5 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { getStackEnvVars, setStackEnvVars, getStackSource } from '$lib/server/db';
|
||||
import { getStackEnvVars, setStackEnvVars, getStackSource, getStackInjectedSecretKeys, getSecretProviderById } from '$lib/server/db';
|
||||
import { findStackDir } from '$lib/server/stacks';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
@@ -107,7 +107,16 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
}
|
||||
}
|
||||
|
||||
return json({ variables });
|
||||
// Provider-injected key NAMES from the last deploy (no values) + the bound
|
||||
// provider's type/name, so the UI can show "these were loaded from <provider>".
|
||||
const injectedSecretKeys = [...await getStackInjectedSecretKeys(stackName, envIdNum ?? undefined)];
|
||||
let secretProvider: { type: string; name: string } | null = null;
|
||||
if (source?.secretProviderId) {
|
||||
const p = await getSecretProviderById(source.secretProviderId);
|
||||
if (p) secretProvider = { type: p.type, name: p.name };
|
||||
}
|
||||
|
||||
return json({ variables, injectedSecretKeys, secretProvider });
|
||||
} catch (error) {
|
||||
console.error('Error getting stack env vars:', error);
|
||||
return json({ error: 'Failed to get environment variables' }, { status: 500 });
|
||||
|
||||
Reference in new issue
Block a user