mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,402 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { readFileSync } = require('node:fs');
|
||||
const { request: httpsRequest } = require('node:https');
|
||||
const { resolve } = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
ClusterPluginPackageManagementHttpConfigurationError,
|
||||
startClusterPluginPackageManagementHttp,
|
||||
} = require('@qinglong/cluster-admin/plugin-package-management-http');
|
||||
const {
|
||||
startClusterWorkerCredentialManagementHttp,
|
||||
} = require('@qinglong/cluster-admin/worker-credential-management-http');
|
||||
const {
|
||||
WorkerCredentialManagementAuthorizationError,
|
||||
WorkerCredentialManagementConflictError,
|
||||
WorkerCredentialManagementQuotaExceededError,
|
||||
WorkerCredentialManagementRequestError,
|
||||
WorkerCredentialManagementUnavailableError,
|
||||
} = require('@qinglong/cluster-admin/worker-credential-management');
|
||||
|
||||
const SERVER_KEY = resolve(
|
||||
__dirname,
|
||||
'../../ql3-cluster-control/test/fixtures/mtls/server-key.pem',
|
||||
);
|
||||
const SERVER_CERT = resolve(
|
||||
__dirname,
|
||||
'../../ql3-cluster-control/test/fixtures/mtls/server-cert.pem',
|
||||
);
|
||||
const CLIENT_KEY = resolve(
|
||||
__dirname,
|
||||
'../../ql3-cluster-control/test/fixtures/mtls/client-key.pem',
|
||||
);
|
||||
const CLIENT_CERT = resolve(
|
||||
__dirname,
|
||||
'../../ql3-cluster-control/test/fixtures/mtls/client-cert.pem',
|
||||
);
|
||||
const CLIENT_CA = resolve(
|
||||
__dirname,
|
||||
'../../ql3-cluster-control/test/fixtures/mtls/ca-cert.pem',
|
||||
);
|
||||
const EMPTY_CRL = resolve(
|
||||
__dirname,
|
||||
'../../ql3-cluster-control/test/fixtures/mtls/empty-crl.pem',
|
||||
);
|
||||
const REVOKED_CLIENT_CRL = resolve(
|
||||
__dirname,
|
||||
'../../ql3-cluster-control/test/fixtures/mtls/revoked-client-crl.pem',
|
||||
);
|
||||
const NEXT_CLIENT_CERT = resolve(
|
||||
__dirname,
|
||||
'fixtures/management-service-cert.pem',
|
||||
);
|
||||
const NEXT_CLIENT_KEY = resolve(
|
||||
__dirname,
|
||||
'fixtures/management-service-key.pem',
|
||||
);
|
||||
const WORKER_PATH = '/api/v3/worker-credentials/management';
|
||||
|
||||
function command() {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
operation: 'worker-credential.inspect',
|
||||
request: {
|
||||
actionRef: 'worker-credential:worker-1:rotate:1',
|
||||
authorityProjectId: 'cluster-authority',
|
||||
approvalRequestId: 'approval-worker-1',
|
||||
inspectionId: 'inspection-worker-1',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function identities() {
|
||||
return {
|
||||
async reload() {},
|
||||
bind(assertion) {
|
||||
assert.equal(assertion, 'assertion-value');
|
||||
return {
|
||||
async authenticate() {
|
||||
return {
|
||||
subject: { type: 'user', id: 'cluster-reviewer' },
|
||||
authenticationId: 'authentication-1',
|
||||
authenticatedAtMs: 900,
|
||||
expiresAtMs: 10_000,
|
||||
assurance: 'multi_factor',
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function start(execute, options = {}) {
|
||||
return startClusterWorkerCredentialManagementHttp({
|
||||
host: '127.0.0.1',
|
||||
port: 0,
|
||||
tls: {
|
||||
privateKey: Buffer.from(readFileSync(SERVER_KEY)),
|
||||
certificate: Buffer.from(readFileSync(SERVER_CERT)),
|
||||
clientCertificateAuthority: Buffer.from(readFileSync(CLIENT_CA)),
|
||||
clientCertificateRevocationList: Buffer.from(
|
||||
readFileSync(options.revoked ? REVOKED_CLIENT_CRL : EMPTY_CRL),
|
||||
),
|
||||
},
|
||||
identities: options.identities ?? identities(),
|
||||
transport: { execute },
|
||||
limits: { requestTimeoutMs: 2_000, drainTimeoutMs: 500 },
|
||||
now: () => 1_000,
|
||||
createRequestId: () => 'worker-request-1',
|
||||
});
|
||||
}
|
||||
|
||||
async function request(application, path = WORKER_PATH, client = true) {
|
||||
const body = Buffer.from(JSON.stringify(command()));
|
||||
return new Promise((resolvePromise, reject) => {
|
||||
const outgoing = httpsRequest(
|
||||
{
|
||||
host: '127.0.0.1',
|
||||
port: application.address.port,
|
||||
path,
|
||||
method: 'POST',
|
||||
rejectUnauthorized: false,
|
||||
...(client
|
||||
? {
|
||||
cert: readFileSync(CLIENT_CERT),
|
||||
key: readFileSync(CLIENT_KEY),
|
||||
}
|
||||
: {}),
|
||||
agent: false,
|
||||
headers: {
|
||||
authorization: 'Bearer assertion-value',
|
||||
'content-type': 'application/json',
|
||||
'content-length': String(body.length),
|
||||
},
|
||||
},
|
||||
(incoming) => {
|
||||
const chunks = [];
|
||||
incoming.on('data', (chunk) => chunks.push(chunk));
|
||||
incoming.once('end', () => {
|
||||
resolvePromise({
|
||||
statusCode: incoming.statusCode,
|
||||
body: JSON.parse(Buffer.concat(chunks).toString('utf8')),
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
outgoing.once('error', reject);
|
||||
outgoing.end(body);
|
||||
});
|
||||
}
|
||||
|
||||
async function health(application, path) {
|
||||
return new Promise((resolvePromise, reject) => {
|
||||
const outgoing = httpsRequest(
|
||||
{
|
||||
host: '127.0.0.1',
|
||||
port: application.address.port,
|
||||
path,
|
||||
method: 'GET',
|
||||
rejectUnauthorized: false,
|
||||
agent: false,
|
||||
},
|
||||
(incoming) => {
|
||||
incoming.resume();
|
||||
incoming.once('end', () => resolvePromise(incoming.statusCode));
|
||||
},
|
||||
);
|
||||
outgoing.once('error', reject);
|
||||
outgoing.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function startWithClientTrust(certificateAuthority, execute) {
|
||||
return startClusterWorkerCredentialManagementHttp({
|
||||
host: '127.0.0.1',
|
||||
port: 0,
|
||||
tls: {
|
||||
privateKey: Buffer.from(readFileSync(SERVER_KEY)),
|
||||
certificate: Buffer.from(readFileSync(SERVER_CERT)),
|
||||
clientCertificateAuthority: certificateAuthority,
|
||||
clientCertificateRevocationList: Buffer.from(readFileSync(EMPTY_CRL)),
|
||||
},
|
||||
identities: identities(),
|
||||
transport: { execute },
|
||||
limits: { requestTimeoutMs: 2_000, drainTimeoutMs: 500 },
|
||||
now: () => 1_000,
|
||||
createRequestId: () => 'worker-request-rotation',
|
||||
});
|
||||
}
|
||||
|
||||
async function requestWithClientIdentity(application, certificate, key) {
|
||||
const body = Buffer.from(JSON.stringify(command()));
|
||||
return new Promise((resolvePromise, reject) => {
|
||||
const outgoing = httpsRequest(
|
||||
{
|
||||
host: '127.0.0.1',
|
||||
port: application.address.port,
|
||||
path: WORKER_PATH,
|
||||
method: 'POST',
|
||||
rejectUnauthorized: false,
|
||||
cert: readFileSync(certificate),
|
||||
key: readFileSync(key),
|
||||
agent: false,
|
||||
headers: {
|
||||
authorization: 'Bearer assertion-value',
|
||||
'content-type': 'application/json',
|
||||
'content-length': String(body.length),
|
||||
},
|
||||
},
|
||||
(incoming) => {
|
||||
const chunks = [];
|
||||
incoming.on('data', (chunk) => chunks.push(chunk));
|
||||
incoming.once('end', () => {
|
||||
resolvePromise({
|
||||
statusCode: incoming.statusCode,
|
||||
body: JSON.parse(Buffer.concat(chunks).toString('utf8')),
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
outgoing.once('error', reject);
|
||||
outgoing.end(body);
|
||||
});
|
||||
}
|
||||
|
||||
test('serves only the fixed Worker credential management route', async () => {
|
||||
const calls = [];
|
||||
const application = await start(async (value, authentication) => {
|
||||
calls.push({ value, principal: await authentication.authenticate() });
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
operation: 'worker-credential.inspect',
|
||||
plan: null,
|
||||
approval: null,
|
||||
stale: false,
|
||||
};
|
||||
});
|
||||
try {
|
||||
const response = await request(application);
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert.equal(response.body.requestId, 'worker-request-1');
|
||||
assert.equal(response.body.result.operation, 'worker-credential.inspect');
|
||||
assert.deepEqual(calls[0].value, command());
|
||||
assert.deepEqual(calls[0].principal.subject, {
|
||||
type: 'user',
|
||||
id: 'cluster-reviewer',
|
||||
});
|
||||
assert.equal(
|
||||
(await request(application, '/api/v3/plugin-packages/management'))
|
||||
.statusCode,
|
||||
404,
|
||||
);
|
||||
assert.equal(calls.length, 1);
|
||||
} finally {
|
||||
await application.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('requires an authorized client certificate before OIDC or body parsing', async () => {
|
||||
let identityBinds = 0;
|
||||
let transportCalls = 0;
|
||||
const identity = identities();
|
||||
const application = await start(
|
||||
async () => {
|
||||
transportCalls += 1;
|
||||
},
|
||||
{
|
||||
identities: {
|
||||
...identity,
|
||||
bind(assertion) {
|
||||
identityBinds += 1;
|
||||
return identity.bind(assertion);
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
try {
|
||||
assert.equal(await health(application, '/livez'), 200);
|
||||
assert.equal(await health(application, '/readyz'), 200);
|
||||
const response = await request(application, WORKER_PATH, false);
|
||||
assert.equal(response.statusCode, 401);
|
||||
assert.equal(response.body.error.code, 'client_certificate_required');
|
||||
assert.equal(identityBinds, 0);
|
||||
assert.equal(transportCalls, 0);
|
||||
} finally {
|
||||
await application.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects a CRL-revoked client certificate before OIDC', async () => {
|
||||
let identityBinds = 0;
|
||||
const identity = identities();
|
||||
const application = await start(async () => assert.fail('must not execute'), {
|
||||
revoked: true,
|
||||
identities: {
|
||||
...identity,
|
||||
bind(assertion) {
|
||||
identityBinds += 1;
|
||||
return identity.bind(assertion);
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await request(application);
|
||||
assert.equal(response.statusCode, 401);
|
||||
assert.equal(response.body.error.code, 'client_certificate_required');
|
||||
assert.equal(identityBinds, 0);
|
||||
} finally {
|
||||
await application.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('maps Worker credential management failures to stable HTTP errors', async () => {
|
||||
for (const [failure, statusCode, code] of [
|
||||
[new WorkerCredentialManagementRequestError('invalid'), 400, 'request_invalid'],
|
||||
[new WorkerCredentialManagementAuthorizationError(), 403, 'forbidden'],
|
||||
[new WorkerCredentialManagementConflictError('conflict'), 409, 'conflict'],
|
||||
[new WorkerCredentialManagementQuotaExceededError(1_250), 429, 'quota_exceeded'],
|
||||
[new WorkerCredentialManagementUnavailableError(), 503, 'unavailable'],
|
||||
]) {
|
||||
const application = await start(async () => {
|
||||
throw failure;
|
||||
});
|
||||
try {
|
||||
const response = await request(application);
|
||||
assert.equal(response.statusCode, statusCode);
|
||||
assert.equal(response.body.error.code, code);
|
||||
} finally {
|
||||
await application.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects arbitrary management paths at configuration time', async () => {
|
||||
const privateKey = Buffer.from(readFileSync(SERVER_KEY));
|
||||
try {
|
||||
await assert.rejects(
|
||||
startClusterPluginPackageManagementHttp({
|
||||
host: '127.0.0.1',
|
||||
port: 0,
|
||||
tls: {
|
||||
privateKey,
|
||||
certificate: Buffer.from(readFileSync(SERVER_CERT)),
|
||||
},
|
||||
identities: identities(),
|
||||
transport: { async execute() {} },
|
||||
managementPath: '/api/v3/arbitrary',
|
||||
}),
|
||||
ClusterPluginPackageManagementHttpConfigurationError,
|
||||
);
|
||||
} finally {
|
||||
privateKey.fill(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('accepts both client CAs during overlap then rejects the retired CA', async () => {
|
||||
const execute = async () => ({
|
||||
schemaVersion: 1,
|
||||
operation: 'worker-credential.inspect',
|
||||
plan: null,
|
||||
approval: null,
|
||||
stale: false,
|
||||
});
|
||||
const oldAuthority = Buffer.from(readFileSync(CLIENT_CA));
|
||||
const nextAuthority = Buffer.from(readFileSync(NEXT_CLIENT_CERT));
|
||||
const overlap = await startWithClientTrust(
|
||||
Buffer.concat([oldAuthority, nextAuthority]),
|
||||
execute,
|
||||
);
|
||||
try {
|
||||
assert.equal((await request(overlap)).statusCode, 200);
|
||||
assert.equal(
|
||||
(
|
||||
await requestWithClientIdentity(
|
||||
overlap,
|
||||
NEXT_CLIENT_CERT,
|
||||
NEXT_CLIENT_KEY,
|
||||
)
|
||||
).statusCode,
|
||||
200,
|
||||
);
|
||||
} finally {
|
||||
await overlap.close();
|
||||
}
|
||||
|
||||
const retired = await startWithClientTrust(nextAuthority, execute);
|
||||
try {
|
||||
assert.equal((await request(retired)).statusCode, 401);
|
||||
assert.equal(
|
||||
(
|
||||
await requestWithClientIdentity(
|
||||
retired,
|
||||
NEXT_CLIENT_CERT,
|
||||
NEXT_CLIENT_KEY,
|
||||
)
|
||||
).statusCode,
|
||||
200,
|
||||
);
|
||||
} finally {
|
||||
await retired.close();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user