Declare what a per-viewer verdict rests on once, at the assignment matcher
AssignmentMatcher implements CacheableDependencyInterface so a consumer can
merge "what a match rests on". Those three methods take no account, so they
report the user cache context — which partitions per uid and never
invalidates — and cannot name the account's own tag. Candidacy reads the
account's roles, so every consumer was left to know that and add the tag
itself.
Five did, each with its own comment saying why:
WorkItemManager::checkActionAccess(), OperationAccessCheck,
InstanceReadAccessCheck, InboxController::checkReassignAccess() and
AssignmentCacheTrait.
The sixth already existed. OrchestraInteractionHandler::refuseSubmitThatCanDoNothing()
disables the webform submit and warns the visitor, and declared that decision's
cacheability by hand as two lines: the token list tag and the user context.
The verdict it declares for is OperationResumer::checkCompleteAccess(), which
reads four things — the token's state, the resolved tenant, the account's roles,
and standing cover that lapses on the clock. Three of the four were undeclared,
so a page cached while a stand-in could not yet complete the step kept telling
them it was not theirs after their cover began, and kept offering the submit
after it lapsed or their role was revoked. Not a bypass — preSave() re-checks
— but the delegation case silently blocks the person brought in to cover.
The resumers carried no cacheability on the stated grounds that the answer is "never merged into a render array", which stopped being true when [#3621674] rendered it into a form.
The fix is one seam
AssignmentMatcher::getAccountCacheability(AccountInterface) returns the whole
set for an account, and the five consumers merge that one thing instead of
restating part of it. checkCompleteAccess() now carries it, and the webform
handler merges whatever the verdict carried rather than naming a subset. The
refusal is decided before its cacheability is applied, because deciding is what
gathers it — one of the answers is which token is live.
OperationAccessCheckTest already asserted all three things the matcher now
supplies, and stays green unchanged, which is what says the seam preserves the
contract. A new test pins it at the matcher, so a consumer added later inherits
the whole set by asking.
And two tenant checks, from the other direction
InstanceTenantAccessCheck and IncidentTenantAccessCheck both ended in
setCacheMaxAge(0) — the safe answer to "what does this rest on?" and the
expensive one, since it makes every page behind the check uncacheable in order
to say the realm comes from the request. TenantContext is a declared
cacheable dependency; the tree already treats it as one in TenantCacheContext,
in the matcher, and in this very webform handler.
The second half is the part the max-age was hiding: the administrator branch
declared cachePerPermissions() and the tenant branch declared nothing, so a
verdict stored for an account without the permission would not have moved when
it was granted. Removing one without the other would have been the wrong half,
so both branches now share one dependency set.
A missed site of the lane [#3621292] closed.
Tests
Three, each proven red without its fix and green with it:
- the matcher answers for an account, and the verdict carries exactly what the matcher gave it, so the two cannot drift;
- the built webform form declares the realm, the account's tag and the actor context — asserted on the assignee's own build, the one a cache stores, since a refused build throws;
- all six tenant verdicts (run and token through one check, incident through the other, each as an administrator and as a plain account) are cacheable, name the realm and declare the permission context — plus a second test that the confinement itself still refuses another realm, so the cacheability was not bought by allowing everything.
The tenant test creates user 1 first and uses it as the administrator, because a plain account created first would be the superuser and every verdict would take the administer branch and prove nothing.