Spaces:
Runtime error
Runtime error
File size: 9,100 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | /**
* T-013 β GET /api/settings/authz-inventory.
*
* Covers spec AC-1, AC-2, AC-12 (and AC-13 PATCH coverage continues to live
* in `tests/unit/settings/authz-bypass.test.ts`; this file only asserts the
* inventory endpoint itself).
*
* - AC-1 response shape: 5 tiers, each with prefixes; bypassEnabled / bypassPrefixes / spawnCapablePrefixes present.
* - AC-2 bypass-state flags match getSettings() and update after PATCH.
* - AC-12 anonymous request β 401/403 (no inventory leak).
*/
import test from "node:test";
import assert from "node:assert/strict";
import { setupSettingsFixture } from "../_mocks/settings.ts";
import { makeManagementSessionRequest } from "../../helpers/managementSession.ts";
const fixture = setupSettingsFixture("authz-inventory");
process.env.OMNIROUTE_DISABLE_REDIS_AUTH_CACHE = "1";
const ORIGINAL_JWT_SECRET = process.env.JWT_SECRET;
const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD;
const core = await import("../../../src/lib/db/core.ts");
const settingsDb = await import("../../../src/lib/db/settings.ts");
const runtime = await import("../../../src/lib/config/runtimeSettings.ts");
const inventoryRoute = await import("../../../src/app/api/settings/authz-inventory/route.ts");
const apiKeysDb = await import("../../../src/lib/db/apiKeys.ts");
test.beforeEach(async () => {
await fixture.resetStorage();
apiKeysDb.resetApiKeyState();
runtime.resetRuntimeSettingsStateForTests();
});
test.after(() => {
core.resetDbInstance();
fixture.cleanup();
if (ORIGINAL_JWT_SECRET === undefined) delete process.env.JWT_SECRET;
else process.env.JWT_SECRET = ORIGINAL_JWT_SECRET;
if (ORIGINAL_INITIAL_PASSWORD === undefined) delete process.env.INITIAL_PASSWORD;
else process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD;
});
// βββ AC-1 β shape βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
test("AC-1: GET returns 5 tiers with prefixes + bypass state envelope", async () => {
process.env.JWT_SECRET = "test-jwt-secret-authz-inventory";
process.env.INITIAL_PASSWORD = "initial-pass-ac1";
await settingsDb.updateSettings({ requireLogin: true });
const request = await makeManagementSessionRequest(
"http://localhost/api/settings/authz-inventory",
{ method: "GET" }
);
const response = await inventoryRoute.GET(request);
assert.equal(response.status, 200);
const body = (await response.json()) as {
tiers: Array<{ name: string; prefixes: string[]; description: string; bypassable: boolean }>;
bypassEnabled: boolean;
bypassPrefixes: string[];
spawnCapablePrefixes: string[];
};
assert.equal(body.tiers.length, 5);
const names = body.tiers.map((t) => t.name).sort();
assert.deepEqual(names, ["ALWAYS_PROTECTED", "CLIENT_API", "LOCAL_ONLY", "MANAGEMENT", "PUBLIC"]);
const localOnly = body.tiers.find((t) => t.name === "LOCAL_ONLY");
assert.ok(localOnly);
assert.ok(localOnly!.prefixes.includes("/api/mcp/"));
assert.ok(localOnly!.prefixes.includes("/api/cli-tools/runtime/"));
assert.equal(localOnly!.bypassable, true);
const alwaysProtected = body.tiers.find((t) => t.name === "ALWAYS_PROTECTED");
assert.ok(alwaysProtected);
assert.ok(alwaysProtected!.prefixes.includes("/api/shutdown"));
assert.equal(alwaysProtected!.bypassable, false);
// Every tier carries a non-empty description.
for (const tier of body.tiers) {
assert.ok(tier.description.length > 0, `tier ${tier.name} missing description`);
}
assert.ok(body.spawnCapablePrefixes.includes("/api/cli-tools/runtime/"));
});
// βββ AC-2 β flags match getSettings() pre- and post-mutation ββββββββββββββ
test("AC-2: bypassEnabled + bypassPrefixes reflect getSettings() (defaults)", async () => {
process.env.JWT_SECRET = "test-jwt-secret-authz-inventory";
process.env.INITIAL_PASSWORD = "initial-pass-ac2a";
await settingsDb.updateSettings({ requireLogin: true });
const response = await inventoryRoute.GET(
await makeManagementSessionRequest("http://localhost/api/settings/authz-inventory", {
method: "GET",
})
);
assert.equal(response.status, 200);
const body = (await response.json()) as {
bypassEnabled: boolean;
bypassPrefixes: string[];
};
// Default snapshot: kill-switch ON, single prefix /api/mcp/.
assert.equal(body.bypassEnabled, true);
assert.deepEqual(body.bypassPrefixes, ["/api/mcp/"]);
});
test("AC-2: bypassEnabled flips after settings mutation", async () => {
process.env.JWT_SECRET = "test-jwt-secret-authz-inventory";
process.env.INITIAL_PASSWORD = "initial-pass-ac2b";
await settingsDb.updateSettings({ requireLogin: true });
// Mutate directly through the settings DB (bypasses the password gate β
// we are not testing the gate here, only the inventory's reflection of
// the persisted state).
await settingsDb.updateSettings({ localOnlyManageScopeBypassEnabled: false });
const response = await inventoryRoute.GET(
await makeManagementSessionRequest("http://localhost/api/settings/authz-inventory", {
method: "GET",
})
);
assert.equal(response.status, 200);
const body = (await response.json()) as { bypassEnabled: boolean };
assert.equal(body.bypassEnabled, false);
});
test("AC-2: bypassPrefixes additions land in the inventory", async () => {
process.env.JWT_SECRET = "test-jwt-secret-authz-inventory";
process.env.INITIAL_PASSWORD = "initial-pass-ac2c";
await settingsDb.updateSettings({ requireLogin: true });
await settingsDb.updateSettings({
localOnlyManageScopeBypassPrefixes: ["/api/mcp/", "/api/mcp/v2/"],
});
const response = await inventoryRoute.GET(
await makeManagementSessionRequest("http://localhost/api/settings/authz-inventory", {
method: "GET",
})
);
assert.equal(response.status, 200);
const body = (await response.json()) as { bypassPrefixes: string[] };
assert.deepEqual(body.bypassPrefixes, ["/api/mcp/", "/api/mcp/v2/"]);
});
// βββ AC-12 β anonymous request rejected (no inventory leak) βββββββββββββββ
test("AC-12: anonymous request (no cookie, no Bearer) β 401", async () => {
process.env.JWT_SECRET = "test-jwt-secret-authz-inventory";
process.env.INITIAL_PASSWORD = "initial-pass-ac12";
// Bootstrap a password so isAuthRequired() returns true even on loopback.
await settingsDb.updateSettings({ requireLogin: true });
const { ensurePersistentManagementPasswordHash } =
await import("../../../src/lib/auth/managementPassword.ts");
await ensurePersistentManagementPasswordHash({ source: "test.bootstrap" });
const anonRequest = new Request("https://dashboard.example/api/settings/authz-inventory", {
method: "GET",
});
const response = await inventoryRoute.GET(anonRequest);
assert.ok(
response.status === 401 || response.status === 403,
`expected 401/403, got ${response.status}`
);
const body = (await response.json()) as { error?: { message?: string } };
// Should NOT leak the inventory shape.
assert.ok(!("tiers" in body));
});
test("AC-12: anonymous request with bogus Bearer β 403", async () => {
process.env.JWT_SECRET = "test-jwt-secret-authz-inventory";
process.env.INITIAL_PASSWORD = "initial-pass-ac12b";
await settingsDb.updateSettings({ requireLogin: true });
const { ensurePersistentManagementPasswordHash } =
await import("../../../src/lib/auth/managementPassword.ts");
await ensurePersistentManagementPasswordHash({ source: "test.bootstrap" });
const bogus = new Request("https://dashboard.example/api/settings/authz-inventory", {
method: "GET",
headers: new Headers({ authorization: "Bearer not-a-real-key" }),
});
const response = await inventoryRoute.GET(bogus);
assert.equal(response.status, 403);
});
// βββ OQ-5 β any valid API key (no manage scope required) β 200 ββββββββββββ
test("OQ-5: any valid API key (read-only scope) β 200 inventory", async () => {
process.env.JWT_SECRET = "test-jwt-secret-authz-inventory";
process.env.INITIAL_PASSWORD = "initial-pass-oq5";
await settingsDb.updateSettings({ requireLogin: true });
const { ensurePersistentManagementPasswordHash } =
await import("../../../src/lib/auth/managementPassword.ts");
await ensurePersistentManagementPasswordHash({ source: "test.bootstrap" });
// Key with NO manage scope β would be rejected by /api/settings PATCH,
// but the inventory read endpoint is intentionally one rung lower (OQ-5).
const created = await apiKeysDb.createApiKey("oq5-read", "machine-oq5", []);
const request = new Request("https://dashboard.example/api/settings/authz-inventory", {
method: "GET",
headers: new Headers({ authorization: `Bearer ${created.key}` }),
});
const response = await inventoryRoute.GET(request);
assert.equal(response.status, 200);
const body = (await response.json()) as { tiers: unknown[] };
assert.equal(body.tiers.length, 5);
});
|