mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
285 lines
9.2 KiB
JavaScript
285 lines
9.2 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
const {
|
|
WorkerCredentialDeliveryUnavailableError,
|
|
} = require('@qinglong/runtime-core/worker-credential-delivery');
|
|
const {
|
|
WORKER_CREDENTIAL_KUBERNETES_TOKEN_REQUEST_SECONDS,
|
|
WorkerCredentialKubernetesTokenRequestUnavailableError,
|
|
createWorkerCredentialKubernetesTokenRequestSession,
|
|
} = require(
|
|
'@qinglong/cluster-admin/worker-credential-kubernetes-token-request'
|
|
);
|
|
|
|
const NOW_MS = 1_800_000_000_000;
|
|
const DELIVERY = Object.freeze({
|
|
clusterIdentity: 'cluster-production-a',
|
|
stageNamespace: 'qinglong-worker-a-stage',
|
|
namespace: 'qinglong-worker-a',
|
|
targetSecretName: 'worker-a-credential',
|
|
targetDeploymentName: 'worker-a',
|
|
targetDataKey: 'credential-token',
|
|
});
|
|
|
|
function segment(value) {
|
|
return Buffer.from(JSON.stringify(value), 'utf8').toString('base64url');
|
|
}
|
|
|
|
function jwt(overrides = {}) {
|
|
const issuedAt = Math.floor(NOW_MS / 1_000);
|
|
const header = { alg: 'RS256', typ: 'JWT', ...(overrides.header ?? {}) };
|
|
const claims = {
|
|
sub:
|
|
'system:serviceaccount:qinglong-worker-a-stage:' +
|
|
'ql3-worker-credential-admin',
|
|
iat: issuedAt,
|
|
exp: issuedAt + WORKER_CREDENTIAL_KUBERNETES_TOKEN_REQUEST_SECONDS,
|
|
aud: ['https://kubernetes.default.svc'],
|
|
...(overrides.claims ?? {}),
|
|
};
|
|
return `${segment(header)}.${segment(claims)}.${Buffer.from('signature').toString('base64url')}`;
|
|
}
|
|
|
|
function permission(attributes) {
|
|
if (
|
|
attributes.namespace === DELIVERY.stageNamespace &&
|
|
attributes.resource === 'secrets'
|
|
) {
|
|
return ['get', 'list', 'create', 'delete'].includes(attributes.verb);
|
|
}
|
|
if (
|
|
attributes.namespace === DELIVERY.namespace &&
|
|
attributes.resource === 'secrets' &&
|
|
attributes.name === DELIVERY.targetSecretName
|
|
) {
|
|
return ['get', 'update'].includes(attributes.verb);
|
|
}
|
|
return attributes.namespace === DELIVERY.namespace &&
|
|
attributes.group === 'apps' &&
|
|
attributes.resource === 'deployments' &&
|
|
attributes.name === DELIVERY.targetDeploymentName &&
|
|
['get', 'update'].includes(attributes.verb);
|
|
}
|
|
|
|
function issuerPermission(attributes) {
|
|
return attributes.namespace === DELIVERY.stageNamespace &&
|
|
attributes.verb === 'create' &&
|
|
attributes.resource === 'serviceaccounts' &&
|
|
attributes.subresource === 'token' &&
|
|
attributes.name === 'ql3-worker-credential-admin';
|
|
}
|
|
|
|
function fixture(options = {}) {
|
|
const value = options.token ?? jwt();
|
|
const issuedAt = Math.floor(NOW_MS / 1_000);
|
|
const response = {
|
|
apiVersion: 'authentication.k8s.io/v1',
|
|
kind: 'TokenRequest',
|
|
status: {
|
|
token: value,
|
|
expirationTimestamp:
|
|
options.expirationTimestamp ??
|
|
new Date(
|
|
(issuedAt + WORKER_CREDENTIAL_KUBERNETES_TOKEN_REQUEST_SECONDS) *
|
|
1_000,
|
|
),
|
|
},
|
|
};
|
|
const requests = [];
|
|
const receivedTokens = [];
|
|
let active = false;
|
|
let disposals = 0;
|
|
const tokenRequests = {
|
|
async createNamespacedServiceAccountToken(request) {
|
|
requests.push(structuredClone(request));
|
|
if (options.requestError) throw new Error('sensitive upstream failure');
|
|
return response;
|
|
},
|
|
};
|
|
const issuerAuthorization = {
|
|
async createSelfSubjectAccessReview(request) {
|
|
return {
|
|
status: {
|
|
allowed: options.issuerOverbroad === true
|
|
? true
|
|
: issuerPermission(request.body.spec.resourceAttributes),
|
|
},
|
|
};
|
|
},
|
|
};
|
|
const createRestrictedClients = (token) => {
|
|
receivedTokens.push(token);
|
|
active = true;
|
|
const assertActive = () => {
|
|
if (!active) {
|
|
throw Object.assign(new Error('disposed credential'), { code: 401 });
|
|
}
|
|
};
|
|
return {
|
|
secrets: {
|
|
async readNamespacedSecret() { assertActive(); throw Object.assign(new Error('absent'), { code: 404 }); },
|
|
async createNamespacedSecret() { assertActive(); throw new Error('unused'); },
|
|
async replaceNamespacedSecret() { assertActive(); throw new Error('unused'); },
|
|
async deleteNamespacedSecret() { assertActive(); throw new Error('unused'); },
|
|
async listNamespacedSecret() { assertActive(); return { items: [] }; },
|
|
},
|
|
deployments: {
|
|
async readNamespacedDeployment() { assertActive(); throw new Error('unused'); },
|
|
async replaceNamespacedDeployment() { assertActive(); throw new Error('unused'); },
|
|
},
|
|
authorization: {
|
|
async createSelfSubjectAccessReview(request) {
|
|
assertActive();
|
|
return {
|
|
status: {
|
|
allowed: options.overbroad === true
|
|
? true
|
|
: permission(request.body.spec.resourceAttributes),
|
|
},
|
|
};
|
|
},
|
|
},
|
|
dispose() {
|
|
disposals += 1;
|
|
active = false;
|
|
if (options.disposeError) throw new Error('sensitive dispose failure');
|
|
},
|
|
};
|
|
};
|
|
const session = createWorkerCredentialKubernetesTokenRequestSession(
|
|
tokenRequests,
|
|
issuerAuthorization,
|
|
createRestrictedClients,
|
|
{
|
|
serviceAccountName: 'ql3-worker-credential-admin',
|
|
identitySecretName: 'worker-a-identity',
|
|
delivery: DELIVERY,
|
|
now: () => NOW_MS,
|
|
},
|
|
);
|
|
return {
|
|
session,
|
|
response,
|
|
requests,
|
|
receivedTokens,
|
|
get active() { return active; },
|
|
get disposals() { return disposals; },
|
|
};
|
|
}
|
|
|
|
test('mints one bounded token, proves exact RBAC and disposes retained clients', async () => {
|
|
const state = fixture();
|
|
let retained;
|
|
const result = await state.session.withDelivery(async (context) => {
|
|
retained = context.delivery;
|
|
assert.deepEqual(context.evidence, {
|
|
tokenLifetimeSeconds: 600,
|
|
issuerAllowedChecks: 1,
|
|
issuerDeniedChecks: 8,
|
|
allowedChecks: 8,
|
|
deniedChecks: 20,
|
|
});
|
|
assert.match(context.delivery.deploymentTargetDigest, /^[0-9a-f]{64}$/);
|
|
return 'published';
|
|
});
|
|
assert.equal(result, 'published');
|
|
assert.deepEqual(state.requests, [{
|
|
name: 'ql3-worker-credential-admin',
|
|
namespace: DELIVERY.stageNamespace,
|
|
body: {
|
|
apiVersion: 'authentication.k8s.io/v1',
|
|
kind: 'TokenRequest',
|
|
spec: { expirationSeconds: 600 },
|
|
},
|
|
}]);
|
|
assert.equal(state.receivedTokens.length, 1);
|
|
assert.equal(state.response.status.token, '');
|
|
assert.equal(state.disposals, 1);
|
|
assert.equal(state.active, false);
|
|
await assert.rejects(
|
|
retained.inspect('123e4567-e89b-42d3-a456-426614174901'),
|
|
WorkerCredentialDeliveryUnavailableError,
|
|
);
|
|
state.receivedTokens[0] = '';
|
|
});
|
|
|
|
test('rejects an overbroad delivery token before invoking credential work', async () => {
|
|
const state = fixture({ overbroad: true });
|
|
let invoked = false;
|
|
await assert.rejects(
|
|
state.session.withDelivery(async () => {
|
|
invoked = true;
|
|
}),
|
|
WorkerCredentialKubernetesTokenRequestUnavailableError,
|
|
);
|
|
assert.equal(invoked, false);
|
|
assert.equal(state.response.status.token, '');
|
|
assert.equal(state.disposals, 1);
|
|
assert.equal(state.active, false);
|
|
state.receivedTokens[0] = '';
|
|
});
|
|
|
|
test('rejects an overbroad token issuer before requesting a token', async () => {
|
|
const state = fixture({ issuerOverbroad: true });
|
|
await assert.rejects(
|
|
state.session.withDelivery(async () => undefined),
|
|
WorkerCredentialKubernetesTokenRequestUnavailableError,
|
|
);
|
|
assert.equal(state.requests.length, 0);
|
|
assert.equal(state.receivedTokens.length, 0);
|
|
assert.equal(state.disposals, 0);
|
|
});
|
|
|
|
test('rejects malformed, wrong-subject and overlong token responses before client creation', async () => {
|
|
const issuedAt = Math.floor(NOW_MS / 1_000);
|
|
const cases = [
|
|
{ token: 'not-a-jwt' },
|
|
{ token: jwt({ header: { alg: 'none' } }) },
|
|
{ token: jwt({ claims: { sub: 'system:serviceaccount:other:other' } }) },
|
|
{ token: jwt({ claims: { exp: issuedAt + 601 } }), expirationTimestamp: new Date((issuedAt + 601) * 1_000) },
|
|
{ token: jwt(), expirationTimestamp: new Date((issuedAt + 599) * 1_000) },
|
|
];
|
|
for (const value of cases) {
|
|
const state = fixture(value);
|
|
await assert.rejects(
|
|
state.session.withDelivery(async () => undefined),
|
|
WorkerCredentialKubernetesTokenRequestUnavailableError,
|
|
);
|
|
assert.equal(state.receivedTokens.length, 0);
|
|
assert.equal(state.response.status.token, '');
|
|
}
|
|
});
|
|
|
|
test('maps TokenRequest failures to a stable secret-free error', async () => {
|
|
const state = fixture({ requestError: true });
|
|
await assert.rejects(
|
|
state.session.withDelivery(async () => undefined),
|
|
(error) => {
|
|
assert.ok(
|
|
error instanceof WorkerCredentialKubernetesTokenRequestUnavailableError,
|
|
);
|
|
assert.doesNotMatch(error.message, /sensitive|upstream/i);
|
|
return true;
|
|
},
|
|
);
|
|
assert.equal(state.receivedTokens.length, 0);
|
|
});
|
|
|
|
test('fails closed when restricted client disposal fails', async () => {
|
|
const state = fixture({ disposeError: true });
|
|
await assert.rejects(
|
|
state.session.withDelivery(async () => 'would-have-succeeded'),
|
|
(error) => {
|
|
assert.ok(
|
|
error instanceof WorkerCredentialKubernetesTokenRequestUnavailableError,
|
|
);
|
|
assert.doesNotMatch(error.message, /sensitive|dispose/i);
|
|
return true;
|
|
},
|
|
);
|
|
assert.equal(state.response.status.token, '');
|
|
assert.equal(state.disposals, 1);
|
|
assert.equal(state.active, false);
|
|
state.receivedTokens[0] = '';
|
|
});
|