feat(ql3): add short-lived security administration command

This commit is contained in:
whyour
2026-08-25 02:09:06 +08:00
parent 9c736f0943
commit 7b3d49acda
17 changed files with 1756 additions and 17 deletions
@@ -0,0 +1,361 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
createClusterAdministrationCommandRunner,
normalizeClusterAdministrationCommand,
publishClusterAdministrationCredentialDelivery,
} = require('@qinglong/cluster-admin/administration-command');
const PRINCIPAL = Object.freeze({
subject: { type: 'user', id: 'security-owner' },
authenticationId: 'assertion:security-command-1',
authenticatedAtMs: 900,
expiresAtMs: 2_000,
assurance: 'multi_factor',
});
const SUBJECT = Object.freeze({ type: 'api_app', id: 'automation-client' });
const PATHS = Object.freeze({
commandFile: '/private/command.json',
assertionFile: '/private/assertion.jwt',
keysetFile: '/private/keyset.json',
pepperFile: '/private/pepper',
});
function identityCommand(overrides = {}) {
return {
schemaVersion: 1,
operation: 'identity.register',
request: {
mutationId: '123e4567-e89b-42d3-a456-426614174301',
requestId: 'security-identity-register-1',
expectedCurrentVersion: 0,
subject: SUBJECT,
},
...overrides,
};
}
function credentialCommand(overrides = {}) {
return {
schemaVersion: 1,
operation: 'credential.issue',
request: {
mutationId: '123e4567-e89b-42d3-a456-426614174302',
requestId: 'security-credential-issue-1',
expectedCurrentVersion: 0,
credentialId: 'automation-primary',
subject: SUBJECT,
notBeforeAtMs: 1_000,
expiresAtMs: 2_000,
},
...overrides,
};
}
function authority(overrides = {}) {
const calls = [];
let closes = 0;
const credential = {
credentialId: 'automation-primary',
subject: SUBJECT,
secretDigest: 'digest',
pepperKeyId: 'legacy-v1',
state: 'active',
version: 1,
createdAtMs: 1_000,
notBeforeAtMs: 1_000,
expiresAtMs: 2_000,
};
const value = {
administration: {
async registerIdentity(request) {
calls.push(['identity.register', request]);
return {
status: 'inserted',
identity: {
subject: request.subject,
status: 'active',
version: 1,
createdAtMs: 1_000,
updatedAtMs: 1_000,
},
mutation: {},
};
},
async enableIdentity() {
throw new Error('unexpected enable');
},
async disableIdentity() {
throw new Error('unexpected disable');
},
async issueCredential(request) {
calls.push(['credential.issue', request]);
return {
status: 'inserted',
credential,
mutation: {},
token: 'ql3c_automation-primary_private-secret',
};
},
async rotateCredential() {
throw new Error('unexpected rotate');
},
async revokeCredential() {
throw new Error('unexpected revoke');
},
},
audit: {
async list(query) {
calls.push(['audit.list', query]);
return { records: [], nextCursor: null };
},
},
async close() {
closes += 1;
},
...overrides,
};
return { value, calls, closes: () => closes };
}
function runner(command, authorityValue, published = []) {
const buffers = [];
const pepper = 'A'.repeat(43);
const files = new Map([
[PATHS.commandFile, `${JSON.stringify(command)}\n`],
[PATHS.assertionFile, 'signed.assertion.value'],
[PATHS.pepperFile, pepper],
]);
const authentications = [];
const opens = [];
const instance = createClusterAdministrationCommandRunner({
async openAuthority(environment, candidatePepper) {
opens.push({ environment, pepper: candidatePepper });
return authorityValue;
},
async authenticate(keysetFile, assertion) {
authentications.push({ keysetFile, assertion });
return PRINCIPAL;
},
readFile(filePath) {
const value = files.get(filePath);
if (value === undefined) throw new Error(`unexpected file: ${filePath}`);
const buffer = Buffer.from(value);
buffers.push(buffer);
return buffer;
},
publishDelivery(filePath, bytes) {
published.push({ filePath, bytes: Buffer.from(bytes) });
},
});
return { instance, buffers, authentications, opens };
}
test('executes one strongly authenticated identity mutation and closes authority', async () => {
const target = authority();
const execution = runner(identityCommand(), target.value);
const result = await execution.instance.run(PATHS, { deployment: 'test' });
assert.deepEqual(result, {
schemaVersion: 1,
operation: 'identity.register',
status: 'inserted',
subject: SUBJECT,
version: 1,
identityStatus: 'active',
});
assert.deepEqual(execution.authentications, [
{
keysetFile: PATHS.keysetFile,
assertion: 'signed.assertion.value',
},
]);
assert.equal(execution.opens[0].pepper, 'A'.repeat(43));
assert.equal(target.calls[0][1].principal, PRINCIPAL);
assert.equal(target.closes(), 1);
assert.equal(
execution.buffers.every((value) => value.every((byte) => byte === 0)),
true,
);
});
test('publishes a credential token only to the private delivery boundary', async () => {
const target = authority();
const published = [];
const execution = runner(credentialCommand(), target.value, published);
const paths = { ...PATHS, deliveryFile: '/private/delivery.json' };
const result = await execution.instance.run(paths, {});
assert.equal(result.operation, 'credential.issue');
assert.equal(result.status, 'inserted');
assert.equal('token' in result, false);
assert.deepEqual(result.delivery.fileName, 'delivery.json');
assert.match(result.delivery.digest, /^[0-9a-f]{64}$/);
assert.equal(published.length, 1);
const delivery = JSON.parse(published[0].bytes.toString('utf8'));
assert.equal(delivery.token, 'ql3c_automation-primary_private-secret');
assert.equal(delivery.mutationId, credentialCommand().request.mutationId);
assert.equal(target.closes(), 1);
});
test('does not recreate lost token material during exact credential replay', async () => {
const base = authority();
base.value.administration.issueCredential = async () => ({
status: 'existing',
credential: {
credentialId: 'automation-primary',
subject: SUBJECT,
state: 'active',
version: 1,
createdAtMs: 1_000,
notBeforeAtMs: 1_000,
expiresAtMs: 2_000,
},
mutation: {},
token: null,
});
const published = [];
const execution = runner(credentialCommand(), base.value, published);
const result = await execution.instance.run(
{ ...PATHS, deliveryFile: '/private/delivery.json' },
{},
);
assert.equal(result.status, 'existing');
assert.equal('delivery' in result, false);
assert.deepEqual(published, []);
assert.equal(base.closes(), 1);
});
test('revokes a credential without requiring or publishing a delivery file', async () => {
const target = authority();
target.value.administration.revokeCredential = async (request) => ({
status: 'inserted',
credential: {
credentialId: request.credentialId,
subject: request.subject,
state: 'revoked',
version: 2,
createdAtMs: 1_000,
notBeforeAtMs: 1_000,
expiresAtMs: 2_000,
},
mutation: {},
});
const published = [];
const execution = runner(
{
schemaVersion: 1,
operation: 'credential.revoke',
request: {
mutationId: '123e4567-e89b-42d3-a456-426614174303',
requestId: 'security-credential-revoke-1',
expectedCurrentVersion: 1,
credentialId: 'automation-primary',
subject: SUBJECT,
},
},
target.value,
published,
);
const result = await execution.instance.run(PATHS, {});
assert.deepEqual(result, {
schemaVersion: 1,
operation: 'credential.revoke',
status: 'inserted',
subject: SUBJECT,
credentialId: 'automation-primary',
version: 2,
state: 'revoked',
});
assert.deepEqual(published, []);
assert.equal(target.closes(), 1);
});
test('keeps audit query bounded and rejects widened command shapes before admission', async () => {
const query = {
schemaVersion: 1,
operation: 'audit.list',
request: { limit: 25, filter: { outcome: 'allowed' } },
};
const target = authority();
const execution = runner(query, target.value);
const result = await execution.instance.run(PATHS, {});
assert.deepEqual(result, {
schemaVersion: 1,
operation: 'audit.list',
page: { records: [], nextCursor: null },
});
assert.deepEqual(target.calls, [
['audit.list', { limit: 25, filter: { outcome: 'allowed' } }],
]);
assert.throws(
() => normalizeClusterAdministrationCommand({ ...query, debug: true }),
/command shape is invalid/,
);
assert.throws(
() =>
normalizeClusterAdministrationCommand({
...query,
request: { limit: 201, filter: {} },
}),
/audit query is invalid/,
);
});
test('rejects widened path authority before reading a command file', async () => {
let reads = 0;
const instance = createClusterAdministrationCommandRunner({
async openAuthority() {
throw new Error('must not open authority');
},
async authenticate() {
throw new Error('must not authenticate');
},
readFile() {
reads += 1;
throw new Error('must not read');
},
publishDelivery() {
throw new Error('must not publish');
},
});
await assert.rejects(
instance.run({ ...PATHS, ambientCredential: true }, {}),
/command paths shape is invalid/,
);
assert.equal(reads, 0);
});
test('publishes a 0600 no-replace delivery and leaves an existing target intact', (t) => {
const directory = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-security-delivery-'),
);
fs.chmodSync(directory, 0o700);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const filePath = path.join(directory, 'credential.json');
const bytes = Buffer.from('{"token":"secret"}\n');
publishClusterAdministrationCredentialDelivery(filePath, bytes);
assert.equal(fs.statSync(filePath).mode & 0o777, 0o600);
assert.deepEqual(fs.readFileSync(filePath), bytes);
assert.throws(
() =>
publishClusterAdministrationCredentialDelivery(
filePath,
Buffer.from('{"token":"replacement"}\n'),
),
/could not be published/,
);
assert.deepEqual(fs.readFileSync(filePath), bytes);
});
@@ -13,6 +13,7 @@ const {
createClusterApprovalIdentityKeysetFile,
createClusterModelProviderCredentialIdentityKeysetFile,
createClusterRunIdentityKeysetFile,
createClusterSecurityAdministrationIdentityKeysetFile,
} = require('@qinglong/cluster-admin/plugin-package-identity-keyset');
const NOW_MS = 1_700_000_000_000;
@@ -249,6 +250,38 @@ function runAssertion(key, overrides = {}) {
).toString('base64url')}`;
}
function securityAdministrationAssertion(key, overrides = {}) {
const header = Buffer.from(
JSON.stringify({
alg: 'EdDSA',
kid: key.kid,
typ: 'ql3-security-administration+jwt',
}),
).toString('base64url');
const now = Math.floor(NOW_MS / 1000);
const payload = Buffer.from(
JSON.stringify({
acr: 'urn:ql3:mfa',
amr: ['pwd', 'otp'],
aud: 'qinglong3-security-administration',
auth_time: now - 10,
exp: now + 120,
iat: now,
iss: ISSUER,
jti: `security-administration-assertion-${key.kid}`,
ql3_purpose: 'security-administration',
sub: 'security-owner-1',
...overrides,
}),
).toString('base64url');
const signed = `${header}.${payload}`;
return `${signed}.${sign(
null,
Buffer.from(signed, 'ascii'),
key.privateKey,
).toString('base64url')}`;
}
async function atomicWrite(filePath, document) {
const nextPath = `${filePath}.next`;
await writeFile(nextPath, `${JSON.stringify(document)}\n`, { mode: 0o644 });
@@ -337,10 +370,9 @@ test('loads an automation keyset with a purpose isolated from other management p
type: 'user',
id: 'automation-operator-1',
});
await assert.rejects(
provider.bind(workerAssertion(key)).authenticate(),
{ code: 'CLUSTER_PLUGIN_PACKAGE_IDENTITY_ASSERTION_INVALID' },
);
await assert.rejects(provider.bind(workerAssertion(key)).authenticate(), {
code: 'CLUSTER_PLUGIN_PACKAGE_IDENTITY_ASSERTION_INVALID',
});
await assert.rejects(provider.bind(assertion(key)).authenticate(), {
code: 'CLUSTER_PLUGIN_PACKAGE_IDENTITY_ASSERTION_INVALID',
});
@@ -358,14 +390,19 @@ test('loads an Approval keyset isolated by type, purpose and audience', async ()
filePath,
now: () => NOW_MS,
});
const principal = await provider.bind(approvalAssertion(key)).authenticate();
const principal = await provider
.bind(approvalAssertion(key))
.authenticate();
assert.deepEqual(principal.subject, {
type: 'user',
id: 'approval-owner-1',
});
await assert.rejects(provider.bind(automationAssertion(key)).authenticate(), {
code: 'CLUSTER_PLUGIN_PACKAGE_IDENTITY_ASSERTION_INVALID',
});
await assert.rejects(
provider.bind(automationAssertion(key)).authenticate(),
{
code: 'CLUSTER_PLUGIN_PACKAGE_IDENTITY_ASSERTION_INVALID',
},
);
await assert.rejects(provider.bind(assertion(key)).authenticate(), {
code: 'CLUSTER_PLUGIN_PACKAGE_IDENTITY_ASSERTION_INVALID',
});
@@ -435,6 +472,40 @@ test('loads a Run keyset isolated from every other management purpose', async ()
});
});
test('loads a Security Administration keyset isolated from other management purposes', async () => {
await fixture(async ({ filePath }) => {
const key = reviewedKey('security-administration-key-1');
await atomicWrite(filePath, {
...keyset(1, [key]),
audience: 'qinglong3-security-administration',
});
const provider = createClusterSecurityAdministrationIdentityKeysetFile({
filePath,
now: () => NOW_MS,
});
const principal = await provider
.bind(securityAdministrationAssertion(key))
.authenticate();
assert.deepEqual(principal.subject, {
type: 'user',
id: 'security-owner-1',
});
await assert.rejects(provider.bind(runAssertion(key)).authenticate(), {
code: 'CLUSTER_PLUGIN_PACKAGE_IDENTITY_ASSERTION_INVALID',
});
await assert.rejects(
provider
.bind(
securityAdministrationAssertion(key, {
ql3_purpose: 'run-management',
}),
)
.authenticate(),
{ code: 'CLUSTER_PLUGIN_PACKAGE_IDENTITY_ASSERTION_INVALID' },
);
});
});
test('supports overlap rotation then immediately revokes the previous key', async () => {
await fixture(async ({ filePath }) => {
const first = reviewedKey('issuer-key-1');
@@ -336,7 +336,7 @@ function validContextFixture(t) {
test('catalog exposes only reviewed product entrypoints from the same package', () => {
assert.equal(manifest.bin['ql3-cluster-admin'], 'dist/product-cli/cli.js');
assert.equal(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length, 12);
assert.equal(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length, 13);
assert.equal(
new Set(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.map(({ name }) => name)).size,
QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length,
@@ -354,6 +354,7 @@ test('catalog exposes only reviewed product entrypoints from the same package',
);
assert.equal(
command.binary.includes('-client') ||
command.binary === 'ql3-security-admin' ||
command.binary === 'ql3-copilot-mcp' ||
command.binary === 'ql3-copilot-console' ||
command.binary === 'ql3-copilot-evidence-verify',
@@ -391,6 +392,7 @@ test('help and version are bounded installation-derived product facts', () => {
/\n evidence-verify\s+verify one redacted Console evidence/,
);
assert.match(help, /Server, migration, recovery, executor and key-custody/);
assert.match(help, /\n security\s+administer identities, API credentials/);
assert.equal(help.includes('plugin-package-manage'), false);
assert.equal(
loadQingLong3ClusterProductVersion(moduleDirectory),
@@ -923,6 +925,11 @@ test('binary exposes help/version and delegates without a shell', () => {
assert.match(delegatedHelp.stdout, /^Usage: ql3-run-client /);
assert.equal(delegatedHelp.stderr, '');
const securityHelp = runCli(['security', '--help']);
assert.equal(securityHelp.status, 0);
assert.match(securityHelp.stdout, /^Usage: ql3-security-admin /);
assert.equal(securityHelp.stderr, '');
const rejected = runCli(['../../tmp/not-a-command']);
assert.equal(rejected.status, 64);
assert.equal(rejected.stdout, '');