Alfresco · Personalizar el subsistema de sincronización de usuarios

Alfresco provee un subsistema de sincronización de usuarios que permite mantener en el propio Alfresco los usuarios gestionados por un sistema de identificación externo. Esta tarea se realiza mediante un proceso de ejecución programada con expresiones cron que permite realizar sincronizaciones de usuarios y de grupos de usuarios totales o parciales. No obstante, este subsistema de sincronización es únicamente lanzado cuando se configura la identificación a través de mecanismo LDAP, obviando su inclusión para el resto (passthru, kerberos, external…).

A continuación se exponen los pasos necesarios para desarrollar un módulo de sincronización de usuarios que permite obtener los datos de usuario de cualquier tipo de sistema (base de datos, servicio web…).

Implementación de la sincronización

Se realiza en una clase Java del módulo, que debe implementar la interfaz UserRegistry.

package es.keensoft.alfresco.repo.sync;

import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.alfresco.repo.management.subsystems.ActivateableBean;
import org.alfresco.repo.security.sync.NodeDescription;
import org.alfresco.repo.security.sync.UserRegistry;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.PropertyMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;

public class CustomUserRegistry implements UserRegistry, InitializingBean, ActivateableBean {

    private final Log log = LogFactory.getLog(CustomUserRegistry.class);

    // Alfresco User properties
    private final static String QNAME_KEY_USERNAME = "cm:userName";
    private final static String QNAME_KEY_FIRSTNAME = "cm:firstName";
    private final static String QNAME_KEY_LASTNAME = "cm:lastName";
    private final static String QNAME_KEY_EMAIL = "cm:email";
    private final static String QNAME_KEY_ORGANIZATION = "cm:organization";
    private final static String QNAME_KEY_JOBTITLE = "cm:jobTitle";
    private NamespaceService namespaceService;
    private boolean active = true;
    private Map<String, String> personDefaultProperties = Collections.emptyMap();
    private Set personMappedProperties = new HashSet();
    private ExternalUserSyncDatasource externalUserSyncDatasource;

    public void setNamespaceService(NamespaceService namespaceService) {
	    this.namespaceService = namespaceService;
    }

    public void setActive(boolean active) {
	    this.active = active;
    }

    public void setPersonDefaultProperties(Map<String, String> personDefaultProperties) {
	    this.personDefaultProperties = personDefaultProperties;
    }

    public void setExternalUserSyncDatasource(ExternalUserSyncDatasource externalUserSyncDatasource) {
	    this.externalUserSyncDatasource = externalUserSyncDatasource;
    }

    @Override
    public Collection getPersons(Date modifiedSince) {
        
        final List persons = new LinkedList();

        // Users can be retrieved from any external Datasource (database, web service...)
        Collection users = externalUserSyncDatasource.getUsers(modifiedSince);
        
        for (UserSync user : users) {
        	NodeDescription node = new NodeDescription(user.getUserName());
        	PropertyMap properties = node.getProperties();
        	if (user.getUserName() != null) properties.put(QName.createQName(QNAME_KEY_USERNAME, namespaceService), user.getUserName());
        	if (user.getFirstName() != null) properties.put(QName.createQName(QNAME_KEY_FIRSTNAME, namespaceService), user.getFirstName());
        	if (user.getLastName() != null) properties.put(QName.createQName(QNAME_KEY_LASTNAME, namespaceService), user.getLastName());
        	if (user.getEmail() != null) properties.put(QName.createQName(QNAME_KEY_EMAIL, namespaceService), user.getEmail());
        	if (user.getOrganization() != null) properties.put(QName.createQName(QNAME_KEY_ORGANIZATION, namespaceService), user.getOrganization());
        	if (user.getJobTitle() != null) properties.put(QName.createQName(QNAME_KEY_JOBTITLE, namespaceService), user.getJobTitle());
        	for (Map.Entry<String, String> defaultProperty : personDefaultProperties.entrySet()) {
        		properties.put(QName.createQName(defaultProperty.getKey(), namespaceService), defaultProperty.getValue());
        	}
        	persons.add(node);
        }

        return persons;

    }

    @Override
    public Collection getPersonNames() {
        return externalUserSyncDatasource.getUserNames();
    }

    @Override
    public boolean isActive() {
        return active;
    }

    @Override
    public Set getPersonMappedProperties() {
        return personMappedProperties;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
    	personMappedProperties.add(QName.createQName(QNAME_KEY_USERNAME, namespaceService));
    	personMappedProperties.add(QName.createQName(QNAME_KEY_FIRSTNAME, namespaceService));
    	personMappedProperties.add(QName.createQName(QNAME_KEY_LASTNAME, namespaceService));
    	personMappedProperties.add(QName.createQName(QNAME_KEY_EMAIL, namespaceService));
    	personMappedProperties.add(QName.createQName(QNAME_KEY_ORGANIZATION, namespaceService));
    	personMappedProperties.add(QName.createQName(QNAME_KEY_JOBTITLE, namespaceService));
    }

    @Override
    public Collection getGroups(Date modifiedSince) {
	log.warn("Group sync is not supported!");
        final Map<String, NodeDescription> lookup = new TreeMap<String, NodeDescription>();
        return lookup.values();
	}

    @Override
    public Collection getGroupNames() {
	log.warn("Group sync is not supported!");
        final List groupNames = new LinkedList();
        return groupNames;
    }
}

Configuración Spring

Una vez implementada la clase, será necesario inicializarla en el XML de contexto de Spring que acompaña al módulo enlazada con el resto de objetos de Alfresco para el subsistema de sincronización.

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'>
<beans>

    <!-- Module properties -->
    <bean id="ksUserSyncRepo.ConfigurationProperties"
          class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">	      
        <property name="location" value="classpath:alfresco/extension/ks-user-sync-repo.properties" />
        <!-- Use of customized prefix and suffix in order to avoid collisions with Alfresco property placeholders -->
        <property name="placeholderPrefix" value="%{ks-user-sync-repo:" />
        <property name="placeholderSuffix" value="}" />
    </bean>

    <!-- ALFRESCO : bean for Scheduled job -->
    <bean id="ksUserSyncRepo.LoginRepoSyncTrigger" class="org.alfresco.util.CronTriggerBean">
        <property name="jobDetail">
            <bean id="peopleJobDetail" class="org.springframework.scheduling.quartz.JobDetailBean">
                <property name="jobClass">
                    <value>org.alfresco.repo.security.sync.UserRegistrySynchronizerJob</value>
                </property>
                <property name="jobDataAsMap">
                    <map>
                        <entry key="userRegistrySynchronizer">
                            <!-- Custom user registry synchronizer -->
                            <ref bean="ksUserSyncRepo.UserRegistrySynchronizer" />
                        </entry>
                        <entry key="synchronizeChangesOnly">
                            <value>%{ks-user-sync-repo:ks.synchronization.synchronizeChangesOnly}</value>
                        </entry>
                    </map>
                </property>
            </bean>
        </property>
        <property name="cronExpression">
            <value>%{ks-user-sync-repo:ks.synchronization.import.cron}</value>
        </property>
        <property name="scheduler">
            <ref bean="schedulerFactory" />
        </property>
    </bean>

    <!-- ALFRESCO : The chaining user registry synchronizer -->
    <bean id="ksUserSyncRepo.UserRegistrySynchronizer"
          class="org.alfresco.repo.security.sync.ChainingUserRegistrySynchronizer">
        <property name="syncWhenMissingPeopleLogIn">
            <value>%{ks-user-sync-repo:ks.synchronization.syncWhenMissingPeopleLogIn}</value>
        </property>
        <property name="syncOnStartup">
            <value>%{ks-user-sync-repo:ks.synchronization.syncOnStartup}</value>
        </property>
        <property name="autoCreatePeopleOnLogin">
            <value>%{ks-user-sync-repo:ks.synchronization.autoCreatePeopleOnLogin}</value>
        </property>
        <property name="authorityService">
            <ref bean="authorityService" />
        </property>
        <property name="personService">
            <ref bean="personService" />
        </property>
        <property name="attributeService">
            <ref bean="attributeService" />
        </property>
        <property name="applicationContextManager">
            <ref bean="Authentication" />
        </property>
        <property name="transactionService">
            <ref bean="transactionService" />
        </property>
        <property name="ruleService">
            <ref bean="ruleService" />
        </property>
        <property name="jobLockService">
            <ref bean="jobLockService" />
        </property>
        <property name="sourceBeanName">
            <!-- Custom UserRegistry bean -->
            <value>ksUserSyncRepo.UserRegistry</value>
        </property>
        <property name="loggingInterval">
            <value>%{ks-user-sync-repo:ks.synchronization.loggingInterval}</value>
        </property>
        <property name="workerThreads">
            <value>%{ks-user-sync-repo:ks.synchronization.workerThreads}</value>
        </property>
        <property name="allowDeletions">
            <value>%{ks-user-sync-repo:ks.synchronization.allowDeletions}</value>
        </property>
    </bean>

    <!-- User registry -->
    <bean id="ksUserSyncRepo.UserRegistry" class="es.keensoft.alfresco.repo.sync.GenericUserRegistry">
        <property name="active">
            <value>%{ks-user-sync-repo:ks.synchronization.active}</value>
        </property>
        <property name="externalUserSyncDatasource">
            <ref bean="ksUserSyncRepo.MockExternalUserSyncDatasource"/>
        </property>
        <property name="personDefaultProperties">
            <map>
                <entry key="cm:homeFolderProvider">
                    <null/>
                </entry>
            </map>
        </property>
        <property name="namespaceService">
            <ref bean="namespaceService"/>
        </property>
    </bean>

    <!-- Custom Datasource synchronizer (database, web service...) -->
    <bean id="ksUserSyncRepo.MockExternalUserSyncDatasource"
          class="es.keensoft.alfresco.repo.sync.mock.MockExternalUserSyncDatasourceImpl" />
          
</beans>

Configuración de propiedades

Finalmente, en el fichero de propiedades se establecen los parámetros deseados para la sincronización.

# Enable / disable user synchronization
ks.synchronization.active=true

# The cron expression defining when imports should take place
ks.synchronization.import.cron=0 * * * * ?

# Should we trigger a differential sync when missing people log in?
ks.synchronization.syncWhenMissingPeopleLogIn=true

# Should we trigger a differential sync on startup?
ks.synchronization.syncOnStartup=true

# Should we auto create a missing person on log in?
ks.synchronization.autoCreatePeopleOnLogin=true

# The number of entries to process before logging progress
ks.synchronization.loggingInterval=100

# The number of threads to use when doing a batch (scheduled or startup) sync
ks.synchronization.workerThreads=2

# Synchronization with deletions
ks.synchronization.allowDeletions=true

# Should the scheduled sync job use differential or full queries on the user
# registries to determine the set of local users to be updated?
ks.synchronization.synchronizeChangesOnly=true

Conclusiones

Existen muchas organizaciones que no disponen de un LDAP o un ActiveDirectory centralizado para la identificación de usuarios por lo que, a pesar de que la autenticación CAS está cada vez más extendida, resulta necesario disponer de un subsistema de sincronización de usuarios con Alfresco personalizado.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s