Explorar o código

Refactor HttpApi auth middleware wiring (#24168)

Kit Langton hai 4 meses
pai
achega
435becbea0

+ 66 - 0
packages/opencode/src/server/routes/instance/httpapi/auth.ts

@@ -0,0 +1,66 @@
+import { Effect, Encoding, Layer, Redacted, Schema } from "effect"
+import { HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi"
+import { Flag } from "@/flag/flag"
+
+class Unauthorized extends Schema.TaggedErrorClass<Unauthorized>()(
+  "Unauthorized",
+  { message: Schema.String },
+  { httpApiStatus: 401 },
+) {}
+
+export class Authorization extends HttpApiMiddleware.Service<Authorization>()("@opencode/ExperimentalHttpApiAuthorization", {
+  error: Unauthorized,
+  security: {
+    basic: HttpApiSecurity.basic,
+    authToken: HttpApiSecurity.apiKey({ in: "query", key: "auth_token" }),
+  },
+}) {}
+
+const emptyCredential = {
+  username: "",
+  password: Redacted.make(""),
+}
+
+function validateCredential<A, E, R>(
+  effect: Effect.Effect<A, E, R>,
+  credential: { readonly username: string; readonly password: typeof emptyCredential.password },
+) {
+  return Effect.gen(function* () {
+    if (!Flag.OPENCODE_SERVER_PASSWORD) return yield* effect
+
+    if (credential.username !== (Flag.OPENCODE_SERVER_USERNAME ?? "opencode")) {
+      return yield* new Unauthorized({ message: "Unauthorized" })
+    }
+    if (Redacted.value(credential.password) !== Flag.OPENCODE_SERVER_PASSWORD) {
+      return yield* new Unauthorized({ message: "Unauthorized" })
+    }
+    return yield* effect
+  })
+}
+
+function decodeCredential(input: string) {
+  return Encoding.decodeBase64String(input).asEffect().pipe(
+    Effect.match({
+      onFailure: () => emptyCredential,
+      onSuccess: (header) => {
+        const parts = header.split(":")
+        if (parts.length !== 2) return emptyCredential
+        return {
+          username: parts[0],
+          password: Redacted.make(parts[1]),
+        }
+      },
+    }),
+  )
+}
+
+export const authorizationLayer = Layer.succeed(
+  Authorization,
+  Authorization.of({
+    basic: (effect, { credential }) => validateCredential(effect, credential),
+    authToken: (effect, { credential }) =>
+      Effect.gen(function* () {
+        return yield* validateCredential(effect, yield* decodeCredential(Redacted.value(credential)))
+      }),
+  }),
+)

+ 3 - 1
packages/opencode/src/server/routes/instance/httpapi/config.ts

@@ -2,6 +2,7 @@ import { Config } from "@/config"
 import { Provider } from "@/provider"
 import { Effect, Layer } from "effect"
 import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
+import { Authorization } from "./auth"
 
 const root = "/config"
 
@@ -33,7 +34,8 @@ export const ConfigApi = HttpApi.make("config")
           title: "config",
           description: "Experimental HttpApi config routes.",
         }),
-      ),
+      )
+      .middleware(Authorization),
   )
   .annotateMerge(
     OpenApi.annotations({

+ 3 - 1
packages/opencode/src/server/routes/instance/httpapi/file.ts

@@ -1,6 +1,7 @@
 import { File } from "@/file"
 import { Effect, Layer, Schema } from "effect"
 import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
+import { Authorization } from "./auth"
 
 const FileQuery = Schema.Struct({
   path: Schema.String,
@@ -51,7 +52,8 @@ export const FileApi = HttpApi.make("file")
           title: "file",
           description: "Experimental HttpApi file routes.",
         }),
-      ),
+      )
+      .middleware(Authorization),
   )
   .annotateMerge(
     OpenApi.annotations({

+ 3 - 1
packages/opencode/src/server/routes/instance/httpapi/mcp.ts

@@ -1,6 +1,7 @@
 import { MCP } from "@/mcp"
 import { Effect, Layer, Schema } from "effect"
 import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
+import { Authorization } from "./auth"
 
 export const McpPaths = {
   status: "/mcp",
@@ -25,7 +26,8 @@ export const McpApi = HttpApi.make("mcp")
           title: "mcp",
           description: "Experimental HttpApi MCP routes.",
         }),
-      ),
+      )
+      .middleware(Authorization),
   )
   .annotateMerge(
     OpenApi.annotations({

+ 3 - 1
packages/opencode/src/server/routes/instance/httpapi/permission.ts

@@ -2,6 +2,7 @@ import { Permission } from "@/permission"
 import { PermissionID } from "@/permission/schema"
 import { Effect, Layer, Schema } from "effect"
 import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
+import { Authorization } from "./auth"
 
 const root = "/permission"
 
@@ -35,7 +36,8 @@ export const PermissionApi = HttpApi.make("permission")
           title: "permission",
           description: "Experimental HttpApi permission routes.",
         }),
-      ),
+      )
+      .middleware(Authorization),
   )
   .annotateMerge(
     OpenApi.annotations({

+ 3 - 1
packages/opencode/src/server/routes/instance/httpapi/project.ts

@@ -2,6 +2,7 @@ import { Instance } from "@/project/instance"
 import { Project } from "@/project"
 import { Effect, Layer, Schema } from "effect"
 import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
+import { Authorization } from "./auth"
 
 const root = "/project"
 
@@ -33,7 +34,8 @@ export const ProjectApi = HttpApi.make("project")
           title: "project",
           description: "Experimental HttpApi project routes.",
         }),
-      ),
+      )
+      .middleware(Authorization),
   )
   .annotateMerge(
     OpenApi.annotations({

+ 3 - 1
packages/opencode/src/server/routes/instance/httpapi/provider.ts

@@ -6,6 +6,7 @@ import { ProviderID } from "@/provider/schema"
 import { mapValues } from "remeda"
 import { Effect, Layer, Schema } from "effect"
 import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
+import { Authorization } from "./auth"
 
 const root = "/provider"
 
@@ -59,7 +60,8 @@ export const ProviderApi = HttpApi.make("provider")
           title: "provider",
           description: "Experimental HttpApi provider routes.",
         }),
-      ),
+      )
+      .middleware(Authorization),
   )
   .annotateMerge(
     OpenApi.annotations({

+ 3 - 1
packages/opencode/src/server/routes/instance/httpapi/question.ts

@@ -2,6 +2,7 @@ import { Question } from "@/question"
 import { QuestionID } from "@/question/schema"
 import { Effect, Layer, Schema } from "effect"
 import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
+import { Authorization } from "./auth"
 
 const root = "/question"
 
@@ -45,7 +46,8 @@ export const QuestionApi = HttpApi.make("question")
           title: "question",
           description: "Question routes.",
         }),
-      ),
+      )
+      .middleware(Authorization),
   )
   .annotateMerge(
     OpenApi.annotations({

+ 12 - 72
packages/opencode/src/server/routes/instance/httpapi/server.ts

@@ -1,14 +1,14 @@
-import { Effect, Layer, Redacted, Schema } from "effect"
-import { HttpApiBuilder, HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi"
+import { Effect, Layer, Schema } from "effect"
+import { HttpApiBuilder } from "effect/unstable/httpapi"
 import { HttpRouter, HttpServer, HttpServerRequest } from "effect/unstable/http"
 import { AppRuntime } from "@/effect/app-runtime"
 import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
 import { Observability } from "@/effect"
-import { Flag } from "@/flag/flag"
 import { InstanceBootstrap } from "@/project/bootstrap"
 import { Instance } from "@/project/instance"
 import { lazy } from "@/util/lazy"
 import { Filesystem } from "@/util"
+import { authorizationLayer } from "./auth"
 import { ConfigApi, configHandlers } from "./config"
 import { FileApi, fileHandlers } from "./file"
 import { McpApi, mcpHandlers } from "./mcp"
@@ -38,56 +38,6 @@ function decode(input: string) {
   }
 }
 
-class Unauthorized extends Schema.TaggedErrorClass<Unauthorized>()(
-  "Unauthorized",
-  { message: Schema.String },
-  { httpApiStatus: 401 },
-) {}
-
-class Authorization extends HttpApiMiddleware.Service<Authorization>()("@opencode/ExperimentalHttpApiAuthorization", {
-  error: Unauthorized,
-  security: {
-    basic: HttpApiSecurity.basic,
-  },
-}) {}
-
-const normalize = HttpRouter.middleware()(
-  Effect.gen(function* () {
-    return (effect) =>
-      Effect.gen(function* () {
-        const query = yield* HttpServerRequest.schemaSearchParams(Query)
-        if (!query.auth_token) return yield* effect
-        const req = yield* HttpServerRequest.HttpServerRequest
-        const next = req.modify({
-          headers: {
-            ...req.headers,
-            authorization: `Basic ${query.auth_token}`,
-          },
-        })
-        return yield* effect.pipe(Effect.provideService(HttpServerRequest.HttpServerRequest, next))
-      })
-  }),
-).layer
-
-const auth = Layer.succeed(
-  Authorization,
-  Authorization.of({
-    basic: (effect, { credential }) =>
-      Effect.gen(function* () {
-        if (!Flag.OPENCODE_SERVER_PASSWORD) return yield* effect
-
-        const user = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
-        if (credential.username !== user) {
-          return yield* new Unauthorized({ message: "Unauthorized" })
-        }
-        if (Redacted.value(credential.password) !== Flag.OPENCODE_SERVER_PASSWORD) {
-          return yield* new Unauthorized({ message: "Unauthorized" })
-        }
-        return yield* effect
-      }),
-  }),
-)
-
 const instance = HttpRouter.middleware()(
   Effect.gen(function* () {
     return (effect) =>
@@ -110,27 +60,17 @@ const instance = HttpRouter.middleware()(
   }),
 ).layer
 
-const QuestionSecured = QuestionApi.middleware(Authorization)
-const PermissionSecured = PermissionApi.middleware(Authorization)
-const ProjectSecured = ProjectApi.middleware(Authorization)
-const ProviderSecured = ProviderApi.middleware(Authorization)
-const ConfigSecured = ConfigApi.middleware(Authorization)
-const WorkspaceSecured = WorkspaceApi.middleware(Authorization)
-const FileSecured = FileApi.middleware(Authorization)
-const McpSecured = McpApi.middleware(Authorization)
-
 export const routes = Layer.mergeAll(
-  HttpApiBuilder.layer(ConfigSecured).pipe(Layer.provide(configHandlers)),
-  HttpApiBuilder.layer(FileSecured).pipe(Layer.provide(fileHandlers)),
-  HttpApiBuilder.layer(McpSecured).pipe(Layer.provide(mcpHandlers)),
-  HttpApiBuilder.layer(ProjectSecured).pipe(Layer.provide(projectHandlers)),
-  HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(questionHandlers)),
-  HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)),
-  HttpApiBuilder.layer(ProviderSecured).pipe(Layer.provide(providerHandlers)),
-  HttpApiBuilder.layer(WorkspaceSecured).pipe(Layer.provide(workspaceHandlers)),
+  HttpApiBuilder.layer(ConfigApi).pipe(Layer.provide(configHandlers)),
+  HttpApiBuilder.layer(FileApi).pipe(Layer.provide(fileHandlers)),
+  HttpApiBuilder.layer(McpApi).pipe(Layer.provide(mcpHandlers)),
+  HttpApiBuilder.layer(ProjectApi).pipe(Layer.provide(projectHandlers)),
+  HttpApiBuilder.layer(QuestionApi).pipe(Layer.provide(questionHandlers)),
+  HttpApiBuilder.layer(PermissionApi).pipe(Layer.provide(permissionHandlers)),
+  HttpApiBuilder.layer(ProviderApi).pipe(Layer.provide(providerHandlers)),
+  HttpApiBuilder.layer(WorkspaceApi).pipe(Layer.provide(workspaceHandlers)),
 ).pipe(
-  Layer.provide(auth),
-  Layer.provide(normalize),
+  Layer.provide(authorizationLayer),
   Layer.provide(instance),
   Layer.provide(HttpServer.layerServices),
   Layer.provideMerge(Observability.layer),

+ 3 - 1
packages/opencode/src/server/routes/instance/httpapi/workspace.ts

@@ -4,6 +4,7 @@ import { WorkspaceAdaptorEntry } from "@/control-plane/types"
 import * as InstanceState from "@/effect/instance-state"
 import { Effect, Layer, Schema } from "effect"
 import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
+import { Authorization } from "./auth"
 
 const root = "/experimental/workspace"
 export const WorkspacePaths = {
@@ -49,7 +50,8 @@ export const WorkspaceApi = HttpApi.make("workspace")
           title: "workspace",
           description: "Experimental HttpApi workspace routes.",
         }),
-      ),
+      )
+      .middleware(Authorization),
   )
   .annotateMerge(
     OpenApi.annotations({