Manutenzione del canale di autorizzazione
manutenzione-canale.RmdQuesta guida è per chi mantiene il canale: oggi noi, domani chi verrà dopo. Risponde a «da dove arriva questo valore e che cosa si rompe se lo cambio», che è una domanda diversa da quella di chi compila una richiesta — per quella ci sono le work instruction nella cartella condivisa.
Questa guida nomina i ruoli, non i valori. «L’istanza che ospita il registro», «la macchina da cui gira il job», «il segreto del registro in Key Vault». Il repository è pubblico, quindi ogni volta che servirebbe un valore concreto qui si trova il nome del parametro e il rimando al foglio dei parametri di esercizio, che vive nella cartella condivisa. Dei segreti si cita il nome sotto cui stanno, mai il contenuto: il nome è un indirizzo, non una credenziale.
1. Che cos’è il canale
Sette pezzi, ciascuno con un mestiere. Nessuno dei sette sta per «il canale» da solo, e buona parte dei guasti che si vedono in esercizio sono guasti di giunzione fra due di essi.
Il registro delle richieste. Un progetto REDCap su un’istanza dedicata, dove i referenti scrivono chi deve avere che cosa. Il registro dichiara delle richieste; non descrive la realtà. Una coppia che esiste su un’istanza e non compare nel registro non significa niente — in particolare non significa «da revocare». È anche la coda di lavoro: non ce n’è una seconda, e il §4 dice perché.
Il modulo REDCap. Codice PHP installato sulle
istanze servite. Espone tre operazioni — leggere lo stato di una coppia,
asserire dei diritti, revocarli — dietro un segreto condiviso e una
lista di indirizzi ammessi. È l’unico pezzo che tocca
\UserRights, e l’unico che calcola le due impronte: quella
della superficie di REDCap e quella della propria lista di indirizzi.
Vive in inst/redcap-module/ dentro questo
repository, e build_redcap_module() ne costruisce
l’archivio prendendo il numero di versione da DESCRIPTION:
le cartelle numerate sulle istanze sono deployment, non sorgenti.
Il client R. Questo pacchetto. Parla al modulo, risolve le identità, valida le richieste, calcola il confronto fra desiderato e reale, decide che cosa dichiarare al modulo quando chiede di scrivere, compone le notifiche e scrive gli esiti.
Il perimetro. Una macchina virtuale dedicata in Azure, con identità gestita, da cui partono due lavorazioni separate — l’osservatore e il giro del canale. Sul disco della macchina non c’è nessun segreto: il token si chiede al servizio di metadati dell’istanza, che risponde solo a chi gira su quella macchina, e con quel token si legge il Key Vault. L’indirizzo pubblico della macchina è statico per necessità e non per comodità — è l’unica voce ammessa nelle liste di indirizzi delle istanze, e cambiarlo le chiude tutte insieme.
L’anagrafica. Microsoft Entra, letta via Graph con l’identità gestita. Serve a due cose: stabilire chi sia la persona nominata in una richiesta, e crearle un’utenza quando non ne ha. I permessi applicativi sono due e nessun altro — leggere gli utenti e crearne — e la scelta è deliberata: nessuno dei due consente di modificare, disabilitare o cancellare un’utenza che esiste, né di toccarne le credenziali. Chi un giorno aggiungesse un terzo permesso sta allargando quel confine, e questa frase è ciò che glielo dice.
Il servizio di posta. Un servizio esterno a cui il giro consegna i messaggi. Il pacchetto non sa da dove esce la posta: riceve una chiusura a cui chiedere di mandare, e chi la costruisce è la lavorazione, che legge la chiave dal Key Vault e non la lascia uscire dalla funzione che la usa. Il servizio risponde «presa in carico», non «consegnata»: un rimbalzo avviene dopo, in modo asincrono, e non torna dentro il giro.
La telemetria e gli allarmi. Ogni esecuzione di entrambe le lavorazioni lascia un record in un workspace di Log Analytics, attraverso un punto di raccolta e una regola di raccolta. Le due lavorazioni non condividono la tabella, quindi non condividono nessuna regola: una regola del canale non può leggere un record dell’osservatore, e viceversa. Le regole attive sono undici — sei dell’osservatore, cinque del canale.
referenti chi mantiene
| |
v v
+--------------+ +----------------------+
| registro | legge le richieste | giro del canale |
| richieste |<---------------------| (client R, |
| (REDCap) | scrive identita' | ogni quattro ore) |
+--------------+ e i soli esiti +---+--------------+---+
| |
stato / asserisci / | | manda PRIMA
revoca | | di scrivere
v v
+-------------------------+ +--------------+ +--------------+
| perimetro | | modulo | | servizio |
| - identita' gestita |----->| REDCap sulle | | di posta |
| - segreti in Key Vault | | istanze | +--------------+
| - due lavorazioni: | | servite |
| osservatore (giorno) | +------+-------+
| canale (4 ore) | |
+----+---------------+----+ | due impronte:
| | | superficie, indirizzi
| | chi e' la |
| | persona, e |
| | creala se manca |
| v |
| +--------------+ |
| | anagrafica | |
| | (Entra/Graph)| |
| +--------------+ |
| un record per lavorazione |
v |
+-----------------------------+ |
| telemetria: due tabelle |<--------+
| + undici regole d'allarme |
+-----------------------------+
La macchina del perimetro ospita due lavorazioni, non
una, ed è il punto che il diagramma dice per primo.
L’osservatore gira ogni giorno, legge lo stato di ogni
istanza che ha il modulo e non confronta niente — non legge il registro
e non chiama provisioning_diff(), e una guardia in
tests/testthat/test-runner-guardie.R lo sorveglia.
Il giro del canale è un job separato, apposta:
osservare e agire hanno cadenze diverse e conseguenze diverse, e un
guasto nell’uno non deve fermare l’altro. Le ragioni della divisione, e
perché solo l’osservatore doveva restare senza confronto, stanno nel
§2.
La cadenza di quattro ore è una decisione, non un caso. Dice: chi compila una richiesta sa entro quattro ore se è formalmente corretta e, quando la scrittura è accesa, se è stata evasa. Non sono due tempi — il giro valida e applica nella stessa passata.
L’ordine dentro un giro, che è la parte da avere in testa. Il giro (1) legge il dizionario del registro e si ferma se è derivato da quello impacchettato; (2) legge le richieste; (3) risolve l’identità di ogni riga contro l’anagrafica, e crea l’utenza dove manca; (4) scrive identità e username nel registro, per una porta che non prende altro; (5) chiede a ogni istanza il cancello sull’ambito e confronta desiderato e reale per le sole coppie di cui qualcuno ha chiesto; (6) applica; (7) rilegge; (8) manda la posta; (9) scrive gli esiti. Le due scritture nel registro sono due porte distinte e restano tali: la prima porta i verbali del verdetto, la seconda l’esito.
«Manda prima, scrivi dopo» è un contratto, non un ordine di comodo. Una riga entra nel registro solo se la sua notizia è partita: se l’invio fallisce la riga resta come il registro la porta, e il giro seguente la ritrova cambiata e riprova. La coda è il registro stesso — nessun secondo stato, niente da tenere allineato. Il costo accettato in cambio è che una notifica possa arrivare due volte, mai che si perda.
La posta è spenta di default, e la ragione non è intuitiva: con «manda prima, scrivi dopo» una posta semplicemente spenta bloccherebbe ogni scrittura nel registro. Quindi senza la variabile che la accende il giro fa esattamente quello che faceva prima che la posta esistesse — calcola, scrive, non manda — così installare il pacchetto non fa partire nessun messaggio al timer successivo.
Che cosa garantisce una simulazione, e che cosa no.
Un giro simulato non scrive né sulle istanze né in ambito, ma
riporta l’esito nel registro: è simulated,
con applied_as che dice che cosa succederebbe. Fino alla
versione 0.10.0 del modulo quel «che cosa succederebbe» era l’eco della
richiesta e non un piano verificato: il modulo risolveva il nome di un
ruolo e quello di un gruppo solo dentro il ramo che scrive, quindi una
richiesta che nominava un ruolo inesistente usciva creato
in simulazione e sarebbe stata rifiutata da una scrittura vera. Dalla
versione successiva le due risoluzioni girano su ogni giro — sono
letture — e una simulazione rifiuta ciò che rifiuterebbe una scrittura.
Resta fuori dalla garanzia l’esistenza dell’utente
sull’istanza: nessun punto del modulo verifica che uno username
esista lì, e REDCap accetta diritti per un login che non c’è, ed è così
che nascono le righe orfane. Su un’istanza che non abbia ancora la
versione nuova, un simulated va letto come «nessun ostacolo
di forma», non come «funzionerà».
2. I due cancelli, e che cosa fa scattare ciascuno
Il canale non decide di scrivere in un posto solo. Due cancelli indipendenti stanno fra una richiesta e una scrittura, e rispondono a due domande diverse.
Il soffitto di major
Il primo cancello confronta le versioni, ed è l’unico punto del codice che lo fa. La finestra è espressa in major interi, non in versioni complete: due versioni dentro lo stesso major non hanno bisogno di essere ordinate, il che toglie di mezzo la trappola dell’ordinamento lessicografico.
Tre regimi:
| Regime | Quando | Effetto |
|---|---|---|
| sotto il pavimento | major minore del minimo | il canale rifiuta ogni operazione |
| collaudata | dentro la finestra | operazione ordinaria |
| sopra il soffitto | major maggiore del massimo | la scrittura è forzata a simulazione |
Il major è una dichiarazione di politica, non una garanzia di compatibilità. Dice quali versioni scegliamo di sostenere e su quali proviamo, e questo è tutto ciò che dice. La compatibilità la controlla l’altro cancello, e la misura sul parco lo mostra: la superficie da cui il canale dipende cambia dentro un major e resta identica attraverso tre major consecutivi.
La stretta di mano sull’impronta
Il secondo cancello confronta le superfici. Il modulo calcola l’impronta della superficie che il canale usa — le firme di sei metodi e la mappa dei privilegi — e la riporta a ogni risposta. Il client dichiara, quando chiede di scrivere, l’elenco delle impronte contro cui è stato collaudato. Il modulo concede la scrittura solo se la propria impronta è in quell’elenco.
check_fingerprint() declassa soltanto:
può portare un’istanza da collaudata a non collaudata, mai il contrario.
La direzione unica è la proprietà che rende il meccanismo sicuro da
estendere.
Un’impronta ignota non è un guasto: è una versione che aspetta di essere collaudata. Il declassamento è il modo in cui il sistema chiede di essere ricollaudato invece di scrivere alla cieca.
Anche una risposta senza impronta, o con un’impronta di forma sbagliata, declassa. La forma si controlla prima del confronto e mai dopo, perché un elemento JSON che arriva come lista di uno invece che come scalare verrebbe altrimenti confrontato — e potrebbe combaciare — dove sta un valore singolo.
Misurate e certificate non sono la stessa cosa
Il registro delle superfici, un file CSV impacchettato, porta due colonne che si somigliano e non vanno confuse.
-
tested_fingerprints()restituisce le impronte misurate: qualcuno ha letto lo stato di quell’istanza e ha registrato che cosa ha visto. -
certified_fingerprints()restituisce le sole impronte con una data di conformità: su quella superficie una run di conformità ha scritto per davvero, riletto, e visto che il risultato era quello asserito.
Il chiamante ordinario dichiara le certificate. La run di conformità dichiara le misurate, e deve: la superficie che sta per certificare non ha una data per definizione, e pretenderla ricreerebbe la trappola d’ordine — il cancello forzerebbe la simulazione, la run non potrebbe scrivere, e la data non potrebbe essere guadagnata mai. Una riga aggiunta da una semplice lettura di stato misura una superficie, non certifica che cosa succede a scriverci.
Due punti ciechi, dichiarati
L’impronta misura dichiarazioni, non comportamenti. Un cambiamento nel corpo di uno dei sei metodi, a firma invariata, resta invisibile. È per costruzione, non per difetto: la conferma che il lettore legge non allarga ciò che guarda.
L’impronta ha un ingresso che non è la versione di REDCap. La mappa dei privilegi può guadagnare un campo in base a un’impostazione dell’istanza, fuori dal blocco condizionato dal progetto. Se quell’impostazione venisse accesa, l’impronta cambierebbe senza alcun aggiornamento di REDCap: le scritture si degraderebbero a simulazione e si andrebbe a cercare un cambio di versione che non c’è stato. È un rischio di diagnosi, non di sicurezza, ed è scritto qui perché costa mezza giornata a chi non lo sa.
Il confronto fra desiderato e reale: spento nell’osservatore, acceso nel giro
provisioning_diff() esiste, è collaudato, ed è la parte
del canale che decide che cosa succede. L’osservatore non lo
chiama, e non deve.
La ragione è di ordine, non di qualità. Il registro dichiara il
desiderato, e l’osservatore legge lo stato di un’istanza per
intero — nessuno gli chiede coppie specifiche, è la sola
passata che esiste finché non c’è un registro da cui ricavarne una
stretta (dev/runner-osservatore.R lo dice con queste
parole). Un confronto fatto lì, fra il desiderato dichiarato dal
registro e quel reale letto per intero, classificherebbe ogni
coppia esistente come non più voluta, ogni volta che il
registro non nomina ancora quella coppia — e all’inizio della migrazione
del parco non ne nomina quasi nessuna. Non è la chiamata che scrive a
essere pericolosa: è il confronto che la alimenterebbe. Per questo
l’osservatore non lo calcola affatto, invece di calcolarlo e non usarlo
— e un controllo in tests/testthat/test-runner-guardie.R
sorveglia che quel job resti così.
Il giro del canale lo chiama, e può fin da subito. La differenza non è che il registro sia più popolato oggi di quanto lo sarebbe stato ieri: è che il giro non legge mai lo stato di un’istanza per intero. Chiede al modulo le sole coppie che il registro nomina — quelle desiderate e quelle da revocare, più quelle su cui il cancello sull’ambito deve interrogare — e confronta il reale con il desiderato solo su quelle. Un registro vuoto o appena iniziato non produce righe in più da confrontare: produce meno domande da fare, e zero righe è un risultato quieto, non un falso «tutto revocato». Non c’è quindi un interruttore da aspettare: la sicurezza non viene dal registro essere pieno, viene dal confronto non guardare mai più di quanto qualcuno ha chiesto — una proprietà del codice, non una soglia di avanzamento della migrazione.
Questo non riguarda la scrittura, che resta un interruttore separato e distinto dal confronto: il giro simula di default, e scrive solo quando la scrittura è richiesta esplicitamente (§1). Il confronto sempre acceso dice solo che cosa cambierebbe; se scriverlo per davvero è una decisione ulteriore, presa altrove.
3. Il protocollo di riqualificazione
Un aggiornamento di REDCap che cambia la superficie è procedura ordinaria, non caso eccezionale. Da qui in avanti il parco si aggiorna, e ogni aggiornamento che tocca la superficie chiede questi quattro passi, in quest’ordine.
- Leggere l’impronta corrente dall’istanza, con una lettura di stato.
- Aggiungere la riga al registro delle superfici, con la data di oggi e la conformità vuota. Le righe vecchie non si toccano: certificano le superfici che le istanze non ancora aggiornate espongono ancora.
- Eseguire la conformità, che può scrivere perché dichiara le impronte misurate e non quelle certificate.
- La run scrive lei la data. Non si digita a mano: un innesco che dipende da qualcuno che si ricorda di scrivere una data è un promemoria, ed è proprio la cosa che il meccanismo esiste per sostituire.
Fra il passo 1 e il passo 4 il chiamante ordinario continua a leggere, e le sue scritture sono degradate a simulazione. È il comportamento voluto.
dev/riqualifica-superficie.R esegue i quattro passi
nell’ordine, e ha un modo --diagnosi che dice solo dove
siamo senza scrivere niente. Esiste come script invece che come
procedura da rifare a mano perché il momento in cui serve è subito dopo
una finestra di manutenzione, che è il momento peggiore per improvvisare
quattro passi in ordine.
Il vincolo d’ordine fra i due cancelli. Se l’istanza è sopra il soffitto di major, la scrittura è forzata a simulazione e i cinque casi della conformità escono rossi per una ragione che non è una difformità. Va alzato prima il soffitto nel modulo e ridistribuito il modulo, e solo dopo rieseguita la conformità. Mai il contrario. Per un aggiornamento dentro lo stesso major questo ramo non scatta, ed è il caso ordinario.
Due precondizioni che lo script non può verificare da solo, e che vanno controllate prima:
- l’account di prova non deve avere diritti nel progetto di conformità. Il passo di revoca non ripristina niente: toglie i diritti, e questo coincide con lo stato iniziale solo se quello stato era vuoto;
- il progetto di conformità deve già definire i ruoli e il gruppo di accesso che i casi nominano. Il canale asserisce le appartenenze e non le crea, e un ruolo mancante torna come errore di dato su un caso — che somiglia a una difformità e non lo è.
Il progetto di conformità dev’essere un progetto destinato a durare. Il primo non lo era: nato da uno spike, marcato come cancellato quando lo spike è stato smontato, avrebbe continuato a funzionare per tutto il conto alla rovescia di REDCap e poi sarebbe stato ripulito. Un progetto di conformità marcato per la cancellazione fallisce nel modo peggiore: non adesso, ma alla prima run dopo un cambio di versione — cioè la run il cui fallimento verrebbe letto come «la versione nuova rompe il canale».
4. Il registro delle richieste
Lo schema è un file, non del codice
Il dizionario del registro vive come CSV impacchettato, e REDCap lo importa com’è: un artefatto, due consumatori. I nomi dei campi del registro sono anche i nomi dei campi della richiesta, quindi fra i due non c’è nessuna mappa e nessuna chiave può cadere in un valore di riserva senza che qualcuno se ne accorga. È l’intera ragione per cui quei nomi sono stati scelti così.
Chi scrive che cosa
Il referente compila la richiesta. I campi d’esito li scrive la
lavorazione, e sul form portano @READONLY: la marca governa
il form, non l’API, quindi la lavorazione continua a
scriverli mentre chi compila non può. Senza quella marca un referente
potrebbe digitare «applicata» nel campo d’esito, e il registro
porterebbe un successo che nessuno ha prodotto.
Il giro scrive da tre porte, e restano separate
La prima porta deposita identità e username appena la risoluzione è fatta, e non prende altro. La seconda deposita l’esito, e avviene dopo l’invio della notifica. In mezzo c’è la posta. La terza deposita il sigillo, per ultima.
Il sigillo ha una porta sua e non è una colonna dell’esito, e la
ragione è che il corpo dell’esito si scrive a ogni esito: una colonna
del sigillo lì dentro porterebbe un valore ogni volta, e il valore
onesto quando non si è applicato niente è vuoto. Un
data_error su una riga già applicata azzererebbe allora il
sigillo, togliendo la protezione proprio alle righe che sono le sole ad
averla.
La conseguenza da conoscere quando si tocca il testo dei messaggi: il frame da cui il giro compone la notifica è il registro come è stato letto, e resta volutamente immobile — è il metro contro cui si misura che cosa è cambiato. Quindi ciò che il messaggio deve dire e che quel frame ancora non porta va passato esplicitamente a chi compone. Allargare il frame dell’esito sarebbe la scorciatoia sbagliata: quel frame è anche il corpo dell’import, e allargarlo farebbe passare l’identità per la porta dell’esito.
Una richiesta è una persona su un progetto. Una coppia che compare due volte è un errore su entrambe le righe, non su una: una delle due è l’errore e non c’è modo di sapere quale. Indovinare quale vince è ciò che fa un libro mastro; un registro si rifiuta.
Una riga cambiata dopo essere stata applicata
REDCap non ha una proprietà per riga: i diritti sono per strumento, e la sola cosa che partiziona le righe sono i gruppi di accesso, che qui non ci sono per scelta — nasconderebbero le righe altrui e renderebbero i doppioni incomprensibili. Chiunque abbia il registro può quindi modificare la riga di un altro.
Il giro scrive accanto a ogni riga applicata il
sigillo di ciò che ha applicato: dodici caratteri sui
campi di intento, e su nessun altro. Un sigillo che al giro dopo non
corrisponde significa «modificata dopo l’applicazione»: la riga prende
l’esito held, si ferma dove si ferma una revocata — prima
che il piano si formi, quindi nessuna istanza viene
interrogata — e la notizia va a requested_by.
Tre cose da sapere quando si tocca questo meccanismo:
- il sigillo copre ciò che è stato chiesto, non ciò che è successo. Se si muovesse quando il giro scrive l’esito, ogni riga applicata sembrerebbe modificata un giro dopo: il meccanismo accuserebbe se stesso, su ogni riga, per sempre;
-
il sigillo si calcola sulla riga che il giro sta per
lasciare, non su quella che ha letto. Il giro risolve
usernamee lo riscrive nella stessa passata, eusernamesta dentro al sigillo perché cambiarlo cambia a chi si dà l’accesso. Sigillare la riga come letta sarebbe lo stesso difetto di sopra, per mano del giro invece che dell’esito; -
il campo di approvazione sta fuori dal sigillo pur non
essendo
@READONLY. Porta il sigillo che approva, quindi scriverlo non può spostare ciò che confronta. È il primo campo per cui «scritto dal canale» e «fuori dal sigillo» smettono di essere lo stesso insieme, ed è per questo che sono due liste.
L’approvazione vale a due condizioni: porta il sigillo di
quella versione della riga, e viene da qualcun
altro. La seconda si verifica leggendo il registro degli eventi
di REDCap, che dice chi era autenticato e non che cosa
qualcuno ha scritto in un campo — la stessa ragione per cui
requested_by non basta da solo il giorno in cui si concede
il Data Import Tool. Un registro degli eventi illeggibile lascia la riga
ferma: «non ho potuto chiedere chi è stato» non è «l’ha approvata un
altro».
La finestra del log si chiede nell’ora dell’istanza, non in UTC: REDCap timbra col proprio orologio, e il giro delle 21:03 UTC compare alle 23:03. Lo scarto non è una costante, perché cambia con l’ora legale — per questo la finestra è larga un giorno e la conversione è una comodità, non un cardine.
Verificare che il progetto sia ancora quello che il canale si aspetta
compare_dictionary() rilegge il dizionario dal progetto
vivo e lo confronta con quello impacchettato. I due nascono identici; la
domanda è se lo siano ancora dopo un import che ha perso un campo, una
modifica a mano sul form, o un tipo allentato per far passare una
validazione.
Sette codici, e nessuno è cosmetico:
| Codice | Che cosa è successo | Perché conta |
|---|---|---|
| campo assente | un campo del dizionario non c’è più | la richiesta perde quel campo senza dirlo |
| campo in più | c’è un campo che il dizionario non prevede | non rompe niente, ed è la prova diretta che qualcuno ha modificato il form a mano |
| tipo diverso | il tipo del campo è cambiato | uno stato di richiesta diventato testo libero trasforma un refuso in una richiesta di concedere ciò che qualcuno ha chiesto di revocare |
| scelte diverse | stesso tipo, valori ammessi diversi | il valore che revoca può non esserci più |
| readonly caduto | la marca di sola lettura non c’è più | chi compila può scrivere un esito che nessuno ha prodotto |
| colonna assente | manca una colonna del dizionario stesso | il dizionario è illeggibile, e senza questo codice risulterebbe conforme invece che illeggibile |
| scelte non confrontate | l’elenco delle istanze non è stato fornito | le scelte di quel campo sono la flotta, non lo schema: senza l’elenco se ne può giudicare solo la forma, e un confronto che non ha potuto guardare non deve restituire un verde |
Un import riuscito non basta, ed è il punto della sezione. Nessuno di quei difetti solleva da nessun’altra parte: la lettura di una riga tratta una colonna assente come «non impostato» invece che come errore, e il canale ignora i campi che non conosce. Sono entrambi comportamenti corretti, e sono anche la ragione per cui la deriva qui è silenziosa. Il confronto è l’unica cosa che guarda.
Il codice sulla colonna assente merita una riga in più: senza di lui un dizionario a cui manca una colonna verrebbe letto come conforme, perché non ci sarebbe niente da confrontare e «nessuna differenza» è la risposta che si dà quando non si è guardato niente.
Il settimo dice una cosa diversa dagli altri sei: non riporta una deriva, riporta un limite del giudizio. Le scelte di quel campo sono l’elenco delle istanze, cioè un dato che cambia quando cambia la flotta e non quando cambia il contratto, e che per questo non vive nel pacchetto — il pacchetto ne porta un modello con quella cella vuota, e chi confronta fornisce l’elenco. Chi lo fornisce riceve il confronto per intero; chi non lo fornisce riceve un giudizio parziale che si dichiara tale, invece di un verde che non ha guardato.
Il modello, di suo, non è importabile: REDCap rifiuta un campo a scelta singola che non ha scelte. È voluto, ed è ciò che impedisce di importarlo per sbaglio al posto del dizionario vero.
5. Gli allarmi
Undici regole attive, tutte con risoluzione automatica, divise in due famiglie che non si parlano: sei dell’osservatore e cinque del canale. La divisione non è organizzativa ma tecnica — le due lavorazioni scrivono in due tabelle diverse, e una regola non può leggere la tabella dell’altra. Chi cerca il perché di una regola mancante deve guardare prima di tutto quale tabella potrebbe leggerla.
La gravità dice la famiglia del problema, non chi lo ha trovato:
- gravità 1 — «non possiamo più vedere» o «non sta lavorando»: la lavorazione non ha lasciato nessun record; il record c’è ma non porta le colonne che deve; l’ultimo giro si è fermato prima di agire; un’utenza è nata e la sua credenziale non è arrivata a nessuno;
- gravità 2 — «abbiamo trovato un problema»: una o più istanze non si sono lasciate leggere; un’istanza non è in stato collaudato; la superficie di un’istanza è cambiata; le liste di indirizzi sono cambiate o divergono; gli esiti sono stati calcolati e non scritti; una notifica non è stata recapitata.
I nomi esatti, le gravità e le finestre stanno nel foglio dei parametri.
Le finestre sono diverse, e devono esserlo
L’osservatore gira una volta al giorno e le sue regole guardano indietro due giorni. Il canale gira ogni quattro ore, e le sue guardano indietro cinque, dentro una finestra di sei: la finestra deve contenere il periodo che la query interroga, o la regola scatta sempre.
Una regola vede solo ciò che la sua lavorazione interroga
Il canale interroga solo le istanze che hanno righe nel registro, quindi il suo contatore delle istanze irraggiungibili conta quelle, non la flotta servita. Con un registro che nomina una sola istanza, la regola tace sulle altre anche se sono spente — ed è corretto così: un’istanza che nessuno ha chiesto non è un guasto. Chi vuole sapere se la flotta è viva guarda l’osservatore, che le legge tutte.
Questa asimmetria è costata un guasto vero, ed è la ragione per cui la quinta regola del canale esiste: cinque giri di fila hanno perso l’unica istanza con righe nel registro senza che nessuna delle quattro regole di allora si accorgesse di niente. Non ne avevano motivo — «non ho raggiunto l’istanza» non è un errore del giro, è il suo esito, e finiva in un campo che nessuna regola guardava.
I banchi spenti non fanno scattare la regola sulle irraggiungibili
I banchi sono istanze di prova, spente di norma e
accese al bisogno. Stanno fra le istanze col modulo dell’inventario,
perché quando sono accese vanno lette, ma la loro voce porta
"banco": true. Contati con le altre, terrebbero rossa ogni
giorno la regola dell’osservatore sulle istanze irraggiungibili, e una
regola sempre rossa non la guarda più nessuno.
Per questo il record dell’osservatore porta due campi accanto a
irraggiungibili, che resta com’era:
irraggiungibili_produzione, che non conta i banchi, e
irraggiungibili_nomi, che elenca tutte le irraggiungibili,
banchi compresi. La regola guarda
irraggiungibili_produzione, quindi un banco
marcato non la fa scattare. Un campo banco assente, o con
un valore che non sia esattamente true, vale produzione:
un’istanza esce dall’allarme solo se qualcuno l’ha marcata. Un banco
spento resta invece un buco di copertura, e
flotta_a_una_major resta falso: potrebbe stare su un’altra
major.
La garanzia vale per quella regola sola, e solo finché almeno
un’istanza col modulo risponde. Se le istanze col modulo sono
tutte banchi e sono tutte spente, l’osservatore non legge niente:
letture_riuscite è zero e tutti_collaudati è
falso, perché un giro che non ha visto nessun cancello non ne ha visto
uno buono. Suonano allora la regola sull’assenza, a gravità 1, e quella
sul cancello, a gravità 2, e restano rosse finché i banchi restano
spenti. Marcare i banchi non le spegne, e non deve: in quel caso
l’osservatore non vede nessuna istanza, che è ciò che la gravità 1 dice.
Misurato il 2026-08-20, con i soli banchi agganciati e tutti spenti: il
cancello è scattato mezz’ora dopo il giro, l’assenza due ore dopo. Il
rosso fisso sparisce quando fra le istanze col modulo c’è almeno
un’istanza di produzione, e risponde.
Il rilascio segue l’ordine della sezione sulla regola di raccolta,
più sotto: prima le due colonne nella tabella
dell’osservatore e nella sua regola di raccolta, perché le colonne che
la regola non conosce vengono scartate in silenzio; poi
il pacchetto e la copia della lavorazione in /opt, in
quest’ordine, perché la lavorazione nuova passa un argomento che il
pacchetto vecchio non conosce; per ultime, nella stessa
finestra e dopo aver visto arrivare un record che porta
irraggiungibili_produzione con un valore, la regola
d’allarme e quella sul record incompleto. Aggiornata prima, la regola
d’allarme leggerebbe una colonna vuota, e in KQL un confronto su un
valore nullo è falso e non un errore: tacerebbe anche con una produzione
spenta.
La regola sul record incompleto elenca per nome le colonne che
pretende, e irraggiungibili_produzione va aggiunta
all’elenco. Finché la regola d’allarme guardava
irraggiungibili, un campo che la lavorazione scrive da
sempre, non serviva; spostata sulla colonna nuova, guarda un campo che
nessuna regola sorveglia. Se la colonna tornasse vuota, per un ritorno
indietro di pacchetto e lavorazione o per una regola di raccolta
riapplicata dalla definizione di prima, il record resterebbe accettato,
irraggiungibili continuerebbe a essere scritto, e
un’istanza di produzione spenta non farebbe scattare niente. È il passo
che la regola sul record incompleto del canale ha fatto con le colonne
della posta, col rilascio della 0.12.0.
La forma della query, e perché è quella
Ogni regola prende l’ultima osservazione e la
giudica: top 1 by TimeGenerated desc prima
dei filtri, non un filtro su una finestra temporale.
La differenza non è di stile. Una query nella forma «è successo qualcosa di brutto nelle ultime ventiquattr’ore?» resta vera per ventiquattr’ore, perché i record sono immutabili: un guasto riparato in cinque minuti tiene la regola rossa per un giorno intero. La forma giusta è «l’ultima osservazione com’è andata?», che torna verde appena atterra una run buona.
Questa è anche la ragione per cui una regola sola con quattro rami in
or è stata ritirata: spezzarla in quattro senza rimediare
prima la finestra avrebbe fatto ereditare a tutte e quattro il rosso
appiccicoso, moltiplicando le notifiche invece di dividerle.
La regola di raccolta impara le colonne prima che il pacchetto le emetta
Vale ogni volta che il record del giro guadagna un campo, e non dà errore se lo si sbaglia — dà silenzio. L’API d’ingestione scarta senza dirlo ogni colonna che la regola di raccolta non conosce. Rilasciare il pacchetto prima e aggiornare la regola dopo non produce un errore: produce colonne vuote, e quindi allarmi che sorvegliano un campo mai scritto — regole che non possono scattare, installate nella convinzione che stiano guardando qualcosa.
Quindi l’ordine è: prima la regola di raccolta, poi il pacchetto, poi gli allarmi nuovi. E una regola d’allarme che verifica quali colonne il record porta va aggiornata nella stessa finestra del rilascio, o scatterebbe su un record perfettamente sano.
Per il dizionario del progetto il vincolo d’ordine è lo stesso, ma guardare i soli campi non basta a decidere se serva l’atto unico, e la distinzione è costata una finestra di canale fermo il 2026-09-01.
Sui campi l’asimmetria è reale e mite: un campo che
il pacchetto si aspetta e il progetto non ha ferma il
giro (DIZIONARIO_CAMPO_ASSENTE), mentre uno che il progetto
ha e il pacchetto non conosce è tollerato
(DIZIONARIO_CAMPO_IN_PIU). Da soli, quei due direbbero che
basta importare prima e installare dopo.
Sulle scelte di un campo codificato non c’è nessuna
asimmetria: DIZIONARIO_SCELTE_DIVERSE ferma il giro
in tutti e due i versi, perché una scelta in più da un lato e
una in meno dall’altro sono la stessa differenza vista da due parti.
Quindi:
- una modifica che aggiunge solo campi si può fare in due tempi;
- una che tocca una parola di un vocabolario — cioè
le scelte di
outcome,identity,request_status,seal_state— vuole l’atto unico: import e installazione nella stessa finestra, con il giro fermo in mezzo.
La 0.14.0 sembrava del primo tipo perché aggiunge tre campi, ed era
del secondo: aggiungeva anche held alle scelte di
outcome. Il giro si è fermato su
DIZIONARIO_DERIVATO fra i due gesti, come doveva.
E server non è un vocabolario del pacchetto, è
la flotta. Le sue scelte si generano da instances,
e la lavorazione passa lì l’intera flotta
dell’inventario, non le sole istanze col modulo. Un dizionario generato
con le tre servite supera il controllo dei campi e ferma il giro su
DIZIONARIO_SCELTE_DIVERSE:server — misurato lo stesso
giorno, generandolo sbagliato.
Due domande diverse decidono la forma di un rilascio
Vanno fatte separatamente, perché fino al 2026-09-01 avevano sempre risposto insieme:
- tocca lo schema? Allora serve l’atto unico: import del dizionario e installazione nella stessa finestra, con il giro fermo in mezzo.
-
cambia la firma di ciò che la lavorazione chiama?
Allora i gesti sono due, e la lavorazione in
/optva ricopiata insieme al pacchetto.
La 0.13.0 ha risposto no alla prima e
sì alla seconda: nessun dizionario da importare, ma
round_record() aveva preso un terzo argomento obbligatorio,
e un pacchetto nuovo con la lavorazione vecchia muore su
argument "posta" is missing — cioè il giro non ritorna, non
lascia record, e l’allarme sull’assenza suona a gravità 1.
6. Che cosa fare quando suona
Una voce per regola, con la prima cosa da guardare. Le regole sono nominate per contenuto; i nomi stanno nel foglio dei parametri.
Nessun record (gravità 1). La lavorazione non è
partita, o è morta prima di emettere. Guardare lo stato dell’unità di
sistema e i suoi log sulla macchina del perimetro. Se l’unità è partita
e ha fallito, l’errore è quasi sempre nel recupero del token o del
segreto. La regola dell’osservatore però non chiede un record qualsiasi,
ne chiede uno con almeno una lettura riuscita: se l’unità è sana e il
record c’è con letture_riuscite a zero, nessuna istanza col
modulo ha risposto, e quando sono tutte banchi spenti è il caso
descritto nel §5.
Record incompleto (gravità 1). Il record c’è ma manca una colonna pretesa. Due cause, e vanno distinte prima di toccare qualcosa: o il modulo ha smesso di riportare quel campo su qualche istanza, oppure la regola di raccolta non conosce ancora una colonna che la lavorazione ha cominciato a emettere. La seconda si riconosce dal fatto che la colonna è nuova, e si ripara aggiornando la regola di raccolta.
Istanze irraggiungibili (gravità 2). Una o più
istanze di produzione non hanno risposto: nell’osservatore la regola
conta irraggiungibili_produzione, che lascia fuori i banchi
(§5), e non il campo storico irraggiungibili. Quali siano
lo dice irraggiungibili_nomi, che elenca anche i banchi
spenti; quali di quei nomi siano banchi lo dice l’inventario. Tolti i
banchi, guardare quante: se sono tutte le istanze di produzione, il
sospetto è sul perimetro, tipico l’indirizzo della macchina cambiato,
che chiude tutte le liste insieme. Se è una sola, il sospetto è su
quell’istanza.
Cancello non collaudato (gravità 2). Un’istanza non
è in stato collaudato. Leggere quale dei due cancelli l’ha declassata:
se è il soffitto di major, l’istanza è stata aggiornata oltre la
finestra del modulo; se è l’impronta, la superficie è cambiata dentro lo
stesso major. Nel primo caso si alza il soffitto prima di ricollaudare,
nel secondo si va direttamente al protocollo di §3. Con
cancelli vuoto non c’è nessun declassamento da leggere: il
giro non ha letto nessuna istanza, e suona anche la regola
sull’assenza.
Superficie disomogenea (gravità 2). Le istanze osservate non espongono la stessa impronta di superficie. Normale durante un’ondata di aggiornamenti, in cui le istanze passano alla versione nuova una per volta; da spiegare se l’ondata è finita.
Liste di indirizzi divergenti (gravità 2). Le istanze non ammettono lo stesso insieme di indirizzi. Un’impronta unica su tutte significa che le liste coincidono; se ne compaiono due, due istanze ammettono indirizzi diversi, e la differenza va spiegata prima di essere accettata. L’impronta è un digest di una manciata di indirizzi: lo spazio dei candidati è minuscolo, e ricostruire il contenuto per enumerazione costa meno che andare a leggere la configurazione. È un rilevatore di cambiamento, non un segreto.
Prima di inseguire un allarme isolato. L’ingestione ha consistenza eventuale: per qualche decina di secondi due query identiche possono vedere «ultimo record» diversi. Una regola che valuta dentro quella finestra può scegliere un record vecchio e produrre uno scatto che si richiude alla valutazione dopo. Con una valutazione all’ora e una run al giorno è raro e costa poco — ma se non lo si sa, è il classico allarme fantasma che si insegue per settimane.
7. Le trappole misurate
Ogni voce è un fatto già pagato una volta. Sono scritte qui perché ricomprarle costa quanto la prima volta.
L’ingestione scarta in silenzio le colonne che la regola di raccolta non conosce. Chi aggiunge un campo al record e non aggiorna prima la regola non ottiene un errore: ottiene un record accettato con quel campo vuoto. Da qui la regola d’ordine: si aggiorna la regola di raccolta prima di distribuire la lavorazione che emette la colonna nuova, mai dopo.
readr::read_csv() ha trim_ws = TRUE
per default. Un confronto cella per cella fra due dizionari
diceva «nessuna differenza», ed era lo strumento a dirlo: lo spazio
iniziale che REDCap aggiunge alle celle Field Annotation veniva
tagliato in lettura. Ricompare rileggendo con
trim_ws = FALSE.
Su Windows az è az.cmd. Un
URL con una & dentro viene rianalizzato da
cmd.exe, che spezza il comando. Si passano i parametri con
l’opzione dedicata, senza mettere la & nell’URL. E
l’uscita in formato tabulato emette terminatori di riga CRLF: il ritorno
a capo va tolto prima di confrontare la stringa con qualcosa.
E non è solo la &: anche la barra
verticale. Una query di telemetria è fatta di barre verticali,
e passata come parametro a az su Windows viene rianalizzata
dalla stessa shell. Il modo in cui si scopre è il peggiore possibile:
il comando esce zero con una risposta vuota, quindi non
somiglia a un errore ma a un workspace senza dati — che è la conclusione
sbagliata più costosa che si possa trarre guardando la telemetria.
L’aggiramento che vale per entrambe è lo stesso, e toglie la causa
invece di schivarla: si chiede il token con az e si esegue
la chiamata come richiesta REST, con la query nel corpo. Un corpo JSON
non passa da nessuna shell, quindi nessun carattere ha bisogno di essere
protetto.
Alcune sottoutilità di az chiedono conferma
interattiva per installare un’estensione. In una shell non
interattiva muoiono con un errore di lettura da terminale, che non ha
niente a che vedere con il comando. Si aggira chiamando l’API di
gestione direttamente, che non installa niente.
accountEnabled non torna se non lo si
seleziona. La lettura di un’utenza dalla riga di comando omette
la proprietà se non la si chiede esplicitamente, e in formato tabulato
esce come «nessun valore» — che si legge come «disattivato» o come «non
so», a seconda di come si è disposti. Va chiesta con la selezione
esplicita dei campi.
Bloccare l’utenza nel sistema di identità non ferma il canale, e va tenuta bloccata: il token dell’API non passa dal sistema di identità, quindi l’unica credenziale che apre il registro resta quella in Key Vault. Non sospendere invece l’utente dentro REDCap: è una leva dal nome simile e blocca anche l’API.
Tre servizi Azure con consistenza eventuale. Una rilettura immediata non è una verifica. Vale per l’ingestione della telemetria, per lo stato delle regole d’allarme e per le proprietà di un’utenza appena modificata. Quando la domanda è «è cambiato davvero?», la risposta si prende due volte a distanza.
paste0("X:", character(0)) torna
"X:", non character(0). Un vettore
vuoto in paste0() non propaga il vuoto: lo riempie. È il
modo in cui un elenco di errori vuoto diventa un elenco di un errore
senza contenuto, che poi viene contato come «c’è un problema».
Il modulo si distribuisce con git archive, mai
copiando dal working tree. Su Windows la copia dal working tree
porta con sé i terminatori di riga CRLF, e il PHP arriva sull’istanza
diverso da quello in git. E in PowerShell la pipe fra
git archive e ssh corrompe
l’archivio: la pipe non è binaria. Si scrive il tar su file e
lo si trasferisce, in due passi.
Il verde di un controllo mai stato rosso vale poco. Ogni guardia di questo canale è stata provata in entrambe le direzioni, e in un caso pinnando la finestra su record storici di stato noto invece di iniettare record falsi nella tabella — che poi resterebbero a inquinare ogni lettura futura.
8. Dove stanno i valori
Nel foglio dei parametri di esercizio, nella cartella condivisa. Il rimando è sempre per nome del parametro, mai per posizione nel foglio, così che riordinare il foglio non invalidi questa guida.
Il foglio raccoglie otto famiglie:
- il registro delle richieste: URL, istanza, identificativo del progetto, nome del modulo e numero dei campi;
- l’utenza di servizio: nome utente, gruppo di appartenenza, stato dell’account, ruolo sul registro;
- i segreti per nome: il Key Vault, il token del registro, il segreto del modulo per ciascuna istanza;
- la macchina del perimetro: nome, dimensione, indirizzo pubblico, alias di connessione, unità di sistema e orario;
- la telemetria: workspace, tabella, punto di raccolta, regola di raccolta, gruppo di azione;
- le variabili dell’unità di sistema, che è dove entrambe le lavorazioni prendono i nomi delle risorse e gli interruttori — la scrittura, la posta, il dirottamento delle notifiche;
- gli allarmi: le regole attive per ciascuna delle due lavorazioni, la loro gravità, la finestra e lo stato di collaudo;
- la flotta osservata: quali istanze hanno il modulo, le due impronte correnti, il numero di coppie e quante hanno una scadenza.
Ogni riga del foglio porta la data in cui il valore è stato verificato, e non quella in cui è stato scritto. Su servizi con consistenza eventuale «quando l’hai guardato» è parte del valore: una riga senza data è una riga di cui nessuno sa se è ancora vera.
Le righe della flotta osservata non si modificano a mano: sono misure, e il loro valore corretto è quello che la lavorazione riporta. Stanno nel foglio perché chi legge un allarme deve poter confrontare ciò che vede con ciò che era normale l’ultima volta che qualcuno ha guardato.
Che cosa questa guida non copre
Il provisioning delle identità. Non è ancora automatizzato: la creazione dell’account si fa a mano, con la procedura storica. Il confine è nominato invece di taciuto, perché chi cerca «come nasce l’account» deve trovare una risposta, e «non ancora automatizzato» è una risposta.
Il caricamento in batch delle richieste. Passa da IT. Un import per identificatore di record permetterebbe a un referente di sovrascrivere le righe di un altro, e la difesa nativa non è ancora in piedi.
Quando passare dalla simulazione alla scrittura vera. Vedi §1: il giro del canale simula finché nessuno gli chiede di scrivere per davvero, e il confronto fra desiderato e reale è acceso da subito — non è più lui l’interruttore, vedi §2. Questa guida non descrive quando si decide di girare quell’interruttore, ed è deliberato: una procedura scritta è una procedura che qualcuno esegue prima che sia il momento.
-
Il cancello sull’ambito delle richieste. Il pacchetto porta
scope_errors(), che rifiuta una richiesta il cui autore non detiene il permessouser_rightssul progetto che la richiesta nomina — la regola è che il canale non deve permettere a nessuno ciò che non potrebbe già fare a mano. È in catena: il giro lo interroga per ogni istanza che ha risposto, e una guardia nega qualunque funzione del pacchetto che sappia scrivere su un’istanza senza nominarlo. Il cancello si interroga sulle risposte e mai sulle assenze, e la distinzione ha una grana precisa. Un’istanza irraggiungibile torna con il proprio codice di trasporto — quello che riportamodule_state(), oppureTRASPORTO_ISTANZA_SENZA_MODULOoTRASPORTO_SEGRETO_NON_LEGGIBILEse non è nemmeno stata interrogata; una che risponde ma il cui modulo è troppo vecchio per riportare il permesso su qualunque riga torna inveceTRASPORTO_AMBITO_NON_LEGGIBILE. In entrambi i casi rimettono in coda le sole righe già risolte in una coppia, mai una dichiarazione di fuori ambito — dire «non sei autorizzato» quando la verità è «non ho potuto chiedere» manda a correggere la persona sbagliata. La stessa regola vale una grana più sotto: una singola riga la cui autorizzazione non si lascia risolvere — un ruolo cancellato sotto di lei, su un’istanza che altrove risponde — tornaTRASPORTO_PERMESSO_NON_LEGGIBILEe rientra in coda. È rifiutata comunque, perché l’alternativa sarebbe allargare l’accesso proprio nel momento in cui il canale ha smesso di poterlo vedere; ma il rifiuto è indirizzato a chi può ripararlo. Chi ha compilato quella richiesta l’ha compilata bene e potrebbe benissimo detenere il permesso: ciò che è rotto sta nella tabella dei diritti di REDCap, dove arriva IT e non lui.La guardia ha un’eccezione dichiarata: lo strumento di conformità (
run_conformance_check()), che non legge il registro e non ha un richiedente su cui interrogare il cancello — la regola che il cancello applica («il richiedente avrebbe potuto concederlo a mano») non si pone per uno strumento che un operatore lancia a mano contro un progetto di conformità dedicato, mai su dati derivati dal registro. Dirlo qui evita di promettere una garanzia più larga di quella che il codice mantiene: una guardia descritta come più ampia di quanto sia viene creduta più di quanto meriti.