28/9/15

Obtener roles y privilegios de un usuario con FetchXml

Los datos que muestran los informes en CRM están filtrados por el perfil de seguridad del usuario que los ejecuta. Hasta aquí, todo bien: nuestros informes no serán un hueco de seguridad, siempre que usemos las vistas Filtered de la base de datos como fuente. En CRM Online (o On-Premise con Claims Authentication) la cosa cambia: la consulta se escribe como una expresión FetchXml que igualmente está filtrada según los privilegios. El problema viene cuando un usuario no tiene el permiso mínimo de lectura a nivel usuario. En este caso el informe simplemente lanzará una excepción, cuando tal vez lo que esperaríamos es que mostrara una lista vacía. Para evitar esto no queda más remedio que hacer un pequen˜o rodeo:
  1. Crear un informe principal que obtiene información de seguridad del usuario actual.
  2. Crear un informe secundario con los datos que realmente queremos mostrar.
  3. En el informe principal, comprobar si el usuario tiene permisos de lectura en la entidad y, en caso afirmativo, mostrar el informe secundario.
¿Y cómo obtenemos la información de seguridad (roles y privilegios) de un usuario? De eso va esta entrada. Empezamos por anadir el parámetro estándar CRM_FullName y llenando un DataSet con la información siguiente:

Para obtener los roles de seguridad del usuario actual:

<fetch>
  <entity name="systemuserroles">
    <link-entity alias="u" from="systemuserid" name="systemuser" to="systemuserid">
      <filter>
        <condition attribute="fullname" operator="eq" value="@CRM_FullName" />
      </filter>
    </link-entity>
    <link-entity alias="r" from="roleid" name="role" to="roleid">
      <attribute name="name" />
    </link-entity>
  </entity>
</fetch>

Para obtener los privilegios de lectura del usuario actual:

<fetch>
  <entity name="systemuserroles">
  <link-entity alias="u" from="systemuserid" name="systemuser" to="systemuserid">
    <filter>
      <condition attribute="fullname" operator="eq" value="@CRM_FullName">
    </condition></filter>
  </link-entity>
  <link-entity alias="_r" from="roleid" name="role" to="roleid">
    <link-entity alias="r" from="roleid" name="role" to="parentrootroleid">
      <link-entity alias="rp" from="roleid" name="roleprivileges" to="roleid">
        <attribute name="privilegedepthmask">
        <link-entity alias="p" from="privilegeid" name="privilege" to="privilegeid">
          <attribute name="name">
            <filter>
              <condition attribute="name" operator="like" value="prvRead%">
            </condition></filter>          
        </attribute></link-entity>
      </attribute></link-entity>
    </link-entity>
  </link-entity>
 </entity>
</fetch>

Una vez creado el DataSet (lo he llamdo Privileges), controlamos la visibilidad del subinforme con una expresión similar a lo siguiente:

=Lookup("prvReadaccount",Fields!p_name.Value,Fields!rp_privilegedepthmaskValue.Value,"Privileges")

Que básicamente busca en el DataSet a través de la función Lookup un privilegio de nombre prvReadaccount. En el valor de esta expresión obtenemos un 1 si el privilegio es nivel usuario, un 2 si es de unidad de negocio, 4 si es "deep" y 8 si es global.

18/8/15

Enlazar a un informe estático desde un formulario

A veces hay requisitos de usuario que parecen sencillos y que sin embargo requieren mucho trabajo de desarrollo. Lo que técnicamente (en España al menos) se conoce como una chorrada que luego nos lleva horas o días de investigación y desarrollo (= buscar en Google muy fuerte). Esta es una de ellas: imaginad un formulario en CRM con unos cálculos tan complejos que su explicación requiere un PDF de varias páginas. Algo que no podemos simplemente enchufar como una ristra de HTML porque visualmente no encaja. El primer pensamiento es: no pasa nada, creo un webresource con el PDF y otro webresource con el enlace que apunte al primero. Pero hay un problema: los tipos de webresource son:

  • HTML
  • CSS
  • Javascript
  • XML, XSL
  • Imagen (jpg, gif, png)
  • Silverlight
  • Icono

¿Dónde está el tipo PDF? Pues no está, punto. Si creamos un PDF, le cambiamos la extensión, lo subimos como un CSS... muy complicado.

La solución viene de nuestros amigos los informes. La opción más habitual al crear un informe es cargar ficheros RDL con la opción Cargar un fichero existente. Pero nada impide que este fichero existente sea de otros tipos (PDF, DOC, PPT etc.) siempre que el navegador de nuestro cliente sepa qué hacer con él. Una vez subido el fichero, nos fijamos en la URL del formulario de edición del informe. Tendrá aproximandamente este aspecto:

..../CRMReports/reportproperty.aspx?id=%7bFAFAFAFA-AAFA-FAFA-FAFA-111111111111%7d

Nos apuntamos el código GUID del informe, en este caso: FAFAFAFA-AAFA-FAFA-FAFA-111111111111

Y creamos un webresource de tipo HTML con el siguiente código, que incluye una llamada a crmreports/download.aspx que hace justo lo que su nombre sugiere.

<a href="/crmreports/download.aspx?id=%7bFAFAFAFA-AAFA-FAFA-FAFA-111111111111%7d">Descargar PDF</a>

Ojo, no olvidéis el %7b y el %7d, de otra manera no funcionará. Otra buena noticia es que este código sigue funcionando entre distintos entornos, de modo que si incluimos informe y webresource en una solución, exportamos e importamos, el enlace seguirá funcionando.

20/3/15

Error: Could not retrieve salesperson role

No es un error que nos vayamos a encontrar todos los días, pero aun así... ¡a mí me pasó y dolió! Queda aquí para la próxima vez. Pero bueno, al grano:

Desde la versión 2011 Dynamics CRM permite importar registros de tipo Usuario y Equipo mediante el proceso estándar de importación. Esto introduce una opción interesante para dar de alta usuarios (o equipos) en bloques y simultáneamente asociarlos a una unidad de negocio. Pero siempre hay un "pero": no permite agregar también un rol de seguridad para los usuarios durante la importación. Un usuario sin roles no puede ser propietario de otros registros, así que el sistema, por su cuenta y riesgo, asocia el rol de seguridad Salesperson (Representante de ventas en español) a los nuevos usuarios. Si por el motivo que sea, ese rol no existe en el sistema, la importación fallará en cada registro. El registro de errores de la importación tiene este aspecto:

Could not retrieve salesperson role
Errores durante el proceso de importación

Por qué podría haber desaparecido el rol? Alguien bien intencionado ha hecho "limpia" y se ha pasado de la raya... En cualquier caso, la solución a este problema sería:

  1.  Ir a otra instalación de Dynamics CRM que sí tenga el rol Salesperson
  2.  Crear una solución vacía e incluir el rol Salesperson en la solución
  3.  Exportar e importar en la instalación original.
Otra opción, un poco más rebuscada (y en caso de que no tuviéramos acceso a otra instalación de CRM) sería crear una solución con un rol cualquiera, exportarla, editar el XML resultante y cambiar el GUID del rol por el siguiente:

a4be89ff-7c35-4d69-9900-999c3f603e6f

Esto se puede hacer así porque aparentemente los roles de seguridad estándar tienen siempre el mismo GUID independientemente de la instalación en la que estemos.

8/5/14

Retornar JSON dinámico desde un servicio WCF

Un escenario común asociado a servicios WCF que extraen información de CRM es que los valores retornados correspondan con entidades de CRM. Para crear un endpoint de WCF que devuelva esa información en XML o JSON necesitamos especificar un tipo de retorno, pero si queremos que el servicio sea genérico, no podemos atarnos a una implementación concreta de una clase de .NET. Se necesita un tipo que acepte "de todo", como un System.Object, un string o un dynamic. Mi primera opción fue usar un string, pero me encontré que como el valor ya estaba expresado en JSON, WCF lo estaba volviendo a serializar, con lo que acababa con un string con un montón de comillas y caracteres "escapados". Bueno, pues en este blog de Carlos Figueira encontré una opción que no conocía y que permite manejar hasta el nivel de byte el resultado que genera el servicio WCF: devolver un Stream.

[WebInvoke(Method = "GET",
 UriTemplate = "search/{queryName}/{searchString}",
 RequestFormat = WebMessageFormat.Json, 
 ResponseFormat = WebMessageFormat.Json, 
 BodyStyle = WebMessageBodyStyle.Bare)]
public Stream Search(string queryName, string searchString)
{
  var jsonString = ".....";
  WebOperationContext.Current.OutgoingResponse.ContentType =
    "application/json; charset=utf-8";
  return new MemoryStream(Encoding.UTF8.GetBytes(jsonString));
}

La línea en la que se establece el ContentType ayudará al cliente a determinar cuál es el tipo. En este caso sigue siendo JSON, pero igualmente podría ser un generador de imágenes, o cualquier otro tipo de contenido.

11/6/13

Oscuro error Javascript al ejecutar un informe

He aquí un error que he podido solucionar en minuto y medio gracias a una blogueresa (?) llamada Arpita y que de otra manera habría llevado horas. Al lío: al ejecutar un informe ocurre lo siguiente en el navegador:



Esto se debe (agárrate) a que:
1. El usuario actual se creó en una organización diferente a aquella en la que se está ejecutando el informe.
2. Esa organización primitiva está habilitada..
3. En esa organización, el usuario actual no tiene asignado rol de seguridad.

Sobre el punto (1) nada podemos hacer, pero sí podemos (a) inhabilitar la organización primitiva o (b) otorgar roles de seguridad al usuario en esta organización.

Qué cosas...

Enlace al artículo completo:
http://blogs.msdn.com/b/arpita/archive/2012/07/21/microsoft-dynamics-crm-2011-unable-to-run-any-reports-due-to-missing-security-role-in-default-organization.aspx

29/5/13

Incluir librerías Javascript en una acción del Ribbon

El escenario es el siguiente: queremos añadir alguna acción adicional (o modificar una de las existentes) al Ribbon de una entidad o al de nivel de aplicación. El código que responde a la acción suele ser una función en Javascript que está incluida en un Web Resource que hemos cargado previamente en la solución. Hasta ahí, todo correcto.Pero ¿qué ocurre si queremos incluir, por ejemplo, código jquery en esa función? El código de esa librería lo tendremos probablemente en otro Web Resource que utilizamos en otros lados (por ejemplo en eventos de formulario). El truco para conseguir que el Ribbon incluya ese otro Web Resource es crear una función fantasma junto a la verdadera. En esa función especificamos la librería que necesitamos y establecemos como nombre de función una función estándar de Javascript que "no haga nada", por ejemplo isNaN. En la siguiente captura, quiero añadir el código de la librería json2.js

La función "fantasma" justo encima de la que contiene el código que queremos ejecutar

La idea la encontré en este post de Rajeev Pentyala y la captura de pantalla es del Ribbon Editor del legendario autor de Dynamics CRM Tools Tanguy

8/5/13

Un mensaje de (des)esperanza

Inauguro aquí la sección crueldad en los mensajes de error. El siguiente me lo he encontrado al intentar guardar un formulario de CRM, y la verdad, no deja mucho lugar al optimismo.

















Con cursiva y en español es más bonito aún, fijáos:

"El destinatario no está disponible y desapareció; todas las conexiones son inválidas. La llamada no se ejecutó, las flores se marchitan, el invierno llegará y la última esperanza se deshace como una lágrima en la lluvia."

PD: Hay que culpar al Update 2 de Visual Studio 2012 de la creación de esta entrada. Por el error, y por el tiempo libre inesperado sin máquina de desarrollo.