From 2e0b9456a9caa24029d55ffe7827c8f07ccdab55 Mon Sep 17 00:00:00 2001 From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com> Date: Wed, 26 Aug 2026 07:57:01 +0200 Subject: [PATCH] Prava plati za firmu, ne za cloveka permissionsOf(user) scitalo role pres vsechna clenstvi, takze kdo byl spravce v jedne firme, jednal jako spravce ve vsech, kam patril. Uzivatel ve dvou firmach je vzacny, dusledek ne. - firma je povinny argument u permissionsOf, hasPermission i hasAnyPermission. Zamerne povinny: kdyby byl nepovinny, prvni volani bez nej by tise vratilo vsechna prava. Prekladac tim rovnou nasel vsech ctrnact mist, ktera se ptaji. - kazde misto se pta za tu spravnou firmu: akce nad ticketem za firmu toho ticketu, uprava zaznamu za firmu toho zaznamu, zalozky a helpdesk za firmu, kterou ma clovek prepnutou - 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. - cache prav ma v klici firmu - readScope cte z jedne firmy, te prepnute, ne ze vsech. V nastaveni se driv michaly typy ticketu a role napric firmami bez ohledu na vyber. Opraveno u toho: systemove role se pri startu srovnaji s kodem (syncSystemRoles). Vychozi sada se pouzije jen do prazdneho uloziste, takze nove pravo v katalogu se k uz bezici instalaci nikdy nedostalo - ticket.create ani obe prava helpdesku by spravci firmy nenabehla a zalozka Helpdesk by se neobjevila. Meni se jen prava, jen u nasich roli, nazvu se to netyka. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/07-firmy-a-prava.md | 45 ++++++++++++--- documentation/99-zmeny.md | 31 ++++++++++ src/data/access.ts | 24 +++++++- src/data/bootstrap.ts | 15 +++++ src/data/permissions.ts | 94 +++++++++++++++++++++++++++---- src/data/tenantFeatures.ts | 2 +- src/routes/crud.ts | 36 ++++++++++-- src/routes/dashboard.ts | 9 +-- src/routes/helpdesk.ts | 35 +++++++----- src/routes/tenantScripts.ts | 5 +- src/routes/ticketActions.ts | 7 ++- 11 files changed, 254 insertions(+), 49 deletions(-) diff --git a/documentation/07-firmy-a-prava.md b/documentation/07-firmy-a-prava.md index b7b5164..0e82eb5 100644 --- a/documentation/07-firmy-a-prava.md +++ b/documentation/07-firmy-a-prava.md @@ -69,6 +69,44 @@ 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 @@ -149,13 +187,6 @@ podle toho, ktera firma je prave zvolena. ## Co chybi -**Prava se scitaji pres vsechny firmy, neprepocitavaji se podle vybrane.** -`permissionsOf(user)` vezme role ze vsech clenstvi dohromady. Kdo je spravce -v jedne firme a resitel v druhe, ma prava spravce i po prepnuti do te druhe. -Data se tim neprolomi - server je porad filtruje podle firmy - ale tlacitka -ukazuji vic, nez by mela. Napravit to znamena predat firmu do `permissionsOf` -a prepocitat cache za dvojici uzivatel a firma. - | Chybi | Poznamka | | ------------------------- | ------------------------------------------------ | | Sprava clenstvi z portalu | memberships jdou zmenit jen v kodu | diff --git a/documentation/99-zmeny.md b/documentation/99-zmeny.md index e21f339..e0a1137 100644 --- a/documentation/99-zmeny.md +++ b/documentation/99-zmeny.md @@ -2,6 +2,37 @@ Nejnovejsi nahore. +## 2026-08-26 - prava plati za firmu, ne za cloveka + +`permissionsOf(user)` scitalo role pres vsechna clenstvi. Kdo byl spravce +v jedne firme, jednal jako spravce ve vsech, kam patril. Uzivatel ve dvou +firmach je vzacny, dusledek ne - to neni edge case, to je diera v pravech. + +### Zmeneno + +- **Firma je povinny argument** u `permissionsOf`, `hasPermission` + i `hasAnyPermission`. Zamerne povinny: kdyby byl nepovinny, prvni volani bez + nej by tise vratilo vsechna prava. Prekladac tim rovnou nasel vsech ctrnact + mist, ktera se ptaji. +- **Kazde misto se pta za tu spravnou firmu.** Akce nad ticketem za firmu toho + ticketu, uprava zaznamu za firmu toho zaznamu, zalozky a helpdesk za firmu, + kterou ma clovek prepnutou. Kdo edituje zaznam jine firmy, musi mit pravo tam, + ne tam, kde je zrovna prepnuty. +- **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. +- **Cache prav ma v klici firmu.** Bez toho by prvni dotaz odpovedel i na druhou. +- **`readScope` cte z jedne firmy, te prepnute**, ne ze vsech, kam clovek patri. + V nastaveni se driv michaly typy ticketu a role napric firmami bez ohledu na + vyber - stejna vada jako u prepinace, jen na jinem miste. + +### Opraveno + +- **Systemove role se pri startu srovnaji s kodem** (`syncSystemRoles`). + Vychozi sada se pouzije jen do prazdneho uloziste, takze nove pravo + v katalogu se k uz bezici instalaci nikdy nedostalo - `ticket.create` + a obe prava helpdesku by spravci firmy nikdy nenabehla a zalozka by se + neobjevila. Meni se jen prava, jen u nasich roli, nazvu se to netyka. + ## 2026-08-26 - prepinac firmy plati pro cely portal Prepinac byl stav uvnitr stranky Prehled. Prepnuti na LogiTrans zmenilo prehled diff --git a/src/data/access.ts b/src/data/access.ts index 6088625..70a72a0 100644 --- a/src/data/access.ts +++ b/src/data/access.ts @@ -76,14 +76,34 @@ export function accessFor(user: User, tenantId?: string | null): Access { tenants: tenants.map((tenant) => ({ id: tenant.id, name: tenant.name })), defaultTenantId, // Zustava kvuli klientovi, ale uz se pocita z prava, ne z role. - canAssignOthers: hasPermission(user, 'ticket.assign.others'), + canAssignOthers: hasPermission(user, 'ticket.assign.others', activeTenant), personId: person?.id ?? null, - permissions: [...permissionsOf(user)].sort(), + // Prava se pocitaji za **vybranou firmu**. Kdo je spravce v jedne firme + // a resitel v druhe, uvidi po prepnuti jen to, co smi tam. + permissions: [...permissionsOf(user, activeTenant)].sort(), nav: navFor(user, activeTenant), platformAdmin: user.platformAdmin, }; } +/** + * Firma, za kterou request jedna. + * + * Jedno misto, kde se to rozhoduje, protoze podle toho se pocitaji prava. + * Vraci `null`, kdyz uzivatel do zadane firmy nepatri nebo zadnou nema - + * volajici pak nema pravo na nic, coz je spravna odpoved. + */ +export function actingTenant(user: User, requested?: string): string | null { + const access = accessFor(user, requested); + const tenantId = requested ?? access.defaultTenantId; + if (!tenantId) return null; + if (!access.tenants.some((tenant) => tenant.id === tenantId)) { + console.warn(`[access] ${user.email}: pokus o firmu ${tenantId} bez clenstvi`); + return null; + } + return tenantId; +} + export interface ResolvedScope { scope: TicketScope; /** Firmy, ze kterych se smi vracet data. Prazdne pole = nic. */ diff --git a/src/data/bootstrap.ts b/src/data/bootstrap.ts index 2082e77..36a623b 100644 --- a/src/data/bootstrap.ts +++ b/src/data/bootstrap.ts @@ -30,6 +30,7 @@ import { refreshRoles, roleStore, setDynamicPermissions, + syncSystemRoles, systemRoles, invalidatePermissions, } from './permissions.js'; @@ -122,6 +123,20 @@ export async function bootstrapData(options: { databaseReady: boolean }): Promis } } + /* + * Systemove role srovnat s kodem **pred** naplnenim kesi. Vychozi sada se + * pouzije jen do prazdneho uloziste, takze bez tohohle by nove pravo + * v katalogu k uz bezici instalaci nikdy nedoslo. + */ + try { + await syncSystemRoles(); + } catch (err) { + console.error( + '[data] systemove role se nepodarilo srovnat:', + err instanceof Error ? err.message : err, + ); + } + await refreshCaches(); console.info(`[data] nactena uloziste (${mode}): ${entities.map((e) => e.store.kind).join(', ')}`); diff --git a/src/data/permissions.ts b/src/data/permissions.ts index 1ca6e2c..bb40a17 100644 --- a/src/data/permissions.ts +++ b/src/data/permissions.ts @@ -235,6 +235,46 @@ export async function refreshRoles(): Promise { invalidatePermissions(); } +/** + * Srovna ulozene systemove role s tim, jak jsou napsane v kodu. + * + * Vychozi sada se pouzije jen do prazdneho uloziste. Bez tohohle by nove pravo + * v katalogu k existujici instalaci nikdy nedoslo: `role_admin` by zustala + * s vyctem, ktery se ulozil pri prvnim startu, a nova zalozka by se spravci + * firmy neobjevila. Presne to se stalo u prava zalozit ticket a u helpdesku. + * + * Meni se **jen prava**, a jen u roli, ktere jsou nase (`tenantId: null` + * a ID ze `systemRoles`). Nazev ani popis se neprepisuje, ty uz nekdo mohl + * zmenit, a vlastnich roli firmy se to netyka vubec. + */ +export async function syncSystemRoles(): Promise { + const stored = new Map((await roleStore.listAll()).map((role) => [role.id, role])); + let changed = 0; + + for (const role of systemRoles()) { + const current = stored.get(role.id); + if (!current || current.tenantId !== null) continue; + + const same = + current.permissions.length === role.permissions.length && + role.permissions.every((permission) => current.permissions.includes(permission)); + if (same) continue; + + const added = role.permissions.filter((p) => !current.permissions.includes(p)); + const removed = current.permissions.filter((p) => !role.permissions.includes(p)); + await roleStore.update(role.id, { permissions: role.permissions }, { tenantIds: [], includeGlobal: true }); + changed += 1; + + console.info( + `[permissions] role ${role.key} srovnana s kodem` + + (added.length > 0 ? `, pribylo: ${added.join(', ')}` : '') + + (removed.length > 0 ? `, ubylo: ${removed.join(', ')}` : ''), + ); + } + + if (changed > 0) await refreshRoles(); +} + export function rolesById(): Map { return new Map(loadedRoles.map((role) => [role.id, role])); } @@ -245,18 +285,26 @@ export function rolesFor(tenantId: string | null): Role[] { } /** - * Prava uzivatele. + * Prava uzivatele **v jedne firme**. * - * Spojeni pres vsechna clenstvi, protoze pravo je bud, nebo neni - rozlisovat - * ho za firmu by znamenalo predavat firmu do kazde kontroly a to je presne to - * misto, kde se na ni zapomene. + * Driv se 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 + * i ve vsech ostatnich, kam patri. To neni drobnost, to je diera v pravech. * - * Spravce platformy ma vsechno. Je to nase pravo, ne zakaznicke. + * Firma je proto **povinny argument**. Zapomenout na ni nejde: kdyby byla + * nepovinna, prvni volani bez ni by tise vratilo vsechna prava. + * + * `tenantId: null` znamena "mimo firmu", tedy zadna zakaznicka prava. Deje se + * to u uctu bez clenstvi; spravce platformy se sem nedostane, ten ma vsechno. */ -export function permissionsOf(user: User): Set { +export function permissionsOf(user: User, tenantId: string | null): Set { + // Spravce platformy ma vsechno. Je to nase pravo, ne zakaznicke. if (user.platformAdmin) return new Set(allPermissions().map((item) => item.key)); + if (!tenantId) return new Set(); - const cached = effective.get(user.id); + // Klic nese firmu, jinak by se prvni dotaz odpovedel i na druhou. + const cacheKey = `${user.id}:${tenantId}`; + const cached = effective.get(cacheKey); if (cached && Date.now() - cached.at < CACHE_MS) return cached.permissions; const byId = rolesById(); @@ -264,6 +312,8 @@ export function permissionsOf(user: User): Set { const result = new Set(); for (const membership of user.memberships) { + if (membership.tenantId !== tenantId) continue; + for (const roleRef of membership.roleIds) { // Odkaz muze byt ID nebo klic systemove role, aby slo zapsat oboji. const role = byId.get(roleRef) ?? byKey.get(roleRef); @@ -271,6 +321,18 @@ export function permissionsOf(user: User): Set { console.warn(`[permissions] ${user.email}: clenstvi odkazuje na neznamou roli ${roleRef}`); continue; } + /* + * Role patri bud vsem (`tenantId: null`, systemova), nebo jedne firme. + * Cizi firemni role se ignoruje - kdyby platila, staciloby si ji pripsat + * do clenstvi v jine firme a prava by se prenesla. + */ + if (role.tenantId !== null && role.tenantId !== tenantId) { + console.warn( + `[permissions] ${user.email}: role ${role.key} patri firme ${role.tenantId}, ` + + `nepouzije se ve firme ${tenantId}`, + ); + continue; + } for (const permission of role.permissions) { if (knownPermission(permission)) result.add(permission); else console.warn(`[permissions] role ${role.key}: nezname pravo ${permission}`); @@ -278,16 +340,24 @@ export function permissionsOf(user: User): Set { } } - effective.set(user.id, { at: Date.now(), permissions: result }); + effective.set(cacheKey, { at: Date.now(), permissions: result }); return result; } -export function hasPermission(user: User, permission: PermissionKey): boolean { - return permissionsOf(user).has(permission); +export function hasPermission( + user: User, + permission: PermissionKey, + tenantId: string | null, +): boolean { + return permissionsOf(user, tenantId).has(permission); } /** Ma uzivatel aspon jedno z prav? Pro zalozky, kde staci cokoliv z modulu. */ -export function hasAnyPermission(user: User, permissions: PermissionKey[]): boolean { - const owned = permissionsOf(user); +export function hasAnyPermission( + user: User, + permissions: PermissionKey[], + tenantId: string | null, +): boolean { + const owned = permissionsOf(user, tenantId); return permissions.some((permission) => owned.has(permission)); } diff --git a/src/data/tenantFeatures.ts b/src/data/tenantFeatures.ts index 74c1552..1dcd89d 100644 --- a/src/data/tenantFeatures.ts +++ b/src/data/tenantFeatures.ts @@ -182,7 +182,7 @@ export function navFor(user: User, tenantId: string | null): NavItem[] { if (module.platform) return user.platformAdmin; if (!module.required && !enabled.has(module.key)) return false; if (module.permissions.length === 0) return true; - return hasAnyPermission(user, module.permissions); + return hasAnyPermission(user, module.permissions, tenantId); }) .map((module) => ({ key: module.key, label: module.label })); } diff --git a/src/routes/crud.ts b/src/routes/crud.ts index 8fbf168..c9edc97 100644 --- a/src/routes/crud.ts +++ b/src/routes/crud.ts @@ -23,7 +23,7 @@ import { randomUUID } from 'node:crypto'; import { Router, type Request, type Response } from 'express'; import type { z } from 'zod'; -import { accessFor } from '../data/access.js'; +import { accessFor, actingTenant } from '../data/access.js'; import { hasPermission } from '../data/permissions.js'; import { nowIso, type EntityStore, type ListOptions, type TenantEntity } from '../data/store/index.js'; @@ -46,9 +46,33 @@ export function currentTenant(req: Request, res: Response): string | null { } /** Rozsah pro cteni: firmy uzivatele plus volitelne platformni zaznamy. */ +/** + * Firma, za kterou request jedna. Podle ni se pocitaji prava. + * + * Na rozdil od `currentTenant` neodpovida - vraci jen hodnotu, protoze + * kontrola prava chce sama rict, co se stalo. + */ +export function requestTenant(req: Request): string | null { + return actingTenant( + req.user!, + typeof req.query.tenantId === 'string' ? req.query.tenantId : undefined, + ); +} + +/** + * Ze ktere firmy smi request cist. + * + * **Jedna firma, ta prepnuta**, ne vsechny, do kterych clovek patri. Driv to + * byly vsechny a v nastaveni se pak michaly typy ticketu i role napric firmami + * bez ohledu na to, co mel uzivatel vybrane - stejna vada jako u prepinace, + * jen na jinem miste. + * + * `includeGlobal` propousti platformni zaznamy (`tenantId: null`), tedy + * systemove role a podobne. Ty nepatri zadne firme a plati pro vsechny. + */ export function readScope(req: Request, includeGlobal = true): ListOptions { - const access = accessFor(req.user!); - return { tenantIds: access.tenants.map((tenant) => tenant.id), includeGlobal }; + const tenantId = requestTenant(req); + return { tenantIds: tenantId ? [tenantId] : [], includeGlobal }; } export interface CrudOptions { @@ -92,7 +116,11 @@ export function crudRouter(options: CrudOptions { const scope = scopeOrDeny(req, res); if (!scope) return; - if (!hasPermission(req.user!, 'connector.manage')) { + if (!hasPermission(req.user!, 'connector.manage', scope.tenantId)) { return res.status(403).json({ error: 'forbidden', message: 'Adresu pro příjem vidí ten, kdo spravuje napojení.', @@ -326,7 +326,7 @@ dashboardRouter.post('/intake/regenerate', (req, res) => { const scope = scopeOrDeny(req, res); if (!scope) return; - if (!hasPermission(req.user!, 'connector.manage')) { + if (!hasPermission(req.user!, 'connector.manage', scope.tenantId)) { return res.status(403).json({ error: 'forbidden', message: 'Adresu pro příjem mění ten, kdo spravuje napojení.', @@ -580,7 +580,7 @@ dashboardRouter.post('/tickets', (req, res) => { const scope = scopeOrDeny(req, res); if (!scope) return; - if (!hasPermission(req.user!, 'ticket.create')) { + if (!hasPermission(req.user!, 'ticket.create', scope.tenantId)) { return res.status(403).json({ error: 'forbidden', message: 'Nemáte právo zakládat tickety.', @@ -685,7 +685,8 @@ dashboardRouter.post('/tickets/:id/claim', (req, res) => { if (current !== null && current !== access.personId) { // Uz to nekdo resi. Prehodit praci nekomu jinemu je jine rozhodnuti. - if (!hasPermission(req.user!, 'ticket.assign.others')) { + // Pravo se pta za firmu ticketu, ne za tu prave prepnutou. + if (!hasPermission(req.user!, 'ticket.assign.others', ticket.tenantId)) { return res.status(409).json({ error: 'already_assigned', message: 'Ticket už někdo řeší. Přehodit ho může jen ten, kdo na to má právo.', diff --git a/src/routes/helpdesk.ts b/src/routes/helpdesk.ts index b23c6d5..87bdfe8 100644 --- a/src/routes/helpdesk.ts +++ b/src/routes/helpdesk.ts @@ -25,7 +25,7 @@ import { Router, type Request, type Response } from 'express'; import { z } from 'zod'; -import { accessFor } from '../data/access.js'; +import { actingTenant } from '../data/access.js'; import { recordAudit } from '../data/audit.js'; import { hasPermission } from '../data/permissions.js'; import { findPersonByEmail } from '../data/people.js'; @@ -42,7 +42,25 @@ export const helpdeskRouter = Router(); * kdo se pta. */ function asking(req: Request, res: Response, permission: string): string | null { - if (!hasPermission(req.user!, permission)) { + /* + * Nejdriv firma, teprve pak pravo. Prava se pocitaji **za firmu**, takze + * bez ni neni ceho se ptat - a kdyz clovek patri do vic firem, rozhoduje + * ta, kterou ma prepnutou. + */ + const tenantId = actingTenant( + req.user!, + typeof req.query.tenantId === 'string' ? req.query.tenantId : undefined, + ); + + if (!tenantId) { + res.status(403).json({ + error: 'no_tenant', + message: 'Účet nepatří do žádné firmy, za kterou by šlo požadavek poslat.', + }); + return null; + } + + if (!hasPermission(req.user!, permission, tenantId)) { res.status(403).json({ error: 'forbidden', message: @@ -53,17 +71,6 @@ function asking(req: Request, res: Response, permission: string): string | null return null; } - const requested = typeof req.query.tenantId === 'string' ? req.query.tenantId : undefined; - const access = accessFor(req.user!, requested); - const tenantId = requested ?? access.defaultTenantId; - - if (!tenantId || !access.tenants.some((tenant) => tenant.id === tenantId)) { - res.status(403).json({ - error: 'no_tenant', - message: 'Účet nepatří do žádné firmy, za kterou by šlo požadavek poslat.', - }); - return null; - } return tenantId; } @@ -109,7 +116,7 @@ helpdeskRouter.get('/', (req, res) => { tenantId, /** Komu se posila. Klient podle toho rekne, na koho se pozadavek obraci. */ provider: provider ? { id: provider, name: findTenant(provider)?.name ?? provider } : null, - canCreate: hasPermission(req.user!, 'helpdesk.create'), + canCreate: hasPermission(req.user!, 'helpdesk.create', tenantId), }); }); diff --git a/src/routes/tenantScripts.ts b/src/routes/tenantScripts.ts index 5557fb5..b06c2b5 100644 --- a/src/routes/tenantScripts.ts +++ b/src/routes/tenantScripts.ts @@ -25,7 +25,7 @@ import { import { hasPermission } from '../data/permissions.js'; import { requireAuth } from '../middleware/auth.js'; import { runSandbox } from '../runtime/sandbox.js'; -import { currentTenant } from './crud.js'; +import { currentTenant, requestTenant } from './crud.js'; export const tenantScriptRouter = Router(); @@ -36,7 +36,8 @@ const PERMISSION = 'action.manage'; /** Vrati false a odpovi, kdyz volajici na zmenu nema pravo. */ function mayWrite(req: Parameters[0], res: Parameters[1]): boolean { - if (hasPermission(req.user!, PERMISSION)) return true; + // Pravo za firmu, kterou ma clovek prepnutou. Skript patri jedne firme. + if (hasPermission(req.user!, PERMISSION, requestTenant(req))) return true; console.warn(`[skripty] ${req.user!.email}: chybi pravo ${PERMISSION}`); res.status(403).json({ error: 'forbidden', message: 'K této změně nemáte oprávnění.' }); return false; diff --git a/src/routes/ticketActions.ts b/src/routes/ticketActions.ts index 0b46cf0..f68dddf 100644 --- a/src/routes/ticketActions.ts +++ b/src/routes/ticketActions.ts @@ -118,7 +118,8 @@ ticketActionsRouter.get('/:id/actions', async (req, res) => { [ticket.tenantId], { typeId: ticket.typeId, tags: ticket.tags }, factsOf(ticket), - (permission) => hasPermission(req.user!, permission), + // Pravo za firmu ticketu: akce patri jemu, ne prepnute firme. + (permission) => hasPermission(req.user!, permission, ticket.tenantId), ); return res.json({ items: actions.map(publicAction) }); @@ -150,7 +151,7 @@ ticketActionsRouter.post('/:id/actions/:actionId', async (req, res) => { return res.status(404).json({ error: 'not_found', message: 'Akce neexistuje.' }); } - if (!hasPermission(req.user!, actionPermission(action.id))) { + if (!hasPermission(req.user!, actionPermission(action.id), ticket.tenantId)) { recordAudit({ userId: req.user!.id, userEmail: req.user!.email, @@ -364,7 +365,7 @@ function builtinAction(options: { const ticket = getTicket(req.params.id, scope.tenantIds); if (!ticket) return res.status(404).json({ error: 'not_found', message: 'Ticket neexistuje.' }); - if (!hasPermission(req.user!, options.permission)) { + if (!hasPermission(req.user!, options.permission, ticket.tenantId)) { recordAudit({ userId: req.user!.id, userEmail: req.user!.email,