mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Harden release integration bootstrap gate
This commit is contained in:
parent
6169f3cea5
commit
377fd5131d
4 changed files with 168 additions and 3 deletions
15
.github/workflows/create-release.yml
vendored
15
.github/workflows/create-release.yml
vendored
|
|
@ -9,7 +9,7 @@ on:
|
|||
required: true
|
||||
type: string
|
||||
release_notes:
|
||||
description: 'Release notes (markdown) - generated by Claude'
|
||||
description: 'Release notes (markdown)'
|
||||
required: true
|
||||
type: string
|
||||
promoted_from_tag:
|
||||
|
|
@ -529,6 +529,19 @@ jobs:
|
|||
|
||||
node scripts/apply-entitlement-profile.mjs
|
||||
|
||||
echo "Validating seeded bootstrap token..."
|
||||
BOOTSTRAP_STATUS=$(curl -s -o /tmp/bootstrap-token-validation.txt -w "%{http_code}" \
|
||||
-X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{\"token\":\"${PULSE_E2E_BOOTSTRAP_TOKEN}\"}" \
|
||||
http://localhost:7655/api/security/validate-bootstrap-token || true)
|
||||
echo "Bootstrap token validation endpoint returned HTTP ${BOOTSTRAP_STATUS}"
|
||||
if [ "${BOOTSTRAP_STATUS}" != "204" ]; then
|
||||
cat /tmp/bootstrap-token-validation.txt || true
|
||||
docker logs pulse-test-server || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Running update API route smoke check..."
|
||||
STATUS=$(curl -s -o /tmp/update-status.json -w "%{http_code}" http://localhost:7655/api/updates/status || true)
|
||||
echo "Update status endpoint returned HTTP ${STATUS}"
|
||||
|
|
|
|||
|
|
@ -6,10 +6,17 @@ services:
|
|||
container_name: pulse-test-seed-bootstrap-token
|
||||
environment:
|
||||
- PULSE_E2E_BOOTSTRAP_TOKEN=${PULSE_E2E_BOOTSTRAP_TOKEN:-0123456789abcdef0123456789abcdef0123456789abcdef}
|
||||
- PUID=${PUID:-1000}
|
||||
- PGID=${PGID:-1000}
|
||||
volumes:
|
||||
- test-data:/data
|
||||
command: >
|
||||
sh -c "set -e; umask 077; echo \"$$PULSE_E2E_BOOTSTRAP_TOKEN\" > /data/.bootstrap_token"
|
||||
sh -c "set -e;
|
||||
umask 077;
|
||||
printf '%s\n' \"$$PULSE_E2E_BOOTSTRAP_TOKEN\" > /data/.bootstrap_token;
|
||||
chown \"$${PUID:-1000}:$${PGID:-1000}\" /data /data/.bootstrap_token;
|
||||
chmod 700 /data;
|
||||
chmod 600 /data/.bootstrap_token"
|
||||
networks:
|
||||
- test-network
|
||||
|
||||
|
|
@ -59,6 +66,8 @@ services:
|
|||
- PULSE_MULTI_TENANT_ENABLED=${PULSE_MULTI_TENANT_ENABLED:-false}
|
||||
- PULSE_ALLOW_DOCKER_UPDATES=true
|
||||
- PULSE_UPDATE_STAGE_DELAY_MS=250
|
||||
- PUID=${PUID:-1000}
|
||||
- PGID=${PGID:-1000}
|
||||
volumes:
|
||||
- test-data:/data
|
||||
extra_hosts:
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ const truthy = (value) => {
|
|||
if (!value) return false;
|
||||
return ['1', 'true', 'yes', 'on'].includes(String(value).trim().toLowerCase());
|
||||
};
|
||||
const trim = (value) => String(value ?? '').trim();
|
||||
|
||||
const shouldSkipDocker = truthy(process.env.PULSE_E2E_SKIP_DOCKER);
|
||||
const shouldSkipPlaywrightInstall = truthy(process.env.PULSE_E2E_SKIP_PLAYWRIGHT_INSTALL);
|
||||
|
|
@ -145,6 +146,54 @@ export const waitForHealth = async (
|
|||
throw new Error(`Timed out waiting for ${healthURL} after ${attempt} attempts`);
|
||||
};
|
||||
|
||||
export const validateBootstrapToken = async (
|
||||
baseURL,
|
||||
bootstrapToken,
|
||||
{ fetchImpl = fetch } = {},
|
||||
) => {
|
||||
const token = trim(bootstrapToken);
|
||||
if (token === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetchImpl(`${baseURL.replace(/\/+$/, '')}/api/security/validate-bootstrap-token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ token }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const detail = typeof response.text === 'function' ? await response.text() : '';
|
||||
throw new Error(
|
||||
`Bootstrap token validation failed: HTTP ${response.status}${detail ? ` ${detail}` : ''}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const validateBootstrapTokenForFirstRun = async (
|
||||
baseURL,
|
||||
bootstrapToken,
|
||||
{ fetchImpl = fetch } = {},
|
||||
) => {
|
||||
const token = trim(bootstrapToken);
|
||||
if (token === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedBaseURL = baseURL.replace(/\/+$/, '');
|
||||
const statusResponse = await fetchImpl(`${normalizedBaseURL}/api/security/status`);
|
||||
if (statusResponse.ok && typeof statusResponse.json === 'function') {
|
||||
const status = await statusResponse.json().catch(() => null);
|
||||
if (status?.hasAuthentication !== false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await validateBootstrapToken(normalizedBaseURL, token, { fetchImpl });
|
||||
};
|
||||
|
||||
export const main = async () => {
|
||||
if (!shouldSkipPlaywrightInstall) {
|
||||
await run(npxCmd, ['playwright', 'install', 'chromium']);
|
||||
|
|
@ -191,6 +240,7 @@ export const main = async () => {
|
|||
try {
|
||||
await waitForHealth(`${baseURL}/api/health`);
|
||||
console.log('[pretest] Health check passed!');
|
||||
await validateBootstrapTokenForFirstRun(baseURL, process.env.PULSE_E2E_BOOTSTRAP_TOKEN);
|
||||
await applyRequestedEntitlementProfile();
|
||||
} catch (error) {
|
||||
console.error('[pretest] Health check failed:', error.message);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { resolveHealthCheckStrategy } from './pretest.mjs';
|
||||
import {
|
||||
resolveHealthCheckStrategy,
|
||||
validateBootstrapToken,
|
||||
validateBootstrapTokenForFirstRun,
|
||||
} from './pretest.mjs';
|
||||
|
||||
test('resolveHealthCheckStrategy uses plain HTTP mode for HTTP health URLs', async () => {
|
||||
const strategy = await resolveHealthCheckStrategy('http://localhost:7655/api/health');
|
||||
|
|
@ -49,3 +53,92 @@ test('resolveHealthCheckStrategy scopes insecure HTTPS mode to curl when availab
|
|||
assert.equal(strategy.mode, 'curl');
|
||||
assert.equal(strategy.parsedURL.protocol, 'https:');
|
||||
});
|
||||
|
||||
test('validateBootstrapToken posts the deterministic token to the public validation route', async () => {
|
||||
const calls = [];
|
||||
await validateBootstrapToken('http://localhost:7655/', ' token-123 ', {
|
||||
fetchImpl: async (url, options) => {
|
||||
calls.push({ url, options });
|
||||
return {
|
||||
ok: true,
|
||||
status: 204,
|
||||
text: async () => '',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
{
|
||||
url: 'http://localhost:7655/api/security/validate-bootstrap-token',
|
||||
options: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ token: 'token-123' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('validateBootstrapToken fails with response detail when validation is rejected', async () => {
|
||||
await assert.rejects(
|
||||
() => validateBootstrapToken('http://localhost:7655', 'bad-token', {
|
||||
fetchImpl: async () => ({
|
||||
ok: false,
|
||||
status: 409,
|
||||
text: async () => 'Bootstrap token unavailable',
|
||||
}),
|
||||
}),
|
||||
/HTTP 409 Bootstrap token unavailable/,
|
||||
);
|
||||
});
|
||||
|
||||
test('validateBootstrapTokenForFirstRun skips validation once auth is configured', async () => {
|
||||
const calls = [];
|
||||
await validateBootstrapTokenForFirstRun('http://localhost:7655', 'token-123', {
|
||||
fetchImpl: async (url, options) => {
|
||||
calls.push({ url, options });
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ hasAuthentication: true }),
|
||||
text: async () => '',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
{
|
||||
url: 'http://localhost:7655/api/security/status',
|
||||
options: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('validateBootstrapTokenForFirstRun validates the seed token before first-run setup', async () => {
|
||||
const calls = [];
|
||||
await validateBootstrapTokenForFirstRun('http://localhost:7655', 'token-123', {
|
||||
fetchImpl: async (url, options) => {
|
||||
calls.push({ url, options });
|
||||
if (url.endsWith('/api/security/status')) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ hasAuthentication: false }),
|
||||
text: async () => '',
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
status: 204,
|
||||
text: async () => '',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(calls.length, 2);
|
||||
assert.equal(calls[0].url, 'http://localhost:7655/api/security/status');
|
||||
assert.equal(calls[1].url, 'http://localhost:7655/api/security/validate-bootstrap-token');
|
||||
assert.equal(calls[1].options.body, JSON.stringify({ token: 'token-123' }));
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue