Provider di attestazioni personalizzate Azure per un progetto di SharePoint - Parte 2
Articolo originale pubblicato mercoledì 15 febbraio 2012
Nella Parte 1 di questa serie di post ho brevemente illustrato in termini generali gli obiettivi di questo progetto, ovvero utilizzare Windows Azure Table Storage come archivio dati per un provider di attestazioni personalizzate di SharePoint. Il provider di attestazioni utilizzerà il kit CASI per recuperare da Windows Azure i dati necessari per fornire la funzionalità di risoluzione dei nomi del controllo di digitazione e di Selezione utenti, ossia della Rubrica.
Nella Parte 3 creerò tutti i componenti utilizzati nella farm di SharePoint, incluso un componente personalizzato basato sul kit CASI che gestisce tutte le comunicazioni tra SharePoint e Azure. È disponibile una web part personalizzata che acquisisce le informazioni relative ai nuovi utenti e le inserisce in una coda di Azure. È infine disponibile un provider di attestazioni personalizzate che comunica con Azure Table Storage tramite WCF, grazie al componente personalizzato del kit CASI, per consentire la funzionalità Selezione utenti e controllo di digitazione.
Cerchiamo ora di studiare questo scenario in modo più approfondito.
Questo tipo di soluzione è adatto per uno scenario piuttosto comune, in cui si desidera una rete Extranet con una gestione minima. Supponete ad esempio di voler consentire a partner o clienti di accedere a un vostro sito Web, richiedere un account e quindi effettuare automaticamente il "provisioning" per tale account, intendendo per "provisioning" attività diverse per utenti diversi. Questo sarà il nostro scenario di base, ma parte del lavoro sarà svolto anche dalle nostre risorse pubbliche o del cloud.
Iniziamo a esaminare i componenti del cloud che svilupperemo direttamente:
- Una tabella per tenere traccia di tutti i tipi di attestazione da supportare
- Una tabella per tenere traccia di tutti i valori di attestazione univoci per Selezione utenti
- Una coda in cui poter inviare i dati da aggiungere all'elenco dei valori di attestazione univoci
- Alcune classi di accesso ai dati per la lettura e la scrittura di dati dalle tabelle di Azure e per la scrittura di dati nella coda
- Un ruolo di lavoro di Azure che leggerà i dati dalla coda e li inserirà nella tabella dei valori di attestazione univoci
- Un'applicazione WCF che costituirà l'endpoint attraverso il quale la farm di SharePoint comunica per ottenere l'elenco dei tipi di attestazione, ricercare le attestazioni, risolvere un'attestazione e aggiungere dati alla coda
Passiamo ora a illustrare in modo più specifico i singoli componenti.
Tabella dei tipi di attestazione
La tabella dei tipi di attestazione è la tabella in cui verranno archiviati tutti i tipi di attestazione che il nostro provider di attestazioni personalizzate può utilizzare. In questo scenario utilizzeremo un solo tipo di attestazione, ovvero l'attestazione di identità, che in questo caso corrisponderà all'indirizzo di posta elettronica. Si potrebbero utilizzare altre attestazioni, ma per semplificare lo scenario utilizzeremo soltanto questa. In Azure Table Storage è possibile aggiungere istanze delle classi a una tabella, pertanto dobbiamo creare una classe per descrivere i tipi di attestazione. Anche in questa circostanza si potrebbero aggiungere istanze di diversi tipi di classe alla stessa tabella di Azure ma, per non complicare troppo le operazioni, procederemo in un altro modo. La classe che verrà utilizzata dalla tabella è la seguente:
namespace AzureClaimsData
{
public class ClaimType : TableServiceEntity
{
public string ClaimTypeName { get; set; }
public string FriendlyName { get; set; }
public ClaimType() { }
public ClaimType(string ClaimTypeName, string FriendlyName)
{
this.PartitionKey = System.Web.HttpUtility.UrlEncode(ClaimTypeName);
this.RowKey = FriendlyName;
this.ClaimTypeName = ClaimTypeName;
this.FriendlyName = FriendlyName;
}
}
}
Non tratterò tutti i concetti di base relativi all'utilizzo di Azure Table Storage perché sono già disponibili numerose risorse in grado di fornire tali informazioni. Se perciò desiderate ulteriori dettagli su cosa sia PartitionKey o RowKey e sul relativo utilizzo, il motore di ricerca Bing locale, che conoscete fin troppo bene, potrà esservi sicuramente di aiuto. L'unico aspetto che voglio sottolineare è che codificherò con URL il valore da archiviare per PartitionKey. Perché? In questo caso, PartitionKey corrisponde al tipo di attestazione, che può accettare diversi formati: urn:foo:blah, https://www.foo.com/blah e così via. Nel caso di un tipo di attestazione contenente barre, Azure non è in grado di archiviare PartitionKey con tali valori. Li codificheremo quindi con un formato noto riconosciuto da Azure. Come ho già spiegato più indietro, nel nostro caso utilizzeremo l'attestazione corrispondente all'indirizzo di posta elettronica, pertanto il relativo tipo di attestazione sarà https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress.
Tabella dei valori di attestazione univoci
La tabella dei valori di attestazione univoci è la tabella in cui vengono archiviati tutti i nostri valori di attestazione univoci. In questo caso, archivieremo un solo tipo di attestazione, ovvero l'attestazione di identità, pertanto per definizione tutti i valori di attestazione saranno univoci. Ho tuttavia scelto questo approccio per motivi di estendibilità. Supponete ad esempio di voler iniziare più avanti a utilizzare attestazioni di ruolo con questa soluzione. Non avrebbe perciò senso archiviare l'attestazione di ruolo "Employee" (Dipendente) o "Customer" (Cliente) e simili per un migliaio di volte. Per Selezione utenti è sufficiente sapere che il valore esiste perché possa essere reso disponibile. Non ha quindi importanza a chi sia associato. È solo necessario che possa essere utilizzato durante la concessione dei diritti in un sito. Ecco quindi la classe che archivierà i valori di attestazione univoci:
namespace AzureClaimsData
{
public class UniqueClaimValue : TableServiceEntity
{
public string ClaimType { get; set; }
public string ClaimValue { get; set; }
public string DisplayName { get; set; }
public UniqueClaimValue() { }
public UniqueClaimValue(string ClaimType, string ClaimValue, string DisplayName)
{
this.PartitionKey = System.Web.HttpUtility.UrlEncode(ClaimType);
this.RowKey = ClaimValue;
this.ClaimType = ClaimType;
this.ClaimValue = ClaimValue;
this.DisplayName = DisplayName;
}
}
}
Sono due i punti da porre in evidenza. Innanzitutto, come la classe precedente, PartitionKey utilizza un valore codificato con URL (UrlEncode) perché sarà il tipo di attestazione e conterrà le barre. In secondo luogo, come osservo spesso quando utilizzo Azure Table Storage, i dati vengono denormalizzati perché non vi è un concetto JOIN come in SQL. Da un punto di vista tecnico, è possibile eseguire un JOIN in LINQ, ma gli elementi di LINQ non consentiti quando si opera con dati di Azure sono talmente tanti (o assicurano prestazioni talmente scarse) che trovo più semplice effettuare la semplice denormalizzazione. Se avete alternative, inviatemi i vostri commenti perché sono curioso di sapere cosa pensate in proposito. Nel nostro caso, il nome visualizzato pertanto sarà "Email" perché è il tipo di attestazione che archivieremo in questa classe.
Coda delle attestazioni
La coda delle attestazioni ha un funzionamento abbastanza lineare. In questa coda archivieremo le richieste di "nuovi utenti" e quindi un processo di lavoro di Azure le leggerà dalla coda e sposterà i dati nella tabella dei valori di attestazione univoci. Il motivo principale è rappresentato dal fatto che l'utilizzo di Azure Table Storage a volte può rallentare le operazioni, mentre l'inserimento di un elemento in una coda è piuttosto rapido. Questo approccio ci consente di ridurre al minimo l'impatto sul sito Web di SharePoint.
Classi di accesso ai dati
Uno degli aspetti alquanto banali dell'utilizzo di Azure Table Storage e delle code è rappresentato dal fatto che è sempre necessario scrivere la propria classe di accesso ai dati. Per Azure Table Storage, è necessario scrivere una classe contesto dati e una classe origine dati. Non perderò molto tempo su questo argomento perché è possibile trovare valanghe di informazioni specifiche sul Web. Allego inoltre a questo post il mio codice sorgente per il progetto di Azure in modo che possiate utilizzarlo come desiderate.
Vorrei tuttavia richiamare la vostra attenzione su una questione importante, che dipende solo da una scelta personale. Io preferisco inserire tutto il mio codice di accesso ai dati di Azure in un progetto separato. In questo modo, posso compilarlo nel relativo assembly e utilizzarlo anche da progetti non di Azure. Nel codice di esempio che caricherò troverete un'applicazione di Windows Form che ho utilizzato per testare le diverse parti del back-end Azure. Non include informazioni relative ad Azure, tranne per il fatto che contiene un riferimento ad alcuni assembly di Azure e al mio assembly di accesso ai dati. Posso quindi utilizzarla in quel progetto e altrettanto facilmente nel progetto WCF che utilizzo per fornire un'interfaccia front-end per l'accesso ai dati per SharePoint.
Ecco comunque di seguito alcuni dei dettagli relativi alle classi di accesso ai dati:
- · Dispongo di una classe "contenitore" distinta per i dati da restituire, ovvero i tipi di attestazione e i valori di attestazione univoci. Per classe contenitore intendo una classe semplice con una proprietà pubblica di tipo List<>. Quando vengono richiesti dati, viene restituita questa classe anziché un elenco (List<>) di risultati. Procedo in questo modo perché, quando restituisco un elenco List<> da Azure, il client ottiene solo l'ultima voce dell'elenco (non si verificano invece problemi nel caso di WCF ospitato localmente). Per ovviare a questo problema, preferisco restituire i tipi di attestazione in una classe come la seguente:
namespace AzureClaimsData
{
public class ClaimTypeCollection
{
public List<ClaimType> ClaimTypes { get; set; }
public ClaimTypeCollection()
{
ClaimTypes = new List<ClaimType>();
}
}
}
E la classe per la restituzione dei valori di attestazione univoci è la seguente:
namespace AzureClaimsData
{
public class UniqueClaimValueCollection
{
public List<UniqueClaimValue> UniqueClaimValues { get; set; }
public UniqueClaimValueCollection()
{
UniqueClaimValues = new List<UniqueClaimValue>();
}
}
}
- · Le classi contesto dati sono piuttosto semplici da capire, niente di veramente brillante (come direbbe il mio amico Vesa). La classe è la seguente:
namespace AzureClaimsData
{
public class ClaimTypeDataContext : TableServiceContext
{
public static string CLAIM_TYPES_TABLE = "ClaimTypes";
public ClaimTypeDataContext(string baseAddress, StorageCredentials credentials)
: base(baseAddress, credentials)
{ }
public IQueryable<ClaimType> ClaimTypes
{
get
{
//this is where you configure the name of the table in Azure Table Storage
//that you are going to be working with
return this.CreateQuery<ClaimType>(CLAIM_TYPES_TABLE);
}
}
}
}
- · Nelle classi origine dati adotto un approccio leggermente diverso per la connessione ad Azure. La maggior parte degli esempi che vedo sul Web prevede la lettura delle credenziali con una classe di impostazioni del Registro di sistema di cui non ricordo il nome esatto. Tale approccio nel nostro caso presenterebbe un problema perché non dispongo di un contesto specifico di Azure, in quanto desidero che la mia classe di dati funzioni al di fuori di Azure. Nelle proprietà del mio progetto perciò creo semplicemente un'impostazione in cui includo il nome dell'account e la chiave necessaria per la connessione al mio account Azure. Entrambe le mie classi origine dati avranno perciò codice simile al seguente per creare la connessione ad Azure Table Storage:
private static CloudStorageAccount storageAccount;
private ClaimTypeDataContext context;
//static constructor so it only fires once
static ClaimTypesDataSource()
{
try
{
//get storage account connection info
string storeCon = Properties.Settings.Default.StorageAccount;
//extract account info
string[] conProps = storeCon.Split(";".ToCharArray());
string accountName = conProps[1].Substring(conProps[1].IndexOf("=") + 1);
string accountKey = conProps[2].Substring(conProps[2].IndexOf("=") + 1);
storageAccount = new CloudStorageAccount(new StorageCredentialsAccountAndKey(accountName, accountKey), true);
}
catch (Exception ex)
{
Trace.WriteLine("Error initializing ClaimTypesDataSource class: " + ex.Message);
throw;
}
}
//new constructor
public ClaimTypesDataSource()
{
try
{
this.context = new ClaimTypeDataContext(storageAccount.TableEndpoint.AbsoluteUri, storageAccount.Credentials);
this.context.RetryPolicy = RetryPolicies.Retry(3, TimeSpan.FromSeconds(3));
}
catch (Exception ex)
{
Trace.WriteLine("Error constructing ClaimTypesDataSource class: " + ex.Message);
throw;
}
}
- · L'effettiva implementazione delle classi origine dati include un metodo per aggiungere un nuovo elemento per un tipo di attestazione e un valore di attestazione univoco. Il codice è molto semplice ed è come quello riportato di seguito:
//add a new item
public bool AddClaimType(ClaimType newItem)
{
bool ret = true;
try
{
this.context.AddObject(ClaimTypeDataContext.CLAIM_TYPES_TABLE, newItem);
this.context.SaveChanges();
}
catch (Exception ex)
{
Trace.WriteLine("Error adding new claim type: " + ex.Message);
ret = false;
}
return ret;
}
Una differenza importante da notare nel metodo Add per l'origine dati dei valori di attestazione univoci è che non genera un errore o restituisce false quando si verifica un'eccezione durante il salvataggio delle modifiche. Ecco perché immagino che molti utenti per sbaglio o per altri motivi tentino di eseguire l'accesso più volte. Dopo che la loro attestazione di posta elettronica è stata registrata, qualsiasi tentativo successivo di aggiungerla genererà comunque un'eccezione. Dal momento che Azure non prevede eccezioni fortemente tipizzate e che non desidero riempire il registro di traccia con informazioni inutili, non mi preoccupo quando si verifica questa situazione.
- · La ricerca delle attestazioni è leggermente più interessante, anche se di nuovo in LINQ sono consentite operazioni che non è possibile eseguire in LINQ con Azure. Riporto di seguito il codice e più avanti spiegherò alcune delle scelte che ho fatto:
public UniqueClaimValueCollection SearchClaimValues(string ClaimType, string Criteria, int MaxResults)
{
UniqueClaimValueCollection results = new UniqueClaimValueCollection();
UniqueClaimValueCollection returnResults = new UniqueClaimValueCollection();
const int CACHE_TTL = 10;
try
{
//look for the current set of claim values in cache
if (HttpRuntime.Cache[ClaimType] != null)
results = (UniqueClaimValueCollection)HttpRuntime.Cache[ClaimType];
else
{
//not in cache so query Azure
//Azure doesn't support starts with, so pull all the data for the claim type
var values = from UniqueClaimValue cv in this.context.UniqueClaimValues
where cv.PartitionKey == System.Web.HttpUtility.UrlEncode(ClaimType)
select cv;
//you have to assign it first to actually execute the query and return the results
results.UniqueClaimValues = values.ToList();
//store it in cache
HttpRuntime.Cache.Add(ClaimType, results, null,
DateTime.Now.AddHours(CACHE_TTL), TimeSpan.Zero,
System.Web.Caching.CacheItemPriority.Normal,
null);
}
//now query based on criteria, for the max results
returnResults.UniqueClaimValues = (from UniqueClaimValue cv in results.UniqueClaimValues
where cv.ClaimValue.StartsWith(Criteria)
select cv).Take(MaxResults).ToList();
}
catch (Exception ex)
{
Trace.WriteLine("Error searching claim values: " + ex.Message);
}
return returnResults;
}
Il primo aspetto da notare è che non posso utilizzare StartsWith con dati di Azure. Questo significa perciò che devo recuperare tutti i dati localmente e quindi utilizzare l'espressione StartsWith. Poiché il recupero di tutti questi dati può essere un'operazione onerosa (si tratta infatti di un'analisi delle tabelle per il recupero di tutte le righe), lo eseguo una sola volta e memorizzo nella cache i dati. In questo modo, devo solo effettuare un "vero" richiamo ogni 10 minuti. Lo svantaggio è che, in caso di aggiunta di utenti durante tale intervallo di tempo, non saremo in grado di vederli in Selezione utenti finché la cache non scade e non richiamiamo di nuovo tutti i dati. Ricordate questo punto quando esaminate i risultati.
Dopo aver ottenuto il set di dati, posso eseguire StartsWith e limitare il numero di record restituiti. Per impostazione predefinita, SharePoint non visualizza più di 200 record in Selezione utenti, pertanto questo sarà il numero massimo di record che intendo richiedere quando il metodo viene chiamato. Lo sto tuttavia includendo come parametro, in modo che possiate procedere come preferite.
Classe di accesso alla coda
Questo francamente non è un componente particolarmente interessante. Si tratta di un insieme di metodi di base per aggiungere, leggere ed eliminare messaggi dalla coda.
Ruolo di lavoro di Azure
Non vi è molto da dire nemmeno a proposito del ruolo di lavoro. Si attiva ogni 10 secondi e verifica se vi siano nuovi messaggi nella coda. A tale scopo, chiama la classe di accesso alla coda. Se vi trova elementi, suddivide il contenuto (delimitato da punti e virgola) nelle relative parti costitutive, crea una nuova istanza della classe UniqueClaimValue e quindi tenta di aggiungere tale istanza alla tabella dei valori di attestazione univoci. Elimina quindi il messaggio dalla coda e passa all'elemento successivo finché non raggiunge il numero massimo di messaggi che possono essere letti in una volta (32) o finché non si esauriscono i messaggi.
Applicazione WCF
Come illustrato in precedenza, l'applicazione WCF è il componente con cui il codice di SharePoint interagisce per aggiungere elementi alla coda, ottenere l'elenco dei tipi di attestazione e ricercare o risolvere un valore di attestazione. Come un'applicazione attendibile ben funzionante, dispone di una relazione di trust con la farm di SharePoint da cui viene chiamata. Questo impedisce qualsiasi tipo di spoofing dei token durante la richiesta dei dati. A questo punto non vi è alcun meccanismo di sicurezza più specifico nell'applicazione WCF. Per completezza, quest'ultima è stata testata prima in un server Web locale e quindi spostata in Azure, dove è stata testata di nuovo allo scopo di verificare il funzionamento di tutti gli aspetti.
Ecco le nozioni fondamentali sui componenti di Azure di questa soluzione. Spero che queste informazioni di base vi siano utili per comprendere i diversi elementi coinvolti e il relativo utilizzo. Nella parte successiva parlerò del provider di attestazioni personalizzate di SharePoint e di come collegare tutti questi elementi per la nostra soluzione Extranet "chiavi in mano". Nei file allegati a questo post è contenuto tutto il codice sorgente per la classe di accesso ai dati, il progetto di test, il progetto di Azure, il ruolo di lavoro e i progetti WCF. Una copia di questo post è inoltre inclusa in un documento di Word, in modo che possiate comprendere i concetti che volevo esprimere prima dell'adattamento resosi necessario per il rendering di tale contenuto in questo sito.
Questo è un post di blog localizzato. L'articolo originale è disponibile in The Azure Custom Claim Provider for SharePoint Project Part 2.