jueves, 7 de septiembre de 2017

PowerShell script para sincronizar las imágenes de usuario con AD

Si en vuestra granja de SharePoint no conseguís que las fotos de los usuarios aparezcan en su perfil, pese a que esté bien introducida en el AD y mapeada en el Servicio de User Profile de SharePoint, es porque respecto a esta questión, en determinadas configuraciones, a SharePoint hay que darle un "empujoncito".

Existen ciertos programas (Microsoft Identity Manager (MIM), Forefront Identity Manager (FIM)), que pueden ayudarnos a realizar esta sincronización de imágenes automáticamente, pero si no teneis acceso a ellos (básicamente porque requieren licencia), siempre podéis utilizar una solución gratuita con nuestro siempre buen amigo PowerShell (el script puede programarse como scheduled task, para que se ejecute diariamente).

Aquí os dejo un ejemplo de un script que he creado para dicha tarea. Básicamente se trata de realizar una query al AD que nos retorne todos los usuarios con imagen en su perfil, y para cada uno de ellos descargar la imagen en el directorio local donde estamos ejecutando el script (ojo: elegid bien el directorio donde lo vais a ejecutar), para luego subirla con su correspondiente nombre (siempre debe empezar por "0c37852b-34d0-418e-91c6-2ac25af4be5b_" seguido del parámetro "RecordId" que el usuario tenga en el User Profile Service. Por último, el comando Updata-SPProfilePhotoStore realiza el procesado de cada imagen, para importarla al perfil de usuario, creando los thumbnails correspondientes.

Recordad que debéis modificar los valores de las variables $URL y $Users con vuestro nombre de dominio. También $picLibrary & $picFolder si vuestra instalación de SharePoint no está realizada en inglés.

Import-Module ActiveDirectory  
Add-PSSnapin Microsoft.SharePoint.PowerShell -EA SilentlyContinue

$URL = "https://mysite.mydomain.com:443/"
#Next variables are correct for english SP installation, otherwise check names and change them.
$picLibrary = "User%20Photos"
$picFolder = "Profile%20Pictures"

$web = Get-SPWeb $URL
$site = Get-SPSite -Identity $URL
$context = Get-SPServiceContext -Site $site
$upm = New-Object Microsoft.Office.Server.UserProfiles.UserProfileManager($context) -ErrorAction Stop
$users = Get-ADUser -Filter * -SearchBase "DC=mydomain,DC=local" -Properties thumbnailPhoto | ? {$_.thumbnailPhoto} | select SamAccountName,thumbnailPhoto

foreach ($user in $users) {
    $name = $user.SamAccountName + ".jpg"  
    write-host $name
    $user.thumbnailPhoto | Set-Content $name -Encoding byte
    $file = Get-Item $name
    #Open file
    $fileStream = ([System.IO.FileInfo] (Get-Item $file.FullName)).OpenRead()
    $folder =  $web.GetFolder($picFolder)
    $spFile = $folder.Files.Add("/" + $picLibrary + "/" + $folder.Url + "/" + "0c37852b-34d0-418e-91c6-2ac25af4be5b_" + $upm.GetUserProfile($user.SamAccountName).RecordId + ".jpg", [System.IO.Stream]$fileStream, $true)
    #Close file stream
    $fileStream.Close();
}

Update-SPProfilePhotoStore -MySiteHostLocation $URL -CreateThumbnailsForImportedPhotos:$true -ErrorAction Stop

Con la ejecución de este script (tarda un rato), ya deberíais poder ver las imágenes de los usuarios, cuando paséis por encima el ratón sobre un "created by" en una lista/biblioteca, en los resultados de búsqueda, en el perfil de usuario del MySite, en el organigrama empresarial...

Aquí seguimos, después de tantos años, dando "empujoncitos" a SharePoint, para que termine haciendo lo que se supone que debería hacer por defecto. 

;-)

¡¡Un saludo a tod@s!!

martes, 22 de agosto de 2017

Cómo evitar que SharePoint 2016 abra los ficheros pdf en Word (OOS)

Recientemente he empezado a trabajar con una granja de SharePoint 2016. 
Tras la migración de contenidos, una de las primeras quejas que recibí fue que "los pdf se abren con word". Esto generaba problemas a los usuarios de IOS, que no podían abrirlos debido a cierta incompatibilidad entre sistemas. 



Tras estudiar un poco el caso, comprové que, efectivamente, nuestro reción instalado servicio de Office Online Server (OOS), está por defecto configurado para abrir todos los ficheros pdf con la aplicación "WordPDF", que forma parte del propio OOS.

Para ver qué tipos de fichero y acciones controla OOS, tenemos un comando muy representativo en PowerShell:

     Get-SPWOPIBinding

Esto nos proporciona un listado de qué aplicación abre cada tipo de extensión, para realizar cada acción predeterminada (lectura, escritura, vista previa, etc.). Por defecto, en el listado se incluyen muchas acciones relativas a los ficheros PDF, todas ellas a ejecutar con WordPdf.


Así que una vez entendido esto, la solución es bien sencilla: Indicarle a OOS que no haga nada en las acciones de lectura de un PDF. Cómo hacerlo? La forma más directa y sencilla (para los administradores, claro está) está siempre con PowerShell:

      Remove-SPWOPIBinding -Application WordPDF -Action view

Este comando eliminará el mapeo del OOS, únicamente para la aplicación WordPDF, y únicamente para las acciones de visualización (aunque podríamos quitarlo de tantas acciones como quisiéramos, siempre intento realizar las acciones necesarias con el mínimo impacto posible).

Tras la ejecución del comando, PowerShell nos pedirá una confirmación. El comando ejecutado, puede afectar a diversos bindings simultáneamente (para diferentes zonas, por ejemplo). Pulsamos "a" para aceptar todo y dejamos que el comando termine su ejecución.


Listo! A partir de ahora, en nuestra granja SharePoint 2016, OOS ya no volverá a mostrar los ficheros pdf dentro de WordPdf, dejando que se abra en su aplicación por defecto (el propio navegador).


Como veis, es importante tener controlado nuestro OOS, sabiendo qué ficheros y acciones abrirán qué aplicación. Una vez más, PowerShell acude a nuestro rescate.

¡¡Un saludo!!

miércoles, 16 de noviembre de 2016

Filtrar por carpeta en una vista de un WebPart de tipo Lista/Biblioteca

Recientemente me han solicitado que en la página de inicio de un sitio web, mostrara una vista de una carpeta concreta de una lista. Aquí lo ideal sería utilizar el WebPart por defecto de la lista, pero si jugamos con él desde la interface de SharePoint, pronto deduciremos que no es posible realizar ningún filtro para una carpeta en concreto (y este es el tipo de cosas que me encantaría que Microsoft mejorara en SharePoint, pero parece más centrada en evolucionar la arquitectura base y la integración con la nube).

Así pues, ¿Existe alguna forma de filtrar por carpeta en un WebPart de tipo Lista/Biblioteca?

Una vez más, SharePoint Designer acude a nuestro rescate (si eres, como yo, de los que quiere evitar código tanto como se pueda).

Para ello tendremos que crear una vista específica en la lista que queremos mostrar, típicamente con el nombre de la carpeta a filtrar. Esto lo podemos hacer directamente por la interface normal de SharePoint.

Una vez tengamos la filtra creada (campos a mostrar, filtros adicionales, número de elementos...), abriremos nuestro SharePoint Designer e iremos al sitio donde reside dicha lista. Entonces iremos a "Listas y Librerías", y seleccionaremos aquella en la que estamos trabajando.

Una vez dentro, en la sección de "Vistas", seleccionaremos la que acabamos de crear y, con el botón derecho, la editaremos en modo avanzado.



Ya dentro del código de la vista, buscaremos una línea que empieza por un string similar a:

<View Name="{8B3F6F46-F283-4B78-BB42-558FE0899479}"

y dentro de esa línea, buscaremos el tag <Query>.

Tras la especificación del orden de los elementos (por el tag <OrderBy>), deberíamos encontrar la sección de filtro (<Where>), que es la que nos interesa modificar. Sin eliminar todo cuanto ya exista entre el <Where> y su cierre </Where> deberíamos añadir el filtro "FileDirRef" (<FieldRef Name="FileDirRef"/>), con el valor de nuestra URL a partir de la base de nuestro site collection ("/Sitio/Librería/Carpeta")

El filtro debería quedaros tal y como se muestra en las siguientes líneas (añadiendo los filtros adicionales que podáis tener en vuestra vista):

<Where><Contains><FieldRef Name="FileDirRef"/><Value Type="Computed">/Sitio/Librería/Carpeta</Value></Contains></Where>

Si guardamos los cambios realizados en esta vista, podremos comprobar en SharePoint que la vista filtra adecuadamente en la carpeta especificada.

Ya solo quedará crear el webpart de esta Lista/Biblioteca, y en las opciones de configuración, hacer que apunte a la vista que hemos creado a tal efecto.

Y el resultado: Solo mostrará los documentos contenidos en la carpeta que le hemos configurado en SharePoint Designer.

¡¡Seguimos sorteando los obstáculos que Microsoft se empeña en dejarnos en SharePoint!!

viernes, 28 de octubre de 2016

¿Por qué "no funcionan" las políticas de retención?

Recientemente un cliente me solicitó que desactivara un "contrato" de una lista de contratos una vez la fecha de "Caducidad" fuera alcanzada. Esta lista es una custom list, con un montón de metadatos personalizados y ficheros adjuntos. La fecha de "Caducidad" es un campo de tipo "Fecha y hora" (llamado "Automatic expiration") dentro de la misma lista. Por "desactivar" entendemos que hay otro campo de tipo booleano (llamado "Active"), que indica si el contrato está activo (="Yes") o inactivo (="No").

Sabiendo que el camino más fácil y rápido para lograr este objetivo es definir una política de retención, así lo hice: Creé un workflow muy sencillo que modificaba el valor del campo booleano, pasando de "Yes" a "No", y una política de retención para ejecutarlo una vez se alcanzara la fecha indicada por el campo "Automatic expiration" + 1 día (Día después de haber caducado).


Una vez implementado este sencillo proceso, creé un contrato de prueba y esperé a que se alcanzara la fecha (al día siguiente) para comprobar si había funcionado o no.

Para variar, como muchas veces en la vida, las cosas no funcionaron a la primera. La data había vencido, pero el contrato seguía activo. ¿Por qué?

La respuesta, en esta ocasión es fácil de entender y también de solucionar:

En la administración central de SharePoint existen 2 Timer Jobs encargados de ejecutar las políticas de retención:
  • "Information management policy"
  • "Expiration policy"
Por defecto estos Timer Jobs están configurados para ejecutarse semanalmente, así que este tipo de políticas basadas en un día concreto, solo se verían reflejadas una vez por semana.

Sabiendo esto, si vamos a la sección Monitoring de la administración central y pulsamos sobre "Job Definitions", podemos buscar estos dos Timer Jobs en la app web que nos concierna, pulsar sobre su nombre y modificar su ejecución para que sea diaria en lugar de semanal:


Tened en cuenta que es importante que el Timer Job "Information management policy" debe ejecutarse ANTES que el "Expiration policy". En mi caso programé el primero para las 2 a.m. y el segundo para las 3.a.m.

Guardamos nuestros cambios y, ahora sí, todas la políticas de este tipo que tengamos bajo esta aplicación web, van a funcionar perfectamente (salvo caída del servicio, claro está).

Para comprobarlo podemos forzar un "Run now" y quedarnos así, con una sonrisa al ver que todo funciona como esperábamos que lo hiciera.

¡¡Saludos y hasta la próxima!!

jueves, 20 de octubre de 2016

Control permanente para autenticarse como otro usuario

Según mi experiencia, una de las "pérdidas" más incomprensibles que sufrimos al migrar a SharePoint 2013 desde SharePoint 2010 es que el usuario pierde la capacidad de autenticarse como otra persona.

En terminales compartidos, como salas de formación, puede llegar a ser un verdadero calvario tener que explicar constantemente a los usuarios que para realizar dicho proceso, deben añadir a la URL del navegador la extensión _layouts/closeConnection.aspx?loginasanotheruser=true (ver post relacionado).

Esta operación manual retorna al nuevo usuario a la raíz del site collection, donde puede ser que no tenga permisos de acceso, y es bastante engorroso encontrarse con un error de acceso no autorizado tras cada re-login.

Para hacerle la vida más fácil al usuario, y tener una solución permanente de simple uso y acceso, recomiendo fehacientemente añadir el control en el menú de usuario, tal y como se muestra en la siguiente imagen.


La solución es relativamente sencilla, siempre que tengáis acceso a los servidores de SharePoint.

Se trata de añadir en el fichero "\15\TEMPLATE\CONTROLTEMPLATES\Welcome.ascx"  el siguiente código justo antes del control que tiene el ID "ID_RequestAccess" (Editar con Notepad):

  <SharePoint:MenuItemTemplate runat="server" ID="ID_LoginAsDifferentUser" 
    Text="<%$Resources:wss,personalactions_loginasdifferentuser%>" 
    Description="<%$Resources:wss,personalactions_loginasdifferentuserdescription%>"
    MenuGroupId="100" 
    Sequence="100" 
    UseShortId="true" 
    />


El fichero resultante debería tener un aspecto como el siguiente:


Guardamos el resultado y aplicamos lo mismo a todos los servidores frontales (WFE) que tengamos en nuestra granja.

Si no queremos aplicarlo en toda nuestra granja, y tan solo lo necesitamos en un site/site collection específico, siempre podemos añadirlo a nuestra master page, añadiendo un control bajo la etiqueta <div id="suiteBarButtons"> y copiando el siguiente código entre los tags <SharePoint:FeatureMenuTemplate ....> y </SharePoint:FeatureMenuTemplate>


<SharePoint:MenuItemTemplate runat="server" id="MenuItem_SignInAsDifferentUser"
         Text="SignIn As Different User"
         Description="To log in as a different user"
         MenuGroupId="200"
         Sequence="250"
         UseShortId="true"
         ClientOnClickNavigateUrl="~siteLayouts/closeConnection.aspx?loginasanotheruser=true"
         PermissionMode="All"
/>


El resultado será el mismo, pero el ámbito estará limitado a donde aplique dicha .master page.

Con cualquiera de las dos soluciones, el control de "Sign in as different user" debería aparecer en el menú de control de usuario. Es multi-language por defecto y redirige al mismo sitio y página desde la que se efectúa la operación, así que es perfecto para nuestros propósitos.

Al ver los resultados, uno no puede evitar preguntarse: ¿Por qué no está incluido OOB? Pero todos sabemos que a Microsoft les encanta darnos trabajo a los consultores de SharePoint (cosa que, personalmente, debo agradecerles, claro está...).

En fin,  ¡Eso es todo por hoy amigos!

viernes, 23 de septiembre de 2016

Como utilizar las credenciales de forma segura en PowerShell

Powershell requiere en algunos de sus comandos el uso de las credenciales de usuario, para poder utilizarlas en llamadas a otros servicios (como por ejemplo al usar "Send-MailMessage").

Inicialmente, podemos crear una variable que contenga estas credenciales, y luego utilizarla a nuestro antojo. Una forma común de hacerlo sería con un script del tipo:

$userName = Get-ADUser "myUserLoginName" -Properties name | Select-Object -ExpandProperty Name
$password = "myPassword"
$secstr = New-Object -TypeName System.Security.SecureString
$password.ToCharArray() | ForEach-Object {$secstr.AppendChar($_)}
$cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $userName, $secstr


Sin embargo, esta práctica no es muy recomendable, pues estamos añadiendo en nuestros scripts el password de nuestro usuario en claro, y cualquiera que abra nuestro fichero de scripts podría verlo y copiarlo sin problemas.

Para evitar esto, siempre es importante exportar nuestras credenciales cifradas en un fichero XML externo, y recuperarlas cuando sea necesario, sin necesidad de mostrar el password en ninguna de nuestras líneas.

Lo primero que haríamos sería generar ese fichero .XML en una ruta específica de nuestro servidor. Para ello ejecutaremos las siguientes líneas en nuestro PowerShell (evidentemente, directo en la consola, para no meter el password en ningún script)

$credPath = "C:\URLdeNuestraCarpetaEnElServer\myUser.xml"
$userName = Get-ADUser "myUserLoginName" -Properties name | Select-Object -ExpandProperty Name
$password = "myPassword"
$secstr = New-Object -TypeName System.Security.SecureString
$password.ToCharArray() | ForEach-Object {$secstr.AppendChar($_)}
$cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $userName, $secstr | Export-Clixml $credPath


Tras ello, podremos verificar que el fichero "myUser.xml" se ha creado correctamente en la ruta indicada. Si tratamos de abrirlo, comprobaremos que el password está completamente encriptado, así que ya nadie podrá descifrarlo aunque tuviera acceso al fichero.

Ahora, en nuestros scripts de PowerShell, siempre que tengamos que utilizar esta credencial, podemos obtenerla de forma directa y sencilla con la siguiente línea:

$cred = Import-Clixml $credPath 

Podemos crear tantas credenciales de diversos usuarios como queramos, e invocarlas de la misma forma (modificando la ruta de acceso en cada caso).

Y.. ¡¡Esto es todo!! Siempre nos ahorraremos dolores de cabeza si, cuando implementamos algo en nuestra granja, lo hacemos utilizando las buenas prácticas que vamos aprendiendo a lo largo de nuestro camino.

Como siempre, espero que a alguien le sirva de ayuda.

¡¡Un saludo!!

viernes, 15 de julio de 2016

Como operar sobre un conjunto de items/ficheros filtrados con PowerShell

Últimamente estoy usando mucho más PowerShell que en el resto de mi vida profesional con SharePoint. Así que voy a empezar a escribir algunos de los scripts que siempre van bien tener a mano.

Uno de los usos que, personalmente más le suelo dar a PowerShell es la interacción sobre un grupo de elementos de una lista o biblioteca (por ejemplo, asignar un determinado permiso de seguridad a un grupo de ficheros que comparten una característica común (metadatos)).

Pero, ¿Cuál sería el script básico para obtener un grupo de elementos de una biblioteca/lista de SharePoint?

A continuación os dejo un código de ejemplo:

$webUrl = "La URL de tu site aquí"
$ListName = "El nombre de tu lista aquí"
$web = Get-SPWeb $webUrl
$list = $web.Lists[$ListName]
$items= $list.Items | where {($_['NombreCampo1'] -eq 'ValorDeseado1') -and ($_['NombreCampo2'] -eq 'ValorDeseado2') -and (($_['NombreCampo3'] -eq 'ValorDeseado3')-or ($_['NombreCampo3'] -eq 'ValorDeseado4'))}
foreach ($item in $items) {
    Write-Host ("-Realizando acciones para el ítem " + $item.ID) -ForegroundColor Yellow
}

A partir de aquí cada cual hará lo que tenga que hacer sobre cada uno de los elementos retornados. En el ejemplo tan solo escribe un mensaje con el ID del ítem en la consola, aunque en otros post compartiré cómo realizar algunas operaciones interesantes con esta base.

He puesto un ejemplo que utiliza un filtro donde contiene algún AND (-and), OR (-or) y EQUALS (-eq), para que se vea más o menos una sentencia con diversos valores, pero cada uno debería adecuar el filtro a sus propias necesidades, añadiendo o eliminando condicionantes.

Nada más por hoy, aunque en post posteriores voy a profundizar en el tema.

¡Un saludo!