File size: 2,717 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
import { type AccessScope } from "@/lib/accessTokens/scopes";

/**
 * Required-scope inference for remote CLI access tokens.
 *
 * Policy (owner-confirmed 2026-06-19): infer from HTTP method —
 *   - GET/HEAD/OPTIONS → read
 *   - POST/PUT/PATCH/DELETE → write
 * with two admin overrides:
 *   - ADMIN_SCOPE_PREFIXES: admin for ANY method (inherently sensitive surfaces).
 *   - ADMIN_MUTATION_PREFIXES: admin only when mutating; GET/HEAD stay read so a
 *     `read` token can still inspect status under these prefixes.
 *
 * This covers every existing management route with zero per-route edits. A NEW
 * mutating route nasce exigindo `write` por padrão (não `admin`) — quando uma
 * rota nova for sensível, adicione seu prefixo a uma das listas abaixo.
 *
 * Note: this only governs the access-token credential path. Dashboard JWT, the
 * loopback CLI machine-id token, and manage-scope API keys remain full-access.
 * Loopback-only routes that spawn processes are blocked before auth regardless.
 */

/** Sensitive management surfaces — require `admin` for ALL methods. */
export const ADMIN_SCOPE_PREFIXES: readonly string[] = [
  "/api/cli/tokens", // access-token management (create/list/revoke)
  "/api/oauth", // OAuth authorization flows
  "/api/auth", // login / logout / session
  "/api/policy", // policy engine
  "/api/services", // embedded-service lifecycle (also loopback-blocked)
  "/api/mcp", // MCP process surface (also loopback-blocked)
];

/** Require `admin` only for mutating methods; GET/HEAD under these stay `read`. */
export const ADMIN_MUTATION_PREFIXES: readonly string[] = [
  "/api/providers", // POST add provider / rotate key = admin; GET status = read
  "/api/cli-tools/apply", // writes config onto the host filesystem
];

const READ_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);

function matchesPrefix(path: string, prefixes: readonly string[]): boolean {
  // Exact match or a true path-segment boundary (`pre/...`). A bare
  // `startsWith(pre)` would over-match lookalikes (e.g. "/api/auth" vs
  // "/api/authz-inventory"), so it is intentionally NOT used.
  return prefixes.some((pre) => path === pre || path.startsWith(pre + "/"));
}

/**
 * Determine the minimum access-token scope required to call `method path`.
 * Pure + deterministic — safe to unit test exhaustively.
 */
export function inferRequiredScope(method: string, path: string): AccessScope {
  const m = (method || "GET").toUpperCase();
  const p = path || "/";

  if (matchesPrefix(p, ADMIN_SCOPE_PREFIXES)) return "admin";

  const isMutation = !READ_METHODS.has(m);
  if (isMutation && matchesPrefix(p, ADMIN_MUTATION_PREFIXES)) return "admin";

  return isMutation ? "write" : "read";
}