Eaco ha raggiunto la v1.0.0 il 5 maggio 2016 โ messaggio di commit: “This is v1.0.0. Two years in production.” E’ cresciuto fino a 54 stelle, 8 fork, 240 commit, e ha gestito l’autorizzazione all’IFAD per altri cinque anni. Geremia Taglialatela l’ha preso in mano nel 2020 e l’ha mantenuto su Rails 6.0 e 6.1, poi ha modernizzato la CI a fine 2025. Il pattern ABAC con ACL-come-hash si e’ rivelato esattamente la scelta giusta per un’organizzazione dove l’accesso e’ determinato da posizione, dipartimento e gruppo di lavoro โ non solo “admin o no.” Il repo e’ ancora online, e la documentazione YARD e’ ancora tra le piu’ complete che abbia mai scritto per una gem.
Scriptoria e’ un’applicazione interna di workflow all’IFAD โ un’agenzia specializzata delle Nazioni Unite a Roma โ e il suo layer di autorizzazione mi sta dando sui nervi da mesi. Il codice funziona, ma e’ aggrovigliato nell’app. Ogni volta che dobbiamo aggiungere un nuovo ruolo o cambiare chi puo’ accedere a cosa, stiamo editando codice applicativo che non dovrebbe occuparsi di semantica di autorizzazione.
Cosi’ otto giorni fa ho iniziato a estrarlo. Oggi rilascio il risultato: Eaco โ un framework di Attribute-Based Access Control per Ruby, che prende il nome da Eaco, il guardiano delle chiavi dell’Ade nella mitologia greca.
172 commit. Cinque release. 100% di test coverage. E un sabato pomeriggio che non rivedro’ mai piu'.
Perche’ non Cancan/Pundit/Rolify?
Perche’ pensano tutti all’autorizzazione nel modo sbagliato โ o almeno, sbagliato per il nostro caso d’uso.
I framework basati sui ruoli ti danno “questo utente e’ un admin” o “questo utente e’ un editor.” Va bene per un blog. All’IFAD ci serve “questo utente puo’ leggere questo specifico documento perche’ e’ un revisore nel dipartimento Prestiti, o perche’ occupa la posizione di VP a cui e’ stato concesso l’accesso, o perche’ e’ taggato come editor di lingua inglese.” L’accesso e’ sulla risorsa, non sull’utente. Ed e’ determinato dagli attributi dell’utente, non da una singola colonna ruolo.
Questo e’ ABAC โ Attribute-Based Access Control. La risorsa ha una ACL. La ACL dice quali designator (attributi di sicurezza) concedono accesso e a quale livello. L’utente ha dei designator raccolti dalla sua identita’, appartenenza a gruppi, dipartimento, posizione, tag โ qualunque cosa assomigli alla struttura della tua organizzazione. L’intersezione determina l’accesso.
Le ACL sono solo hash
Ecco la semplificazione radicale: una ACL e’ un semplice Hash Ruby. Le chiavi sono stringhe designator, i valori sono simboli ruolo:
document.acl
#=> #<Document::ACL {"user:10" => :owner, "group:reviewers" => :reader}>
Tutto qui. Niente tabelle di join, niente associazioni polimorfiche, niente tabelle di autorizzazione con chiavi esterne che puntano ovunque. Un hash. Salvato come una singola colonna jsonb in PostgreSQL.
Questo significa che controllare l’accesso e’ un lookup di chiave nell’hash. Concedere accesso e’ impostare una chiave. Revocare e’ cancellare una chiave. E interrogare “tutti i documenti che questo utente puo’ vedere” e’ una singola operazione SQL usando l’operatore ?| di PostgreSQL:
WHERE documents.acl ?| array['user:42', 'group:employees', 'tag:english']::varchar[]
Una query. Niente join. Niente subselect. Il database fa l’intersezione degli insiemi per te.
Il DSL
Le regole di autorizzazione vivono in config/authorization.rb โ un file, dichiarativo, leggibile da chiunque:
authorize Document, using: :pg_jsonb do
roles :owner, :editor, :reader
permissions do
reader :read
editor reader, :edit
owner editor, :destroy
end
end
actor User do
admin do |user|
user.admin?
end
designators do
user from: :id
group from: :groups
tag from: :tags
end
end
Il blocco permissions e’ il pezzo di cui vado piu’ fiero in tutta la gem. Guarda cosa succede: reader :read definisce il ruolo reader con il permesso :read. Poi editor reader, :edit passa il ruolo reader come argomento โ e siccome reader e’ adesso un metodo che restituisce il suo set di permessi, l’editor eredita tutto quello che il reader puo’ fare, piu’ :edit. E’ method_missing come DSL:
def method_missing(role, *permissions)
if @permissions.key?(role)
@permissions[role]
else
save_permission(role, permissions)
end
end
La prima chiamata definisce il ruolo. La seconda restituisce i suoi permessi. L’ereditarieta’ dei ruoli emerge naturalmente dall’ordine di valutazione di Ruby. Nessuna sintassi speciale necessaria.
I Designator
I designator sono il ponte tra la struttura della tua organizzazione e il modello di autorizzazione di Eaco. Ereditano da String e hanno l’aspetto di "user:42" o "group:reviewers":
class User::Designators::Group < Eaco::Designator
label "Group"
end
L’opzione from: :groups nel DSL dice a Eaco di chiamare user.groups e avvolgere ogni risultato in un designator Group. Quando controlli user.can?(:read, document), Eaco raccoglie tutti i designator dell’utente, li interseca con le chiavi dell’ACL del documento, e verifica se qualcuno dei ruoli risultanti ha il permesso :read.
Il sistema dei designator e’ completamente pluggabile. La tua organizzazione ha dipartimenti? Posizioni? Comitati? Strutture a matrice? Definisci una classe designator, puntala a un metodo, e Eaco gestisce il resto.
L’adapter PostgreSQL jsonb
Qui e’ dove il design ripaga davvero. L’intero adapter sono 13 righe:
def accessible_by(actor)
return scoped if actor.is_admin?
designators = actor.designators.map {|d| sanitize(d) }
column = "#{connection.quote_table_name(table_name)}.acl"
where("#{column} ?| array[#{designators.join(',')}]::varchar[]")
end
Document.accessible_by(user) restituisce una normale relazione ActiveRecord โ puoi concatenarla con .where, .order, .limit, quello che vuoi. Sotto il cofano, l’operatore ?| di PostgreSQL controlla se l’oggetto jsonb contiene una qualsiasi delle chiavi date. Indice GIN sulla colonna acl e hai finito. Questa query scala a milioni di documenti.
Ho anche un adapter CouchDB-Lucene per un altro progetto IFAD, ma quello jsonb e’ la star dello show.
La Guardiana
Il controllo di autorizzazione nel controller e’ sorvegliato da una figura ASCII art di 48 righe nei commenti del sorgente. Si chiama La Guardiana, e veglia su ogni chiamata before_filter :confront_eaco. Perche’ se devi negare l’accesso, devi farlo con stile. (Non e’ sola โ Ayanami Rei veglia sulla suite di test.)
class DocumentsController < ApplicationController
before_filter :find_document
authorize :show, [:document, :read]
authorize :edit, [:document, :edit]
private
def find_document
@document = Document.find(params[:id])
end
end
Questa e’ l’intera integrazione col controller. Eaco installa un before_filter, controlla la variabile d’istanza @document contro current_user.can?(:read, @document), e lancia Eaco::Forbidden se negato. Nessuna cerimonia.
Lo sprint
Otto giorni. 20 febbraio, ore 2:06 โ commit iniziale. 20 febbraio, ore 2:12 โ “Import Scriptoria’s authorization code.” Poi e’ iniziata l’estrazione: ristrutturare in una gem ben fatta, costruire il DSL, scrivere gli adapter, aggiungere supporto Rails 3.2/4.0/4.1/4.2 via Appraisal, e costruire la suite di test.
v0.5.0 il 24 febbraio โ supporto Rails 3.2. v0.6.0 il 27 febbraio. v0.7.0 il 28 febbraio alle 2:57. Poi lo sprint finale: lo scenario Cucumber “Enterprise Authorization” con una gerarchia organizzativa NERVE completa (dipartimenti, posizioni, utenti), piu’ spec, piu’ feature, 100% di coverage.
cc3c2b4 ACHIEVEMENT UNLOCKED: 100% coverage :party:
4a5723a This is v0.8.0
28 febbraio, ore 13:33. Sessantasette minuti tra il raggiungimento del 100% di coverage e il tag della release. Cinque release in otto giorni. Denominazione d’Origine Controllata.
Come si presenta in pratica
# Controlla il permesso
user.can? :read, document #=> true
# Stessa verifica, altra direzione
document.allows? :read, user #=> true
# Che ruoli ha questo utente?
document.roles_of user #=> [:owner]
# Concedi accesso
document.grant :reader, :group, "reviewers"
# Revoca accesso
document.revoke :user, 42
# Tutti i documenti accessibili da questo utente
Document.accessible_by(user) # => ActiveRecord::Relation
Entrambe le direzioni funzionano โ user.can? e document.allows? โ perche’ a volte ragioni dalla prospettiva dell’utente e a volte da quella della risorsa. Stessa logica sotto.
Made in Italy
La gem e’ sotto licenza MIT e su GitHub. La documentazione YARD copre ogni metodo pubblico. Le feature Cucumber si leggono come specifiche โ lo scenario Enterprise modella una struttura organizzativa reale con la complessita’ che ci si aspetta da un’agenzia ONU.
Se le tue esigenze di autorizzazione vanno oltre “admin o no” โ se l’accesso dipende da chi e’ l’utente in relazione alla risorsa โ prova Eaco. gem install eaco e crea il tuo config/authorization.rb.
Le chiavi dell’Ade sono in buone mani.