diff --git a/services/webmail/src/app/api/v1/[...path]/route.ts b/services/webmail/src/app/api/v1/[...path]/route.ts index 92280a4..f56a176 100644 --- a/services/webmail/src/app/api/v1/[...path]/route.ts +++ b/services/webmail/src/app/api/v1/[...path]/route.ts @@ -16,12 +16,23 @@ const AUTH_SERVICE_URL = process.env.AUTH_SERVICE_URL ?? "http://auth:3001"; // sees one origin and one cookie either way. const PROTOCOLS_SERVICE_URL = process.env.PROTOCOLS_SERVICE_URL ?? "http://protocols:8080"; +/** + * Admin areas the protocols service owns rather than the auth service. + * + * Each is here because that process holds something the auth service does not: + * the DKIM vault, the certificate watcher, and the DNS resolver plus the + * expected-record spec. + * + * Adding a route to protocols without adding it here sends the request to the + * auth service, which answers 401 from its admin guard - indistinguishable from + * a session problem, and the reason to keep this list beside the services it + * names. + */ +const PROTOCOLS_ADMIN_AREAS = new Set(["dkim", "tls", "dns"]); + function upstreamFor(path: string[]): string { - // Mail reads and sends go to protocols, and so does DKIM: the private key - // has to be sealed with that service's vault, which derives its key - // differently from the auth service's. if (path[0] === "mail") return PROTOCOLS_SERVICE_URL; - if (path[0] === "admin" && (path[1] === "dkim" || path[1] === "tls")) return PROTOCOLS_SERVICE_URL; + if (path[0] === "admin" && PROTOCOLS_ADMIN_AREAS.has(path[1])) return PROTOCOLS_SERVICE_URL; return AUTH_SERVICE_URL; } diff --git a/services/webmail/src/interface-integrity.test.ts b/services/webmail/src/interface-integrity.test.ts index 3c3d452..090c9aa 100644 --- a/services/webmail/src/interface-integrity.test.ts +++ b/services/webmail/src/interface-integrity.test.ts @@ -172,3 +172,26 @@ describe("two-factor enrolment", () => { expect(source).toContain("onContinue"); }); }); + +describe("API proxy routing", () => { + // A route added to the protocols service but not to the proxy's list is sent + // to the auth service instead, which answers 401 from its admin guard. That + // is indistinguishable from a session problem, so the failure looks like + // "you are not logged in" rather than "this was routed to the wrong + // service" - which is exactly how it went unnoticed once already. + it("sends every protocols-owned admin area to the protocols service", () => { + const proxy = readFileSync(resolve(process.cwd(), "src/app/api/v1/[...path]/route.ts"), "utf8"); + + // Areas the protocols service actually serves, taken from its router. + for (const area of ["dkim", "tls", "dns"]) { + expect(proxy).toContain(`"${area}"`); + } + expect(proxy).toMatch(/PROTOCOLS_ADMIN_AREAS/); + }); + + it("still sends mail to protocols and everything else to auth", () => { + const proxy = readFileSync(resolve(process.cwd(), "src/app/api/v1/[...path]/route.ts"), "utf8"); + expect(proxy).toMatch(/path\[0\] === "mail".*PROTOCOLS_SERVICE_URL/s); + expect(proxy).toContain("return AUTH_SERVICE_URL"); + }); +});