Le fournisseur de revendications personnalisées Azure pour projet SharePoint - Partie 2

Article d’origine publié le mercredi 15 février 2012

Dans la Partie 1 de cette série de billets, j’ai brièvement décrit l’objectif de ce projet, qui, à un niveau élevé, est d’utiliser le stockage tabulaire Windows Azure comme un magasin de données pour un fournisseur de revendications personnalisées SharePoint. Le fournisseur de revendications va utiliser le kit CASI pour récupérer les données dont il a besoin dans Windows Azure afin de fournir les fonctionnalités de sélecteur de personnes (c’est-à-dire de carnet d’adresses) et de résolution de nom du contrôle de la saisie.

Dans la Partie 3, je crée tous les composants utilisés dans la batterie de serveurs SharePoint, notamment un composant personnalisé basé sur le kit CASI, qui gère toutes les communications entre SharePoint et Azure. Il y a également un composant WebPart personnalisé qui capture les informations sur les nouveaux utilisateurs et les place dans une file d’attente Azure. Enfin, il y a un fournisseur de revendications personnalisées qui communique avec le stockage tabulaire Azure via un serveur WCF - par le biais du composant personnalisé du kit CASI - pour activer le contrôle de la saisie et les fonctionnalités du sélecteur de personnes.

Maintenant, nous allons développer un peu plus ce scénario.

Ce type de solution s’adapte assez joliment à un scénario assez commun, qui est de disposer d’un extranet avec une gestion minime. Par exemple, vous souhaitez que vos partenaires ou clients puissent accéder à l’un de vos sites Web, demander un compte, et ensuite avoir la possibilité d’approvisionner automatiquement ce compte, sachant que le terme « approvisionner » peut signifier un tas de choses différentes selon les personnes. Ce cas de figure sera notre scénario de base dans ces billets, mais bien sûr, laissons nos ressources cloud publiques faire une partie du travail pour nous.

Commençons par lister les composants cloud que nous allons développer nous-mêmes :

  • Une table pour garder une trace de tous les types de revendications que nous allons prendre en charge
  • Une table pour garder une trace de toutes les valeurs de revendications uniques pour le sélecteur de personnes
  • Une file d’attente où envoyer les données qui doivent être ajoutées à la liste des valeurs de revendications uniques
  • Des classes d’accès aux données pour lire et écrire des données dans les tables Azure et pour écrire des données dans la file d’attente
  • Un rôle de travail Azure qui va lire les données de la file d’attente et remplir la table des valeurs de revendications uniques
  • Une application WCF qui sera le point de terminaison par lequel la batterie SharePoint communique pour obtenir la liste des types de revendications, rechercher les revendications, résoudre une revendication et ajouter des données à la file d’attente

Nous allons maintenant étudier chaque composant un peu plus en détail.

Table des types de revendications

La table des types de revendications va nous permettre de stocker tous les types de revendications que notre fournisseur de revendications personnalisées peut utiliser. Dans ce scénario, nous allons utiliser un seul type de revendication, qui est la revendication de l’identité – qui sera l’adresse de messagerie ici. Il est possible d’utiliser d’autres revendications, mais pour simplifier ce scénario, nous allons juste utiliser celle-ci. Dans le stockage tabulaire Azure, vous ajoutez des instances de classes à une table, il faut donc créer une classe pour décrire les types de revendications. Une fois de plus, notez que vous pouvez avoir des instances de différents types de classes dans la même table Azure, mais afin de ne pas complexifier les choses, nous n’allons pas le faire. La classe que va utiliser cette table ressemble à ceci :

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;

        }

    }

}

 

Je ne vais pas aborder toutes les notions de base de l’utilisation du stockage tabulaire Azure parce qu’il y a déjà beaucoup de ressources sur la question. Donc si vous voulez plus de détails sur les clés PartitionKey ou RowKey et savoir comment vous en servir, votre moteur de recherche convivial Bing local peut vous y aider. La seule chose qui mérite d’être soulignée ici est que j’effectue un codage URL de la valeur que je stocke pour la clé PartitionKey. Pourquoi cela ? Eh bien dans ce cas, ma clé PartitionKey est le type de revendication, qui peut prendre un certain nombre de formats : urn:foo:blabla, https://www.foo.com/blabla, etc. Dans le cas d’un type de revendication qui inclut des barres obliques, Azure ne peut pas stocker la clé PartitionKey avec ces valeurs. Donc, nous les encodons dans un format convivial compatible avec Azure. Comme je l’ai dit plus haut, dans notre cas nous utilisons la revendication de messagerie et donc le type de revendication est https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress.

Table des valeurs de revendications uniques

La table des valeurs de revendications uniques est l’endroit où toutes les valeurs de revendications uniques obtenues sont stockées. Dans notre cas, nous ne stockons qu’un seul type de revendication – la revendication de l’identité – donc par définition, toutes les valeurs de revendications vont être uniques. Cependant, j’ai adopté cette approche pour raisons d’extensibilité. Par exemple, supposons que vous vous décidiez à commencer à utiliser les revendications de rôle avec cette solution. Il ne serait alors pas logique de stocker la revendication de rôle « Employé » ou « Client » ou quelque autre que ce soit un millier de fois ; pour le sélecteur de personnes, on a seulement besoin de savoir si la valeur existe afin de la rendre disponible dans le sélecteur. Après cela, quiconque l’a, l’a une bonne fois pour toutes - il suffit de la rendre disponible lors de l’octroi des droits sur un site. Partant de cela, voici à quoi la classe qui va stocker les valeurs de revendications uniques ressemble :

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;

        }

    }

}

 

Il y a deux choses qu’il est utile de signaler ici. Tout d’abord, comme la classe précédente, PartitionKey utilise une valeur UrlEncoded car ce sera le type de revendication, qui contiendra des barres obliques. Deuxièmement, comme je le vois souvent dans l’utilisation du stockage tabulaire Azure, les données sont dénormalisées parce qu’il n’y a pas de concept de jointure comme il en existe dans SQL. Techniquement, vous pouvez faire une jointure dans LINQ, mais il y a tellement de choses existant dans LINQ qui ont été interdites pour le travail avec les données Azure (ou fonctionnent tellement mal) que je trouve qu’il est plus facile de simplement dénormaliser. Si vous avez d’autres idées sur la question, mentionnez-les dans les commentaires – je serais curieux de connaître votre opinion. Dans notre cas, le nom d’affichage sera donc « Email », parce que c’est le type de revendication que nous stockons dans cette classe.

La file d’attente des revendications

La file d’attente des revendications est assez simple : nous allons stocker les demandes de « nouveaux utilisateurs » dans cette file d’attente, ensuite un processus de travail Azure les lira à partir de la file d’attente et déplacera les données vers la table des valeurs de revendications uniques. La raison principale de cela est que travailler avec le stockage tabulaire Azure peut parfois être assez latent, alors que placer un élément dans une file d’attente est assez rapide. Grâce à cette approche, nous pouvons minimiser l’impact sur notre site Web SharePoint.

Classes d’accès aux données

L’un des aspects plutôt banals du travail avec les files d’attente et le stockage tabulaire Azure est qu’il vous faut toujours écrire votre propre classe d’accès aux données. Pour le stockage en table, vous devez écrire une classe de contexte de données et une classe de source de données. Je ne vais pas passer beaucoup de temps sur cela parce que vous pouvez lire des tartines à ce sujet sur le Web ; par ailleurs, j’attache aussi le code source de mon projet Azure à ce billet pour que vous puissiez vous en servir à votre guise.

Il y a une chose importante que je voudrais souligner ici, qui est juste un choix de style personnel. J’aime bien mettre tout mon code d’accès aux données Azure dans un projet distinct. De cette façon, je peux le compiler dans son propre assembly, et je peux l’utiliser même à partir de projets non-Azure. Par exemple, dans l’exemple de code que je fournis, vous trouverez une application de formulaire Windows que j’ai utilisée pour tester les différentes parties du traitement dorsal Azure. Elle ne sait rien d’Azure, sauf qu’elle possède une référence à certains assemblys d’Azure et à mon assembly d’accès aux données. Je peux l’utiliser dans le cadre de ce projet et tout aussi facilement dans mon projet WCF pour le traitement frontal de l’accès aux données SharePoint.

Voici quelques détails sur les classes d’accès aux données :

  • ·         J’ai une classe « conteneur » distincte pour les données que je vais retourner – les types de revendications et les valeurs de revendications uniques. Ce que je veux dire par classe conteneur, c’est que j’ai une classe simple avec une propriété publique du type liste. Je retourne cette classe lorsque des données sont demandées, plutôt qu’une simple liste de résultats. En effet, quand je retourne une liste à partir d’Azure, le client n’obtient que le dernier élément de la liste (si vous faites la même chose à partir d’un WCF hébergé localement, cela fonctionne très bien). Donc pour contourner ce problème, je retourne les types de revendications dans une classe qui ressemble à ceci :

namespace AzureClaimsData

{

    public class ClaimTypeCollection

    {

        public List<ClaimType> ClaimTypes { get; set; }

 

        public ClaimTypeCollection()

        {

            ClaimTypes = new List<ClaimType>();

        }

 

    }

}

 

Et la classe qui retourne les valeurs de revendications uniques ressemble à ceci :

namespace AzureClaimsData

{

    public class UniqueClaimValueCollection

    {

        public List<UniqueClaimValue> UniqueClaimValues { get; set; }

 

        public UniqueClaimValueCollection()

        {

            UniqueClaimValues = new List<UniqueClaimValue>();

        }

    }

}

 

 

  • ·         Les classes de contexte de données sont assez simples – rien de vraiment brillant (comme dirait mon ami Vesa) :

 

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

            {

                //c’est là que vous configurez le nom de la table dans le stockage Azure

                //avec laquelle vous allez travailler

                return this.CreateQuery<ClaimType>(CLAIM_TYPES_TABLE);

            }

        }

 

    }

}

 

  • ·         Dans les classes de source de données, j’adopte une approche légèrement différente pour établir la connexion à Azure. La plupart des exemples que je vois sur le Web lisent les informations d’identification avec une classe de paramètres d’inscription (ce n’est pas son nom exact, mais je ne m’en souviens pas). Le problème de cette approche ici est que je n’ai aucun contexte spécifique à Azure puisque je veux que ma classe de données fonctionne à l’extérieur d’Azure. Je crée donc juste un paramètre dans les propriétés de mon projet et j’y inclus le nom et la clé du compte nécessaires pour se connecter à mon compte Azure. Donc, mes deux classes de source de données ont un code qui ressemble à ceci pour créer cette connexion au stockage Azure :

 

        private static CloudStorageAccount storageAccount;

        private ClaimTypeDataContext context;

 

 

        //constructeur statique donc ne se déclenche qu’une fois

        static ClaimTypesDataSource()

        {

            try

            {

                //obtenir les infos de connexion au compte de stockage

                string storeCon = Properties.Settings.Default.StorageAccount;

 

                //extraire les infos du compte

                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;

            }

        }

 

 

        //nouveau constructeur

        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("Erreur lors de la construction de la classe ClaimTypesDataSource : " + ex.Message);

                throw;

            }

        }

 

  • ·         L’implémentation réelle des classes de source de données inclut une méthode pour ajouter un nouvel élément pour un type de revendication ainsi que pour une valeur de revendication unique. C’est un code très simple qui ressemble à ceci :

 

        //ajouter un nouvel élément

        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("Erreur lors de l’ajout d’un nouveau type de revendication : " + ex.Message);

                ret = false;

            }

 

            return ret;

        }

 

Une différence importante à noter dans la méthode Add de la source de données des valeurs de revendications uniques est qu’elle ne génère pas d’erreur ni retourne la valeur false en cas d’exception lors de l’enregistrement des modifications. C’est parce que je m’attends tout à fait à ce que des personnes, par erreur ou pour tout autre motif, essaient de s’inscrire plusieurs fois. Une fois nous avons un enregistrement de leur revendication de messagerie, toute tentative ultérieure d’ajout génèrera une exception. Puisque Azure ne fournit pas des exceptions fortement typées, et que je ne veux pas que le journal de suivi se remplisse d’infos inutiles, lorsque cette situation se produit, je ne m’en soucie guère.

  • ·         La recherche de revendications est un peu plus intéressante, dans la mesure où elle expose à nouveau certaines choses que vous pouvez faire dans LINQ, mais pas dans LINQ avec Azure. J’ajoute le code ici et expliquerai ensuite certains choix que j’ai faits :

 

        public UniqueClaimValueCollection SearchClaimValues(string ClaimType, string Criteria, int MaxResults)

        {

            UniqueClaimValueCollection results = new UniqueClaimValueCollection();

            UniqueClaimValueCollection returnResults = new UniqueClaimValueCollection();

 

            const int CACHE_TTL = 10;

 

            try

            {

                //rechercher l’ensemble des valeurs de revendications en cache

                if (HttpRuntime.Cache[ClaimType] != null)

                    results = (UniqueClaimValueCollection)HttpRuntime.Cache[ClaimType];

                else

                {

                    //pas en cache donc interroger Azure

 

                    //Azure ne prend pas en charge "commence par", donc extraire toutes les données pour le type de revendication

                    var values = from UniqueClaimValue cv in this.context.UniqueClaimValues

                                  where cv.PartitionKey == System.Web.HttpUtility.UrlEncode(ClaimType)

                                  select cv;

 

                    //il faut l’assigner d’abord pour pouvoir exécuter la requête et renvoyer les résultats

                    results.UniqueClaimValues = values.ToList();

 

                    //stocker en cache

                    HttpRuntime.Cache.Add(ClaimType, results, null,

                        DateTime.Now.AddHours(CACHE_TTL), TimeSpan.Zero,

                        System.Web.Caching.CacheItemPriority.Normal,

                        null);

                }

 

                //maintenant une requête basée sur des critères, pour max de résultats

                returnResults.UniqueClaimValues = (from UniqueClaimValue cv in results.UniqueClaimValues

                           where cv.ClaimValue.StartsWith(Criteria)

                           select cv).Take(MaxResults).ToList();

            }

            catch (Exception ex)

            {

                Trace.WriteLine("Erreur lors de la recherche des valeurs de revendications : " + ex.Message);

            }

 

            return returnResults;

        }

 

La première chose à noter est que vous ne pouvez pas utiliser StartsWith avec les données Azure. Cela signifie donc que vous devez récupérer toutes les données localement, puis utiliser votre expression StartsWith. Comme l’extraction des données peut être une opération coûteuse (c’est effectivement une analyse de table pour récupérer toutes les lignes), je fais cela une fois puis je mets en cache les données. De cette façon, il me suffit de faire un rappel « réel » toutes les 10 minutes. L’inconvénient est que, si des utilisateurs sont ajoutés durant cette période, ils n’apparaîtront pas dans le sélecteur de personnes jusqu’à l’expiration du cache où nous récupérons à nouveau toutes les données. N’oubliez pas cela lorsque vous examinez les résultats.

Une fois que j’ai vraiment mon ensemble de données, je peux effectuer StartsWith et limiter aussi la quantité d’enregistrements retournés. Par défaut, SharePoint n’affiche pas plus de 200 enregistrements dans le sélecteur de personnes, ce sera donc la quantité maximum que je prévois de demander lorsque cette méthode est appelée. Mais je l’inclus en tant que paramètre ici pour que vous puissiez faire ce que vous voulez.

La classe d’accès à la file d’attente

Honnêtement il n’y a rien de très intéressant ici. Quelques méthodes de base seulement pour ajouter, lire et supprimer des messages dans la file d’attente.

Rôle de travail Azure

Le rôle de travail n’est pas non plus très intéressant. Il se réveille toutes les 10 secondes et regarde s’il y a de nouveaux messages dans la file d’attente. Il effectue cela en appelant la classe d’accès à la file d’attente. Si elle trouve des éléments, elle ventile le contenu extrait (délimité par point-virgule) dans ses éléments constitutifs, crée une nouvelle instance de la classe UniqueClaimValue et tente ensuite d’ajouter cette instance à la table des valeurs de revendications uniques. Après cela, elle supprime le message de la file d’attente et passe à l’élément suivant, jusqu’à atteindre le nombre maximal de messages pouvant être lus en une fois (32) ou qu’il n’y plus aucun message restant.

Application WCF

Comme décrit précédemment, c’est avec l’application WCF que le code SharePoint communique pour ajouter des éléments à la file d’attente, obtenir la liste des types de revendications et rechercher ou résoudre une valeur de revendication. Comme une bonne application de confiance, une approbation est établie entre elle et la batterie SharePoint qui l’appelle. Cela empêche tout type d’usurpation de jeton lors de l’interrogation des données. À ce stade, il n’existe pas de sécurité plus fine implémentée dans l’application WCF elle-même. Par souci d’exhaustivité, l’application WCF a été testée d’abord sur un serveur Web local, puis déplacée vers Azure où elle a été à nouveau testée pour vérifier que tout fonctionne.

Voici donc les principes de base des composants Azure de cette solution. J’espère que cela explicite les différentes parties agissantes et comment elles sont utilisées. Dans la prochaine partie, j’aborderai le fournisseur de revendications personnalisées SharePoint et montrerai comment raccorder tous ces morceaux pour former la solution extranet « clé en main ». Les fichiers joints à ce billet contiennent tout le code source de la classe d’accès aux données, du projet test, du projet Azure, du rôle de travail et des projets WCF. Il y a aussi une copie de ce billet au format Word, pour que vous puissiez réellement comprendre mon intention avec un contenu non massacré par l’affichage sur le site.

Ce billet de blog a été traduit de l’anglais. Vous trouverez la version originale ici The Azure Custom Claim Provider for SharePoint Project Part 2