Files
service-finder/backend/app/api/deps.py

300 lines
12 KiB
Python
Executable File

# /opt/docker/dev/service_finder/backend/app/api/deps.py
from typing import Optional, Dict, Any, Union
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
from app.core.capabilities import SYSTEM_CAPABILITIES_MATRIX, role_has_capability
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
async def get_current_admin(
current_user: User = Depends(get_current_user)
) -> User:
"""
Csak admin/moderátor/superadmin szerepkörrel rendelkező felhasználók számára.
"""
# A UserRole Enum értékeit használjuk (RBAC Phase 1)
allowed_roles = {
UserRole.SUPERADMIN,
UserRole.ADMIN,
UserRole.MODERATOR,
}
if current_user.role not in allowed_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=t("AUTH.INSUFFICIENT_ADMIN_PERMISSIONS")
)
return current_user
# ═══════════════════════════════════════════════════════════════════════════════
# RBAC Phase 2: Capability Dependencies (Kapuőrök)
# ═══════════════════════════════════════════════════════════════════════════════
def RequireSystemCapability(capability_name: str):
"""
🎯 Rendszerszintű képesség-ellenőrző dependency.
Használat:
@router.get("/admin/users")
async def list_users(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
_ = Depends(RequireSystemCapability("can_manage_users"))
):
Logika:
1. Ha a user SUPERADMIN → azonnal átengedi (True).
2. Egyébként lekéri a user role-ját, és ellenőrzi a
SYSTEM_CAPABILITIES_MATRIX-ból, hogy a kért capability True-e.
3. Ha nincs meg a capability → 403 Forbidden.
"""
async def system_capability_checker(
current_user: User = Depends(get_current_user),
) -> bool:
# SUPERADMIN mindent visz
if current_user.role == UserRole.SUPERADMIN:
return True
# Ellenőrizzük a SYSTEM_CAPABILITIES_MATRIX-ból
role_key = current_user.role.value # pl. "ADMIN", "MODERATOR", "USER"
if not role_has_capability(role_key, capability_name):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"SYSTEM_CAPABILITY_DENIED: A '{capability_name}' képesség nem elérhető a '{role_key}' szerepkör számára."
)
return True
return system_capability_checker
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