# 🚨 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`](backend/app/api/v1/endpoints/assets.py:229) **Szignatúra:** ```python @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`](backend/app/api/v1/endpoints/assets.py:259) ```python 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`](backend/app/api/v1/endpoints/assets.py:286) ```python 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`](frontend/src/stores/cost.ts:71) A költség létrehozásánál defense-in-depth jelleggel injektálja az `active_organization_id`-t: ```typescript 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`](frontend/src/views/costs/CostsView.vue:435) A `fetchCosts()` függvény helyesen adja át az `active_organization_id`-t a backendnek: ```typescript const params: Record = { 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`](frontend/src/views/costs/CostsView.vue:559) ```typescript 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`](frontend/src/views/costs/CostsView.vue:549) ```typescript 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`](frontend/src/stores/vehicle.ts:141) ```typescript 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 ```mermaid 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
(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`](frontend/src/views/costs/CostsView.vue:549) **Jelenleg:** ```typescript watch( () => authStore.user?.active_organization_id, () => { currentPage.value = 1 fetchCosts() } ) ``` **Javítás után:** ```typescript 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`](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: ```typescript async function fetchVehicles(organizationId?: number | null) { isLoading.value = true error.value = null try { const params: Record = {} 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`](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: ```typescript // 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`](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): ```python @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](frontend/src/views/costs/CostsView.vue) - A hibás watch blokk (549-556) és onMounted (558-565) - [vehicle.ts store](frontend/src/stores/vehicle.ts) - A fetchVehicles() függvény (141-158) - [auth.ts store](frontend/src/stores/auth.ts) - switchOrganization() (687) és active_organization_id (70) - [cost.ts store](frontend/src/stores/cost.ts) - Defense-in-depth org_id injektálás (71) - [assets.py (backend)](backend/app/api/v1/endpoints/assets.py) - GET /assets/vehicles (229-318) --- *Audit lezárva: 2026-06-23 22:12 UTC*