La gerarchia privilege → duty → role
La sicurezza delle finance and operations apps è dichiarativa: i suoi elementi vivono nell’AOT come gli altri artefatti, si versionano nel modello e si distribuiscono nel deployable package. Il mattone di base è il privilege, che collega un entry point — un menu item, un form, una service operation, una tabella — a un livello di accesso (No access, Read, Update, Create, Correct, Delete). Il duty raggruppa i privilege necessari a completare un compito di business (“registrare le fatture fornitore”): è il livello su cui si ragiona quando si parla di segregation of duties, perché le regole di incompatibilità si dichiarano fra duty, non fra privilege. Il role è l’unica cosa che si assegna davvero alla persona, e contiene duty (e, all’occorrenza, privilege singoli).
La regola di sviluppo è la stessa del resto della piattaforma: non si modificano gli elementi standard. Se un nuovo form custom deve essere visibile a un ruolo esistente, si crea un privilege nuovo e lo si aggancia con una extension del duty o del role, esattamente come si fa con table extension e form extension. L’overlayering qui non è una strada praticabile.
Extensible Data Security: filtrare le righe
Ruoli, duty e privilege rispondono alla domanda “a quali funzioni e operazioni si accede”. Non rispondono a “quali record si vedono”: due utenti con lo stesso ruolo vedono lo stesso set di dati. È il compito dell’Extensible Data Security (XDS), che applica una query al livello dati come filtro implicito.
Una policy XDS si basa su una primary table (quella che porta il criterio, per esempio l’assegnazione utente-territorio), su una query costruita su di essa e su una o più constrained table, che sono le tabelle effettivamente filtrate tramite la relazione con la primary table. Il context stabilisce quando la policy è attiva: legata a un ruolo, a una proprietà di ruolo o impostata dall’applicazione a runtime.
Il punto d’esame è che il filtro agisce sotto il livello applicativo: vale per form, report SSRS, query e codice X++ che legge quelle tabelle, senza toccarli uno per uno. È anche il suo costo: ogni lettura eredita un join in più, quindi una policy scritta su una query poco selettiva o priva di indici adeguati diventa un problema di prestazioni globale.
Individuare il collo di bottiglia prima di intervenire
L’ottimizzazione parte dalla misura, non dall’intuizione. Prima si isola dove si perde tempo — query più costose, blocchi e lock, piani di esecuzione, numero di round trip verso il database — usando gli strumenti di monitoraggio dell’ambiente e le diagnostiche disponibili nell’IDE e nel portale; solo dopo si sceglie il rimedio. Riscrivere codice che pesa il 2% del tempo totale è la risposta sbagliata anche quando la riscrittura è tecnicamente corretta.
Sugli indici conta la selettività: un indice utile riduce molto le righe lette; uno su una colonna con pochi valori distinti spesso viene ignorato e resta solo un costo in scrittura. Il RecId è la surrogate key di ogni tabella e la base delle surrogate foreign key: gli accessi puntuali per RecId sono la lettura più economica disponibile.
Set-based, caching e batch
Un while select che aggiorna riga per riga genera un round trip per record. Le operazioni set-based (insert_recordset, update_recordset, delete_from) fanno lo stesso lavoro in una sola istruzione, ma il kernel le degrada silenziosamente a row-based quando la tabella ha logica che deve essere eseguita per riga: metodi insert/update/delete sovrascritti, database log, alert, delete action. Esistono metodi che permettono di rinunciare esplicitamente a quella logica per mantenere il comportamento set-based: sono una scelta funzionale, non un trucco di performance.
Il caching delle tabelle si governa con la proprietà di cache lookup: EntireTable per tabelle piccole e statiche, Found/FoundAndEmpty per letture puntuali, e in ogni caso la cache lavora sulle ricerche per chiave unica.
Per i carichi lunghi — import massivi, ricalcoli, elaborazioni periodiche — la risposta è il batch framework, con job schedulabili, suddivisibili in task e assegnabili a batch group, non un’esecuzione interattiva più lunga.
Trappole tipiche d’esame
- Un venditore deve vedere solo i clienti della propria area → XDS, non un nuovo ruolo: creare ruoli paralleli non filtra le righe; duplica solo il modello di sicurezza. Il filtro per record è una policy.
- Un form custom va reso accessibile a un ruolo standard → nuovo privilege aggiunto via extension: modificare il duty o il role standard è overlayering; si estende, non si sovrascrive.
- Regola di incompatibilità fra “creare un fornitore” e “pagarlo” → segregation of duties fra duty: la regola si dichiara a livello di duty, non fra privilege né fra entry point.
- Aggiornamento di decine di migliaia di righe lento →
update_recordset, verificando che resti set-based: se la tabella haupdate()sovrascritto o database log attivo, l’istruzione torna row-based e il guadagno sparisce. - Tabella di parametri letta dentro un ciclo → cache lookup, non una variabile globale:
EntireTableoFoundrisolvono il problema alla radice, ma la cache si attiva solo su lookup per chiave unica. - Import notturno che va in timeout in sessione interattiva → batch framework: la risposta non è alzare i timeout né spezzare il lavoro lato client; è schedulare il job in batch.