diff --git a/src/routes/api/environments/test/+server.ts b/src/routes/api/environments/test/+server.ts index d407200..b18bd2a 100644 --- a/src/routes/api/environments/test/+server.ts +++ b/src/routes/api/environments/test/+server.ts @@ -1,6 +1,8 @@ import { json } from '@sveltejs/kit'; import { unixSocketRequest, httpsAgentRequest } from '$lib/server/docker'; import type { DockerClientConfig } from '$lib/server/docker'; +import { getEnvironment } from '$lib/server/db'; +import { authorize } from '$lib/server/authorize'; import { isSafeNotificationUrl } from '$lib/server/url-safety'; import type { RequestHandler } from './$types'; @@ -15,6 +17,14 @@ interface TestConnectionRequest { tlsKey?: string; tlsSkipVerify?: boolean; hawserToken?: string; + /** + * When editing an existing environment, secrets (hawserToken, tlsKey) are never sent + * back to the client, so an unchanged token comes through empty. Passing the env id lets + * the server fall back to the STORED secret for any secret field the body left blank, so + * "Test connection" works without re-typing the token (#1483). Non-secret fields still + * come from the body, so the test reflects the edited form. + */ + environmentId?: number; } function cleanPem(pem: string): string { @@ -45,16 +55,36 @@ function buildDockerClientConfig(config: TestConnectionRequest): DockerClientCon * * @openapi * summary: Test a Docker/Hawser connection configuration WITHOUT saving it as an environment - * body: {connectionType:string!, socketPath:string, host:string, port:integer, protocol:string, tlsCa:string, tlsCert:string, tlsKey:string, tlsSkipVerify:boolean, hawserToken:string} + * description: Pass environmentId to fall back to that saved environment's stored secrets (hawserToken, tlsKey) for any secret field left blank in the body, so Test connection works when editing without re-entering the token. + * body: {connectionType:string!, socketPath:string, host:string, port:integer, protocol:string, tlsCa:string, tlsCert:string, tlsKey:string, tlsSkipVerify:boolean, hawserToken:string, environmentId:integer} * body-example: {"connectionType":"socket","socketPath":"/var/run/docker.sock"} * resp-200: {success:boolean!, info:{serverVersion:string, containers:integer, images:integer, name:string}, hawser:{}} * resp-200-desc: success:false with a human-readable error message is also returned as HTTP 200 (connection failures are not transport errors) * resp-400: Host is required for direct/hawser-standard connection types + * resp-403: The caller cannot access the environment named by environmentId */ -export const POST: RequestHandler = async ({ request }) => { +export const POST: RequestHandler = async ({ request, cookies }) => { try { const config: TestConnectionRequest = await request.json(); + // Fill unchanged secrets from the stored environment when editing (#1483): the client + // never receives hawserToken / tlsKey, so an untouched field arrives empty. Only fall + // back for a BLANK field, so a freshly typed token/key still wins. + if (config.environmentId) { + // Gate the id: without this, any authenticated user could name another + // environment's id and have its stored token sent to a body-supplied host, + // exfiltrating a secret they may have no access to (env-scoped RBAC). + const auth = await authorize(cookies); + if (!(await auth.canAccessEnvironment(config.environmentId))) { + return json({ success: false, error: 'Access denied' }, { status: 403 }); + } + const stored = await getEnvironment(config.environmentId); + if (stored) { + if (!config.hawserToken) config.hawserToken = stored.hawserToken || undefined; + if (!config.tlsKey) config.tlsKey = stored.tlsKey || undefined; + } + } + // Build fetch options based on connection type let response: Response; diff --git a/src/routes/settings/environments/EnvironmentModal.svelte b/src/routes/settings/environments/EnvironmentModal.svelte index e31e046..f1ef14a 100644 --- a/src/routes/settings/environments/EnvironmentModal.svelte +++ b/src/routes/settings/environments/EnvironmentModal.svelte @@ -743,7 +743,10 @@ tlsCert: cleanCertificate(formTlsCert), tlsKey: cleanCertificate(formTlsKey), tlsSkipVerify: formTlsSkipVerify, - hawserToken: formHawserToken || pendingToken + hawserToken: formHawserToken || pendingToken, + // When editing, let the server fall back to the stored token/key for any + // secret the form left blank (secrets are never sent back to the client) (#1483). + environmentId: isEditing && environment ? environment.id : undefined }) }); @@ -961,9 +964,9 @@ // - socket/direct: the deploy staging dir AND the in-app editor source // (containers' compose project labels point here, so redeploy is // required after rename or next restart fails) - // - hawser-standard/edge: the in-app editor staging source for stacks - // (the agent's deployed dir lives on the remote host and is not affected) - // Git repository clones are shared per-repo under git-repos//. + // - hawser-standard/edge: the in-app editor source for ALL stacks + // PLUS the git-repos clones for git stacks (the agent's deployed + // dir lives on the remote host and is not affected) // → fire the warning whenever the name changes. const newName = formName.trim(); const willRenameOnDisk = newName !== environment.name; @@ -3143,11 +3146,8 @@ - + @@ -3156,13 +3156,17 @@ Rename environment? -

The following staging directories will be moved on the Dockhand host (local deployed stacks under STACKS_DIR, when configured, are not renamed):

+

The following directories will be moved on the Dockhand host:

$DATA_DIR/stacks/{renameConfirmFrom}/ $DATA_DIR/stacks/{renameConfirmTo}/ - (staging) +
+
+ $DATA_DIR/git-repos/{renameConfirmFrom}/ + + $DATA_DIR/git-repos/{renameConfirmTo}/
{#if renameCountsUnknown} @@ -3212,7 +3216,7 @@

Their containers run on the Hawser agent host, where the deploy directory doesn't include the env name — those keep working without - a redeploy. Only the local editor staging directory is renamed. + a redeploy. Only the local editor source and git clone caches move.

{/if}