Přeskočit na obsah

RBAC cache — invalidace je no-op, když klíč dostal prefix a delete ne

import { Aside } from ‘@astrojs/starlight/components’;

Admin odebere uživateli právo (nebo změní scope práva role) — a uživatel s právem dál normálně pracuje ještě až 5 minut. Žádná chyba v logu; po chvíli se to „samo spraví” (vyprší TTL cache), takže se bug dlouho tváří jako flaky chování, ne jako díra v invalidaci.

In-memory permission cache dostala při přechodu na multi-tenant tenant prefix v klíči, ale cílená invalidace zůstala z single-tenant éry:

// zápis do cache — klíč S prefixem
const cacheKey = `${tenantCacheKey()}:${userId}`;
userPermissionCache.set(cacheKey, map);
// invalidace — klíč BEZ prefixu → nikdy nic nesmaže
export function clearUserPermissionCache(userId: string) {
userPermissionCache.delete(userId); // no-op!
}

Map.delete() s neexistujícím klíčem tiše vrátí false — nic nespadne, typecheck projde, testy „cache funguje” projdou. Jediné, co zmizelo, je efekt invalidace. Stejný vzor postihl i rolovou cache (delete(role) vs. klíč tenant:role) a jedno místo invalidaci nemělo vůbec (smazání role).

Druhá vrstva téhož problému: in-memory invalidace zafunguje jen v procesu, který právo změnil. Práva ale mění i jiné procesy (seed skripty, CLI, případný PM2 cluster) — tam žádný Map.delete() běžící aplikaci nedoběhne.

  1. Tenant-aware mazání podle suffixu — iterace Map je levná a over-invalidace bezpečná (stojí jeden SELECT navíc):
export function clearUserPermissionCache(userId: string) {
const suffix = `:${userId}`;
for (const key of userPermissionCache.keys()) {
if (key.endsWith(suffix)) userPermissionCache.delete(key);
}
}

Mazání „s aktuálním tenant prefixem” je past — zapisovatel může běžet pod jinou podobou klíče (slug fallback vs. dbName, CLI/master-admin kontext) a miss = tichý návrat bugu. Suffix obsahuje oddělovač :, takže :SALES netrefí :PRESALES.

  1. Cross-process verzovací klíč v DB — key-value řádek rbac_cache_version v tenant DB. Každý zapisovatel práv (API routy i seed skripty) po zápisu verzi bumpne; čtenář ji throttlovaně ověří při přístupu do cache (1 indexovaný SELECT / ~10 s / tenant / proces) a při změně zahodí cache celého tenanta. Efekt: vlastní proces okamžitě, ostatní procesy do ~10 s místo 5 min. Bez nové tabulky a migrace.

Jak se tomu vyvarovat v jiných systémech

Sekce “Jak se tomu vyvarovat v jiných systémech”
  • Detection: grepni každou new Map() cache na páry set(/delete( a porovnej, jak se klíč skládá — jakýkoli rozdíl v konstrukci klíče mezi zápisem a mazáním = bug. Code review otázka: „kdo všechno tenhle klíč maže a skládá ho stejně?”
  • Anti-pattern: klíč se skládá inline template stringem na víc místech; invalidace přijímá „logické id” (userId), zatímco cache klíčuje kompozitem (tenant:userId). TTL maskuje rozbitou invalidaci.
  • Lepší přístup: jediná funkce pro tvorbu klíče + invalidace mazáním podle suffixu/prefixu; deterministický test okamžitého grant/revoke (změna v DB + invalidace → hned účinná, bez čekání na TTL); u více procesů (cluster, seed skripty) verzovací klíč v DB kontrolovaný při cache-hitu, ne spoléhání na in-memory delete.

Sister bugs / související

Sekce “Sister bugs / související”
Přidal aiarchitekt.cz · 21. 7. 2026 2:00
Provozuje aiarchitekt.cz