admin felület különválasztva
This commit is contained in:
366
docs/p0_tenant_isolation_vehicle_mapping_audit_report.md
Normal file
366
docs/p0_tenant_isolation_vehicle_mapping_audit_report.md
Normal file
@@ -0,0 +1,366 @@
|
||||
# 🚨 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<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`](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<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`](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<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`](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*
|
||||
Reference in New Issue
Block a user