diff --git a/apps/relay/src/access.ts b/apps/relay/src/access.ts index 7db314a23aa..f3b71d0766b 100644 --- a/apps/relay/src/access.ts +++ b/apps/relay/src/access.ts @@ -1,17 +1,38 @@ +import { parseHostRoutingKey } from "@superset/shared/host-routing"; import { LRUCache } from "lru-cache"; import { createApiClient } from "./api-client"; +import type { AuthContext } from "./auth"; +const ALLOWED_TTL_MS = 15 * 60 * 1000; +const DENIED_TTL_MS = 30 * 1000; + +// Cache by (userId, hostId), not (token, hostId). Tokens rotate on every JWT +// refresh while the underlying user→host authorization is stable, so a +// token-keyed cache effectively expires with each refresh and burns +// host.checkAccess calls on the API for no reason. const allowedCache = new LRUCache({ max: 50_000, - ttl: 5 * 60 * 1000, + ttl: ALLOWED_TTL_MS, +}); +const deniedCache = new LRUCache({ + max: 10_000, + ttl: DENIED_TTL_MS, }); export async function checkHostAccess( + auth: AuthContext, token: string, hostId: string, ): Promise { - const key = `${token}:${hostId}`; + // Short-circuit "not in org" locally: the API does this same check from + // the JWT before hitting the DB, so the round trip is wasted. + const parsed = parseHostRoutingKey(hostId); + if (!parsed) return false; + if (!auth.organizationIds.includes(parsed.organizationId)) return false; + + const key = `${auth.sub}:${hostId}`; if (allowedCache.has(key)) return true; + if (deniedCache.has(key)) return false; try { const client = createApiClient(token); @@ -19,6 +40,8 @@ export async function checkHostAccess( const ok = result.allowed && result.paidPlan; if (ok) { allowedCache.set(key, true); + } else { + deniedCache.set(key, true); } return ok; } catch { diff --git a/apps/relay/src/index.ts b/apps/relay/src/index.ts index 793ef87fa81..7e3a21e0053 100644 --- a/apps/relay/src/index.ts +++ b/apps/relay/src/index.ts @@ -108,7 +108,7 @@ const authMiddleware: MiddlewareHandler = async (c, next) => { return c.json({ error: "Host not connected" }, 503); } - const hasAccess = await checkHostAccess(token, hostId); + const hasAccess = await checkHostAccess(auth, token, hostId); if (!hasAccess) return c.json({ error: "Forbidden" }, 403); c.set("auth", auth); @@ -140,7 +140,7 @@ app.get( return; } - const hasAccess = await checkHostAccess(token, hostId); + const hasAccess = await checkHostAccess(auth, token, hostId); if (!hasAccess) { ws.close(1008, "Forbidden"); return;