mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 22:22:57 +03:00
fix(auth+build): Bearer manage scope on management routes + lazy-load deepseek PoW solver (#2308)
Integrated into release/v3.8.0
This commit is contained in:
@@ -9,9 +9,17 @@ import { dirname, join } from "node:path";
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Load the exact solver extracted from DeepSeek's worker chunk
|
||||
// Load the exact solver extracted from DeepSeek's worker chunk.
|
||||
// Lazy-loaded inside the function so the standalone Next build can collect
|
||||
// page data without executing a dynamic require() at module-load time.
|
||||
const require = createRequire(import.meta.url);
|
||||
const { U } = require(join(__dirname, "deepseek-pow-solver.cjs"));
|
||||
let _U: any | undefined;
|
||||
function loadU(): any {
|
||||
if (_U === undefined) {
|
||||
_U = require(join(__dirname, "deepseek-pow-solver.cjs")).U;
|
||||
}
|
||||
return _U;
|
||||
}
|
||||
|
||||
export function solveDeepSeekPow(
|
||||
algorithm: string,
|
||||
@@ -23,6 +31,7 @@ export function solveDeepSeekPow(
|
||||
if (algorithm !== "DeepSeekHashV1") throw new Error(`Unsupported: ${algorithm}`);
|
||||
const prefix = `${salt}_${expireAt}_`;
|
||||
|
||||
const U = loadU();
|
||||
const createHash = () => {
|
||||
const self: any = {};
|
||||
self._sponge = new U({ capacity: 256, padding: 6 });
|
||||
|
||||
@@ -3,11 +3,21 @@ import { createErrorResponse } from "@/lib/api/errorResponse";
|
||||
import { extractApiKey, isValidApiKey } from "@/sse/services/auth";
|
||||
import { getApiKeyMetadata } from "@/lib/db/apiKeys";
|
||||
import { isCliTokenAuthValid } from "@/lib/middleware/cliTokenAuth";
|
||||
import {
|
||||
MANAGE_SCOPE,
|
||||
hasManageScope as hasManageScopeShared,
|
||||
} from "@/shared/constants/managementScopes";
|
||||
|
||||
export const MANAGE_SCOPE = "manage";
|
||||
export { MANAGE_SCOPE };
|
||||
|
||||
/**
|
||||
* Check whether any of the supplied scopes authorizes management API access.
|
||||
*
|
||||
* Re-exported here for backwards compatibility with existing callers. The
|
||||
* canonical definition lives in `@/shared/constants/managementScopes`.
|
||||
*/
|
||||
export function hasManageScope(scopes: string[] = []): boolean {
|
||||
return scopes.includes("manage") || scopes.includes("admin");
|
||||
return hasManageScopeShared(scopes);
|
||||
}
|
||||
|
||||
export async function requireManagementAuth(request: Request): Promise<Response | null> {
|
||||
|
||||
@@ -9,6 +9,25 @@ export function isLoopback(ip: string): boolean {
|
||||
return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a header value preferring the Request's own headers (works in any
|
||||
* context — App Router request handlers, unit tests, raw fetch) and falling
|
||||
* back to `next/headers` only when the request object isn't carrying them.
|
||||
*
|
||||
* Calling `headers()` outside a request scope throws (see Next.js
|
||||
* `next-dynamic-api-wrong-context`), so we guard the import.
|
||||
*/
|
||||
async function readHeader(request: Request, name: string): Promise<string | null> {
|
||||
const fromRequest = request.headers?.get(name);
|
||||
if (fromRequest != null) return fromRequest;
|
||||
try {
|
||||
const hdrs = await headers();
|
||||
return hdrs.get(name);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the CLI machine-id token sent by the local omniroute CLI.
|
||||
* Only accepted from loopback IPs. Disabled via OMNIROUTE_DISABLE_CLI_TOKEN=true.
|
||||
@@ -16,13 +35,13 @@ export function isLoopback(ip: string): boolean {
|
||||
export async function isCliTokenAuthValid(request: Request): Promise<boolean> {
|
||||
if (process.env.OMNIROUTE_DISABLE_CLI_TOKEN === "true") return false;
|
||||
|
||||
const hdrs = await headers();
|
||||
const token = hdrs.get(HEADER_NAME);
|
||||
const token = await readHeader(request, HEADER_NAME);
|
||||
if (!token || token.length !== 32) return false;
|
||||
|
||||
// Only allow loopback origin — check forwarded-for, real-ip, then host header.
|
||||
const ip =
|
||||
(hdrs.get("x-forwarded-for") ?? "").split(",")[0].trim() || hdrs.get("x-real-ip") || "";
|
||||
const forwardedFor = (await readHeader(request, "x-forwarded-for")) ?? "";
|
||||
const realIp = (await readHeader(request, "x-real-ip")) ?? "";
|
||||
const ip = forwardedFor.split(",")[0].trim() || realIp;
|
||||
if (ip && !isLoopback(ip)) return false;
|
||||
|
||||
let expected: string;
|
||||
|
||||
31
src/shared/constants/managementScopes.ts
Normal file
31
src/shared/constants/managementScopes.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Management API key scopes — the set of API key scopes that authorize a
|
||||
* Bearer key on management routes (`/api/*` excluding `/api/v1/*` and the
|
||||
* public allowlist).
|
||||
*
|
||||
* Single source of truth shared by:
|
||||
* - `src/lib/api/requireManagementAuth.ts` (`hasManageScope`)
|
||||
* - `src/shared/utils/apiAuth.ts` (`validateBearerApiKeyForManagement`)
|
||||
*
|
||||
* Keep both helpers in sync by importing `MANAGEMENT_API_KEY_SCOPES` from
|
||||
* here — never re-declare the list inline.
|
||||
*/
|
||||
|
||||
/** Canonical scope name granted to the default environment key. */
|
||||
export const MANAGE_SCOPE = "manage";
|
||||
|
||||
/**
|
||||
* Set of scopes that grant access to management API routes.
|
||||
* `admin` is treated as a superset of `manage`.
|
||||
*/
|
||||
export const MANAGEMENT_API_KEY_SCOPES = new Set<string>(["manage", "admin"]);
|
||||
|
||||
/**
|
||||
* Check whether any of the given scopes authorizes management API access.
|
||||
*/
|
||||
export function hasManageScope(scopes: readonly string[] = []): boolean {
|
||||
for (const scope of scopes) {
|
||||
if (MANAGEMENT_API_KEY_SCOPES.has(scope)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -160,6 +160,35 @@ async function validateBearerApiKey(apiKey: string | null): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a Bearer API key is valid AND carries a scope that authorizes
|
||||
* it on management API routes (`/api/*` excluding `/api/v1/*` and the public
|
||||
* allowlist). Returns `false` for unscoped keys so that the existing
|
||||
* default-deny posture on management routes is preserved.
|
||||
*
|
||||
* Scope set is sourced from `@/shared/constants/managementScopes` so this
|
||||
* helper stays in lockstep with `requireManagementAuth.hasManageScope`.
|
||||
*/
|
||||
async function validateBearerApiKeyForManagement(apiKey: string | null): Promise<boolean> {
|
||||
if (!apiKey) return false;
|
||||
|
||||
try {
|
||||
const [{ validateApiKey, getApiKeyMetadata }, { hasManageScope }] = await Promise.all([
|
||||
import("@/lib/db/apiKeys"),
|
||||
import("@/shared/constants/managementScopes"),
|
||||
]);
|
||||
const valid = await validateApiKey(apiKey);
|
||||
if (!valid) return false;
|
||||
|
||||
const metadata = await getApiKeyMetadata(apiKey);
|
||||
if (!metadata) return false;
|
||||
|
||||
return hasManageScope(metadata.scopes);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isManagementApiRequest(request: RequestLike | Request): boolean {
|
||||
const pathname = getRequestPathname(request);
|
||||
if (!pathname?.startsWith("/api/")) return false;
|
||||
@@ -221,6 +250,9 @@ export async function verifyAuth(request: any): Promise<string | null> {
|
||||
|
||||
const bearerToken = getBearerToken(request);
|
||||
if (isManagementApiRequest(request)) {
|
||||
if (await validateBearerApiKeyForManagement(bearerToken)) {
|
||||
return null;
|
||||
}
|
||||
return bearerToken ? "Invalid management token" : "Authentication required";
|
||||
}
|
||||
|
||||
@@ -250,11 +282,12 @@ export async function isAuthenticated(request: Request): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
const bearerToken = getBearerToken(request);
|
||||
if (isManagementApiRequest(request)) {
|
||||
return false;
|
||||
return validateBearerApiKeyForManagement(bearerToken);
|
||||
}
|
||||
|
||||
return validateBearerApiKey(getBearerToken(request));
|
||||
return validateBearerApiKey(bearerToken);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -155,6 +155,65 @@ test("isAuthenticated rejects bearer API keys on management routes", async () =>
|
||||
assert.equal(result, false);
|
||||
});
|
||||
|
||||
test("verifyAuth accepts bearer API keys with manage scope on management routes", async () => {
|
||||
const key = await apiKeysDb.createApiKey("mcp-management", "machine1234567890", ["manage"]);
|
||||
const result = await apiAuth.verifyAuth({
|
||||
cookies: {
|
||||
get() {
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
headers: new Headers({ authorization: `Bearer ${key.key}` }),
|
||||
url: "https://example.com/api/providers",
|
||||
});
|
||||
|
||||
assert.equal(result, null);
|
||||
});
|
||||
|
||||
test("verifyAuth accepts bearer API keys with admin scope on management routes", async () => {
|
||||
const key = await apiKeysDb.createApiKey("mcp-admin", "machine1234567890", ["admin"]);
|
||||
const result = await apiAuth.verifyAuth({
|
||||
cookies: {
|
||||
get() {
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
headers: new Headers({ authorization: `Bearer ${key.key}` }),
|
||||
url: "https://example.com/api/settings",
|
||||
});
|
||||
|
||||
assert.equal(result, null);
|
||||
});
|
||||
|
||||
test("verifyAuth still rejects unscoped bearer API keys on management routes", async () => {
|
||||
const key = await apiKeysDb.createApiKey("integration-no-scope", "machine1234567890");
|
||||
const result = await apiAuth.verifyAuth({
|
||||
cookies: {
|
||||
get() {
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
headers: new Headers({ authorization: `Bearer ${key.key}` }),
|
||||
url: "https://example.com/api/providers",
|
||||
});
|
||||
|
||||
assert.equal(result, "Invalid management token");
|
||||
});
|
||||
|
||||
test("isAuthenticated accepts bearer API keys with manage scope on management routes", async () => {
|
||||
process.env.INITIAL_PASSWORD = "bootstrap-password";
|
||||
await localDb.updateSettings({ requireLogin: true, password: "" });
|
||||
|
||||
const key = await apiKeysDb.createApiKey("mcp-management", "machine1234567890", ["manage"]);
|
||||
const request = new Request("https://example.com/api/providers", {
|
||||
headers: { authorization: `Bearer ${key.key}` },
|
||||
});
|
||||
|
||||
const result = await apiAuth.isAuthenticated(request);
|
||||
|
||||
assert.equal(result, true);
|
||||
});
|
||||
|
||||
test("monitoring health reset route requires dashboard authentication", async () => {
|
||||
process.env.INITIAL_PASSWORD = "bootstrap-password";
|
||||
await localDb.updateSettings({ requireLogin: true, password: "" });
|
||||
|
||||
Reference in New Issue
Block a user