Usando declarações SAML, SharePoint, WCF, declarações para o Serviço de Token do Windows e a delegação restrita para acessar o SQL Server

Artigo original publicado domingo, dia 07 de agosto de 2011

Certo, essa deve ser a postagem com o título mais longo que eu já escrevi, mas eu queria ter certeza de que cobria todas as tecnologias relevantes em discussão. Essa é uma área da qual ouvi muita coisa recentemente e que realmente fala sobre como eu posso usar um usuário de declarações SAML e um contexto do Windows para acessar outro aplicativo. O SharePoint 2010 tem suporte limitado para uso das Declarações para Serviço de Token do Windows (a partir de agora chamada de c2wts), mas somente para usuários de declarações do Windows com poucos aplicativos de serviço. Uma pergunta comum é por que não posso usar usuários de declarações SAML com uma declaração UPN válida, e realmente não há um motivo tecnológico para não poder. Portanto, entre a limitação em tipos de autenticação e a limitação em aplicativos de serviço que podem usá-lo, você pode ficar em uma posição na qual precisa construir uma forma de conectar os usuários SAML a outros aplicativos, como a conta do Windows subjacente. Espero que esta postagem o ajude a entender as noções básicas de como isso pode ser feito.

A abordagem básica usada por esse cenário é criar um WCF Services Application que processa todas as solicitações de usuário final por dados do outro aplicativo, que em nosso caso é o SQL Server. Então, quero usar um usuário de SAML que está acessando o site do SharePoint fazer uma solicitação como a conta do Windows para esse usuário SAML ao recuperar os dados do SQL Server. OBSERVAÇÃO: Apesar de esse artigo ser sobre usuários de declarações SAML, a mesma metodologia pode ser usada para usuários de declarações do Windows; eles recebem uma declaração UPN por padrão quando entram. Veja um diagrama de todo o processo:

Configurando o SQL Server

Vamos começar no lado do SQL Server. Em meu cenário, o SQL Server está executando em um servidor chamado “SQL2”. O serviço SQL está executando como Serviço de rede. Isso significa que eu não preciso criar um SPN para ele; se ele estivesse em execução como uma conta de domínio, seria necessário criar um SPN para essa conta de serviço para MSSQLSvc. Para esse cenário específico, vou usar o banco de dados antigo Northwinds a fim de recuperar dados. Quero demonstrar facilmente a identidade do usuário que está fazendo a solicitação, portanto, modifiquei o procedimento armazenado Ten Most Expensive Products de modo que pareça com o seguinte:

 

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

 

O principal a ser observado aqui é que adicionei SYSTEM_USER à instrução SELECT; tudo o que ela faz é retornar o usuário atual na coluna. Isso significa que quando eu executo uma consulta e recebo os resultados, vejo uma coluna em minha grade contendo o nome do usuário atual, de modo que eu possa ver facilmente se a consulta foi executada como a identidade do usuário atual ou não. Nesse cenário específico, concedi a três usuários do Windows direitos para executar esse procedimento armazenado; qualquer outro usuário não poderá fazer isso (o que também será um exemplo útil no resultado final).

Criando o WCF Services Application

A próxima coisa que fiz foi criar um WCF Services Application que recuperou os dados do SQL. Segui as diretrizes que descrevi anteriormente na postagem CASI Kit parte 2 (https://blogs.msdn.com/b/sharepoint_br/archive/2010/12/20/the-claims-azure-and-sharepoint-integration-toolkit-part-2.aspx); Fiz isso para estabelecer a confiança entre o farm do SharePoint e o aplicativo WCF. Isso foi necessário para que eu pudesse obter as declarações do usuário que estava fazendo a solicitação. Não convém apenas passar o valor da declaração UPN como um parâmetro, por exemplo, pois qualquer pessoa poderia falsificar a identidade de outra pessoa simplesmente passando um valor de declaração UPN diferente. Após a configuração correta da confiança entre o WCF e o SharePoint, pude prosseguir e escrever meu método que irá:

  • Extrair a declaração UPN
  • Representar o usuário que está usando o c2wts
  • Recuperar os dados do SQL como esse usuário

 

Este é o código que usei para fazer isso:

 

//the following added for this code sample:

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;";

 

       //ask for the current claims identity

       IClaimsIdentity ci =

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

 

       //make sure the request had a claims identity attached to it

       if (ci != null)

       {

          //see if there are claims present before running through this

          if (ci.Claims.Count > 0)

          {

              //look for the UPN claim

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

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

              select c;

 

              //if we got a match, then get the value for login

              if (eClaim.Count() > 0)

              {

                 //get the upn claim value

                 string upn = eClaim.First().Value;

 

                 //create the WindowsIdentity for impersonation

                 WindowsIdentity wid = null;

 

                 try

                 {

                     wid = S4UClient.UpnLogon(upn);

                 }

                 catch (SecurityAccessDeniedException adEx)

                 {

                           Debug.WriteLine("Could not map the upn claim to " +

                     "a valid windows identity: " + adEx.Message);

                 }

 

                 //see if we were able to successfully login

                 if (wid != null)

                 {

                        using (WindowsImpersonationContext ctx = wid.Impersonate())

                    {

                       //request the data from 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;

}

 

Em última análise, não é realmente um código muito complicado. Aqui está um breve resumo sobre o que está acontecendo. Primeiro, verifico se temos um contexto de identidade de declarações válido e, se tivermos, consulto a lista de declarações em busca da declaração UPN. Supondo que eu encontre a declaração UPN, eu extraio o valor dela e faço a chamada para o c2wts a fim de realizar um login S4U como esse usuário. Se esse login for bem-sucedido, retornará uma WindowsIdentity. Em seguida, uso essa WindowsIdentity e crio um contexto de representação. Quando estiver representando o usuário, eu crio minha conexão com o SQL Server e recupero os dados. Veja algumas dicas de solução de problema:

  1. Se você não tiver configurado o c2wts a fim de permitir que seu pool de aplicativos o use, receberá um erro preso no bloco catch externo. O erro será algo como “WTS0003: o chamador não está autorizado a acessar o serviço.” Fornecerei detalhes e um link para configurar o c2wts abaixo.
  2. Se a delegação restrita de Kerberos não estiver configurada corretamente, quando você tentar executar o procedimento armazenado com a linha de código da.Fill(ds);, causará uma exceção afirmando que o usuário anônimo não tem direitos para executar esse procedimento armazenado. Veja abaixo alguma dicas sobre como configurar a delegação restrita para esse cenário.

Configurando o C2WTS

O c2wts está configurado por padrão para a) iniciar manualmente e b) não permitir que qualquer pessoa o use. Eu mudei isso de modo que a) ele inicie automaticamente e b) o pool de aplicativos para meu Aplicativo de serviços WCF tem autorização para usá-lo. Em vez de entrar em detalhes sobre como configurar essa autorização, recomendo a leitura deste artigo; as informações de configuração estão no final: https://msdn.microsoft.com/pt-br/library/ee517258.aspx. Isso é realmente tudo o que você precisa fazer para começar. Para obter mais informações de suporte sobre o c2wts, recomendo também a leitura de https://msdn.microsoft.com/pt-br/library/ee517278.aspx.

 

OBSERVAÇÃO: Há um erro ENORME nesse último artigo; ele recomenda a criação de uma dependência para o c2wts por meio da execução deste código: sc config c2wts depend=cryptosvc. NÃO FAÇA ISSO!! Há um erro de ortografia e “cryptosvc” não é um nome de serviço válido, pelo menos não no Windows Server 2008 R2. Se você fizer isso, seu c2wts não será mais iniciado, pois afirmará que a dependência está marcada para exclusão ou não pode ser encontrada. Fiquei nessa situação e mudei a dependência para iisadmin (o que é lógico, pois, em meu caso, pelo menos meu host WCF precisa estar em execução para que eu possa usar o c2wts); caso contrário eu ficava preso.

Configurando a delegação restrita de Kerberos

Certo, antes de alguém ficar muito assustado com esse tópico, deixe-me dizer o seguinte:

  1. Não vou entrar em muitos detalhes sobre como fazer a delegação restrita de kerb funcionar. Há muito material sobre isso por aí.
  2. Se vale alguma coisa, essa parte funcionou muito bem quando escrevi este artigo.

 

Então, vamos analisar o que precisamos para a delegação. Primeiro, como mencionei acima, meu serviço SQL Server está executando como um Serviço de rede, portanto, não preciso fazer nada. Segundo, meu pool de aplicativos WCF está executando como uma conta de domínio chamada vbtoys\portal. Dessa forma, preciso fazer duas coisas com relação a isso:

  1. Criar um HTTP SPN, usando o nome NetBIOS e o nome totalmente qualificado do servidor a partir do qual vai delegar. Em meu caso, meu servidor WCF é chamado de AZ1. Portanto, criei dois SPNs parecidos com o seguinte: 
    1. setspn -A HTTP/az1 vbtoys\portal
    2. setspn -A HTTP/az1.vbtoys.com vbtoys\portal
  2. Preciso configurar minha conta para ser confiável para a delegação restrita de Kerberos para os serviços do SQL Server em execução no servidor “SQL2”. Para fazer isso, fui até meu controlador de domínio e abri Usuários e Computadores do Active Directory. Cliquei duas vezes no usuário vbtoys\portal e cliquei na guia Delegação para configurar essa confiança. Configurei a confiança na delegação somente para serviços específicos, usando qualquer tipo de protocolo de autenticação. Veja um link para uma imagem da aparência dessa configuração de delegação:

 

terceiro, eu precisava configurar meu servidor de aplicativos WCF para ser confiável para delegação restrita. Felizmente, o processo é exatamente o mesmo que eu descrevi acima para o usuário; basta encontrar a conta do computador nos Usuários e Computadores do Active Directory e configurá-la. Veja um link para uma imagem da aparência dessa configuração:

 

 

E, com isso, todas as coisas que não fazem parte do SharePoint são instaladas, configuradas e disponibilizadas para usar. A última coisa necessária é uma web part para teste.

Criando a web part do SharePoint

A criação da web part é bastante simples; eu apenas segui o padrão descrito anteriormente para fazer chamadas WCF para o SharePoint e passar a identidade do usuário atual (https://blogs.technet.com/b/speschka/archive/2010/09/08/calling-a-claims-aware-wcf-service-from-a-sharepoint-2010-claims-site.aspx). Eu também podia ter usado o CASI Kit para fazer a conexão e chamar o WCF, mas decidi fazer isso manualmente a fim de ilustrar tudo com mais facilidade. As etapas básicas para a criação da web part são:

  1. Criar um novo projeto do SharePoint 2010 no Visual Studio 2010.
  2. Criar uma Referência de serviço para meu WCF Services Application.
  3. Adicionar uma nova web part
  4. Adicionar o código à web part a fim de recuperar os dados do WCF e exibi-los em uma grade.
  5. Adicionar todas as informações em app.config geradas no projeto do Visual Studio à seção <system.ServiceModel> do arquivo web.config para o aplicativo Web no qual minha web part será hospedada.

OBSERVAÇÃO: O app.config terá um atributo chamado decompressionEnabled; EXCLUA-O ANTES DE ADICIONÁ-LO AO ARQUIVO WEB.CONFIG. Se você não fizer isso, sua web part gerará um erro ao tentar criar uma instância de seu proxy de referência de serviço.

Com relação às etapas acima, todas devem ser bastante evidentes, com exceção da etapa 4. Portanto, não vou cobrir as outras em detalhes. Este é o código para a web part:

private DataGrid dataGrd = null;

private Label statusLbl = null;

 

 

protected override void CreateChildControls()

{

   try

   {

       //create the connection to the WCF and try retrieving the data

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

 

       //configure the channel so we can call it with FederatedClientCredentials

       SPChannelFactoryOperations.ConfigureCredentials<SqlDataSvc.ISqlData>(

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

 

       //create the endpoint to connect to

       EndpointAddress svcEndPt =

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

 

       //create a channel to the WCF endpoint using the

       //token and claims of the current user

       SqlDataSvc.ISqlData sqlData =

          SPChannelFactoryOperations.CreateChannelActingAsLoggedOnUser

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

 

       //request the data

       DataSet ds = sqlData.GetProducts();

 

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

       {

          statusLbl = new Label();

          statusLbl.Text = "No data was returned at " + 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);

   }

}

 

Novamente, acredito que isso seja bastante auto-explicativo. A primeira parte é sobre como fazer a conexão com o serviço WCF de uma forma que passará adiante as declarações do usuário atual; para obter mais detalhes, consulte o link acima para minha postagem anterior no blog sobre esse tópico. O restante serve apenas para obter um conjunto de dados e vinculá-lo a uma grade, se houver dados ou mostrar um rótulo que afirma não haver dados, se falhar. Para ilustrar todas essas partes trabalhando juntas, veja abaixo três capturas de tela: as duas primeiras mostram trabalhando para dois usuários diferentes, que podem ser vistos na coluna CurrentUser. A terceira mostra um usuário que não concedeu direitos para executar o procedimento armazenado.

 

 

Isso conclui tudo; Anexei o código para o WCF Service Application e a web part nesta postagem, junto com o documento de Word original no qual escrevi este artigo, uma vez que a formatação destas postagens é frequentemente ruim.

Esta é uma postagem de blog traduzida. Encontre o artigo original em Using SAML Claims, SharePoint, WCF, Claims to Windows Token Service and Constrained Delegation to Access SQL Server