Ogni lettura tramite /osl/query è limitata da un
modello di capacità + permesso: chi sta chiedendo (una catena di soggetti
di delega), cosa ogni policy concede loro sul grafo semantico, e come quelle
concessioni si compongono. È fail-closed per costruzione — il default è
deny, e qualsiasi cosa non risolta viene scartata, mai indovinata.
L’accesso viene valutato su una catena di principal, radice → foglia. Il
soggetto agente è l’ultimo anello. La composizione ha due livelli:
All'interno di un principal
Tutte le policy che vincolano quel principal si combinano: le concessioni si
uniscono (union), l’obbligazione più forte vince (deny > mask >
redact), un deny esplicito prevale, e le condizioni (range di IP,
finestra temporale) fanno da gate solo sull’ALLOW — un deny è incondizionato.
Lungo la catena
Gli anelli si combinano con MEET (intersezione): un’entità o colonna è
concessa solo se ogni anello la concede. Il tetto di tier è il minimo (il
più restrittivo) e l’oscuramento richiesto è l’union di ciò che ogni anello
richiede. Un anello vuoto o non scritto fa collassare il meet a deny.
Lo scope semantico di una policy è un insieme di binding, ciascuno con
chiave un nodeId del domain-map (es.
entity:Customer, metric:aum). Un binding dichiara:
Campo
Significato
attributeGrants[]
Concessione per attributo: cleartext, mask, o deny
filters[] (+ filtersBool)
Predicati di riga che delimitano quali righe sono visibili
Le concessioni sono positive: una colonna non concessa viene oscurata, mai
restituita come NULL né scartata in silenzio dallo schema. deny rimuove del
tutto una colonna dalla projection; un deny globale della policy prevale su
qualsiasi concessione di altri binding.
I facet di contenuto sono governati da versioni di oscuramento con nome — un
disaccoppiamento tra un nome rivolto alle persone e un’identità fisica, stabile a
livello di byte:
Una RedactionVersion ha un nome (l’handle della console, es. "PII básica")
e un insieme di tag (le classi di contenuto che oscura, es. {email, phone}).
I tag sono l’identità fisica (invariata a livello di byte, usata come chiave
di tabella); il nome serve solo per l’authoring.
La versione implicita original (senza tag) è la tabella base.
Un facetGrant vincola un nome di versione richiesto per facet. La selezione
è exact-or-deny: il motore serve la versione memorizzata i cui tag coincidono
esattamente, oppure nega il facet — non ripiega mai su una versione più o meno
oscurata.
Lungo una catena, le versioni richieste si compongono per union degli insiemi
di tag (più oscuramento è sempre sicuro).
L’applicazione è server-side, nella cella. Gli esiti:
Situazione
Valore restituito
Nel risultato?
Concessa cleartext
originale
sì
Obbligazione mask
***
sì
Obbligazione redact
⟦redacted⟧
sì (sopravvive per ORDER BY/GROUP BY)
deny / non concessa
—
scartata dalla projection
La sentinella⟦redacted⟧ significa che la colonna sopravvive con un valore
governato (così la forma a valle è stabile), invece di diventare NULL o sparire
— ed è ciò che rende i join cross-modali fail-closed e il modello componibile.
La catena non è un header fidato — è un token firmato. La cella conia e
verifica un JWT HS256 il cui payload contiene la catena stessa, così un chiamante
non può accorciarla o falsificarla senza rompere la firma (una catena
assente/non valida → deny). Vedi Autenticazione.
Due operazioni dry-run calcolano la composizione reale senza servire dati (la
console Enterprise le espone; le API sono nella cella):
:simulate — anteprima di una policy contro una query come soggetto singolo.
:simulate-chain — compila l’intero set di policy scritte del tenant su una
catena root → leaf (il MEET), restituendo lo scope composto più una
ripartizione dei contributi per anello. Un anello non scritto viene
segnalato; il meet si svuota in un empty_meet fail-closed.