environment API uses a write-only model for the tls key and hawser token
This commit is contained in:
6 files changed
+125
-17
No files matched your search
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Strip an environment's decrypted secrets before it leaves the server.
|
||||||
|
*
|
||||||
|
* `getEnvironment(s)` decrypt `tlsKey` (the private TLS client key) and `hawserToken`
|
||||||
|
* for internal use, but neither must ever reach an API response: the TLS key
|
||||||
|
* authenticates directly against the Docker daemon, bypassing Dockhand entirely and
|
||||||
|
* surviving a revoked login. `tlsCa`/`tlsCert` are the public CA + client cert and
|
||||||
|
* are NOT secret, so they stay. In their place we expose `hasTlsKey` / `hasHawserToken`
|
||||||
|
* booleans so the UI can show "configured" without holding the value.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** The two secret fields removed from every environment API response. */
|
||||||
|
export type EnvironmentSecretField = 'tlsKey' | 'hawserToken';
|
||||||
|
|
||||||
|
export function redactEnvironment<T extends { tlsKey?: string | null; hawserToken?: string | null }>(
|
||||||
|
env: T
|
||||||
|
): Omit<T, EnvironmentSecretField> & { hasTlsKey: boolean; hasHawserToken: boolean } {
|
||||||
|
const { tlsKey, hawserToken, ...rest } = env;
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
hasTlsKey: !!(tlsKey && tlsKey.length > 0),
|
||||||
|
hasHawserToken: !!(hawserToken && hawserToken.length > 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -9,13 +9,14 @@ import { resetHostDetection, detectHostDataDir } from '$lib/server/host-path';
|
|||||||
import { serializeLabels, parseLabels, MAX_LABELS } from '$lib/utils/label-colors';
|
import { serializeLabels, parseLabels, MAX_LABELS } from '$lib/utils/label-colors';
|
||||||
import { cleanPem } from '$lib/utils/pem';
|
import { cleanPem } from '$lib/utils/pem';
|
||||||
import { validateEnvName } from '$lib/utils/env-name';
|
import { validateEnvName } from '$lib/utils/env-name';
|
||||||
|
import { redactEnvironment } from '$lib/server/environment-redact';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @openapi
|
* @openapi
|
||||||
* summary: List all Docker environments (hosts) known to Dockhand
|
* summary: List all Docker environments (hosts) known to Dockhand
|
||||||
* resp-200: array<{id:integer!, name:string!, connectionType:string!, host:string, port:integer, protocol:string, icon:string, publicIp:string, timezone:string}>
|
* resp-200: array<{id:integer!, name:string!, connectionType:string!, host:string, port:integer, protocol:string, icon:string, publicIp:string, timezone:string, hasTlsKey:boolean, hasHawserToken:boolean}>
|
||||||
* resp-200-desc: Environments accessible to the caller (filtered by RBAC in Enterprise mode; all environments in Free edition)
|
* resp-200-desc: Environments accessible to the caller (filtered by RBAC in Enterprise mode; all environments in Free edition). The tlsKey (private TLS client key) and hawserToken secrets are NEVER returned; hasTlsKey / hasHawserToken indicate whether one is stored.
|
||||||
* resp-200-example: [{"id":1,"name":"hhdocker01","connectionType":"socket","host":null,"port":2375,"protocol":"http","icon":"server","publicIp":"203.0.113.10","timezone":"Europe/Berlin"},{"id":2,"name":"hhdocker02","connectionType":"hawser-edge","host":null,"port":2375,"protocol":"http","icon":"server","publicIp":null,"timezone":"Europe/Berlin"}]
|
* resp-200-example: [{"id":1,"name":"hhdocker01","connectionType":"socket","host":null,"port":2375,"protocol":"http","icon":"server","publicIp":"203.0.113.10","timezone":"Europe/Berlin","hasTlsKey":false,"hasHawserToken":false},{"id":2,"name":"hhdocker02","connectionType":"hawser-edge","host":null,"port":2375,"protocol":"http","icon":"server","publicIp":null,"timezone":"Europe/Berlin","hasTlsKey":false,"hasHawserToken":true}]
|
||||||
* resp-403: Permission denied (RBAC 'environments:view' missing)
|
* resp-403: Permission denied (RBAC 'environments:view' missing)
|
||||||
* resp-500: Unexpected error while loading environments
|
* resp-500: Unexpected error while loading environments
|
||||||
*/
|
*/
|
||||||
@@ -55,7 +56,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
|||||||
const timezone = await getEnvironmentTimezone(env.id);
|
const timezone = await getEnvironmentTimezone(env.id);
|
||||||
const imagePruneSettings = await getImagePruneSettings(env.id);
|
const imagePruneSettings = await getImagePruneSettings(env.id);
|
||||||
return {
|
return {
|
||||||
...env,
|
...redactEnvironment(env),
|
||||||
labels: parseLabels(env.labels as string | null),
|
labels: parseLabels(env.labels as string | null),
|
||||||
publicIp: publicIps[env.id.toString()] || null,
|
publicIp: publicIps[env.id.toString()] || null,
|
||||||
updateCheckEnabled: updateSettings?.enabled || false,
|
updateCheckEnabled: updateSettings?.enabled || false,
|
||||||
@@ -172,7 +173,7 @@ export const POST: RequestHandler = async (event) => {
|
|||||||
// Audit log
|
// Audit log
|
||||||
await auditEnvironment(event, 'create', env.id, env.name);
|
await auditEnvironment(event, 'create', env.id, env.name);
|
||||||
|
|
||||||
return json(env);
|
return json(redactEnvironment(env));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create environment:', error);
|
console.error('Failed to create environment:', error);
|
||||||
return json({ error: 'Failed to create environment' }, { status: 500 });
|
return json({ error: 'Failed to create environment' }, { status: 500 });
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { authorize } from '$lib/server/authorize';
|
|||||||
import { auditEnvironment } from '$lib/server/audit';
|
import { auditEnvironment } from '$lib/server/audit';
|
||||||
import { refreshSubprocessEnvironments } from '$lib/server/subprocess-manager';
|
import { refreshSubprocessEnvironments } from '$lib/server/subprocess-manager';
|
||||||
import { resetHostDetection, detectHostDataDir } from '$lib/server/host-path';
|
import { resetHostDetection, detectHostDataDir } from '$lib/server/host-path';
|
||||||
|
import { redactEnvironment } from '$lib/server/environment-redact';
|
||||||
import { serializeLabels, parseLabels, MAX_LABELS } from '$lib/utils/label-colors';
|
import { serializeLabels, parseLabels, MAX_LABELS } from '$lib/utils/label-colors';
|
||||||
import { cleanPem } from '$lib/utils/pem';
|
import { cleanPem } from '$lib/utils/pem';
|
||||||
import { validateEnvName } from '$lib/utils/env-name';
|
import { validateEnvName } from '$lib/utils/env-name';
|
||||||
@@ -22,7 +23,8 @@ import { deleteEnvironmentIcon } from '$lib/server/env-icons';
|
|||||||
* @openapi
|
* @openapi
|
||||||
* summary: Get a single environment by id, including its parsed labels and public IP
|
* summary: Get a single environment by id, including its parsed labels and public IP
|
||||||
* path: id:integer! Environment id (from GET /api/environments)
|
* path: id:integer! Environment id (from GET /api/environments)
|
||||||
* resp-200: {id:integer!, name:string!, connectionType:string!, labels:array<string>, publicIp:string}
|
* resp-200: {id:integer!, name:string!, connectionType:string!, labels:array<string>, publicIp:string, hasTlsKey:boolean, hasHawserToken:boolean}
|
||||||
|
* resp-200-desc: The tlsKey (private TLS client key) and hawserToken secrets are NEVER returned; hasTlsKey / hasHawserToken indicate whether one is stored.
|
||||||
* resp-403: Permission denied (RBAC 'environments:view' missing)
|
* resp-403: Permission denied (RBAC 'environments:view' missing)
|
||||||
* resp-404: Environment not found
|
* resp-404: Environment not found
|
||||||
* resp-500: Unexpected error while loading the environment
|
* resp-500: Unexpected error while loading the environment
|
||||||
@@ -47,7 +49,7 @@ export const GET: RequestHandler = async ({ params, cookies }) => {
|
|||||||
|
|
||||||
// Parse labels from JSON string to array
|
// Parse labels from JSON string to array
|
||||||
return json({
|
return json({
|
||||||
...env,
|
...redactEnvironment(env),
|
||||||
labels: parseLabels(env.labels as string | null),
|
labels: parseLabels(env.labels as string | null),
|
||||||
publicIp
|
publicIp
|
||||||
});
|
});
|
||||||
@@ -62,8 +64,10 @@ export const GET: RequestHandler = async ({ params, cookies }) => {
|
|||||||
* summary: Update an environment; renaming also renames its on-disk stacks/git-repos directories
|
* summary: Update an environment; renaming also renames its on-disk stacks/git-repos directories
|
||||||
* path: id:integer! Environment id (from GET /api/environments)
|
* path: id:integer! Environment id (from GET /api/environments)
|
||||||
* body: {name:string, host:string, port:integer, protocol:string, tlsCa:string, tlsCert:string, tlsKey:string, tlsSkipVerify:boolean, icon:string, socketPath:string, collectActivity:boolean, collectMetrics:boolean, highlightChanges:boolean, labels:string, connectionType:string, hawserToken:string, publicIp:string}
|
* body: {name:string, host:string, port:integer, protocol:string, tlsCa:string, tlsCert:string, tlsKey:string, tlsSkipVerify:boolean, icon:string, socketPath:string, collectActivity:boolean, collectMetrics:boolean, highlightChanges:boolean, labels:string, connectionType:string, hawserToken:string, publicIp:string}
|
||||||
|
* body-desc: tlsKey/hawserToken are write-only - a blank value keeps the stored secret; a non-blank value replaces it.
|
||||||
* body-example: {"name":"hhdocker03","collectMetrics":true}
|
* body-example: {"name":"hhdocker03","collectMetrics":true}
|
||||||
* resp-200: {id:integer!, name:string!, connectionType:string!, labels:array<string>, publicIp:string}
|
* resp-200: {id:integer!, name:string!, connectionType:string!, labels:array<string>, publicIp:string, hasTlsKey:boolean, hasHawserToken:boolean}
|
||||||
|
* resp-200-desc: The tlsKey/hawserToken secrets are NEVER returned; hasTlsKey / hasHawserToken indicate whether one is stored.
|
||||||
* resp-400: Invalid new name (rename validation)
|
* resp-400: Invalid new name (rename validation)
|
||||||
* resp-403: Permission denied (RBAC 'environments:edit' missing)
|
* resp-403: Permission denied (RBAC 'environments:edit' missing)
|
||||||
* resp-404: Environment not found
|
* resp-404: Environment not found
|
||||||
@@ -155,6 +159,13 @@ export const PUT: RequestHandler = async (event) => {
|
|||||||
? serializeLabels(Array.isArray(data.labels) ? data.labels.slice(0, MAX_LABELS) : [])
|
? serializeLabels(Array.isArray(data.labels) ? data.labels.slice(0, MAX_LABELS) : [])
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
// The GET/list responses never return the tlsKey / hawserToken secrets, so the
|
||||||
|
// edit form can't round-trip them. A blank value therefore means "keep the stored
|
||||||
|
// secret" (pass undefined so updateEnvironment leaves the column untouched); a
|
||||||
|
// non-blank value replaces it. Same "leave blank to keep" pattern as registries /
|
||||||
|
// git credentials. (Removing a secret entirely is done by deleting the env.)
|
||||||
|
const cleanedTlsKey = cleanPem(data.tlsKey);
|
||||||
|
const trimmedToken = typeof data.hawserToken === 'string' ? data.hawserToken.trim() : data.hawserToken;
|
||||||
const env = await updateEnvironment(id, {
|
const env = await updateEnvironment(id, {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
host: data.host,
|
host: data.host,
|
||||||
@@ -162,7 +173,7 @@ export const PUT: RequestHandler = async (event) => {
|
|||||||
protocol: data.protocol,
|
protocol: data.protocol,
|
||||||
tlsCa: cleanPem(data.tlsCa),
|
tlsCa: cleanPem(data.tlsCa),
|
||||||
tlsCert: cleanPem(data.tlsCert),
|
tlsCert: cleanPem(data.tlsCert),
|
||||||
tlsKey: cleanPem(data.tlsKey),
|
tlsKey: cleanedTlsKey || undefined,
|
||||||
tlsSkipVerify: data.tlsSkipVerify,
|
tlsSkipVerify: data.tlsSkipVerify,
|
||||||
icon: data.icon,
|
icon: data.icon,
|
||||||
socketPath: data.socketPath,
|
socketPath: data.socketPath,
|
||||||
@@ -171,7 +182,7 @@ export const PUT: RequestHandler = async (event) => {
|
|||||||
highlightChanges: data.highlightChanges,
|
highlightChanges: data.highlightChanges,
|
||||||
labels: labels,
|
labels: labels,
|
||||||
connectionType: data.connectionType,
|
connectionType: data.connectionType,
|
||||||
hawserToken: data.hawserToken
|
hawserToken: trimmedToken || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!env) {
|
if (!env) {
|
||||||
@@ -207,7 +218,7 @@ export const PUT: RequestHandler = async (event) => {
|
|||||||
|
|
||||||
// Parse labels from JSON string to array
|
// Parse labels from JSON string to array
|
||||||
return json({
|
return json({
|
||||||
...env,
|
...redactEnvironment(env),
|
||||||
labels: parseLabels(env.labels as string | null),
|
labels: parseLabels(env.labels as string | null),
|
||||||
publicIp
|
publicIp
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -121,7 +121,9 @@
|
|||||||
formProtocol = env.protocol;
|
formProtocol = env.protocol;
|
||||||
formTlsCa = env.tlsCa || '';
|
formTlsCa = env.tlsCa || '';
|
||||||
formTlsCert = env.tlsCert || '';
|
formTlsCert = env.tlsCert || '';
|
||||||
formTlsKey = env.tlsKey || '';
|
// The API never returns the tlsKey secret; leave blank on edit (a blank submit
|
||||||
|
// keeps the stored key via `tlsKey: formTlsKey || undefined` below).
|
||||||
|
formTlsKey = '';
|
||||||
formError = '';
|
formError = '';
|
||||||
formSaving = false;
|
formSaving = false;
|
||||||
showEditModal = true;
|
showEditModal = true;
|
||||||
|
|||||||
@@ -109,7 +109,10 @@
|
|||||||
protocol: string;
|
protocol: string;
|
||||||
tlsCa?: string;
|
tlsCa?: string;
|
||||||
tlsCert?: string;
|
tlsCert?: string;
|
||||||
tlsKey?: string;
|
// tlsKey / hawserToken are never returned by the API (write-only secrets); the
|
||||||
|
// has* flags say whether one is stored.
|
||||||
|
hasTlsKey?: boolean;
|
||||||
|
hasHawserToken?: boolean;
|
||||||
tlsSkipVerify?: boolean;
|
tlsSkipVerify?: boolean;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
socketPath?: string;
|
socketPath?: string;
|
||||||
@@ -122,6 +125,7 @@
|
|||||||
hawserAgentName?: string;
|
hawserAgentName?: string;
|
||||||
hawserVersion?: string;
|
hawserVersion?: string;
|
||||||
hawserCapabilities?: string;
|
hawserCapabilities?: string;
|
||||||
|
labels?: string;
|
||||||
publicIp?: string;
|
publicIp?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -281,6 +285,9 @@
|
|||||||
let formTlsCa = $state('');
|
let formTlsCa = $state('');
|
||||||
let formTlsCert = $state('');
|
let formTlsCert = $state('');
|
||||||
let formTlsKey = $state('');
|
let formTlsKey = $state('');
|
||||||
|
// Whether the environment already has a stored tlsKey (the value itself is never
|
||||||
|
// returned by the API). Drives the "leave blank to keep" hint on the field.
|
||||||
|
let hasStoredTlsKey = $state(false);
|
||||||
let formTlsSkipVerify = $state(false);
|
let formTlsSkipVerify = $state(false);
|
||||||
let formIcon = $state('globe');
|
let formIcon = $state('globe');
|
||||||
let pendingIconData = $state<string | null>(null);
|
let pendingIconData = $state<string | null>(null);
|
||||||
@@ -303,6 +310,8 @@
|
|||||||
const usesStackPath = (ct: ConnectionType) => ct === 'direct' || ct === 'hawser-standard' || ct === 'hawser-edge';
|
const usesStackPath = (ct: ConnectionType) => ct === 'direct' || ct === 'hawser-standard' || ct === 'hawser-edge';
|
||||||
const isHawserConn = (ct: ConnectionType) => ct === 'hawser-standard' || ct === 'hawser-edge';
|
const isHawserConn = (ct: ConnectionType) => ct === 'hawser-standard' || ct === 'hawser-edge';
|
||||||
let formHawserToken = $state('');
|
let formHawserToken = $state('');
|
||||||
|
// Whether a hawser token is already stored (value never returned by the API).
|
||||||
|
let hasStoredHawserToken = $state(false);
|
||||||
let formLabels = $state<string[]>([]);
|
let formLabels = $state<string[]>([]);
|
||||||
let newLabelInput = $state('');
|
let newLabelInput = $state('');
|
||||||
let showLabelDropdown = $state(false);
|
let showLabelDropdown = $state(false);
|
||||||
@@ -578,7 +587,11 @@
|
|||||||
formProtocol = environment.protocol;
|
formProtocol = environment.protocol;
|
||||||
formTlsCa = environment.tlsCa || '';
|
formTlsCa = environment.tlsCa || '';
|
||||||
formTlsCert = environment.tlsCert || '';
|
formTlsCert = environment.tlsCert || '';
|
||||||
formTlsKey = environment.tlsKey || '';
|
// The API never returns the tlsKey / hawserToken secrets (they're stripped
|
||||||
|
// server-side). Start blank and remember whether one is stored so the field
|
||||||
|
// can say "leave blank to keep"; a blank submit keeps the existing secret.
|
||||||
|
formTlsKey = '';
|
||||||
|
hasStoredTlsKey = !!environment.hasTlsKey;
|
||||||
formTlsSkipVerify = environment.tlsSkipVerify ?? false;
|
formTlsSkipVerify = environment.tlsSkipVerify ?? false;
|
||||||
formIcon = environment.icon || 'globe';
|
formIcon = environment.icon || 'globe';
|
||||||
formSocketPath = environment.socketPath || '/var/run/docker.sock';
|
formSocketPath = environment.socketPath || '/var/run/docker.sock';
|
||||||
@@ -586,7 +599,8 @@
|
|||||||
formCollectMetrics = environment.collectMetrics ?? true;
|
formCollectMetrics = environment.collectMetrics ?? true;
|
||||||
formHighlightChanges = environment.highlightChanges ?? true;
|
formHighlightChanges = environment.highlightChanges ?? true;
|
||||||
formConnectionType = (environment.connectionType as ConnectionType) || 'socket';
|
formConnectionType = (environment.connectionType as ConnectionType) || 'socket';
|
||||||
formHawserToken = environment.hawserToken || '';
|
formHawserToken = '';
|
||||||
|
hasStoredHawserToken = !!environment.hasHawserToken;
|
||||||
formLabels = parseLabels(environment.labels);
|
formLabels = parseLabels(environment.labels);
|
||||||
newLabelInput = '';
|
newLabelInput = '';
|
||||||
formPublicIp = environment.publicIp || '';
|
formPublicIp = environment.publicIp || '';
|
||||||
@@ -620,6 +634,7 @@
|
|||||||
formTlsCa = '';
|
formTlsCa = '';
|
||||||
formTlsCert = '';
|
formTlsCert = '';
|
||||||
formTlsKey = '';
|
formTlsKey = '';
|
||||||
|
hasStoredTlsKey = false;
|
||||||
formTlsSkipVerify = false;
|
formTlsSkipVerify = false;
|
||||||
formIcon = 'globe';
|
formIcon = 'globe';
|
||||||
pendingIconData = null;
|
pendingIconData = null;
|
||||||
@@ -633,6 +648,7 @@
|
|||||||
formDiskWarningThresholdGb = 50;
|
formDiskWarningThresholdGb = 50;
|
||||||
formConnectionType = 'socket';
|
formConnectionType = 'socket';
|
||||||
formHawserToken = '';
|
formHawserToken = '';
|
||||||
|
hasStoredHawserToken = false;
|
||||||
formLabels = [];
|
formLabels = [];
|
||||||
newLabelInput = '';
|
newLabelInput = '';
|
||||||
formPublicIp = '';
|
formPublicIp = '';
|
||||||
@@ -2137,7 +2153,7 @@
|
|||||||
<textarea
|
<textarea
|
||||||
id="edit-env-tls_key"
|
id="edit-env-tls_key"
|
||||||
bind:value={formTlsKey}
|
bind:value={formTlsKey}
|
||||||
placeholder="-----BEGIN PRIVATE KEY-----"
|
placeholder={hasStoredTlsKey ? 'Configured - leave blank to keep, or paste a new key to replace it' : '-----BEGIN PRIVATE KEY-----'}
|
||||||
class="flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
class="flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
@@ -2245,7 +2261,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Input id="edit-env-hawser-token" type="password" bind:value={formHawserToken} placeholder="Token for agent authentication" oninput={() => generatedStandardToken = null} />
|
<Input id="edit-env-hawser-token" type="password" bind:value={formHawserToken} placeholder={hasStoredHawserToken ? 'Configured - leave blank to keep, or enter a new token to replace it' : 'Token for agent authentication'} oninput={() => generatedStandardToken = null} />
|
||||||
{#if generatedStandardToken}
|
{#if generatedStandardToken}
|
||||||
<div class="p-3 bg-amber-50 dark:bg-amber-950/30 border border-amber-300 dark:border-amber-700 rounded-md space-y-2">
|
<div class="p-3 bg-amber-50 dark:bg-amber-950/30 border border-amber-300 dark:border-amber-700 rounded-md space-y-2">
|
||||||
<p class="text-xs font-medium text-amber-700 dark:text-amber-400 flex items-center gap-1">
|
<p class="text-xs font-medium text-amber-700 dark:text-amber-400 flex items-center gap-1">
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* redactEnvironment: strip the tlsKey / hawserToken secrets from an environment before
|
||||||
|
* it leaves the server, replacing them with hasTlsKey / hasHawserToken booleans.
|
||||||
|
* The private TLS client key authenticates straight to the Docker daemon, so it must
|
||||||
|
* never appear in an API response.
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from 'bun:test';
|
||||||
|
import { redactEnvironment } from '../src/lib/server/environment-redact';
|
||||||
|
|
||||||
|
describe('redactEnvironment', () => {
|
||||||
|
it('removes tlsKey and hawserToken from the output', () => {
|
||||||
|
const out = redactEnvironment({
|
||||||
|
id: 1,
|
||||||
|
name: 'prod',
|
||||||
|
tlsKey: '-----BEGIN PRIVATE KEY-----\nSECRET\n-----END PRIVATE KEY-----',
|
||||||
|
hawserToken: 'super-secret-token',
|
||||||
|
tlsCa: 'CA',
|
||||||
|
tlsCert: 'CERT'
|
||||||
|
} as any);
|
||||||
|
expect('tlsKey' in out).toBe(false);
|
||||||
|
expect('hawserToken' in out).toBe(false);
|
||||||
|
// the JSON serialization a client would see must not contain the secret text
|
||||||
|
expect(JSON.stringify(out)).not.toContain('SECRET');
|
||||||
|
expect(JSON.stringify(out)).not.toContain('super-secret-token');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets hasTlsKey / hasHawserToken true when a secret is present', () => {
|
||||||
|
const out = redactEnvironment({ id: 1, name: 'p', tlsKey: 'k', hawserToken: 't' } as any);
|
||||||
|
expect(out.hasTlsKey).toBe(true);
|
||||||
|
expect(out.hasHawserToken).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the flags false when the secret is null / empty', () => {
|
||||||
|
const out = redactEnvironment({ id: 1, name: 'p', tlsKey: null, hawserToken: '' } as any);
|
||||||
|
expect(out.hasTlsKey).toBe(false);
|
||||||
|
expect(out.hasHawserToken).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the public tlsCa / tlsCert and all other fields', () => {
|
||||||
|
const out = redactEnvironment({
|
||||||
|
id: 7,
|
||||||
|
name: 'p',
|
||||||
|
tlsCa: 'CA-PUBLIC',
|
||||||
|
tlsCert: 'CERT-PUBLIC',
|
||||||
|
connectionType: 'direct',
|
||||||
|
tlsKey: 'k',
|
||||||
|
hawserToken: 't'
|
||||||
|
} as any) as any;
|
||||||
|
expect(out.tlsCa).toBe('CA-PUBLIC');
|
||||||
|
expect(out.tlsCert).toBe('CERT-PUBLIC');
|
||||||
|
expect(out.connectionType).toBe('direct');
|
||||||
|
expect(out.id).toBe(7);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user