O Provedor de Declarações Personalizado Azure para o Projeto SharePoint Parte 3

Artigo original publicado terça-feira, 21 de fevereiro de 2012

Na Parte 1 desta série, eu esbocei resumidamente a meta deste projeto, que, em um alto nível, é usar o armazenamento de tabelas do Windows Azure como armazenamento de dados para um provedor de declarações personalizado SharePoint. O provedor de declarações usará o Kit CASI para recuperar os dados necessários do Windows Azure para fornecer a funcionalidade de selecionador de pessoas (ou seja, catálogo de endereços) e de resolução de nome de controle de tipo. 

Na Parte 2, passei por todos os componentes executados na nuvem – as classes de dados usadas para trabalhar com o armazenamento de tabelas do Azure e filas, uma função de funcionário para ler itens das filas e preencher o armazenamento de tabelas e um front-end WCF que permite que um aplicativo cliente crie novos itens na fila e também faça toda a tarefa do selecionador de pessoas SharePoint padrão – fornecer uma lista dos tipos de declarações suportados, procurar valores de declarações e resolver declarações.

Nesta, a parte final desta série, veremos os diferentes componentes usados no lado do SharePoint. Ela inclui a construção de um componente personalizado usando o Kit CASI para adicionar itens à fila e também fazer chamadas para o armazenameto de tabelas do Azure. Inclui também o provedor de declarações personalizado, que usará o componente Kit CASI para conectar o SharePoint a essas funções do Azure.

Para começar, vamos dar uma rápida olhada no componente personalizado Kit CASI. Não vou gastar muito tempo aqui porque o Kit CASI é amplamente discutido neste blog. Este componente particular é descrito na Parte 3 da série Kit CASI. Resumidamente, o que eu fiz foi criar um novo projeto de biblioteca de classe do Windows. Adicionei referências ao assembly de classe de base do Kit CASI e os outros assemblies .NET necessários (que descrevo na parte 3). Adicionei uma Referência de Serviço em meu projeto ao ponto de extremidade do WCF criado na Parte 2 deste projeto. Finalmente, adicionei uma nova classe ao projeto, fiz ela herdar a classe de base do Kit CASI e adicionei o código para substituir o método ExecuteRequest. Como visto na série do Kit CASI, a aparência do meu código é a seguinte para substituir ExecuteRequest:

       public class DataSource : AzureConnect.WcfConfig

       {

              public override bool ExecuteRequest()

              {

                     try

                     {

                           //create the proxy instance with bindings and endpoint the base class

                           //configuration control has created for this

                           AzureClaims.AzureClaimsClient cust =

                                  new AzureClaims.AzureClaimsClient(this.FedBinding,

                                   this.WcfEndpointAddress);

 

                           //configure the channel so we can call it with

                           //FederatedClientCredentials.

                           SPChannelFactoryOperations.ConfigureCredentials<AzureClaims.IAzureClaims>

                                   (cust.ChannelFactory,

                                   Microsoft.SharePoint.SPServiceAuthenticationMode.Claims);

 

                           //create a channel to the WCF endpoint using the

                           //token and claims of the current user

                           AzureClaims.IAzureClaims claimsWCF =

                                  SPChannelFactoryOperations.CreateChannelActingAsLoggedOnUser

                                  <AzureClaims.IAzureClaims>(cust.ChannelFactory,

                                  this.WcfEndpointAddress,

                                  new Uri(this.WcfEndpointAddress.Uri.AbsoluteUri));

 

                           //set the client property for the base class

                           this.WcfClientProxy = claimsWCF;

                     }

                     catch (Exception ex)

                     {

                           Debug.WriteLine(ex.Message);

                     }

 

                     //now that the configuration is complete, call the method

                     return base.ExecuteRequest();

              }

       }

 

“AzureClaims” é o nome da Referência de Serviço criada e ela usa a interface IAzureClaims definida em meu projeto do WCF no Azure. Conforme explicado anteriormente na série do Kit CASI, este é basicamente um código de clichês, eu apenas conectei o nome da interface e da classe exposta no aplicativo WCF. A outra coisa que fiz, como também explicado na série do Kit CASI, foi criar uma página ASPX chamada AzureClaimProvider.aspx. Apenas copiei e colei no código descrito na Parte 3 da série do Kit CASI e substitui o nome da classe e o ponto de extremidade no qual ela pode ser atingida. A tag de controle na página ASPX para meu componente personalizado do Kit CASI é semelhante ao seguinte:

<AzWcf:DataSource runat="server" id="wcf" WcfUrl="https://spsazure.vbtoys.com/ AzureClaims.svc" OutputType="Page" MethodName="GetClaimTypes" AccessDeniedMessage="" />

 

O principal a ser observado aqui é que eu criei um registro CNAME para “spsazure.vbtoys.com” que aponta para meu aplicativo Azure em cloudapp.net (isso também é descrito na Parte 3 do Kit CASI). Configurei o MethodName padrão que a página chamará para ser GetClaimTypes, que é um método que não pega nenhum parâmetro e retorna uma lista de tipos de declarações que o provedor de declarações do Azure suporta. Isso faz com que seja um bom teste para validar a conectividade entre o aplicativo Azure e o SharePoint. Posso simplesmente ir para https://anySharePointsite/_layouts/AzureClaimProvider.aspx e, se tudo estiver configurado corretamente, verei alguns dados na página. Depois de implementar meu projeto adicionando o assembly ao Cache de Assembly Global e implementar a página para o diretório _layouts do SharePoint, foi exatamente isso que fiz – visitei a página em um de meus sites e verifiquei que ela retornou dados, então, fiquei sabendo que minha conexão entre o SharePoint e o Azure estava funcionando.

Agora que os canais estão em vigor, eu finalmente passo para a parte “divertida” do projeto, que é fazer duas coisas:

  1. 1.       Criar “algum componente” que envie informações sobre novos usuários para a minha fila do Azure
  2. 2.       Criar um provedor de declarações personalizado que usará meu componente personalizado do Kit CASI para fornecer tipos de declarações, resolução de nomes e irá procurar declarações.

Este é realmente um ponto bom para parar e retroceder um pouco. Nesse caso particular, eu gostaria de listar algumas coisas o mais rápido possível. Portanto, o que fiz foi criar um novo aplicativo da Web e ativar o acesso anônimo. Como tenho certeza que todos vocês sabem, apenas ativá-lo no nível de aplicativo da Web NÃO o ativa no nível de conjunto de sites. Assim, para este cenário, ativei também no conjunto de sites raiz apenas; concedi acesso a tudo no site. Todos os outros conjuntos de sites, que não contêm nenhuma informação apenas para membros, NÃO têm o anônimo ativado, sendo assim, os usuários têm que receber os direitos para se unir.

O próximo item sobre o qual pensar é como gerenciar as identidades que usarão o site. Obviamente, isso não é algo que eu queira fazer. Posso ter surgido com vários métodos diferentes para sincronizar contas no Azure ou algo assim, mas, como expliquei na Parte 1 desta série, há um grupo grande de provedores que já fazem isso , então vou deixar eles continuarem fazendo o que fazem. O que quero dizer com isso é que aproveitei outro serviço de nuvem da Microsoft chamado ACS, ou Access Control Service. Em suma, o ACS funciona como um provedor de identidade para o SharePoint. Então, eu apenas criei confiança entre meu farm do SharePoint e a instância do ACS que criei para este POC. No ACS, adicionei o SharePoint como uma parte de confiança, portanto, o ACS sabe onde enviar usuários depois que eles estiverem autenticados. No ACS, também o configurei para permitir que os usuários entrem usando suas contas do Gmail, Yahoo ou Facebook. Depois que eles entram, o ACS recebe de volta uma declaração única que vou usar – endereço de email – e a envia de volta para o SharePoint.

Ok, isso é tudo sobre o plano de fundo sobre os canais – o Azure está fornecendo armazenamento de tabelas e filas para trabalhar com os dados, o ACS está fornecendo serviços de autenticação e o Kit CASI está fornecendo os canais para os dados.

Então, com tudo isso descrito, como vamos usar? Bem, eu ainda queria que o processo se tornasse um membro bastante indolor, então o que fiz foi escrever uma Web Part para adicionar usuários à minha fila do Azure. O que ela faz é verificar se a solicitação está autenticada (ou seja, se o usuário clicou no link Entrar obtido em um site anônimo, se entrou em um dos provedores mencionados acima e se o ACS me enviou de volta suas informações de declarações). Se a solicitação não estiver autenticada, a Web Part não faz nada. No entanto, se a solicitação estiver autenticada, ela renderiza um botão que, quando clicado, pegará a declaração do email do usuário e a adicionará à fila do Azure. Esta é a parte sobre a qual falei que devemos retroceder um pouco e pensar a respeito. Para um POC, está tudo bem, funciona. Porém, você pode pensar sobre outras maneiras pelas quais pode processar esta solicitação. Por exemplo, talvez você escreva as informações em uma lista do SharePoint. Você pode escrever um trabalho de timer personalizado (com o qual o Kit CASI trabalha muito bem) e periodicamente processar novas solicitações fora dessa lista. Pode usar o SPWorkItem para enfileirar as solicitações para processar mais tarde. Pode armazená-las em uma lista e adicionar um fluxo de trabalho personalizado que talvez passe por algum processo de aprovação e, depois que a solicitação for aprovada, usa uma ação do fluxo de trabalho personalizado para chamar o Kit CASI para colocar os detalhes na fila do Azure. Em suma – há MUITA força, flexibilidade e personalização possíveis aqui – tudo depende da sua imaginação. Em algum ponto, de fato, posso escrever outra versão disso que escreve em uma lista personalizada, processa assincronamente em algum ponto, adiciona os dados à fila do Azure e adiciona automaticamente a conta ao grupo Visitantes em um dos subsites, então, o usuário entra e está pronto para continuar. Mas isso fica para outra postagem, seu eu fizer isso.

Então, tudo isso foi dito – como descrevi acima, estou apenas deixando o usuário clicar em um botão se ele tiver entrado e depois vou usar meu componente personalizado do Kit CASI para chamar o ponto de extremidade do WCF e adicionar as informações na fila do Azure. A seguir, é apresentado o código para a Web Part – muito simples, cortesia do Kit CASI:

public class AddToAzureWP : WebPart

{

 

       //button whose click event we need to track so that we can

       //add the user to Azure

       Button addBtn = null;

       Label statusLbl = null;

 

       protected override void CreateChildControls()

       {

              if (this.Page.Request.IsAuthenticated)

              {

                     addBtn = new Button();

                     addBtn.Text = "Request Membership";

                     addBtn.Click += new EventHandler(addBtn_Click);

                     this.Controls.Add(addBtn);

 

                     statusLbl = new Label();

                     this.Controls.Add(statusLbl);

              }

       }

 

       void addBtn_Click(object sender, EventArgs e)

       {

              try

              {

                     //look for the claims identity

                     IClaimsPrincipal cp = Page.User as IClaimsPrincipal;

 

                     if (cp != null)

                     {

                           //get the claims identity so we can enum claims

                           IClaimsIdentity ci = (IClaimsIdentity)cp.Identity;

 

                           //look for the email claim

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

                           if (ci.Claims.Count > 0)

                           {

                                  //look for the email address claim

                                  var eClaim = from Claim c in ci.Claims

                                         where c.ClaimType == "https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"

                                         select c;

 

                                  Claim ret = eClaim.FirstOrDefault<Claim>();

 

                                  if (ret != null)

                                  {

                                         //create the string we're going to send to the Azure queue:  claim, value, and display name

                                         //note that I'm using "#" as delimiters because there is only one parameter, and CASI Kit

                                         //uses ; as a delimiter so a different value is needed.  If ; were used CASI would try and

                                         //make it three parameters, when in reality it's only one

                                         string qValue = ret.ClaimType + "#" + ret.Value + "#" + "Email";

 

                                         //create the connection to Azure and upload

                                         //create an instance of the control

                                         AzureClaimProvider.DataSource cfgCtrl = new AzureClaimProvider.DataSource();

 

                                         //set the properties to retrieve data; must configure cache properties since we're using it programmatically

                                         //cache is not actually used in this case though

                                         cfgCtrl.WcfUrl = AzureCCP.SVC_URI;

                                         cfgCtrl.OutputType = AzureConnect.WcfConfig.DataOutputType.None;

                                         cfgCtrl.MethodName = "AddClaimsToQueue";

                                         cfgCtrl.MethodParams = qValue;

                                         cfgCtrl.ServerCacheTime = 10;

                                         cfgCtrl.ServerCacheName = ret.Value;

                                         cfgCtrl.SharePointClaimsSiteUrl = this.Page.Request.Url.ToString();

 

                                         //execute the method

                                         bool success = cfgCtrl.ExecuteRequest();

 

                                         if (success)

                                         {

                                                //if it worked tell the user

                                                statusLbl.Text = "<p>Your information was successfully added.  You can now contact any of " +

                                                       "the other Partner Members or our Support staff to get access rights to Partner " +

                                                       "content.  Please note that it takes up to 15 minutes for your request to be " +

                                                       "processed.</p>";

                                         }

                                         else

                                         {

                                                statusLbl.Text = "<p>There was a problem adding your info to Azure; please try again later or " +

                                                       "contact Support if the problem persists.</p>";

                                         }

                                  }

                           }

                     }

              }

              catch (Exception ex)

              {

                     statusLbl.Text = "There was a problem adding your info to Azure; please try again later or " +

                           "contact Support if the problem persists.";

                     Debug.WriteLine(ex.Message);

              }

       }

}

 

Assim, um rápido encerramento do código é semelhante ao seguinte: Primeiro, asseguro que a solicitação esteja autenticada; se estiver, adiciono o botão à página e adiciono um manipulador de eventos para o evento de clique do botão. No manipulador de eventos de clique do botão, obtenho uma referência IClaimsPrincipal para o usuário atual e vejo a coleção de declarações do usuário. Realizo uma consulta LINQ em relação à coleção de declarações para procurar a declaração de email, que é a declaração de identidade para o SPTrustedIdentityTokenIssuer. Se localizar a declaração de email, crio uma sequência concatenada com o tipo de declaração, o valor da declaração e o nome amigável para a declaração. Novamente, isso não é estritamente necessário neste cenário, mas como quero que isso possa ser usado em um cenário mais genérico, codifiquei dessa maneira. Essa sequência concatenada é o valor para o método que tenho no WCF que adiciona dados à fila do Azure. Crio, então, uma instância do meu componente personalizado do Kit CASI e a configuro para chamar o método do WCF que adiciona dados à fila e chamo o método ExecuteRequest para realmente eliminar os dados. 

Se recebo uma resposta indicando que fui bem-sucedido em adicionar dados à fila, faço com que o usuário saiba disso; caso contrário, faço com que ele saiba que houve um problema e que talvez seja necessário verificar novamente mais tarde. Em um cenário real, naturalmente, eu teria ainda mais log de erro, portanto, posso controlar exatamente o que aconteceu e por quê. Mesmo com isso assim, o Kit CASI gravará todas as informações de erro nos logs ULS em um SPMonitoredScope, então, tudo o que ele faz para a solicitação terá um ID de correlação exclusivo com o qual podemos visualizar toda a atividade associada à solicitação. Então, estou realmente em um estado muito bom agora.

Ok – passamos por todas as partes dos canais e eu mostrei como os dados são adicionados à fila do Azure e de lá colocados por um processo do funcionário e adicionados ao armazenamento de tabelas. Esta é realmente a meta final, porque agora podemos passar para o provedor de declarações personalizado. Ele vai usar o Kit CASI para chamar e consultar o armazenamento de tabelas do Azure que estou usando. Vamos ver os aspectos mais interessantes do provedor de declarações personalizado.

Primeiro, vamos ver alguns atributos de nível de classe:

//the WCF endpoint that we'll use to connect for address book functions

//test url:  https://az1.vbtoys.com/AzureClaimsWCF/AzureClaims.svc

//production url:  https://spsazure.vbtoys.com/AzureClaims.svc

public static string SVC_URI = "https://spsazure.vbtoys.com/AzureClaims.svc";

 

//the identity claimtype value

private const string IDENTITY_CLAIM =

       "https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress";

 

//the collection of claim type we support; it won't change over the course of

//the STS (w3wp.exe) life time so we'll cache it once we get it

AzureClaimProvider.AzureClaims.ClaimTypeCollection AzureClaimTypes =

       new AzureClaimProvider.AzureClaims.ClaimTypeCollection();

 

Primeiro, uso uma constante para fazer referência ao ponto de extremidade do WCF ao qual o Kit CASI deve se conectar. Você observará que tenho meu ponto de extremidade de teste e meu ponto de extremidade de produção. Quando está usando o Kit CASI programaticamente, como faremos em nosso provedor de declarações personalizado, você sempre deve informar onde está o ponto de extremidade do WCF com o qual ele deve falar.

A seguir, como descrevi anteriormente, estou usando a declaração de email como minha declaração de identidade. Como farei referência a ela várias vezes em todo o provedor, conectei a uma constante no nível de classe.

Finalmente, tenho uma coleção de AzureClaimTypes. Expliquei na Parte 2 desta série por que estou usando uma coleção e a estou armazenando aqui no nível de classe para que eu não tenha que buscar novamente estas informações cada vez que meu método FillHierarchy for chamado. As chamadas para o Azure não são baratas, então, eu as minimizo onde é possível.

Aqui está a próxima parte do código:

internal static string ProviderDisplayName

{

       get

       {

              return "AzureCustomClaimsProvider";

       }

}

 

internal static string ProviderInternalName

{

       get

       {

              return "AzureCustomClaimsProvider";

       }

}

 

//*******************************************************************

//USE THIS PROPERTY NOW WHEN CREATING THE CLAIM FOR THE PICKERENTITY

internal static string SPTrustedIdentityTokenIssuerName

{

       get

       {

              return "SPS ACS";

       }

}

 

 

public override string Name

{

       get

       {

              return ProviderInternalName;

       }

}

 

A razão pela qual eu queria apontar este código é que, como meu provedor está emitindo declarações de identidade, ele DEVE ser o provedor padrão para o SPTrustedIdentityTokenIssuer. Explicar como fazer isso está fora do escopo desta postagem, mas falei sobre isso em outro lugar em meu blog. O principal a lembrar sobre fazer isso é que você deve ter um relacionamento forte entre o nome que usar para seu provedor e o nome usado para o SPTrustedIdentityTokenIssuer. O valor que usei para o ProviderInternalName é o nome que devo conectar à propriedade ClaimProviderName para o SPTrustedIdentityTokenIssuer. Além disso, preciso usar o nome do SPTrustedIdentityTokenIssuer quando estou criando declarações de identidade para usuários. Então, criei um SPTrustedIdentityTokenIssuer chamado “SPS ACS” e o adicionei à minha propriedade SPTrustedIdentityTokenIssuerName. Esta é a razão pela qual tenho esses valores codificados aqui.

Como não estou fazendo nenhum aumento de declarações neste provedor, não escrevi nenhum código para substituir FillClaimTypes, FillClaimValueTypes ou FillEntityTypes. A próxima parte de código que tenho é FillHierarchy, que é onde informo a SharePoint quais os tipos de declarações que suporto. Aqui está o código para isso:

try

{

       if (

               (AzureClaimTypes.ClaimTypes == null) ||

               (AzureClaimTypes.ClaimTypes.Count() == 0)

       )

       {

              //create an instance of the control

              AzureClaimProvider.DataSource cfgCtrl = new AzureClaimProvider.DataSource();

 

              //set the properties to retrieve data; must configure cache properties since we're using it programmatically

              //cache is not actually used in this case though

              cfgCtrl.WcfUrl = SVC_URI;

              cfgCtrl.OutputType = AzureConnect.WcfConfig.DataOutputType.None;

              cfgCtrl.MethodName = "GetClaimTypes";

              cfgCtrl.ServerCacheTime = 10;

              cfgCtrl.ServerCacheName = "GetClaimTypes";

              cfgCtrl.SharePointClaimsSiteUrl = context.AbsoluteUri;

 

              //execute the method

              bool success = cfgCtrl.ExecuteRequest();

 

              if (success)

              {

                     //if it worked, get the list of claim types out

                     AzureClaimTypes =

                                                (AzureClaimProvider.AzureClaims.ClaimTypeCollection)cfgCtrl.QueryResultsObject;

              }

       }

 

       //make sure picker is asking for the type of entity we return; site collection admin won't for example

       if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.User))

              return;

 

       //at this point we have whatever claim types we're going to have, so add them to the hierarchy

       //check to see if the hierarchyNodeID is null; it will be when the control

       //is first loaded but if a user clicks on one of the nodes it will return

       //the key of the node that was clicked on.  This lets you build out a

       //hierarchy as a user clicks on something, rather than all at once

       if (

               (string.IsNullOrEmpty(hierarchyNodeID)) &&

               (AzureClaimTypes.ClaimTypes.Count() > 0)

              )

       {

              //enumerate through each claim type

              foreach (AzureClaimProvider.AzureClaims.ClaimType clm in AzureClaimTypes.ClaimTypes)

              {

                     //when it first loads add all our nodes

                     hierarchy.AddChild(new

                                                Microsoft.SharePoint.WebControls.SPProviderHierarchyNode(

                                   ProviderInternalName, clm.FriendlyName, clm.ClaimTypeName, true));

              }

       }

}

catch (Exception ex)

{

       Debug.WriteLine("Error filling hierarchy: " + ex.Message);

}

 

Aqui, estou vendo se segurei a lista de tipos de declarações que já suporto. Se não, crio uma instância do meu controle personalizado do Kit CASI e faço uma chamada para meu WCF para recuperar os tipos de declarações; faço isso chamando o método GetClaimTypes na minha classe do WCF. Se receber os dados de volta, conecto-os à variável de nível de classe que descrevi anteriormente chamada AzureClaimTypes e a adiciono à hierarquia de tipos de declarações que suporto.

Os próximos métodos que veremos são os métodos FillResolve. Os métodos FillResolve teêm duas assinaturas diferentes porque fazem duas coisas diferentes. Em um cenário, temos uma declaração específica com valor e tipo e o SharePoint só quer verificar se é válida. No segundo caso, um usuário acabou de digitar um valor no controle de tipo do SharePoint e, então, é efetivamente o mesmo que fazer uma procura por declarações. Por causa disso, vou vê-las separadamente.

No caso em que tenho uma declaração específica e o SharePoint quer verificar os valores, chamo um método personalizado que escrevi chamado GetResolveResults. Nesse método, passo a URI na qual a solicitação está sendo feita e também o tipo e o valor da declaração que o SharePoint está procurando validar. GetResolveResults, então, é semelhante ao seguinte:

//Note that claimType is being passed in here for future extensibility; in the

//current case though, we're only using identity claims

private AzureClaimProvider.AzureClaims.UniqueClaimValue GetResolveResults(string siteUrl,

       string searchPattern, string claimType)

{

       AzureClaimProvider.AzureClaims.UniqueClaimValue result = null;

 

       try

       {

              //create an instance of the control

              AzureClaimProvider.DataSource cfgCtrl = new AzureClaimProvider.DataSource();

 

              //set the properties to retrieve data; must configure cache properties since we're using it programmatically

              //cache is not actually used in this case though

              cfgCtrl.WcfUrl = SVC_URI;

              cfgCtrl.OutputType = AzureConnect.WcfConfig.DataOutputType.None;

              cfgCtrl.MethodName = "ResolveClaim";

              cfgCtrl.ServerCacheTime = 10;

              cfgCtrl.ServerCacheName = claimType + ";" + searchPattern;

              cfgCtrl.MethodParams = IDENTITY_CLAIM + ";" + searchPattern;

              cfgCtrl.SharePointClaimsSiteUrl = siteUrl;

 

              //execute the method

              bool success = cfgCtrl.ExecuteRequest();

 

              //if the query encountered no errors then capture the result

              if (success)

                     result = (AzureClaimProvider.AzureClaims.UniqueClaimValue)cfgCtrl.QueryResultsObject;

       }

       catch (Exception ex)

       {

              Debug.WriteLine(ex.Message);

       }

 

       return result;

}

 

Aqui, estou criando uma instância do controle personalizado do Kit CASI e chamando o método ResolveClaim no meu WCF. Esse método toma dois parâmetros, então, passo isso como valores delimitados por ponto e vírgula (porque é assim que o Kit CASI distingue entre diferentes valores de parâmetros). A seguir, executo a solicitação e, se ela encontrar uma correspondência, retornará um único UniqueClaimValue; caso contrário, o valor de retorno será nulo. De volta ao meu método FillResolve, a aparência do meu código é a seguinte:

protected override void FillResolve(Uri context, string[] entityTypes, SPClaim resolveInput, List<PickerEntity> resolved)

{

       //make sure picker is asking for the type of entity we return; site collection admin won't for example

       if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.User))

              return;

 

       try

       {

              //look for matching claims

              AzureClaimProvider.AzureClaims.UniqueClaimValue result =

                     GetResolveResults(context.AbsoluteUri, resolveInput.Value,

                     resolveInput.ClaimType);

 

              //if we found a match then add it to the resolved list

              if (result != null)

              {

                     PickerEntity pe = GetPickerEntity(result.ClaimValue, result.ClaimType,

                     SPClaimEntityTypes.User, result.DisplayName);

                           resolved.Add(pe);

              }

       }

       catch (Exception ex)

       {

              Debug.WriteLine(ex.Message);

       }

}

 

Primeiro, estou verificando para ter certeza de que a solicitação é para uma declaração de Usuário, já que esse é o único tipo de declaração que meu provedor está retornando. Se a solicitação não for para uma declaração de Usuário, eu descarto. A seguir, chamo meu método para resolver a declaração e, se receber um resultado não nulo, processo. Para processá-lo, chamo outro método personalizado que escrevi chamado GetPickerEntity. Aqui, passo o tipo de declaração e o valor para criar uma declaração de identidade e, em seguida, posso adicionar o PickerEntity retornado à Lista de instâncias do PickerEntity passadas no meu método. Não vou falar sobre o método GetPickerEntity porque esta postagem já está incrivelmente longa e eu falei sobre como fazer isso em outras postagens em meu blog.

Agora, vamos falar sobre o outro método FillResolve. Como expliquei anteriormente, ele basicamente funciona como uma procura, portanto, vou combinar os métodos FillResolve e FillSearch aqui. Esses dois métodos vão chamar um método personalizado que escrevi chamado SearchClaims, que é semelhante ao seguinte:

private AzureClaimProvider.AzureClaims.UniqueClaimValueCollection SearchClaims(string claimType, string searchPattern,

       string siteUrl)

{

                    

       AzureClaimProvider.AzureClaims.UniqueClaimValueCollection results =

              new AzureClaimProvider.AzureClaims.UniqueClaimValueCollection();

 

       try

       {

              //create an instance of the control

              AzureClaimProvider.DataSource cfgCtrl = new AzureClaimProvider.DataSource();

 

              //set the properties to retrieve data; must configure cache properties since we're using it programmatically

              //cache is not actually used in this case though

              cfgCtrl.WcfUrl = SVC_URI;

              cfgCtrl.OutputType = AzureConnect.WcfConfig.DataOutputType.None;

              cfgCtrl.MethodName = "SearchClaims";

              cfgCtrl.ServerCacheTime = 10;

              cfgCtrl.ServerCacheName = claimType + ";" + searchPattern;

              cfgCtrl.MethodParams = claimType + ";" + searchPattern + ";200";

              cfgCtrl.SharePointClaimsSiteUrl = siteUrl;

 

              //execute the method

              bool success = cfgCtrl.ExecuteRequest();

 

              if (success)

              {

                     //if it worked, get the array of results

                     results =

 (AzureClaimProvider.AzureClaims.UniqueClaimValueCollection)cfgCtrl.QueryResultsObject;

              }

       }

       catch (Exception ex)

       {

              Debug.WriteLine("Error searching claims: " + ex.Message);

       }

 

       return results;

}

 

Neste método, como foi visto em outro lugar nesta postagem, estou apenas criando uma instância de controle do meu Kit CASI personalizado. Estou chamando o método SearchClaims em meu WCF e estou passando o tipo de declaração que quero procurar, o valor de declaração que quero localizar nesse tipo de declaração e o número máximo de registros a retornar. Você pode relembrar da Parte 2 desta série que SearchClaims apenas faz um BeginsWith no padrão de procura que é passado, assim, com muitos usuários poderia facilmente haver mais de 200 resultados. No entanto, 200 é o número máximo de correspondências que o selecionador de pessoas mostrará, então, isso é tudo o que procuro. Se você realmente acha que os usuários vão rolar por mais de 200 resultados procurando um resultado, estou aqui para dizer a você que isso é improvável.

Agora, temos nossa coleção de UniqueClaimValues de volta, vamos ver como a usamos em nossos dois métodos de substituição no provedor de declarações personalizado. Primeiro, a aparência do método FillResolve é a seguinte:

protected override void FillResolve(Uri context, string[] entityTypes, string resolveInput, List<PickerEntity> resolved)

{

       //this version of resolve is just like a search, so we'll treat it like that

       //make sure picker is asking for the type of entity we return; site collection admin won't for example

       if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.User))

              return;

 

       try

       {

              //do the search for matches

              AzureClaimProvider.AzureClaims.UniqueClaimValueCollection results =

                     SearchClaims(IDENTITY_CLAIM, resolveInput, context.AbsoluteUri);

 

              //go through each match and add a picker entity for it

              foreach (AzureClaimProvider.AzureClaims.UniqueClaimValue cv in results.UniqueClaimValues)

              {

                     PickerEntity pe = GetPickerEntity(cv.ClaimValue, cv.ClaimType, SPClaimEntityTypes.User, cv.DisplayName);

                     resolved.Add(pe);

              }

       }

       catch (Exception ex)

       {

              Debug.WriteLine(ex.Message);

       }

}

Ele apenas chama o método SearchClaims e, para cada resultado que recebe (se houver), cria um novo PickerEntity e o adiciona à Lista passada na substituição. Todos eles serão mostrados no controle de tipo no SharePoint. O método FillSearch o usa da seguinte maneira:

protected override void FillSearch(Uri context, string[] entityTypes, string searchPattern, string hierarchyNodeID, int maxCount, SPProviderHierarchyTree searchTree)

{

       //make sure picker is asking for the type of entity we return; site collection admin won't for example

       if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.User))

              return;

 

       try

       {

              //do the search for matches

              AzureClaimProvider.AzureClaims.UniqueClaimValueCollection results =

                     SearchClaims(IDENTITY_CLAIM, searchPattern, context.AbsoluteUri);

 

              //if there was more than zero results, add them to the picker

              if (results.UniqueClaimValues.Count() > 0)

              {

                     foreach (AzureClaimProvider.AzureClaims.UniqueClaimValue cv in results.UniqueClaimValues)

                     {

                           //node where we'll stick our matches

                           Microsoft.SharePoint.WebControls.SPProviderHierarchyNode matchNode = null;

 

                           //get a picker entity to add to the dialog

                           PickerEntity pe = GetPickerEntity(cv.ClaimValue, cv.ClaimType, SPClaimEntityTypes.User, cv.DisplayName);

 

                           //add the node where it should be displayed too

                           if (!searchTree.HasChild(cv.ClaimType))

                           {

                                  //create the node so we can show our match in there too

                                  matchNode = new

                                         SPProviderHierarchyNode(ProviderInternalName,

                                         cv.DisplayName, cv.ClaimType, true);

 

                                  //add it to the tree

                                  searchTree.AddChild(matchNode);

                           }

                           else

                                  //get the node for this team

                                  matchNode = searchTree.Children.Where(theNode =>

                                         theNode.HierarchyNodeID == cv.ClaimType).First();

 

                           //add the match to our node

                           matchNode.AddEntity(pe);

                     }

              }

       }

       catch (Exception ex)

       {

              Debug.WriteLine(ex.Message);

       }

}

 

No FillSearch, estou chamando meu método SearchClaims novamente. Para cada UniqueClaimValue que recebo de volta (se houver), vejo se adicionei o tipo de declaração ao nó da hierarquia de resultados. Novamente, neste caso sempre retornarei apenas um tipo de declaração (email), mas escrevi isso para que fosse extensível para que você pudesse usar mais tipos de declarações mais tarde. Então, adiciono o nó da hierarquia, se ele não existir, ou o localizo, se existir. Pego o PickerEntity que criei a partir de UniqueClaimValue e o adiciono ao nó da hierarquia. E isso é tudo sobre esse assunto.

Não vou falar sobre o método FillSchema ou qualquer uma das quatro substituições de propriedade booleana que cada provedor de declarações personalizado deve ter porque não há nada de especial neles para este cenário e eu já tratei das noções básicas em outras postagens neste blog. Também não vou falar sobre o receptor de recursos usado para registrar esse provedor de declarações personalizado porque – novamente – não há nada de especial para esse projeto e eu tratei disso em outro lugar. Depois de compilá-lo, você só precisa ter certeza que seu assembly para o provedor de declarações personalizado e também o componente personalizado do Kit CASI estão registrados no Cache de Assembly Global em cada servidor no farm e precisa configurar o SPTrustedIdentityTokenIssuer para usar seu provedor de declarações personalizado como provedor padrão (também explicado em outro lugar neste blog). 

Este é o cenário básico de ponta a ponta. Quando você estiver no site do SharePoint e tentar e adicionar um novo usuário (declaração de email realmente), o provedor de declarações personalizado será chamado primeiro para obter uma lista de tipos de declarações suportados e, em seguida, novamente conforme você digita um valor no controle de tipo ou procura um valor usando o selecionador de pessoas. Em cada caso, o provedor de declarações personalizado usa o controle personalizado do Kit CASI para fazer uma chamada autenticada para o Windows Azure para falar com nosso WCF, que usa nossas classes de dados personalizados para recuperar dados do armazenamento de tabelas do Azure. Ele retorna os resultados e nós não os quebramos e os apresentamos ao usuário. Com isso, você tem sua solução “extranet na caixa” do SharePoint e do Azure de turnkey completo que pode ser usada como está ou que você pode modificar para atender seus objetivos. O código-fonte para o componente personalizado do Kit CASI, a Web Part que registra o usuário em uma fila do Azure e o provedor de declarações personalizado estão todos anexados nesta postagem. Espero que você aproveite, ache útil e possa começar a visualizar como pode juntar esses serviços separados para criar soluções para seus problemas. Aqui estão algumas capturas de tela da solução final:

Site raiz como usuário anônimo:

Aqui está a aparência dele depois da autenticação; observe que a Web Part agora exibe o botão Request Membership:

Aqui está um exemplo do selecionador de pessoas em ação, depois de procurar valores de declarações que começam com “sp”:

 

Esta é uma postagem em blog localizado. Localize o artigo original em The Azure Custom Claim Provider for SharePoint Project Part 3