Spaces:
Runtime error
Runtime error
File size: 10,578 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 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | /**
* Issue #2257 β clientApi policy behavior when an invalid Bearer is sent and
* REQUIRE_API_KEY=false.
*
* The existing `client-api-policy.test.ts` shares a DB-backed setup via
* `resetStorage()` and `apiKeysDb` that has SQLite migration races on this
* branch. This standalone file mocks `validateApiKey` to test the policy's
* fallback branch in isolation β no DB, no migration runner.
*/
import test from "node:test";
import assert from "node:assert/strict";
import Module from "node:module";
// βββ Mock validateApiKey via require interception (so the dynamic import in
// the policy module returns our stub instead of hitting the real DB module) β
type ValidateFn = (key: string) => boolean | Promise<boolean>;
let mockValidateApiKey: ValidateFn = () => false;
const originalResolve = (Module as unknown as { _resolveFilename: typeof Module._resolveFilename })
._resolveFilename;
// Intercept require() / import() resolution for the apiKeys DB module and
// substitute it for our stub. This runs only for the exact path the policy
// imports β production code paths are unaffected.
const POLICY_IMPORT_TARGET = "src/lib/db/apiKeys";
(Module as unknown as { _resolveFilename: typeof Module._resolveFilename })._resolveFilename =
function patched(this: unknown, request: string, ...rest: unknown[]) {
if (request.includes(POLICY_IMPORT_TARGET)) {
// Resolve to a stub file we create below
const stubPath = new URL("./__stub_apiKeys.mjs", import.meta.url).pathname;
// @ts-expect-error - rest spread to original
return originalResolve.call(this, stubPath, ...rest);
}
// @ts-expect-error - rest spread to original
return originalResolve.call(this, request, ...rest);
};
// Write the stub file ad-hoc (Node's loader needs a real file)
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omr-clientapi-policy-fallback-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const STUB_PATH = path.join(__dirname, "__stub_apiKeys.mjs");
fs.writeFileSync(
STUB_PATH,
`export const validateApiKey = (key) => globalThis.__mockValidateApiKey(key);\n`
);
// Wire the stub to our local variable
(globalThis as unknown as { __mockValidateApiKey: ValidateFn }).__mockValidateApiKey = (key) =>
mockValidateApiKey(key);
test.after(() => {
try {
fs.unlinkSync(STUB_PATH);
} catch {
/* ignore */
}
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = ORIGINAL_DATA_DIR;
});
// βββ Load policy fresh (after the interceptor is in place) ββββββββββββββββ
async function loadPolicy() {
const mod = await import(`../../../src/server/authz/policies/clientApi.ts?ts=${Date.now()}`);
return mod.clientApiPolicy;
}
function ctx(headers: Headers, normalizedPath = "/api/v1/chat/completions") {
return {
request: { method: "POST", headers, url: `http://localhost${normalizedPath}` },
classification: {
routeClass: "CLIENT_API" as const,
reason: "client_api_v1" as const,
normalizedPath,
},
requestId: "req_test",
};
}
// βββ Tests ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
test.beforeEach(() => {
// Default to "every key fails" β individual tests override as needed.
mockValidateApiKey = () => false;
delete process.env.REQUIRE_API_KEY;
});
test("#2257 β invalid bearer + REQUIRE_API_KEY=true β 401", async () => {
process.env.REQUIRE_API_KEY = "true";
const policy = await loadPolicy();
const headers = new Headers({ authorization: "Bearer sk-stub-bogus" });
const out = await policy.evaluate(ctx(headers));
assert.equal(out.allow, false);
if (!out.allow) {
assert.equal(out.status, 401);
assert.equal(out.code, "AUTH_002");
}
});
test("#2257 β invalid bearer + REQUIRE_API_KEY=false β anonymous (with warning log)", async () => {
const originalWarn = console.warn;
const warnings: string[] = [];
console.warn = (msg: string) => warnings.push(String(msg));
try {
const policy = await loadPolicy();
const headers = new Headers({ authorization: "Bearer sk-stub-bogus" });
const out = await policy.evaluate(ctx(headers));
assert.equal(out.allow, true);
if (out.allow) {
assert.equal(out.subject.kind, "anonymous");
assert.equal(out.subject.id, "local");
}
assert.ok(
warnings.some((w) => w.includes("[clientApiPolicy]") && w.includes("REQUIRE_API_KEY=false")),
"expected a warning about the fallback"
);
} finally {
console.warn = originalWarn;
}
});
test("#2257 β invalid x-api-key + REQUIRE_API_KEY=false β anonymous (with warning log)", async () => {
const originalWarn = console.warn;
const warnings: string[] = [];
console.warn = (msg: string) => warnings.push(String(msg));
try {
const policy = await loadPolicy();
const headers = new Headers({ "x-api-key": "sk-stub-bogus" });
const out = await policy.evaluate(ctx(headers));
assert.equal(out.allow, true);
if (out.allow) {
assert.equal(out.subject.kind, "anonymous");
assert.equal(out.subject.id, "local");
}
assert.ok(
warnings.some((w) => w.includes("[clientApiPolicy]") && w.includes("REQUIRE_API_KEY=false")),
"expected a warning about the fallback"
);
} finally {
console.warn = originalWarn;
}
});
test("#2257 β fallback warning masks the x-api-key (only last-4 in log)", async () => {
const originalWarn = console.warn;
const warnings: string[] = [];
console.warn = (msg: string) => warnings.push(String(msg));
try {
const policy = await loadPolicy();
const headers = new Headers({ "x-api-key": "sk-secretprefix-secretmiddle-XYZW" });
const out = await policy.evaluate(ctx(headers));
assert.equal(out.allow, true);
assert.ok(
warnings.every((w) => !w.includes("secretprefix") && !w.includes("secretmiddle")),
"warning leaked the full bearer; only masked key id should be logged"
);
assert.ok(
warnings.some((w) => w.includes("key_XYZW")),
"expected masked key id (last-4) in the warning"
);
} finally {
console.warn = originalWarn;
}
});
test("#2257 β fallback warning masks the bearer (only last-4 in log)", async () => {
const originalWarn = console.warn;
const warnings: string[] = [];
console.warn = (msg: string) => warnings.push(String(msg));
try {
const policy = await loadPolicy();
const headers = new Headers({ authorization: "Bearer sk-secretprefix-secretmiddle-XYZW" });
const out = await policy.evaluate(ctx(headers));
assert.equal(out.allow, true);
assert.ok(
warnings.every((w) => !w.includes("secretprefix") && !w.includes("secretmiddle")),
"warning leaked the full bearer; only masked key id should be logged"
);
assert.ok(
warnings.some((w) => w.includes("key_XYZW")),
"expected masked key id (last-4) in the warning"
);
} finally {
console.warn = originalWarn;
}
});
test("#2257 β no bearer + REQUIRE_API_KEY=false β anonymous (unchanged, no fallback warning)", async () => {
const originalWarn = console.warn;
const warnings: string[] = [];
console.warn = (msg: string) => warnings.push(String(msg));
try {
const policy = await loadPolicy();
const out = await policy.evaluate(ctx(new Headers()));
assert.equal(out.allow, true);
if (out.allow) {
assert.equal(out.subject.kind, "anonymous");
}
// No warning should fire when no bearer is sent in the first place β
// the warning is specifically for the "invalid-bearer-fell-through" case.
assert.ok(
warnings.every((w) => !w.includes("[clientApiPolicy]")),
"no fallback warning expected when no bearer was sent"
);
} finally {
console.warn = originalWarn;
}
});
// βββ #3504 β non-usable Authorization must NOT short-circuit the URL path token β
// VS Code Copilot sends its own (empty / non-OmniRoute) Authorization header even
// when the OmniRoute key lives in the URL path of a /vscode tokenized endpoint.
// A non-"Bearer <token>" Authorization must fall through to the URL token instead
// of returning null and 401'ing under REQUIRE_API_KEY=true.
// validateApiKey is the real (no-DB β always-false) implementation here, so we
// distinguish "URL token was extracted" from "no token found" by the rejection
// MESSAGE: an extracted-but-unknown token β "Invalid API key"; nothing extracted
// β "Authentication required". On the pre-fix code a non-Bearer Authorization
// returned null, so these would all 401 with "Authentication required".
test("#3504 β empty 'Bearer ' Authorization falls through to the URL path token", async () => {
process.env.REQUIRE_API_KEY = "true";
const policy = await loadPolicy();
const headers = new Headers({ authorization: "Bearer " });
const out = await policy.evaluate(
ctx(headers, "/api/v1/vscode/sk-url-token/chat/completions")
);
assert.equal(out.allow, false);
if (!out.allow) {
assert.equal(out.status, 401);
assert.equal(
out.message,
"Invalid API key",
"URL token must be extracted (β 'Invalid API key'), not skipped (β 'Authentication required')"
);
}
});
test("#3504 β a non-Bearer scheme (Basic) also falls through to the URL token", async () => {
process.env.REQUIRE_API_KEY = "true";
const policy = await loadPolicy();
const headers = new Headers({ authorization: "Basic Zm9vOmJhcg==" });
const out = await policy.evaluate(
ctx(headers, "/api/v1/vscode/sk-url-token/chat/completions")
);
assert.equal(out.allow, false);
if (!out.allow) assert.equal(out.message, "Invalid API key");
});
test("#3504 β non-Bearer Authorization with NO URL token still rejects as unauthenticated", async () => {
process.env.REQUIRE_API_KEY = "true";
const policy = await loadPolicy();
const headers = new Headers({ authorization: "Bearer " });
const out = await policy.evaluate(ctx(headers, "/api/v1/chat/completions"));
assert.equal(out.allow, false);
if (!out.allow) assert.equal(out.message, "Authentication required");
});
|