admin felület különválasztva

This commit is contained in:
Roo
2026-06-24 11:29:45 +00:00
parent 71ef33bb85
commit 80a5d67f79
462 changed files with 87873 additions and 312 deletions

View 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*