Pohled na celou firmu dostal kazdy, kdo do ni patril: scopes.push('tenant')
se v access.ts neptalo na nic. mine byl dobrovolny filtr, ne strop, takze
resitel s roli agent poslal ?scope=tenant a dostal cely provoz.
Rozdil, na kterem to ted stoji: pohled je co chci videt, strop je co vubec smim
videt. Existoval jen pohled a klientovi se veril.
Model:
- Membership.seesAllTenant - vidi cely provoz firmy. Postaveni uctu ve firme,
ne vlastnost resitele: clovek muze byt ve dvou firmach jednou reditel
a jednou brigadnik
- PersonGroup.members je { personId, seesAll } misto holeho personIds. seesAll
je vedouci sekce. Priznak visi na clenstvi, ne na cloveku a ne na skupine -
diky tomu muze byt clovek v peti sekcich a jen ve dvou videt vsechno, coz
role rict neumi, ta je jedna na celou firmu
- stara podoba personIds se dal cte, prevadi ji groupMembers
Vypocet stropu (visibilityFor), sjednoceni ne prunik: spravce platformy nebo
seesAllTenant vidi celou firmu, jinak svoje tickety plus vse ze sekci, kde ma
zaskrtnuto, plus jejich fronta. U zaznamu bez priznaku rozhoduje pravo
ticket.assign.others - kdo smel prehazovat cizi praci, uz stejne cely provoz
videl, takze se mu nic nebere.
Vynuceni:
- strop je povinna soucast TicketFilter, stejne jako tenantIds. Nepovinny filtr
na prava je filtr, ktery jednou nekde chybi - takhle prekladac ukazal vsech
trinact mist, ktera ho jeste nemela
- getWorkload a getAgentStats uz nesahaji do pole ticketu primo, jdou pres
listTickets. Driv obchazely kazde omezeni viditelnosti
- getTicket kontroluje strop i u jednoho ticketu, bez toho by stacilo znat ID
- detail a prevzeti pocitaji strop za firmu ticketu, ne za prave prepnutou
Helpdesk se ridi toutez hierarchii, jen "moje" znamena, co jsem zalozil -
zadavatel pozadavek nikdy nema prirazeny. Ticket proto nese createdById.
Zalozka Firma: pod jednim mistem Prehled, Resitele, Sekce, Role a prava, Typy
ticketu a Pozvanky. V Nastaveni zustal Muj ucet a platformni veci. Role a Typy
jsou samostatne komponenty, sekce maji vlastni panel - u kazdeho clenstvi je
prepinac a radek na cloveka obecny EntityAdmin neumi.
Obsah ticketu se cte i v helpdesku, v seznamu jako jednoradkovy nahled.
Overeno na bezici instanci se tremi ucty nad tymiz daty: spravce platformy vidi
vsechny tri tickety vcetne neprirazeneho, Vomacka jako vedouci Servicedesku dva
(tickety obou clenu sekce), Kriz jako radovy clen jeden, jen svuj. Adresa cizho
ticketu vraci 404, vytizeni tymu ukazuje jen viditelne a po zaskrtnuti priznaku
se rozsah zmeni hned.
Co z toho plyne: neprirazeny ticket bez skupiny nevidi nikdo krome toho, kdo
vidi celou firmu. Prichozi praci musi nekdo smerovat.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
11 KiB
07 - Firmy a prava
Dve ruzne "firmy"
Snadno se pletou, proto hned na zacatku:
- Tenant je firma, ktera portal pouziva. Ma v nem svuj tym, svoje tickety a svoje automatizace. Je to hranice viditelnosti.
ticket.customerje zakaznik toho tenanta, tedy kdo pozadavek poslal. S pravy nema nic spolecneho.
Tenanti jsou v src/data/tenants.ts.
Uzivatel muze byt ve vic firmach
Proto memberships, ne jedno tenantId. Typicky externista, ktery dela
servicedesk dvema klientum a v kazdem ma jinou roli.
interface User {
id: string;
email: string;
name: string;
platformAdmin: boolean; // vidi napric vsemi firmami
memberships: Membership[]; // { tenantId, role }
}
type TenantRole = 'admin' | 'agent';
Role je vzdy az uvnitr firmy. Pristup napric firmami je zvlast jako
platformAdmin - to je nase pravo, ne zakaznicke. Kdyby byla role jen jedna
globalni, nesla by tahle situace vubec zapsat.
Tri pohledy na tickety
| Pohled | Co ukazuje | Kdo smi |
|---|---|---|
all |
napric vsemi firmami | jen platformAdmin |
tenant |
cela jedna firma | kdokoliv, kdo do ni patri |
mine |
jen tickety prihlaseneho | kdo ma navazaneho resitele |
Posilaji se jako query: ?scope=tenant&tenantId=tnt_automia.
U tenant a mine je potreba vedet kterou firmu, protoze uzivatel jich
muze mit vic. Bez tenantId se pouzije prvni.
Prepinac firmy je nad celym portalem
Vybrana firma je jedna hodnota pro cely dashboard (web/src/lib/tenant.ts),
ne stav jedne stranky. Prepinac je v sidebaru nad navigaci, tedy nad vsim, co se
pod nim kresli, a apiFetch doplnuje tenantId do kazdeho dotazu
na /api/dashboard.
Doplnuje se to v API vrstve schvalne, ne na strankach. Kdyz si to mela pridavat kazda stranka sama, vetsina na to zapomnela a server sahnul po vychozi firme - uzivatel se prepnul na LogiTrans a v Lidech koukal na lidi Automie. To neni nepohodli, to jsou cizi data pod hlavickou jine firmy.
Vyjimky, kdy se nedoplnuje:
| Kdy | Proc |
|---|---|
Cesta uz tenantId nese |
proklik z widgetu na konkretni firmu ma prednost |
scope=all |
pohled pres vsechny firmy, jedna firma tam nic neznamena |
| Neni vybrano | server pouzije vychozi firmu uctu |
Cesta mimo /api/dashboard |
prihlaseni a verejny web s firmou nepracuji |
Prepnuti prekresli obsah cely. Obsah dashboardu ma key podle vybrane
firmy, takze jina firma znamena novy strom komponent.
Bez toho to nestacilo: useApiQuery se sice zeptat umi, ale komponenty, ktere
volaji apiFetch primo - EntityAdmin, a tim cele Nastaveni a zalozky
Resitele a Skupiny - o zmene nevi a zustala by na nich data predchozi firmy.
Doplnovat zavislost do kazde z nich je totez zapomenuti, jen o patro niz.
Ztrata rozepsaneho stavu je pritom spravne: filtr na cloveka z firmy A nema ve firme B co znamenat.
Volba prezije obnoveni stranky (localStorage). Kdyz ulozena firma uz mezi
access.tenants neni - clovek z ni odesel, prihlasil se nekdo jiny na tomtez
pocitaci - spadne se na vychozi firmu uctu, aby kazdy dotaz nekoncil na 403.
Prava plati za firmu, ne za cloveka
permissionsOf(user, tenantId) pocita prava v jedne firme. Firma je
povinny argument: kdyby byla nepovinna, prvni volani bez ni by tise vratilo
vsechna prava, a presne takova chyba se nehleda.
Driv se prava scitala pres vsechna clenstvi. Znelo to rozumne - pravo je bud, nebo neni - ale znamenalo to, ze kdo je spravce v jedne firme, jednal jako spravce ve vsech, kam patri. Uzivatel ve dvou firmach je vzacny, dusledek ne.
| Kde se pta | Ktera firma rozhoduje |
|---|---|
| Akce nad ticketem | firma toho ticketu |
| Uprava zaznamu v nastaveni | firma toho zaznamu |
| Zalozeni ticketu, helpdesk | firma, kterou ma clovek prepnutou |
| Zalozky v navigaci | firma, kterou ma clovek prepnutou |
Rozdil mezi prvnimi dvema a zbytkem je zamer: kdo edituje zaznam jine firmy, musi mit pravo tam, ne tam, kde je zrovna prepnuty.
tenantId: null znamena "mimo firmu", tedy zadna zakaznicka prava. Deje se to
u uctu bez clenstvi. Spravce platformy ma vzdycky vsechno - to je nase pravo,
ne zakaznicke.
Cizi firemni role se ignoruje, i kdyby na ni clenstvi odkazovalo: jinak by stacilo pripsat si ji do clenstvi v jine firme a prava by se prenesla.
Systemove role se srovnavaji s kodem pri startu
Vychozi sada se pouzije jen do prazdneho uloziste. Nove pravo v katalogu by
se proto k uz bezici instalaci nikdy nedostalo: role_admin by zustala s vyctem
ulozenym pri prvnim startu a nova zalozka by se spravci firmy neobjevila.
syncSystemRoles() proto pri startu srovna prava systemovych roli s tim, jak
jsou napsane v kodu. Meni jen prava, jen u roli s tenantId: null a jen
u tech, ktere jsou v systemRoles(). Nazvu se to netyka (ten uz nekdo mohl
zmenit) a vlastnich roli firmy uz vubec.
Server je autorita
Vsechno rozhoduje src/data/access.ts, jedno misto pro cely portal. Kdyby se to
rozlezlo po routach, driv nebo pozdeji vznikne endpoint, ktery filtr zapomene.
GET /api/dashboard/access vraci, co uzivatel smi:
{
"scopes": ["all", "tenant", "mine"],
"tenants": [{ "id": "tnt_automia", "name": "Automia" }],
"defaultTenantId": "tnt_automia",
"canAssignOthers": true,
"personId": "ppl_uhlir"
}
Klient podle toho kresli prepinac. Nesmi si to dovozovat sam - jinak by se prava pocitala na dvou mistech a jednou se rozejdou.
Nikdy tise nezuzujeme
Pozadavek na pohled nebo firmu, na kterou uzivatel nema pravo, vraci chybu, ne potichu zuzeny vysledek:
| Situace | Odpoved |
|---|---|
| pohled bez opravneni | 403 |
| firma, do ktere nepatri | 404 |
| ucet bez firmy | 403 |
| prirazeni ostatnim bez prava | 403 |
Duvod: kdyby se pozadavek na cizi firmu jen prepnul na vlastni, uzivatel by koukal na cizi cisla v domneni, ze jsou spravna. To je horsi nez chyba.
Filtr v ulozisti je povinny
listTickets, listPeople a listAutomations maji tenantIds jako povinny
argument, ne volitelny. Zapomenuty filtr tak neznamena "vse", ale nezkompiluje se.
Zapis (assignTicket, updateTicketStatus, addComment, updateAutomation,
deleteAutomation) bere tenantIds taky. Cizi zaznam se chova jako neexistujici,
tedy 404, ne 403 - z odpovedi nemá jit poznat, ze takove ID vubec existuje.
Vyjimka je verejny webhook. Ten se autorizuje tokenem v adrese, ne prihlasenim, takze si automatizaci najde pres vsechny firmy.
Jedina vyjimka: helpdesk
Ticket patri jedne firme a to plati dal. U pozadavku z helpdesku ale figuruji
dve: vlastnikem je ta, ktera ho resi, a v helpdeskSourceId je ta, ktera ho
poslala. Zadavatel se k nemu dostane jen pres helpdesk a jen ke svym
pozadavkum; bezny seznam ticketu zustava vlastnikovi.
Neni to obchazeni hranice, je to druha cesta dovnitr s vlastnim scopem. Popis je v 18-ticketovaci-system.md.
Prirazeni jen v ramci firmy
assignTicket odmitne resitele z jine firmy. Jinak by ticket zmizel z prehledu
sve firmy a objevil se nekomu, kdo do ni nepatri.
Prehazovat praci mezi lidmi smi jen admin. agent si smi vzit ticket na sebe,
ale nemuze ho poslat kolegovi - to hlida canAssignOthers.
Demo ucty
Heslo je u vsech demo1234.
| Kdo je | |
|---|---|
admin@automia.cz |
spravce platformy, vidi vsechny tri firmy |
karel.vomacka@automia.cz |
agent v Automii, admin u Nordisu - dve firmy |
martin.kriz@automia.cz |
bezny resitel jedne firmy |
Druhy ucet je ten zajimavy: ukazuje prepinac firem i to, ze prava se lisi podle toho, ktera firma je prave zvolena.
Co chybi
| Chybi | Poznamka |
|---|---|
| Sprava clenstvi z portalu | memberships jdou zmenit jen v kodu |
| Pozvanky uzivatelu | zadny onboarding |
| Tenant u incidentu | incidenty jsou zatim spolecne, nefiltruji se |
| Tenant u konektoru | katalog je spolecny, napojeni se zatim neeviduje |
| Audit pristupu | odepreni se jen loguje, nikde se neuklada |
Strop viditelnosti
Pohled je co chci videt, strop je co vubec smim videt. Driv existoval jen
pohled: scope=tenant dostal kazdy, kdo do firmy patril, a mine byl
dobrovolny filtr.
Strop pocita visibilityFor v src/data/access.ts a nese ho ResolvedScope,
protoze tudy prochazi kazdy dotaz na tickety:
spravce platformy nebo Membership.seesAllTenant -> cela firma
jinak -> moje tickety
+ vse ze sekci, kde mam
u clenstvi zaskrtnuto
+ fronta techhle sekci
Priznak visi na clenstvi, ne na cloveku a ne na skupine. Clovek muze byt v peti sekcich a jen ve dvou z nich videt vsechno - to role rict neumi, ta je jedna na celou firmu.
seesAllTenant === undefined jsou zaznamy ulozene driv. Rozhoduje u nich
pravo ticket.assign.others: kdo smel prehazovat cizi praci, uz stejne cely
provoz videl.
Vynucuje se v ulozisti, ne v routach
Strop je povinna soucast TicketFilter, stejne jako tenantIds. Nepovinny
filtr na prava je filtr, ktery jednou nekde chybi. getWorkload
a getAgentStats proto uz nesahaji do pole ticketu primo, jdou pres
listTickets, a getTicket ho kontroluje i u jednoho ticketu - bez toho by
stacilo znat ID.
Detail a prevzeti pocitaji strop za firmu ticketu, ne za prave prepnutou: odkaz z pohledu "vse" muze vest do jine firmy uzivatele.
Helpdesk
Tataz hierarchie, jen "moje" znamena neco jineho: zadavatel pozadavek nikdy
nema prirazeny. Ticket proto nese createdById a plati, ze kdo vidi celou
firmu, vidi vsechny jeji pozadavky, ostatni jen ty svoje.
Co z toho plyne
Neprirazeny ticket bez skupiny nevidi nikdo krome toho, kdo vidi celou firmu. Prichozi praci musi nekdo smerovat, jinak se k radovym clenum nedostane.