Claims e policy
Inventario delle lettere autorizzative e dei claim custom in uso in TrainingHub, con il rispettivo significato e i punti di codice che li referenziano.
Cosa fa
TrainingHub usa KSet + un policy provider custom (KSet.Auth.Engines.LettersClaimsPolicyProvider, in D:\repos\dev3sd\Kset) che valuta le policy ASP.NET come richieste di lettere sul claim base associato all'utente. Questa pagina è la mappa unica di:
- quali lettere (
R,C,U, ...) sono effettivamente in uso e cosa significano; - quali claim custom esistono al di fuori del pattern auto-generato dal CRUD;
- quali combinazioni
ruolo × claimvengono seedate dal database.
Quando aggiungi una nuova lettera o un claim non-CRUD, aggiorna questa pagina in contestuale al PR.
Modello
I suffissi _X vivono in due posti diversi con ruoli diversi:
| Dove | Esempio | Cosa contiene |
|---|---|---|
| Codice / config | [Authorize(Policy = "page-edu-foo_RU")], <AuthorizeView Policy="page-edu-foo_S">, oss.menu.authPolicy = 'page-edu-foo_M' | Le lettere richieste per accedere alla risorsa. |
DB — kset.claims.slug | page-edu-foo | Solo il nome base. Mai con suffisso _X. |
DB — kset.rolesClaims.authorizations | 'RCUD', 'RS', NULL (= tutte le lettere) | Le lettere granted a un ruolo per quel claim. |
LettersClaimsPolicyProvider:
- Trasforma il claim utente in
"{claim.slug}_{rolesClaims.authorizations}"(trim_finale seauthorizationsè null). - Estrae la base (parte prima dell'ultimo
_) sia dalla policy richiesta che dal claim utente e fa il join su quella. - Se entrambi hanno
_, verifica che le lettere richieste siano sottoinsieme di quelle granted. - Se solo uno dei due contiene
_, la policy passa (clausola permissiva XOR).
Conseguenza pratica della (4): mettere il suffisso direttamente in claims.slug "funziona" per coincidenza ma rompe il modello. Vedi CLAUDE.md § Convenzioni.
Lettere in uso
| Lettera | Semantica | Origine | Esempi d'uso |
|---|---|---|---|
R | Read — accesso alla pagina/grid in lettura. | CRUD generato + pagine custom. | page-job-workers_R (WorkerProfile.razor), page-reg-companies_R (CompanyProfile.razor), page-edu-vw_trainingExpirations_R (menu Scadenze formazione). |
C | Create — inserimento di nuovi record. | CRUD generato. | page-edu-courseSessions_C, page-edu-priorTrainings_C (quick action su WorkerProfile). |
U | Update — modifica record esistente. | CRUD generato. | page-job-workers_U (quick action "Modifica anagrafica"). |
D | Delete — cancellazione record. | CRUD generato. | Tutti i CRUD generati. |
M | Menu — visibilità voce nel menu laterale. | Convention oss.menu.authPolicy. | page-edu-teachers_M, page-edu-trainingTopics_M, page-iso-certifications_M, ecc. (definite in TrainingHub.Database/Scripts/PostDeploy/02-menu.sql). |
S | Send / azione custom — gating di un'azione non-CRUD specifica della pagina. | Uso manuale. | page-teacher-letters_S (firma lettera in LetterDetail.razor), page-teacher-attendance_S (upload PDF registro in Attendance.razor), page-fin-expenses_S (override «Modifica fuori finestra» in ExpensesWorksheet.razor:19-21). |
E | Edit esteso — riapertura di task chiusi propri. | Uso manuale. | page-defa-taskItems_E (sync-rolesClaims.sql:88-89, grant CRUDMET). |
T | Time — suddivisione dei tempi sul task. | Uso manuale. | page-defa-taskItems_T (stesso grant CRUDMET). |
Note di interpretazione.
R/C/U/Dsono prodotte automaticamente dal CRUD generator: ogni pagina CRUD generata espone tutte e quattro le lettere per la propria entità.Mè il gating del link nel menu, non della pagina: separare_Mda_Rconsente di mostrare/nascondere la voce indipendentemente dall'accesso alla pagina (es. dare R ad un ruolo ma tenere il link fuori dal menu finché non ottiene anche M).Sè semantica: il policy provider non sa che "S = send". È convenzione applicativa: tutte le azioni non-CRUD passano per_S. Se serve discriminare più azioni custom sulla stessa entità, vedi Anti-pattern sotto.
Nessuna altra lettera è in uso. Le lettere sono case-sensitive nel confronto del policy provider, quindi mantenere maiuscole anche nelle nuove introduzioni.
Claim CRUD con uso non-standard delle lettere
Alcune entità CRUD reali usano le lettere in modo più granulare rispetto al semplice "pagina completa". Documentarle qui evita ambiguità nella gestione dei grant.
Slug (in kset.claims) | Entità DB | _R | _C | _D | Note |
|---|---|---|---|---|---|
page-edu-sessionDocuments | edu.sessionDocuments | Vista step Documenti + bottoni Genera | Upload file (AddPolicy del DocumentsEmbedGrid) | Elimina file | Claim auto-generata da Oster; admin riceve CRUDM via pattern page-edu-% in sync-rolesClaims.sql — nessun grant manuale necessario. |
page-fin-callDocuments | fin.callDocuments | Griglia bridge (no vista dedicata) | Upload file (AddPolicy del DocumentsEmbedGrid nello step Documenti del wizard Avviso) | Elimina file | Claim auto-generata da Oster; admin riceve CRUDM via pattern page-fin-% — nessun grant manuale necessario. |
page-fin-callAmendments | fin.callAmendments | Griglia rettifiche (/fin/callAmendments) e tab "Rettifiche" del dettaglio Avviso (Call.razor, DetailTab con Policy="page-fin-callAmendments") | Bottone "Nuova rettifica" della griglia | Cancellazione di una rettifica | Claim auto-generata da Oster; admin e training-admin ricevono CRUDM via pattern page-fin-% — identico a page-fin-calls, nessun grant manuale necessario (chi amministra gli avvisi amministra le rettifiche). Nessuna voce di menu: la M resta concessa dal pattern ma non gate nulla. |
page-fin-callAmendmentWindows | fin.callAmendmentWindows | Griglia sportelli coinvolti (/fin/callAmendmentWindows) e step 2 del wizard Rettifica (CallAmendmentFormPopup) | Aggiunta di uno sportello alla rettifica | Rimozione di uno sportello dalla rettifica | Idem sopra: CRUDM via pattern page-fin-%, stesse lettere di page-fin-calls. |
page-fin-callAmendmentDocuments | fin.callAmendmentDocuments | Griglia bridge (/fin/callAmendmentDocuments) | Upload file (AddPolicy del DocumentsEmbedGrid nello step Documenti del wizard Rettifica) | Elimina file | Idem sopra: CRUDM via pattern page-fin-%. La categoria documento fin_call_amendment_document (PostDeploy/07-fin-document-categories.sql) referenzia questa policy: il guard THROW 50004 dello script richiede che la claim esista già, quindi Oster deve aver generato le claim prima del publish. |
page-fin-membershipDocuments | fin.membershipDocuments | Griglia bridge (no vista dedicata) | Upload file (AddPolicy del DocumentsEmbedGrid nella sezione Documenti del form Adesione) | Elimina file | Claim auto-generata da Oster; admin riceve CRUDM via pattern page-fin-% — nessun grant manuale necessario. |
page-fin-accountDocuments | fin.accountDocuments | Griglia bridge (no vista dedicata) | Upload file (AddPolicy del DocumentsEmbedGrid nella sezione Documenti del form Conto) | Elimina file | Claim auto-generata da Oster; admin riceve CRUDM via pattern page-fin-% — nessun grant manuale necessario. |
page-fin-projects | fin.projects | Vista cockpit (ProjectDetail.razor) e vista/pagina fase (ProjectPhases.razor, /fin/project-phases/{id}) | — (CRUD standard) | — (CRUD standard) | _S gate il bottone che avanza lo stato ("Cambia stato" nel cockpit, "Avanza stato" in ProjectPhases.razor — stesso ChangeStatusPopup, stesso motore, stesso gate). _U gate solo "Sblocca hardBlock" e le caselle di spunta dei requisiti di tipo azione. Non ci sono più azioni "Completa fase"/"Salta fase"/"Riapri fase": la fase si muove solo attraverso il cambio di stato — vedi spec 2026-07-14-fonditalia-status-drives-phases-design.md. |
page-fin-projectDocuments | fin.projectDocuments | Non consumata per il gating della pagina fase: /fin/project-phases/{id} usa page-fin-projects_R (stesso claim del cockpit, così il link dal cockpit funziona sempre a prescindere da questo claim) | Upload documento di progetto (AddPolicy di DocumentsEmbedGrid/DocumentSlot in PhaseDocumentsPanel) | Elimina file | Claim auto-generata da Oster dalla CRUD ProjectDocument, che esiste solo perché Oster la generi (nessuna voce menu, nessuna griglia embeddata altrove); admin riceve CRUDM via pattern page-fin-% — nessun grant manuale necessario. |
page-fin-companyDocuments | fin.companyDocuments | Non consumata: vive solo sulla CRUD orfana CompanyDocument.razor, mai raggiunta dall'utente | Upload nella matrice per-azienda (PhaseEntityDocumentsMatrix dentro ProjectPhases.razor) | Elimina file | Claim auto-generata da Oster dalla CRUD CompanyDocument (idem sopra); admin riceve CRUDM via pattern page-fin-% — nessun grant manuale necessario. |
page-fin-workerDocuments | fin.workerDocuments | Non consumata: vive solo sulla CRUD orfana WorkerDocument.razor, mai raggiunta dall'utente | Upload nella matrice per-lavoratore (PhaseEntityDocumentsMatrix dentro ProjectPhases.razor) | Elimina file | Claim auto-generata da Oster dalla CRUD WorkerDocument (idem sopra); admin riceve CRUDM via pattern page-fin-% — nessun grant manuale necessario. |
page-fin-projectVariations | fin.projectVariations | Card "Variazioni" nel cockpit progetto (ProjectDetail.razor) | Bottone "Nuova variazione" in testata della card | Cancellazione di una variazione | _U copre sia il caricamento/eliminazione dei documenti del fascicolo (DocumentSlot in ProjectVariationPopup) sia la registrazione dell'esito (Registra esito): non ci sono due lettere separate per queste due azioni. Claim auto-generata da Oster dalla CRUD ProjectVariation, che esiste solo perché Oster la generi (nessuna voce menu, nessuna griglia usata: UI hand-built) — stesso meccanismo di page-fin-projectDocuments sopra. Override in sync-rolesClaims.sql: admin e training-admin ricevono RCUD (non il default CRUDM — niente M, non c'è voce di menu). |
page-inv-invoices | inv.invoices | Griglia + form fattura (Invoice.razor) | — (CRUD standard) | — (CRUD standard) | _S gate il bottone "Invia copia di cortesia" nello step Riepilogo di InvoiceFormPopup.razor (HandleSendCourtesyCopy), visibile solo quando lo stato fattura è Accepted. Override in sync-rolesClaims.sql: admin riceve CRUDMS (default CRUDM + S). |
page-fin-expenses | fin.expenses | Griglia + form spesa (Expense.razor) e foglio di rendicontazione (ExpensesWorksheet.razor, /fin/expenses-matrix) | — (CRUD standard) | — (CRUD standard) | _S gate il toggle "Modifica fuori finestra" in ExpensesWorksheet.razor: bypassa il blocco di editing di preventivo/consuntivo dettato dallo stato del progetto (WorksheetEditPolicy, enforcement in ExpensesWorksheetService.EnsureEditableAsync). Override in sync-rolesClaims.sql: admin riceve CRUDMS (default CRUDM + S). |
page-fin-calendarEntries | fin.calendarEntries | Griglia CRUD e l'intera pagina hand-built calendario ore (HoursCalendar.razor, /fin/hours-calendar); è anche la authPolicy della voce di menu Calendario ore (_M) | Bottone "Genera calendario" (lo spalmo delle ore), oltre alla creazione della singola voce | — (CRUD standard) | La _C non gate solo l'inserimento manuale: autorizza la generazione automatica che scrive N voci in blocco. Claim auto-generata da Oster; admin riceve CRUDM via pattern page-fin-%. |
page-fin-calendarBlocks | fin.calendarBlocks | Griglia CRUD e bottone "Chiusure e ferie" in HoursCalendar.razor (senza _R il bottone non compare, la pagina resta usabile) | — (CRUD standard) | — (CRUD standard) | Chi non ha _R vede il calendario ma non può ispezionare né modificare chiusure e ferie, che però continuano a limitare lo spalmo. Claim auto-generata da Oster; admin riceve CRUDM via pattern page-fin-%. |
page-edu-vw_configurationGaps | edu.vw_configurationGaps | Griglia CRUD generata (/edu/vw_configurationGaps), unica pagina del controllo configurazione: authPolicy della voce di menu e del badge nella KPI bar compliance | — (vista, nessun insert) | — (vista, nessuna eliminazione) | Claim auto-generata da Oster da sys.views (non esiste una claim page-edu-configurationCheck: KSet genera i claim di pagina solo da sys.tables/sys.views, uno slug senza tabella/vista dietro non nasce mai — vedi KSet.Database/Scripts/PostDeploy/01-claims.sql). Il pattern escluderebbe le vw_*: override in sync-rolesClaims.sql, @patternExtra → admin e training-admin ricevono RM (stesso meccanismo di page-inv-vw_billableItems sopra). |
page-fin-companyMembershipStatusHistory | fin.companyMembershipStatusHistory | Storico degli stati di adesione | — (sola consultazione) | — | Override in sync-rolesClaims.sql:85: RM invece del CRUDM del pattern page-fin-%. È uno storico: si legge, non si scrive a mano. |
page-job-workerJobHistory | job.workerJobHistory | Storico mansioni del lavoratore | — | — | Extra fuori pattern in sync-rolesClaims.sql:120: R soltanto. Lo storico è alimentato dal QueryModifier, non dall'utente. |
page-kset-users | kset.users | Non è una pagina: serve come CellPolicy sulla colonna utente della griglia task | — | — | Extra fuori pattern in sync-rolesClaims.sql:127: R. Senza, la colonna utente non è leggibile nella griglia defa.taskItems. |
Claim custom (non auto-generate da CRUD)
Le claim auto-generate seguono page-<schema>-<table> dove <schema> corrisponde a uno schema del DB (edu, job, reg, inv, iso, ...). Le claim qui sotto non corrispondono a un'entità CRUD: hanno schema o slug "virtuale" e vanno mantenute manualmente.
Slug (in kset.claims) | Schema "virtuale" | Cosa gating | Punti d'uso |
|---|---|---|---|
page-dashboard-compliance | dashboard | Cruscotto conformità aziende (no CRUD sotto). | Components/Pages/ComplianceDashboard/CompanyProspect.razor, CompaniesTrainingOverview.razor (_R). |
page-import-training-courses | import | Wizard import corsi pregressi da Excel. ⚠️ La stessa claim gating anche l'import verifica dipendenti. | Components/Pages/Import/TrainingCoursesImport.razor (_R) e EmployeeVerificationImport.razor:2 (_R). |
page-teacher-dashboard | teacher | Area docente — home. | Components/Pages/TeacherArea/Dashboard.razor (_R). |
page-teacher-calendar | teacher | Area docente — calendario impegni. | Components/Pages/TeacherArea/Calendar.razor (_R). |
page-teacher-letters | teacher | Area docente — lettere d'incarico (lettura + firma). | Letters.razor, LetterDetail.razor (_R, _S per firma). |
page-teacher-attendance | teacher | Area docente — registro presenze (lettura + chiusura + upload). | Attendance.razor (_R, _U chiusura, _S upload PDF). |
page-edu-sessionPlanner | edu (virtuale) | Non gating più nulla. Né sessionPlanner né new_session compaiono in PostDeploy/02-menu.sql; l'unico riferimento a page-edu-sessionPlanner_S è in Scripts/_archive/2026-06/menu-add-training-requests.sql:58, fuori dai meccanismi di esecuzione. Il wizard si apre dai punti d'ingresso contestuali. | — |
page-appointments-calendar | appointments | Calendario globale appuntamenti formativi (non è CRUD diretto). | Voce menu appointments_calendar (definita in PostDeploy/02-menu.sql, _R). |
Le
kset.claimssono auto-generate da Oster (non seedate a mano). I grant per ruolo (kset.rolesClaims) dei ruoli applicativi vivono nello scriptTrainingHub.Database/Scripts/oster/sync-rolesClaims.sql, registrato su Oster come maintenance script idempotente. Le voci di menu (con il loroauthPolicy) sono dichiarate inTrainingHub.Database/Scripts/PostDeploy/02-menu.sql.
Ruoli seedati e loro grant
Cinque ruoli sono dichiarati dal repo: sync-rolesClaims.sql li crea se mancanti (insert-only: label e authorizationLevel ritoccati da UI sopravvivono) e ne riallinea i grant in modo autoritativo — DELETE di ciò che non è più dichiarato, MERGE delle lettere. Niente grant manuali via UI per questi ruoli: al prossimo migrate spariscono.
I livelli (authorizationLevel/authorizationLevelPermission) alla creazione: admin 900/900; training-admin e invoicing un gradino sotto e alla pari fra loro, 800/799; consultant e teacher 0/0. admin è già presente ovunque (creato a mano): l'insert-only non lo tocca, i valori qui servono solo a un DB nuovo. Essendo insert-only, ritocchi successivi da UI reggono al migrate.
| Ruolo | Label | Come è costruito |
|---|---|---|
admin | Amministratore | Pattern page-{schema}-* → CRUDM sugli schemi appointments, dashboard, defa, edu, fin, import, inv, iso, job, reg, con esclusioni (cache, storici, motore Mulet), override (_S su progetti/spese/fatture, CRUDMET sui task) ed extra fuori pattern (Mola, Ploc, Oss documenti, viste vw_* incluse a mano). |
training-admin | Amministratore formazione | Identico ad admin meno tutto page-inv-*: stesse regole, schema inv fuori dal pattern e righe page-inv-% filtrate da override ed extra. Fonditalia (fin) resta — è formazione finanziata, non fatturazione. |
invoicing | Fatturazione | Pattern page-inv-* → CRUDM (quindi anche il gruppo di menu Configurazione fatturazione), con le stesse eccezioni di admin: fuori page-inv-invoiceStatusHistory e le vw_*, page-inv-vw_billableItems → RM, page-inv-invoices → CRUDMS. Più il blocco letture condiviso. |
consultant | Consulente | Lista curata. Gestione (RCUDM): aziende, sedi/reparti aziendali, assegnazione RSPP, mansioni, reparti, attrezzature; (RCUD, senza menu, si editano dai profili): contatti azienda, rischi aziendali, mansioni e rischi del lavoratore. Consultazione (RM): dipendenti, viste lavoratori/mansioni, scadenze formative, compliance + prospetto, stato formazione, riepilogo compliance, rischi effettivi, dettaglio formazione, sessioni, presenze; (R): calendario appuntamenti. Niente fatturazione, Fonditalia, certificazioni, opzioni, import. |
teacher | Docente | Lista curata: page-teacher-dashboard R, page-teacher-calendar R, page-teacher-letters RS (S = firma), page-teacher-attendance RUS (U = chiusura registro, S = upload PDF). |
Il blocco letture condiviso
consultant e invoicing ricevono in più un elenco di sole letture senza menu: le lookup FK e le entità di contesto (anagrafiche reg/job, argomenti e varianti, sessioni, appuntamenti, documenti Oss, viste del profilo lavoratore). Non è un di più prudenziale: le griglie generate avvolgono ogni colonna FK in <AuthorizeView Policy="page-<schema>-<tabella>_R">, quindi senza lettura sul bersaglio la colonna sparisce senza errore né traccia. Dove il ruolo ha già un grant più forte, quello vince (NOT EXISTS nello script).
Quando aggiungi una colonna FK a una griglia che questi ruoli vedono, controlla che il bersaglio sia in quell'elenco.
Anti-pattern noti
1. Suffisso _X nello slug di kset.claims
-- ❌ SBAGLIATO
INSERT INTO [kset].[claims] ([slug]) VALUES (N'page-teacher-letters_S');
-- ✅ CORRETTO
INSERT INTO [kset].[claims] ([slug]) VALUES (N'page-teacher-letters');
INSERT INTO [kset].[rolesClaims] ([roleId], [claimId], [authorizations])
VALUES (@teacherRoleId, @claimId, N'RS');
Errore "trascinato" su più sessioni. Lo slug in kset.claims deve restare senza suffisso; le lettere vanno solo in kset.rolesClaims.authorizations.
2. Suffisso semantico multi-parola (_S_StartWizard, _S_RemoveAndQueue)
Presenti in:
Components/CRUD/edu/TrainingRequest.razor:74—page-edu-trainingRequests_S_StartWizardComponents/CRUD/edu/WorkerTrainingDetail.razor:294—page-edu-workerTrainingDetails_S_RemoveAndQueue
Il policy provider prende le lettere dopo l'ultimo _: in questi casi diventa StartWizard / RemoveAndQueue, lettere maiuscole e minuscole che non sono lettere autorizzative reali. Funziona oggi solo per via della clausola XOR permissiva (utenti con claim senza _ passano comunque). Da rifattorizzare: una sola azione _S per entità è gestibile; per due azioni distinte servono o due claim differenti (page-edu-trainingRequests-startWizard come claim a sé) o due lettere differenti (ma siamo limitati al pool ASCII maiuscole).
3. Una claim per ogni lettera (anziché una claim base + lettere granted)
-- ❌ SBAGLIATO — esplode il numero di righe in claims
('page-edu-foo_R'), ('page-edu-foo_C'), ('page-edu-foo_U'), ('page-edu-foo_D')
-- ✅ CORRETTO — una sola claim, lettere in rolesClaims.authorizations
('page-edu-foo')
Come aggiungere un nuovo claim custom
- Decidi se è davvero non-CRUD (un'entità CRUD generata espone già
R/C/U/Dautomaticamente). - Le
kset.claimssono auto-generate da Oster: non seedarle a mano. Se serve un claim "virtuale" non legato a un'entità (es.page-teacher-*,page-import-*), verifica come venga prodotto da Oster. - Concedi le lettere ai ruoli gestiti dal repo aggiungendo la riga in
TrainingHub.Database/Scripts/oster/sync-rolesClaims.sql(poi ri-registralo su Oster come maintenance scriptisMaintenance=1,preSync=0, ordinato dopo lo script Oster di generazione claim).admin/training-adminprendono le claim CRUD standard dal pattern, senza edit;invoicing/consultant/teachersono liste curate. Per un ruolo non gestito dal repo: UI Role Claims Assignment. - Se il claim gating una voce di menu, dichiarala in
TrainingHub.Database/Scripts/PostDeploy/02-menu.sqlcon il relativoauthPolicy(suffisso_M/_R/_S). - Usa la policy in codice con il suffisso corretto:
[Authorize(Policy = "page-...-foo_R")],AuthorizeView,oss.menu.authPolicy. - Aggiorna la sezione Claim custom di questa pagina.
Come aggiungere una nuova lettera
- Verifica che la semantica non sia già coperta da
R/C/U/D/S/M. In particolare:_Sè il catch-all per qualsiasi azione non-CRUD; introdurre una nuova lettera ha senso solo se serve discriminare più azioni custom sulla stessa entità per ruoli diversi. - Sceglie una lettera maiuscola non in uso (lookup nella tabella Lettere in uso sopra).
- Documenta la nuova lettera prima di iniziare ad usarla.
- Concedi la lettera ai ruoli interessati via UI Role Claims Assignment o via seed.
Riferimenti codice
D:\repos\dev3sd\Kset\KSet.Auth\Engines\LettersClaimsPolicyProvider.cs— logica di match policy → claim utente.D:\repos\dev3sd\Kset\KSet.Auth\Engines\ClaimsTransformationBase.cs— costruzione del claim utente dakset.claims+kset.rolesClaims.TrainingHub.Database/Scripts/oster/sync-rolesClaims.sql— grant dichiarativikset.rolesClaimsper i cinque ruoli applicativi gestiti dal repo:admin,training-admin,invoicing,consultant,teacher(maintenance script Oster idempotente).TrainingHub.Database/Scripts/PostDeploy/02-menu.sql— definizione dichiarativa del menu (conoss.menu.authPolicy, suffissi_M/_R/_S); rigenerabile conPostDeploy/_gen/gen-menu.ps1.CLAUDE.md§ Convenzioni — promemoria sul modello policy vs claim.