groupe-meditation/backend/app/core/security.py
Daniel Allaire 393f9243cb Renforcer isolation multi-tenant
- scoper les postes et permissions par groupe

- retirer les lectures district du calendrier, des evenements et anniversaires

- documenter les regles d isolation multi-groupe
2026-05-31 18:12:26 -04:00

84 lines
3 KiB
Python

"""Authentification JWT + hachage PIN."""
from datetime import datetime, timedelta, timezone
from jose import jwt, JWTError
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
import bcrypt
from app.core.config import settings
from app.core.database import get_db
security = HTTPBearer()
def hash_pin(pin: str) -> str:
return bcrypt.hashpw(pin.encode("utf-8"), bcrypt.gensalt(rounds=12)).decode("utf-8")
def verify_pin(pin: str, hashed: str) -> bool:
return bcrypt.checkpw(pin.encode("utf-8"), hashed.encode("utf-8"))
def create_token(membre_id: str, groupe_id: str, expire_jours: int = None, admin_id: str | None = None) -> str:
jours = expire_jours or settings.JWT_EXPIRE_DAYS
expire = datetime.now(timezone.utc) + timedelta(days=jours)
payload = {"sub": str(membre_id), "grp": str(groupe_id), "exp": expire}
if admin_id:
payload["adm"] = str(admin_id)
return jwt.encode(payload, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM)
def decode_token(token: str) -> dict:
try:
return jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM])
except JWTError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token invalide ou expiré")
def is_sysadmin(membre) -> bool:
return bool(getattr(membre, "is_sysadmin", False))
async def get_current_membre(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: AsyncSession = Depends(get_db),
):
from app.models.membre import Membre
payload = decode_token(credentials.credentials)
membre_id = payload.get("sub")
groupe_id = payload.get("grp")
if not membre_id:
raise HTTPException(status_code=401, detail="Token invalide")
result = await db.execute(select(Membre).where(Membre.id == membre_id, Membre.actif == True))
membre = result.scalar_one_or_none()
if not membre:
raise HTTPException(status_code=401, detail="Membre introuvable ou inactif")
if groupe_id and str(membre.groupe_id) != str(groupe_id):
raise HTTPException(status_code=401, detail="Session invalide pour ce groupe")
if payload.get("adm"):
setattr(membre, "_sysadmin_session", True)
return membre
async def require_executif(
membre=Depends(get_current_membre),
db: AsyncSession = Depends(get_db),
):
if is_sysadmin(membre):
return membre
from app.models.affectation import Affectation
from app.models.poste import Poste
result = await db.execute(
select(Affectation).join(Poste).where(
Affectation.membre_id == membre.id,
Affectation.groupe_id == membre.groupe_id,
Affectation.statut == "actif",
Poste.groupe_id == membre.groupe_id,
Poste.categorie == "executif",
)
)
if not result.scalars().first():
raise HTTPException(status_code=403, detail="Réservé à l'exécutif")
return membre