Permessi come policy#
Di default i permessi sono i rbac.bindings di manent.yml (Permessi). Con rbac.engine: opa li decidono le stesse revisioni Rego della policy di governance: caricate come bozza, provate, approvate da una seconda persona, attivate senza riavviare i server e registrate nell'audit.
Una policy può esprimere ciò che i binding non sanno dire:
- accessi temporanei: un permesso di reperibilità che smette di valere a fine turno (
until); - proprietà: un team riceve le applicazioni dei tenant con il tag
owner=<team>, senza un binding per applicazione; - condizioni sulla richiesta: i contractor non cambiano mai la produzione, le migrazioni in produzione solo dal gruppo DBA, un deploy annullato solo da chi l'ha chiesto.
governance:
engine: opa # obbligatorio: le due policy condividono revisioni e ciclo di vita
auth:
oidc:
policy_claims: [email, name, employee_type] # claim visibili alla policy (default [email, name])
rbac:
engine: opa # builtin (default) | opa
opa:
cache: 30s # per quanto si riusano i permessi di un'identità
cache_size: 10000 # identità in cache
bindings: # policy.* sempre da qui
- {groups: [platform-release], role: admin, applications: ["*"], environments: ["*"]}
Con rbac.engine: opa e nessun Rego scritto non cambia nulla: una revisione senza package manent.authz riceve un modulo predefinito che applica i rbac.bindings esattamente come il motore builtin (un test verifica l'equivalenza).
Due parti: permessi e divieti#
data.manent.authz.grants → i permessi di un'identità (array o insieme di oggetti)
data.manent.authz.deny → motivi che rifiutano una modifica (stringhe o {code, message})
- I permessi si calcolano una volta per identità e restano in cache per
rbac.opa.cache: letture, elenchi, interfaccia web eGET /menon valutano mai Rego. - I divieti si valutano solo per le azioni che modificano qualcosa, dopo che i permessi le hanno consentite:
deployment.submit,deployment.dry-run,deployment.cancel,deployment.rollback,deployment.approve,deployment.freeze-override,schedule.write,freeze.write.
Permessi#
Input:
{
"schema": 1,
"now": "2026-10-20T09:00:00Z",
"identity": {
"subject": "oidc:alice", "method": "oidc", "name": "alice",
"groups": ["team-shop", "oncall"],
"claims": {"email": "alice@example.com", "employee_type": "contractor"}
}
}
claims contiene solo i claim elencati in auth.oidc.policy_claims: i token possono contenere altri dati personali che la policy non deve vedere. I token statici hanno method: token, i loro gruppi e nessun claim.
Output, un oggetto per permesso:
| Campo | Note |
|---|---|
name |
Etichetta mostrata da manent whoami, nell'interfaccia web e nell'audit. |
role o actions |
Un ruolo (predefinito o di rbac.roles/data.manent.rbac.roles) oppure un elenco di azioni. Le azioni policy.* sono scartate con un avviso: le concede solo rbac.bindings. |
applications |
Pattern qualificati con *: shop solo untenanted, payments/* le applicazioni di un tenant, */* tutte (dettagli). Obbligatorio. |
environments |
Pattern con *, obbligatori. |
tenants |
Selettore sui tag e sul nome del tenant proprietario; assente: tutte le applicazioni. |
artifacts |
Pattern su artifact.name; assente: tutti gli artefatti. |
selector |
Selettore sui tag delle applicazioni, con le chiavi di rbac.selector_keys; assente: tutte le applicazioni. |
until |
Facoltativo, RFC 3339: il permesso smette di valere a quell'istante, anche se è in cache. L'istante si confronta con l'orologio del server API. |
Un permesso non valido (ruolo o azione sconosciuti, pattern mancanti, selettore non valido) viene scartato da solo, con un avviso nel log e in manent_authz_invalid_grants_total; alla compilazione, sugli input di esempio, è un errore.
Divieti#
Input:
{
"schema": 1,
"now": "2026-10-20T09:00:00Z",
"identity": {"subject": "oidc:alice", "method": "oidc", "name": "alice", "groups": ["team-shop"], "claims": {}},
"action": "deployment.submit",
"resource": {
"application": "shop",
"environment": "prod",
"tenant": {"name": "payments", "tags": {"division": "commerce", "owner": "team-shop"}},
"deployment": {
"id": "shop-1.4.0-prod", "kind": "deploy", "dry_run": false,
"artifact": {"type": "application", "name": "shop", "version": "1.4.0"},
"requested_by": "oidc:alice"
}
}
}
resource.deployment è la richiesta per un invio e il deploy esistente per annullamento, rollback e approvazione; manca per le azioni senza deploy (schedule, freeze). L'input non contiene mai segreti né parametri dei plugin.
Un divieto prevale su ogni permesso. La risposta 403 e la voce di audit riportano i motivi e la revisione:
permission denied: oidc:alice cannot perform deployment.submit on shop/prod: contractors cannot change production (policy 14:3f2a9c1e5b7d)
Esempio#
package manent.authz
import rego.v1
import data.manent.lib.authz
# I binding di manent.yml (data.manent.rbac), più le regole sotto.
grants := authz.bindings | owners | oncall
# Un team possiede le applicazioni dei tenant con il suo nome nel tag owner.
owners contains g if {
some group in input.identity.groups
startswith(group, "team-")
g := {
"name": sprintf("owner %s", [group]),
"role": "deployer",
"applications": ["*/*"],
"environments": ["*"],
"tenants": sprintf("owner=%s", [group]),
}
}
# Reperibilità in produzione fino a fine turno (oncall/data.json).
oncall contains g if {
some shift in data.oncall.shifts
shift.subject == input.identity.subject
g := {"name": "oncall", "role": "operator", "applications": ["*/*"], "environments": ["prod"], "until": shift.until}
}
# I contractor non cambiano mai la produzione.
deny contains "contractors cannot change production" if {
input.identity.claims.employee_type == "contractor"
startswith(input.resource.environment, "prod")
}
# Migrazioni del database in produzione: solo i DBA.
deny contains {"code": "dba", "message": "migrations in production are released by the DBA group"} if {
input.action == "deployment.submit"
input.resource.deployment.artifact.type == "migration"
startswith(input.resource.environment, "prod")
not "dba" in input.identity.groups
}
data.manent.lib.authz fornisce bindings (i permessi di data.manent.rbac.bindings per l'identità dell'input) e binding_matches(binding, identity). Le restrizioni sono quelle della policy di governance: Rego v1, nessun built-in non deterministico, niente http.send. I test *_test.rego coprono anche manent.authz. I motivi dei divieti sono in inglese, come gli altri messaggi di errore.
Partire dai binding#
manent-server -c manent.yml policy export-rbac -d policy/
manent-server -c manent.yml policy test -d policy/
manent policy push -d policy/ -m "permessi da manent.yml"
export-rbac scrive manent/rbac/data.json (i binding come data.manent.rbac, che da quel momento non arriva più da manent.yml) e un authz.rego di partenza con esempi commentati. I file esistenti non vengono sovrascritti senza --force.
Separazione dei poteri#
| Azione | Chi decide |
|---|---|
policy.read, policy.write, policy.approve |
Solo rbac.bindings di manent.yml, qualunque sia il motore. |
manent-server admin … (break-glass) |
Chi ha accesso al database, fuori dai permessi. |
| Tutte le altre azioni | La revisione attiva. |
Una revisione non può concedersi il potere di cambiare la policy, e una revisione che toglie tutti i permessi non blocca chi deve correggerla: chi ha policy.* statico può ancora caricare, approvare e attivare una revisione nuova, o tornare a una precedente. La configurazione minima è quindi un binding statico per i release manager della piattaforma.
Simulazione e impatto#
Prima di approvare una revisione, simula le identità che riguarda. Nulla viene letto dal provider di identità né salvato: soggetto, gruppi e claim sono quelli che scrivi.
manent policy access 15 --subject oidc:carol --group team-shop --claim employee_type=contractor --compare
manent policy access 15 --subject oidc:carol --claim employee_type=contractor \
--action deployment.submit -a shop -e prod [-t acme] [-f deploy.yml]
Identity oidc:carol, revision 15:9be1c04d2f11
PERMISSION ACTIONS APPLICATIONS ENVIRONMENTS TENANT UNTIL
owner team-shop deployer * * owner=team-shop -
Compared with the active revision 14:3f2a9c1e5b7d:
+ owner team-shop deployer * * owner=team-shop -
Outcome: denied by the policy (permission owner team-shop)
- contractors cannot change production
current è la revisione in vigore; --json stampa la risposta di POST /api/v1/policies/{revision}/access. La stessa simulazione è nella sezione Accessi della pagina Policy dell'interfaccia web. Richiede policy.write.
Cache e disponibilità#
| Situazione | Effetto |
|---|---|
| Nuova revisione attivata | Vale subito: la cache è per revisione. |
| Permesso tolto dal provider di identità (gruppo, claim) | Vale entro rbac.opa.cache (default 30s). |
until raggiunto |
Vale subito, anche con il permesso in cache. |
Nessuna revisione caricata, o più vecchia di governance.opa.max_staleness |
Ogni azione non statica è rifiutata con 503; le policy.* continuano a funzionare, così si può caricare una revisione. |
| Errore nel calcolo dei permessi | Nessun permesso per quell'identità: tutto negato, errore nel log. |
| Errore nella valutazione dei divieti | La modifica è rifiutata (403, policy evaluation failed). |
Per tornare a rbac.engine: builtin basta un riavvio: i binding di manent.yml sono sempre lì.
Audit e metriche#
Le modifiche consentite riportano nell'audit authz_revision e il grant che le ha concesse; quelle negate la revisione e i motivi. manent whoami e GET /api/v1/me mostrano authz_engine, authz_revision e i permessi con name e until.
| Metrica | Descrizione |
|---|---|
manent_authz_grants_evaluations_total{outcome} |
Permessi calcolati (ok, cached, error). |
manent_authz_deny_evaluations_total{action,outcome} |
Divieti valutati (allowed, denied, error). |
manent_authz_eval_duration_seconds{part} |
Durata delle valutazioni (grants, deny). |
manent_authz_invalid_grants_total |
Permessi scartati perché non validi o con azioni policy.*. |
manent_authz_cache_entries |
Identità in cache. |