maestro/src/bridge/gateway-mount.auth-boundary.test.ts
oss-sync b857c33ef6
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (f6d625db)
2026-06-26 03:35:45 +00:00

141 lines
5.7 KiB
TypeScript

/**
* APIS-052 — Gateway proxy auth boundary.
*
* The gateway proxies LLM traffic. gateway-mount.test.ts covers path
* classification + config equivalence, but NOT the authentication of proxied
* requests. An unauthenticated request slipping past the gateway's auth layer
* would expose backend LLM spend.
*
* This file asserts the boundary at the gateway's real auth middleware
* (`buildAuthMiddleware` from src/gateway/auth.ts — the exact handler the
* gateway sub-app installs ahead of the proxy). We mount it on a bare express
* app in front of a SENTINEL "backend" handler that records whether it was
* reached. No real LLM call is made.
*
* - missing Bearer → 401, backend NOT reached (no passthrough)
* - malformed Authorization → 401, backend NOT reached
* - wrong/unknown key → 401, backend NOT reached
* - valid config key → 200, backend reached, gatewayAuth populated
* - valid DB key (dbLookup) → 200, backend reached
* - 401 body does not leak which step failed
*/
import { afterEach, describe, expect, it, vi } from 'vitest';
import express from 'express';
import request from 'supertest';
import { createHash } from 'node:crypto';
import { buildAuthMiddleware } from '../gateway/auth.js';
import type { GatewayVirtualKey } from '../gateway/config.js';
const VALID_CONFIG_KEY = 'sk-aao-valid-config-key';
const VALID_DB_KEY = 'sk-aao-valid-db-key';
/** Same hashing the middleware uses for dbLookup (sha256 hex of the bearer). */
function sha256hex(s: string): string {
return createHash('sha256').update(s).digest('hex');
}
/**
* Bare app: [json] → [gateway auth middleware] → [sentinel backend].
* The sentinel is the stand-in for the proxied LLM backend; if auth lets a
* request through, `backend.mock.calls.length` becomes > 0.
*/
function makeGatewayApp(opts: {
keys?: GatewayVirtualKey[];
dbLookup?: (keyHash: string) => { id: string; team: string; allowedModels?: string[] | null } | null;
}) {
const backend = vi.fn((_req: express.Request, res: express.Response) => {
res.status(200).json({ ok: true, served: 'fake-backend' });
});
const app = express();
app.use(express.json());
app.use(buildAuthMiddleware({ keys: opts.keys ?? [], dbLookup: opts.dbLookup }));
// Stand-in for the proxied LLM endpoint.
app.post('/v1/chat/completions', backend);
return { app, backend };
}
const configKeys: GatewayVirtualKey[] = [
{ key: VALID_CONFIG_KEY, team: 'team-a' },
];
describe('APIS-052 gateway proxy auth boundary', () => {
afterEach(() => vi.restoreAllMocks());
it('missing Authorization header → 401 and the backend is never reached', async () => {
const { app, backend } = makeGatewayApp({ keys: configKeys });
const res = await request(app)
.post('/v1/chat/completions')
.send({ model: 'auto', messages: [] });
expect(res.status).toBe(401);
expect(res.body).toEqual({ error: 'invalid api key' });
expect(backend).not.toHaveBeenCalled();
});
it('malformed Authorization (non-Bearer scheme) → 401, no passthrough', async () => {
const { app, backend } = makeGatewayApp({ keys: configKeys });
const res = await request(app)
.post('/v1/chat/completions')
.set('Authorization', `Basic ${VALID_CONFIG_KEY}`)
.send({ model: 'auto' });
expect(res.status).toBe(401);
expect(backend).not.toHaveBeenCalled();
});
it('wrong/unknown Bearer key → 401, no passthrough', async () => {
const { app, backend } = makeGatewayApp({ keys: configKeys });
const res = await request(app)
.post('/v1/chat/completions')
.set('Authorization', 'Bearer sk-aao-totally-wrong')
.send({ model: 'auto' });
expect(res.status).toBe(401);
expect(res.body).toEqual({ error: 'invalid api key' });
expect(backend).not.toHaveBeenCalled();
});
it('valid config key → 200, backend reached', async () => {
const { app, backend } = makeGatewayApp({ keys: configKeys });
const res = await request(app)
.post('/v1/chat/completions')
.set('Authorization', `Bearer ${VALID_CONFIG_KEY}`)
.send({ model: 'auto' });
expect(res.status).toBe(200);
expect(res.body.served).toBe('fake-backend');
expect(backend).toHaveBeenCalledTimes(1);
});
it('valid DB key (via dbLookup) → 200, backend reached', async () => {
const dbLookup = (hash: string) =>
hash === sha256hex(VALID_DB_KEY) ? { id: 'k1', team: 'team-db', allowedModels: null } : null;
const { app, backend } = makeGatewayApp({ keys: [], dbLookup });
const res = await request(app)
.post('/v1/chat/completions')
.set('Authorization', `Bearer ${VALID_DB_KEY}`)
.send({ model: 'auto' });
expect(res.status).toBe(200);
expect(backend).toHaveBeenCalledTimes(1);
});
it('a key revoked from the DB (dbLookup miss) and absent from config → 401', async () => {
// Simulate post-revocation: dbLookup returns null for everything, no config keys.
const dbLookup = () => null;
const { app, backend } = makeGatewayApp({ keys: [], dbLookup });
const res = await request(app)
.post('/v1/chat/completions')
.set('Authorization', `Bearer ${VALID_DB_KEY}`)
.send({ model: 'auto' });
expect(res.status).toBe(401);
expect(backend).not.toHaveBeenCalled();
});
it('401 body is identical for missing vs wrong key (no information leak)', async () => {
const { app } = makeGatewayApp({ keys: configKeys });
const missing = await request(app).post('/v1/chat/completions').send({});
const wrong = await request(app)
.post('/v1/chat/completions')
.set('Authorization', 'Bearer sk-aao-nope')
.send({});
expect(missing.body).toEqual(wrong.body);
expect(missing.status).toBe(wrong.status);
});
});