dRBACs.Client 3.3.0
dRBACs.Client
Libreria client per l'integrazione del sistema RBAC (Role-Based Access Control) nei progetti di ASLRoma1.
Installazione
Per utilizzare questa libreria, aggiungi il riferimento al progetto o installa il package NuGet (quando disponibile).
Compatibilità
Il pacchetto è multi-target sulle sole release LTS:
| TFM | Note |
|---|---|
net6.0 |
LTS scaduta (nov 2024), inclusa per i progetti non ancora migrati |
net8.0 |
LTS |
net10.0 |
LTS |
NuGet seleziona automaticamente l'assembly giusto e non serve alcuna configurazione nel progetto consumer. Un progetto su una release STS funziona comunque: risolve all'LTS compatibile più vicina — per esempio un progetto net9.0 usa lib/net8.0.
Il codice sorgente è identico per tutti i target — nessuna #if di compilazione condizionale, quindi il comportamento non cambia da un framework all'altro.
Aggiornamento a v3.3.0 — anagrafica dipendenti (opzionale)
Novità additiva e disattivata per default: chi non la configura non deve cambiare nulla.
L'IAM identifica l'utente con il codice fiscale, mentre le applicazioni preesistenti lo
identificano con lo username aziendale — quello con cui hanno scritto anni di dati. Senza un
ponte fra i due, ogni scrittura fatta dopo il passaggio all'IAM finisce sotto una chiave utente
diversa da tutte quelle già a database. Il lookup risolve il problema chiedendo a un servizio di
anagrafica, dato il codice fiscale, la posizione del dipendente, e ne emette lo USERNAME come
claim preferred_username.
{
"Iam": {
"EmployeeDirectory": {
"Enabled": true,
"BaseUrl": "https://url-del-servizio-anagrafica",
"LookupPath": "api/dipendenti/{cf}",
"CacheTtlMinutes": 60
}
}
}
Token di servizio
L'anagrafica non accetta il token dell'utente: autentica l'applicazione, con un token proprio che
si chiede in POST autenticandosi in Basic. Il token e' opaco e ha una scadenza assoluta, che il
pacchetto legge dalla risposta e usa per rinnovarlo — con un margine, perche' un token scaduto per
una frazione di secondo si manifesta come un 401 intermittente.
{
"Iam": {
"EmployeeDirectory": {
"AuthTokenUrl": "https://admin-mia.aslroma1.it/auth/token",
"AuthBasicCredentials": "<gia' codificate in base64>",
"TokenExpiryMarginSeconds": 60
}
}
}
In alternativa a AuthBasicCredentials si possono passare AuthClientId e AuthClientSecret in
chiaro. Lasciando AuthTokenUrl vuoto si inoltra all'anagrafica il token dell'utente, che resta il
comportamento adatto ai servizi che lo accettano.
Il token e' dell'applicazione e non dell'utente: sta in un singleton, condiviso da tutte le richieste, e il rinnovo e' serializzato — alla scadenza tutte le richieste in volo ripartono insieme, ed e' il momento in cui conviene meno moltiplicare le chiamate.
Il claim viene aggiunto prima degli augmenter e prima dei ruoli, quindi un augmenter lo
trova già fra i claim. Il pacchetto emette solo lo username: il resto dell'anagrafica —
nome, cognome, recapiti, struttura — è a disposizione dell'app in
IamClaimsAugmentationContext.Employee, perché quali altri dati diventino claim è una decisione
applicativa e non del pacchetto.
⚠️ Il campo GRUPPO dell'anagrafica è mappato ma deliberatamente inutilizzato, e non va
trasformato in un ruolo: i ruoli li decide il servizio RBAC, e una seconda sorgente
riaprirebbe esattamente il problema che IgnoreIdentityProviderRoles esiste per chiudere.
Un'anagrafica irraggiungibile non fa fallire il login: si prosegue senza username, coerentemente con quanto già fa la risoluzione dei ruoli. Esiti positivi e finestre di backoff sono in cache come per i ruoli.
Host a token (API, microservizi)
Un'app a cookie risolve lo username una volta sola al login e se lo porta nel cookie. Un'API
quel momento non ce l'ha: deve ricavarlo dal token a ogni richiesta, e per questo si registra
diversamente — senza OIDC, con una IClaimsTransformation:
builder.Services.AddIamEmployeeDirectory(builder.Configuration);
builder.Services.AddRbacClient(builder.Configuration); // se servono anche i ruoli
Richiede SaveToken = true sull'handler JWT: senza, non c'è access token da inoltrare né
all'anagrafica né al servizio RBAC.
Aggiornamento a v3.2.0 — i ruoli dell'IdP non sono più attendibili per default
Quando i ruoli li decide il servizio RBAC (UseRbacRoles = true, il default), i claim di ruolo
presenti nel token vengono scartati prima che l'identity finisca nel cookie.
Il motivo è di sicurezza, non di igiene dei claim. Un IdP può emettere ruoli non pertinenti
all'applicazione — o, come osservato sull'IAM ASLROMA1, lo stesso ruolo admin a ogni
utente che effettua l'accesso. E c'è un secondo motivo, meno evidente: un ruolo lasciato nel
cookie sopravvive a un'indisponibilità del servizio RBAC, perché
RemoteRolesClaimsTransformation restituisce il principal intatto quando il servizio non
risponde. Senza lo scarto, un'interruzione del servizio ruoli diventerebbe una promozione.
Lo scarto copre identity.RoleClaimType e i tipi noti (role, roles, URI WS-Federation), ed
è verificato a posteriori: se un claim di ruolo sopravvive, il login fallisce invece di
proseguire con un cookie potenzialmente privilegiato.
Gli IIamClaimsAugmenter girano dopo lo scarto: un ruolo aggiunto da un innesto applicativo
è una decisione dell'app, non un'affermazione dell'IdP, e resta in piedi.
// Per tornare al comportamento precedente — cioè fidarsi dei ruoli dell'IdP
// quanto di quelli RBAC:
builder.Services.AddIamAuthentication(builder.Configuration, o =>
{
o.IgnoreIdentityProviderRoles = false;
});
L'opzione non ha effetto con UseRbacRoles = false: senza una sorgente alternativa, svuotare i
ruoli lascerebbe ogni utente senza permessi.
Aggiornamento a v3.1.3
Un ruolo già presente nel principal sotto un solo claim type non veniva normalizzato sugli
altri. La verifica di presenza era globale — "questo ruolo c'è già da qualche parte?" — e un
riscontro positivo saltava l'emissione di tutti i tipi, incluso identity.RoleClaimType, che
è quello su cui IsInRole e [Authorize(Roles = ...)] decidono.
Si manifesta con un host che ha MapInboundClaims = true: il claim role del token viene
rimappato sull'URI WS-Federation, mentre RoleClaimType resta "role". Il ruolo si vede
benissimo fra i claim e IsInRole risponde comunque false.
Ora la verifica è per singolo claim type: si aggiungono i tipi mancanti senza duplicare quelli presenti.
Nota di configurazione, indipendente da questa correzione: RoleClaimType e NameClaimType
vanno tenuti coerenti con MapInboundClaims. Con la mappatura inbound attiva i claim del token
arrivano in forma WS-Federation, quindi i valori giusti sono ClaimTypes.Role e
ClaimTypes.Name, non "role" ed "email".
Aggiornamento a v3.1.2
Configure<RbacOptions> chiamata dopo AddRbacClient (o dopo AddIamAuthentication, che
la chiama internamente) non aveva effetto sull'indirizzo usato dal client: il base url veniva
letto subito da Rbac:BaseUrl e catturato nella closure di AddHttpClient. Un'app che tiene
la configurazione in variabili d'ambiente piatte finiva così per chiamare il default
https://localhost:7171.
Sintomo: a log l'indirizzo giusto, e a runtime
Risoluzione ruoli RBAC non riuscita ...: Connection refused (localhost:7171).
Ora il base url si risolve da IOptions<RbacOptions> alla creazione del client, quindi
entrambe le forme funzionano:
// sezione di configurazione
builder.Services.AddRbacClient(builder.Configuration); // legge Rbac:BaseUrl
// oppure valore da variabile d'ambiente piatta
builder.Services.Configure<RbacOptions>(o => o.BaseUrl = Environment.GetEnvironmentVariable("RBAC_BASE_URL")!);
Un BaseUrl esplicitamente vuoto ora fallisce con un messaggio chiaro invece di ripiegare in
silenzio su localhost.
Aggiornamento a v3.1.1
Correzione obbligatoria per chi usa AddIamAuthentication o comunque la risoluzione ruoli:
in v3.1.0 RemoteRolesClaimsTransformation entrava in ricorsione infinita alla prima
richiesta autenticata.
Il token per il servizio RBAC si legge con HttpContext.GetTokenAsync("access_token"), che
internamente chiama AuthenticateAsync, che rinvoca la claims transformation: senza una
guardia il giro non finisce mai. Il risultato è una StackOverflowException — non
intercettabile — quindi il processo terminava subito dopo il login senza lasciare nulla nei
log, e il container ripartiva.
Sintomo tipico: il login sull'IAM va a buon fine, il browser viene rediretto all'applicazione
e l'applicazione si riavvia. Nessun aggiornamento di configurazione è necessario: basta
passare a 3.1.1.
Migrazione a v3.0.0
IRbacClient restituisce RbacResult<T> invece di liste e bool. Il motivo: prima un
servizio irraggiungibile e un utente senza ruoli restituivano entrambi una lista vuota, e la
claims transformation metteva quell'esito in cache — un blip di rete lasciava l'utente senza
permessi per l'intera durata della cache, e una revoca era indistinguibile da un guasto.
// prima (v2.x)
List<RbacRoleDto> roles = await client.GetUserRolesAsync(cf);
// una lista vuota poteva voler dire "nessun ruolo" oppure "servizio giù": impossibile saperlo
// ora (v3.0.0)
RbacResult<IReadOnlyList<RbacRoleDto>> result = await client.GetUserRolesAsync(cf);
if (!result.IsSuccess)
{
// result.Error, result.StatusCode
return;
}
IReadOnlyList<RbacRoleDto> roles = result.Value!;
Altri cambiamenti, senza impatto sul codice consumer:
- il token di accesso viaggia sulla singola richiesta e non più su
HttpClient.DefaultRequestHeaders; - la claims transformation non muta più il principal ricevuto e non emette il claim
RBAC_TRANSFORMED, che se persistito in un cookie avrebbe congelato i ruoli fino al logout; - nuova opzione
Rbac:FailureRetrySeconds(default 15): attesa prima di riprovare dopo un guasto. Impostarla a0disattiva il backoff. AncheRbac:CacheTtlMinutesa0disattiva la cache dei ruoli.
Migrazione da AslRoma1.Rbac.Client (v1.x → v2.0.0)
Il pacchetto è stato rinominato. Per aggiornare un progetto consumer:
| Prima (v1.x) | Ora (v2.0.0) |
|---|---|
PackageReference Include="AslRoma1.Rbac.Client" |
PackageReference Include="dRBACs.Client" |
using AslRoma1.Rbac.Client.Extensions; |
using dRBACs.Client.Extensions; |
using AslRoma1.Rbac.Client.Security; |
using dRBACs.Client.Security; |
using AslRoma1.Rbac.Client.Services; |
using dRBACs.Client.Services; |
AddAslRoma1RbacClient(configuration) |
AddRbacClient(configuration) |
Nessuna chiave di configurazione è cambiata: la sezione Rbac resta identica.
Configurazione
Aggiungi la configurazione RBAC nel file appsettings.json:
{
"Rbac": {
"BaseUrl": "https://url-del-servizio-rbac",
"CacheTtlMinutes": 10,
"FailureRetrySeconds": 15
}
}
Autenticazione IAM (v3.1.0)
Per autenticarsi contro l'IAM ASLROMA1 (LDAP, SPID, CIE) e risolvere i ruoli in un colpo solo:
using dRBACs.Client.Extensions;
builder.Services.AddIamAuthentication(builder.Configuration);
Restano a carico dell'app app.UseAuthentication() e app.UseAuthorization(): l'ordine dei
middleware è una decisione della pipeline, non del pacchetto.
{
"Iam": {
"Authority": "https://iam.aslroma1.it",
"ClientId": "...",
"ClientSecret": "...",
"Scopes": "openid profile",
"ValidationMode": "Compatible"
}
}
Al termine del login l'utente ha: anagrafica da SPID/CIE, codice fiscale su
ClaimTypes.SerialNumber, email e PEC estratte dall'assertion SAML, e i ruoli dal servizio
RBAC — quindi IsInRole(...) e [Authorize(Roles = "...")] funzionano senza altro codice.
Modalità di validazione
| Modalità | Discovery | Firma verificata | Issuer / nonce |
|---|---|---|---|
Compatible (default) |
no, endpoint cablati | no | rilassati |
Discovery |
sì | sì, via JWKS | rilassati |
Strict |
sì | sì | validati |
Compatible è il default perché è la configurazione già in produzione, l'unica di cui si
sappia con certezza che funziona contro questo IAM. Va però capito che in quella modalità
un id_token contraffatto verrebbe accettato: è tollerabile solo perché il token arriva
sul canale server-to-server del code flow, protetto da TLS e PKCE. La direzione è passare a
Discovery appena la si è vista funzionare sul campo.
Durata della sessione
Il cookie eredita la scadenza del token e non si rinnova (UseTokenLifetime = true,
SlidingExpiration = false). L'IAM dichiara solo authorization_code, quindi non esiste
refresh token: lasciando i default del framework il cookie durerebbe 14 giorni rinnovandosi
a ogni richiesta, e sopravviverebbe per settimane a un token scaduto o revocato.
Iam:CookieLifetimeFollowsToken = false disattiva il vincolo, ma è una responsabilità che
ci si assume consapevolmente.
Claim applicativi
Per i claim che il pacchetto non può conoscere — il codice struttura di un'app, un profilo
amministrativo — si implementa IIamClaimsAugmenter:
builder.Services.AddIamAuthentication(builder.Configuration)
.WithClaimsAugmenter<CirCodeAugmenter>();
Adattare i default a un'app esistente
I default seguono l'integrazione collaudata (MapInboundClaims = false, nome da email,
ruoli da role). Un'app che si aspetta la forma WS-Federation li ribalta:
builder.Services.AddIamAuthentication(builder.Configuration, o =>
{
o.MapInboundClaims = true;
o.NameClaimType = "preferred_username";
});
Altre opzioni: ApiPathPrefixes (default ["/api"], sotto cui si risponde 401/403 invece di
redirigere al login), UseRbacRoles (per chi si autorizza diversamente),
EmployeeDirectory (vedi sopra), AccessDeniedPath,
e LogUserInfoPayload — che registra il payload grezzo dello userinfo per riallineare i DTO
quando lo schema IAM cambia. ⚠️ Quel payload contiene dati personali (codice fiscale,
indirizzo, telefono, estremi del documento): va acceso solo il tempo necessario.
Registrazione dei servizi
Nel file Program.cs, utilizza il metodo di estensione per registrare i servizi necessari:
using dRBACs.Client.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Registra i servizi RBAC
builder.Services.AddRbacClient(builder.Configuration);
var app = builder.Build();
// ...
Utilizzo
IUserContext
Puoi iniettare IUserContext per ottenere informazioni sull'utente corrente:
public class MyService
{
private readonly IUserContext _userContext;
public MyService(IUserContext userContext)
{
_userContext = userContext;
}
public void DoSomething()
{
var cf = _userContext.CodiceFiscale;
var isAdmin = _userContext.IsAdmin;
// ...
}
}
IRbacClient
Puoi iniettare IRbacClient per interagire direttamente con il servizio RBAC:
public class MyAdminService
{
private readonly IRbacClient _rbacClient;
public MyAdminService(IRbacClient rbacClient)
{
_rbacClient = rbacClient;
}
public async Task<bool> AssignRole(string cf, string roleId)
{
var result = await _rbacClient.AddRoleToUserAsync(cf, roleId);
return result.IsSuccess;
}
}
No packages depend on dRBACs.Client.