dRBACs.Client 4.3.1

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 v4.3.0 — permission dell'utente (perimetro)

Novità additiva per chi non la usa: un'app che non chiama AddRbacPermissions() non cambia comportamento e non paga alcuna chiamata in più.

Il problema

Il ruolo dice cosa puoi fare; serviva un modo di dire su quale sottoinsieme. Il caso reale: un operatore assegnato a uno o più presidi deve vedere solo i ricoveri di quei presidi, e chi non ha presidi assegnati continua a vederli tutti.

Il perimetro è un attributo dell'utente, non del ruolo — «questo operatore lavora sui presidi X e Y» non è una capacità condivisa da tutti quelli che hanno il suo stesso ruolo. Con le permission sul ruolo l'unica strada sarebbe stata un ruolo per presidio, che gonfia il catalogo e fa smettere ai ruoli di descrivere capacità. Da questa versione le permission stanno quindi sull'associazione utente-ruoli.

Convenzione sui valori

<DOMINIO>:<VALORE>        es. PRESIDIO:120026, ASL:RM1, DISTRETTO:3

Maiuscolo, separatore :, valore senza spazi, massimo 128 caratteri, confronto case-insensitive — il servizio normalizza in maiuscolo sia in scrittura sia in lettura. Il significato del valore non viene validato: lo conosce solo l'applicativo che lo legge.

Il dominio serve anche quando un'istanza del servizio RBAC è condivisa fra progetti: ogni applicativo legge le dimensioni che gli competono e ignora le altre.

Registrazione

builder.Services.AddRbacClient(builder.Configuration);
builder.Services.AddRbacPermissions();   // <-- opt-in

AddRbacPermissions() registra una seconda IClaimsTransformation che, a ogni richiesta autenticata, risolve le permission dell'utente e ne emette una per claim sotto il tipo permission. Ha cache e finestra di backoff proprie (rbac_perms_{CF}), con gli stessi parametri Rbac:CacheTtlMinutes e Rbac:FailureRetrySeconds dei ruoli.

È opt-in perché aggiunge una chiamata al servizio RBAC per ogni richiesta: chi non usa il perimetro non deve pagarla. I metodi su IRbacClient ci sono comunque, anche senza questa chiamata: quello che si aggiunge qui è solo la proiezione automatica sui claim.

Lettura

using dRBACs.Client.Security;

// tutte le permission
IReadOnlyList<string> tutte = PermissionClaims.Read(User);

// una in particolare (confronto case-insensitive)
bool puo = PermissionClaims.Has(User, "PRESIDIO:120026");

// solo i valori di un dominio, senza prefisso: ["120026", "120285"]
IReadOnlyList<string> presidi = PermissionClaims.ValuesOf(User, "PRESIDIO");

Cosa sapere

  • Un guasto del servizio non diventa mai "nessuna permission". La trasformazione lascia il principal intatto quando il servizio non risponde, e il fallimento non va in cache: va in cache solo un successo. Qui l'invariante pesa più che sui ruoli, perché "nessuna permission" di solito significa "vede tutto": trattare un'avaria come assenza di perimetro lo aprirebbe in silenzio. L'applicativo deve quindi distinguere claim assente da claim presente e vuoto, e decidere di conseguenza (nel caso d'uso originale: 503).
  • Cambio di contratto sui DTO. UserRoleDto acquista Permissions; RbacRoleDto la perde. Quel campo era popolato ma non lo leggeva nessun applicativo, e un campo JSON in più nella risposta viene semplicemente ignorato dalla deserializzazione — chi legge i ruoli non se ne accorge.
  • Serve un dRBACs.Server con gli endpoint api/user-permissions. Contro una versione precedente del servizio le letture rispondono 404 e arrivano come RbacResult fallito, cioè come "perimetro non determinabile".
  • La cache è per processo, come quella dei ruoli: una scrittura fatta dall'app A non invalida quella dell'app B.

Aggiornamento a v4.2.0 — Blazor Server e rendering interattivo

Novità additiva: un'app che non chiama AddRbacBlazorServer() non cambia comportamento.

Il problema

Un componente Blazor con rendering interattivo gira due volte: una durante il prerender, dentro la richiesta HTTP, e una sul circuito SignalR, dove non esiste nessuna richiesta HTTP. Sul circuito IHttpContextAccessor.HttpContext è null, e con esso saltano le due cose che nel pacchetto ci passavano attraverso:

// Funziona nel prerender, poi smette. Con il ?. davanti all'await non restituisce
// nemmeno null: await di un Task nullo è una NullReferenceException.
_accessToken = await HttpContextAccessor.HttpContext?.GetTokenAsync("access_token");

IUserContext è il caso peggiore dei due, perché non fallisce: degrada in silenzio ai propri valori di comodo — UserId diventa "dev-user", Email "dev@example.com", IsAdmin falso. Le policy di autorizzazione continuano a funzionare, perché leggono l'AuthenticationState e non questo servizio: il difetto non si manifesta come accesso negato, ma come dati scritti a nome dell'utente sbagliato.

La soluzione

builder.Services.AddIamAuthentication(builder.Configuration)
       .WithClaimsAugmenter<EmailClaimAugmenter>();

// Dopo AddRbacClient / AddIamAuthentication.
builder.Services.AddRbacBlazorServer();

La chiamata fa tre cose:

  1. Aggancia agli eventi del cookie il deposito server-side dell'access token (IRbacAccessTokenStore). Il token viene depositato a ogni richiesta HTTP in cui il cookie viene autenticato — compresa la negoziazione del circuito — e non lascia mai il server: è il motivo per cui non si usa PersistentComponentState, che lo serializzerebbe dentro l'HTML della pagina.
  2. Registra IRbacAccessTokenProvider, che recupera il token da dentro o fuori una richiesta HTTP.
  3. Sostituisce IUserContext con un'implementazione che risolve il principal dalla HttpContext quando c'è e dall'AuthenticationStateProvider quando non c'è.

Nei componenti, al posto di IHttpContextAccessor:

@inject IRbacAccessTokenProvider TokenProvider
@inject IRbacClient RbacClient

@code {
    protected override async Task OnInitializedAsync()
    {
        string? token = await TokenProvider.GetAccessTokenAsync();

        RbacResult<IReadOnlyList<RbacRoleDto>> roles = await RbacClient.GetAllRolesAsync(token);
        if (!roles.IsSuccess)
        {
            // Il servizio non ha risposto: roles.Value è null, non una lista vuota.
            return;
        }

        _availableRoles = roles.Value!.ToList();
    }
}

IUserContext non richiede invece alcuna modifica al codice che lo inietta: cambia solo l'implementazione registrata.

Cosa sapere

  • Il deposito è in memoria, per processo, come la cache dei ruoli. Dopo un riavvio dell'applicazione un circuito già aperto non trova più il token: la richiesta HTTP successiva lo rideposita, quindi un ricaricamento della pagina lo rimette a posto senza rifare login. Un'app su più nodi senza affinità di sessione può reimplementare IRbacAccessTokenStore su un deposito condiviso e registrarla al posto di quella predefinita.
  • La chiave del deposito è sid, altrimenti sub, altrimenti NameIdentifier, altrimenti il codice fiscale. Ripiegando su sub, due login dello stesso utente da dispositivi diversi condividono la voce e l'ultimo sovrascrive il token del primo — sono token equivalenti per il servizio RBAC. Quello che non può succedere è che la chiave di un utente risolva al token di un altro: ogni anello della catena è un claim di identità.
  • Il logout scarta il token dal deposito, senza aspettarne la scadenza.
  • Le opzioni (RbacBlazorOptions) permettono di cambiare lo schema del cookie, disattivare la cattura o la sostituzione di IUserContext, e regolare la durata di ripiego in cache.

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 a 0 disattiva il backoff. Anche Rbac:CacheTtlMinutes a 0 disattiva 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": "Strict"
  }
}

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
Strict (default) sì sì, via JWKS validati
Discovery sì sì, via JWKS rilassati
Compatible no, endpoint cablati no rilassati

Strict è il default perché è stato verificato con login reali contro l'IAM di produzione, un gradino per volta: il jwks_uri esiste e pubblica una chiave RSA 2048 con kid, le firme RS256 verificano, l'iss coincide con l'authority, l'aud è il client_id e il nonce torna riflesso nell'id_token.

Discovery verifica la firma ma lascia rilassati issuer e nonce: serve a isolare un guasto, non come destinazione. Compatible non verifica nulla — un id_token contraffatto verrebbe accettato — ed esiste solo come via di fuga se l'IAM smettesse di pubblicare il JWKS. In quel caso la cosa giusta è segnalarlo all'IAM, non restare lì.

Nota su RequireState: resta disattivato in tutte e tre le modalità, e non è una deroga. Il default del framework è true, ma OpenIdConnectHandler non assegna mai lo State del contesto di validazione: lasciato a true il login fallisce con IDX21329 contro qualunque IdP. La protezione CSRF sul callback la fa il cookie di correlazione, che il handler valida per conto suo.

Cache dei ruoli e invalidazione

I ruoli risolti restano in cache in memoria per Rbac:CacheTtlMinutes (default 10). Ogni scrittura fatta con IRbacClient — AddRoleToUserAsync, RemoveRoleFromUserAsync — scarta la cache di quel codice fiscale non appena il servizio conferma: l'utente vede i ruoli nuovi alla richiesta successiva, senza aspettare il TTL e senza rifare login. Si invalida solo dopo un esito positivo, perché se la scrittura non e' passata i ruoli in cache sono ancora quelli giusti.

Un'app che modifica i ruoli per una strada propria — una API amministrativa che parla direttamente al servizio RBAC invece di passare da IRbacClient — puo' iniettare IRbacRoleCache e invalidare da se':

public class RuoliController(IRbacRoleCache cache)
{
    public async Task Assegna(string cf, string roleId)
    {
        await _mioServizio.AssegnaAsync(cf, roleId);
        cache.Invalidate(cf);   // maiuscole e spazi non contano
    }
}

Limite da conoscere: la cache vive dentro un solo processo. Una scrittura fatta dall'app A non invalida la cache dell'app B, che continuera' a servire i ruoli vecchi fino alla scadenza del TTL. Per l'utente modificato dall'interno della propria applicazione — il caso normale — l'aggiornamento e' invece immediato. Se serve che la modifica si propaghi subito ovunque, l'unica leva oggi e' abbassare Rbac:CacheTtlMinutes.

Durata della sessione

Il cookie eredita la scadenza del token e non si rinnova (UseTokenLifetime = true, SlidingExpiration = false): lasciando i default del framework durerebbe 14 giorni rinnovandosi a ogni richiesta, e sopravviverebbe per settimane a un token scaduto o revocato. L'id_token dell'IAM vive un'ora, e quella diventa la durata della sessione.

Il discovery dichiara solo authorization_code, ma il token endpoint restituisce anche un refresh token. Il pacchetto non lo usa: nulla qui rinnova la sessione. 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;
    }
}

Ogni metodo restituisce un RbacResult<T>, che tiene distinti «il servizio ha risposto, e la risposta è vuota» e «il servizio non ha risposto»: il primo è un IsSuccess con un valore vuoto, il secondo un fallimento con Error e — quando c'è — StatusCode. Collassare i due casi è il difetto che questo tipo esiste per impedire.

Metodo Chiamata al servizio
GetUserRolesAsync(cf) GET api/user-roles/{cf}
GetAllRolesAsync() GET api/roles
GetAllUserRolesAsync() GET api/user-roles
AddRoleToUserAsync(cf, roleId) POST api/user-roles/{cf}/roles/{roleId}
RemoveRoleFromUserAsync(cf, roleId) DELETE api/user-roles/{cf}/roles/{roleId}
GetUserPermissionsAsync(cf) GET api/user-permissions/{cf}
ReplaceUserPermissionsAsync(cf, permissions) PUT api/user-permissions/{cf}
AddPermissionToUserAsync(cf, permission) POST api/user-permissions/{cf}
RemovePermissionFromUserAsync(cf, permission) DELETE api/user-permissions/{cf}/{permission}

Ogni metodo accetta un accessToken facoltativo: quando lo si omette, la richiesta parte senza bearer e il servizio RBAC risponde 401. Dentro una richiesta HTTP il token lo mette la claims transformation; da un componente Blazor interattivo lo si prende da IRbacAccessTokenProvider.

Le scritture sono idempotenti: assegnare una permission già presente o rimuoverne una assente non è un errore. Ogni scrittura riuscita invalida la cache di quel codice fiscale.

No packages depend on dRBACs.Client.

v.4.3.1 - Correzione: AddRbacClient() e AddRbacPermissions() registravano ciascuna una propria IClaimsTransformation; ASP.NET Core pero' non le incatena, ne risolve una sola (AuthenticationServiceImpl la riceve con un parametro singolo nel costruttore, non con IEnumerable<IClaimsTransformation>), quindi girava solo l'ultima registrata e l'altra veniva scartata in silenzio - ruoli assenti dal principal se AddRbacPermissions() veniva chiamata dopo AddRbacClient(), permission mai emesse nel caso opposto. L'ordine di AddRbacBlazorServer() rispetto alle altre due non c'entra, non tocca IClaimsTransformation. Le due trasformazioni confluiscono ora in un'unica IClaimsTransformation interna che le incatena (ruoli poi permission), qualunque sia l'ordine delle chiamate di registrazione. v.4.3.0 - Le permission passano dal ruolo all'utente. Nuovi metodi su IRbacClient per il perimetro dell'utente nella forma DOMINIO:VALORE (per esempio PRESIDIO:120026): GetUserPermissionsAsync piu' i tre di scrittura (sostituzione, aggiunta, rimozione), con lo stesso RbacResult<T> e la stessa invalidazione della cache dopo una scrittura riuscita. Nuova AddRbacPermissions() opt-in: registra una IClaimsTransformation che emette una permission per claim (tipo "permission"), con cache e backoff propri e gli stessi invarianti dei ruoli — un guasto del servizio non viene mai messo in cache come "nessuna permission". Le permission si rileggono con PermissionClaims (Read, Has, ValuesOf per dominio). Chi non chiama AddRbacPermissions() non paga alcuna chiamata in piu'. Cambio di contratto: UserRoleDto acquista Permissions e RbacRoleDto la perde — quel campo era popolato ma mai letto da nessun applicativo, e un campo JSON in piu' nella risposta viene semplicemente ignorato dalla deserializzazione. Richiede dRBACs.Server con gli endpoint api/user-permissions. v.4.2.0 - Supporto al rendering interattivo (Blazor Server / Blazor Web App): AddRbacBlazorServer() rende utilizzabili dal circuito SignalR le due parti del pacchetto che passavano da IHttpContextAccessor e che sul circuito non hanno una HttpContext da cui leggere. L'access token viene depositato lato server a ogni richiesta HTTP autenticata (IRbacAccessTokenStore, in memoria, per processo) e recuperato dai componenti con IRbacAccessTokenProvider: il token non raggiunge mai il browser. IUserContext viene sostituito con un'implementazione che risolve il principal anche dall'AuthenticationStateProvider - senza, dopo il prerender degradava in silenzio a "dev-user" con IsAdmin falso, mentre le policy di autorizzazione continuavano a funzionare. Novita' additiva: chi non chiama AddRbacBlazorServer() non cambia comportamento. v.4.1.0 - La cache dei ruoli viene invalidata dopo ogni scrittura andata a buon fine con AddRoleToUserAsync o RemoveRoleFromUserAsync: l'utente vede i ruoli nuovi alla richiesta successiva, senza attendere la scadenza del TTL ne' rifare login. Nuovo servizio pubblico IRbacRoleCache per le app che modificano i ruoli senza passare da IRbacClient. La cache resta per processo: una scrittura fatta da un'app non invalida quella di un'altra. v.4.0.0 - Sicurezza, cambio di default: Iam:ValidationMode passa da Compatible a Strict. La firma dell'id_token viene ora verificata contro il JWKS dell'IAM, e issuer e nonce sono validati. Il default precedente accettava un id_token contraffatto e nasceva dall'assunto che l'IAM non pubblicasse un jwks_uri: lo pubblica, e ogni gradino e' stato verificato con login reali contro l'IAM di produzione. Per tornare al comportamento precedente impostare Iam:ValidationMode a Compatible. Inoltre: nonce piu' corto (43 caratteri base64url, 256 bit) perche' l'IAM non digerisce quello del framework; RequireState forzato a false ovunque, perche' OpenIdConnectHandler non popola mai lo State del contesto di validazione e con true il login fallisce con IDX21329 contro qualunque IdP; RS256 come unico algoritmo ammesso; cookie di nonce e correlazione con SameSite=None piu' Secure espliciti. Il codice fiscale viene ora letto anche dall'id_token (claim codice_fiscale): lo userinfo dell'IAM risponde una sola volta per token, quindi un suo guasto non e' ritentabile e non fa piu' fallire il login — si prosegue senza i soli claim di profilo. v.3.3.0 - Nuovo lookup opzionale sull'anagrafica dipendenti (Iam:EmployeeDirectory, disattivato per default): dato il codice fiscale risolve lo username aziendale e lo emette come claim preferred_username, cosi' un'app che passa all'IAM continua a identificare l'utente con la stessa chiave con cui ha scritto i propri dati. Disponibile sia sul login a cookie sia, con AddIamEmployeeDirectory, sugli host a token. Le chiamate all'anagrafica sono autenticate con un token di servizio ottenuto in Basic da AuthTokenUrl, tenuto in cache e rinnovato prima della scadenza dichiarata dal servizio. Il DTO completo dell'anagrafica e' esposto agli augmenter tramite IamClaimsAugmentationContext.Employee. v.3.2.0 - Sicurezza: i claim di ruolo emessi dall'IdP vengono scartati prima di emettere il cookie quando i ruoli li decide il servizio RBAC (nuova opzione IgnoreIdentityProviderRoles, attiva per default). Un IdP che emette lo stesso ruolo amministrativo a tutti gli utenti non puo' piu' concedere privilegi, e un'indisponibilita' dell'RBAC non lascia piu' attivi ruoli residui nel cookie. v.3.1.3 - Correzione: un ruolo gia' presente sotto un solo claim type impediva l'emissione dello stesso ruolo sotto RoleClaimType. v.3.1.2 - Correzione: il base url impostato con Configure<RbacOptions> dopo AddRbacClient veniva ignorato. v.3.1.1 - Correzione: ricorsione infinita in RemoteRolesClaimsTransformation.

Version Downloads Last updated
4.3.2 56 09/17/2026
4.3.1 1 09/17/2026
4.3.0 1 09/16/2026
4.2.0 23 09/01/2026
4.1.0 15 09/01/2026
4.0.0 5 08/28/2026
3.3.0 15 08/28/2026
3.2.0 3 08/27/2026
3.1.2 3 08/27/2026
3.1.1 3 08/26/2026
3.1.0 1 08/26/2026