test(acl)(#28): integration cases for windowed + stacked usage caps
Some checks failed
Docker image / build-and-push-image (push) Has been cancelled
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:
parent
f332559b59
commit
59295318f8
1 changed files with 119 additions and 1 deletions
|
|
@ -13,7 +13,7 @@ import fs from 'node:fs';
|
|||
import path from 'node:path';
|
||||
|
||||
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 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 -------------------------------------------------------------
|
||||
|
||||
before(() => {
|
||||
|
|
@ -95,6 +127,7 @@ before(() => {
|
|||
|
||||
beforeEach(async () => {
|
||||
// FK-safe truncation order.
|
||||
await prisma.signingLog.deleteMany();
|
||||
await prisma.signingCondition.deleteMany();
|
||||
await prisma.request.deleteMany();
|
||||
await prisma.token.deleteMany();
|
||||
|
|
@ -193,3 +226,88 @@ test('policy rule kind mismatch -> undefined', async () => {
|
|||
test('no KeyUser -> undefined', async () => {
|
||||
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
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue