Files
service-finder/docs/p0_tenant_isolation_vehicle_mapping_audit_report.md
2026-06-24 11:29:45 +00:00

14 KiB
Raw Permalink Blame History

🚨 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


Audit lezárva: 2026-06-23 22:12 UTC