Category Archives: Alfresco

Entrando por la “puerta de atrás” en Alfresco

Entre la arquitectura de funcionamiento de Alfresco se encuentra la capa de persistencia donde se guardan los datos necesarios para realizar las operaciones y tareas que hacen falta. Esta capa se divide a su vez en 4 elementos, la parte de configuración con ficheros de propiedades, la parte de almacenamiento de los documentos, el almacenamiento de los índices y el almacenamiento de las propiedades y otros valores (incluidas también configuraciones).

Esta última parte es guardada en un SGBD o Sistema de Gestión de Base de Datos (relacional) que puede ser MySQL, PostgreSQL, Oracle, SQL-Server, etc.
A veces, por determinadas circunstancias o necesidades debemos hacer uso de consultas directas a la base de datos para obtener datos, también sería posible modificar estos datos directamente pero no es aconsejable debido a que el control de lo que se guarda, modifica y borra lo tiene exclusivamente la aplicación de Alfresco. Como digo, en determinadas ocasiones es una posibilidad más el poder consultar directamente a la base de datos determinados datos que sean necesarios y de esta forma evitar pasar por la aplicación, por ejemplo en casos de que el servidor de aplicaciones (Alfresco) no levante correctamente, en casos de pérdidas de documentos y datos o de integridad que paren el servidor de aplicaciones o para monitorización por parte de algún programa externo.
Evidentemente este tipo de acciones son muy dependientes de la versión del esquema (schema) de Alfresco ya que este cambia con cada versión si bien, al estar en la capa del modelo de datos (no confundir con el modelo documental) cambia relativamente poco, sobre todo determinadas tablas muy importantes. De todas formas, es bueno tener el mapa de tablas y relaciones entre ellas para poder realizar bien cualquier consulta y tener la seguridad de que los datos proporcionados son coherentes. Hay que tener en cuenta que este modelo ha ido creciendo desde las primeras versiones, por ejemplo en la versión 2.1.0 Community había 24 tablas con el prefijo ALF, en la 3.4.11 había 46 y en la versión 4.2.2 hay 48 (solo dos tablas más con prefijo ALF). No se han tenido en cuenta las tablas usadas para el repositorio AVM, para el motor jBMP ni las utilizadas para Activiti.
Versión 2.1.0

Versión 2.2.6
Versión 3.4.11

Versión 4.2.2

Seguidamente voy a poner una serie de ejemplos en SQL lo más estándar posible, si bien, incluso lo más básico es dependiente también de la base de datos que use Alfresco en determinado momento. Estos son solo unos cuantos ejemplos de extracción de datos a través de la base de datos pero se pueden realizar muchas más operaciones de consulta según la necesidad concreta de cada uno.

Stores disponibles

Aunque no es muy necesario sí que a veces resulta interesante saber cuantos “almacenes” tiene nuestro repositorio, esto se podría realizar con la siguiente consulta:
SELECT * 
FROM alf_store;

Nodos disponibles

En Alfresco casi cualquier cosa es un nodo, un usuario, un grupo, una carpeta o un documento por ejemplo, para sacar los nodos disponibles podemos hacerlo como:
SELECT * 
FROM alf_node;
Pero no vamos a quedarnos aquí, podemos saber a que almacén pertenece cada nodo como:
SELECT uuid, protocol, identifier, alf_store.version 
FROM alf_node, alf_store
WHERE alf_node.store_id = alf_store.id;
Y ahora vamos a realizar una consulta que nos devuelva el nodo con la nomenclatura del tipo NodeRef, pero para esto ya nos encontramos con problemas, con la misma concatenación de campos, ya que dependerá de cada sistema de base de datos:
— Para Oracle
SELECT protocol || ‘://’ || identifier || ‘/’ || uuid AS nodeRef, 
alf_store.version, local_name   
FROM alf_node, 
alf_store, 
alf_qname   
WHERE alf_node.store_id = alf_store.id
AND   alf_node.type_qname_id = alf_qname.id;
— Para MySQL
SELECT CONCAT(protocol, ‘://’, identifier, ‘/’, uuid) AS nodeRef, 
alf_store.version, 
local_name   
FROM alf_node, 
alf_store, 
alf_qname   
WHERE alf_node.store_id = alf_store.id
AND   alf_node.type_qname_id = alf_qname.id;
— Para SQL-Server
SELECT protocol + ‘://’ + identifier + ‘/’ + uuid AS nodeRef, 
alf_store.version, 
local_name   
FROM alf_node, 
alf_store, 
alf_qname   
WHERE alf_node.store_id = alf_store.id
AND   alf_node.type_qname_id = alf_qname.id;

Extrayendo metadatos

La tabla más importante en Alfresco es alf_data_properties, esta es la que mantiene los valores de todas la propiedades de todos los nodos disponibles y es la que es más utilizada para devolver información ya que generalmente casi cualquier dato en Alfresco será una propiedad de un nodo. Por tanto, las siguientes tablas podríamos decir que son las básicas a la hora de realizar consultas directas:
  • alf_store : Guarda la información de los almacenes disponibles
  • alf_node : Guarda los nodos, aquí obtenemos el uuid que corresponde (con otros campos) al llamado NodeRef (referencia de nodo)
  • alf_qname : En versiones posteriores a 2.1 (2.2.x, 3.1.x, 3.2.x, 3.4.x, etc.) contiene los nombres identificativos de los tipos de propiedades, en versiones anteriores se almacenaba en un campo llamado alf_qname de la tabla alf_node_properties
  • alf_node_properties : Sin duda la tabla más importante a la hora de extraer datos en Alfresco, guarda los valores de las propiedades de los modelos de datos documentales de Alfresco, incluyendo los datos de las carpetas, documentos, usuarios, grupos, etc.
Con estas y alguna más de forma auxiliar podremos obtener la información necesaria.
Es interesante el uso de la tabla alf_qname (en versiones 2.2.x en adelante) porque con esta podemos posteriormente determinar que tipo de propiedad queremos, por tanto es buena idea tener un listado de los posibles nombres cualificados posibles:
— Versiones 2.2.x y siguientes
SELECT
FROM    alf_qname;
— Versiones 2.1.x
SELECT alf_node_properties.qname 
FROM alf_node_properties;
Veamos dos ejemplos sobre el uso de extraer propiedades, uno sobre los usuarios y otro sobre los documentos:
En ocasiones necesitamos saber los datos de un usuario en particular, las siguientes consultas nos devuelven un listado de usuarios:
— Versiones 2.2.x y siguientes
SELECT
    alf_node_properties.node_id,
    alf_node_properties.string_value
FROM
    alf_node_properties,
    alf_qname
WHERE
    alf_node_properties.qname_id = alf_qname.id
AND
    alf_qname.local_name = ‘userName’;
— Versiones 2.1.x
SELECT
    alf_node_properties.node_id,
    alf_node_properties.string_value 
FROM
    alf_node_properties 
WHERE
    alf_node_properties.qname = ‘{http://www.alfresco.org/model/content/1.0}userName’;
Y las siguientes (según la versión de Alfresco) nos devolverán la propiedades de un usuario concreto (sustituir NOMBRE-DEL-USUARIO por el nombre del usuario a buscar)
— Versiones 2.2.x y siguientes
SELECT
    alf_node_properties.node_id,
    alf_qname.local_name,
    alf_node_properties.string_value
FROM
    alf_node_properties,
    alf_qname
WHERE
    alf_node_properties.qname_id = alf_qname.id
AND
    alf_node_properties.node_id = (
        SELECT
            alf_node_properties.node_id
        FROM
            alf_node_properties,
            alf_qname
        WHERE
            alf_node_properties.qname_id = alf_qname.id
        AND
            alf_qname.local_name = ‘userName’
        AND
            alf_node_properties.string_value = ‘admin’
        );
— Versiones 2.1.x
SELECT
    alf_node_properties.node_id,
    alf_node_properties.qname, 
    alf_node_properties.string_value 
FROM
    alf_node_properties 
WHERE
    alf_node_properties.node_id = (
        SELECT
            alf_node_properties.node_id
        FROM
            alf_node_properties 
        WHERE
            alf_node_properties.qname = ‘{http://www.alfresco.org/model/content/1.0}userName’
        AND
            alf_node_properties.string_value = ‘NOMBRE-DEL-USUARIO’
        );
Y por último una aplicación real cuando se necesita el nombre que Alfresco ha puesto en el repositorio. En casos de recuperación de documentos necesitamos saber donde está guardado el fichero en el repositorio a partir del nombre del documento guardado en Alfresco. Esta consulta nos devuelve esta información a partir del nombre del documento. Es para la versión 3.4.x y 4.x
— Versiones 3.4.x y 4.x
SELECT alf_node_properties.node_id,
alf_node_properties.string_value,
alf_content_url.content_url
FROM alf_node_properties,
alf_qname,
alf_content_data,
alf_node_properties,
alf_content_url
WHERE alf_node_properties.string_value = ‘NOMBRE-DEL-DOCUMENTO’ 
AND alf_node_properties.qname_id = alf_qname.id 
AND alf_qname.local_name = ‘name’ 
AND alf_content_data.id = alf_node_properties.long_value 
AND alf_node_properties.node_id = alf_node_properties.node_id 
AND alf_node_properties.long_value = alf_content_data.id 
AND alf_content_data.content_url_id = alf_content_url.id;

Cluster con Hazelcast en Alfresco One 4.2.2

Si hay algo en lo que Alfresco ha trabajado en cada una de las versiones que han visto la luz ha sido el tema de cluster y la comunicación entre los nodos. En las versiones 2.x con EHCache y un sistema de multicast que era bastante pobre, usando JGroups en la 3.x hasta llegar a la 4.2.x con Hazelcast.

Pero, ¿qué es Hazelcast?

Es según la propia página web oficial un “Open Source In-Memory Data Grid”, es decir, una plataforma para la distribución de datos de código abierto. Entre sus características podemos encontrar:

  • Implementaciones distribuidas de Set, List, Map, Lock, MultiMap
  • Mensajería distribuida P/S
  • Soporte transaccional e integración JEE vía JCA
  • Soporte encriptación a nivel de sockets
  • Persistencia síncrona o asíncrona
  • Clusterizado Sesión HTTP
  • Discovery dinámico
  • Monitorización JMX
  • Escalado dinámico
  • Particionado dinámico
  • Fail-over dinámico

Como se puede ver, es una herramienta fantástica para cumplir las especificaciones de cluster que necesita Alfresco.

Las funciones de las que se sirve Alfresco y que son comunes a Hazelcast están:

  • Compartir datos/estados entre varios servidores: como compartición sesión Web
  • Cacheo distribuido de datos
  • Comunicación segura entre servidores
  • Particionado de datos en memoria
  • Distribución de trabajo entre servidores
  • Procesamiento paralelo
  • Gestión fail-safe de datos

Además se lleva muy bien con Hibernate como caché de segundo nivel y con Spring.

¿Cómo configuramos el cluster de Alfresco 4.2.2?

Para configurar un sistema de cluster en Alfresco 4.2.2 es tan fácil como cuando se configuraba con EHCache o JGroups e incluso más todavía y eso sí, se comprueba la fiabilidad que tiene este producto integrado en Alfresco.

Hay que entender que aquí explico solamente como montar el cluster, es decir, que ambos nodos se comuniquen entre sí, un sistema completo de alta disponibilidad requiere de un balanceador ya sea hardware o software, un sistema de cluster en la base de datos, etc.

Lo primero que hay que hacer es quitar cualquier referencia a EHCache y JGroups antiguos, esto va orientado a sistemas que han ido siendo actualizados desde versiones antiguas principalmente:

Por ejemplo, el fichero que está dentro de {alfrescoRoot}/tomcat/shared/clases/alfresco/extensión:

ehcache-custom.xml

También en dicha localización (si existe) el fichero:

hazelcastConfig.xml

Este fichero se ha incluido ya dentro del fichero alfresco.war con lo que no hace falta.

Así como las siguientes propiedades que están dentro de {alfrescoRoot}/tomcat/shared/clases/alfresco-global.properties:

alfresco.cluster.name
alfresco.ehcache.rmi.hostname
alfresco.ehcache.rmi.port
alfresco.ehcache.rmi.remoteObjectPort
alfresco.jgroups.defaultProtocol
alfresco.jgroups.bind_address
alfresco.jgroups.bind_interface
alfresco.tcp.start_port
alfresco.tcp.initial_hosts
alfresco.tcp.port_range
alfresco.udp.mcast_addr
alfresco.udp.mcast_port
alfresco.udp.ip_ttl
filesystem.cluster.enabled
filesystem.cluster.configFile

Configuración del cluster para el repositorio

Por defecto si apuntamos dos instancias de Alfresco al mismo repositorio y base de datos, estos formarán de forma automática un grupo de repositorio, no obstante hay que realizar una pequeña configuración para que todo funcione correctamente.

Montar el repositorio de forma compartida y visible para todos los nodos, por ejemplo vía NAS o SAN a través de protocolo NFS.

Configurar el acceso a la base de datos para la misma base de datos en cada uno de los nodos.

Abrir el puerto 5701 TCP en el cortafuegos de los nodos para que puedan ser accesibles entre ellos.

Especificar correctamente la IP (sea en wildcard como por ejemplo 192.168.1.*) de la tarjeta de red del cluster:

alfresco.cluster.interface=192.168.1.101

Fijar la propiedad para activar Hazelcast en JMX

hazelcast.jxm=true

Y por razones de seguridad se debería fijar la contraseña con la siguiente propiedad:

alfresco.hazelcast.password=

Un ejemplo de la parte del fichero alfresco-global.properties para la configuración del cluster puede ser la siguiente:

alfresco.cluster.enabled=true
alfresco.cluster.interface=192.168.1.101
alfresco.hazelcast.password=clavehazelcast
alfresco.hazelcast.port=5701
alfresco.hazelcast.autoinc.port=false
alfresco.hazelcast.mancenter.enabled=false
alfresco.hazelcast.max.no.heartbeat.seconds=15

Una vez arrancada la primera instancia se puede observar un mensaje como el siguiente:

2014-06-30 22:38:36,148 INFO [cluster.core.ClusteringBootstrap] [localhost-startStop-1] Cluster started, name: MainRepository-fea9ebdf-04f3-495e-9456-cf43c24b8e91
2014-06-30 22:38:36,152 INFO [cluster.core.ClusteringBootstrap] [localhost-startStop-1] Current cluster members:
192.168.1.101:5701 (hostname: alfnode1.localdomain)

Finalmente al arrancar el segundo en este se observará lo siguiente:

2014-07-02 10:58:12,108 INFO [cluster.core.ClusteringBootstrap] [localhost-startStop-1] Cluster started, name: MainRepository-fea9ebdf-04f3-495e-9456-cf43c24b8e91
2014-07-02 10:58:12,111 INFO [cluster.core.ClusteringBootstrap] [localhost-startStop-1] Current cluster members:
192.168.1.102:5701 (hostname: alfnode2.localdomain)
192.168.1.101:5701 (hostname: alfnode1.localdomain)

También se puede ver que el cluster está bien configurado mediante la nueva consola de administración cuya URL es:

http://
:8080/alfresco/service/enterprise/admin

En “Servicio de repositorio” y dentro de este en “Agrupación de servidores del repositorio” se puede ver toda la información del cluster, además se puede validar con el botón “Validar grupo” que realiza las comprobaciones necesarias para saber si ambos nodos se están comunicando correctamente:

Propiedades de Hazelcast

Todas las propiedades admitidas por Hazelcast en alfresco-global.properties son:

alfresco.cluster.enabled
Ejemplo: true
Descripción: Activa el cluster de Alfresco para este nodo

alfresco.cluster.interface
Ejemplo: 192.168.80.1
Descripción: Especifica la tarjeta de red usada para el cluster. Se puede usar tipo de dirección wildcard, por ejemplo 192.168.80.*

alfresco.cluster.nodetype
Ejemplo: NodoDesconectado001
Descripción: Especifica un nombre “amigable” para ese nodo del cluster, generalmente utilizado para servidores que se han unido al repositorio pero no forman parte del cluster (p.e. servidores de indexación)

alfresco.hazelcast.password
Ejemplo: mipasswd
Descripción: Define el password que usarán los nodos del cluster

alfresco.hazelcast.port
Ejemplo: 5701
Descripción: Establece el puerto de comunicación entre nodos del cluster

alfresco.hazelcast.autoinc.port
Ejemplo: false
Descripción: Realiza varios intentos de puertos para hayar uno libre desde la configuración alfresco.hazelcast.port. Alfresco no recomienda establecer esta propiedad

alfresco.hazelcast.mancenter.enabled
Ejemplo: false
Descripción: Activa las estadísticas y otros valores del cluster donde se puede acceder a través del Centro de gestión de Hazelcast

alfresco.hazelcast.mancenter.url
Ejemplo: http://localhost:8080/mancenter
Descripción: URL de acceso al centro de gestión de Hazelcast, evidentemente alfresco.hazelcast.mancenter.enabled debe estar en valor true

alfresco.hazelcast.max.no.heartbeat.seconds
Ejemplo: 15
Descripción: Tiempo máximo de monitorización para que se de por hecho que un nodo no está respondiendo

Configuración de Hazelcast en Share

En un entorno de cluster, Alfresco Share ahora utiliza Hazelcast para proporcionar mensajes entre los nodos de la capa web. Como resultado, las cachés ya no necesitan estar deshabilitadas para cualquier nodo. Cada uno funciona prácticamente tan rápido como una sola instancia de Share, mejorando así su rendimiento general.

Se pueden realizar dos configuraciones según las necesidades, con multicast o a nivel de TCP directo.

En todo caso, en balanceadores hay que seguir usando el sistema de Sticky-Session para funcionar correctamente. Hay que configurar correctamente el fichero share-config-custom.xml dentro de {extensionRoot}/alfresco/classes/web-extension poniendo correctamente el host y puerto de acceso al repositorio en caso necesario.

También hay que tener en cuenta que si se usa autenticación Kerberos o NTML con SSO las sesiones utilizarán la cookie JSESSIONID por lo que habrá que tenerla en cuenta por parte del balanceador.

Para esto hay que configurar el fichero custom-slingshot-application-context.xml
que hay en {extensionRoot}/alfresco/classes/web-extension (quitándole la extensión .sample)

Ejemplo para multicast:

 <!– Hazelcast distributed messaging configuration – Share web-tier cluster
    config (3.4.8 and 4.0.1) – see http://www.hazelcast.com/docs.jsp – and specifically
    http://www.hazelcast.com/docs/1.9.4/manual/single_html/#SpringIntegration –>
<!– Configure cluster to use either Multicast or direct TCP-IP messaging
    – multicast is default –>
<!– Optionally specify network interfaces – server machines likely to have
    more than one interface –>
<!– The messaging topic – the "name" is also used by the persister config
    below –>
<hz:topic id="topic" instance-ref="webframework.cluster.slingshot"
    name=”slingshot-topic” />

    
        
        
            
                <hz:multicast enabled="true" multicast-group="224.2.2.5"
                    multicast-port=”54327″ />
                
                    
                
            
            
                192.168.1.*
            
        
    

<bean id="webframework.slingshot.persister.remote"
    class=”org.alfresco.web.site.ClusterAwarePathStoreObjectPersister”
    parent=”webframework.sitedata.persister.abstract”>
    
    
        alfresco/site-data/${objectTypeIds}
    
    
    
        slingshot-topic
    

<bean id="webframework.factory.requestcontext.servlet" class="org.alfresco.web.site.ClusterAwareRequestContextFactory"
    parent=”webframework.factory.base”>
    
    
    
    

Ejemplo para conexión directa TCP:

<!– Hazelcast distributed messaging configuration – Share web-tier cluster
    config (3.4.8 and 4.0.1) – see http://www.hazelcast.com/docs.jsp – and specifically
    http://www.hazelcast.com/docs/1.9.4/manual/single_html/#SpringIntegration –>
<!– Configure cluster to use either Multicast or direct TCP-IP messaging
    – multicast is default –>
<!– Optionally specify network interfaces – server machines likely to have
    more than one interface –>
<!– The messaging topic – the "name" is also used by the persister config
    below –>
<hz:topic id="topic" instance-ref="webframework.cluster.slingshot"
    name=”slingshot-topic” />

    
        
        
            
                <hz:multicast enabled="false" multicast-group="224.2.2.5"
                    multicast-port=”54327″ />
                
                    alfnode1,alfnode2
                
            
            
                192.168.1.*
            
        
    

<bean id="webframework.slingshot.persister.remote"
    class=”org.alfresco.web.site.ClusterAwarePathStoreObjectPersister”
    parent=”webframework.sitedata.persister.abstract”>
    
    
        alfresco/site-data/${objectTypeIds}
    
    
    
        slingshot-topic
    

<bean id="webframework.factory.requestcontext.servlet" class="org.alfresco.web.site.ClusterAwareRequestContextFactory"
    parent=”webframework.factory.base”>
    
    
    
    

Centro de gestión Hazelcast (mancenter):

El centro de gestión Hazelcast (mancenter) permite monitorizar y administrar los servidores que ejecutan Hazelcast. Además, mancenter permite supervisar el estado general de los clústeres, y analizar y examinar las estructuras de datos en detalle.

Para instalarlo, se puede instalar tanto en un tomcat distinto como en el mismo de Alfresco. Solo hay que bajar una versión de Hazelcast (mancenter) y copiar el fichero mancenter-x.x.x.war al directorio de aplicaciones de tomcat.

Por ejemplo:

cp mancenter-2.4.1.war /opt/Alfresco422/tomcat/webapps/mancenter.war

Establecer la propiedad hazelcast.mancenter.home con el directorio donde se almacenan los datos, aquí se puede poner en la misma línea de opciones de Java (JAVA_OPTS), por ejemplo:

-Dhazelcast.mancenter.home=/opt/Alfresco422/tomcat/mancenter_data

Acordarse de activarlo en alfresco-global.properties:

alfresco.hazelcast.mancenter.enabled=true

Establecer la url de acceso, por ejemplo:

alfresco.hazelcast.mancenter.url=http://192.168.1.101:8080/mancenter

Por último, si se produce un error de serialización en el arranque, descomentar la siguiente línea en el contex.xml del servidor Tomcat:


 

Monitorización del funcionamiento de Hazelcast en Alfresco

La mejor forma como siempre es usando Log4j y para esto se puede usar la siguiente propiedad:

log4j.logger.org.alfresco.enterprise.repo.cluster=info

Para monitorizar la caché también se usan las siguientes propiedades:

log4j.logger.org.alfresco.enterprise.repo.cluster.cache=DEBUG
log4j.logger.org.alfresco.repo.cache=DEBUG

A nivel del propio Hazelcast:

log4j.logger.com.hazelcast=info

Y para aumentar el registro de seguimiento también se puede usar:

log4j.logger.com.hazelcast.impl.TcpIpJoiner=debug

Para finalizar

Las pruebas realizadas con un cluster de Alfresco One 4.2.2 usando Hazelcast han resultado ser muy satisfactorias, he realizado pruebas de subida de documentos, cambios de propiedades, etc. y eran instantáneas en ambos nodos.

Hay que tener en cuenta además que hay que configurar Solr (si se usa esta opción de indexado) correctamente para que se use de forma compartida, siempre y cuando no se utilize protocolo NFS para estos recursos compartidos de red ya que no está aconsejado. En este cado (uso de NFS) también se puede seguir usando una configuración similar a la que se utilizaba con Lucene, es decir, mantener índices locales por cada nodo.

Más información

http://docs.alfresco.com/4.2/concepts/ha-intro.html
http://hazelcast.org
http://unpocodejava.wordpress.com/2013/01/21/que-es-hazelcast/

Alfviral 1.3.0.420 para Alfresco 4.2

He subido una nueva versión de Alfviral (Alfresco Virus Alert) adaptado para que funcione en la nueva versión 4.2 de Alfresco. Los cambios han sido en su mayoría sobre las acciones de usuario en Share.

Testeado en:
    Alfresco Community 4.2c
    Tomcat 7.0.47
    Java JDK/Jre 1.7.0_45

Se encuentra disponible para su descarga en: http://code.google.com/p/alfviral/downloads/detail?name=fegorsoft-alfviral-1.3.0.420.zip&can=2&q=#makechanges

Alfresco, CMIS y Python

El estandar CMIS (Content Management Interoperability Services) es un estandar para la gestion de contenidos aprobada por OASIS.

http://en.wikipedia.org/wiki/Content_Management_Interoperability_Services

Esto nos permite tener una capa de llamadas estandar para multitud de productos de gestion de contenidos que incluyan CMIS y por tanto también acceder desde multitud de lenguajes y no solo desde Java.

Este es el caso de cmislib de Jeff Potts para Python.

La installación es muy sencilla, necesitamos un interprete Python (versión
2.7) además del módulo cmislib y setuptools para instalar el primero y
Alfresco 4.

Podemos usar la línea de comandos de Python para hacer las siguientes pruebas:

Importamos la librería:

from cmislib.model import CmisClient, Repository

Creamos el cliente configurando el end-point y las credenciales:

client = CmisClient(‘http://localhost:8080/alfresco/s/cmis’, ‘admin’, ‘admin’)

Recuperamos el servicio de repositorio por defecto:

repo = client.getDefaultRepository()
repo.getRepositoryId()

Recuperamos información del repositorio:

info = repo.getRepositoryInfo()
for k,v in info.items():
    print “%s:%s” % (k,v)

Deberia devolvernos algo así:

cmisSpecificationTitle:Version 1.0 OASIS Standard
aclCapability:

cmisVersionSupported:1.0
principalAnonymous:guest
principalAnyone:GROUP_EVERYONE
repositoryDescription:None
changesOnType:cmis:document
changesIncomplete:true
productVersion:4.0.0 (4003)
rootFolderId:workspace://SpacesStore/b92b669b-4f2a-42ab-a9d8-1451b1596e80
repositoryId:6891ed19-41e3-4160-8c77-65de35c5a428
repositoryName:Main Repository
vendorName:Alfresco
productName:Alfresco Repository (Community)

Bien, ya lo tenemos, ahora creamos…

… una carpeta:

rootFolder = repo.rootFolder
newFolder = rootFolder.createFolder(‘Carpeta nueva’)
newFolder.id

… un contenido:

f = open(‘Mi fichero’, ‘r’)
newDoc = newFolder.createDocument(‘New Document’, contentFile=f)

… y listamos las propiedades:

for k,v in newDoc.properties.items()
    print ‘%s = %s’ % (k,v)

Personalmente me parece una idea muy buena y más con el lenguaje más popular
del momento 😉

Más información en:

https://code.google.com/p/cmislib/
http://chemistry.apache.org/python/cmislib.html
https://pypi.python.org/pypi/setuptools#files
http://www.python.org/
https://www.oasis-open.org/committees/tc_home.php?wg_abbrev=cmis
http://cmis.alfresco.com/

Transformar ficheros RAW en Alfresco

Como ya sabemos, Alfresco tiene muchas posibilidades de manipulación y transformación de ficheros/documentos. Una de las cosas más flexibles es el uso de transformadores que puden ser de dos tipos básicamente:

  1. Llamadas directas a algún comando o utilidad del sistema
  2. Desarrollar una clase Java que realice la transformación

Aquí vamos a ver la primera para utilizar la transformación de fotografías tomadas en formato RAW (o bruto como generalmente se llama) a TIFF y a JPEG. Actualmente hago fotos con Nikon y Olympus y me interesa pasar el formato NEF (por poner el de Nikon) a TIFF y a JPEG. ImageMagick puede hacerlo pero me ha dado problemas tanto en el resultado (no se pueden ver) como en la propia conversión por lo que voy a usar otra utilidad que existe para Linux, Mac OS X y Windows.

Esta utilidad se llama dcraw y podemos bajarla de http://www.cybercom.net/~dcoffin/dcraw/ para el sistema operativo que tengamos. En este caso voy a utilizar la que hay para Windows compilada con MingW desde la dirección http://www.rawness.es/dcraw/?lang=es

Una vez instalada o descomprimida en el directorio que nos guste obtendremos el programa ejecutable dcraw.exe

Bien, ahora a configurar el transformador…

Toda la configuración se basa en un bean llamado RuntimeExecutableContentTransformer para versiones anteriore a la 3.2 de Alfresco o RuntimeExecutableContentTransformeWorker para las siguientes. A su vez este bean contiene las siguientes propiedades principales:

  • transformCommand: Define el comando y parámetros usando variables como ${source} y ${target}
  • checkCommand: Chequea si la sintaxis es correcta y si ocurre un error también se puede definir con errorCodes. Cuando no está disponible el método getReliability devuelve 0:0 este método se usa para utilizar el transformador más eficiente en caso de que haya varios definidos que conviertan de la misma fuente al mismo destino.
  • explicitTransformations establece el tipo MIME fuente y destino de forma explícita. 

Creamos el fichero nef-tiff-jpeg-transform-context.xml:

   
       
           
               
                   

                       
                            ${dcraw.exe} -i ${dcraw.home}/test.nef
                       
                   
               
               
                    1
               
           
       
       
           
               
                   

                                           
                            ${dcraw.exe} -T ‘${source}’ ‘${target}’
                       
                       
                            ${dcraw.exe} -T ‘${source}’ ‘${target}’
                                           
                                       
                            ${dcraw.exe} -T “${source}” “${target}”
                       
                   
               
               
                    true
               
           
       
       
           
               
                    image/x-nikon-nef
                    image/tiff
               
           
       
       
           
       
   
   
       
           
       
   
    <bean id="transformer.complex.Nef.Jpg"
        class=”org.alfresco.repo.content.transform.ComplexContentTransformer”
        parent=”baseContentTransformer” >
       
           
               
               
           
       
       
           
                image/tiff
           
       
   

Como vemos hay además otro bean definido de la clase ComplexContentTransformer, este sirve para realizar transformaciones a partir de otras, es decir, si no podemos transformar una imagen del formato NEF al formato JPEG pero si podemos realizar la transformación de NEF a TIFF y de TIFF a JPEG podemos configurarlo a través de este bean en el que hay que poner los transformadores y el mimetype intermedio de la transformación.

 Con esto ya podemos transformar imágenes tomadas directamente en RAW (NEF en el caso de Nikon) a TIFF y a si vez y mediante ComplexContentTransformer directamente a JPEG.

Ahora con dcraw.exe…

Esta utilidad de línea de comandos al igual que convert.exe de ImageMagick tiene la siguiente sintaxis:

C:Usersfernando.gonzalezworkspaceCommonMingW-release-orig-x32>dcraw.exe
Raw photo decoder “dcraw” v9.06

by Dave Coffin, dcoffin a cybercom o net
Usage:  dcraw.exe [OPTION]… [FILE]…
-v        Print verbose messages
-c        Write image data to standard output
-e        Extract embedded thumbnail image
-i        Identify files without decoding them
-i -v     Identify files and show metadata
-z        Change file dates to camera timestamp
-w        Use camera white balance, if possible
-a        Average the whole image for white balance
-A Average a grey box for white balance
-r Set custom white balance
+M/-M     Use/don’t use an embedded color matrix
-C   Correct chromatic aberration
-P Fix the dead pixels listed in this file
-K Subtract dark frame (16-bit raw PGM)
-k   Set the darkness level
-S   Set the saturation level
-n   Set threshold for wavelet denoising
-H [0-9]  Highlight mode (0=clip, 1=unclip, 2=blend, 3+=rebuild)
-t [0-7]  Flip image (0=none, 3=180, 5=90CCW, 6=90CW)
-o [0-5]  Output colorspace (raw,sRGB,Adobe,Wide,ProPhoto,XYZ)
-d        Document mode (no color, no interpolation)
-D        Document mode without scaling (totally raw)
-j        Don’t stretch or rotate raw pixels
-W        Don’t automatically brighten the image
-b   Adjust brightness (default = 1.0)
-g

Set custom gamma curve (default = 2.222 4.5)

-q [0-3]  Set the interpolation quality
-h        Half-size color image (twice as fast as “-q 0”)
-f        Interpolate RGGB as four colors
-m   Apply a 3×3 median filter to R-G and B-G
-s [0..N-1] Select one raw image or “all” from each file
-6        Write 16-bit instead of 8-bit
-4        Linear 16-bit, same as “-6 -W -g 1 1”
-T        Write TIFF instead of PPM

Podemos observar tres cosas:

  1. Podemos usar el modificador -i para chequear la utilidad, solo hay que usar una imagen y llamarla por ejemplo test.nef
  2. Debemos usar el modificador -T para pasar las imágenes a TIFF en lugar de PPM que es lo que hace por defecto
  3. Tenemos un problema, el resultado de la transformación es siempre el mismo fichero pero con la extensión cambiada, es decir, no podemos obligar a guardar el resultado en otro fichero (bueno, si, con el modificador -c que saca la salida por consola y capturandola con una redirección “>” pero esto no funciona bien)
Como Alfresco utiliza en la transformación dos variables ${source} y ${target} y ambos son distintos (genera los nombres de estos ficheros distintas) nos encontramos con el problema de que no podemos llamarlo directamente por lo que hay que crear un script que llame correctamente al ejecutable con los modificadores adecuados.

Este script en formato BAT/CMD de Windows y llamado dcraw.cmd es el siguiente:

@ECHO OFF

set path_dcraw=%0
SET opts=%1
SET source=%2
SET target=%3
%path_dcraw:~,-3%exe %opts% %source%
IF “%opts%”==”-i” GOTO END
MOVE %source:~,-4%tiff” %target%
:END

Seguidamente vamos a crear las variables en el fichero de propiedades, en este caso utilizo el mismo alfresco-global.properties:

dcraw.home=C:/Users/fernando.gonzalez/workspace/Common/MingW-release-orig-x32

dcraw.exe=${dcraw.home}/dcraw.cmd

Y por último debemos declarar los tipos MIME (mimetypes) en el fichero mimetypes-extension-map.xml que está en /alfresco/extension/mimetype:


   
       
           
                nef
           
           
       
   

 Listo, ya solo nos falta hacer las reglas necesarias para convertir NEF a TIFF o a JPEG directamente en nuesto estudio fotográfico y tener almacenadas y ordenadas todas nuestras fotografías. Además, podemos usar Alfresco de previsualizador de imágenes RAW aunque no tengamos nigún programa para ello como podemos ver en la siguiente imagen:

Para depurar podemos activar las siguientes líneas en Log4J (log4j.properties o custom-log4j.propeties en el extension):

log4j.logger.org.alfresco.repo.content.transform.TransformerDebug=debug

log4j.logger.org.alfresco.util.exec.RuntimeExec=debug
log4j.logger.org.alfresco.repo.content.transform.ContentTransformerRegistry=debug

Cifrado de contenido en Alfresco

En muchas ocasiones es necesario el cifrado del contenido en Alfresco, en este sentido ya Alfresco en la versión 4.0 puede cifrar propiedades (http://wiki.alfresco.com/wiki/Data_Encryption) y también hay un módulo para utilizar el cifrado (http://addons.alfresco.com/addons/alfresco-encryption-module).

En este caso vamos a realizar dos acciones que cifren y descifren el contenido de un documento (propiedad content) usando el algoritmo de cifrado simétrico AES. Estos sirven para la versión 3.4 de Alfresco y siguientes.

El código también se encuentra en: http://code.google.com/p/alfcrypto

Algunas cosas importantes primero: Este software es una versión alpha o beta o como queráis llamarla pero sobre todo es un código hecho de forma más o menos rápida y por tanto no hay garantía ninguna de funcionamiento, se ha probado solo con algunos documentos MS-Word y PDF. Además, ya he detectado un problema, cuando se descifra el tipo MIME en el que se guarda la copia desencriptada es plain/text (no es que no funcione, si descargais el documento se puede abrir/editar, etc) por lo que hay que incluir en el modelo de datos una propiedad que guarde el valor original y lo restaure posteriormente (ya lo haré cuando consiga algo de tiempo). Se ha realizado solamente con carácter educativo y por lo tanto no lo recomiendo para su uso en sistemas de producción sin realizar antes algunas modificaciones y pruebas.

Otro apunte más, como bien comenta Toni de la Fuente (blyx.com), existen algunas restricciones derivadas del cifrado, la primera es en la previsualización, evidentemente no existe cuando los documentos están cifrados y no tiene sentido descifrar para previsualizar ya que rompería la seguridad. Tampoco es posible la indexación full-text ya que no es posible al igual que en contenidos de imágenes (JPG, GIF, etc.), es más, es una buena idea cambiar el tipo MIME a algún tipo que Alfresco no indexe y cuando sea descifrado reponer el tipo MIME original. El uso de este sistema sería para documentos que son necesariamente obligados a ser cifrados y solo se tenga acceso a ellos por personal especial (Recursos Humanos, I+D+I, datos con carácter especial de protección de datos, etc.) que una vez descifrados (en otra ubicación a la original principalmente) sean descargados y borrados (la copia descifrada) vaciando la papelera e incluso modificando la configuración para que no sean guardados en esta. Además recomendaría que o bien el cifrado, o el descifrado se aloje en otras unidades de disco diferentes usando para ello Content Store Selector (en este último caso solo para las versiones Enterprise).

Con todo esto, comencemos:

Lo primero que hay que hacer es construir la clase de cifrado que en este caso se llamará crypto.java:

/*
 * alfcrypto is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * alfcrypto is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see .
 */
package com.fegor.alfresco.security.crypto;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.AlgorithmParameters;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.security.spec.KeySpec;

import org.apache.log4j.Logger;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;

public class Crypto {
    private final Logger logger = Logger.getLogger(Crypto.class);

    String password = null;
    public final static int SALT_LEN = 8;
    byte[] vector_init = null;
    byte[] salt_pos = null;

    byte[] input;
    byte[] output;

    Cipher eCipher = null;
    Cipher deCipher = null;

    private final int KEYLEN_BITS = 128;
    private final int ITERATIONS = 65536;

    /**
     * Constructor
     */
    public Crypto() {
    }

    /**
     * Encryption configuration
     *
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     * @throws NoSuchPaddingException
     * @throws InvalidParameterSpecException
     * @throws IllegalBlockSizeException
     * @throws BadPaddingException
     * @throws UnsupportedEncodingException
     * @throws InvalidKeyException
     */
    public void configEncrypt() throws NoSuchAlgorithmException,
            InvalidKeySpecException, NoSuchPaddingException,
            InvalidParameterSpecException, IllegalBlockSizeException,
            BadPaddingException, UnsupportedEncodingException,
            InvalidKeyException {
        SecretKeyFactory factory = null;
        SecretKey tmp = null;

        salt_pos = new byte[SALT_LEN];
        SecureRandom rnd = new SecureRandom();
        rnd.nextBytes(salt_pos);

        if (logger.isDebugEnabled())
            logger.debug(this.getClass().getName() + “: [salt: “
                    + (new String(Hex.encodeHex(salt_pos))) + “]”);

        factory = SecretKeyFactory.getInstance(“PBKDF2WithHmacSHA1”);

        /*
         * http://www.javamex.com/tutorials/cryptography/unrestricted_policy_files
         * .shtml
         */
        KeySpec spec = new PBEKeySpec(password.toCharArray(), salt_pos,
                ITERATIONS, KEYLEN_BITS);
        tmp = factory.generateSecret(spec);
        SecretKey secret = new SecretKeySpec(tmp.getEncoded(), “AES”);

        eCipher = Cipher.getInstance(“AES/CBC/PKCS5Padding”);
        eCipher.init(Cipher.ENCRYPT_MODE, secret);
        AlgorithmParameters params = eCipher.getParameters();

        vector_init = params.getParameterSpec(IvParameterSpec.class).getIV();

        if (logger.isDebugEnabled())
            logger.debug(this.getClass().getName() + “: [vector ini: “
                    + (new String(Hex.encodeHex(vector_init))) + “]”);
    }

    /**
     * Decryption configuration
     *
     * @param initvec
     * @param salt
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     * @throws NoSuchPaddingException
     * @throws InvalidKeyException
     * @throws InvalidAlgorithmParameterException
     * @throws DecoderException
     */
    public void configDecrypt(String initvec, String salt)
            throws NoSuchAlgorithmException, InvalidKeySpecException,
            NoSuchPaddingException, InvalidKeyException,
            InvalidAlgorithmParameterException, DecoderException {
        SecretKeyFactory factory = null;
        SecretKey tmp = null;
        SecretKey secret = null;

        salt_pos = Hex.decodeHex(salt.toCharArray());

        if (logger.isDebugEnabled())
            logger.debug(this.getClass().getName() + “: [salt: “
                    + (new String(Hex.encodeHex(salt_pos))) + “]”);

        vector_init = Hex.decodeHex(initvec.toCharArray());
        if (logger.isDebugEnabled())
            logger.debug(this.getClass().getName() + “: [vector ini: “
                    + (new String(Hex.encodeHex(vector_init))) + “]”);

        /*
         * http://www.javamex.com/tutorials/cryptography/unrestricted_policy_files
         * .shtml
         */
        factory = SecretKeyFactory.getInstance(“PBKDF2WithHmacSHA1”);
        KeySpec spec = new PBEKeySpec(password.toCharArray(), salt_pos,
                ITERATIONS, KEYLEN_BITS);

        tmp = factory.generateSecret(spec);
        secret = new SecretKeySpec(tmp.getEncoded(), “AES”);

        deCipher = Cipher.getInstance(“AES/CBC/PKCS5Padding”);
        deCipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(
                vector_init));
    }

    /**
     * Cipher input
     *
     * @param input
     *            – the cleartext file to be encrypted
     * @param output
     *            – the encrypted data file
     * @throws IOException
     * @throws IllegalBlockSizeException
     * @throws BadPaddingException
     * @throws ShortBufferException
     */
    public void Cipher() throws IOException, IllegalBlockSizeException,
            BadPaddingException, ShortBufferException {
        try {
            this.output = eCipher.doFinal(this.input);
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        }
    }

    /**
     * Decipher input
     *
     * @param input
     *            – the cleartext file to be encrypted
     * @param output
     *            – the encrypted data file
     * @throws IOException
     * @throws IllegalBlockSizeException
     * @throws BadPaddingException
     * @throws ShortBufferException
     */
    public void Decipher() throws IOException, IllegalBlockSizeException,
            BadPaddingException, ShortBufferException {
        try {
            this.output = deCipher.doFinal(this.input);
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        }
    }

    /*
     * Methods setter and getter
     */
    public void setInput(byte[] input) {
        this.input = input;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getSalt() {
        return (new String(Hex.encodeHex(salt_pos)));
    }

    public String getVectorInit() {
        return (new String(Hex.encodeHex(vector_init)));
    }

    public byte[] getOutput() {
        return this.output;
    }
}

Como se observa es una clase normal con los métodos para configurar el cifrado y descifrado y la llamada para realizar las acciones correspondientes.

Utilizaremos dos aspectos para saber que documentos están cifrados y cuales han sido descifrados, el modelo de datos a utilizar será el siguiente:

   
    Alfresco Crypto Model
    Fernando González Ruano (twitter://fegorama)
    1.0
   
        <import uri="http://www.alfresco.org/model/dictionary/1.0"
            prefix=”d” />
       
   
   
        <namespace uri="http://www.fegorsoft.com/model/alfcrypto/1.0"
            prefix=”acr” />
   
   
       
            Ciphered
           
               
                    d:text
                    false
                   
                        false
                        false
                        false
                   
               
               
                    d:text
                    false
                   
                        false
                        false
                        false
                   
                               
           
       
       
            Deciphered
       
   

Para llamar a esta clase se necesitan dos acciones de Alfresco, estas serán CipherContent.java y DecipherContent.java:

Fichero: CipherContent.java
/*
 * alfcrypto is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * alfcrypto is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see .
 */
package com.fegor.alfresco.action;

import java.io.IOException;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.util.HashMap;
import java.util.List;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.ShortBufferException;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.action.executer.ActionExecuterAbstractBase;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ParameterDefinition;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.namespace.QName;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;

import com.fegor.alfresco.model.AlfCryptoModel;
import com.fegor.alfresco.security.crypto.Crypto;
import com.google.gdata.util.common.util.Base64;

/**
 * CryptoRepo Action
 *
 * @author fegor
 *
 */
public class CipherContent extends ActionExecuterAbstractBase {

    private final Logger logger = Logger.getLogger(CipherContent.class);

    /*
     * Services
     */
    private ContentService contentService;
    private NodeService nodeService;

    private String password;
    //
    // TODO Poder usar más algoritmos que AES
    //
    // private String algorithm;

    private String salt;
    private String vector_init;

    /*
     * (non-Javadoc)
     *
     * @see
     * org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl
     * (org.alfresco.service.cmr.action.Action,
     * org.alfresco.service.cmr.repository.NodeRef)
     */
    @Override
    protected void executeImpl(Action action, NodeRef actionedUponNodeRef) {
        if (!nodeService.hasAspect(actionedUponNodeRef,
                AlfCryptoModel.ASPECT_CIPHERED)) {
            if (logger.isDebugEnabled()) {
                logger.debug(this.getClass().getName() + “: [Action for: “
                        + actionedUponNodeRef + ” is ciphering…]”);
            }
            if (actionedUponNodeRef != null)
                try {
                    this.cryptoFileCipher(actionedUponNodeRef);
                } catch (ContentIOException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#
     * addParameterDefinitions(java.util.List)
     */
    @Override
    protected void addParameterDefinitions(List arg0) {
    }

    /**
     * Crypto file for nodeRef
     *
     * @param nodeRef
     * @throws IOException
     * @throws ContentIOException
     */
    private void cryptoFileCipher(NodeRef nodeRef) throws ContentIOException,
            IOException {
        ContentReader contentReader = this.contentService.getReader(nodeRef,
                ContentModel.PROP_CONTENT);
        ContentWriter contentWriter = this.contentService.getWriter(nodeRef,
                ContentModel.PROP_CONTENT, true);

        if (contentReader != null) {
            Crypto crypto = new Crypto();
            crypto.setPassword(this.password);
            byte[] crb = IOUtils.toByteArray(contentReader
                    .getContentInputStream());

            try {
                crypto.configEncrypt();
            } catch (InvalidKeyException e) {
                e.printStackTrace();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (InvalidKeySpecException e) {
                e.printStackTrace();
            } catch (NoSuchPaddingException e) {
                e.printStackTrace();
            } catch (InvalidParameterSpecException e) {
                e.printStackTrace();
            } catch (IllegalBlockSizeException e) {
                e.printStackTrace();
            } catch (BadPaddingException e) {
                e.printStackTrace();
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }

            crypto.setInput(crb);
            try {
                crypto.Cipher();
            } catch (IllegalBlockSizeException e) {
                e.printStackTrace();
            } catch (BadPaddingException e) {
                e.printStackTrace();
            } catch (ShortBufferException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }

            contentWriter.putContent(Base64.encode(crypto.getOutput()));

            this.salt = crypto.getSalt();
            this.vector_init = crypto.getVectorInit();

            this.removeAspect(nodeRef);
            this.addAspect(nodeRef);

        } else {
            if (logger.isDebugEnabled())
                logger.debug(this.getClass().getName()
                        + “: [contentReader is null]”);
        }
    }

    /**
     * Remove aspect Deciphered
     *
     * @param nodeRef
     */
    private void removeAspect(NodeRef nodeRef) {
        if (nodeService.hasAspect(nodeRef, AlfCryptoModel.ASPECT_DECIPHERED)) {
            nodeService.removeAspect(nodeRef, AlfCryptoModel.ASPECT_DECIPHERED);
        }
    }

    /**
     * Add aspect Ciphered
     *
     * @param nodeRef
     */
    private void addAspect(NodeRef nodeRef) {
        HashMap properties = new HashMap(
                1, 1.0f);
        properties.put(AlfCryptoModel.PROP_SALT, this.salt);
        properties.put(AlfCryptoModel.PROP_VECTOR_INIT, this.vector_init);
        if (!nodeService.hasAspect(nodeRef, AlfCryptoModel.ASPECT_CIPHERED)) {
            nodeService.addAspect(nodeRef, AlfCryptoModel.ASPECT_CIPHERED,
                    properties);
        }
    }

    /**
     * @param contentService
     */
    public void setContentService(ContentService contentService) {
        this.contentService = contentService;
    }

    /**
     * @param nodeService
     */
    public void setNodeService(NodeService nodeService) {
        this.nodeService = nodeService;
    }

    /**
     * @param password
     */
    public void setPassword(String password) {
        this.password = password;
    }

    /**
     * @param algorithm
     */
    //
    // TODO Poder usar más algoritmos que AES
    //
    // public void setAlgorithm(String algorithm) {
    // this.algorithm = algorithm;
    // }
}

Fichero: DecipherContent.java
/*
 * alfcrypto is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * alfcrypto is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see .
 */
package com.fegor.alfresco.action;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.List;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.ShortBufferException;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.action.executer.ActionExecuterAbstractBase;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ParameterDefinition;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.apache.commons.codec.DecoderException;
import org.apache.log4j.Logger;

import com.fegor.alfresco.model.AlfCryptoModel;
import com.fegor.alfresco.security.crypto.Crypto;
import com.google.gdata.util.common.util.Base64;
import com.google.gdata.util.common.util.Base64DecoderException;

/**
 * DecryptoRepo Action
 *
 * @author fegor
 *
 */
public class DecipherContent extends ActionExecuterAbstractBase {

    private final Logger logger = Logger.getLogger(DecipherContent.class);

    /*
     * Services
     */
    private ContentService contentService;
    private NodeService nodeService;

    private String password;
    //
    // TODO Poder usar más algoritmos que AES
    //
    // private String algorithm;

    private String salt;
    private String vector_init;

    /*
     * (non-Javadoc)
     *
     * @see
     * org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl
     * (org.alfresco.service.cmr.action.Action,
     * org.alfresco.service.cmr.repository.NodeRef)
     */
    @Override
    protected void executeImpl(Action action, NodeRef actionedUponNodeRef) {
        if (nodeService.hasAspect(actionedUponNodeRef,
                AlfCryptoModel.ASPECT_CIPHERED)) {
            if (logger.isDebugEnabled()) {
                logger.debug(this.getClass().getName() + “: [Action for: “
                        + actionedUponNodeRef + ” is deciphering…]”);
            }
            if (actionedUponNodeRef != null)
                try {
                    this.cryptoFileDecipher(actionedUponNodeRef);
                } catch (InvalidAlgorithmParameterException e) {
                    e.printStackTrace();
                } catch (DecoderException e) {
                    e.printStackTrace();
                }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#
     * addParameterDefinitions(java.util.List)
     */
    @Override
    protected void addParameterDefinitions(List arg0) {
    }

    /**
     * Scan file for nodeRef
     *
     * @param nodeRef
     * @throws DecoderException
     * @throws InvalidAlgorithmParameterException
     */
    private void cryptoFileDecipher(NodeRef nodeRef)
            throws InvalidAlgorithmParameterException, DecoderException {
        ContentReader contentReader = this.contentService.getReader(nodeRef,
                ContentModel.PROP_CONTENT);
        ContentWriter contentWriter = this.contentService.getWriter(nodeRef,
                ContentModel.PROP_CONTENT, true);

        if (contentReader != null) {

            byte[] crb = contentReader.getContentString().getBytes();
            Crypto crypto = new Crypto();
            crypto.setPassword(this.password);
            this.salt = (String) nodeService.getProperty(nodeRef,
                    AlfCryptoModel.PROP_SALT);
            this.vector_init = (String) nodeService.getProperty(nodeRef,
                    AlfCryptoModel.PROP_VECTOR_INIT);

            try {
                crypto.configDecrypt(this.vector_init, this.salt);
            } catch (InvalidKeyException e) {
                e.printStackTrace();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (InvalidKeySpecException e) {
                e.printStackTrace();
            } catch (NoSuchPaddingException e) {
                e.printStackTrace();
            }

            try {
                crypto.setInput(Base64.decode(crb));
            } catch (Base64DecoderException e1) {
                e1.printStackTrace();
            }

            try {
                crypto.Decipher();
            } catch (IllegalBlockSizeException e) {
                e.printStackTrace();
            } catch (BadPaddingException e) {
                e.printStackTrace();
            } catch (ShortBufferException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            contentWriter.setMimetype(“text/plain”);
            contentWriter.putContent((InputStream) (new ByteArrayInputStream(
                    crypto.getOutput())));

            this.removeAspect(nodeRef);
            this.addAspect(nodeRef);

        } else {
            if (logger.isDebugEnabled())
                logger.debug(this.getClass().getName()
                        + “: [contentReader is null]”);
        }
    }

    /**
     * Remove aspect Ciphered
     *
     * @param nodeRef
     */
    private void removeAspect(NodeRef nodeRef) {
        if (nodeService.hasAspect(nodeRef, AlfCryptoModel.ASPECT_CIPHERED)) {
            nodeService.removeAspect(nodeRef, AlfCryptoModel.ASPECT_CIPHERED);
        }
    }

    /**
     * Add aspect Deciphered
     *
     * @param nodeRef
     */
    private void addAspect(NodeRef nodeRef) {
        if (!nodeService.hasAspect(nodeRef, AlfCryptoModel.ASPECT_CIPHERED)) {
            nodeService
                    .addAspect(nodeRef, AlfCryptoModel.ASPECT_CIPHERED, null);
        }
    }

    /**
     * @param contentService
     */
    public void setContentService(ContentService contentService) {
        this.contentService = contentService;
    }

    /**
     * @param nodeService
     */
    public void setNodeService(NodeService nodeService) {
        this.nodeService = nodeService;
    }

    /**
     * @param password
     */
    public void setPassword(String password) {
        this.password = password;
    }

    /**
     * @param algorithm
     */
    //
    // TODO Poder usar más algoritmos que AES
    //
    // public void setAlgorithm(String algorithm) {
    // this.algorithm = algorithm;
    // }
}

El siguiente paso es configurar y registrar las acciones en Alfresco:

Fichero: actions-context.xml

    
       
            true
       
       
           
                classpath:alfresco/extension/alfcrypto.properties
           
       
   
   
    <bean id="alfcrypto.cipher.action" class="com.fegor.alfresco.action.CipherContent"
        parent=”action-executer”>
       
           
       
       
           
       
       
        <!–
       
            ${alfviral.algorithm}
       
         –>
       
            ${alfviral.password}
       
   

    <bean id="alfcrypto.decipher.action" class="com.fegor.alfresco.action.DecipherContent"
        parent=”action-executer”>
       
           
       
       
           
       
       
        <!–
       
            ${alfviral.algorithm}
       
         –>
       
            ${alfviral.password}
       
       

Fichero: model-context.xml

       
   
       
           
                alfresco/module/alfcrypto/model/alfcryptoModel.xml
           
       
     


Fichero: webclient-context.xml

    
       
           
                alfresco.module.alfcrypto.messages.alfcrypto
           
       
    
  
    <bean id="alfcrypto.webclient.configBootstrap" class="org.alfresco.web.config.WebClientConfigBootstrap"
        init-method=”init”>
       
           
                classpath:alfresco/module/alfcrypto/ui/web-client-config-custom.xml
           
       
    

Fichero: web-client-config-custom.xml

  
     
        
        
     
  
  
  
   
  
  
  
     
        
        
        
  
  

Fichero: alfcrypto.properties (messages)
alfcrypto.cipher.action.title=Cifrar
alfcrypto.cipher.action.description=Cifrado del contenido

alfcrypto.decipher.action.title=Descifrar
alfcrypto.decipher.action.description=Descifrado del contenido

alfcrypto.label.ciphered=Cifrado
alfcrypto.label.deciphered=Descifrado

Y por último construir el fichero de configuración:

Fichero: alfcrypto.properties
# La elección de algoritmo (alfcrypto.algorithm) no está implementado todavía
alfcrypto.algorithm=AES
alfcrypto.password=estoesunaclavesecreta

A partir de aquí podemos utilizar el sistema para cifrar y descifrar, para ello podemos usar reglas para que el contenido en una carpeta sea cifrado, crear un workflow para descifrar el contenido de los documentos cifrados, etc.

Solo cifra el contenido, no las propiedades de los documentos.

Más sobre este tema:
http://es.wikipedia.org/wiki/Advanced_Encryption_Standard
http://en.wikipedia.org/wiki/Advanced_Encryption_Standard
http://wiki.alfresco.com/wiki/Data_Encryption
http://addons.alfresco.com/addons/alfresco-encryption-module

Actualización de Alfviral 1.2.1

Debido a un “desliz” en mi código con la versión 1.2.0 se me olvidó declarar el servicio NodeService para usarlo en los modos COMMAND y INSTREAM por lo que he arreglado esto y algún que otro retoque más y he subido la versión 1.2.1 que os recomiendo que instaléis en lugar de la versión anterior.

Agradezco a Niccolò Pasquetto su aportación y aviso.

La nueva versión se puede descargar directamente desde:

https://alfviral.googlecode.com/files/fegorsoft-alfviral-1.2.1.zip

Exclusión de ficheros para Alfviral (versión 1.2.0)

Hace unos días recibí una solicitud de un usuario sobre mi módulo para escanear ficheros desde Alfresco. La nueva funcionalidad solicitada era la de poder seleccionar qué ficheros queremos que sean enviados al antivirus que tengamos configurado.

Anoche estuve retocando un poco el módulo e incluí la funcionalidad en forma de ficheros excluidos, es decir, una lista de ficheros que no queremos que se envíen al antivirus para ser escaneados, en este caso podemos incluir ficheros PDF, TXT, CSV, etc. y no se enviarán para ser analizados. En la nueva configuración solo hay un parámetro más configurado por ejemplo como sigue:

alfviral.file.exceptions=text/html|text/xml|application/pdf|image/jpeg|text/plain

Donde en la lista y separada por “|” podemos incluir los archivos según su “Content-Type”.

Espero que con esta nueva funcionalidad pueda “discriminarse” mejor que documentos/ficheros necesitamos que sean analizados.

He incluido el módulo como versión 1.2.0.

Como no sobrecargar form.get.head.ftl en Alfresco

En el blog de Michal Wróbel y su magnífico artículo “How to perform form field validation in Alfresco Share?” se explica la forma de usar los eventos para validaciones de campos en formulario de Alfresco Share. Lo hace sobreescribiendo la restricción obligatoria (mandatory constraint).

El problema viene cuando creamos un módulo para Alfresco Share con su fichero JAR correspondiente y lo instalamos en un Alfresco Share donde hay otro fichero form.get.head.ftl ya que el del JAR anulará al anterior con lo que no funcionarán las validaciones.

Para esto he decidido usar otra forma de tener cargado el fichero .js que necesitamos para las validaciones, por ejemplo form_validation_module.js dentro de nuestra instalación. La forma es usando la referencia a ficheros javascript de la configuración de los “forms” de forma que podamos tener y cargar los ficheros por cada módulo que tengamos sin tener que tocar el fichero original.

De esta forma, podremos tener el mismo fichero form_validation_module.js en la misma ubicación tomcat/webapps/share/components/form pero ahora lo referenciamos no en form.get.head.ftl sino dentro de nuestro fichero de configuración de los campos, por ejemplo module-form-config.xml de esta forma:


   
       
           
               
           

       
   


   
       
           
               
                   
               
               
                   
                       
                            <constraint type="mandatory"
                                validation-handler=”Alfresco.forms.validation.prueba”/>
                       
                   
               
           
       
   

Lo único que cambia del anterior método es que aquí no hace falta referenciar la parte ${page.url.context}/res y que hay que añadir “-min” al fichero si lo vamos a “compactar”.

De esta forma tendrémos más seguridad a la hora de desplegar distintos módulos para Alfresco Share donde haya otros ya instalados.

Alfviral versión 1.1.0

Por fin el 21 de agosto liberé la versión 1.1.0 del módulo para escaneo de documentos mediante antivirus llamado Alfviral (el nombre a lo mejor no es muy apropiado pero fue el que se me ocurrió en ese momento).

Básicamente lo que se ha incluido ha sido una pequeña refactorización del código y la inclusión de la posibilidad de poder escanear los documentos mediante la página www.virustotal.com que a su vez utiliza más de 40 motores de antivirus.

Las características actuales del módulo son:

1. Escanear en base a tres modos distintos:

COMMAND: Permite utilizar un script o directamente el programa ejecutable del antivirus que se quiera siempre y cuando permita al menos un parámetro que sea el fichero a escanear. Este proceso depende del arranque del comando antivirus en si.

INSTREAM: Utilizado para lanzar un flujo de datos del fichero en Alfresco hacia el puerto (3310TCP) utilizado por ClamAV. Esto permite tener un antivirus ClamAV central en un servidor y utilizarlo remotamente desde Alfresco.

VIRUSTOTAL: En este modo se sube el fichero que sube o se actualiza en Alfresco a la web virustotal.com vía HTTP mediante el método POST y se recoge el resultado mediante JSON para su análisis.

2. Si el documento se detecta como infectado se añade el aspecto “infected” así como un aspecto adicional dependiendo del método utilizado para su análisis.

3. Uso de las “policies” de Alfresco usando los métodos onContentUpdate y onContentRead para analizar los documentos que son subidos, modificados y/o leídos.

4. Se puede utilizar el análisis mediante una programación de tiempo indicando a partir de qué espacio se quiere analizar y si se utiliza en profundidad, es decir, a los subespacios.

5. Implemena acciones de usuario tanto en la interface /alfresco como en /share para poder analizar documentos de forma interactiva.

6. Uso de reglas para personalizar los análisis utilizando la acción de escanear.

7. Facilidad de instalación utilizando las Alfresco Module Management Tools para la parte de repositorio y copiando directamente una librería en extension/lib para Share.

8. Configuración flexible y sencilla para establecer el modo de análisis y las formas en las que se quiere realizar el análisis:

– Al subir/modificar un documento
– Al leer un documento
– En una programación horaria y/o de fecha concreta
– Desde una carpeta (espacio de trabajo) en adelante

Un ejemplo de configuración es:

# Command to exec, i.e. clamscan, alfviral.sh, etc.
alfviral.command=C:\Users\fegor\Documents\alfviral.bat
# Config for ClamAV in stream data
alfviral.timeout=30000
alfviral.host=127.0.0.1
alfviral.port=3310
#Config for VIRUSTOTAL
vt.key=246df658bca5e096895683c01ba4bd2eb3a00303b506bda774b71488134bf984
vt.url=https://www.virustotal.com/vtapi/v2/file/scan
# Modes: COMMAND, INSTREAM, VIRUSTOTAL
alfviral.mode=VIRUSTOTAL
# Events
alfviral.on_update=TRUE
alfviral.on_read=FALSE
# Scheduled action
alfviral.scheduled.pathQuery=/app:company_home/st:sites
alfviral.scheduled.cronExpression=* * 3 * * ? 2099

Toda la información tanto en español como en inglés está en la página principal del proyecto: http://code.google.com/p/alfviral/

Como es un proyecto libre y personal las mejoras y nuevas funcionalidades dependen mucho de mi tiempo libre pero más o menos algunas ideas que tengo en mente son:

– Añadir estadísticas sobre los documentos infectados
– Posibilidad de mover los documentos infectados a un espacio de cuarentena
– Recuperación de los documentos infectados a su ubicación original si son desinfectados o se eliminan los aspectos de infección.
– Añadir nuevos protocolos de comunicación con antivirus (Symantec, McAfee,…)
– Poder utilizar distintos modos de análisis al mismo tiempo y para diferentes objetos (espacios y ficheros)