Autenticación app-only para usar la API Microsoft Graph en webjobs de Azure

Muy buenas a todos

En entradas anteriores, hemos visto cómo empezar a trabajar con el servicio de búsqueda de Azure, creando los índices. Dentro de mi objetivo a la hora de trabajar con este servicio, estaba el de indexar contenido de Office 365 usando la API Microsoft Graph por medio de webjobs. Y es aquí donde entra lo que os quiero contar hoy.

El flujo habitual de autenticación para usar la API Microsoft Graph en nuestras aplicaciones está basado en el protocolo OAuth2. Este protocolo parte de un token que hace una delegación de una serie de permisos específicos para un usuario concreto. Para obtener este token, es necesario que el usuario se identifique en Office 365 al menos una vez, por lo que tiene que introducir sus credenciales en una pantalla de autenticación que se muestra en pantalla.

Sin embargo, hay una serie de aplicaciones en las que este flujo de autenticación no es posible, aplicaciones que ejecutan tareas en background y en las que no hay un usuario como pueden ser los webjobs. Para este tipo de aplicaciones, el protocolo OAuth2 proporcionar un flujo adicional de autenticación y es el que vamos a usar para acceder a la API de Microsoft Graph en una aplicación que se va a ejecutar en un webjob de Azure. Podemos usar este flujo de autenticación, de momento, para obtener acceso a las API de contactos, mail y calendario. He estado probando y para OneDrive aún no está soportado.

Antes de nada, os voy a dejar los enlaces donde he encontrado toda la información y el tutorial para poder configurar la aplicación y todo lo que hay que hacer en Azure para que funcione correctamente.

http://www.eliostruyf.com/building-daemon-or-service-app-with-the-microsoft-graph-api/

http://blogs.msdn.com/b/exchangedev/archive/2015/01/21/building-demon-or-service-apps-with-office-365-mail-calendar-and-contacts-apis-oauth2-client-credential-flow.aspx

Creando los permisos para la aplicación en AAD

El primer paso que tenemos daremos será configurar los permisos de nuestra aplicación en Azure Active Directory.

1.- Accedemos a Azure, el portal clásico y vamos a la sección de Azure Active Directory. Una vez ahí nos vamos al apartado de aplicaciones, donde añadiremos una nueva aplicación.

Captura de pantalla 2016-01-24 a las 16.28.51

2.- Seleccionamos la opción de “Agregar una aplicación que mi organización está desarrollando” y de tipo “Aplicación Web y/o API Web” ya que la otra opción no soporta el tipo de autenticación app-only que vamos a configurar.

Captura de pantalla 2016-01-24 a las 16.29.04

Captura de pantalla 2016-01-24 a las 16.29.28

3.- Indicamos la URL de inicio de sesión y la URI de la aplicación. El primero de los dos valores, no es importante, en este caso hemos añadido la url donde el webjob estará alojado.

Captura de pantalla 2016-01-24 a las 16.30.49

4.- Vamos a la nueva aplicación que hemos añadido y en la pestaña “Configuración”, vamos a la parte final, de permisos, ahí añadimos permisos para Microsoft Graph.

Captura de pantalla 2016-01-24 a las 16.31.30

Captura de pantalla 2016-01-24 a las 16.31.42

5.- Una vez añadidos, tenemos que indicar en el desplegable de “Permisos de la Aplicación” (ya que los permisos los vamos a conceder directamente a la aplicación), los servicios para los que tendrá permisos.

Captura de pantalla 2016-01-24 a las 16.31.57

6.- Una vez terminado pulsamos Guardar.

Creando y configurando los certificados

A continuación necesitamos generar los certificados y configurarlos en nuestra aplicación web (que también tendremos que crear) y en la aplicación que acabamos de crear en nuestro AAD.

1.- En primer lugar, abrimos la consola de comandos y creamos el certificado, sustituyendo Tenant y AppName por los valores que correspondan

makecert -r -pe -n “CN=Tenant AppName Cert” -b 11/25/2015 -e 11/25/2017 -ss my -len 2048

2.- A continuación, vamos a exportar dicho certificado como PFX y CER. Para ello primero abrimos el Microsoft Management Console (mmc.exe).

3.- Una vez aquí, vamos a archivo->Agregar o quitar complemento y ahí añadimos la opción certificados.

cer1

4.- A continuación, buscamos el certificado que acabamos de crear y hacemos click sobre el botón derecho del mismo y seleccionamos Todas las tareas->exportar

cer3

Tenemos dos opciones de exportación, “Exportar la clave privada” y “No exportar la clave privada”, ejecutaremos la exportación dos veces y completaremos el asistente para cada una de éstas opciones, generando los dos certificados.

5.- Ahora, volvemos nuevamente a la aplicación que hemos creado en Azure Active Directory para configurar el certificado en la misma. Esto no se puede hacer a través de una interfaz gráfica, por lo que habrá que hacerlo modificando el manifest. Para ello, lo primero que debemos hacer es recuperar las claves del certificado CER que hemos obtenido, esto lo podemos hacer por medio del siguiente script de powershell:


$certPath = Read-Host "Enter certificate path (.cer)"
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$cert.Import($certPath)
$rawCert = $cert.GetRawCertData()
$base64Cert = [System.Convert]::ToBase64String($rawCert)
$rawCertHash = $cert.GetCertHash()
$base64CertHash = [System.Convert]::ToBase64String($rawCertHash)
$KeyId = [System.Guid]::NewGuid().ToString()
Write-Host "base64Cert:" $base64Cert
Write-Host "base64CertHash:" $base64CertHash
Write-Host "KeyId:" $KeyId

Este script nos devuelve las tres claves que nos hacen falta.

6.- Ahora, descargamos el manifest de la aplicación:

Captura de pantalla 2016-01-24 a las 17.46.09

Captura de pantalla 2016-01-24 a las 17.46.25

7.- Y sustituimos la siguiente cadena:


"keyCredentials": []

por


"keyCredentials": [
 {
 "customKeyIdentifier": "base64CertHash",
 "keyId": "KeyId",
 "type": "AsymmetricX509Cert",
 "usage": "Verify",
 "value": "base64Cert"
 }
],

Sustituyendo los valores “base64CertHash”, “KeyId” y “base64Cert”, por la cadena correspondiente obtenida de la ejecución del script powershell anterior.

8.- Ahora tenemos que volver a cargar el manifest modificado en la aplicación con lo que habremos terminado de configurar los permisos de nuestra app en Azure Active Directory.

9.- A continuación, creamos una aplicación web de Azure y cargamos el certificado PFX en la misma.

Captura de pantalla 2016-01-24 a las 17.57.36

Es necesario escalar la aplicación web a nivel básico

Captura de pantalla 2016-01-24 a las 17.58.28

En la pestaña configuración de la aplicación web, hacemos click en Cargar un certificado

Captura de pantalla 2016-01-24 a las 17.59.15

Captura de pantalla 2016-01-24 a las 18.01.35

En la sección de configuración de aplicación, añadimos el valor WEBSITE_LOAD_CERTIFICATES con la huella digital que hemos obtenido al cargar el certificado.

Captura de pantalla 2016-01-24 a las 18.07.42

Y por fin, habremos configurado los certificados en Azure. Ahora no solo nos queda publicar y probar el WebJobs.

Creando y publicando el webjob en Azure 

Vamos a crear una nueva aplicación de consola con el código del WebJob que vamos a publicar.

Para esta aplicación hay que añadir las siguientes referencias:

  • System.Configuration

Y Añadir a través de Nuget la librería ADAL y RestSharp.

Además en el app.config, debemos añadir las siguientes propiedades:


 <appSettings>;
 <add key="GraphUrl" value="https://graph.microsoft.com"/>;
 <add key="ClientId" value="clientidaad"/>;
 <add key="Thumbprint" value="thumbprint"/>;
 <add key="Authority" value="https://login.windows.net/tenant.onmicrosoft.com/"/>;
 </appSettings>;

Debemos de sustituir el clientidaad por el ClientId de la aplicación que hemos creado en el Azure Active Directory, el valor de thumbprint por la huella obtenida al cargar el certificado en la aplicación web y por último sustituir el valor tenant por el nombre de nuestra organización.

Lo que nos queda ahora, es el código de la aplicación de consola de ejemplo, que es el siguiente:


class Program
 {
 private static readonly string GraphUrl = ConfigurationManager.AppSettings["GraphUrl"];
 private static readonly string ClientId = ConfigurationManager.AppSettings["ClientId"];
 private static readonly string Authority = ConfigurationManager.AppSettings["Authority"];
 private static readonly string Thumbprint = ConfigurationManager.AppSettings["Thumbprint"];

static void Main(string[] args)
 {
 //&nbsp;Recuperar el certificado
 var certificate = GetCertificate();

//&nbsp;Recuperar el token de acceso
 var token = GetAccessToken(certificate);

//&nbsp;Obtener los contactos
 var client = new RestClient(GraphUrl);
 var request = new RestRequest("/v1.0/users/&lt;userid&gt;/contacts", Method.GET);
 request.AddHeader("Authorization", "Bearer " + token.Result);
 request.AddHeader("Content-Type", "application/json");
 request.AddHeader("Accept", "application/json");

var response = client.Execute(request);
 var content = response.Content;

Console.WriteLine(content);

}

private static X509Certificate2 GetCertificate()
 {
 X509Certificate2 certificate = null;
 var certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
 certStore.Open(OpenFlags.ReadOnly);
 var certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, Thumbprint, false);

 if (certCollection.Count &gt; 0)
 {
 certificate = certCollection[0];
 }
 certStore.Close();
 return certificate;
 }

private static async Task&lt;string&gt; GetAccessToken(X509Certificate2 certificate)
 {
 var authenticationContext = new AuthenticationContext(Authority, false);
 var cac = new ClientAssertionCertificate(ClientId, certificate);
 var authenticationResult = await authenticationContext.AcquireTokenAsync(GraphUrl, cac);
 return authenticationResult.AccessToken;
 }
 }

El último paso es publicar la aplicación como un WebJob de Azure. Para ello, haciendo click en botón derecho sobre el proyecto de consola, seleccionaremos la opción Publish as Azure WebJob

public1

A continuación, indicamos la planificación del WebJob:

public2

Seleccionamos la aplicación web a la que queremos asociar el WebJob:

public3

Por último publicamos el WebJob.

public4

Y esto es todo, podemos probarlo ejecutando una vez el WebJob en Azure y ver que al final, se devuelven los contactos del usuario que le hemos indicado.

Captura de pantalla 2016-01-24 a las 19.05.09

Hasta aquí el post de hoy. Con lo que hemos visto en este post, podemos consumir información de Office 365 desde aplicaciones en background que no nos permiten usar el flujo habitual de autenticación del protocolo OAuth2.

Espero que os haya resultado interesante, en futuros post usaremos todo lo que hemos visto hoy para indexar contenido de Office 365 en Azure Search a través de un WebJob como el que hemos creado hoy.

Hasta la próxima

 

 

Anuncios

Primeros pasos con Azure Search: Los índices

Muy buenas a todos.

Hoy, quiero empezar a hablar sobre un servicio al que tenía muchas ganas de dedicarle un tiempo, el servicio Azure Search. A lo largo de los próximos días haremos un overview completo sobre este servicio y veremos cómo funciona y qué podemos hacer con él.

El servicio Azure Search permite añadir una robusta experiencia de búsqueda a nuestras aplicaciones usando una API REST o una SDK .NET sin gestionar la infraestructura de búsqueda o llegar a ser un experto en búsqueda.

En el siguiente enlace, podréis ver una descripción más profunda sobre este servicio:

https://azure.microsoft.com/en-us/documentation/articles/search-what-is-azure-search/

En el post de hoy, vamos a  ver cómo configurar el servicio de búsqueda y el primer paso a la hora de poder comenzar a trabajar con el mismo, los índices.

Configurando el servicio de búsqueda en Azure

Lo primero que debemos hacer es aprovisionar el servicio de búsqueda. Para ello, en el nuevo portal de Azure, iremos a Nuevo->Datos y almacenamiento->Búsqueda de Azure, como se ve en la siguiente imagen.

Captura de pantalla 2016-01-16 a las 16.08.41

A continuación, tenemos que indicar los datos básicos de configuración del servicio: el nombre del servicio, el grupo de recursos al que pertenece, la ubicación y el nivel de precios.

Captura de pantalla 2016-01-16 a las 16.11.14

Para este servicio disponemos de dos niveles de precios, con diferentes características que harán que en función de la finalidad del uso del servicio, elijamos uno u otro.

Captura de pantalla 2016-01-18 a las 23.27.48

Una vez configurados todos los parámetros, pulsamos sobre crear y el servicio se aprovisionará, con lo que ya lo tendremos listo para usar.

Los índices en Azure Search

Un índice es el medio principal para organizar y buscar documentos en el servicio de búsqueda de Azure, similar a cómo se organizan los registros en una tabla de una base de datos. Cada índice tiene una colección de documentos que cumplen el esquema de índice (nombres de campo, tipos de datos y propiedades), pero los índices también especifican construcciones adicionales (proveedores de sugerencias, perfiles de puntuación y configuración de CORS) que definen otros comportamientos de la búsqueda.

Por tanto es necesario, como paso previo a subir documentos al servicio de búsqueda, definir un índice y su estructura, que no es más que una estructura de tablas que acepta datos y consultas. Los índices en el servicio de búsqueda de Azure, se pueden crear de tres formas, o desde el propio portal de Azure o por medio de código usando una SDK de .NET o una API REST.

A continuación, vamos a ver como crear un índice por medio de la SDK y la API REST en un solución de consola de Visual Studio.

Creando índices en Azure Search

Para ello, vamos a crear una aplicación de consola en Visual Studio. Una vez creada, tenemos que añadir las referencias a la SDK de Azure Search. Esto lo conseguiremos ejecutando en la consola de Nuget el siguiente comando:

Install-Package Microsoft.Azure.Search -Pre

Nos instalará todas las referencias necesarias para poder trabajar con la SDK correspondientes. Además, para el resto del ejemplo necesitaremos añadir otras dos referencias:

System.Configuration (Con la que tendremos la posibilidad de hacer referencia al App.Config de nuestra aplicación)

System.Net (Necesaria para poder llamadas REST desde la aplicación de consola).

Una vez añadidas todas las referencias, podemos pasar a ver el código. Para la aplicación de consola hemos creado un sencillo menú que nos permite elegir entre las dos opciones que tiene nuestra aplicación, crear un índice usando la SDK, o crear un índice por medio de API REST. Este es el código que implementa el menú:


class Program
 {
 static void Main(string[] args)
 {
 int option;

do
 {
 option = menu();
 switch (option)
 {
 case 1:

SearchServiceClient serviceClient = GetSearchServiceClient();

DeleteIndexIfExists(serviceClient,"files");
 CreateIndexUsingNetSDK(serviceClient,"files");

Console.WriteLine("Index successfully created");

break;
 case 2:

SearchServiceClient serviceClient2 = GetSearchServiceClient();

DeleteIndexIfExists(serviceClient2,"files");

CreateIndexUsingRESTAPI("files");

break;
 default:
 break;
 }
 }
 while (option != 0);
 }

private static int menu()

 {
 int value = 0;

Console.WriteLine("-----------------------------------");
 Console.WriteLine("1.- Create an index using .NET SDK");
 Console.WriteLine("2.- Create an index using REST API");
 Console.WriteLine("0.- Exit");
 Console.WriteLine("-----------------------------------");

do
 {
 Console.WriteLine("Enter an valid option: ");
 value = Int16.Parse(Console.ReadLine());
 }
 while (value &lt; 0 &amp;&amp; value &gt; 2);

return value;
 }

}

Como veréis, este código hace referencia a cuatro funciones, que son la clave de este post y que son en las que nos vamos a centrar para describir a continuación.

GetSearchServiceClient: Esta función nos va a devolver un objeto de tipo SearchServiceClient que es el que nos permite interaccionar con el servicio de búsqueda.

private static SearchServiceClient GetSearchServiceClient()
{
string serviceName = ConfigurationManager.AppSettings["SearchServiceName"];
string apiKey = ConfigurationManager.AppSettings["ApiKey"];

SearchServiceClient serviceClient = new SearchServiceClient(serviceName, new SearchCredentials(apiKey));

return serviceClient;
}

Este método solo se encarga de crear un nuevo objeto de tipo SearchServiceClient. El constructor de esta clase, recibe dos parámetros: el nombre del servicio que podemos obtener de la URL del servicio (https://%5Bnombreservicio%5D.search.windows.net) y la api-key para la autenticación del servicio. Ambos parámetros, para el ejemplo, los he almacenado en el App.Config de la aplicación de consola.

DeleteIndexIfExists: Una buena práctica, es comprobar si el índice existe previamente antes de crearlo y si es así, eliminarlo. De esto se encarga la función que vamos a ver a continuación:

private static void DeleteIndexIfExists(SearchServiceClient serviceClient, string indexName)
{
if (serviceClient.Indexes.Exists(indexName))
serviceClient.Indexes.Delete(indexName);
}

CreateIndexUsingNetSDK: Este método, crea un índice usando la SDK .NET de Azure Search.

private static void CreateIndexUsingNetSDK(SearchServiceClient serviceClient, string indexName)
{
var index = new Index()
{
Name = indexName,
Fields = new[]{
new Field("DocumentId",DataType.String) { IsKey = true, IsRetrievable = true, IsFilterable = true},
new Field("Name", DataType.String) { IsRetrievable = true, IsSearchable = true, IsFilterable = true},
new Field("Url", DataType.String) { IsRetrievable = true, IsSearchable = true, IsFilterable = true},
new Field("CreatedDate", DataType.String) { IsRetrievable = true, IsSearchable = true, IsFilterable = true}
}
};

serviceClient.Indexes.Create(index);
}

El método recibe como parámetros el objeto que hace referencia al servicio de búsqueda y el nombre del índice que vamos a crear. A continuación crea un objeto de tipo Index y le indica el nombre y los campos que va a tener dicho índice.

Cada campo se crea por medio de un objeto de tipo Field. El constructor de dicho objeto tiene dos parámetros, el nombre del campo y el tipo del mismo. Luego, podremos indicar otras propiedades de cada campo, como se ve en el ejemplo.

Por último, se crea el índice propiamente dicho con la llamada serviceClient.Indexes.Create(index) que vemos al final de la función.

CreateIndexUsingRESTAPI: La última función, crea un índice por medio de una llamada API REST.:

private static void CreateIndexUsingRESTAPI(string indexName)
{
string serviceName = ConfigurationManager.AppSettings["SearchServiceName"];
string apiKey = ConfigurationManager.AppSettings["ApiKey"];

string body = @"{
'name': '" + indexName + @"',
'fields': [
{'name': 'DocumentId', 'type': 'Edm.String', 'key': true, 'searchable': true},
{'name': 'Name', 'type': 'Edm.String'},
{'name': 'Url', 'type': 'Edm.String'},
{'name': 'CreatedDate', 'type': 'Edm.String'}
]
}";

using(var client = new HttpClient())
{
client.BaseAddress = new Uri("https://" + serviceName + ".search.windows.net");

client.DefaultRequestHeaders.Add("api-key", apiKey);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

var response = client.PostAsync("/indexes?api-version=2015-02-28", new StringContent(body,Encoding.UTF8, "application/json")).Result;

if(response.IsSuccessStatusCode)
{
Console.WriteLine("Index successfully created");
}
else
{
Console.WriteLine("HTTP Status: " + response.StatusCode.ToString() + " - Reason: " + response.ReasonPhrase);
}
}
}

Esta función en primer lugar, crea una cadena de texto con la estructura JSON del índice, y la almacena en la variable body. A continuación, establece la URL base del servicio REST de Azure Search (https://%5Bnombreservicio%5D.search.windows.net).

Tras esto, hace la llamada haciendo uso de la clase HttpClient. A esta llamada, de tipo POST y con la URL (/indexes?api-version=2015-02-28), hay que añadirle en las cabeceras la api-key del servicio.

Una vez que se recibe la respuesta, podremos comprobar si hemos recibido un estado correcto, en cuyo caso el índice se habrá creado correctamente, o no.

Probando el ejemplo
Una vez terminado el código, si depuramos el ejemplo nos aparece una pantalla como la siguiente:

test

Pulsando sobre cualquier de las dos opciones, veremos que se ejecuta correctamente, y si accedemos posteriormente al portal de Azure, al servicio de búsqueda, veremos que el índice se ha creado.

Captura de pantalla 2016-01-18 a las 20.44.17
Os voy a dejar algunos enlaces en los que me he basado y que os pueden resultar de interés.

https://azure.microsoft.com/en-us/services/search/

https://azure.microsoft.com/en-us/documentation/articles/search-get-started/

https://azure.microsoft.com/en-us/documentation/articles/search-howto-dotnet-sdk/

https://azure.microsoft.com/en-us/documentation/articles/search-create-service-portal/

Y esto es todo hasta aquí, en este post, hemos configurado y puesto en marcha el servicio de búsqueda creando los índices. En próximos posts, comenzaremos a indexar contenido y veremos cómo podemos hacerlo y consumiremos el servicio de búsqueda con todas las opciones de que disponemos para trabajar con el mismo.

Espero, como siempre, que os haya resultado interesante.

Hasta la próxima.

Versión 1.0 de la API de Office 365: Microsoft Graph

Muy buenas a todos.

Hace unos meses, desde Microsoft se lanzó lo que se denominó “Office 365 unified API” en su versión preview. El objetivo, era agrupar en un mismo endpoint, todas las APIs que permitieran el acceso a los distintos servicios en la nube que se ofrecen y hacerlo siguiendo el estándar de la industria, por medio de una API REST. Esto supuso un importante avance en la forma en que se podía acceder a los servicios en la nube. Sobre todo esto hablé en una entrada del blog, en la que os contaba cómo usar esta API, que os dejo a continuación:

La nueva API de Office 365 Unificada

Lo que os quiero contar hoy, es que recientemente, se ha publicado la v1.0 de esta API que desde ahora pasará a llamarse Microsoft Graph y que incluye una serie de servicios que dejan de estar en Preview y que según nos dicen pueden ya ser usados con todas las garantías en nuestros proyectos finales.

Today-at-Connect-1

Como dice en la página principal de la documentación y de la que os dejo el enlace más adelante:

One endpoint to rule them all
No more obtaining separate tokens for different services or calling a different endpoint for each API.
Leverage the power of Microsoft Graph, a unified API endpoint, for accessing data, intelligence, and insights coming from the Microsoft cloud.

Más que entrar en detalle en cómo podemos usarla, quería dejaros una serie de enlaces interesantes e información sobre la misma, ya que la forma de uso sigue siendo muy similar a la que ya os comenté en el artículo al que me he referido anteriormente.

Para usar esta primera versión, lo único que tendremos que hacer es cambiar el endpoint. Si antes accedíamos a la API a través del siguiente enlace:

https://graph.microsoft.com/beta

Ahora lo haremos a través de este:

https://graph.microsoft.com/v1.0

La versión beta sigue estando disponible, y es donde encontraremos todo el conjunto de opciones disponibles, los que ya han pasado a la primera versión, y los que aún se encuentran en preview. Evidentemente se puede seguir usando esta preview, aunque desde Microsoft no recomiendan hacerlo para proyectos finales (como cualquier versión de estas características). En el siguiente enlace podéis ver los distintos servicios que se ofrecen en una versión y otra:

http://graph.microsoft.io/docs

Y aquí entra otro de los motivos que más me ha sorprendido, y es la cantidad de documentación que Microsoft se ha encargado de generar para que la adopción de esta API sea relativamente rápida. Y  no es que desde Microsoft no se genere documentación suficiente sobre cómo usar sus SDK´s o todo lo que ponen a disposición de los desarrolladores, es que para Microsoft Graph lo han hecho de una forma ligeramente diferente, que a mi particularmente me gusta (me recuerda más a como encontramos la documentación para la cantidad de proyectos que tenemos disponible para desarrolladores hoy en día, por decirlo de alguna forma, más actual).

Toda la información sobre esta API la podemos encontrar en:

http://graph.microsoft.io

En Channel 9 tenéis este vídeo, muy recomendable, donde podéis ver en 11 minutos bastante información sobre Microsoft Graph:

https://channel9.msdn.com/Events/Visual-Studio/Connect-event-2015/301

Si queréis testear y probar los endpoint, encontraréis a vuestra disposición este enlace:

https://graphexplorer2.azurewebsites.net/

Y bueno, os dejo, una serie de enlaces más para que podáis encontrar más información:

https://blogs.office.com/2015/11/18/today-at-connect-introducing-the-microsoft-graph/

http://dev.office.com/getting-started/office365apis

http://dev.office.com/chooseapiendpoint

Y esto es todo por hoy, espero que lo encontréis interesante y os animo a que le echéis un vistazo a toda la información que hay.

Un saludo a todos

Paginando en SharePoint 2013 con API REST

Muy buenas a todos,

Hoy quería contaros un tema con el que me he estado peleando hoy en mi proyecto con el que estoy haciendo un uso bastante intensivo de la API REST de SharePoint con AngularJS. Una experiencia que os debo decir, me está encantando.

El tema ha venido a la hora de “paginar” una consulta en SharePoint que estaba haciendo vía API REST. Según pensaba, usando las opciones $top y $skip se podría hacer la paginación sin problemas. Para ello pensaba usar el endpoint que habitualmente uso para trabajar con las listas de SharePoint

http://<misitio>/_api/web/lists/getbytitle(‘lista&#8217;)/items

Mi sorpresa ha sido darme cuenta que con este endpoint la opción $skip no funciona. Indagando un poco he descubierto que efectivamente, esta opción no funciona para lista de elementos, solo funciona para colecciones de datos como colecciones de listas.

http://sharepoint.stackexchange.com/questions/45719/paging-using-rest-odata-with-sp-2013

https://msdn.microsoft.com/en-us/library/office/fp142385.aspx

Para hacer la paginación via API REST podemos usar el antiguo endpoint OData V2 listdata.svc. Con esta versión si funciona la opción $skip correctamente:

http://<misitio>/_vti_bin/ListData.svc/<lista&gt;

Ejemplo: http://<misitio>/_vti_bin/ListData.svc/<lista&gt;?$top=2&$skip=2

Salvando este inconveniente sobre el endpoint que debemos usar, usando estas dos opciones $top y $skip, podremos paginar nuestras consultas de una forma muy sencilla.

Y esto es todo por hoy, espero que os resulte útil, como siempre, y que si os habéis encontrado con esta problemática, tardéis menos tiempo en resolverlo.

Saludos.

[OffTopic]

No suelo acostumbrar a hacer nada de esto. Pero hoy ha nacido mi sobrino, del que tendré la oportunidad además de ser su padrino. Así que esta entrada de hoy va dedicada a él y a sus padres. Aunque no lo podré conocer hasta dentro de 15 días porque me ha salido “conejero” y está en Lanzarote, que ilusión más grande.

Resolviendo el error 403 cuando añadimos elementos por medio de API REST en SharePoint 2013

Hola a todos,

Hoy os quiero mostrar en una breve entrada cómo resolver el error 403 que surge cuando hacemos una petición POST para añadir elementos en una lista a través de la API REST de SharePoint 2013.

En mi caso, estoy trabajando en un proyecto en el que utilizo SharePoint y AngularJS, por eso, los ejemplos de las peticiones que os voy a mostrar, son usando el módulo $http de AngularJS.

Cuando hacemos una petición como esta:

$http({
            url: urlBase,
            method: "POST",
            data: JSON.stringify(likeJSON),
            headers: {
                "accept": "application/json;odata=verbose",
                "content-type": "application/json;odata=verbose",
            }
        });

El resultado que nos devuelve es un error como éste:

“The security validation for this page is invalid and might be corrupted. Please use your web browser’s Back button to try your operation again.”

Para solucionarlo, debemos añadir en la cabecera de la petición el parámetro X-RequestDigest como sigue:

$http({
            url: urlBase,
            method: "POST",
            data: JSON.stringify(likeJSON),
            headers: {
                "accept": "application/json;odata=verbose",
                "content-type": "application/json;odata=verbose",
                "X-RequestDigest": requestDigest,
            }
        });

Esta cabecera contiene información conocida como form digest. Ésta es un objeto que se inserta en una página por SharePoint y es usado para validar las peticiones de cliente

La pregunta ahora es: ¿Cómo conseguimos ese valor?. Pues lo podemos obtener por medio de la siguiente petición:

$http({
        url: "/_api/contextinfo",
        method: "POST",
        headers: {
            "accept": "application/json;odata=verbose",
            "content-type": "application/json;odata=verbose"
        }
    }).then(function (response) {
        requestDigest = response.data.d.GetContextWebInformation.FormDigestValue;
    },
    function () {

    });

Por medio de una petición POST al servicio REST con el EndPoint en la url /_api/contextinfo

De esta forma conseguimos resolver el error 403 que nos devuelven las peticiones POST al servicio REST en SharePoint.

Os dejo un enlace que me ha servido de guía, y en el que me he basado.

http://blogs.msdn.com/b/nadeemis/archive/2012/10/23/tip-handling-http-403-forbidden-when-querying-the-search-rest-service-using-the-postquery-method.aspx

Espero que os haya resultado interesante.

Un saludo

Sharepoint-search: un componente Polymer para usar la búsqueda de SharePoint On-Line

Muy buenas a todos.

En una entrada anterior, os contaba cómo usar la búsqueda de SharePoint a través de su servicio API REST. El enlace ha dicha entrada es la siguiente:

Search Driven Development. Usando la API REST de SharePoint para el servicio de búsqueda

Al final de esta entrada os contaba que como objetivo, tenía el desarrollo de un componente Polymer que permitiera usar este servicio, algo que me resultaba muy útil e interesante. Después de varios días de trabajo, quería compartir con vosotros una primera “versión” de dicho componente. Aún queda trabajo por delante, pero ya hay un punto de partida funcional a partir del cual seguir trabajando.

El componente debía cumplir con los siguientes requisitos:

  • Permitir la posibilidad de usar refinadores.
  • Permitir la posibilidad de establecer opciones de ordenamiento.
  • Controlar la paginación y el número de resultados por página en los resultados de búsqueda.
  • Dar la posibilidad de estilar el contenido del componente en función de como se desee.

El componente “sharepoint-search” nos permite cubrir estos requisitos. Para usarlo, necesitaremos tener cargados los ficheros de Polymer y los propios del componente y utilizaremos la etiqueta de la siguiente manera:

<sharepoint-search tenant="organizer" clientid="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" refinement="author,size,created" ordination="size,created" pageElement="15">
</sharepoint-search>

Para que funcione correctamente, necesitaremos el clientID de una aplicación de Azure AD con los permisos para el servicio de búsqueda de SharePoint concedidos, y obviamente estar logado en nuestra aplicación contra esa aplicación de Azure AD, para ello podemos usar el componente para la autenticación que creé en una entrada anterior y seguir los correspondientes pasos para obtener el clientid:

Registrando una aplicación en Azure Active Directory

login-azuread: Un Componente Web con Polymer y Javascript ADAL

Además deberemos de configurar la aplicación de nuestra Azure AD para permitir el OAuth 2.0 implicit grant flow, lo haremos de la siguiente forma:

  • Acceder a la configuración de la aplicación de Azure AD correspondiente
  • Descargamos el manifiesto
  • Modificamos el parámetro oauth2AllowImplicitFlow dándole el valor true en lugar de false que tiene establecido inicialmente
  • Cargamos de nuevo el manifiesto modificado

El componente “sharepoint-search” permite configurar los siguientes parámetros:

  • tenant: Indicar el tenant al que nos queremos conectar
  • clientid: Clientid de la aplicación con los permisos adecuados
  • refinement: Propiedades para las cuales queremos obtener refinamiento
  • ordination: Propiedades por las que queremos hacer ordenación
  • pageElement: Número de elementos por página a mostrar

Una vez que lo añadimos, funciona como vemos a continuación.

sharepointsearchinit

sharepointsearchsearched

Por defecto, el componente nos muestra los resultados en una lista con el título del documento y la posibilidad de descargar el mismo. Aunque como se indica a continuación, en futuras mejoras, se trabajará en flexibilizar este aspecto del componente.

Os dejo el enlace a mi cuenta de GitHub donde podéis descargar el código del componente web y donde podréis encontrar información sobre cómo usarlo.

https://github.com/jcroav/sharepoint-search

Y esto es todo por hoy, este componente aún tiene áreas de mejora, a las que intentaré dedicarle tiempo en las próximas semanas. Os dejo algunas de las ideas:

  • El componente está pensado para que los resultados de búsqueda usen a su vez un subcomponente para ser mostrados, queda pendiente de desarrollar esa parte.
  • Completar el componente con la funcionalidad de sugerencias
  • Mejorar toda la parte de estilado
  • Refactorizar y trabajar sobre la optimiazación del código para hacer un componente más robusto

Como siempre, todo aquel que quiera colaborar, o quiera usar el componente que os he mostrado hoy para mejorarlo, es libre de hacerlo. El objetivo de estos desarrollos, a parte de para aprender a usar las funcionalidades de Office 365 y SharePoint On-Line, es contribuir a la comunidad.

Un saludo y espero que os sea útil

Search Driven Development. Usando la API REST de SharePoint para el servicio de búsqueda

Muy buenas a todos,

Recientemente he publicado algunos artículos sobre Search Driven Development para SharePoint Online y SharePoint 2013. Además hace poco tiempo tuve la oportunidad de participar en una mesa redonda con la gente de MadPoint en la que se hablaba de técnicas de desarrollo tanto para SharePoint OnLine como 2013 y Office 365. En esta mesa redonda salió la posibilidad de usar en los desarrollos la API REST del servicio de búsquedas de SharePoint y los nuevos frameworks de Javascript para crear nuestra propia solución basada en búsquedas.

Hasta entonces no había tenido la oportunidad de probar esta API REST, pero hoy os traigo una entrada en la que os quiero enseñar como usar las búsquedas a través de la API REST que nos proporciona esta versión de SharePoint.

En una primera parte de la entrada os enseñaré que consultas REST podemos hacer al servicio y lo que nos devuelve y a continuación veremos ejemplos de cómo utilizar el mismo en una app for SharePoint (o SharePoint Add-ins??).

Llamadas REST para el servicio de búsqueda

Uno de los aspectos importantes que tenemos que tener en cuenta cuando trabajamos con la API REST del servicio de búsqueda es que las consultas al mismo podemos hacerlas a través de peticiones GET y a través de peticiones POST. Esto es así, porque como es sabido por todos, las peticiones GET tienen limitaciones en cuanto al número de caracteres y puede ocurrir que una consulta que queramos hacer sobrepase ese límite, por lo que tendremos la oportunidad de usar las peticiones POST para superar dicha restricción.

A continuación voy a enseñar algunos ejemplos básicos de cómo utilizar ambos tipos de peticiones, aunque las posibilidades que tenemos a la hora de hacer las peticiones son muy extensas. Para esto os dejo el enlace a la referencia de la MSDN, donde podréis analizar al completo las opciones de la API.

https://msdn.microsoft.com/es-es/library/office/jj163876.aspx

Usando peticiones GET

Las peticiones GET se hacen a través de la siguiente URL:

http://servidor/_api/search/query

Consulta con un texto a buscar
/_api/search/query?querytext=’textoAconsultar’
Consulta usando una query
/_api/search/query?querytemplate='{searchterms} FileExtension: doc’
Usando la ordenación
/_api/search/query?querytext=’textAconsultar’&sortlist=’created:ascending,rank:descending,’
Indicando los refinadores a utilizar
/_api/search/query?querytext=’textAconsultar’&refiners=’author,size,fileExtension’
Filtrando la consulta usando el refinamiento
/_api/search/query?querytext=’textAconsultar’&refinementfilters=’fileExtension:equals(“docx”)’

Usando peticiones POST

Las peticiones POST se hacen a través de la siguiente URL:

http://servidor/_api/search/postquery

Consulta con un texto a buscar
{'request':
    { 
      '__metadata' : {'type' : 
                   'Microsoft.Office.Server.Search.REST.SearchRequest'},
      'Querytext' : 'ejemploAconsultar'
    }
}

Son muy importantes las mayúsculas y minúsculas en las consultas usando el método POST. Por ejemplo, en la consulta anterior, tenemos que usar ‘Querytext’ y no ‘QueryText’ o ‘querytext’ como hacemos en la consulta GET.

Estas consultas devuelven un JSON con toda la información necesaria de la búsqueda. Al igual que el resto de servicios REST, este de búsquedas es compatible con JSON Light, por lo que si lo deseeamos podemos obtener respuestas más ligeras y rápidas. Los resultados se encuentran en el objeto devuelto en:

data.body.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results (para GET)
data.body.d.postquery.PrimaryQueryResult.RelevantResult.Table.Rows.results (para POST)

Este elemento “results” json es un array que contiene una fila para cada uno de los resultados, en cada columna o celda de esta fila tenemos una de las propiedades del objeto devuelto. Para simplificar un poco la tarea, os voy a indicar en qué posición se encuentra la información que habitualmente es más importante de los objetos devueltos.

  • Posición 3: Title
  • Posición 4: Author
  • Posición 5: Size
  • Posición 6: Path
  • Posición 7: Description
  • Posición 8: Write
  • Posición 11: HitHighlightedSummary
  • Posición 18: FileExtension
  • Posición 31: FileType

El mismo servicio, si le hemos indicado para qué propiedades queremos obtener refinadores, nos devuelve los refinadores para esa búsqueda en:

data.body.d.query.PrimaryQueryResult.RefinementResults.Refiners.results

Esto nos devuelve, para cada refinador, un objeto de la siguiente forma:

{'Entries':
    {'results'
         {
             RefinementCount: count,
             RefinementToken: token,
             RefinementName: name,
             RefinementValue: value
         },
         {
             RefinementCount: count,
             RefinementToken: token,
             RefinementName: name,
             RefinementValue: value
         }
    },
  'Name':Refiner
}

Cada uno de los elementos contenidos en el objeto Entries, nos indica una opción de refinamiento para el refinado correspondiente. A continuación vamos a ver cómo he usado este servicio en una aplicación para SharePoint a modo de prueba.

Usando la API REST para búsquedas en una App for SharePoint

El ejemplo lo he realizado usando JQuery para manipular el DOM de la página del mismo. En primer lugar vamos a ver una captura de la aplicación para explicar que hace cada sección de la misma:

appsearchexample

En la aplicación que os quiero enseñar he hecho 5 ejemplos de como usar la API REST del servicio de búsqueda:

  • El primero ejemplo es una consulta usando GET a partir de una cadena de texto.
  • El segundo ejemplo es una consulta por POST a partir de una cadena de texto.
  • El tercer ejemplo usar una query para hacer la llamada al servicio de búsqueda por GET.
  • En el cuarto ejemplo se usa la ordenación.
  • El quinto ejemplo utiliza los refinadores.

Código del primer ejemplo

$("#searchbutton1").click(function () {

        var searchText = $("#searchbox1").val();

        executor.executeAsync({
            method: "GET",
            url: appweburl + "/_api/search/query?querytext='" + searchText + "'",
            headers: {
                "accept": "application/json;odata=verbose",
                "content-type": "application/json;odata=verbose"
            },
            success: function (data) {

                var element = document.getElementById("resultsList1");

                $('#resultsList1 > li').remove();

                var jsonObject = JSON.parse(data.body);

                console.log(jsonObject);

                var results = jsonObject.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results;

                for(var i = 0; i < results.length; i++)
                {
                    var li = document.createElement("li");

                    li.innerText = results[i].Cells.results[3].Value + "
" + results[i].Cells.results[5].Value + "&nbsp;" + results[i].Cells.results[4].Value + "
" + results[i].Cells.results[11].Value;

                    element.appendChild(li);
                }
            },
            error: function (data) {
            }
        });

        return false;

    });

Código del segundo ejemplo

$("#searchbutton2").click(function () {

        var searchText = $("#searchbox2").val();

        executor.executeAsync({
            method: "POST",
            url: appweburl + "/_api/search/postquery",
            body: "{'request': { '__metadata' : {'type' : 'Microsoft.Office.Server.Search.REST.SearchRequest'}, 'Querytext' : '" + searchText + "' }}",
            headers: {
                "accept": "application/json;odata=verbose",
                "content-type": "application/json;odata=verbose"
            },
            success: function (data) {

                var element = document.getElementById("resultsList2");

                $('#resultsList2 > li').remove();

                var jsonObject = JSON.parse(data.body);
                var results = jsonObject.d.postquery.PrimaryQueryResult.RelevantResults.Table.Rows.results;

                for (var i = 0; i < results.length; i++) {
                    var li = document.createElement("li");

                    li.innerText = results[i].Cells.results[3].Value;

                    element.appendChild(li);
                }
            },
            error: function (data) {
            }
        });

        return false;

    });

Código del tercer ejemplo

executor.executeAsync({
        method: "GET",
        url: appweburl + "/_api/search/query?querytemplate='{searchterms} FileExtension: doc'",
        headers: {
            "accept": "application/json;odata=verbose",
            "content-type": "application/json;odata=verbose"
        },
        success: function (data) {

            var element = document.getElementById("resultsList3");

            $('#resultsList3 > li').remove();

            var jsonObject = JSON.parse(data.body);

            var results = jsonObject.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results;

            for (var i = 0; i < results.length; i++) {
                var li = document.createElement("li");

                li.innerText = results[i].Cells.results[3].Value;

                element.appendChild(li);
            }
        },
        error: function (data) {
        }
    });

Los tres primeros ejemplos son muy similares. Entre el primero y el segundo la única diferencia es que una petición se hace por GET y otra petición se hace por POST. En el tercero, se vuelve a usar una consulta por GET pero en este caso en lugar de un texto lo que se pasa es una consulta como tal. Todos los ejemplos usan una lista para representar los resultados devueltos.

Código del cuarto ejemplo

$("#searchbutton4").click(function () {

        var searchText = $("#searchbox4").val();

        executor.executeAsync({
            method: "POST",
            url: appweburl + "/_api/search/postquery",
            body: "{'request': { '__metadata' : {'type' : 'Microsoft.Office.Server.Search.REST.SearchRequest'}, 'Querytext' : '" + searchText + "' }}",
            headers: {
                "accept": "application/json;odata=verbose",
                "content-type": "application/json;odata=verbose"
            },
            success: function (data) {

                var element = document.getElementById("resultsList4");

                $('#resultsList4 > li').remove();

                var jsonObject = JSON.parse(data.body);
                var results = jsonObject.d.postquery.PrimaryQueryResult.RelevantResults.Table.Rows.results;

                for (var i = 0; i < results.length; i++) {
                    var li = document.createElement("li");

                    li.innerText = results[i].Cells.results[3].Value;

                    element.appendChild(li);
                }
            },
            error: function (data) {
            }
        });

        return false;

    });

    $("#orderbutton4").change(function () {

        var searchText = $("#searchbox4").val();
        var value = $("#orderbutton4").val();
        var order = "";

        if(value == "asc")
        {
            order = "created:ascending";
        }
        else
        {
            order = "created:descending";
        }

        executor.executeAsync({
            method: "GET",
            url: appweburl + "/_api/search/query?querytext='" + searchText + "'&sortlist='" + order + "'",
            headers: {
                "accept": "application/json;odata=verbose",
                "content-type": "application/json;odata=verbose"
            },
            success: function (data) {

                var element = document.getElementById("resultsList4");

                $('#resultsList4 > li').remove();

                var jsonObject = JSON.parse(data.body);

                var results = jsonObject.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results;

                for (var i = 0; i < results.length; i++) {
                    var li = document.createElement("li");

                    li.innerText = results[i].Cells.results[3].Value + " " + results[i].Cells.results[8].Value;

                    element.appendChild(li);
                }
            },
            error: function (data) {
            }
        });

        return false;
    });

Básicamente este ejemplo funciona de la misma forma que los anteriores. Lo único extraordinario, tiene que ver con que queremos usar la ordenación por la propiedad “size” de los resultados. Hemos creado un combo donde podemos seleccionar, si queremos, orden ascendente y descendente para el tamaño. Lo que se ha hecho es crear otro evento que se dispara cuando cambia el valor seleccionado del combo para que se haga la ordenación. Para ello se añade a la consulta la parte correspondiente (sortlist) en función de si hemos seleccionado ascendente o descendente.

Código del último ejemplo

$("#searchbutton5").click(function () {

        var searchText = $("#searchbox5").val();

        executor.executeAsync({
            method: "GET",
            url: appweburl + "/_api/search/query?querytext='" + searchText + "'&refiners='author,size'",
            headers: {
                "accept": "application/json;odata=verbose",
                "content-type": "application/json;odata=verbose"
            },
            success: function (data) {

                var element = document.getElementById("resultsList5");

                $('#resultsList5 > li').remove();

                var jsonObject = JSON.parse(data.body);

                console.log(jsonObject);

                var refiners = jsonObject.d.query.PrimaryQueryResult.RefinementResults.Refiners.results;

                $("#titlerefiner").text(refiners[0].Name);

                $.each(refiners[0].Entries.results, function (i, item) {
                    $('#valuerefiners').append($('<option>', {
                        value: refiners[0].Name + ":" + item.RefinementToken,
                        text: item.RefinementName
                    }));
                });

                var results = jsonObject.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results;

                for (var i = 0; i < results.length; i++) {
                    var li = document.createElement("li");

                    li.innerText = results[i].Cells.results[3].Value;

                    element.appendChild(li);
                }
            },
            error: function (data) {
            }
        });

        return false;

    });

    $("#valuerefiners").change(function () {

        var searchText = $("#searchbox5").val();
        var value = $("#valuerefiners").val();

        executor.executeAsync({
            method: "GET",
            url: appweburl + "/_api/search/query?querytext='" + searchText + "'&refinementfilters='" + value + "'",
            headers: {
                "accept": "application/json;odata=verbose",
                "content-type": "application/json;odata=verbose"
            },
            success: function (data) {

                var element = document.getElementById("resultsList5");

                $('#resultsList5 > li').remove();

                var jsonObject = JSON.parse(data.body);

                var results = jsonObject.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results;

                for (var i = 0; i < results.length; i++) {
                    var li = document.createElement("li");

                    li.innerText = results[i].Cells.results[3].Value;

                    element.appendChild(li);
                }
            },
            error: function (data) {
            }
        });

        return false;
    });

Este ejemplo igualmente es similar en cuanto a la forma en la que se representan los datos a los anteriores. La diferencia aquí es que a la petición le estamos indicando las propiedades de los resultados obtenidos por las que queremos poder refinar (Línea 7). Una vez que se obtienen los resultados, se carga uno de los refinadores en un combo preparado para tal efecto (Líneas 22 a 31), este combo se carga automáticamente con los valores de refinamiento obtenidos.

Además se añade otro evento para cuando ese combo cambia, con la selección de un refinamiento, de manera que se haga el filtrado correspondiente. Para ello, solo se añade a la petición el refinementfilters con el contenido a filtrar (Línea 58).

Conclusiones

Trabajar con las búsquedas de SharePoint OnLine y 2013 nos ofrece una gran cantidad de oportunidades a la hora de desarrollar de una forma sencilla, la API REST del servicio de búsqueda es muy potente y merece la pena tenerla en cuenta. En la entrada de hoy, os he introducido al uso de la misma y os he mostrado un ejemplo de cómo usarla en una ejemplo de una aplicación para SharePoint.

Y esto es todo por hoy, espero que os resulte interesante. ¿Cuál es el siguiente paso?, bueno, como sabéis me gusta mucho Polymer, y creo que podría ser interesante crear un componente Polymer que use este servicio de búsqueda de forma completa. Espero en breve compartirlo con vosotros y ponerlo disponible para que lo podáis utilizar.

Un saludo y hasta la próxima