Skip to main content

ADR-003 — Scope dell'utente e self-service del dipendente

Stato: proposta · Proposta il: 16 agosto 2026 · Prerequisito di: Fase 1 punto 7, Fase 2

Contesto

Il modello di profilazione poggia su una regola dichiarata: un utente appartiene a una e una sola organizzazione. La regola è imposta dal database, non lasciata all'applicazione:

vincolodoveeffetto
ux_member_single_organization_per_userbackbone_auth.member(user_id)una sola membership per utente
email text NOT NULL UNIQUEbackbone_auth."user"la stessa email non esiste due volte
ux_organization_email_domains_domainorganization_email_domains(domain)un dominio appartiene a una sola organizzazione

La migrazione 081 che introduce il primo vincolo è autoverificante: se trovasse un utente con più membership solleva un'eccezione elencando i colpevoli, invece di creare l'indice e lasciare il problema in piedi.

Da questa regola discende lo scope che data-service impone: gli oggetti dell'organizzazione dell'utente, risolta dal token e mai dall'header x-organization-id, che viene confrontato con la membership e rifiutato se non combacia.

Manca però la seconda metà dello scope. Il modello dei ruoli prevede un livello più stretto — il dipendente che vede i propri dati — e lo prevede in tre posti:

  • la migrazione 089 crea il ruolo rendicontazione_employee, descritto come «Accesso self-service del dipendente»;
  • ROLE_CAPABILITIES in data-service gli assegna la capability self;
  • la migrazione 094 gli concedeva RENDICONTAZIONE_READ in GET su tutto /rendicontazione/**.

Nessuno dei tre livelli necessari esiste. requiredCapability() non restituisce mai self: ogni GET richiede read, quindi quell'account riceve comunque 403. Nessuna query di rendicontazione filtra per il dipendente dell'utente. E soprattutto rendicontazione.employees non ha alcuna colonna che colleghi una riga a un account — né user_idemail — quindi non esiste nemmeno il dato con cui sapere quale riga sia «la propria». L'identità del dipendente, nel POC, è il nome: la chiave di import è E-<hash(cognome|nome)>.

Decisione

Finché il collegamento dipendente-account non esiste, il ruolo rendicontazione_employee non concede alcun accesso.

La migrazione 095 ritira i due permessi dati dalla 094, con una verifica finale che fallisce se il ruolo conserva permessi attivi.

Questo non è un buco richiuso: un utente con quel solo ruolo riceveva 403 comunque, perché il controllo fine di data-service chiede read. È una trappola disinnescata. Il permesso di lettura generalizzato e la capability self raccontavano due cose diverse, e nel momento in cui qualcuno le avesse allineate — cosa che sarebbe sembrata una correzione di coerenza — quel dipendente avrebbe visto il costo orario dei colleghi.

Meglio un account che non funziona di uno che funziona troppo: il primo si nota il giorno stesso, il secondo può passare inosservato per mesi.

Conseguenze

Il self-service resta non disponibile, ed è ora scritto invece che implicito. Il ruolo esiste in anagrafica ma non va assegnato.

Il prerequisito è un dato, non un permesso. Serve una colonna che leghi rendicontazione.employees a un account, con due vincoli:

  1. chiave esterna composita (organization_id, user_id) verso la membership, così un dipendente non può essere collegato a un utente di un'altra organizzazione — coerente con ADR-001, dove le relazioni cross-tenant sono impossibili a livello di database e non solo sconsigliate;
  2. unicità: un account corrisponde ad al più un dipendente.

Cosa vede e cosa può fare un dipendente — deciso il 16 agosto 2026:

Vedei propri dati anagrafici, e nient'altro
Modificaniente, per ora

I progetti sono fuori perimetro: un dipendente non vede né i propri progetti né quelli altrui. Restringere è reversibile, allargare no.

Il self-service nasce in sola lettura. L'unica scrittura prevista è la propria foto, non ancora implementata.

Aggiornamento — 18 agosto 2026: la prima scrittura è arrivata

Il piano ferie (PUT /rendicontazione/me/leave/plan) è la prima scrittura concessa a self, e ha seguito alla lettera lo schema descritto più sotto: un endpoint dedicato che accetta self, risolve da sé di quale dipendente si tratta e impone i campi che il chiamante non può scegliere.

Lo scope di colonna continua a non servire, e la ragione è cambiata. Non perché non ci sia niente da scrivere, ma perché quella scrittura non tocca employees: scrive employee_monthly_leave_plan, una tabella in cui l'unico dato che la persona decide è quanti giorni mettere in un mese. employee_id e plan_month li impone il servizio. Non esiste una colonna che vada protetta da lei, quindi non esiste un elenco di campi consentiti da configurare.

Lo scope di colonna resta necessario quando si potrà modificare l'anagrafica, che è il caso per cui era stato pensato.

Le due regole che quella rotta impone — non si pianifica un mese già cominciato, non si pianificano più giorni di quanti se ne abbiano — sono al cancello e non nel browser, per lo stesso motivo per cui /forecast/rows impone origine e stato delle righe. ::: Ne consegue che lo scope di colonna — l'elenco configurabile di campi modificabili — non serve adesso: non c'è niente da scrivere su cui applicarlo. Resta descritto qui sotto perché la decisione è presa, non perché vada costruita ora.

Ne discende una cosa sola per il momento: serve lo scope di riga.

Serve uno scope di riga. Quale riga sia «la propria» richiede il collegamento employees.user_id, che non esiste.

Quando ci sarà da scrivere

Quanto segue vale dal momento in cui il self-service permetterà una modifica. Oggi non è codice da scrivere.

Serve uno scope di colonna. «Tutto tranne alcuni campi» non è una restrizione di riga: è un elenco di campi scrivibili. Finora il sistema ha solo permessi per tabella e per riga, mai per campo. L'elenco deve essere imposto dal server: nascondere due campi nell'interfaccia non impedisce a nessuno di inviarli.

Si configura cosa è scrivibile, non cosa è vietato. La differenza sembra formale e non lo è. Con un elenco di campi vietati, ogni colonna aggiunta in futuro a employees diventa scrivibile per omissione: nessuno se ne accorge finché non serve. Con un elenco di campi consentiti, una colonna nuova è di sola lettura finché qualcuno non decide il contrario. La configurazione deve poter solo restringere il perimetro, mai ampliarlo oltre ciò che il codice già ammette.

Alcuni campi non sono configurabili, in nessun caso. Costo orario e inquadramento sono il motivo per cui questa discussione esiste; organization_id e il collegamento all'account sono il confine di tenant; is_active decide se la persona esiste ancora per il sistema. Una configurazione sbagliata non deve poterli aprire.

Serve un endpoint dedicato per la modifica. Il CRUD generico su employees richiede hr, che un dipendente non ha e non deve avere — dargliela gli aprirebbe l'anagrafica di tutti. Vale lo stesso schema già usato per le righe di previsione: un endpoint che accetta self, che risolve da solo di quale dipendente si tratta, e che scrive solo i campi consentiti. Il costo orario non arriva al database perché il servizio non lo copia, non perché il client non lo manda.

Chi ha sia self sia read vede tutto. Le capability sono additive: self concede l'accesso ai propri dati, read a quelli dell'organizzazione, e possederle entrambe non restringe. Un responsabile HR che è anche dipendente usa quindi le viste complete, e le pagine self-service gli mostrano i propri dati come a chiunque.

Il rimpatrio di workforce è già avvenuto senza il filtro: self non era implementabile senza il collegamento. Il codice è ora in rendicontazione-service/modules/workforce, ed è lì che il filtro andrà.

Nome e cognome non sono campi qualunque. L'identità di un dipendente nell'import è il suo nome: la chiave è E-<hash(cognome|nome)>, e il riconoscimento in fase di apply confronta nome e cognome esatti. Un dipendente che corregge il proprio cognome cambia la propria chiave di import — al foglio Excel successivo risulterà una persona nuova, e insieme una persona cancellata.

Non è un motivo per vietare la correzione: è un motivo per non trattarla come una modifica qualunque. O passa da chi gestisce l'anagrafica, o genera un alias di riconciliazione, che è il meccanismo che già esiste per questo.

Cosa un dipendente debba poter modificare davvero resta da decidere. Le colonne di employees sono quasi tutte anagrafiche gestite da altri — inquadramento, sede, area, responsabile, tipo di contratto, date di assunzione e cessazione — oppure identità con la conseguenza appena descritta. Il perimetro reale potrebbe essere molto piccolo, ed è meglio saperlo prima di costruire l'endpoint che dopo.

Come nasce il collegamento

L'email è il punto d'incontro, user_id è il risultato. Ogni volta che un dipendente ha un'email — creato a mano o importato da Excel — si applica la stessa regola, e l'ordine in cui le cose sono successe non conta:

  1. si cerca un account con quell'indirizzo;
  2. se esiste, lo si collega e basta;
  3. se non esiste, lo si crea con il solo ruolo rendicontazione_employee, e lo si collega.

Nessuna email viene inviata. L'account nasce senza credenziale, e la persona entra quando vuole passando dal «password dimenticata» della pagina di login. È il motivo per cui non serve un pulsante «crea accesso»: non c'è niente da approvare, e importare cento dipendenti non manda cento inviti.

Collegare non significa toccare i ruoli. È il punto in cui è più facile fare danni. Gli amministratori del verticale vengono creati come account prima che esistano i dipendenti, e sono anche loro dipendenti: quando l'anagrafica verrà importata, i loro indirizzi troveranno un account già presente. Se il collegamento riassegnasse i ruoli, l'import del personale declasserebbe gli amministratori a semplici dipendenti — e se ne accorgerebbero al login successivo, senza capire perché.

Essere un dipendente è un fatto di dominio; avere dei permessi è un fatto di piattaforma. Il collegamento scrive user_id e nient'altro.

Il senso è uno solo: dal dipendente all'account. Da un account non nasce mai un dipendente. Non tutti quelli che accedono lavorano per l'organizzazione — un amministratore esterno, un consulente — e crearne l'anagrafica li farebbe comparire nei conteggi e nei costi.

Un account di un'altra organizzazione non è collegabile. La foreign key composita lo impedisce, e quello è l'esito giusto: se mario@alfa.it ha già un account in un'altra organizzazione, quella persona non può essere anche dipendente qui. Va però detto con parole comprensibili, non lasciando affiorare un errore di vincolo — e senza rivelare quale organizzazione.

La foto probabilmente non è di Rendicontazione

backbone_auth."user" ha già una colonna image, e auth-service la restituisce fra i dati dell'utente. Una foto profilo è un attributo dell'account, non del rapporto di lavoro: la stessa persona che usasse due verticali avrebbe due foto se ciascuno tenesse la propria.

Se la foto vive sull'account, la sua modifica è self-service di piattaforma — e Rendicontazione non ha bisogno né di un endpoint di scrittura né dello scope di colonna. Si limiterebbe a mostrare ciò che GET /rendicontazione/users/:id già restituisce, aggiungendo image ai tre campi che espone oggi.

Va verificato con chi governa auth-service prima di costruire alternative: una seconda foto nel verticale sarebbe uno stato duplicato che può divergere, lo stesso motivo per cui il tenant locale è stato scartato.

Alternative considerate

Lasciare il permesso e implementare il filtro subito. Scartata per sequenza, non per merito: senza la colonna di collegamento il filtro non è scrivibile, e tenere il permesso nel frattempo significa lasciare la trappola armata per la durata del lavoro.

Collegare per email. Non praticabile oggi: employees non ha una colonna email. Anche con quella, l'uguaglianza fra email dell'account e email del dipendente è una convenzione, non un vincolo — due persone possono cambiare indirizzo in momenti diversi. Il collegamento esplicito è verificabile, quello per corrispondenza va ricontrollato ogni volta.

Approvazione

CampoValore
Approvata dada compilare
Datada compilare
Note o riserveIl perimetro del self-service è stato deciso il 16 agosto 2026 (vedi Conseguenze). Resta aperto cosa un dipendente vede dei progetti a cui è assegnato oltre alla propria assegnazione.

Per approvare: compilare i campi e portare Stato ad accettata in cima alla pagina.