test(acl)(#28): integration cases for windowed + stacked usage caps
Some checks failed
Docker image / build-and-push-image (push) Has been cancelled

9 cases: under/at limit, signings outside the window excluded, uncapped,
lifetime (null window) all-time count, kind-specific counting, both
stacked-cap directions (hourly binds vs daily binds), and the
record->count->deny loop via recordSigning. 22 integration + 7 unit green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-20 21:51:05 +02:00
commit 59295318f8

View file

@ -13,7 +13,7 @@ import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import prisma from '../src/db'; import prisma from '../src/db';
import { checkIfPubkeyAllowed } from '../src/daemon/lib/acl/index'; import { checkIfPubkeyAllowed, recordSigning } from '../src/daemon/lib/acl/index';
const KEY = 'test-key'; const KEY = 'test-key';
const PUB = 'client-pubkey-1'; const PUB = 'client-pubkey-1';
@ -74,6 +74,38 @@ async function seedCondition(
}); });
} }
type RuleSeed = { method: string; kind: string | null; maxUsageCount?: number | null; windowSeconds?: number | null };
async function seedPolicyRules(rules: RuleSeed[]): Promise<number> {
const policy = await prisma.policy.create({
data: {
name: 'p',
rules: {
create: rules.map((r) => ({
method: r.method,
kind: r.kind ?? undefined,
maxUsageCount: r.maxUsageCount ?? null,
windowSeconds: r.windowSeconds ?? null,
})),
},
},
});
return policy.id;
}
async function seedSigningLog(
keyUserId: number,
method: string,
kind: string | null,
count: number,
ageMs = 0,
): Promise<void> {
const createdAt = new Date(Date.now() - ageMs);
for (let i = 0; i < count; i++) {
await prisma.signingLog.create({ data: { keyUserId, method, kind, createdAt } });
}
}
// ---- lifecycle ------------------------------------------------------------- // ---- lifecycle -------------------------------------------------------------
before(() => { before(() => {
@ -95,6 +127,7 @@ before(() => {
beforeEach(async () => { beforeEach(async () => {
// FK-safe truncation order. // FK-safe truncation order.
await prisma.signingLog.deleteMany();
await prisma.signingCondition.deleteMany(); await prisma.signingCondition.deleteMany();
await prisma.request.deleteMany(); await prisma.request.deleteMany();
await prisma.token.deleteMany(); await prisma.token.deleteMany();
@ -193,3 +226,88 @@ test('policy rule kind mismatch -> undefined', async () => {
test('no KeyUser -> undefined', async () => { test('no KeyUser -> undefined', async () => {
assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), undefined); assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), undefined);
}); });
// ---- usage caps (#28) ------------------------------------------------------
test('usage cap: under the limit -> allowed', async () => {
const pid = await seedPolicyRules([{ method: 'sign_event', kind: '1', maxUsageCount: 3, windowSeconds: 3600 }]);
const ku = await seedKeyUser();
await seedToken(ku, pid, {});
await seedSigningLog(ku, 'sign_event', '1', 2); // 2 < 3
assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), true);
});
test('usage cap: at the limit -> denied', async () => {
const pid = await seedPolicyRules([{ method: 'sign_event', kind: '1', maxUsageCount: 3, windowSeconds: 3600 }]);
const ku = await seedKeyUser();
await seedToken(ku, pid, {});
await seedSigningLog(ku, 'sign_event', '1', 3); // 3 >= 3
assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), undefined);
});
test('usage cap: signings outside the window do not count', async () => {
const pid = await seedPolicyRules([{ method: 'sign_event', kind: '1', maxUsageCount: 2, windowSeconds: 3600 }]);
const ku = await seedKeyUser();
await seedToken(ku, pid, {});
await seedSigningLog(ku, 'sign_event', '1', 5, 2 * 3600 * 1000); // 5, aged 2h (outside 1h)
await seedSigningLog(ku, 'sign_event', '1', 1); // 1 recent
assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), true); // only 1 in window
});
test('uncapped rule -> allowed regardless of log volume', async () => {
const pid = await seedPolicyRules([{ method: 'sign_event', kind: '1', maxUsageCount: null }]);
const ku = await seedKeyUser();
await seedToken(ku, pid, {});
await seedSigningLog(ku, 'sign_event', '1', 100);
assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), true);
});
test('lifetime cap (windowSeconds null) counts all-time', async () => {
const pid = await seedPolicyRules([{ method: 'sign_event', kind: '1', maxUsageCount: 2, windowSeconds: null }]);
const ku = await seedKeyUser();
await seedToken(ku, pid, {});
await seedSigningLog(ku, 'sign_event', '1', 2, 10 * 24 * 3600 * 1000); // 2, aged 10 days
assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), undefined); // lifetime: 2 >= 2
});
test('kind-specific cap counts only that kind', async () => {
const pid = await seedPolicyRules([{ method: 'sign_event', kind: '1', maxUsageCount: 2, windowSeconds: 3600 }]);
const ku = await seedKeyUser();
await seedToken(ku, pid, {});
await seedSigningLog(ku, 'sign_event', '2', 5); // kind 2 — irrelevant to a kind-1 cap
await seedSigningLog(ku, 'sign_event', '1', 1);
assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), true); // kind-1 count = 1 < 2
});
test('stacked caps: the hourly cap binds even when the daily is fine', async () => {
const pid = await seedPolicyRules([
{ method: 'sign_event', kind: '1', maxUsageCount: 20, windowSeconds: 3600 },
{ method: 'sign_event', kind: '1', maxUsageCount: 200, windowSeconds: 86400 },
]);
const ku = await seedKeyUser();
await seedToken(ku, pid, {});
await seedSigningLog(ku, 'sign_event', '1', 20); // hits the 20/hr cap; well under 200/day
assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), undefined);
});
test('stacked caps: the daily cap binds even when the hourly is fine', async () => {
const pid = await seedPolicyRules([
{ method: 'sign_event', kind: '1', maxUsageCount: 20, windowSeconds: 3600 },
{ method: 'sign_event', kind: '1', maxUsageCount: 200, windowSeconds: 86400 },
]);
const ku = await seedKeyUser();
await seedToken(ku, pid, {});
await seedSigningLog(ku, 'sign_event', '1', 15); // hourly: 15 < 20 OK
await seedSigningLog(ku, 'sign_event', '1', 190, 2 * 3600 * 1000); // +190 aged 2h: in day, not hour
// hourly = 15 (OK), daily = 205 >= 200 -> denied
assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), undefined);
});
test('recordSigning feeds the cap: record then re-check denies at the limit', async () => {
const pid = await seedPolicyRules([{ method: 'sign_event', kind: '1', maxUsageCount: 1, windowSeconds: 3600 }]);
const ku = await seedKeyUser();
await seedToken(ku, pid, {});
assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), true); // 0 < 1
await recordSigning(KEY, PUB, 'sign_event', signEvt); // now 1 recorded
assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), undefined); // 1 >= 1
});