# /opt/docker/dev/service_finder/backend/app/api/deps.py from typing import Optional, Dict, Any, Union, List import logging from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from sqlalchemy.orm import selectinload, joinedload from app.db.session import get_db from app.core.security import decode_token, DEFAULT_RANK_MAP from app.models.identity import User, UserRole # JAVÍTVA: Új Identity modell használata from app.models.marketplace.organization import OrgRole, OrganizationMember from app.core.config import settings from app.core.translation_helper import t # Translation helper logger = logging.getLogger(__name__) # --- GONDOLATMENET / THOUGHT PROCESS --- # 1. Az OAuth2 folyamat a központosított bejelentkezési végponton keresztül fut. # 2. A token visszafejtésekor ellenőrizni kell a 'type' mezőt, hogy ne lehessen refresh tokennel belépni. # 3. A felhasználó lekérésekor a SQLAlchemy 2.0 aszinkron 'execute' és 'scalar_one_or_none' metódusait használjuk. # 4. A Scoped RBAC (Role-Based Access Control) biztosítja, hogy a felhasználók ne férjenek hozzá egymás flottáihoz. # --------------------------------------- # Az OAuth2 folyamat a bejelentkezési végponton keresztül reusable_oauth2 = OAuth2PasswordBearer( tokenUrl=f"{settings.API_V1_STR}/auth/login" ) async def get_current_token_payload( token: str = Depends(reusable_oauth2) ) -> Dict[str, Any]: """ JWT token visszafejtése és a típus (access) ellenőrzése. """ # Fejlesztői bypass (opcionális, csak DEBUG módban) if settings.DEBUG and token == "dev_bypass_active": return { "sub": "1", "role": "superadmin", "rank": 100, "scope_level": "global", "scope_id": "all", "type": "access" } payload = decode_token(token) if not payload or payload.get("type") != "access": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=t("AUTH.INVALID_OR_EXPIRED_SESSION") ) return payload async def get_current_user( db: AsyncSession = Depends(get_db), payload: Dict = Depends(get_current_token_payload) ) -> User: """ Lekéri a felhasználót a token 'sub' mezője alapján (SQLAlchemy 2.0 aszinkron módon). """ user_id = payload.get("sub") if not user_id: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=t("AUTH.TOKEN_IDENTIFICATION_ERROR") ) # JAVÍTVA: Modern SQLAlchemy 2.0 aszinkron lekérdezés with eager loading # We need to load the person relationship to avoid lazy loading issues # Also load Person.address for nested profile response from app.models.identity import Person from app.models.identity.address import Address, GeoPostalCode stmt = ( select(User) .where(User.id == int(user_id)) .options( joinedload(User.person) .joinedload(Person.address) .joinedload(Address.postal_code) # <-- EZ KELL IDE: postal_code eager loading a zip/city property-khez ) ) result = await db.execute(stmt) user = result.unique().scalar_one_or_none() if not user or user.is_deleted: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=t("AUTH.USER_NOT_FOUND") ) return user async def get_current_active_user( current_user: User = Depends(get_current_user), ) -> User: """ Ellenőrzi, hogy a felhasználó aktív-e (KYC Step 2 kész). """ if not current_user.is_active: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=t("AUTH.ACTIVE_PROFILE_KYC_REQUIRED") ) return current_user async def check_resource_access( resource_scope_id: Union[str, int], current_user: User = Depends(get_current_user) ): """ Scoped RBAC: Megakadályozza a jogosulatlan hozzáférést mások adataihoz. """ if current_user.role == UserRole.SUPERADMIN: return True user_scope = str(current_user.scope_id) if current_user.scope_id else None requested_scope = str(resource_scope_id) # 1. Saját ID ellenőrzése if str(current_user.id) == requested_scope: return True # 2. Szervezeti/Flotta scope ellenőrzése if user_scope and user_scope == requested_scope: return True raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=t("AUTH.NO_PERMISSION_FOR_RESOURCE") ) def check_min_rank(role_key: str): """ Dinamikus Rank ellenőrzés a system_parameters tábla alapján. """ async def rank_checker( db: AsyncSession = Depends(get_db), payload: Dict = Depends(get_current_token_payload) ): # A settings.get_db_setting-et használjuk a dinamikus lekéréshez ranks = await settings.get_db_setting( db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP ) # A DEFAULT_RANK_MAP nagybetűs kulcsokat vár, ezért átalakítjuk role_key_upper = role_key.upper() required_rank = ranks.get(role_key_upper, 0) user_rank = payload.get("rank", 0) if user_rank < required_rank: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"Alacsony jogosultsági szint. (Elvárt: {required_rank})" ) return True return rank_checker # ═══════════════════════════════════════════════════════════════════════════════ # RBAC Phase 1.5: Staff & Flexible Role Dependencies # ═══════════════════════════════════════════════════════════════════════════════ # All internal staff roles that can access the Backoffice (admin.servicefinder.hu) STAFF_ROLES = { UserRole.SUPERADMIN, UserRole.ADMIN, UserRole.MODERATOR, UserRole.SALES_REP, UserRole.SERVICE_MGR, } async def get_current_staff( current_user: User = Depends(get_current_user) ) -> User: """ 🎯 Staff-level access dependency. Admits all internal staff roles: SUPERADMIN, ADMIN, MODERATOR, SALES_REP, and SERVICE_MGR. Use this for Backoffice (admin.servicefinder.hu) endpoints that should be accessible to all staff members, not just superadmins. """ if current_user.role not in STAFF_ROLES: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="AUTH.STAFF_ONLY" ) return current_user # ═══════════════════════════════════════════════════════════════════════════════ # RBAC Phase 2: Capability Dependencies (Kapuőrök) # ═══════════════════════════════════════════════════════════════════════════════ def RequireOrgCapability(capability_name: str): """ 🎯 Szervezeti képesség-ellenőrző dependency. Használat: @router.post("/expenses") async def create_expense( expense: AssetCostCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), _ = Depends(RequireOrgCapability("can_add_expense")) ): Logika: 1. Ha a user SUPERADMIN → azonnal átengedi (True). 2. Lekéri a user OrgRole-ját az adott organization_id alapján a fleet.organization_members táblából. 3. Kikeresi a fleet.org_roles táblából a szerepkörhöz tartozó permissions JSONB oszlopot. 4. Ha a JSONB-ben a kért capability_name nem True → 403 Forbidden. Fontos: Ez a dependency egy organization_id paramétert vár a végponttól. A végpontnak át kell adnia az org_id-t a dependency-nek. """ async def org_capability_checker( organization_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ) -> bool: # SUPERADMIN mindent visz if current_user.role == UserRole.SUPERADMIN: return True # 1. Lekérjük a user szerepkörét a szervezetben member_stmt = select(OrganizationMember.role).where( OrganizationMember.user_id == current_user.id, OrganizationMember.organization_id == organization_id, OrganizationMember.status == "active" ).limit(1) member_result = await db.execute(member_stmt) org_role_name = member_result.scalar_one_or_none() if not org_role_name: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="ORG_MEMBERSHIP_REQUIRED: You are not a member of this organization." ) # 2. Lekérjük a szerepkör permissions JSONB-jét a fleet.org_roles táblából role_stmt = select(OrgRole.permissions).where( OrgRole.name_key == org_role_name, OrgRole.is_active == True ).limit(1) role_result = await db.execute(role_stmt) permissions = role_result.scalar_one_or_none() if not permissions: # Fallback: ha nincs a fleet.org_roles táblában, használjuk a # OrganizationMember.permissions mezőt (örökölt viselkedés) member_perm_stmt = select(OrganizationMember.permissions).where( OrganizationMember.user_id == current_user.id, OrganizationMember.organization_id == organization_id, OrganizationMember.status == "active" ).limit(1) member_perm_result = await db.execute(member_perm_stmt) permissions = member_perm_result.scalar_one_or_none() or {} # 3. Ellenőrizzük a kért képességet if not isinstance(permissions, dict): permissions = {} if not permissions.get(capability_name, False): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"ORG_CAPABILITY_DENIED: A '{capability_name}' képesség nem elérhető a '{org_role_name}' szerepkör számára ebben a szervezetben." ) return True return org_capability_checker # ═══════════════════════════════════════════════════════════════════════════════ # RBAC Phase 2: DB-Driven Permission Code Dependency (RequirePermission) # ═══════════════════════════════════════════════════════════════════════════════ def RequirePermission(permission_code: str): """ 🎯 DB-driven permission code dependency factory. Queries the system.role_permissions table (via RBACService.get_role_permissions) to check if the current user's role has the specified permission code granted. SUPERADMIN bypass: If the user's role rank is 100 (SUPERADMIN), access is granted immediately without a database lookup. Usage: @router.get("/admin/sensitive-data") async def get_sensitive_data( current_user: User = Depends(get_current_user), _ = Depends(RequirePermission("fleet:view")), ): Args: permission_code: The permission code to check (e.g., "fleet:view", "user:create") Returns: A dependency callable that returns the current_user if permission is granted, or raises HTTPException(403) if denied. """ async def permission_checker( current_user: User = Depends(get_current_active_user), db: AsyncSession = Depends(get_db), ) -> User: # ── SUPERADMIN bypass ── # SUPERADMIN has rank 100 → bypass all permission checks if current_user.role == UserRole.SUPERADMIN: return current_user # ── Resolve role_id ── # Use the DB-driven role_id if available, otherwise fall back to # the legacy UserRole enum for backward compatibility role_id = getattr(current_user, 'role_id', None) if role_id is None: # Fallback: SUPERADMIN already handled above, so this is a non-admin # user without a role_id assigned yet. Deny access. raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"Missing permission: {permission_code}. No role_id assigned to user." ) # ── Query permissions from DB ── from app.services.rbac_service import rbac_service user_permissions = await rbac_service.get_role_permissions(db, role_id) # ── Check the specific permission ── if permission_code not in user_permissions: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"Missing permission: {permission_code}" ) return current_user return permission_checker