feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
+606
View File
@@ -0,0 +1,606 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const { QueryTypes, Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const {
PROJECT_ROLE_BINDING_TABLE,
PROJECT_TABLE,
projectPolicyMigration,
} = require('../../back/migrations/0017-project-policy');
const {
PROJECT_OWNER_BOOTSTRAP_CHALLENGE_ID_INDEX,
PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE,
PROJECT_OWNER_BOOTSTRAP_CURRENT_INDEX,
projectOwnerBootstrapMigration,
} = require('../../back/migrations/0018-project-owner-bootstrap');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizeProjectOwnerBootstrapRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/projectOwnerBootstrapRepository');
const {
LegacySequelizeProjectPolicyRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/projectPolicyRepository');
const {
ProjectOwnerBootstrapService,
} = require('../../back/runtime/application/projectOwnerBootstrapService');
const {
AuthenticatedPrincipalExpiredError,
normalizeAuthenticatedPrincipal,
} = require('../../back/runtime/domain/authenticatedPrincipal');
const {
ProjectOwnerBootstrapChallengeActiveError,
ProjectOwnerBootstrapClaimRejectedError,
ProjectOwnerBootstrapProjectInactiveError,
ProjectOwnerBootstrapProjectNotFoundError,
ProjectOwnerBootstrapProjectNotPristineError,
ProjectOwnerBootstrapUnauthorizedError,
ProjectOwnerBootstrapUnavailableError,
} = require('../../back/runtime/domain/projectOwnerBootstrap');
const PROJECT_ID = 'default';
const NOW = 100_000;
const TTL_MS = 60_000;
function principal(subject = { type: 'user', id: 'owner-1' }, overrides = {}) {
return {
subject,
authenticationId: 'auth-1',
authenticatedAtMs: 0,
expiresAtMs: NOW + TTL_MS * 10,
assurance: 'multi_factor',
...overrides,
};
}
function localConsolePrincipal(overrides = {}) {
return principal(
{ type: 'system', id: 'owner-bootstrap' },
{ assurance: 'local_console', ...overrides },
);
}
function deterministicRandomSource(seed = 1) {
let next = seed;
return {
bytes(size) {
const value = Buffer.alloc(size, next);
next += 1;
return value;
},
};
}
async function setup(t, storage = ':memory:') {
const database = new Sequelize({
dialect: 'sqlite',
storage,
logging: false,
});
t.after(() => database.close());
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations: [projectPolicyMigration, projectOwnerBootstrapMigration],
logger: { info() {} },
});
const repository = new LegacySequelizeProjectOwnerBootstrapRepository(
database,
);
return {
database,
repository,
service: new ProjectOwnerBootstrapService(
repository,
deterministicRandomSource(),
),
};
}
test('normalizes an exact, bounded and live authenticated principal', () => {
const value = principal();
assert.deepEqual(normalizeAuthenticatedPrincipal(value), value);
assert.throws(
() => normalizeAuthenticatedPrincipal({ ...value, scopes: ['*'] }),
/shape is invalid/,
);
assert.throws(
() =>
normalizeAuthenticatedPrincipal({
...value,
authenticationId: 'auth with spaces',
}),
/authenticationId is invalid/,
);
assert.throws(
() =>
normalizeAuthenticatedPrincipal({
...value,
expiresAtMs: value.authenticatedAtMs,
}),
/lifetime is invalid/,
);
});
test('migration records versioned challenges and both bounded lookup indexes', async (t) => {
const { database, service } = await setup(t);
const issued = await service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
});
assert.match(issued.challengeId, /^[A-Za-z0-9_-]{22}$/);
assert.match(issued.token, /^[A-Za-z0-9_-]{43}$/);
assert.equal(issued.expiresAtMs, NOW + TTL_MS);
const rows = await database
.getQueryInterface()
.select(null, PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE);
assert.equal(rows.length, 1);
assert.equal(rows[0].version, 1);
assert.equal(rows[0].challenge_id, issued.challengeId);
assert.match(rows[0].token_digest, /^[0-9a-f]{64}$/);
assert.notEqual(rows[0].token_digest, issued.token);
assert.equal(JSON.stringify(rows).includes(issued.token), false);
assert.equal(rows[0].consumed_at_ms, null);
assert.equal(
(
await database
.getQueryInterface()
.select(null, PROJECT_ROLE_BINDING_TABLE)
).length,
0,
);
const indexes = new Set(
(
await database
.getQueryInterface()
.showIndex(PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE)
).map((index) => index.name),
);
assert.ok(indexes.has(PROJECT_OWNER_BOOTSTRAP_CHALLENGE_ID_INDEX));
assert.ok(indexes.has(PROJECT_OWNER_BOOTSTRAP_CURRENT_INDEX));
});
test('only an active local-console bootstrap principal can issue', async (t) => {
const { service } = await setup(t);
for (const issuer of [
principal(),
localConsolePrincipal({ assurance: 'service' }),
principal(
{ type: 'system', id: 'different-system' },
{ assurance: 'local_console' },
),
]) {
await assert.rejects(
service.issue({
projectId: PROJECT_ID,
issuer,
nowMs: NOW,
ttlMs: TTL_MS,
}),
ProjectOwnerBootstrapUnauthorizedError,
);
}
await assert.rejects(
service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal({ expiresAtMs: NOW }),
nowMs: NOW,
ttlMs: TTL_MS,
}),
AuthenticatedPrincipalExpiredError,
);
await assert.rejects(
service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
subject: { type: 'user', id: 'injected-owner' },
}),
/request shape is invalid/,
);
});
test('does not replace a live challenge and versions a replacement only after expiry', async (t) => {
const { database, service } = await setup(t);
const request = {
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
};
const first = await service.issue(request);
await assert.rejects(
service.issue({ ...request, nowMs: NOW + 1 }),
ProjectOwnerBootstrapChallengeActiveError,
);
const second = await service.issue({
...request,
nowMs: NOW + TTL_MS,
});
assert.notEqual(second.challengeId, first.challengeId);
const rows = await database.query(
`SELECT version, challenge_id FROM "${PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE}" ORDER BY version`,
{ type: QueryTypes.SELECT },
);
assert.deepEqual(rows, [
{ version: 1, challenge_id: first.challengeId },
{ version: 2, challenge_id: second.challengeId },
]);
await assert.rejects(
service.claim({
projectId: PROJECT_ID,
challengeId: first.challengeId,
token: first.token,
principal: principal(),
nowMs: NOW + TTL_MS + 1,
}),
ProjectOwnerBootstrapClaimRejectedError,
);
});
test('claims one owner atomically and permits only exact idempotent replay', async (t) => {
const { database, service } = await setup(t);
const issued = await service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
});
const request = {
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: issued.token,
principal: principal(),
nowMs: NOW + 1,
};
const claimed = await service.claim(request);
assert.equal(claimed.status, 'claimed');
assert.deepEqual(claimed.binding, {
projectId: PROJECT_ID,
subject: { type: 'user', id: 'owner-1' },
version: 1,
state: 'active',
role: 'owner',
mutationId: `owner-bootstrap:${issued.challengeId}`,
changedBy: { type: 'system', id: 'owner-bootstrap' },
createdAtMs: NOW + 1,
});
const replay = await service.claim({ ...request, nowMs: NOW + 2 });
assert.equal(replay.status, 'existing');
assert.deepEqual(replay.binding, claimed.binding);
const policyRepository = new LegacySequelizeProjectPolicyRepository(database);
assert.deepEqual(
(await policyRepository.resolve(PROJECT_ID, principal().subject)).binding,
claimed.binding,
);
const challenge = (
await database
.getQueryInterface()
.select(null, PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE)
)[0];
assert.equal(Number(challenge.consumed_at_ms), NOW + 1);
assert.equal(challenge.claimed_subject_type, 'user');
assert.equal(challenge.claimed_subject_id, 'owner-1');
await assert.rejects(
service.claim({
...request,
principal: principal({ type: 'user', id: 'owner-2' }),
nowMs: NOW + 2,
}),
ProjectOwnerBootstrapClaimRejectedError,
);
});
test('rejects expired, malformed, mismatched and non-user claims without leaking the token', async (t) => {
const { service } = await setup(t);
const issued = await service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
});
await assert.rejects(
service.claim({
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: Buffer.alloc(32, 99).toString('base64url'),
principal: principal(),
nowMs: NOW + 1,
}),
(error) => {
assert.ok(error instanceof ProjectOwnerBootstrapClaimRejectedError);
assert.equal(error.message.includes(issued.token), false);
return true;
},
);
await assert.rejects(
service.claim({
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: issued.token,
principal: principal(),
nowMs: NOW + TTL_MS,
}),
ProjectOwnerBootstrapClaimRejectedError,
);
await assert.rejects(
service.claim({
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: issued.token,
principal: principal({ type: 'api_app', id: 'app-1' }),
nowMs: NOW + 1,
}),
ProjectOwnerBootstrapUnauthorizedError,
);
await assert.rejects(
service.claim({
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: `${issued.token}x`,
principal: principal(),
nowMs: NOW + 1,
}),
(error) => {
assert.equal(error.message.includes(issued.token), false);
return true;
},
);
await assert.rejects(
service.claim({
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: issued.token,
principal: principal(),
nowMs: NOW + 1,
subject: { type: 'user', id: 'injected-owner' },
}),
/request shape is invalid/,
);
});
test('refuses bootstrap whenever the Project already has any role binding', async (t) => {
const first = await setup(t);
await first.database.query(
`INSERT INTO "${PROJECT_ROLE_BINDING_TABLE}"
(project_id, subject_type, subject_id, version, state, role,
mutation_id, changed_by_type, changed_by_id, created_at_ms)
VALUES
('default', 'user', 'existing', 1, 'active', 'viewer',
'existing-binding', 'system', 'owner-bootstrap', 1)`,
);
await assert.rejects(
first.service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
}),
ProjectOwnerBootstrapProjectNotPristineError,
);
const second = await setup(t);
const issued = await second.service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
});
await second.database.query(
`INSERT INTO "${PROJECT_ROLE_BINDING_TABLE}"
(project_id, subject_type, subject_id, version, state, role,
mutation_id, changed_by_type, changed_by_id, created_at_ms)
VALUES
('default', 'user', 'existing', 1, 'active', 'viewer',
'existing-binding', 'system', 'owner-bootstrap', 1)`,
);
await assert.rejects(
second.service.claim({
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: issued.token,
principal: principal(),
nowMs: NOW + 1,
}),
ProjectOwnerBootstrapProjectNotPristineError,
);
});
test('fails closed for missing and archived Projects', async (t) => {
const missing = await setup(t);
await assert.rejects(
missing.service.issue({
projectId: 'missing',
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
}),
ProjectOwnerBootstrapProjectNotFoundError,
);
const archived = await setup(t);
const issued = await archived.service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
});
await archived.database.query(
`UPDATE "${PROJECT_TABLE}" SET status = 'archived' WHERE id = 'default'`,
);
await assert.rejects(
archived.service.claim({
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: issued.token,
principal: principal(),
nowMs: NOW + 1,
}),
ProjectOwnerBootstrapProjectInactiveError,
);
});
test('rolls back challenge consumption when owner binding insertion fails', async (t) => {
const { database, service } = await setup(t);
const issued = await service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
});
await database.query(
`CREATE TRIGGER reject_bootstrap_owner
BEFORE INSERT ON "${PROJECT_ROLE_BINDING_TABLE}"
WHEN NEW.mutation_id LIKE 'owner-bootstrap:%'
BEGIN
SELECT RAISE(ABORT, 'forced bootstrap binding failure');
END`,
);
const request = {
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: issued.token,
principal: principal(),
nowMs: NOW + 1,
};
await assert.rejects(
service.claim(request),
ProjectOwnerBootstrapUnavailableError,
);
let challenge = (
await database
.getQueryInterface()
.select(null, PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE)
)[0];
assert.equal(challenge.consumed_at_ms, null);
assert.equal(
(
await database
.getQueryInterface()
.select(null, PROJECT_ROLE_BINDING_TABLE)
).length,
0,
);
await database.query('DROP TRIGGER reject_bootstrap_owner');
assert.equal((await service.claim(request)).status, 'claimed');
challenge = (
await database
.getQueryInterface()
.select(null, PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE)
)[0];
assert.equal(Number(challenge.consumed_at_ms), NOW + 1);
});
test('rolls back when SQLite silently ignores challenge consumption', async (t) => {
const { database, service } = await setup(t);
const issued = await service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
});
await database.query(
`CREATE TRIGGER ignore_bootstrap_consumption
BEFORE UPDATE ON "${PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE}"
WHEN NEW.consumed_at_ms IS NOT NULL
BEGIN
SELECT RAISE(IGNORE);
END`,
);
await assert.rejects(
service.claim({
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: issued.token,
principal: principal(),
nowMs: NOW + 1,
}),
ProjectOwnerBootstrapUnavailableError,
);
const challenge = (
await database
.getQueryInterface()
.select(null, PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE)
)[0];
assert.equal(challenge.consumed_at_ms, null);
assert.equal(
(
await database
.getQueryInterface()
.select(null, PROJECT_ROLE_BINDING_TABLE)
).length,
0,
);
});
test('serializes claims from separate SQLite connections so exactly one owner wins', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-bootstrap-db-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
const storage = path.join(root, 'database.sqlite');
const first = await setup(t, storage);
const issued = await first.service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
});
const secondDatabase = new Sequelize({
dialect: 'sqlite',
storage,
logging: false,
});
t.after(() => secondDatabase.close());
const secondService = new ProjectOwnerBootstrapService(
new LegacySequelizeProjectOwnerBootstrapRepository(secondDatabase),
deterministicRandomSource(50),
);
const claim = (service, ownerId) =>
service.claim({
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: issued.token,
principal: principal({ type: 'user', id: ownerId }),
nowMs: NOW + 1,
});
const results = await Promise.allSettled([
claim(first.service, 'owner-a'),
claim(secondService, 'owner-b'),
]);
assert.equal(
results.filter((result) => result.status === 'fulfilled').length,
1,
);
const rejected = results.find((result) => result.status === 'rejected');
assert.ok(rejected.reason instanceof ProjectOwnerBootstrapClaimRejectedError);
const bindings = await first.database
.getQueryInterface()
.select(null, PROJECT_ROLE_BINDING_TABLE);
assert.equal(bindings.length, 1);
assert.equal(bindings[0].role, 'owner');
assert.ok(['owner-a', 'owner-b'].includes(bindings[0].subject_id));
});
test('rejects non-SQLite bootstrap repositories', () => {
assert.throws(
() =>
new LegacySequelizeProjectOwnerBootstrapRepository({
getDialect() {
return 'postgres';
},
}),
/SQLite-only/,
);
});