Skip to content

Commit 39ef3bd

Browse files
committed
fix(auth): prefer a mid-flight published version over the default on read failure
Review catch. The success path re-checks the cache after its await so a concurrent publish wins, but the catch path returned `DEFAULT_VERSION` unconditionally. The default is not the safe fallback it looks like — it is precisely the version a never-bumped org carries. So a lookup that started before a revoke, and then failed, could report `1` while this process already held the bumped counter: the cookie-cache version would still MATCH pre-bump cookies and the just-revoked session would keep being served from cache, which is the one thing the bump exists to stop. The catch now returns the cached value when one is present. It can only be equal to or higher than the default, so it forces the same revalidation or more.
1 parent b237aa4 commit 39ef3bd

2 files changed

Lines changed: 24 additions & 9 deletions

File tree

apps/sim/lib/auth/security-policy.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,19 @@ describe('security policy', () => {
7676
expect(await getSecurityPolicyVersion(orgId)).toBe(7)
7777
})
7878

79-
it('falls back to the default when the read fails', async () => {
79+
it('prefers a version published mid-flight over the default when the read fails', async () => {
80+
const orgId = nextOrgId()
81+
// A revoke/policy save publishes the bump while this read is in flight,
82+
// then the read fails. Returning the default would reproduce the exact
83+
// version a pre-bump cookie carries and keep it matching.
84+
dbChainMockFns.limit.mockImplementationOnce(() => {
85+
setSecurityPolicyVersion(orgId, 6)
86+
throw new Error('connection reset')
87+
})
88+
expect(await getSecurityPolicyVersion(orgId)).toBe(6)
89+
})
90+
91+
it('falls back to the default when the read fails and nothing is cached', async () => {
8092
dbChainMockFns.limit.mockImplementationOnce(() => {
8193
throw new Error('connection reset')
8294
})

apps/sim/lib/auth/security-policy.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,12 @@ const membershipCache = new LRUCache<string, MembershipCacheEntry>({
6666
* planned consumers): any feature that needs cached session cookies to
6767
* re-validate bumps this one counter.
6868
*
69-
* A failed read falls back to the default rather than the last known value.
70-
* That errs toward MORE revalidation, not less: a version that reads lower than
71-
* the stored one mismatches the cookie and forces a database session read.
69+
* A failed read prefers whatever this process last knew over the default. The
70+
* default is not the safe fallback it looks like: it is the version a
71+
* never-bumped org carries, so returning it can MATCH a pre-bump cookie and
72+
* keep a just-revoked session serving from the cookie cache. A retained or
73+
* freshly published value can only be equal or higher, so it either forces the
74+
* same revalidation or more of it.
7275
*/
7376
export async function getSecurityPolicyVersion(
7477
organizationId: string | null | undefined
@@ -96,11 +99,11 @@ export async function getSecurityPolicyVersion(
9699
versionCache.set(organizationId, version)
97100
return version
98101
} catch (error) {
99-
logger.error('Failed to resolve security policy version; using default', {
100-
organizationId,
101-
error,
102-
})
103-
return DEFAULT_VERSION
102+
logger.error('Failed to resolve security policy version', { organizationId, error })
103+
// A publish, or a concurrent read, may have landed while this one was in
104+
// flight. Prefer it — the default could reproduce exactly the version a
105+
// pre-bump cookie carries, leaving a revoked session on the cookie cache.
106+
return versionCache.get(organizationId) ?? DEFAULT_VERSION
104107
}
105108
}
106109

0 commit comments

Comments
 (0)