mirror of https://github.com/garrytan/gstack.git
147 lines
5.7 KiB
TypeScript
147 lines
5.7 KiB
TypeScript
// Allowlist tests — codex flagged identity canonicalization gaps.
|
|
|
|
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
import { mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from 'fs';
|
|
import { tmpdir } from 'os';
|
|
import { join } from 'path';
|
|
import {
|
|
loadAllowlist,
|
|
findEntry,
|
|
hasCapability,
|
|
grantIdentity,
|
|
revokeIdentity,
|
|
saveAllowlist,
|
|
} from '../src/allowlist';
|
|
|
|
let tmpDir: string;
|
|
let listPath: string;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = mkdtempSync(join(tmpdir(), 'ios-qa-allowlist-'));
|
|
listPath = join(tmpDir, 'allowlist.json');
|
|
});
|
|
|
|
describe('Allowlist', () => {
|
|
test('loadAllowlist returns empty on missing file', async () => {
|
|
const list = await loadAllowlist(listPath);
|
|
expect(list).toEqual({ version: 1, entries: [] });
|
|
});
|
|
|
|
test('saveAllowlist writes mode 0600 JSON', async () => {
|
|
await saveAllowlist({
|
|
version: 1,
|
|
entries: [{ identity: 'user@example.com', capabilities: ['observe'], expires_at: null }],
|
|
}, listPath);
|
|
expect(existsSync(listPath)).toBe(true);
|
|
const raw = readFileSync(listPath, 'utf-8');
|
|
expect(JSON.parse(raw).entries[0].identity).toBe('user@example.com');
|
|
});
|
|
|
|
test('findEntry matches exact identity', async () => {
|
|
const list = {
|
|
version: 1 as const,
|
|
entries: [{ identity: 'user@example.com', capabilities: ['mutate' as const], expires_at: null }],
|
|
};
|
|
expect(findEntry(list, 'user@example.com')?.identity).toBe('user@example.com');
|
|
expect(findEntry(list, 'USER@example.com')).toBeNull(); // exact-match only
|
|
expect(findEntry(list, 'unknown@example.com')).toBeNull();
|
|
});
|
|
|
|
test('findEntry skips expired entries', async () => {
|
|
const list = {
|
|
version: 1 as const,
|
|
entries: [
|
|
{ identity: 'expired', capabilities: ['observe' as const], expires_at: new Date(Date.now() - 60_000).toISOString() },
|
|
],
|
|
};
|
|
expect(findEntry(list, 'expired')).toBeNull();
|
|
});
|
|
|
|
test('findEntry accepts future expiry', async () => {
|
|
const list = {
|
|
version: 1 as const,
|
|
entries: [
|
|
{ identity: 'future', capabilities: ['observe' as const], expires_at: new Date(Date.now() + 60_000).toISOString() },
|
|
],
|
|
};
|
|
expect(findEntry(list, 'future')?.identity).toBe('future');
|
|
});
|
|
|
|
test('hasCapability is tier-aware', async () => {
|
|
const list = {
|
|
version: 1 as const,
|
|
entries: [
|
|
{ identity: 'restore-user', capabilities: ['restore' as const], expires_at: null },
|
|
{ identity: 'observe-user', capabilities: ['observe' as const], expires_at: null },
|
|
],
|
|
};
|
|
expect(hasCapability(list, 'restore-user', 'observe')).toBe(true);
|
|
expect(hasCapability(list, 'restore-user', 'interact')).toBe(true);
|
|
expect(hasCapability(list, 'restore-user', 'mutate')).toBe(true);
|
|
expect(hasCapability(list, 'restore-user', 'restore')).toBe(true);
|
|
expect(hasCapability(list, 'observe-user', 'observe')).toBe(true);
|
|
expect(hasCapability(list, 'observe-user', 'interact')).toBe(false);
|
|
expect(hasCapability(list, 'observe-user', 'mutate')).toBe(false);
|
|
expect(hasCapability(list, 'observe-user', 'restore')).toBe(false);
|
|
});
|
|
|
|
test('grantIdentity adds a new entry', async () => {
|
|
await grantIdentity({
|
|
identity: 'new@example.com',
|
|
capability: 'interact',
|
|
path: listPath,
|
|
});
|
|
const list = await loadAllowlist(listPath);
|
|
expect(list.entries).toHaveLength(1);
|
|
expect(list.entries[0]!.identity).toBe('new@example.com');
|
|
expect(list.entries[0]!.capabilities).toContain('interact');
|
|
});
|
|
|
|
test('grantIdentity upgrades an existing entry', async () => {
|
|
await grantIdentity({ identity: 'u', capability: 'observe', path: listPath });
|
|
await grantIdentity({ identity: 'u', capability: 'restore', path: listPath });
|
|
const list = await loadAllowlist(listPath);
|
|
expect(list.entries).toHaveLength(1);
|
|
expect(list.entries[0]!.capabilities).toContain('restore');
|
|
});
|
|
|
|
test('grantIdentity with ttl sets expires_at', async () => {
|
|
await grantIdentity({ identity: 'u', capability: 'observe', ttlSeconds: 3600, path: listPath });
|
|
const list = await loadAllowlist(listPath);
|
|
const exp = Date.parse(list.entries[0]!.expires_at!);
|
|
expect(exp).toBeGreaterThan(Date.now());
|
|
expect(exp).toBeLessThan(Date.now() + 3700 * 1000);
|
|
});
|
|
|
|
test('revokeIdentity removes the entry', async () => {
|
|
await grantIdentity({ identity: 'u', capability: 'observe', path: listPath });
|
|
await revokeIdentity('u', listPath);
|
|
const list = await loadAllowlist(listPath);
|
|
expect(list.entries).toHaveLength(0);
|
|
});
|
|
|
|
// Codex-flagged identity canonicalization variants — verify the matcher
|
|
// works for each.
|
|
test('user identity, tagged node, node key, expired node all canonicalize distinctly', async () => {
|
|
const list = {
|
|
version: 1 as const,
|
|
entries: [
|
|
{ identity: 'alice@example.com', capabilities: ['observe' as const], expires_at: null },
|
|
{ identity: 'tag:ci', capabilities: ['mutate' as const], expires_at: null },
|
|
{ identity: 'node:abcdef0123', capabilities: ['observe' as const], expires_at: null },
|
|
{ identity: 'bob@example.com', capabilities: ['observe' as const], expires_at: new Date(Date.now() - 1000).toISOString() },
|
|
],
|
|
};
|
|
expect(hasCapability(list, 'alice@example.com', 'observe')).toBe(true);
|
|
expect(hasCapability(list, 'tag:ci', 'mutate')).toBe(true);
|
|
expect(hasCapability(list, 'node:abcdef0123', 'observe')).toBe(true);
|
|
expect(hasCapability(list, 'bob@example.com', 'observe')).toBe(false); // expired
|
|
expect(hasCapability(list, 'tag:CI', 'mutate')).toBe(false); // case-sensitive — canonicalize before lookup
|
|
});
|
|
});
|
|
|
|
import { afterEach } from 'bun:test';
|
|
afterEach(() => {
|
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|