mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(01-05): enforce MCP health read scope
- require health:read or wildcard for MCP read capability - reject insufficient scopes before downstream gates and server construction
This commit is contained in:
1 parent
8ca6ebd773
commit
20a6731c8b
3 files changed
+23
-8
No files matched your search
+13
-1
@@ -45,7 +45,11 @@
|
||||
*/
|
||||
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
||||
|
||||
import { createMcpServer, resolveMcpAuthContext } from "@/lib/mcp";
|
||||
import {
|
||||
createMcpServer,
|
||||
resolveMcpAuthContext,
|
||||
SCOPE_HEALTH_READ,
|
||||
} from "@/lib/mcp";
|
||||
import { withBackgroundEvent } from "@/lib/logging/background";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { isModuleEnabled } from "@/lib/modules/gate";
|
||||
@@ -163,6 +167,14 @@ async function handleMcp(request: Request): Promise<Response> {
|
||||
return unauthorized(request);
|
||||
}
|
||||
|
||||
// Authorization is distinct from credential validity. A write-only or
|
||||
// unrelated bearer is still a real token, but it must not reach rate
|
||||
// limiting, module discovery, server construction, or any health reader.
|
||||
if (!ctx.canRead) {
|
||||
annotate({ action: { name: "mcp.auth.insufficient_scope" } });
|
||||
return unauthorized(request, SCOPE_HEALTH_READ);
|
||||
}
|
||||
|
||||
// 2. RATE LIMIT — per `<userId>:<tokenId>` credential binding.
|
||||
const rl = await checkMcpRateLimit(ctx.binding);
|
||||
if (!rl.allowed) {
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@ export interface McpAuthContext {
|
||||
scopes: string[];
|
||||
/** `<user_id>:<token_id>` — the session binding (REQ-SEC-11). */
|
||||
binding: string;
|
||||
/** Whether this session may read (always true for a valid token). */
|
||||
/** Whether this session may read (requires `health:read` or wildcard). */
|
||||
canRead: boolean;
|
||||
/** Whether this session may write (requires `health:write`; gates the live write tools). */
|
||||
canWrite: boolean;
|
||||
@@ -61,7 +61,7 @@ export async function resolveMcpAuthContext(
|
||||
tokenId,
|
||||
scopes: permissions,
|
||||
binding: sessionBinding(user.id, tokenId),
|
||||
canRead: tokenAllowsRead(),
|
||||
canRead: tokenAllowsRead(permissions),
|
||||
canWrite: tokenAllowsWrite(permissions),
|
||||
};
|
||||
}
|
||||
@@ -28,12 +28,15 @@ export const SCOPE_HEALTH_READ = "health:read";
|
||||
export const SCOPE_HEALTH_WRITE = "health:write";
|
||||
|
||||
/**
|
||||
* Whether a token's scopes admit the read surface. Any valid token qualifies —
|
||||
* the underlying reads are unscoped — so this is permissive by design and exists
|
||||
* to document the contract and to give a single place to tighten later.
|
||||
* Whether a token's scopes admit the read surface. Reads require the explicit
|
||||
* `health:read` grant or the wildcard; write and unrelated grants never imply
|
||||
* read access.
|
||||
*/
|
||||
export function tokenAllowsRead(): boolean {
|
||||
return true;
|
||||
export function tokenAllowsRead(permissions: readonly string[]): boolean {
|
||||
return (
|
||||
permissions.includes(SCOPE_WILDCARD) ||
|
||||
permissions.includes(SCOPE_HEALTH_READ)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user