3ebdbfec81
- F1 [medium — the substantive one]: hasRestrictedPagesInWorkspace is now UNCACHED (a plain EXISTS per call, like its sibling hasRestrictedPagesInSpace). Caching it (even 5s) reintroduced an access-control leak the space path never had: a concurrent whole-workspace read in the insert->commit window of the FIRST restricted page could re-populate `false` under withCache (read-then-set, no del-during-read guard) and override the insert-time bust, leaking that page to unauthorized users for up to the TTL. Uncaching removes both the DB/cache asymmetry and the TOCTOU race; the space path already accepts this per-call cost. Reverted the now-unnecessary insertPageAccess cache-bust and removed the dead HAS_RESTRICTED_PAGES_IN_WORKSPACE cache key. - F2 [test]: page-permission-workspace-filter.int-spec.ts (real PG) — the short-circuit returns the full input set with zero restrictions AND filters out the page the user can't reach when a restriction is present (proving the authz behavior is unchanged), the 0->1 transition flips immediately, and the flag is per-workspace scoped. - F3 [doc]: documented the deploy-time write-lock in the migration header — the non-CONCURRENT GIN trigram builds take a SHARE lock that blocks writes on pages/users/… for minutes on a large tenant; run in a maintenance window or build CONCURRENTLY out-of-band for big installs. - F4 [doc]: corrected the jwt.strategy comment — the reused req.raw.workspace is the middleware's selectAll superset (not "the exact row this query returns"), harmless because AuthWorkspace already preferred that object. Gate: server tsc 0; the new int-spec 3/3 on real Postgres. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
25 lines
1.2 KiB
TypeScript
25 lines
1.2 KiB
TypeScript
export const CacheKey = {
|
|
LICENSE_VALID: (workspaceId: string) => `license:valid:${workspaceId}`,
|
|
SPACE_ROLES: (userId: string, spaceId: string) =>
|
|
`perm:space-roles:${userId}:${spaceId}`,
|
|
PAGE_CAN_EDIT: (userId: string, pageId: string) =>
|
|
`perm:can-edit:${userId}:${pageId}`,
|
|
// #348 — DomainMiddleware workspace resolution. Self-hosted resolves the single
|
|
// workspace (constant key); cloud resolves by the request subdomain (lowercased
|
|
// to match the case-insensitive `LOWER(hostname)` lookup). Every WorkspaceRepo
|
|
// mutator busts these, so staleness is bounded by both explicit invalidation and
|
|
// the short TTL below.
|
|
WORKSPACE_SELF_HOSTED: 'workspace:self-hosted',
|
|
WORKSPACE_BY_HOST: (subdomain: string) =>
|
|
`workspace:byhost:${subdomain.toLowerCase()}`,
|
|
};
|
|
|
|
// Permission caches dedupe repeated checks within and across short request bursts.
|
|
// 5s keeps staleness on revocations bounded.
|
|
export const PERMISSION_CACHE_TTL_MS = 5_000;
|
|
|
|
// #348 — workspace row changes rarely; a short TTL bounds staleness of
|
|
// security-relevant fields (enforceSso/enforceMfa/status) even if an explicit
|
|
// bust is ever missed, while still removing the per-request workspace query.
|
|
export const WORKSPACE_CACHE_TTL_MS = 15_000;
|