type Resource = 'users' | 'billing' | 'reports';
type Action = 'read' | 'create' | 'update' | 'delete';
type WildcardPermission = `${Resource}:*` | '*:*';
type StandardPermission = `${Resource}:${Action}`;
export type AppPermission = StandardPermission | WildcardPermission;
interface AuthUser {
id: string;
permissions: AppPermission[];
}
export function checkPermission(user: AuthUser, required: AppPermission): boolean {
if (user.permissions.includes('*:*')) return true;
const [resource] = required.split(':') as [Resource, Action];
if (user.permissions.includes(`${resource}:*`)) return true;
return user.permissions.includes(required);
}Am pățit-o acum vreo doi ani pe o platformă cu vreo 40 de microservicii și vreo 12 roluri interne. Cineva a scris users:udpate în loc de users:update într-un decorator de permisiune, iar la code review ne-a scăpat tuturor fiindcă parametrul era un banal string. Endpoint-ul a rămas complet deschis timp de două zile până când un tester a observat că un cont cu drept de simplu viewer putea trimite payload de patch.
După incidentul ăla am zis că nu mai accept niciun string arbitrar când vine vorba de RBAC. TypeScript a introdus Template Literal Types prin versiunea 4.1, dar multă lume încă le evită crezând că-s doar un moft academic.
Cum construiești matricea de permisiuni
Secretul stă în combinarea tipurilor union primitive. Dacă definești resursele și acțiunile separat, TypeScript face produsul cartezian automat pentru tine fără să scrii sute de linii de cod manual.
Imaginează-ți că ai resursele principale din sistem: utilizatori, facturi și rapoarte. Pe lângă acțiunile CRUD standard, mai vrei permisiuni globale de tip wildcard (cum ar fi admin:*) sau acțiuni specifice pe un namespace anume (de exemplu audit:read).
Când combini aceste uniuni folosind backticks într-un type alias, compilatorul generează toate variantele posibile. În editor primești autocomplete instant: tastezi billing: și primești direct sugestiile valide billing:read, billing:create sau billing:delete. Dacă scrii greșit o singură literă, build-ul crapă imediat.
Trade-off-ul: atenție la explozia combinatorie
Sună idilic, dar vine cu un cost real dacă exagerezi. La un proiect anterior am încercat să mapăm permisiuni de formatul organization:${OrgId}:${Resource}:${SubResource}:${Action}. Când am combinat 15 resurse, 4 acțiuni și câteva niveluri ierarhice, am ajuns la peste 120.000 de combinații teoretice.
Rezultatul? TypeScript serverul din VS Code a început să consume 4GB de RAM și dura aproape 12 secunde să ofere autocomplete. Soluția sănătoasă este să ții template-urile la maxim două sau trei segmente: scope:resource:action. Ce e dinamic (cum ar fi ID-ul organizației sau al utilizatorului) se validează la runtime prin context, nu în definiția statică a tipului.
Un alt minus: TypeScript verifică doar codul tău static. Dacă permisiunile vin dintr-un token JWT decodat sau dintr-un tabel de Postgres, tot ai nevoie de un type guard sau de o schemă Zod la intrare ca să faci cast de la string la tipul tău strict.
Cum arată în producție
Noi folosim o funcție simplă de hasPermission care primește un user și permisiunea cerută. Datorită template literals, refactorizările sunt banale: dacă redenumești resursa invoices în billing, schimbi uniunea într-un singur fișier și TypeScript îți subliniază instant toate cele 30 de locuri din controller-e unde ai rămas cu vechea denumire.
Voi cum gestionați permisiunile în stack-urile voastre? Mergeți pe string-uri validate cu Zod la runtime sau vă bazați pe verificări stricte încă din compiler?