Article d’origine publié le dimanche 7 août 2011

C’est un fait, jamais je n’avais rédigé un billet aussi long. Il faut dire que j’avais à cœur de traiter à fond la question des technologies concernées. C’est un domaine qui, ces derniers temps, fait beaucoup parler. De fait, ces discussions tournent autour de la question suivante : comment obtenir, pour un utilisateur de revendications SAML donné, un contexte Windows afin d’accéder à une autre application. Si SharePoint 2010 offre une prise en charge limitée du service d’émission de jetons Revendications vers Windows (appelé ici c2wts), cela est vrai uniquement pour les utilisateurs de revendications Windows qui exécutent un petit nombre d’applications de service. J’entends souvent la question suivante : pourquoi ne puis-je pas utiliser des utilisateurs de revendications SAML avec une revendication UPN valide ? La raison est d’ordre technologique. Ainsi, entre la limitation liée aux types d’authentification et celle des applications de service qui peuvent l’utiliser, vous pouvez très bien vous trouver dans une situation où vous devez trouver un moyen de connecter les utilisateurs SAML à d’autres applications avec leur compte Windows sous-jacent. J’espère que ce billet vous aidera à comprendre globalement comment réaliser cela.

L’approche de base de ce scénario consiste à créer une application de services WCF qui traite toutes les demandes de données formulées par les utilisateurs finals à partir de l’autre application, en l’occurrence, SQL Server. Je souhaite donc prendre un utilisateur SAML qui accède au site SharePoint et je formule une demande pour le compte Windows de cet utilisateur SAML au moment où je récupère les données de SQL Server. Remarque : bien que cet article porte sur les utilisateurs de revendications SAML, la même méthodologie peut être employée à l’identique pour les utilisateurs de revendications Windows ; ils obtiennent une revendication UPN par défaut lorsqu’ils se connectent. Le schéma suivant illustre l’ensemble du processus :

Configuration de SQL Server

Commençons du côté de SQL Server. Dans mon scénario, SQL Server s’exécute sur un serveur appelé « SQL2 ». Le service SQL proprement dit s’exécute en tant que service réseau. Autrement dit, je n’ai pas besoin de lui créer de SPN ; s’il s’exécutait en tant que compte de domaine, je devrais créer un SPN pour ce compte de service pour MSSQLSvc. Dans le cadre de ce scénario, je vais utiliser l’ancienne base de données Les Comptoirs (« Northwind », dans le code) pour la récupération de données. Comme je souhaitais pouvoir identifier facilement l’identité de l’utilisateur qui formule la demande, j’ai modifié la procédure stockée Ten Most Expensive Products comme suit :

 

CREATE procedure [dbo].[TenProductsAndUser] AS

SET ROWCOUNT 10

SELECT Products.ProductName AS TenMostExpensiveProducts, Products.UnitPrice, SYSTEM_USER As CurrentUser

FROM Products

ORDER BY Products.UnitPrice DESC

 

Le point principal à noter ici est que j’ai ajouté SYSTEM_USER à l’instruction SELECT, ce qui a simplement pour effet de renvoyer l’utilisateur actif dans la colonne. Ainsi, lorsque j’exécute une requête et que j’obtiens les résultats en retour, le nom de l’utilisateur actif figure dans une colonne de la grille. De ce fait, je peux facilement déterminer si la requête a bien été exécutée sous l’identité de l’utilisateur actif. Dans ce scénario, j’ai accordé le droit d’exécuter cette procédure stockée à trois utilisateurs Windows ; aucun autre utilisateur ne peut le faire (cela donnera aussi une sortie finale intéressante).

Création de l’application de services WCF

L’étape suivante qui j’ai exécutée visait à créer une application de services WCF Services pour récupérer les données de SQL. J’ai suivi les recommandations que j’ai eu l’occasion de décrire dans la 2ème partie de l’article Kit CASI (http://blogs.msdn.com/b/sharepoint_fr/archive/2010/12/14/kit-de-ressources-casi-160-160-partie-160-2.aspx), le but étant d’établir l’approbation entre la batterie de serveurs SharePoint et l’application WCF. Cela était nécessaire pour pouvoir obtenir les revendications de l’utilisateur formulant la demande. Il est déconseillé de transmettre simplement la valeur de revendication UPN en tant que paramètre car, par exemple, quiconque pourrait usurper l’identité d’une autre personne rien qu’en transmettant une valeur de revendication UPN différente. Une fois l’approbation correctement configurée entre l’application WCF et SharePoint, je peux enchaîner et écrire une méthode destinée à :

  • extraire la revendication UPN ;
  • emprunter l’identité de l’utilisateur utilisant les services c2wts ;
  • récupérer les données de SQL sous l’identité de cet utilisateur.

 

Pour ce faire, voici le code que j’ai utilisé :

 

//informations ajoutées pour cet exemple de code :

using Microsoft.IdentityModel;

using Microsoft.IdentityModel.Claims;

using System.Data;

using System.Data.SqlClient;

using System.Security.Principal;

using Microsoft.IdentityModel.WindowsTokenService;

using System.ServiceModel.Security;

 

 

public DataSet GetProducts()

{

 

   DataSet ds = null;

 

   try

   {

       string conStr = "Data Source=SQL2;Initial Catalog=

       Northwind;Integrated Security=True;";

 

       //demande de l’identité basée sur les revendications actuelles

       IClaimsIdentity ci =

          System.Threading.Thread.CurrentPrincipal.Identity as IClaimsIdentity;

 

       //vérification que la demande était associée à une identité basée sur des revendications

       if (ci != null)

       {

          //vérification pour déterminer s’il existe des revendications avant de continuer

          if (ci.Claims.Count > 0)

          {

              //recherche de la revendication UPN

              var eClaim = from Microsoft.IdentityModel.Claims.Claim c in ci.Claims

              where c.ClaimType == System.IdentityModel.Claims.ClaimTypes.Upn

              select c;

 

              //en cas de correspondance, obtention de la valeur pour la connexion

              if (eClaim.Count() > 0)

              {

                 //obtention de la valeur de revendication upn

                 string upn = eClaim.First().Value;

 

                 //création de l’identité Windows pour l’emprunt d’identité

                 WindowsIdentity wid = null;

 

                 try

                 {

                     wid = S4UClient.UpnLogon(upn);

                 }

                 catch (SecurityAccessDeniedException adEx)

                 {

                           Debug.WriteLine("Impossible de mapper la revendication upn à " +

                     "une identité Windows valide : " + adEx.Message);

                 }

 

                 //vérification de l’établissement de la connexion

                 if (wid != null)

                 {

                        using (WindowsImpersonationContext ctx = wid.Impersonate())

                    {

                       //demande des données auprès de SQL Server

                        using (SqlConnection cn = new SqlConnection(conStr))

                        {

                           ds = new DataSet();

                           SqlDataAdapter da =

                               new SqlDataAdapter("TenProductsAndUser", cn);

                           da.SelectCommand.CommandType =

                               CommandType.StoredProcedure;

                           da.Fill(ds);

                        }

                     }

                 }

              }

          }

       }

   }

   catch (Exception ex)

   {

       Debug.WriteLine(ex.Message);

   }

 

   return ds;

}

 

En fin de compte, ce code n’est pas très compliqué. Voici un bref récapitulatif du déroulement des opérations. Dans un premier temps, je vérifie que nous disposons d’un contexte d’identité par revendications valide. Si c’est le cas, j’interroge la liste des revendications pour rechercher la revendication UPN. En supposant que je la trouve, j’en extrais la valeur et j’appelle c2wts pour établir une connexion S4U sous l’identité de cet utilisateur. Si cette connexion aboutit, une identité Windows est renvoyée. À partir de cette identité Windows, je crée un contexte d’emprunt d’identité. Une fois que l’identité de l’utilisateur a été empruntée, je crée ma connexion à SQL Server et récupère les données. Je vous invite maintenant à prendre note de ces quelques conseils rapides de dépannage :

  1. Si vous n’avez pas configuré les services c2wts pour autoriser votre pool d’applications à les utiliser, vous obtiendrez une erreur qui est interceptée dans le bloc catch extérieur.  L’erreur s’apparente au message suivant : « WTS0003 : L’appelant n’est pas autorisé à accéder au service. » Je vous donnerai des indications et un lien pour configurer les services c2wts ci-après.
  2. Si la délégation Kerberos contrainte n’est pas correctement configurée, au moment d’exécuter la procédure stockée avec la ligne de code da.Fill(ds);, elle lèvera une exception qui indique qu’un utilisateur anonyme ne dispose pas des droits permettant d’exécuter cette procédure stockée. Vous trouverez ci-après quelques conseils pour configurer la délégation contrainte pour ce scénario.

Configuration des services C2WTS

Les services c2wts sont configurés par défaut pour démarrer manuellement et pour empêcher quiconque de les utiliser. Je les ai modifiés de sorte qu’ils démarrent automatiquement et que le pool d’applications de mon application de services WCF soit autorisé à les utiliser. Plutôt que d’entrer dans les détails de la configuration de cette autorisation, je vous invite à lire cet article ; les informations de configuration se trouvent à la fin de celui-ci : http://msdn.microsoft.com/en-us/library/ee517258.aspx.  Vous n’avez rien d’autre à faire pour commencer.  Pour obtenir des informations générales sur c2wts, je vous recommande également de consulter l’article suivant : http://msdn.microsoft.com/en-us/library/ee517278.aspx.

 

Remarque : ce dernier article comporte une erreur MONUMENTALE ; il recommande de créer une dépendance pour les services c2wts en exécutant ce code :  sc config c2wts depend=cryptosvcNE FAITES PAS CETTE ERREUR !!  Il s’agit d’une coquille : « cryptosvc » n’est pas un nom de service valide, du moins, sous Windows Server 2008 R2. Si vous suivez cette recommandation, vos services c2wts ne démarreront plus, car le code indique que la dépendance est marquée pour suppression ou qu’elle est introuvable. Je me suis trouvé dans cette situation et j’ai modifié la dépendance en indiquant la valeur iisadmin (ce qui est logique, puisque dans mon cas du moins, mon hôte WCF doit s’exécuter automatiquement pour utiliser c2wts) ; sinon, j’aurais été bloqué.

Configuration de la délégation Kerberos contrainte

Tout d’abord, je tiens à rassurer ceux et celles qui seraient intimidés par le sujet :

  1. Je ne vais pas entrer dans des détails pratiques expliquant comment faire fonctionner la délégation Kerberos contrainte. Il existe des tas de références sur la question.
  2. Quoi qu’il en soit, cette partie ne m’a posé aucun problème particulier au moment de la rédiger.

 

Faisons l’inventaire de ce qui est nécessaire à la délégation. Primo, comme je l’ai précisé plus haut, mon service SQL Server s’exécute en tant que service réseau. Je n’ai donc pas besoin de faire quoi que ce soit à ce niveau-là. Secundo, mon pool d’applications WCF s’exécute en tant que compte de domaine appelé vbtoys\portal. J’ai donc ici deux choses à faire :

  1. Lui créer un SPN HTTP utilisant le nom NetBIOS et le nom complet du serveur à partir duquel il assurera la délégation. Dans mon cas, mon serveur WCF est appelé AZ1. J’ai donc créé deux SPN qui se présentaient comme suit : 
    1. setspn -A HTTP/az1 vbtoys\portal
    2. setspn -A HTTP/az1.vbtoys.com vbtoys\portal
  2. Configurer mon compte de sorte qu’il soit approuvé pour la délégation Kerberos contrainte vers les services SQL Server s’exécutant sur le serveur « SQL2 ». Pour ce faire, sur le contrôleur de domaine, j’ai ouvert Utilisateurs et ordinateurs Active Directory, double-cliqué sur l’utilisateur vbtoys\portal, puis cliqué sur l’onglet Délégation pour configurer cette approbation. Je l’ai configuré pour approuver la délégation pour certains services uniquement en utilisant un type de protocole d’authentification quelconque. Voici un lien vers une image illustrant la configuration de la délégation :

 

Tertio, j’ai dû configurer mon serveur d’applications WCF de sorte qu’il soit approuvé pour la délégation contrainte. Heureusement, le processus est exactement le même que celui que j’ai décrit précédemment pour l’utilisateur ; il suffit de rechercher le compte d’ordinateur dans Utilisateurs et ordinateurs Active Directory et de le configurer. Voici un lien vers une image illustrant sa configuration :

 

 

Et avec cela, tous les composants non SharePoint sont configurés et prêts à l’emploi. Le dernier élément nécessaire est un composant WebPart à des fins de test.

Création du composant WebPart SharePoint

La création du composant WebPart est assez simple ; j’ai juste suivi le schéma que j’ai décrit précédemment pour effectuer des appels WCF à destination de SharePoint et transmettre l’identité de l’utilisateur actif (http://blogs.technet.com/b/speschka/archive/2010/09/08/calling-a-claims-aware-wcf-service-from-a-sharepoint-2010-claims-site.aspx). J’aurais pu également utiliser le kit CASI pour établir la connexion et appeler WCF, mais j’ai décidé de procéder manuellement pour, si je puis dire, faciliter la démonstration. Pour créer le composant WebPart, exécutez les étapes de base suivantes :

  1. Créez un projet SharePoint 2010 dans Visual Studio 2010.
  2. Créez une référence de service à mon application de services WCF.
  3. Ajoutez un nouveau composant WebPart.
  4. Ajoutez le code au composant WebPart pour récupérer les données à partir de WCF et les afficher dans une grille.
  5. Ajoutez toutes les informations du fichier app.config généré dans le projet Visual Studio à la section <system.ServiceModel> du fichier web.config de l’application Web dans laquelle le composant WebPart va être hébergé.

Remarque : le fichier app.config contient un attribut appelé decompressionEnabled ; vous DEVEZ LE SUPPRIMER AVANT D’AJOUTER LES INFORMATIONS AU FICHIER WEB.CONFIG. Si vous le conservez, le composant WebPart lèvera une erreur lors de la tentative de création d’une instance de votre proxy de référence de service.

En ce qui concerne les étapes précédentes, aucune ne présente de difficultés notoires hormis l’étape n °4. Je ne m’attarderai donc pas sur les autres. Voici toutefois le code du composant WebPart :

private DataGrid dataGrd = null;

private Label statusLbl = null;

 

 

protected override void CreateChildControls()

{

   try

   {

       //création de la connexion à WCF et tentative de récupération des données

       SqlDataSvc.SqlDataClient sqlDC = new SqlDataSvc.SqlDataClient();

 

       //configuration du canal de sorte que nous puissions l’appeler avec FederatedClientCredentials

       SPChannelFactoryOperations.ConfigureCredentials<SqlDataSvc.ISqlData>(

       sqlDC.ChannelFactory, Microsoft.SharePoint.SPServiceAuthenticationMode.Claims);

 

       //création du point de terminaison auquel se connecter

       EndpointAddress svcEndPt =

          new EndpointAddress("https://az1.vbtoys.com/ClaimsToSqlWCF/SqlData.svc");

 

       //création d’un canal à destination du point de terminaison WCF en utilisant le

       //jeton et les revendications de l’utilisateur actif

       SqlDataSvc.ISqlData sqlData =

          SPChannelFactoryOperations.CreateChannelActingAsLoggedOnUser

          <SqlDataSvc.ISqlData>(sqlDC.ChannelFactory, svcEndPt);

 

       //demande des données

       DataSet ds = sqlData.GetProducts();

 

       if ((ds == null) || (ds.Tables.Count == 0))

       {

          statusLbl = new Label();

          statusLbl.Text = "Aucune donnée renvoyée à " + DateTime.Now.ToString();

          statusLbl.ForeColor = System.Drawing.Color.Red;

          this.Controls.Add(statusLbl);

       }

       else

       {

          dataGrd = new DataGrid();

          dataGrd.AutoGenerateColumns = true;

          dataGrd.DataSource = ds.Tables[0];

          dataGrd.DataBind();

          this.Controls.Add(dataGrd);

       }

   }

   catch (Exception ex)

   {

       Debug.WriteLine(ex.Message);

   }

}

 

À nouveau, je pense que cela se passe d’explication. La première partie consiste à établir la connexion au service WCF de telle sorte que les revendications de l’utilisateur actuel soient transmises ; pour plus d’informations, suivez le lien ci-dessus permettant d’accéder à mon précédent billet sur la question. Ensuite, il s’agit simplement d’obtenir un dataset en retour et de le lier à une grille s’il existe des données ou d’afficher une étiquette qui indique qu’il n’y a pas de données en cas d’échec. Pour illustrer le fonctionnement combiné de ces différents éléments, vous trouverez ci-dessous trois captures d’écran : les deux premières les montrent en action pour deux utilisateurs différents, que vous pouvez identifier dans la colonne CurrentUser. La troisième les montre pour un utilisateur qui n’a pas reçu les droits permettant d’exécuter la procédure stockée.

 

 

C’est ici que prend fin ce billet ; vous trouverez ci-joint le code pour l’application de services WCF et le composant WebPart, ainsi que le document Word d’origine dans lequel j’ai rédigé ce billet pour échapper au format peu séduisant de ces publications.

Ce billet de blog a été traduit de l’anglais. Vous trouverez la version originale sur Using SAML Claims, SharePoint, WCF, Claims to Windows Token Service and Constrained Delegation to Access SQL Server