From 83bb0a29ec0808e866530a458afa4689047e5992 Mon Sep 17 00:00:00 2001 From: BFLOWS Date: Mon, 27 Apr 2026 09:06:10 +0200 Subject: [PATCH] feat(auth): autorizza ROLE_CONFIDI come proprietario pratica (parallelo BENEFICIARY) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Risoluzione 403 segnalato da Rinaldo Bonazzo su upload fattura con utente ROLE_CONFIDI (confidi4@test.test). Pattern allineato al BE Gepafin che in DashboardDao, CompanyDocumentDao e FaqDao raggruppa BENEFICIARY+CONFIDI con stessi diritti operativi sulla pratica. ==RAZIONALE== Sui bandi con call.confidi=true il confidi sottomette la application per conto dell'azienda e diventa user_id della application. Lato microservizio rendicontazione la pratica viene ereditata con stesso user_id, quindi il confidi e proprietario della pratica e deve poter fare upload/download/delete come il beneficiario. ==MODIFICHE== app/auth.py: - Aggiunto AuthUser.is_confidi() — controlla ROLE_CONFIDI - Aggiunto AuthUser.is_owner_role() — True per BENEFICIARY o CONFIDI - Aggiornato docstring header con ROLE_CONFIDI - Manteno is_beneficiary() per backward compat (non rimosso, non chiamato) Sostituzione is_beneficiary() -> is_owner_role() in 11 punti dove la semantica era 'proprietario pratica': - app/routers/files.py: 3 (_can_upload, _can_download, _can_delete) - app/routers/instructor.py: 2 (respond-beneficiary, ack-amendment) - app/routers/practices.py: 3 (visibilita, create, schema gating) - app/routers/custom_checks.py: 3 (declared, gate) ==COMPORTAMENTO== Per ROLE_CONFIDI vale ora la stessa regola di BENEFICIARY: - upload/download/delete: solo se practice.user_id == user.user_id AND practice.status IN ('DRAFT','AWAITING_AMENDMENT') - respond-beneficiary: solo se proprietario pratica - visualizzazione: solo proprie pratiche - creazione: solo se schema PUBLISHED Confidi su pratica di altri o su pratica non editabile -> 403 come prima. ==TEST E2E (4 step verdi)== /tmp/test_confidi_upload.py: 1. CONFIDI proprietario DRAFT upload Invoice_zapier2024.pdf -> 200 (era 403) 2. CONFIDI NON proprietario -> 403 (scoping) 3. CONFIDI proprietario ma SUBMITTED -> 403 (stato) 4. BENEFICIARY proprietario DRAFT (regressione) -> 200 --- app/auth.py | 11 ++++++++++- app/routers/custom_checks.py | 6 +++--- app/routers/files.py | 6 +++--- app/routers/instructor.py | 4 ++-- app/routers/practices.py | 6 +++--- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/app/auth.py b/app/auth.py index 1e637e3..f451fdd 100644 --- a/app/auth.py +++ b/app/auth.py @@ -3,7 +3,7 @@ JWT validation compatibile con GEPAFIN-BE. Il BE Spring emette token HS512 con payload: sub: "email:userId:hubId" userId: int - auth: "ROLE_SUPER_ADMIN" | "ROLE_BENEFICIARY" | ... + auth: "ROLE_SUPER_ADMIN" | "ROLE_BENEFICIARY" | "ROLE_CONFIDI" | ... exp: unix timestamp loginAttemptId: int """ @@ -29,6 +29,15 @@ class AuthUser: def is_beneficiary(self) -> bool: return self.role == "ROLE_BENEFICIARY" + def is_confidi(self) -> bool: + return self.role == "ROLE_CONFIDI" + + def is_owner_role(self) -> bool: + """Ruoli che possono essere proprietari di una pratica (user_id match): + BENEFICIARY (azienda diretta) o CONFIDI (delegato per conto azienda). + Pattern allineato al BE Gepafin (DashboardDao, CompanyDocumentDao).""" + return self.role in ("ROLE_BENEFICIARY", "ROLE_CONFIDI") + def get_current_user( credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme), diff --git a/app/routers/custom_checks.py b/app/routers/custom_checks.py index 4d04d1c..8b05de6 100644 --- a/app/routers/custom_checks.py +++ b/app/routers/custom_checks.py @@ -36,16 +36,16 @@ def _get_practice(db: Session, practice_id: UUID, user: AuthUser) -> RemissionPr if not p: raise HTTPException(status_code=404, detail=f"Pratica {practice_id} non trovata") # Autorizzazione base: beneficiario owner o istruttore - if user.is_beneficiary() and p.user_id != user.user_id: + if user.is_owner_role() and p.user_id != user.user_id: raise HTTPException(status_code=403, detail="Accesso negato") - if not user.is_beneficiary() and not _is_instructor(user): + if not user.is_owner_role() and not _is_instructor(user): raise HTTPException(status_code=403, detail="Ruolo non autorizzato") return p def _can_declare(user: AuthUser, practice: RemissionPractice) -> bool: """Solo beneficiario owner e solo su DRAFT | AWAITING_AMENDMENT.""" - if not user.is_beneficiary(): + if not user.is_owner_role(): return False if practice.user_id != user.user_id: return False diff --git a/app/routers/files.py b/app/routers/files.py index e90de84..3bd3842 100644 --- a/app/routers/files.py +++ b/app/routers/files.py @@ -38,7 +38,7 @@ def _can_upload(user: AuthUser, practice: RemissionPractice) -> bool: """Beneficiario proprietario in DRAFT/AWAITING_AMENDMENT oppure istruttore.""" if _is_instructor(user): return True - if user.is_beneficiary() and practice.user_id == user.user_id: + if user.is_owner_role() and practice.user_id == user.user_id: return practice.status in ("DRAFT", "AWAITING_AMENDMENT") return False @@ -46,14 +46,14 @@ def _can_upload(user: AuthUser, practice: RemissionPractice) -> bool: def _can_download(user: AuthUser, practice: RemissionPractice) -> bool: if _is_instructor(user): return True - if user.is_beneficiary() and practice.user_id == user.user_id: + if user.is_owner_role() and practice.user_id == user.user_id: return True return False def _can_delete(user: AuthUser, practice: RemissionPractice) -> bool: """Solo beneficiario su pratica modificabile. Istruttore non elimina file.""" - if user.is_beneficiary() and practice.user_id == user.user_id: + if user.is_owner_role() and practice.user_id == user.user_id: return practice.status in ("DRAFT", "AWAITING_AMENDMENT") if user.is_superadmin(): return True diff --git a/app/routers/instructor.py b/app/routers/instructor.py index fc1b1c9..22e2afd 100644 --- a/app/routers/instructor.py +++ b/app/routers/instructor.py @@ -446,7 +446,7 @@ def respond_amendment_beneficiary(practice_id: UUID, amendment_id: UUID, user: AuthUser = Depends(get_current_user)): """Beneficiario risponde al soccorso istruttorio (stato AWAITING -> RESPONSE_RECEIVED).""" p = _get_practice_or_404(db, practice_id) - if user.is_beneficiary() and p.user_id != user.user_id: + if user.is_owner_role() and p.user_id != user.user_id: raise HTTPException(status_code=403, detail="Non sei il proprietario della pratica") ar = _amendment_or_404(db, practice_id, amendment_id) @@ -620,7 +620,7 @@ async def upload_response_document(practice_id: UUID, amendment_id: UUID, """Beneficiario allega un documento come supporto alla sua risposta al soccorso. Consentito su amendment in stato AWAITING, solo dal proprietario pratica.""" p = _get_practice_or_404(db, practice_id) - if user.is_beneficiary() and p.user_id != user.user_id: + if user.is_owner_role() and p.user_id != user.user_id: raise HTTPException(status_code=403, detail="Non sei il proprietario della pratica") ar = _amendment_or_404(db, practice_id, amendment_id) diff --git a/app/routers/practices.py b/app/routers/practices.py index 6dd845f..42ed63d 100644 --- a/app/routers/practices.py +++ b/app/routers/practices.py @@ -39,7 +39,7 @@ def _get_practice_or_404(db: Session, practice_id: UUID, user: AuthUser) -> Remi raise HTTPException(status_code=404, detail=f"Pratica {practice_id} non trovata") # Solo il beneficiario owner o un superadmin può accedere - if user.is_beneficiary() and practice.user_id != user.user_id: + if user.is_owner_role() and practice.user_id != user.user_id: raise HTTPException(status_code=403, detail="Accesso negato a questa pratica") return practice @@ -438,7 +438,7 @@ def start_practice(body: PracticeStartRequest, db: Session = Depends(get_db), if app_row["status"] != "CONTRACT_SIGNED": raise HTTPException(status_code=409, detail=f"Application in stato {app_row['status']}, richiesto CONTRACT_SIGNED") - if user.is_beneficiary() and app_row["user_id"] != user.user_id: + if user.is_owner_role() and app_row["user_id"] != user.user_id: raise HTTPException(status_code=403, detail="Application non di tua proprieta") # Schema del bando @@ -446,7 +446,7 @@ def start_practice(body: PracticeStartRequest, db: Session = Depends(get_db), if not schema: raise HTTPException(status_code=409, detail="Nessuno schema di rendicontazione configurato per questo bando.") - if schema.status != "PUBLISHED" and user.is_beneficiary(): + if schema.status != "PUBLISHED" and user.is_owner_role(): raise HTTPException(status_code=409, detail="Lo schema di rendicontazione non e ancora stato pubblicato.")