Artículo original publicado el domingo, 7 de agosto de 2011

Bueno, espero que esta sea la entrada con el título más largo que vaya a escribir jamás, pero quería asegurarme de cubrir todas las tecnologías de las que vamos a hablar. Esta es un área sobre la que ha habido mucho ruido últimamente, y trata sobre cómo poder tomar un usuario de notificaciones de SAML y hacer que un contexto de Windows acceda a alguna otra aplicación. SharePoint 2010 tiene una compatibilidad limitada para las notificaciones del servicio de token de Windows (que llamaremos NSTW), pero solo para los usuario de notificaciones de Windows con un reducido número de aplicaciones de servicios. Una pregunta común es, ¿por qué no se puede utilizar usuarios de notificaciones de SAML con una notificación de UPN válida? Y es que, en realidad, no hay ninguna razón tecnológica por la que no pueda hacerlo. Por lo tanto, entre la limitación de los tipos de autenticación así como de las aplicaciones de servicio que pueden utilizarlo, es muy posible que se encuentre en una posición en la que tenga que crear una manera de conectar los usuarios de notificaciones de SAML a otras aplicaciones como su cuenta de Windows subyacente. Espero que esta entrada le ayude a comprender los conceptos básicos sobre cómo puede hacerse.

El enfoque básico que va a tomar este escenario es la creación de una aplicación de servicios de WCF que procese todas las solicitudes de datos de los usuarios finales desde la otra aplicación, que en nuestro caso es SQL Server. Tengo que tomar un usuario de SAML que esté accediendo al sitio SharePoint y realizar solicitudes como la cuenta de Windows de dicho usuario de SAML cuando recupere datos desde SQL Server. NOTA: A pesar de que este artículo trata sobre usuarios de notificaciones de SAML, puede utilizarse exactamente la misma metodología para los usuarios de notificaciones de Windows; obtienen una notificación de UPN de forma predeterminada cuando inician sesión. He aquí un diagrama de cómo es el proceso:

Configuración de SQL Server

Comencemos por el lado de SQL Server. En mi escenario, SQL Server se ejecuta en un servidor denominado “SQL2”. El propio servicio SQL se ejecuta como servicio de red. Esto significa que no necesito crear un SPN; si estuviera ejecutándose como cuenta de dominio, entonces tendría que crear un SPN de esa cuenta de servicio para MSSQLSvc. En este escenario en particular, voy a utilizar la antigua base de datos Northwinds para recuperar datos. Quiero demostrar fácilmente la identidad del usuario que está realizando la solicitud, de modo que he modificado el procedimiento almacenado Ten Most Expensive Products (los diez productos más caros) para que sea así:

 

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

 

Lo más importante a tener en cuenta aquí es que he agregado SYSTEM_USER a la instrucción SELECT; eso hace que se devuelva el usuario actual de la columna. Eso significa que cuando ejecuto una consulta y obtengo los resultados, veo una columna en mi cuadrícula que contiene el nombre del usuario actual para poder ver fácilmente si la consulta se ejecutó como la identidad del usuario actual o no. En este escenario concreto, otorgué a tres usuarios de Windows los derechos de ejecutar este procedimiento almacenado; ningún otro usuario podrá hacerlo (lo cual también será un ejemplo útil del resultado final).

Creación de la aplicación de servicios de WCF

Lo siguiente que hice fue crear una aplicación de servicios de WCF que recuperaba datos desde SQL. Seguí las instrucciones que describí anteriormente en la parte 2 de la entrada CASI Kit (http://blogs.msdn.com/b/sharepoint_sp/archive/2010/12/16/parte-2-del-kit-de-herramientas-de-integraci-243-n-de-notificaciones-azure-y-sharepoint.aspx); lo hice para establecer la confianza entre la granja de servidores de SharePoint y la aplicación de WCF. Eso era necesario para poder obtener las notificaciones del usuario que realizaba la solicitud. No convendría simplemente pasar el valor de la notificación de UPN como parámetro, por ejemplo, ya que en ese caso cualquiera podría suplantar la identidad (spoofing) de cualquier otra persona simplemente pasando un valor de notificación de UPN diferente. Una vez esté configurada la confianza correctamente entre WCF y SharePoint, podré seguir adelante y escribir el método, que hará lo siguiente:

  • Extraer la notificación de UPN
  • Representar al usuario que utiliza NSTW
  • Recuperar los datos desde SQL como ese usuario

 

He aquí el código que utilicé para hacerlo:

 

//se ha agregado lo siguiente para este ejemplo de código

utilizando Microsoft.IdentityModel;

utilizando Microsoft.IdentityModel.Claims;

utilizando System.Data;

utilizando System.Data.SqlClient;

utilizando System.Security.Principal;

utilizando Microsoft.IdentityModel.WindowsTokenService;

utilizando System.ServiceModel.Security;

 

 

public DataSet GetProducts()

{

 

   DataSet ds = null;

 

   try

   {

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

       Northwind;Integrated Security=True;";

 

       //pedir la identidad de notificaciones actual

       IClaimsIdentity ci =

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

 

       //asegúrese de que la solicitud tenía una identidad de notificaciones adjunta

       if (ci != null)

       {

          //compruebe si hay notificaciones presentes antes de ejecutar esto

          if (ci.Claims.Count > 0)

          {

              //busque la notificación de UPN

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

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

              select c;

 

              //si obtiene una coincidencia, obtenga el valor para iniciar sesión

              if (eClaim.Count() > 0)

              {

                 //obtenga el valor de notificación de UPN

                 string upn = eClaim.First().Value;

 

                 //cree la WindowsIdentity para la representación

                 WindowsIdentity wid = null;

 

                 try

                 {

                     wid = S4UClient.UpnLogon(upn);

                 }

                 catch (SecurityAccessDeniedException adEx)

                 {

                           Debug.WriteLine("No se pudo asignar la notificación UPN a " +

                     "una identidad de Windows válida: " + adEx.Message);

                 }

 

                 //compruebe si ha iniciado sesión correctamente

                 if (wid != null)

                 {

                        using (WindowsImpersonationContext ctx = wid.Impersonate())

                    {

                       //solicite datos desde 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;

}

 

Al fin y al cabo no es un código muy complicado; he aquí una explicación breve del mismo. Lo primero que hago es asegurarme de que hay un contexto válido de identidad de notificaciones y, si es así, hago una consulta de la lista de notificaciones para buscar la notificación de UPN. Suponiendo que encuentro la notificación de UPN, extraigo el valor de la misma y llamo a las NSTW para que realicen un inicio de sesión S4U como ese usuario. Si el inicio de sesión es correcto, devolverá una WindowsIdentity. A continuación, tomo esa WindowsIdentity y creo un contexto de representación. Una vez me encuentre representando al usuario, creo mi conexión a SQL Server y recupero los datos. He aquí algunos consejos para solucionar problemas a tener en cuenta:

  1. Si no ha configurado las NSTW para que el grupo de aplicaciones pueda utilizarlas, obtendrá un error que está atrapado en el bloque de filtrado de fuera. El error será algo así como “WTS0003: el autor de la llamada no está autorizado a acceder al servicio”. A continuación, proporcionaré los detalles y un vínculo para configurar las NSTW.
  2. Si la delegación restringida de Kerberos no está configurada correctamente, cuando intente ejecutar el procedimiento almacenado con la línea de código da.Fill(ds);, se producirá una excepción que dice que el usuario anónimo no tiene permiso para ejecutar este procedimiento almacenado. A continuación proporcionaré algunos consejos para configurar la delegación restringida para este escenario.

Configuración de NSTW

Las NSTW están configuradas de forma predeterminada para a) iniciarse manualmente y b) no permitir a nadie que las utilice.  Lo he cambiado para que a) se inicie automáticamente y b) el grupo de aplicaciones de mi aplicación de servicios de WCF esté autorizado para utilizarlas. En lugar de entrar en detalles sobre cómo configurar esta autorización, recomiendo que lea este artículo; la información de configuración está al final: http://msdn.microsoft.com/en-us/library/ee517258.aspx. Eso es todo lo que necesita para seguir adelante. Para obtener más información de fondo sobre NSTW, también recomiendo que consulte http://msdn.microsoft.com/en-us/library/ee517278.aspx.

 

NOTA: Hay un ENORME error en este último artículo; en él se recomienda que cree una dependencia para las NSTW mediante la ejecución de este código:  sc config c2wts depend=cryptosvc¡NO LO HAGA! Hay un error de escritura y “cryptosvc” no es un nombre de servicio válido, al menos no en Windows Server 2008 R2. Si lo hace, las NSTW dejarán de iniciarse porque dirá que la dependencia está marcada para su eliminación o que no puede encontrarse. Yo mismo me encontré en esa situación y cambié la dependencia por iisadmin (lo que es lógico porque en mi case mi host de WCF tiene que estar ejecutándose para que pueda utilizar NSTW); si no, se estancaba.

Configuración de la delegación restringida de Kerberos

Bueno, antes de que nadie se asuste por este tema, déjenme que diga esto:

  1. No voy a meterme en el meollo de la cuestión sobre la puesta en funcionamiento de la delegación restringida de Kerberos. Hay muchísima información sobre el tema en otros lugares.
  2. Por si sirve de algo, esta parte funcionó estupendamente cuando la escribí.

 

Veamos las cosas que necesitamos para la delegación. Primero, como he mencionado antes, mi servicio de SQL Server se ejecuta como servicio de red, de modo que no tengo que hacer nada en cuanto a eso. Segundo, mi grupo de aplicaciones WCF se ejecuta como una cuenta de dominio denominada vbtoys\portal. Por lo tanto, tengo que hacer dos cosas:

  1. Crear un HTTP SPN, mediante el nombre NetBIOS y el nombre completo del servidor desde el que se realizará la delegación. En mi caso, mi servidor WCF se denomina AZ1, de modo que he creado dos SPN como estos: 
    1. setspn -A HTTP/az1 vbtoys\portal
    2. setspn -A HTTP/az1.vbtoys.com vbtoys\portal
  2. Tengo que configurar mi cuenta para que tenga confianza para la delegación restringida de Kerberos a los servicios de SQL Server que se ejecutan en el servidor “SQL2”. Para ello, entré en mi controlador de dominio y abrí Usuarios y equipos de Active Directory. Hice doble clic en el usuario vbtoys\portal y después en la ficha Delegación para configurar la confianza. La configuré para confiar en la delegación para solo unos servicios específicos, mediante cualquier tipo de protocolo de autenticación. He aquí un vínculo a una imagen que muestra el aspecto que tenía la configuración de la delegación:

 

Tercero, necesitaba configurar mi servidor de aplicaciones de WCF para que tuviera confianza para la delegación restringida.  Afortunadamente, el proceso es exactamente igual que el que he descrito antes para el usuario; solo tiene que encontrar la cuenta del equipo en Usuarios y equipos de Active Directory y configurarlo allí. He aquí un vínculo a una imagen que muestra el aspecto de dicha configuración:

 

 

Y con ello, todo lo que no es de SharePoint ya está configurado y listo para usar. Lo último que necesitaba era un elemento web para probarlo.

Creación del elemento web de SharePoint

La creación del elemento web es bastante sencilla; simplemente seguí el patrón descrito anteriormente para realizar llamadas de WCF a SharePoint y pasar la identidad del usuario actual (http://blogs.technet.com/b/speschka/archive/2010/09/08/calling-a-claims-aware-wcf-service-from-a-sharepoint-2010-claims-site.aspx).  También podía haber utilizado el CASI Kit para realizar la conexión y llamar a WCF, pero decidí hacerlo manualmente, por así decirlo, para que fuera más fácil de ilustrar. Los pasos básicos para la creación del elemento web fueron los siguientes:

  1. Crear un nuevo proyecto de SharePoint 2010 en Visual Studio 2010.
  2. Crear una referencia de servicio a mi aplicación de servicios de WCF.
  3. Agregar un nuevo elemento web
  4. Agregar el código al elemento web para recuperar los datos desde WCF y mostrarlos en la cuadrícula.
  5. Agregar toda la información del archivo app.config que se genera en el proyecto de Visual Studio a la sección <system.ServiceModel> del archivo web.config de la aplicación web en el que se hospedará mi elemento web.

NOTA: El archivo app.config tendrá un atributo denominado decompressionEnabled; ES NECESARIO ELIMINARLO ANTES DE AGREGARLO AL ARCHIVO WEB.CONFIG. Si lo deja allí, el elemento web producirá un error al intentar crear una instancia del proxy de referencia del servicio.

En cuanto a los pasos descritos arriba, todos deberían ser bastante evidentes excepto el nº 4, de modo que no voy explicar los demás en detalle. De todas formas, he aquí el código del elemento web:

private DataGrid dataGrd = null;

private Label statusLbl = null;

 

 

protected override void CreateChildControls()

{

   try

   {

       //cree la conexión a WCF e intente recuperar los datos

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

 

       //configure el canal para poder llamarlo con FederatedClientCredentials

       SPChannelFactoryOperations.ConfigureCredentials<SqlDataSvc.ISqlData>(

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

 

       //crear el extremo al que conectarse

       EndpointAddress svcEndPt =

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

 

       //cree un canal hacia el extremo de WCF que utiliza el

       //token y las notificaciones del usuario actual

       SqlDataSvc.ISqlData sqlData =

          SPChannelFactoryOperations.CreateChannelActingAsLoggedOnUser

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

 

       //solicite los datos

       DataSet ds = sqlData.GetProducts();

 

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

       {

          statusLbl = new Label();

          statusLbl.Text = "No se devolvió ningún dato a las " + 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);

   }

}

 

Repito, todo esto es bastante evidente. La primera parte trata sobre la realización de la conexión al servicio de WCF de modo que pase las notificaciones del usuario actual; para saber más detalles, consulte el vínculo a mi entrada de blog anterior sobre este tema. El resto trata sobre recuperar el conjunto de datos y enlazarlos a una cuadrícula (si hay datos) o mostrar una etiqueta que diga que no hay datos (si se produce un error).  Para ilustrar cómo funcionan todas estas piezas juntas, abajo hay tres capturas de pantalla: las dos primeras muestran cómo funciona para dos usuarios diferentes, que puede ver en la columna CurrentUser. La tercera muestra cómo funciona para un usuario a quien no se ha otorgado el permiso para ejecutar el procedimiento almacenado.

 

 

Eso es prácticamente todo; he adjuntado en esta entrada el código de la aplicación de servicio de WCF y del elemento web, así como el documento original de Word en el que escribí esto, ya que el formato de estas entradas suele dejar bastante que desear.

Esta entrada de blog es una traducción. Puede consultar el artículo original en Using SAML Claims, SharePoint, WCF, Claims to Windows Token Service and Constrained Delegation to Access SQL Server