mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 03:18:09 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
LocalSecretAuthorizationFenceConflictError,
|
||||
} = require('@qinglong/runtime-core/local-secret-administration');
|
||||
const {
|
||||
LocalSecretMutationConflictError,
|
||||
} = require('@qinglong/runtime-core/local-secret');
|
||||
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
|
||||
const {
|
||||
openLocalSqliteRuntimeDatabase,
|
||||
} = require('@qinglong/local-sqlite/runtime');
|
||||
const {
|
||||
LocalSecretKeyringFileProvider,
|
||||
provisionLocalSecretKeyring,
|
||||
} = require('@qinglong/local-secret');
|
||||
const {
|
||||
LocalSecretAdministrationAuthenticationError,
|
||||
LocalSecretAdministrationAuthorizationError,
|
||||
LocalSecretAdministrationUnavailableError,
|
||||
createLocalSecretAdministrationService,
|
||||
} = require('@qinglong/local-admin/secret-administration');
|
||||
|
||||
const NOW = 1_760_000_000_000;
|
||||
|
||||
function fixture(t) {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-secret-admin-'));
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||
return {
|
||||
directory,
|
||||
profile: 'edge',
|
||||
databasePath: path.join(directory, 'qinglong3.sqlite'),
|
||||
keyringPath: path.join(directory, 'secret-keyring.json'),
|
||||
};
|
||||
}
|
||||
|
||||
function principal(overrides = {}) {
|
||||
return {
|
||||
subject: { type: 'user', id: 'user-owner' },
|
||||
authenticationId: 'local-console-1',
|
||||
authenticatedAtMs: NOW - 1_000,
|
||||
expiresAtMs: NOW + 60_000,
|
||||
assurance: 'local_console',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function request(mutationId, overrides = {}) {
|
||||
return {
|
||||
projectId: 'default',
|
||||
name: 'TOKEN',
|
||||
plaintext: 'never-persist-this-plaintext',
|
||||
mutationId,
|
||||
requestId: `request-${mutationId.slice(-4)}`,
|
||||
expectedCurrentVersion: 0,
|
||||
principal: principal(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function openStore(t) {
|
||||
const value = fixture(t);
|
||||
await migrateLocalSqlitePath(value);
|
||||
await provisionLocalSecretKeyring(value.keyringPath);
|
||||
const runtime = await openLocalSqliteRuntimeDatabase(value);
|
||||
t.after(() => runtime.close());
|
||||
const keys = new LocalSecretKeyringFileProvider(value.keyringPath);
|
||||
return { ...value, runtime, keys };
|
||||
}
|
||||
|
||||
async function bind(runtime, role, version = 1, state = 'active') {
|
||||
return runtime.projectPolicy.append({
|
||||
expectedCurrentVersion: version - 1,
|
||||
binding: {
|
||||
projectId: 'default',
|
||||
subject: { type: 'user', id: 'user-owner' },
|
||||
version,
|
||||
state,
|
||||
...(state === 'active' ? { role } : {}),
|
||||
mutationId: `binding-${version}`,
|
||||
changedBy: { type: 'system', id: 'owner-bootstrap' },
|
||||
createdAtMs: NOW + version,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function service(value, overrides = {}) {
|
||||
return createLocalSecretAdministrationService(
|
||||
overrides.projectPolicy ?? value.runtime.projectPolicy,
|
||||
overrides.mutations ?? value.runtime.localSecretAdministration,
|
||||
overrides.audit ?? value.runtime.securityAudit,
|
||||
overrides.keys ?? value.keys,
|
||||
{ now: () => NOW, nonceFactory: () => Buffer.alloc(12, 7) },
|
||||
);
|
||||
}
|
||||
|
||||
test('owner writes one encrypted envelope and allowed audit atomically', async (t) => {
|
||||
const value = await openStore(t);
|
||||
assert.equal(
|
||||
(await value.runtime.projectPolicy.resolve('default', principal().subject))
|
||||
.binding,
|
||||
undefined,
|
||||
);
|
||||
await bind(value.runtime, 'owner');
|
||||
const mutationId = '00000000-0000-4000-8000-000000000001';
|
||||
const result = await service(value).put(request(mutationId));
|
||||
assert.equal(result.status, 'inserted');
|
||||
assert.equal(result.version, 1);
|
||||
|
||||
const database = new DatabaseSync(value.databasePath, { readOnly: true });
|
||||
try {
|
||||
const secret = database
|
||||
.prepare(
|
||||
`SELECT version, ciphertext FROM "QingLong3LocalSecretEnvelopes"
|
||||
WHERE mutation_id = ?`,
|
||||
)
|
||||
.get(mutationId);
|
||||
const audit = database
|
||||
.prepare(
|
||||
`SELECT operation_id, outcome, subject_id, fence_project_version,
|
||||
fence_binding_version
|
||||
FROM "QingLong3SecurityAuditEvents" WHERE event_id = ?`,
|
||||
)
|
||||
.get(mutationId);
|
||||
assert.equal(secret.version, 1);
|
||||
assert.equal(
|
||||
Buffer.from(secret.ciphertext).includes(
|
||||
Buffer.from('never-persist-this-plaintext'),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.deepEqual(
|
||||
{ ...audit },
|
||||
{
|
||||
operation_id: 'secret.create',
|
||||
outcome: 'allowed',
|
||||
subject_id: 'user-owner',
|
||||
fence_project_version: 1,
|
||||
fence_binding_version: 1,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('denial is audited before key access and ownerless default stays closed', async (t) => {
|
||||
const value = await openStore(t);
|
||||
let keyReads = 0;
|
||||
const keys = {
|
||||
async active() {
|
||||
keyReads += 1;
|
||||
throw new Error('must not run');
|
||||
},
|
||||
async resolve() {
|
||||
keyReads += 1;
|
||||
throw new Error('must not run');
|
||||
},
|
||||
};
|
||||
const mutationId = '00000000-0000-4000-8000-000000000002';
|
||||
await assert.rejects(
|
||||
service(value, { keys }).put(request(mutationId)),
|
||||
LocalSecretAdministrationAuthorizationError,
|
||||
);
|
||||
assert.equal(keyReads, 0);
|
||||
const database = new DatabaseSync(value.databasePath, { readOnly: true });
|
||||
try {
|
||||
assert.equal(
|
||||
database
|
||||
.prepare(
|
||||
`SELECT outcome FROM "QingLong3SecurityAuditEvents"
|
||||
WHERE event_id = ?`,
|
||||
)
|
||||
.get(mutationId).outcome,
|
||||
'denied',
|
||||
);
|
||||
assert.equal(
|
||||
database
|
||||
.prepare(
|
||||
'SELECT COUNT(*) AS count FROM "QingLong3LocalSecretEnvelopes"',
|
||||
)
|
||||
.get().count,
|
||||
0,
|
||||
);
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('weak authentication is audited without Policy or key access', async (t) => {
|
||||
const value = await openStore(t);
|
||||
let policyReads = 0;
|
||||
let keyReads = 0;
|
||||
const projectPolicy = {
|
||||
async resolve() {
|
||||
policyReads += 1;
|
||||
throw new Error('must not run');
|
||||
},
|
||||
append: value.runtime.projectPolicy.append,
|
||||
};
|
||||
const keys = {
|
||||
async active() {
|
||||
keyReads += 1;
|
||||
throw new Error('must not run');
|
||||
},
|
||||
async resolve() {
|
||||
keyReads += 1;
|
||||
throw new Error('must not run');
|
||||
},
|
||||
};
|
||||
const mutationId = '00000000-0000-4000-8000-000000000003';
|
||||
await assert.rejects(
|
||||
service(value, { projectPolicy, keys }).put(
|
||||
request(mutationId, {
|
||||
principal: principal({ assurance: 'single_factor' }),
|
||||
}),
|
||||
),
|
||||
LocalSecretAdministrationAuthenticationError,
|
||||
);
|
||||
assert.equal(policyReads, 0);
|
||||
assert.equal(keyReads, 0);
|
||||
});
|
||||
|
||||
test('revocation after Policy decision is fenced inside the write transaction', async (t) => {
|
||||
const value = await openStore(t);
|
||||
await bind(value.runtime, 'admin');
|
||||
let revoked = false;
|
||||
const projectPolicy = {
|
||||
async resolve(projectId, subject) {
|
||||
const snapshot = await value.runtime.projectPolicy.resolve(
|
||||
projectId,
|
||||
subject,
|
||||
);
|
||||
if (!revoked) {
|
||||
revoked = true;
|
||||
await bind(value.runtime, undefined, 2, 'revoked');
|
||||
}
|
||||
return snapshot;
|
||||
},
|
||||
append: (...args) => value.runtime.projectPolicy.append(...args),
|
||||
};
|
||||
const mutationId = '00000000-0000-4000-8000-000000000004';
|
||||
await assert.rejects(
|
||||
service(value, { projectPolicy }).put(request(mutationId)),
|
||||
LocalSecretAuthorizationFenceConflictError,
|
||||
);
|
||||
const database = new DatabaseSync(value.databasePath, { readOnly: true });
|
||||
try {
|
||||
assert.equal(
|
||||
database
|
||||
.prepare(
|
||||
'SELECT COUNT(*) AS count FROM "QingLong3LocalSecretEnvelopes"',
|
||||
)
|
||||
.get().count,
|
||||
0,
|
||||
);
|
||||
assert.equal(
|
||||
database
|
||||
.prepare(
|
||||
`SELECT COUNT(*) AS count FROM "QingLong3SecurityAuditEvents"
|
||||
WHERE event_id = ?`,
|
||||
)
|
||||
.get(mutationId).count,
|
||||
0,
|
||||
);
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('audit insertion failure rolls the encrypted envelope back', async (t) => {
|
||||
const value = await openStore(t);
|
||||
await bind(value.runtime, 'owner');
|
||||
const database = new DatabaseSync(value.databasePath);
|
||||
database.exec(`
|
||||
CREATE TRIGGER reject_secret_audit
|
||||
BEFORE INSERT ON "QingLong3SecurityAuditEvents"
|
||||
WHEN NEW.operation_id IN ('secret.create', 'secret.rotate')
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'audit unavailable');
|
||||
END
|
||||
`);
|
||||
database.close();
|
||||
|
||||
await assert.rejects(
|
||||
service(value).put(request('00000000-0000-4000-8000-000000000005')),
|
||||
LocalSecretAdministrationUnavailableError,
|
||||
);
|
||||
const check = new DatabaseSync(value.databasePath, { readOnly: true });
|
||||
try {
|
||||
assert.equal(
|
||||
check
|
||||
.prepare(
|
||||
'SELECT COUNT(*) AS count FROM "QingLong3LocalSecretEnvelopes"',
|
||||
)
|
||||
.get().count,
|
||||
0,
|
||||
);
|
||||
assert.equal(
|
||||
check
|
||||
.prepare('SELECT COUNT(*) AS count FROM "QingLong3SecurityAuditEvents"')
|
||||
.get().count,
|
||||
0,
|
||||
);
|
||||
} finally {
|
||||
check.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('semantic replay returns no plaintext and conflicting replay fails closed', async (t) => {
|
||||
const value = await openStore(t);
|
||||
await bind(value.runtime, 'owner');
|
||||
const mutationId = '00000000-0000-4000-8000-000000000006';
|
||||
const admin = service(value);
|
||||
assert.equal((await admin.put(request(mutationId))).status, 'inserted');
|
||||
assert.equal((await admin.put(request(mutationId))).status, 'existing');
|
||||
await assert.rejects(
|
||||
admin.put(request(mutationId, { plaintext: 'different' })),
|
||||
LocalSecretMutationConflictError,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user