-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Plan 10 PR A — bootstrap registry seed + is_bootstrap derivation #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
19 changes: 19 additions & 0 deletions
19
control-plane/migrations/015_bootstrap_removal/migration.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| -- control-plane/migrations/015_bootstrap_removal/migration.sql | ||
|
|
||
| -- Plan 10 PR A: explicit removal state for bootstrap issuers so a seed rerun | ||
| -- (insert-only manifest) never reinstates an issuer de-emphasized/removed by | ||
| -- staff, and is_bootstrap derivation can exclude removed rows. | ||
| ALTER TABLE bootstrap_issuers | ||
| ADD COLUMN removed_from_registry_at TIMESTAMPTZ; | ||
|
|
||
| -- Recalculate is_bootstrap for existing rows under the new semantics so the | ||
| -- derived flag is correct immediately (no reliance on a later seed/PATCH): | ||
| -- true exactly for registry members that are not removed and still carry a | ||
| -- root weight > 0. Mirrors deriveIsBootstrap in bootstrapSeeder.ts. | ||
| UPDATE issuers i | ||
| SET is_bootstrap = EXISTS ( | ||
| SELECT 1 FROM bootstrap_issuers b | ||
| WHERE b.principal_id = i.principal_id | ||
| AND b.removed_from_registry_at IS NULL | ||
| AND b.current_weight > 0 | ||
| ); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
238 changes: 238 additions & 0 deletions
238
control-plane/src/__tests__/integration/bootstrap-seed.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,238 @@ | ||
| // control-plane/src/__tests__/integration/bootstrap-seed.test.ts | ||
| process.env.DATABASE_URL ||= | ||
| 'postgresql://verilink:verilink@127.0.0.1:15432/verilink_test'; | ||
| process.env.API_KEY_HMAC_SECRET ||= 'test-hmac-secret-for-integration'; | ||
|
|
||
| import { describe, it, before, after, beforeEach } from 'node:test'; | ||
| import assert from 'node:assert/strict'; | ||
| import type pg from 'pg'; | ||
| import { setupTestDb, teardownTestDb, resetTestData } from '../../testutil/testDb.js'; | ||
| import { | ||
| seedTenant, | ||
| seedApiKey, | ||
| authHeaders, | ||
| } from '../../testutil/seedData.js'; | ||
| import { startControlPlane, type ControlPlaneHarness } from '../../testutil/appHarness.js'; | ||
| import { SEED_ISSUERS } from '../../domains/bootstrap/seedManifest.js'; | ||
|
|
||
| /** | ||
| * Plan 10 PR A: idempotent bootstrap seed, is_bootstrap derivation, PATCH | ||
| * removal survival across reruns, root-weight write-through to the graph | ||
| * loader, and manifest-conflict abort. All seeded rows use the app pool | ||
| * (dynamic imports after env setup). | ||
| */ | ||
| describe('Bootstrap Seed Integration', () => { | ||
| let pool: pg.Pool; | ||
| let harness: ControlPlaneHarness; | ||
|
|
||
| // Dynamic imports so db/client.ts evaluates after DATABASE_URL is set. | ||
| let seedBootstrapRegistry: () => Promise<{ issuers: number; roots: number }>; | ||
| let loadAttestationGraph: (evaluationTime: Date) => Promise<{ | ||
| roots: Array<{ id: string; weight: number }>; | ||
| }>; | ||
|
|
||
| before(async () => { | ||
| pool = await setupTestDb(); | ||
| harness = await startControlPlane(); | ||
| ({ seedBootstrapRegistry } = await import('../../domains/bootstrap/bootstrapSeeder.js')); | ||
| ({ loadAttestationGraph } = await import('../../domains/graph/attestationGraphLoader.js')); | ||
| }); | ||
|
|
||
| after(async () => { | ||
| await harness.stop(); | ||
| await teardownTestDb(pool); | ||
| }); | ||
|
|
||
| beforeEach(async () => { | ||
| await resetTestData(pool); | ||
| }); | ||
|
|
||
| async function seededState() { | ||
| const { rows } = await pool.query( | ||
| `SELECT | ||
| (SELECT count(*)::int FROM issuers WHERE is_bootstrap) AS bs_issuers, | ||
| (SELECT count(*)::int FROM bootstrap_issuers) AS bs_rows, | ||
| (SELECT count(*)::int FROM principals) AS principals, | ||
| (SELECT count(*)::int FROM principal_keys) AS keys` | ||
| ); | ||
| return rows[0]; | ||
| } | ||
|
|
||
| it('seed is idempotent with exact manifest identities', async () => { | ||
| const first = await seedBootstrapRegistry(); | ||
| assert.equal(first.issuers, SEED_ISSUERS.length); | ||
| assert.equal(first.roots, SEED_ISSUERS.length); | ||
|
|
||
| const { rows: seeded } = await pool.query( | ||
| `SELECT p.id, p.entity_kind, p.name, i.trust_weight::float AS trust_weight, | ||
| i.is_bootstrap, k.key_id, b.current_weight::float AS current_weight | ||
| FROM principals p | ||
| JOIN issuers i ON i.principal_id = p.id | ||
| JOIN principal_keys k ON k.principal_id = p.id | ||
| JOIN bootstrap_issuers b ON b.principal_id = p.id | ||
| ORDER BY p.id` | ||
| ); | ||
| assert.equal(seeded.length, SEED_ISSUERS.length); | ||
| for (const entry of SEED_ISSUERS) { | ||
| const row = seeded.find((r) => r.id === entry.id); | ||
| assert.ok(row, `seeded principal ${entry.id} present`); | ||
| assert.equal(row.entity_kind, entry.entityKind); | ||
| assert.equal(row.name, entry.name); | ||
| assert.equal(row.trust_weight, 1.0, 'trust_weight untouched by seed'); | ||
| assert.equal(row.is_bootstrap, true); | ||
| assert.equal(row.key_id, entry.keyId); | ||
| assert.equal(row.current_weight, 1.0); | ||
| } | ||
|
|
||
| const afterFirst = await seededState(); | ||
| assert.equal(afterFirst.bs_issuers, SEED_ISSUERS.length); | ||
| assert.equal(afterFirst.keys, SEED_ISSUERS.length); | ||
|
|
||
| const second = await seedBootstrapRegistry(); | ||
| assert.equal(second.issuers, SEED_ISSUERS.length); | ||
| assert.equal(second.roots, SEED_ISSUERS.length); | ||
| const afterSecond = await seededState(); | ||
| assert.deepEqual(afterSecond, afterFirst, 'rerun is a no-op'); | ||
| }); | ||
|
|
||
| it('PATCH removal clears is_bootstrap and seed rerun does not reinstate', async () => { | ||
| await seedBootstrapRegistry(); | ||
| const target = SEED_ISSUERS[0]; | ||
|
|
||
| const tenantA = await seedTenant(pool, `bs-rm-${Date.now()}`); | ||
| const staffKey = await seedApiKey(pool, tenantA.id, ['admin:read']); | ||
|
|
||
| const res = await fetch(`${harness.url}/v1/admin/bootstrap-issuers`, { | ||
| method: 'PATCH', | ||
| headers: { 'Content-Type': 'application/json', ...authHeaders(staffKey) }, | ||
| body: JSON.stringify({ principal_id: target.id, remove_from_registry: true }), | ||
| }); | ||
| assert.equal(res.status, 200, JSON.stringify(await res.json())); | ||
|
|
||
| const { rows: afterRemove } = await pool.query( | ||
| `SELECT i.is_bootstrap, b.removed_from_registry_at IS NOT NULL AS removed | ||
| FROM issuers i JOIN bootstrap_issuers b ON b.principal_id = i.principal_id | ||
| WHERE i.principal_id = $1`, | ||
| [target.id] | ||
| ); | ||
| assert.equal(afterRemove[0].is_bootstrap, false, 'removal clears is_bootstrap'); | ||
| assert.equal(afterRemove[0].removed, true); | ||
|
|
||
| await seedBootstrapRegistry(); | ||
|
|
||
| const { rows: afterRerun } = await pool.query( | ||
| `SELECT i.is_bootstrap, b.removed_from_registry_at IS NOT NULL AS removed | ||
| FROM issuers i JOIN bootstrap_issuers b ON b.principal_id = i.principal_id | ||
| WHERE i.principal_id = $1`, | ||
| [target.id] | ||
| ); | ||
| assert.equal(afterRerun[0].is_bootstrap, false, 'seed rerun does not reinstate removed issuer'); | ||
| assert.equal(afterRerun[0].removed, true, 'removal state preserved across rerun'); | ||
| }); | ||
|
|
||
| it('PATCH current_weight write-through: GraphRoot.weight changes, trust_weight stays 1.0', async () => { | ||
| await seedBootstrapRegistry(); | ||
| const target = SEED_ISSUERS[0]; | ||
|
|
||
| const tenantA = await seedTenant(pool, `bs-wt-${Date.now()}`); | ||
| const staffKey = await seedApiKey(pool, tenantA.id, ['admin:read']); | ||
|
|
||
| const res = await fetch(`${harness.url}/v1/admin/bootstrap-issuers`, { | ||
| method: 'PATCH', | ||
| headers: { 'Content-Type': 'application/json', ...authHeaders(staffKey) }, | ||
| body: JSON.stringify({ principal_id: target.id, current_weight: 0.5 }), | ||
| }); | ||
| assert.equal(res.status, 200, JSON.stringify(await res.json())); | ||
|
|
||
| const { rows: issuerRows } = await pool.query( | ||
| `SELECT trust_weight::float AS trust_weight FROM issuers WHERE principal_id = $1`, | ||
| [target.id] | ||
| ); | ||
| assert.equal(issuerRows[0].trust_weight, 1.0, 'trust_weight untouched by de-emphasis'); | ||
|
|
||
| const graph = await loadAttestationGraph(new Date()); | ||
| const root = graph.roots.find((r) => r.id === target.id); | ||
| assert.ok(root, 'seeded issuer is a graph root'); | ||
| assert.equal(root.weight, 0.5, 'GraphRoot.weight reflects PATCHed current_weight'); | ||
|
|
||
| const { rows: bsRows } = await pool.query( | ||
| `SELECT is_bootstrap FROM issuers WHERE principal_id = $1`, | ||
| [target.id] | ||
| ); | ||
| assert.equal(bsRows[0].is_bootstrap, true, 'positive weight keeps is_bootstrap true'); | ||
| }); | ||
|
|
||
| it('zero weight or removal drops the issuer from the graph roots', async () => { | ||
| await seedBootstrapRegistry(); | ||
| const target = SEED_ISSUERS[0]; | ||
|
|
||
| const tenantA = await seedTenant(pool, `bs-z-${Date.now()}`); | ||
| const staffKey = await seedApiKey(pool, tenantA.id, ['admin:read']); | ||
|
|
||
| await fetch(`${harness.url}/v1/admin/bootstrap-issuers`, { | ||
| method: 'PATCH', | ||
| headers: { 'Content-Type': 'application/json', ...authHeaders(staffKey) }, | ||
| body: JSON.stringify({ principal_id: target.id, current_weight: 0 }), | ||
| }); | ||
|
|
||
| const graph = await loadAttestationGraph(new Date()); | ||
| assert.ok( | ||
| !graph.roots.some((r) => r.id === target.id), | ||
| 'zero-weight root excluded from graph' | ||
| ); | ||
| }); | ||
|
|
||
| it('seed aborts on conflict and rolls back earlier inserts; no registry root created', async () => { | ||
| // Target a LATER manifest entry so earlier entries are inserted first and | ||
| // must be rolled back when the conflict aborts the transaction. | ||
| const target = SEED_ISSUERS[1]; | ||
| const earlier = SEED_ISSUERS[0]; | ||
|
|
||
| // Preload the manifest principal + issuer with a DIFFERENT key under the | ||
| // manifest key id (simulates a key rotation that diverged from the manifest). | ||
| await pool.query( | ||
| `INSERT INTO principals (id, entity_kind, name) VALUES ($1, $2, $3)`, | ||
| [target.id, target.entityKind, target.name] | ||
| ); | ||
| await pool.query(`INSERT INTO issuers (principal_id) VALUES ($1)`, [target.id]); | ||
| const otherRaw = Buffer.from('a'.repeat(32), 'utf8'); | ||
| await pool.query( | ||
| `INSERT INTO principal_keys | ||
| (principal_id, key_id, public_key_raw, public_key_jwk, key_hash, control_verified_at) | ||
| VALUES ($1, $2, $3, $4, $5, NOW() - INTERVAL '1 hour')`, | ||
| [ | ||
| target.id, | ||
| target.keyId, | ||
| otherRaw, | ||
| JSON.stringify({ kty: 'OKP', crv: 'Ed25519', x: 'a'.repeat(43) }), | ||
| 'deadbeef', | ||
| ] | ||
| ); | ||
|
|
||
| await assert.rejects( | ||
| () => seedBootstrapRegistry(), | ||
| /bootstrap seed conflict: key .* different public key/ | ||
| ); | ||
|
|
||
| const { rows } = await pool.query( | ||
| `SELECT count(*)::int AS n FROM bootstrap_issuers WHERE principal_id = ANY($1::text[])`, | ||
| [[earlier.id, target.id]] | ||
| ); | ||
| assert.equal(rows[0].n, 0, 'no registry root created for conflicting key or earlier entries (rolled back)'); | ||
| }); | ||
|
|
||
| it('seed is gated by BOOTSTRAP_SEED and refuses to run without it', async () => { | ||
| const { execFileSync } = await import('node:child_process'); | ||
| assert.throws(() => { | ||
| execFileSync('npx', ['tsx', 'src/scripts/seed-bootstrap.ts'], { | ||
| cwd: process.cwd(), | ||
| env: { | ||
| ...process.env, | ||
| DATABASE_URL: 'postgresql://verilink:verilink@127.0.0.1:15432/verilink_test', | ||
| BOOTSTRAP_SEED: '0', | ||
| }, | ||
| encoding: 'utf-8', | ||
| }); | ||
| }, /Refusing to seed without BOOTSTRAP_SEED=1/); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.