Role Provider è un servizio di ASP.NET, disponibile dalla versione 2.0 del Framework .Net, che permette la gestione dei ruoli.
I ruoli sono gruppi di utenti che condividono gli stessi privilegi all'interno di un sito internet. Ad esempio, in un blog potrebbero essere definiti i ruoli di amministratore, utente registrato ed utente anonimo per mostrare nelle pagine soltanto le funzioni appropriate al gruppo a cui appartiene il visitatore. L'utente deve quindi essere identificato, compito di cui si occupa un altro servizio di ASP.NET, Membership Provider. Membership e Role Provider sono stati progettati, infatti, per lavorare fianco a fianco.
In questo articolo realizziamo un Role Provider personalizzato basato su database SQLite e come Membership Provider utilizziamo quello presentato nell'articolo Un Membership Provider con SQLite, che è utile consultare per maggiori informazioni. Per un'introduzione a SQLite si veda invece l'articolo ASP.NET & SQLite.
I concetti qui esposti possono comunque essere applicati anche ad altri database, come MySql o Oracle, avendo soltanto cura di modificare in modo appropriato il provider ADO.NET utilizzato.
Il web.config
Prima di passare ad analizzare il codice è necessario modificare il file di configurazione dell'applicazione Web aggiungendo la definizione del roleManager
all'interno della sezione <system.web>
.
Listato 1. Modifica al web.config
<system.web>
[...]
<roleManager defaultProvider="SQLiteRoleProvider" enabled="true" cacheRolesInCookie="true"
cookieName=".ASPROLES" cookieTimeout="30" cookiePath="/" cookieRequireSSL="false" cookieSlidingExpiration="true" cookieProtection="All">
<providers>
<clear/>
<add name="SQLiteRoleProvider" type="Example.SQLiteRoleProvider"
connectionStringName="SQLiteConnString" applicationName="Example" />
</providers>
</roleManager>
[...]
</system.web>
ASP.NET è ora configurato per ignorare il Role Provider predefinito. Al suo posto verrà caricata la classe Example.SQLiteRoleProvider
, di cui ci occuperemo tra poco. Tutte le altre impostazioni sono quelle predefinite del Role Provider. Per maggiori informazioni vi invito a consultare la documentazione ufficiale su MSDN.
A questo punto è necessario configurare la stringa di connessione al database.
Listato 2. Dichiarare la strina di connessione
<connectionStrings>
<remove name="SQLiteConnString" />
<add connectionString="Data Source=|DataDirectory|Example.db;"
name="SQLiteConnString" providerName="System.Data.SQLite"
/>
</connectionStrings>
Ho volontariamente omesso, per chiarezza e perché esula dagli scopi di questo articolo, la configurazione del Membership Provider. Per saperne di più, consiglio di leggere l'articolo Un Membership Provider con SQLite.
Il database
Il Role Provider ha bisogno di due tabelle: aspnet_Roles
ed aspnet_UsersInRoles
. Nella prima sono memorizzati i ruoli, mentre nella seconda questi sono associati ad ogni singolo utente. Vediamo come sono strutturate le due tabelle.
aspnet_Roles | |
---|---|
Nome | Descrizione |
Rolename | Nome del ruolo |
ApplicationName | Nome dell'applicazione Web a cui il ruolo appartiene |
Listato 3. Dichiarazione SQL per aspnet_Roles
CREATE TABLE aspnet_Roles (
Rolename TEXT NOT NULL,
ApplicationName TEXT NOT NULL
);
aspnet_UsersInRoles | |
---|---|
Nome | Descrizione |
Username | Nome utente |
Rolename | Nome del ruolo |
ApplicationName | Nome dell'applicazione Web a cui il ruolo appartiene |
Listato 4. Dichiarazione SQL per aspnet_UsersInRoles
CREATE TABLE aspnet_UsersInRoles (
Username TEXT NOT NULL,
Rolename TEXT NOT NULL,
ApplicationName TEXT NOT NULL
);
Tutte le colonne sono dichiarate come TEXT
. Per migliorare le prestazioni di accesso ai dati creiamo anche due indici:
CREATE INDEX idxUIR ON aspnet_UsersInroles ('Username', 'Rolename', 'ApplicationName');
CREATE INDEX idxRoles ON aspnet_Roles ('Rolename' , 'ApplicationName');
Creazione della classe ed analisi del codice
Ogni Role Provider deve ereditare dalla classe astratta System.Web.Security.RoleProvider
, quindi anche il nostro (chiamato SQLiteRoleProvider
) seguirà questa regola.
I nomi dei metodi che devono essere sovrascritti (in corsivo) riflettono molto bene la loro funzione. Ad esempio il metodo GetAllRoles()
restituisce un array contenente tutti i ruoli, IsUserInRole()
controlla se un utente appartiene o meno ad un determinato ruolo e così via.
Tutti i metodi sono abbastanza simili e la loro logica di funzionamento può essere riassunta in tre fasi principali:
- connessione al database;
- ricerca o aggiornamento delle informazioni necessarie;
- disconnessione e restituzione dei valori richiesti.
Per ovvi motivi di spazio non sarà possibile analizzare il funzionamento di ogni singola funzione: ne verranno quindi prese in esame due fra le più significative. Vi invito quindi a scaricare il codice completo allegato all'articolo come ulteriore riferimento.
Analizziamo per prima RoleExists()
, il metodo che serve a controllare l'esistenza di un ruolo.
Listato 5. RoleExists()
public override bool RoleExists(string rolename) { bool result = false;
SQLiteConnection conn = new SQLiteConnection(connectionString);
SQLiteCommand cmd = new SQLiteCommand("SELECT * FROM [" + rolesTable + "]" +
" WHERE Rolename = ? AND ApplicationName = ?", conn);
cmd.Parameters.Add("@Rolename", DbType.String, 255).Value = rolename;
cmd.Parameters.Add("@ApplicationName", DbType.String, 255).Value = ApplicationName;
try
{
conn.Open();
if (cmd.ExecuteReader().HasRows) result = true;
}
catch (SQLiteException e) { throw e; }
finally { conn.Close(); }
return result;
}
La logica è abbastanza semplice: per prima cosa viene dichiarata una variabile di supporto chiamata result
, inizialmente impostata su false
. In seguito vengono creati la connessione e l'oggetto SQLiteCommand
e, all'interno di un blocco try
, viene eseguita l'interrogazione. Se l'esito è positivo alla variabile result
viene assegnato il valore true
. Nel caso si verifichi un errore, questo viene intercettato dal blocco catch
. Per semplicità, è stata omessa una gestione più complessa delle eccezioni. Per concludere, dopo essersi assicurati che la connessione sia chiusa all'interno del blocco finally, viene restituito il valore della variabile result
.
Il metodo CreateRole()
si occupa invece di creare un nuovo ruolo.
Listato 6. CreateRole()
public override void CreateRole(string rolename)
{
if (rolename.IndexOf(",") > 0)
throw new ArgumentException("Role names cannot contain commas.");
if (RoleExists(rolename))
throw new ProviderException("Role name already exists.");
SQLiteConnection conn = new SQLiteConnection(connectionString);
SQLiteCommand cmd = new SQLiteCommand("INSERT INTO " + rolesTable +
" (Rolename, ApplicationName) " + " Values(?, ?)", conn);
cmd.Parameters.Add("@Rolename", DbType.String, 255).Value = rolename;
cmd.Parameters.Add("@ApplicationName", DbType.String, 255).Value = ApplicationName;
try
{
conn.Open();
cmd.ExecuteNonQuery();
}
catch (SQLiteException e) { throw e; }
finally { conn.Close(); }
}
Dopo aver controllato che il nome del nuovo ruolo sia valido (non contiene virgole e non è già presente nel database), vengono creati gli oggetti SQLiteConnection e SQLiteCommand necessari. L'interrogazione viene anche in questo caso gestita all'interno di un blocco try-catch-finally. Questa funzione non restituisce alcun valore. Nel caso che il nome del ruolo non sia valido o si presenti qualche errore durante le operazioni sul database viene restituita un'eccezione.
È bene sottolineare che data la semplice gestione degli errori utilizzata nel codice d'esempio, questa si rende necessaria a livelli di codice più alti. È infatti preferibile non mostrare mai all'utente messaggi di errore interni, soprattutto per quanto riguarda servizi di autenticazione e gestione dei privilegi quali Membership e Role Provider.
Non ho volutamente analizzato la funzione Initialize(), molto importante in quanto si preoccupa di caricare le impostazioni dal file web.config, perché molto simile a quella già trattata per il Membership Provider, da cui differisce soltanto per i dati caricati.
Come ulteriore riferimento per la scrittura di provider personalizzati vi rimando all'ottima sezione di MSDN, Provider Toolkit.