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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2329ccbdc1
commit
2e0b9456a9
+32
-4
@@ -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<T extends TenantEntity, C, U> {
|
||||
@@ -92,7 +116,11 @@ export function crudRouter<T extends TenantEntity, C, U>(options: CrudOptions<T,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (!hasPermission(req.user!, options.writePermission)) {
|
||||
/*
|
||||
* Prava se ptaji za firmu **toho zaznamu**, ne za tu, kterou ma clovek
|
||||
* prave prepnutou. Kdo edituje zaznam jine firmy, musi mit pravo tam.
|
||||
*/
|
||||
if (!hasPermission(req.user!, options.writePermission, entity?.tenantId ?? requestTenant(req))) {
|
||||
console.warn(
|
||||
`[crud] ${req.user!.email}: chybi pravo ${options.writePermission} u ${options.store.kind}`,
|
||||
);
|
||||
|
||||
@@ -291,7 +291,7 @@ dashboardRouter.get('/intake', (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 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.',
|
||||
|
||||
+21
-14
@@ -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),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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<typeof requireAuth>[0], res: Parameters<typeof requireAuth>[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;
|
||||
|
||||
@@ -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<T>(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,
|
||||
|
||||
Reference in New Issue
Block a user