Repository navigation
fix: judge "Shared across callers" per route, as the boot warning does - #259
Merged
Merged
Conversation
The boot warning and the dashboard badge exist to say the same thing: this content type caches authenticated responses under a key that does not identify the caller. Since #230 the warning has been computed per route, on the hitpass and keys each route resolved to. The badge was still computed from the content type's own hitpass and keys.useAuth, and those disagree with the resolved routes in both directions: - a content type that keeps the default hitpass, with a listed route that sets `hitpass: false` itself and no useAuth: the server warns, the dashboard showed nothing; - a content type with `hitpass: false` and no useAuth whose routes are all listed: those keep the strategy's bypass (listedRouteHitpass), so nothing authenticated is cached and the server does not warn, but the dashboard flagged it. The first is the one that matters. It is a real cross-caller exposure that the badge, which exists because nobody reads boot logs, did not show. Move the predicate into one helper, used by both the warning and the stats service, and expose its result as a new `unkeyedAuthenticatedRoutes` field on each stats row. The table flags a row when that list is non-empty, and falls back to the old expression only when the field is absent. `hitpass` and `keysAuthIdentity` are unchanged and still describe the content type, so anything else that reads them sees the same values. The warning's text and conditions are unchanged.
…r does generateCacheKey adds the caller's identity whenever `keys.useAuth` is truthy. The shared-across-callers predicate asked `useAuth !== true`, so a route configured with `useAuth: "true"` - what env() returns in a JavaScript config - had its entries keyed per caller and was still named in the boot warning and flagged on the dashboard. The badge and warning exist to say a key does not identify the caller; they now ask the same question the key generator answers. Fixed in the predicate rather than by coercing useAuth when the config is resolved. The listed-route hitpass, the Cache-Control downgrade and the stats' keysAuthIdentity all compare `=== true`, so coercing would change what those configurations bypass and emit. Cache keys are untouched. The new jest case first proves the premise on the live cache - Alice misses then hits, Bob misses - and then asserts no warning and an empty route list. Seen red before the change: the boot warning named the route.
The badge is now raised per route, but its tooltip still said "Hitpass is disabled but keys.useAuth is not set". For a content type that keeps its own hitpass and has a listed route that turns it off, that is false, and it points the admin at a content type setting that is already right. When the server sends the per-route list, the tooltip names those routes and says they cache authenticated requests without keys.useAuth, with en and fr strings under a new key. The old string stays for the fallback, where the badge comes from the content type's own hitpass and useAuth and the sentence is exactly true. Seen red before the change: the (A) case's tooltip read "Hitpass is disabled but keys.useAuth is not set, ..." with no route in it.
4 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed and why
The failure
The boot warning and the dashboard's Shared across callers badge are supposed to say the same thing: this content type caches authenticated responses under a key that does not identify the caller, so one user's response can be served to another. The docs say so in three places ("The admin dashboard shows the same thing as a Shared across callers badge, because nobody reads boot logs").
They did not agree.
resolveUserStrategy.ts). Computed per route since Per-content-type maxAge and hitpass are ignored for routes declared in routes #230, on the values each route resolved to:route.method === 'GET' && route.hitpass === false && route.keys?.useAuth !== true.cacheStats.summary()exposed the content type's ownhitpassandkeys.useAuth, andContentTypeTableflaggedhitpass === false && !keysAuthIdentity.A route's resolved values can differ from its content type's in both directions:
hitpass: falseandkeyswithoutuseAuth./api/editors/:idhitpass: false, nouseAuth, and only listed routes. Since #236 those keep the strategy's bypass vialistedRouteHitpass, so authenticated requests are never cached.(A) is the one that matters. It is a real cross-caller exposure that the dashboard did not show.
The fix
server/src/utils/config/unkeyedAuthenticatedRoutes.tsholds the predicate (isUnkeyedAuthenticatedRoute) and the path list (unkeyedAuthenticatedRoutes). The boot warning and the stats service both call it, so they cannot drift again.ContentTypeStatsgainsunkeyedAuthenticatedRoutes: string[]: the routes the boot warning names for that content type.ContentTypeTableflags a row when that list is non-empty, and falls back to the oldhitpass === false && !keysAuthIdentityonly when the field is absent. The field is typed optional for that reason: an admin bundle cached in a browser across an upgrade may meet an older server.keys.useAuthfor truthiness, asgenerateCacheKeydoes, rather than=== true. The badge and warning exist to say a route's key does not identify the caller, so they ask the question the key generator answers. With=== true, a route configured withuseAuth: "true"(whatenv()returns in a JavaScript config) was keyed per caller and still warned about and flagged. This is fixed in the predicate, not by coercinguseAuthwhen the config is resolved: the listed-route hitpass, theCache-Controldowngrade and the stats'keysAuthIdentityall compare=== true, and coercing would change what those configurations bypass and emit. Cache keys are untouched.contentTypes.authRisk.routesHint). The old string is kept for the fallback path, where the badge comes from exactly those content type settings.resolveUserStrategy.tschange is minimal: one import, and the inline filter replaced by the helper call. The predicate, the warning text and when it fires are byte-for-byte the same logic. Open PR fix: treat options set to undefined as omitted #248 also edits this file (it addsdefinedOnly).git merge-treeagainst fix: treat options set to undefined as omitted #248's head reports a clean merge.admin/index.md(badge description),caching/keys.mdandrecipes/authenticated.md(what the warning names), andreference/routes.md(the new field in the/statsexample, plus a paragraph on how it differs fromhitpass/keysAuthIdentity). The TypeDoc API pages regenerate ondocs:build. I did not commit a regeneration because it rewrites every page's source links.Why this is not breaking
hitpassandkeysAuthIdentitystill describe the content type's own settings, with the same values as before. The jest spec asserts this for both cases above. Anything else reading them (the Keyed per caller badge, the widget, external consumers of/rest-cache/stats) sees no difference. The new field is purely additive.useAuth. The only difference is that a route whoseuseAuthis a truthy non-boolean ("true",1) is no longer named. Those routes' keys already identified the caller, so that warning was false. Nothing about what is cached, keys,Cache-Controlor bypass changes for them.Cache-Controlor bypass is touched. No permission changes.Tests
shared/tests/auth-risk-flag-consistency.test.js(jest). Usesjest.mockon the playground'sconfig/cache-strategyto add cases (A)api::writer.editorand (B)api::page.pagewithout touching the shared config. It captures the boot log and, for each case, asserts:ContentTypeTable's expression, new field first and old fallback second;unkeyedAuthenticatedRoutesis exactly["/api/editors/:id"]for (A) and[]for (B).A third case (C) puts
hitpass: falseandkeys.useAuth: "true"(a string) on/api/articles. It first proves on the live cache that the key identifies the caller: Alice misses then hits, and Bob misses. Then it asserts there is no boot warning,unkeyedAuthenticatedRoutesis[], and there is no badge.A fourth test pins
hitpassandkeysAuthIdentityto their old values.e2e/tests/shared-across-callers.spec.ts(Playwright). The playground's strategy must not contain an unkeyed authenticated route, because every other spec on that server relies on it. So this interceptsGET /rest-cache/stats, fetches the real response, and rewrites rows to stage (A), (B) and an old-server response without the field. It then asserts the badge on the rendered dashboard. It also hovers the badge and asserts the tooltip. For (A) it must name/api/editors/:idand must not say "Hitpass is disabled". For the fallback it must keep the old sentence.Before (unfixed
main), jest: both directions of the disagreement:The preconditions on the line before each of those (
bootWarns(...)true for A, false for B) passed. So the server's own judgement was as described, and the failure is the dashboard disagreeing with it.Before (unfixed
main), Playwright:Test 3 passes on
mainby design. It is the regression guard for the fallback, which is the old behaviour.Follow-up commits, each seen red first:
Case (C) against the
=== truepredicate (the MISS/HIT/MISS precondition passed, so the key did identify the caller):Tooltip assertions against the old tooltip:
After:
Closes #
Checklist
shared/tests/that fails before the change. Browser-visible behaviour goes ine2e/tests/.pnpm run test:e2e:memorypasses. (24 suites, 139 tests)pnpm run test:lintpasses (tsc --noEmitoverserver/andadmin/, plus eslint and prettier on the providers).admin/?pnpm run test:adminwas run and passes. (16 passed)docs/guide/updated in this PR.main.Notes for the reviewer
keysAuthIdentity(the Keyed per caller badge) still comparesuseAuth === true, so a content type configured withuseAuth: "true"does not get that badge although its keys are per caller. I left it alone because this PR promises that field is unchanged.buildCacheControlalso compares=== true, so undercacheControl.scope: "public"a route keyed by"true"is not downgraded toprivate. That one is worth its own issue.git merge-tree).