From 96be8f8d51043cbb49124abcebe39123dbcddd78 Mon Sep 17 00:00:00 2001 From: T3 Code PR Stack <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:14:39 +0200 Subject: [PATCH] fix(server): use single RPC authorization scope table at runtime ws.ts kept a parallel RPC_REQUIRED_SCOPE Map while RpcAuthorization.ts owned the typed RPC_REQUIRED_SCOPES table. Identity methods were added only to the typed table, so production failed with "has no declared authorization scope" for identity.getSessionClaim. Route observeRpc* through requiredScopeForRpcMethod and drop the Map. --- apps/server/src/auth/RpcAuthorization.test.ts | 10 ++ apps/server/src/ws.ts | 112 ++---------------- 2 files changed, 19 insertions(+), 103 deletions(-) diff --git a/apps/server/src/auth/RpcAuthorization.test.ts b/apps/server/src/auth/RpcAuthorization.test.ts index 5aba9783075..a46f3577125 100644 --- a/apps/server/src/auth/RpcAuthorization.test.ts +++ b/apps/server/src/auth/RpcAuthorization.test.ts @@ -12,9 +12,19 @@ import { RPC_REQUIRED_SCOPES, requiredScopeForRpcMethod } from "./RpcAuthorizati describe("RPC authorization scopes", () => { it("declares exactly one scope for every RPC in the server group", () => { + // Single source of truth: ws.ts must call requiredScopeForRpcMethod / RPC_REQUIRED_SCOPES + // and must not keep a parallel Map (that drifted and broke identity.* RPCs in production). expect(new Set(Object.keys(RPC_REQUIRED_SCOPES))).toEqual(new Set(WsRpcGroup.requests.keys())); }); + it("resolves every registered RPC method through requiredScopeForRpcMethod", () => { + for (const method of WsRpcGroup.requests.keys()) { + expect(requiredScopeForRpcMethod(method)).toBe( + RPC_REQUIRED_SCOPES[method as keyof typeof RPC_REQUIRED_SCOPES], + ); + } + }); + it("authorizes background policy reporting and observation deliberately", () => { expect(requiredScopeForRpcMethod(WS_METHODS.serverReportClientActivity)).toBe( AuthOrchestrationReadScope, diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index e1a1a09773d..87ac94ebb0d 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -13,12 +13,7 @@ import * as Stream from "effect/Stream"; import { RpcClientId, DEFAULT_AUTOMATIC_GIT_FETCH_INTERVAL, - AuthOrchestrationOperateScope, AuthOrchestrationReadScope, - AuthReviewWriteScope, - AuthRelayWriteScope, - AuthTerminalOperateScope, - AuthAccessReadScope, AuthAccessStreamError, type AuthAccessStreamEvent, type AiUsageSnapshot, @@ -110,6 +105,7 @@ import * as ReviewService from "./review/ReviewService.ts"; import * as ProjectSetupScriptRunner from "./project/ProjectSetupScriptRunner.ts"; import * as ServerEnvironment from "./environment/ServerEnvironment.ts"; import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; +import { requiredScopeForRpcMethod } from "./auth/RpcAuthorization.ts"; import * as BackgroundPolicy from "./background/BackgroundPolicy.ts"; import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts"; import * as ResourceTelemetry from "./resourceTelemetry/ResourceTelemetry.ts"; @@ -355,94 +351,11 @@ const BOOTSTRAP_WORKTREE_PROJECTION_ATTEMPTS = Math.ceil( // Matches the event store's default page size (DEFAULT_READ_FROM_SEQUENCE_LIMIT). const SHELL_RESUME_MAX_GAP = 1_000; -const RPC_REQUIRED_SCOPE = new Map([ - [ORCHESTRATION_WS_METHODS.dispatchCommand, AuthOrchestrationOperateScope], - [ORCHESTRATION_WS_METHODS.getTurnDiff, AuthOrchestrationReadScope], - [ORCHESTRATION_WS_METHODS.getThreadActivities, AuthOrchestrationReadScope], - [ORCHESTRATION_WS_METHODS.getFullThreadDiff, AuthOrchestrationReadScope], - [ORCHESTRATION_WS_METHODS.searchThreads, AuthOrchestrationReadScope], - [ORCHESTRATION_WS_METHODS.subscribeShell, AuthOrchestrationReadScope], - [ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot, AuthOrchestrationReadScope], - [ORCHESTRATION_WS_METHODS.subscribeThread, AuthOrchestrationReadScope], - [WS_METHODS.serverProbe, AuthOrchestrationReadScope], - [WS_METHODS.serverGetConfig, AuthOrchestrationReadScope], - [WS_METHODS.serverRefreshProviders, AuthOrchestrationOperateScope], - [WS_METHODS.serverUpdateProvider, AuthOrchestrationOperateScope], - [WS_METHODS.serverUpdateServer, AuthOrchestrationOperateScope], - [WS_METHODS.serverUpdateServerWithProgress, AuthOrchestrationOperateScope], - [WS_METHODS.serverUpsertKeybinding, AuthOrchestrationOperateScope], - [WS_METHODS.serverRemoveKeybinding, AuthOrchestrationOperateScope], - [WS_METHODS.serverGetSettings, AuthOrchestrationReadScope], - [WS_METHODS.serverUpdateSettings, AuthOrchestrationOperateScope], - [WS_METHODS.serverDiscoverSourceControl, AuthOrchestrationReadScope], - [WS_METHODS.serverGetTraceDiagnostics, AuthOrchestrationReadScope], - [WS_METHODS.serverGetProcessDiagnostics, AuthOrchestrationReadScope], - [WS_METHODS.serverGetProcessResourceHistory, AuthOrchestrationReadScope], - [WS_METHODS.serverGetHostResourceSnapshot, AuthOrchestrationReadScope], - [WS_METHODS.serverGetResourceTelemetryHistory, AuthOrchestrationReadScope], - [WS_METHODS.serverRetryResourceTelemetry, AuthOrchestrationOperateScope], - [WS_METHODS.serverReportClientActivity, AuthOrchestrationReadScope], - [WS_METHODS.serverReportHostPowerState, AuthOrchestrationOperateScope], - [WS_METHODS.serverGetBackgroundPolicy, AuthOrchestrationReadScope], - [WS_METHODS.subscribeResourceTelemetry, AuthOrchestrationReadScope], - [WS_METHODS.subscribeBackgroundPolicy, AuthOrchestrationReadScope], - [WS_METHODS.serverSignalProcess, AuthOrchestrationOperateScope], - [WS_METHODS.cloudGetRelayClientStatus, AuthRelayWriteScope], - [WS_METHODS.cloudInstallRelayClient, AuthRelayWriteScope], - [WS_METHODS.sourceControlLookupRepository, AuthOrchestrationReadScope], - [WS_METHODS.sourceControlCloneRepository, AuthOrchestrationOperateScope], - [WS_METHODS.sourceControlPublishRepository, AuthOrchestrationOperateScope], - [WS_METHODS.projectsListEntries, AuthOrchestrationReadScope], - [WS_METHODS.projectsReadFile, AuthOrchestrationReadScope], - [WS_METHODS.projectsSearchEntries, AuthOrchestrationReadScope], - [WS_METHODS.projectsWriteFile, AuthOrchestrationOperateScope], - [WS_METHODS.shellOpenInEditor, AuthOrchestrationOperateScope], - [WS_METHODS.filesystemBrowse, AuthOrchestrationReadScope], - [WS_METHODS.assetsCreateUrl, AuthOrchestrationReadScope], - [WS_METHODS.subscribeVcsStatus, AuthOrchestrationReadScope], - [WS_METHODS.vcsRefreshStatus, AuthOrchestrationReadScope], - [WS_METHODS.vcsPull, AuthOrchestrationOperateScope], - [WS_METHODS.gitRunStackedAction, AuthOrchestrationOperateScope], - [WS_METHODS.gitResolvePullRequest, AuthOrchestrationOperateScope], - [WS_METHODS.gitPreparePullRequestThread, AuthOrchestrationOperateScope], - [WS_METHODS.vcsListRefs, AuthOrchestrationReadScope], - [WS_METHODS.vcsResolveBranchChangeRequest, AuthOrchestrationReadScope], - [WS_METHODS.vcsCreateWorktree, AuthOrchestrationOperateScope], - [WS_METHODS.vcsRemoveWorktree, AuthOrchestrationOperateScope], - [WS_METHODS.vcsPreviewWorktreeCleanup, AuthOrchestrationReadScope], - [WS_METHODS.vcsCleanupThreadWorktree, AuthOrchestrationOperateScope], - [WS_METHODS.vcsCreateRef, AuthOrchestrationOperateScope], - [WS_METHODS.vcsSwitchRef, AuthOrchestrationOperateScope], - [WS_METHODS.vcsInit, AuthOrchestrationOperateScope], - [WS_METHODS.reviewGetDiffPreview, AuthReviewWriteScope], - [WS_METHODS.terminalOpen, AuthTerminalOperateScope], - [WS_METHODS.terminalAttach, AuthTerminalOperateScope], - [WS_METHODS.terminalWrite, AuthTerminalOperateScope], - [WS_METHODS.terminalResize, AuthTerminalOperateScope], - [WS_METHODS.terminalClear, AuthTerminalOperateScope], - [WS_METHODS.terminalRestart, AuthTerminalOperateScope], - [WS_METHODS.terminalClose, AuthTerminalOperateScope], - [WS_METHODS.subscribeTerminalEvents, AuthTerminalOperateScope], - [WS_METHODS.subscribeTerminalMetadata, AuthTerminalOperateScope], - [WS_METHODS.previewOpen, AuthOrchestrationOperateScope], - [WS_METHODS.previewNavigate, AuthOrchestrationOperateScope], - [WS_METHODS.previewResize, AuthOrchestrationOperateScope], - [WS_METHODS.previewRefresh, AuthOrchestrationOperateScope], - [WS_METHODS.previewClose, AuthOrchestrationOperateScope], - [WS_METHODS.previewList, AuthOrchestrationReadScope], - // Operate, not read: resolving may publish the port on the tailnet. - [WS_METHODS.previewResolvePort, AuthOrchestrationOperateScope], - [WS_METHODS.previewReportStatus, AuthOrchestrationOperateScope], - [WS_METHODS.previewAutomationConnect, AuthOrchestrationOperateScope], - [WS_METHODS.previewAutomationRespond, AuthOrchestrationOperateScope], - [WS_METHODS.previewAutomationFocusHost, AuthOrchestrationOperateScope], - [WS_METHODS.subscribePreviewEvents, AuthOrchestrationReadScope], - [WS_METHODS.subscribeDiscoveredLocalServers, AuthOrchestrationReadScope], - [WS_METHODS.subscribeAiUsage, AuthOrchestrationReadScope], - [WS_METHODS.subscribeServerConfig, AuthOrchestrationReadScope], - [WS_METHODS.subscribeServerLifecycle, AuthOrchestrationReadScope], - [WS_METHODS.subscribeAuthAccess, AuthAccessReadScope], -]); +// Authorization scopes for every RPC live only in +// `auth/RpcAuthorization.ts` (`RPC_REQUIRED_SCOPES` / `requiredScopeForRpcMethod`). +// Do not reintroduce a parallel Map here — it drifts (identity.* methods were +// registered in the typed table but missing from the runtime Map, so clients +// failed with "has no declared authorization scope"). function toAuthAccessStreamEvent( change: PairingGrantStore.BootstrapCredentialChange | SessionStore.SessionCredentialChange, @@ -585,13 +498,6 @@ const makeWsRpcLayer = ( ? stream : Stream.fail(authorizationError(requiredScope)); }; - const requiredScopeForMethod = (method: string): AuthEnvironmentScope => { - const requiredScope = RPC_REQUIRED_SCOPE.get(method); - if (requiredScope === undefined) { - throw new Error(`RPC method ${method} has no declared authorization scope.`); - } - return requiredScope; - }; const observeRpcEffect = ( method: string, effect: Effect.Effect, @@ -599,7 +505,7 @@ const makeWsRpcLayer = ( ) => instrumentRpcEffect( method, - authorizeEffect(requiredScopeForMethod(method), effect), + authorizeEffect(requiredScopeForRpcMethod(method), effect), traceAttributes, ); const observeRpcStream = ( @@ -609,7 +515,7 @@ const makeWsRpcLayer = ( ) => instrumentRpcStream( method, - authorizeStream(requiredScopeForMethod(method), stream), + authorizeStream(requiredScopeForRpcMethod(method), stream), traceAttributes, ); const observeRpcStreamEffect = ( @@ -623,7 +529,7 @@ const makeWsRpcLayer = ( ) => instrumentRpcStreamEffect( method, - authorizeEffect(requiredScopeForMethod(method), effect), + authorizeEffect(requiredScopeForRpcMethod(method), effect), traceAttributes, ); const toDispatchCommandError = (cause: unknown, fallbackMessage: string) =>