Zašto sam dozvole za fajlove modelirao u Neo4j-u umjesto u SQL tabelama
Na platformi za sastanke pristup fajlu išao je preko sastanka, dokumenta, verzije i potpisa. SQL-u za svaki korak treba još jedan join, a Neo4j te veze prati direktno.
Godine 2023. osam mjeseci sam radio na white-label platformi za sastanke namijenjenoj zdravstvu i finansijama. Bio sam u timu od pet ljudi, angažovan preko agencije. Model podataka bio je moja odluka i to je dio tog proizvoda koji bih branio najupornije.
Platforma je služila za sastanke na daljinu, ali video je bio manje važan od onoga što ostane nakon poziva: ko je prisustvovao, šta je rečeno, o kojim se dokumentima radilo, ko ih je smio vidjeti i ko je potpisao. U zdravstvu i finansijama video poziv nudi mnogo dobavljača, pa klijenti plaćaju zapis sa sastanka.
Zato teško pitanje nije bilo „može li se ova osoba pridružiti“, nego „smije li ova osoba otvoriti ovaj fajl“. To drugo pitanje nije se uklapalo u tabele.
Od čega je zavisilo smije li osoba otvoriti fajl
Na toj platformi odgovor je zavisio od lanca:
- osoba je prisustvovala sastanku
- sastanak je povezan s dokumentom
- dokument ima verzije
- verzija može imati potpis
- dozvola se može dobiti direktno, dodijeljena osobi na samom dokumentu, ili preko sastanka, jer je dokument bio priložen sastanku na kojem je osoba bila
Na još jednom pravilu pada većina šema: dokument istovremeno može biti dio više sastanaka, pa nema jednog roditelja. Nikakva kolona meeting_id ne može reći kojem sastanku fajl pripada, jer tih sastanaka ima više, a svaki od njih može dodijeliti druga prava.
Samih dozvola bilo je malo i bile su sasvim obične: čitanje, pisanje, preuzimanje i brisanje. Problem je bio u putanji od osobe do fajla.
U SQL-u svaka nova vrsta veze znači još jedan join
Ovo se može napisati i relaciono, s po jednim joinom za svaki korak i po jednom granom UNION-a za svaki način na koji se dozvola dobija. Pojednostavljeno, izgleda ovako:
1SELECT EXISTS (2 -- direct grant3 SELECT 14 FROM document_grants g5 WHERE g.person_id = $16 AND g.document_id = $27 AND $3 = ANY (g.actions)8 9 UNION ALL10 11 -- grant through a meeting the person attended12 SELECT 113 FROM attendance a14 JOIN meeting_documents md ON md.meeting_id = a.meeting_id15 WHERE a.person_id = $116 AND md.document_id = $217 AND $3 = ANY (md.actions)18) AS allowed;To radi za dvije putanje. Kad se dodaju verzije, pitanje se odnosi na određenu verziju dokumenta. S potpisima neke radnje zavise od toga da li je verzija potpisana i ko ju je potpisao. Sastanak koji upućuje na dokumente drugog sastanka dodaje još jednu putanju. Svaka nova vrsta veze znači još jedan join i još jednu granu UNION-a, a prije ili kasnije i rekurzivni CTE, jer neki lanci nemaju fiksnu dužinu.
Dvije stvari se pogoršavaju u isto vrijeme. Provjera postaje sporija sa svakom novom vrstom veze, jer baza pri svakom pokretanju ponovo slaže lanac iz redova. Uz to je kod sve teže održavati sa svakim novim pravilom, jer pravila pristupa postoje samo kao oblik upita, pa ih sljedeća osoba mora rekonstruisati iz joinova. U prvom mjesecu se nijedan od ova dva problema ne vidi. Pojave se kad se proizvod počne prodavati.
Domen modeliran kao graf u Neo4j-u
Zato sam domen modelirao kao graf u Neo4j-u. Ljudi, sastanci, dokumenti, verzije i potpisi su čvorovi, a prisustvo, prilaganje i dozvole su grane.
Pitanje dozvole je pitanje putanje, a praćenje putanje je osnovna operacija graf baze. Upit prolazi lancem umjesto da ga pri svakoj provjeri ponovo slaže iz redova.
Nije to ni egzotičan izbor. Google modelira kontrolu pristupa na isti način i iz istog razloga: kad je pristup određen vezama između stvari, logično je te veze čuvati direktno.
Evo pojednostavljene verzije modela, s imenima izabranim za ovaj tekst:
1(:Person)-[:ATTENDED]->(:Meeting)2(:Meeting)-[:ATTACHED {actions}]->(:Document)3(:Person)-[:CAN {actions}]->(:Document)4(:Document)-[:HAS_VERSION]->(:Version)5(:Person)-[:SIGNED]->(:Version)Ista provjera dozvole kao u SQL primjeru, napisana nad ovim modelom:
1MATCH (p:Person {id: $personId}), (d:Document {id: $documentId})2RETURN3 EXISTS {4 MATCH (p)-[g:CAN]->(d)5 WHERE $action IN g.actions6 }7 OR EXISTS {8 MATCH (p)-[:ATTENDED]->(:Meeting)-[a:ATTACHED]->(d)9 WHERE $action IN a.actions10 } AS allowedOsim što je kraći, upit se čita kao pravilo: „osoba ima direktnu dozvolu ili je prisustvovala sastanku na kojem je ovaj dokument priložen s dozvolom za tu radnju“. Novi developer može pravila pristupa pročitati direktno iz upita.
Dodavanje koraka ne mijenja vrstu posla. Pitanje o verziji umjesto o dokumentu dodaje samo još jedan korak duž grane:
1MATCH (p:Person {id: $personId})-[:ATTENDED]->(:Meeting)2 -[a:ATTACHED]->(:Document)-[:HAS_VERSION]->(v:Version {id: $versionId})3WHERE $action IN a.actions4RETURN count(v) > 0 AS allowedZbog tog svojstva sam i izabrao graf: na pitanja o dozvolama odgovara obilazak grafa, koliko god lanac bio dug.
Dokumenti priloženi dok sastanak traje
Da se graf popunjavao prije svakog sastanka i tokom sastanka samo čitao, dobro indeksiran relacioni model bi možda bio dovoljan. Graf se, međutim, nije popunjavao unaprijed.
Dokument se mogao uploadovati unaprijed ili priložiti dok sastanak traje. Čim bi bio priložen, morao je imati ispravne dozvole za sve prisutne i odmah im se pojaviti. Graf se mijenjao uživo, dok su ljudi gledali.
U grafu je prilaganje dokumenta jedna grana:
1MATCH (m:Meeting {id: $meetingId}), (d:Document {id: $documentId})2MERGE (m)-[a:ATTACHED]->(d)3SET a.actions = $actions,4 a.attachedAt = datetime()Ne upisuje se nijedna pojedinačna dozvola. Svi koji su prisustvovali sastanku sada mogu doći do dokumenta, jer je njihov pristup izveden iz putanje, a ne kopiran svakom od njih. Nema ni upisivanja po jednog reda za svakog učesnika, pa se ne može desiti da upis ostane napola završen i da tri osobe vide fajl, a četvrta još ne.
Uopšteno, kad se dozvole kopiraju, svaka promjena strukture postaje problem sinhronizacije, a izvedene dozvole mijenjaju se zajedno sa strukturom. Uz graf su izvedene dozvole bile dovoljno jeftine da se koriste po defaultu.
Isti model je bez posebnih slučajeva podržao i pravilo o više sastanaka. Dokument koji je priložen na tri sastanka ima tri grane ATTACHED, svaku sa svojim skupom dozvoljenih radnji. Ne treba birati vlasnika niti se dogovarati koji je sastanak glavni, a historija verzija i potpisivanje ostaju vezani za dokument, bez obzira na to koliko sastanaka pokazuje na njega.
Transkripti, whiteboard, pristup za goste i ekstenzija za browser
Graf je bio srž, ali se isplatio samo zato što je i ostatak platforme sastanak tretirao kao zapis. O svakoj od ovih stvari mogao bi se napisati zaseban tekst, pa ću biti kratak.
Transkripcija je radila u realnom vremenu, s razdvojenim govornicima, a gotov transkript ulazio je u dnevni red sljedećeg sastanka, pa je postao dio procesa.
Whiteboard se čuvao kao skup objekata, a ne kao slika. Svaki potez, oblik i komad teksta ostao je zaseban element sa svojim svojstvima, pa se tabla nakon sastanka mogla ponovo učitati i dalje uređivati.
Pristup za goste postojao je zbog HIPAA-e: pacijent ne treba otvarati nalog samo da bi prisustvovao konsultaciji. Ovdje je regulatorni zahtjev proizvodu donio novu funkcionalnost, a u grafu je lako izraziti pravilo „ova osoba je ovdje, za ovaj sastanak, i može doći tačno do onoga što ovaj sastanak dodjeljuje“.
Ekstenzija za browser bila je opcionalna i služila je za pokretanje i pregled sastanaka. Tražila je jednu dozvolu i nije imala nikakav pristup audiju ni videu. Ekstenzije za sastanke obično teže širokim dozvolama. U proizvodu koji se prodaje zdravstvu široke dozvole znače teže odobrenje u prodavnici ekstenzija, upozorenje pri instalaciji i pitanje u sigurnosnoj procjeni klijenta na koje nema dobrog odgovora. Proizvod je prodat i jednoj američkoj banci.
Kad ne bih posegnuo za grafom
Ne treba svakom sistemu dozvola graf. Ako o pristupu odlučuju uloga i tenant, red u tabeli i filter su jednostavniji i brži, a lakše je naći i ljude koji s njima znaju raditi. Graf se isplati kad su ispunjena sva tri uslova:
- dozvola se dobija preko veza, a ne samo preko uloga
- lanci su različite dužine ili stalno dobijaju nove vrste veza
- struktura se mijenja dok je ljudi koriste, pa bi kopirane dozvole trebalo stalno sinhronizovati
Ta platforma ispunjavala je sva tri. Većina CRUD aplikacija ne ispunjava nijedan, a za njih je dobro dizajnirana tabela pravo rješenje.
Šta bih uradio drugačije
Model podataka je izdržao. Frontend nije, i za to nije kriva nijedna pojedinačna odluka.
Tim je osam mjeseci gradio funkcionalnosti po cijelom proizvodu, a zajednički sloj komponenti nikad nije nastao. Na kraju je codebase imao tri UI frameworka i četiri biblioteke za padajuće menije. To niko nije izabrao. Nakupilo se postepeno, iz niza lokalno razumnih odluka, jer nije postojala zajednička biblioteka za kojom bi se prvo posegnulo.
Na sljedećem projektu prvo sam napravio zajedničku biblioteku, prije funkcionalnosti, da ne bi svaka od njih donijela svoju.
Blog
Ako radite na nečemu sličnom, javite nam se s detaljima.
Prvi poziv traje 30 minuta i ni na šta vas ne obavezuje.