
Quando si parla di sicurezza in un’applicazione, viene sempre da pensare a qualcosa di indispensabile ma non sempre facile da implementare. Negli anni, mi è capitato di utilizzare diversi modelli di autorizzazione: dal classico Role-Based alle autorizzazioni custom scritte ad hoc per il dominio applicativo. In questo articolo, analizziamo il nuovo modello di autorizzazione basato sulle policy introdotto dal .NET Core, che riesce facilmente ad adattarsi ad una grande varietà di scenari.
Per capire fino in fondo questo modello, dobbiamo fare una veloce panoramica sulle autorizzazioni basate sui Ruoli, probabilmente il modello più utilizzato, dalle cui limitazioni emergono le potenzialità del modello basato su policy.
Un ruolo non è altro che una stringa che identifica un set di permessi per un utente autenticato nel sistema. Il modello di autorizzazione basato sui ruoli permette, in .NET, l’utilizzo dell’attributo Authorize per limitare l’accesso ad una risorsa, in base al ruolo specificato. Quest’ultimo viene applicato a un controller o una action. Il codice che segue mostra come limitare l’accesso al “ReportController” ai soli amministratori del sistema, cioè gli utenti che sono membri del ruolo “Administrator”:
[Authorize(Roles = "Administrator")]
public class ReportController : Controller
{
//Code
}
Naturalmente, è possibile specificare più ruoli ammessi ad utilizzare un determinato controller: basta inserire i ruoli separati da una virgola.
[Authorize(Roles = "Manager,Administrator")]
public class ReportController : Controller
{
//Code
}
L’attributo Authorize, come detto precedentemente, è applicabile sia a livello di controller che di action, questo ci permette di limitare l’accesso a specifiche funzionalità:
[Authorize(Roles = "Manager, Administrator")]
public class ReportController : Controller
{
public ActionResult ViewReport()
{
//Code
}
[Authorize(Roles = "Administrator")]
public ActionResult DeleteAllReports()
{
//Code
}
}
Possiamo limitare l’accesso a ReportController ai soli utenti che appartengono ad entrambi i ruoli “Manager” e “Administrator”, rendendo più stringente l’autorizzazione.
[Authorize(Roles = "Manager")]
[Authorize(Roles = "Administrator")]
public class ReportController : Controller
{
}
Questi esempi evidenziano sia la facilità d’uso che le limitazioni di questo modello. Immaginate ad esempio lo scenario in cui il vostro dominio non ha una sola figura di “Amministratore”, ma molteplici versioni di essa, un “CustomerAdministrator”, un “ProductAdministrator”, un “SuperAdministrator”. La vostra applicazione dovrà tenere conto di tutte queste figure, aumentando la granularità delle vostre autorizzazioni: quando il numero di ruoli aumenta la difficoltà di gestione aumenta con essa. È proprio in questi scenari che il modello basato su policy può fare la differenza.
Partiamo dai tre concetti principali: Policy, Requirements e Handlers. Una policy è un insieme di requisiti (requirements); un requisito è un insieme di parametri che vengono utilizzati per validare l’identità dell’utente, mentre un handler è usato per determinare se un utente ha accesso o meno ad una specifica risorsa utilizzando i parametri contenuti nei requisiti.
Una policy viene di solito registrata allo startup dell’applicazione, più precisamente nel metodo ConfigureServices() della classe Startup.cs
services.AddAuthorization(options =>
{
options.AddPolicy("RequireManagerOnly", policy =>
policy.RequireRole("Manager","Administrator"));
});
Applicare una policy registrata è un’operazione molto semplice, basta utilizzare l’attributo Authorize già visto precedentemente, in una forma leggermente diversa:
[Authorize(Policy = "ShouldBeEmployeeOnly")]
public class ReportController : Controller
{
[Authorize(Policy = "RequireAdminOnly")]
public ActionResult DeleteReports()
{
//code
}
}
La prima cosa che colpisce è sicuramente la maggior espressività delle policy: un altro sviluppatore, che lavorerà sul vostro codice, sarà sicuramente facilitato nel capire come avete protetto una determinata funzionalità.
Nell’esempio sono stati utilizzati i ruoli del nostro dominio. Questa però non è l’unica modalità disponibile: possiamo utilizzare anche i Claim per esprimere i requisiti di una policy.
Un Claim non è altro che una coppia chiave/valore che identifica una caratteristica di un soggetto, come il nome, l’età, il numero di documento, ecc. In questo modo, riusciamo ad esprimere i requisiti di una policy attraverso il controllo del valore contenuto in un determinato Claim, come, ad esempio, abilitare una determinata funzione solo agli utenti maggiorenni.
La registrazione dei requisiti di una policy attraverso i claim viene fatta definendo la policy stessa. Se vogliamo, ad esempio, creare una policy che permette l’accesso sulla base dell’esistenza di un determinato claim per un utente autenticato nel sistema, possiamo farlo in questo modo:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy("ShouldBeOnlyEmployee", policy =>
policy.RequireClaim("EmployeeId"));
});
}
Una volta registrata, la policy è utilizzabile tramite l’attributo Authorize su un controller o su una specifica Action.
[Authorize(Policy = "ShouldBeOnlyEmployee")]
public IActionResult SomeMethod()
{
//Write your code here
}
Come detto precedentemente, è possibile esprimere i requisiti della policy controllando il valore contenuto in un determinato Claim. Ecco lo snippet di codice che effettua questo controllo sul valore contenuto nel Claim “IsAdmin”:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy("CustomSecurityPolicy", policy =>
policy.RequireClaim("IsAdmin", "true"));
});
}
Negli esempi fatti, gli handler erano impliciti. Vediamo adesso come possiamo creare un requirement custom da utilizzare con le nostre policy, e i possibili handler per gestirlo.
Un requirement, in .NET Core, è una classe che implementa l’interfaccia IAuthorizationRequirement e funge da contenitore dei parametri con cui verrà poi gestito il requisito. Eccone un esempio:
public class MinimumYearsInCompanyRequirement : IAuthorizationRequirement
{
public int MinimumYears { get; set; }
public MinimumYearsInCompanyRequirement(int experience)
{
MinimumYears = experience;
}
}
Un requirement può avere uno o più handler, che viene utilizzato per valutarne le proprietà. Un handler non è altro che una classe che estende AuthorizationHandler<T> e implementa il metodo HandleRequirementAsync()
public class MinimumYearsHandler :
AuthorizationHandler<MinimumYearsInCompanyRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumYearsInCompanyRequirement requirement)
{
throw new NotImplementedException();
}
}
Di seguito, il codice che contiene una semplice implementazione dell’handler, che trova il claim e valuta il Requirement:
public class MinimumYearsHandler : AuthorizationHandler<MinimumYearsInCompanyRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumYearsInCompanyRequirement requirement)
{
var user = context.User;
var claim = context.User.FindFirst("MinYears");
if(claim != null)
{
var expInYears = int.Parse(claim?.Value);
if (expInYears >= requirement.MinimumYears)
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Come potete vedere, nel caso in cui la valutazione dei parametri del requirement sia avvenuta con successo, basta chiamare il metodo Succeed() dell’AuthorizationHandlerContext passandogli come argomento l’istanza del Requirement, e rendendolo così soddisfatto e abilitare la funzionalità che avete protetto.
La registrazione degli Handler e dei Requirement custom viene sempre effettuata allo startup dell’applicazione nel metodo ConfigureServices.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(;
services.AddAuthorization(options =>
{
options.AddPolicy("MinYears", policy =>
policy.Requirements.Add(
new MinimumYearsInCompanyRequirement(5)));
});
services.AddSingleton<IAuthorizationHandler,
MinimumYearsHandler>();
}
Semplice, espressivo e potente. Dategli una possibilità e non ve ne pentirete.
Al prossimo articolo!