Files
qinglong/packages/ql3-cluster-admin/test/workerCredentialKubernetesTokenRequest.test.cjs

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] = '';
});