14 KiB
🚨 P0 READ-ONLY AUDIT - Tenant Isolation & Vehicle Mapping Report
Dátum: 2026-06-23
Auditor: Rendszer-Architect (DeepSeek Reasoner)
Státusz: Elkészült
Scope: Full-stack (Database → Backend API → Frontend)
🔍 Executive Summary
A vizsgálat célja a CostsView.vue jármű legördülő menüjében (vehicle dropdown) tapasztalt tenant izolációs hiba (Garage Isolation) kivizsgálása volt. A hibajelenség: amikor a felhasználó garázst/szervezetet vált, a költségek helyesen frissülnek, de a jármű legördülő lista a régi garázs összes járművét mutatja továbbra is.
Verdikt: A backend API helyesen szűr (current_organization_id alapján, JWT scope_id-n keresztül). A hiba gyökere kizárólag a frontend cache elavulásában (cache staleness) rejlik: a járművek csak egyszer töltődnek be (onMounted), és soha nem frissülnek garázsváltáskor.
📊 STEP 1: Database Data Mapping (tester_pro, User ID: 28)
1.1 User Adatok
| Mező | Érték |
|---|---|
id |
28 |
email |
tester_pro@profibot.hu |
role |
ADMIN |
scope_id |
null (personal mode) |
scope_level |
individual |
subscription_plan |
PREMIUM |
ui_mode |
personal |
is_active |
true |
1.2 Organization Tagságok
| Org ID | Név | Típus | Szerepkör | Státusz |
|---|---|---|---|---|
| 1 | Test Company |
individual |
OWNER |
active, verified |
| 21 | Private_28 |
individual |
OWNER |
active, verified |
1.3 Járművek (Asset-ek) Szervezetenként
🏢 Org 1 - Test Company (19 jármű)
| Rendszám | Márka | Modell | VIN | Státusz | Data Status |
|---|---|---|---|---|---|
BMW123 |
BMW | BMWM3 | - | active |
verified |
QWE432 |
BMW | R 1250 GS ADVENTURE | - | active |
verified |
TEST-000 |
Ford | Focus | TESTVIN1111111111 | active |
verified |
TEST-111 |
Ford | Focus | TESTVIN1111111119 | active |
verified |
TEST-123 |
Ford | Focus | TESTVIN1234567890 | active |
draft |
TEST-555 |
Ford | Focus | TESTVIN5555555559 | active |
verified |
TEST-777 |
Ford | Focus | TESTVIN7777777779 | active |
verified |
TEST-888 |
Ford | Focus | TESTVIN8888888889 | active |
draft |
TEST-999 |
Ford | Focus | TESTVIN9999999999 | active |
draft |
TEST-API-999 |
TestBrand | TestModel | - | active |
draft |
DRAFT-000 |
Ismeretlen | - | - | draft |
draft |
DRAFT-111 |
Ismeretlen | - | - | draft |
draft |
DRAFT-456 |
Ismeretlen | - | - | draft |
draft |
DRAFT-555 |
Ismeretlen | - | - | draft |
draft |
DRAFT-777 |
Ismeretlen | - | - | draft |
draft |
DRAFT-888 |
Ismeretlen | - | - | draft |
draft |
DRAFT-999 |
Ismeretlen | - | - | draft |
draft |
TEST123 |
TestBrand | TestModel | TESTVIN123456789 | draft |
draft |
HGV-TEST-01 |
MERCEDES-BENZ | ACTROS | - | archived |
verified |
🏠 Org 21 - Private_28 (6 jármű)
| Rendszám | Márka | Modell | VIN | Státusz | Data Status |
|---|---|---|---|---|---|
ABC-123 |
Toyota | Corolla | 1HGCM82633A123456 | active |
verified |
AIML519 |
Skoda | Citigo e | - | active |
enriched |
PKT215 |
Mazda | 2 | - | active |
draft |
QWE123 |
APRILIA | af1 | - | active |
draft |
TTRFGHZT |
BAYLINER | 1750 CAPRY BOWRIDER | - | active |
draft |
UOK795 |
Honda | CB1000R | ZDCSC60C0FF091488 | active |
verified |
Összesen: 25 jármű a két szervezetben (10+6 aktív, 8 draft, 1 archived).
🏗️ STEP 2: Backend API Endpoint Audit
Végpont: GET /assets/vehicles
Fájl: backend/app/api/v1/endpoints/assets.py
Szignatúra:
@router.get("/vehicles", response_model=List[AssetResponse])
async def get_user_vehicles(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=500),
db: AsyncSession = Depends(get_db),
current_user: AuthContext = Depends(get_current_user)
):
Függőségek: A végpont nem fogad el organization_id query paramétert a frontendtől. Kizárólag a JWT tokenben tárolt current_user.scope_id alapján szűr.
Szűrési Logika
1️⃣ Personal Mode (nincs scope_id beállítva) - assets.py:259
if current_user.scope_id is None:
org_stmt = select(OrganizationMember.organization_id).where(
OrganizationMember.user_id == current_user.id
)
result = await db.execute(org_stmt)
user_org_ids = [row[0] for row in result.fetchall()]
stmt = select(Asset).where(
Asset.current_organization_id.in_(user_org_ids),
Asset.status == "active"
)
Mit csinál: Personal módban lekéri az összes szervezet ID-t, ahol a user tag, majd visszaadja az összes olyan szervezet összes aktív járművét. Ez a teljes flotta a user szemszögéből.
2️⃣ Corporate Mode (scope_id beállítva) - assets.py:286
else:
scope_org_id = int(current_user.scope_id)
stmt = select(Asset).where(
Asset.current_organization_id == scope_org_id,
Asset.status == "active"
)
Mit csinál: Corporate módban kizárólag a scope_id-ban megadott szervezet járműveit adja vissza. Ez a helyes, izolált viselkedés.
Backend Audit Verdict: ✅ Helyes
A backend API logikája megfelelő. A probléma nem a backendben van.
🖥️ STEP 3: Frontend Audit - CostsView.vue & Stores
3.1 A Hibás Működés Rekonstrukciója
✅ Amit a cost.ts store jól csinál - cost.ts:71
A költség létrehozásánál defense-in-depth jelleggel injektálja az active_organization_id-t:
const userOrgId = useAuthStore().user?.active_organization_id
if (userOrgId != null) {
body.organization_id = userOrgId
}
✅ Amit a CostsView.vue jól csinál - CostsView.vue:435
A fetchCosts() függvény helyesen adja át az active_organization_id-t a backendnek:
const params: Record<string, any> = { page: currentPage.value, per_page: itemsPerPage.value }
const userOrgId = authStore.user?.active_organization_id
if (userOrgId != null) {
params.organization_id = userOrgId
}
3.2 A Hiba Gyökere: Cache Staleness
❌ 1. hiba: A járművek csak egyszer töltődnek be - CostsView.vue:559
onMounted(() => {
if (vehicleStore.vehicles.length === 0) {
vehicleStore.fetchVehicles() // <-- CSAK egyszer, onMounted-ben!
}
fetchCosts()
})
A járművek csak akkor töltődnek be, ha a store még üres. Ez azt jelenti, hogy ha a user már járt a Costs oldalon, a járművek a Pinia store-ban cache-elődnek, és soha többé nem frissülnek.
❌ 2. hiba: Nincs watch az org váltásra - CostsView.vue:549
watch(
() => authStore.user?.active_organization_id,
() => {
currentPage.value = 1
fetchCosts() // <-- Csak költségeket frissíti
// 🔴 HIÁNYZIK: vehicleStore.fetchVehicles() !
}
)
Amikor a user garázst vált, a watch csak a költségeket frissíti. A járművek nem frissülnek.
❌ 3. hiba: fetchVehicles() nem fogad el org_id paramétert - vehicle.ts:141
async function fetchVehicles() {
isLoading.value = true
error.value = null
try {
const res = await api.get('/assets/vehicles') // <-- NINCS org_id param
vehicles.value = res.data as Vehicle[]
} catch (err: any) { ... }
}
Még ha meg is hívnánk a fetchVehicles()-t org váltáskor, a függvény jelenleg nem tud szűrni - minden esetben a teljes (scope-alapú) listát kéri le. Szerencsére a backend JWT scope_id-t használ, így ha újrahívjuk, a backend a helyes (már frissített scope) alapján adja vissza az adatokat.
3.3 A Szivárgás Vizualizációja
sequenceDiagram
participant User
participant CostsView
participant vehicleStore
participant authStore
participant Backend
Note over User,Backend: === Initial Load (onMounted) ===
User->>CostsView: Navigates to Costs page
CostsView->>vehicleStore: fetchVehicles() [no org filter]
vehicleStore->>Backend: GET /assets/vehicles [scope_id=null]
Backend-->>vehicleStore: [25 vehicles from ALL orgs]
vehicleStore->>CostsView: vehicles cached in Pinia
Note over User,Backend: === Org Switch ===
User->>CostsView: Switches to Org 21 (Private_28)
CostsView->>authStore: switchOrganization(21)
authStore->>Backend: PATCH /users/me/active-organization
Note over CostsView: WATCH triggers
CostsView->>CostsView: fetchCosts() with org_id=21 ✅
CostsView->>CostsView: 🔴 DOES NOT refresh vehicles!
Note over CostsView: Vehicle dropdown shows ALL 25 vehicles<br/>(from Test Company AND Private_28)
Note over CostsView: But expenses are correctly filtered to Org 21 only!
🎯 STEP 4: Javítási Javaslat (Fix Recommendation)
Hiba Kategorizálás
| Kritérium | Érték |
|---|---|
| Típus | Frontend Cache Staleness (Pinia store nem frissül garázsváltáskor) |
| Súlyosság | P0 - Kritikus (adat szivárgás: user látja más garázs járműveit) |
| Érintett fájlok | CostsView.vue, vehicle.ts |
| Backend érintettség | ❌ Nincs (backend helyesen szűr) |
| Adatbázis érintettség | ❌ Nincs (adatok helyesen vannak tárolva) |
Javítási Terv
1. Kötelező javítás: CostsView.vue watch kiegészítése
Fájl: frontend/src/views/costs/CostsView.vue:549
Jelenleg:
watch(
() => authStore.user?.active_organization_id,
() => {
currentPage.value = 1
fetchCosts()
}
)
Javítás után:
watch(
() => authStore.user?.active_organization_id,
() => {
currentPage.value = 1
vehicleStore.fetchVehicles() // <-- ÚJ: Járművek frissítése org váltáskor
fetchCosts()
}
)
2. Ajánlott javítás: vehicle.ts - add org_id paraméter támogatás
Fájl: frontend/src/stores/vehicle.ts:141
Javaslat: Bár a backend JWT scope alapján is szűr, érdemes explicit organization_id query paraméter támogatást hozzáadni:
async function fetchVehicles(organizationId?: number | null) {
isLoading.value = true
error.value = null
try {
const params: Record<string, any> = {}
if (organizationId != null) {
params.organization_id = organizationId
}
const res = await api.get('/assets/vehicles', { params })
vehicles.value = res.data as Vehicle[]
} catch (err: any) { ... }
}
3. Opcionális javítás: vehicleStore automatikus cache invalidáció
Fájl: frontend/src/stores/vehicle.ts
Adj hozzá egy watch-t a vehicle store-ban, ami automatikusan frissíti a járműveket, ha a user active_organization_id-ja megváltozik:
// vehicle.ts store-ban
watch(
() => useAuthStore().user?.active_organization_id,
() => {
fetchVehicles() // Automatikus cache invalidáció org váltáskor
}
)
4. Opcionális javítás: Backend GET /assets/vehicles endpoint bővítése
Fájl: backend/app/api/v1/endpoints/assets.py:229
Adj hozzá egy opcionális organization_id query paramétert, ami felülbírálja a JWT scope_id-t (admin/jogosultság ellenőrzéssel):
@router.get("/vehicles", response_model=List[AssetResponse])
async def get_user_vehicles(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=500),
organization_id: Optional[int] = Query(None), # <-- ÚJ paraméter
db: AsyncSession = Depends(get_db),
current_user: AuthContext = Depends(get_current_user)
):
📋 Összefoglalás
Valóság vs. Elvárt Működés
| Aspektus | Elvárt | Valóság |
|---|---|---|
tester_pro garázsai |
Csak az aktív garázs adatai | Mindkét garázs (Test Company + Private_28) járművei látszanak |
| Költségek szűrése | Csak az aktív garázsra | ✅ Helyes - org_id alapján szűr |
| Járművek szűrése | Csak az aktív garázsra | ❌ Hibás - nincs org_id filter, cache-ből jön |
| Backend API | Helyes scope alapú szűrés | ✅ Helyes - JWT scope_id alapján dolgozik |
| Adatbázis | Helyes org-hozzárendelés | ✅ Helyes - minden asset.current_organization_id be van állítva |
Következtetés
A tenant izolációs hiba kizárólag frontend oldali. A backend API és az adatbázis séma megfelelő. A javításhoz elegendő a CostsView.vue watch blokkját kiegészíteni egy vehicleStore.fetchVehicles() hívással. A többi javítási javaslat (backend paraméter, store szintű watch) opcionális, de ajánlott a robosztusság növeléséhez.
🔗 Referenciák
- CostsView.vue - A hibás watch blokk (549-556) és onMounted (558-565)
- vehicle.ts store - A fetchVehicles() függvény (141-158)
- auth.ts store - switchOrganization() (687) és active_organization_id (70)
- cost.ts store - Defense-in-depth org_id injektálás (71)
- assets.py (backend) - GET /assets/vehicles (229-318)
Audit lezárva: 2026-06-23 22:12 UTC