Simas Razinskas

Kelių nuomininkų SaaS su izoliacija, užtikrinama kiekvienoje užklausoje

Nuomininkų duomenų nutekėjimo prevencija struktūrine izoliacija: kiekviena užklausa ir API kelias apribojami dar prieš verslo logikai galint suklysti.

Paskelbta 2026-06-30Peržiūrėta 2026-07-02

Trumpai

Problema
Vienas pamirštas nuomininko filtras gali atskleisti vieno kliento duomenis kitam kelių nuomininkų SaaS sistemoje.
Apribojimas
Platformai vis tiek reikėjo rolių, audito, pinigų tvarkymo ir darbo srautų neprašant kiekvieno funkcijos autoriaus prisiminti visas apsaugas.
Sukurta sistema
Tipų saugus viso rinkinio ribos sluoksnis su apribota duomenų prieiga, API leidimų patikromis, rolių hierarchija, auditu ir aiškiais patvirtinimais.
Rezultatas
Nuomininkų izoliacija tapo numatytuoju keliu naujoms funkcijoms, o ne konvencija, kurią galima pamiršti spaudžiant terminams.
Kur tai pritaikoma
Aktualu vidiniams įrankiams ir SaaS produktams, kuriuose nuomininkai, rolės ir jautrūs duomenys auga greičiau nei ad hoc patikros.
Technologijos
tRPC · Prisma · Postgres · Next.js

Kodėl tai svarbu

Jei jūsų komanda planuoja sistemą su tais pačiais gedimo scenarijais — būtent nuo tokio mąstymo prasideda pirmas pokalbis.

Apžvalga

Pirmiau aprašytas apribojimas — visa užduotis: kelių nuomininkų SaaS platformai reikia visko, ko reikia brandžiam produktui — rolių hierarchijos, audito pėdsako, kuris atlaikytų reguliuotojo patikrinimą, teisingai tvarkomų pinigų keliomis valiutomis ir darbo srautų, apsaugančių svarbiausius veiksmus — neprašant kiekvieno inžinieriaus, rašančio užklausą, savarankiškai prisiminti, kurios jo kodo eilutės yra ir saugumo riba. Nė vienas iš šių reikalavimų savaime nėra egzotiškas. Sudėtingas šis derinys tampa dėl masto: dešimtys funkcijų, kurias skirtingu metu rašo skirtingi žmonės daugelį metų, kur vienas praleidimas — viena užklausa be nuomininko filtro, vienas galinis taškas be leidimų patikros — yra visas atstumas tarp „izoliuota“ ir „pažeista“. Toliau aprašyta sistema nebando padaryti to praleidimo retesnio vien budrumu. Ji bando padaryti jį struktūriškai sunkiai parašomą.

Struktūrinis nuomininkų atskyrimas duomenų sluoksnyje

Kelių nuomininkų sistemų gedimo scenarijus beveik visada tos pačios formos: užklausa, skaitanti ar rašanti eilutę, iš anksto nepatikrinusi, ar ta eilutė priklauso nuomininkui, kuris jos prašo. Tam nereikia piktavalio — pranešimų galinis taškas, parašytas spaudžiant terminui, foninė užduotis, jungianti lenteles nepernešusi nuomininko konteksto per sujungimą, naujas sąrašo rodinys, nukopijuotas nuo seno ir pamiršęs vieną eilutę, kuri jį apribojo. Bet kuris iš jų tyliai grąžina kito nuomininko duomenis netinkamam nuomininkui, ir kadangi užklausa vis tiek sėkmingai įvykdoma ir grąžina įtikinamai atrodantį rezultatą, niekas neatrodo sugedę, kol klientas nepastebi konkurento sąskaitos savo prietaisų skydelyje.

Sprendimas čia — ne kodo peržiūros varnelė. Tai galimybės apskritai parašyti neapribotą užklausą pašalinimas. Kiekviena prieiga prie duomenų eina per klientą, kuris jau susietas su nuomininku dar prieš parašant bet kokią užklausą juo — nuomininkas nėra parametras, kurį reikia prisiminti perduoti, tai reikšmė, įskiepyta į patį klientą tuo metu, kai išsprendžiamas užklausos kontekstas, todėl užklausa, parašyta be jos, tiesiog neturi su kuo vykdytis.

function forTenant(tenantId: TenantId): ScopedClient {
  return prisma.$extends({
    query: {
      $allModels: {
        async $allOperations({ args, query }) {
          args.where = { ...args.where, tenantId };
          return query(args);
        },
      },
    },
  });
}

Nuomininku apribotas klientas, sukuriamas kartą per užklausą ir perduodamas per kiekvieną resolverį

Kiekviena procedūra, liečianti duomenų bazę, gauna šį apribotą klientą iš užklausos konteksto, o ne importuoja globalų. Modulio apimtyje nėra neapriboto kliento, prie kurio galėtų kreiptis skubotai rašoma funkcija vietoj to — kreiptis į duomenų bazę apskritai reiškia kreiptis per nuomininko ribą. Tai visas mechanizmas: ne taisyklė, kad užklausos turi būti apribotos, o API paviršius, kuriame neapribota užklausa tiesiog nėra galimas trumpasis kelias.

Kodėl nepasikliauti kodo peržiūra ir kūrėjų disciplina

Akivaizdi alternatyva — kodavimo konvencija: kiekviena užklausa turi turėti WHERE tenant_id = ? sąlygą, tikrinamą peržiūros metu ir, jei įmanoma, „lint“ taisykle. Tai veikia lygiai taip gerai, kaip ir bet kuri konvencija, priklausanti nuo to, kad kiekvienas žmogus ją prisimins, kiekvieną kartą, amžinai — tai yra, veikia, kol nebeveikia, o tas momentas yra tylus. Trūkstamas nuomininko filtras nemeta klaidos. Jis nesulaužo testo, nebent kažkas specialiai parašė testą, numatantį būtent šį praleidimą. Jis grąžina duomenis, teisingai suformatuotus, ekrane, kuriame niekas nėra pasirengęs pastebėti, kad tai — ne to nuomininko duomenys. Klaida nematoma iš vidaus ir katastrofiška iš išorės — blogiausias įmanomas derinys dalykui, kurį turėtų pagauti žmogus, peržvelgdamas pakeitimą.

Peržiūra taip pat suprastėja būtent tada, kai tai svarbiausia: spaudžiant terminams, prie šimtosios panašios užklausos kodo bazėje, kurią rašo naujausiai prisijungęs komandos narys, turintis mažiausiai konteksto, kodėl tas modelis apskritai egzistuoja. Struktūrinė riba nepavargsta, neturi blogos dienos ir jai nereikia būti perskaičiusiai įvadinį dokumentą, paaiškinusį konvenciją prieš pusmetį. Reikalas ne tas, kad komanda nepasitiki savo inžinieriais — reikalas tas, kad joks pasitikėjimas nepakeičia fakto, jog vienintelė vieta, kur tai galima garantuoti, yra užklausų sluoksnis, o ne žmogaus atmintis.

API ribos leidimų patikros ir rolių hierarchija

Nuomininko apribojimas atsako į klausimą „kieno šie duomenys“. Jis neatsako į klausimą „ar šiam skambinančiajam leista tai daryti“. Tai atskiri klausimai, ir antrasis tikrinamas API riboje — dar prieš užklausai pasiekiant resolverį, kuris ją kitaip vykdytų — pagal penkių rolių hierarchiją, siekiančią nuo tik skaitymo prieigos iki galimybės valdyti atsiskaitymus, narystę ir viso nuomininko nustatymus.

Patikra struktūrizuota taip pat, kaip ir duomenų apribojimas: ne kaip kažkas, ką kiekvienas resolverio autorius prisimena iškviesti, o kaip tarpinės grandies žingsnis, per kurį eina kiekviena procedūra pagal konstrukciją. Procedūra deklaruoja minimalią reikalaujamą rolę; sistema vieną kartą išsprendžia skambinančiojo nuomininką ir rolę iš užklausos ir apskritai atsisako iškviesti resolverį, jei skambinantysis neatitinka reikalavimo.

const requireRole = (minimum: Role) =>
  middleware(({ ctx, next }) => {
    if (!ctx.tenant || !ctx.role) {
      throw new TRPCError({ code: "UNAUTHORIZED" });
    }
    if (roleRank[ctx.role] < roleRank[minimum]) {
      throw new TRPCError({ code: "FORBIDDEN" });
    }
    return next({ ctx });
  });

const manageBillingProcedure = protectedProcedure.use(requireRole("admin"));

Rolės patikra, sukomponuota į procedūros apibrėžimą, o ne parašyta kiekviename resolveryje atskirai

Tvarka svarbi lygiai taip pat, kaip ir pati patikra: autorizacija vyksta prieš vykdant verslo logiką, o ne kaip sąlyga, išbarstyta kažkur jos viduje. Resolveris, kuris niekada negauna neautorizuotos užklausos, negali netyčia „nutekinti“ šalutinio efekto — dalinio įrašymo, žurnalo eilutės, į eilę pastatytos užduoties — dar nespėjęs patikrinti leidimo. Jei skambinantysis neatitinka reikalavimo, resolverio kodas apskritai niekada nepasileidžia.

Penkių rolių forma egzistuoja todėl, kad dvinaris „narys arba administratorius“ padalijimas beveik iš karto nepakankamai aptarnauja realią platformą: kažkam reikia matyti ataskaitas negalint keisti atsiskaitymų, kažkam reikia tvarkyti kasdienius nustatymus negalint pašalinti kitų narių, o kažkam reikia visiškos kontrolės, kad kiekviena kita rolė netyčia jos nepaveldėtų. Hierarchija, o ne plokščias nepriklausomų leidimų žymių rinkinys, palaiko šį samprotavimą tvarkingą — kiekviena rolė yra griežtas žemesnės rolės viršrinkinys, todėl „ar ši rolė gali X“ tampa vienu palyginimu, o ne paieška matricoje, kuri su kiekviena nauja funkcija tampa vis labiau linkusi į klaidas.

GDPR lygio audito žurnalai

Izoliacija ir leidimų patikros atsako, kas dabar gali matyti ir daryti ką. Audito žurnalas atsako, kas iš tikrųjų matė ir darė ką — po fakto, o tai nėra pasirenkamas reikalavimas, kai platforma laiko asmens duomenis mokančių klientų vardu, kurie patys yra saistomi duomenų apsaugos teisės. Nuomininko administratoriui, į skundą reaguojančiam reguliuotojui ar incidentą tiriančiam platformos operatoriui — visiems reikia to paties pagrindinio įrašo: kuris veikėjas, kokia role, kokį veiksmą atliko, prieš kokį objektą, kada, kuriame nuomininke.

Tas įrašas turi būti patikimas taip, kaip derinimo žurnalas nėra. Jis rašomas toje pačioje riboje, kuri jau išsprendžia nuomininką ir rolę kiekvienai užklausai — tas pats kontekstas, kuris apriboja užklausą ar tikrina leidimą, yra lygiai tai, ko reikia audito įrašui, todėl įrašas atsiranda kaip užklausos konvejerio šalutinis produktas, o ne kaip eilutė, kurią kažkas turi prisiminti pridėti konkrečiame resolveryje. Jis pridedamas, o ne redaguojamas, nes audito įrašas, kurį būtų galima tyliai pakeisti po fakto, panaikina viso jo turėjimo prasmę. Ir jis apribotas nuomininku, kaip ir viskas kita, todėl pilno vieno nuomininko pėdsako sudarymas — tiksliai tokios formos, kokios paprastai prašo duomenų subjekto prieigos prašymas ar kliento vidaus auditas — yra apibrėžta užklausa, o ne paieška per visos platformos istoriją.

Nė vienas iš šių dalykų nepakeičia žemiau esančių izoliacijos ir leidimų sluoksnių. Audito žurnalas pasako, kas įvyko; jis nesustabdo nieko nuo įvykimo. Jo vertė būtent tuose atvejuose, kai prevencija jau nepavyko arba yra ginčijama — įrodyti, kas įvyko ir kas neįvyko, iki standarto, kuriuo gali pasikliauti kažkas už inžinerijos komandos ribų.

Kelių valiutų pinigų tvarkymas be slankiojo kablelio

Pinigai — tas vienas atvejis, kai bendrinis skaitinis tipas yra aktyviai netinkamas įrankis, ir priežastis mechaninė, ne stilistinė. Slankiojo kablelio skaičiai reiškia reikšmę dvejetainėmis trupmenomis, o dauguma dešimtainių sumų — tų paprastų, pasitaikančių kiekvienoje sąskaitoje, kaip 0,10 ar 19,99 — neturi tikslios dvejetainės išraiškos. Kiekviena atskira apvalinimo klaida menka. Problema ta, kad finansinė aritmetika retai būna viena operacija — tai sumos per eilutes, proporcijos per atsiskaitymo laikotarpius, konvertavimai per valiutas, ir mažos klaidos susikaupia visur taip, kad nesusipanaikina. Suma, klystanti centimo dalimi, nėra apvalinimo kuriozas, kai tai kažkieno sąskaita, kažkieno išmoka ar suderinimas, kuris turi sutapti iki paskutinio vieneto.

Sprendimas — reprezentacinis, o ne taisyklė būti atsargiam. Sumos saugomos ir skaičiuojamos kaip sveikieji skaičiai mažiausiu valiutos vienetu — centais, ne doleriais — naudojant sveikųjų skaičių ar savavališko tikslumo aritmetiką, o ne natyvią slankiojo kablelio matematiką, todėl tiesiog nėra trupmeninės dvejetainės išraiškos, kurioje būtų galima prarasti tikslumą. Pati valiuta pernešama kaip reikšmės dalis, o ne kaip atskiri, lengvai pamestini metaduomenys šalia plikų skaičių, todėl operacija, kuri sumaišytų dvi valiutas — pridėtų sumą viena valiuta prie sumos kita valiuta be aiškaus, sąmoningo konvertavimo žingsnio — tampa ne tik klaida, gaudytina peržiūros metu, o tipu, kurio kodas apskritai neturi kaip netyčia sukonstruoti.

type Money = {
  amountMinorUnits: bigint;
  currency: CurrencyCode;
};

Valiuta keliauja kartu su suma; nėra plikas skaičius, kurį būtų galima klaidingai interpretuoti

Tai nepadaro paties valiutos konvertavimo laisvo nuo sprendimų — konvertavimui vis tiek reikia kurso, apvalinimo taisyklės ir sprendimo, kuri sandorio pusė paima likutį. Tai, ką pašalina, — kur kas dažnesnė nesėkmė: tylus tikslumo praradimas naudojant netinkamą skaičiaus tipą sričiai, kur „pakankamai artima“ niekada iš tikrųjų nebuvo pakankamai artima.

Aiškūs patvirtinimo srautai jautriems veiksmams

Rolės patikra atsako, ar skambinančiajam techniškai leidžiama atlikti veiksmų klasę. Ji neatsako, ar būtent šis konkretus tos klasės atvejis turėtų būti vykdomas vienašališkai, dabar, remiantis vieno žmogaus sprendimu. Daliai veiksmų — tiems, kurių pasekmes brangu ar neįmanoma atsukti atgal, kaip didelis grąžinimas, masinis asmens duomenų eksportas ar nuomininko atsiskaitymo tvarkos pakeitimas — platforma virš leidimų patikros įterpia antrus vartus: veiksmo tik prašoma, jis nevykdomas iš karto, ir jam reikia kito žmogaus patvirtinimo, kad įsigaliotų.

Tai sąmoningas, siauras trinties naudojimas. Dauguma sistemos veiksmų lygiai tokie greiti, kokius leidžia rolių hierarchija, nes daugumai veiksmų nereikia antros nuomonės — trintis visur išmokytų žmones tiesiog perklikti ją neskaitant. Patvirtinimo vartai skirti tiems veiksmams, kur antra akių pora pastebi tai, ko galėtų nepastebėti vienas veikėjas: pakankamai didelis grąžinimas, kad būtų klaida, o ne rutininė korekcija, pakankamai platus eksportas, kad vertėtų paklausti kodėl, atsiskaitymo pakeitimas su pasekmėmis, siekiančiomis toliau nei prašantis asmuo. „Kas gali prašyti“ atskyrimas nuo „kas gali patvirtinti“ taip pat reiškia, kad viena pažeista ar neatsargi paskyra negali vienašališkai įvykdyti pačių svarbiausių platformos veiksmų — tas pats principas, kaip ir rolių hierarchijoje, pritaikytas konkretiems didelės rizikos veiksmams, o ne ištisoms galinių taškų kategorijoms.

Kompromisai ir apribojimai

Nė vienas iš šių dalykų nepakeičia teisingumo kitur, o sąžiningumas dėl to, ko tai negarantuoja, — projekto dalis:

  • Struktūrinis apribojimas apsaugo tik tuos kelius, kurie iš tikrųjų eina per apribotą klientą. Žalia užklausa, vienkartinis migracijos scenarijus ar administravimo įrankis, tiesiogiai pasiekiantis duomenų bazę, vėl atveria lygiai tą pačią skylę, kurią tai buvo sukurta uždaryti — riba tik tiek pilna, kiek pilna disciplina, laikanti kiekvieną prieigos kelią nukreiptą per ją.
  • Penkių rolių hierarchija turi būti peržiūrima, augant produktui. Per stambi — ir naujos funkcijos arba prisegamos prie rolės, kuri joms ne visai tinka, arba apeina hierarchiją vienkartine patikra, ardančia modelį, kurį ji turėjo pakeisti.
  • Audito žurnalai prideda įrašymo pridėtinę apkrovą kiekvienam pakeitimui ir sąmoningai neribotai auga saugykloje — ir kadangi pats žurnalas dažnai turi asmens duomenų, jis paveldi tas pačias duomenų apsaugos prievoles kaip ir duomenys, kuriuos aprašo, o ne lengvesnę jų versiją.
  • Patvirtinimo srautai sąmoningai prideda vėlavimą jautriems veiksmams. Tai teisinga trintis veiksmams, kuriuos jie skirti stabdyti, ir neteisinga trintis viskam, kas neteisingai priskirta tai grupei — per plačiai stabdantis srautas išmoko žmones jį apeiti.
  • Tipų saugumas API ir duomenų prieigos riboje įrodo, kad formos teisingos kompiliavimo metu. Jis neįrodo, kad izoliacijos ir leidimų elgsena teisinga vykdymo metu — teisingo tipo užklausa vis tiek gali būti logiškai klaidinga, todėl šiai ribai reikia savų testų, ne vien kompiliatoriaus, kuris patenkintas.
  • Teisinga pinigų reprezentacija pašalina slankiojo kablelio klaidą; ji nepašalina sprendimų kitur toje pačioje srityje — valiutų kursai, apvalinimo taisyklės ir kas paima likutį lieka verslo sprendimai, kurių tipų sistema už nieką nepriima.

Šaltiniai ir tolesnis skaitymas

Susijusios atvejų analizės

Sistemos, kurias su šia sieja bendras apribojimas — pakartojimai, izoliacija, kaina, atkūrimas.

Technologijos: tRPC · Prisma · Postgres · Next.js← Visos atvejų analizės

Turite sistemą, kuri privalo veikti?

Rezervuokite pokalbį — susidėliosime, ką ji turi daryti, kur ji lūš ir kokią mažiausią versiją verta kurti pirmiausia.