001/*
002 *  Copyright 2016 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.userdirectory.synchronize;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026import java.util.stream.Collectors;
027
028import org.apache.avalon.framework.configuration.Configuration;
029import org.apache.avalon.framework.configuration.ConfigurationException;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.commons.lang.StringUtils;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035
036import org.ametys.cms.repository.Content;
037import org.ametys.cms.repository.ModifiableContent;
038import org.ametys.core.user.UserIdentity;
039import org.ametys.core.user.directory.UserDirectory;
040import org.ametys.core.user.population.UserPopulation;
041import org.ametys.core.user.population.UserPopulationDAO;
042import org.ametys.core.util.JSONUtils;
043import org.ametys.plugins.contentio.synchronize.AbstractSimpleSynchronizableContentsCollection;
044import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollection;
045import org.ametys.plugins.contentio.synchronize.impl.LDAPCollectionHelper;
046import org.ametys.plugins.core.impl.user.directory.JdbcUserDirectory;
047import org.ametys.plugins.core.impl.user.directory.LdapUserDirectory;
048import org.ametys.plugins.userdirectory.DeleteOrgUnitComponent;
049import org.ametys.plugins.userdirectory.DeleteUserComponent;
050
051/**
052 * Implementation of {@link SynchronizableContentsCollection} to be synchronized with a {@link UserPopulation} of the CMS.
053 */
054public class UserPopulationSynchronizableContentsCollection extends AbstractSimpleSynchronizableContentsCollection
055{
056    /** Name of parameter holding the id of population */
057    protected static final String __PARAM_POPULATION_ID = "populationId";
058    /** Name of parameter for the firstname attribute */
059    protected static final String __PARAM_FIRSTNAME_ATTRIBUTE_NAME = "firstname";
060    /** Name of parameter for the lastname attribute */
061    protected static final String __PARAM_LASTNAME_ATTRIBUTE_NAME = "lastname";
062    /** Name of parameter for the email attribute */
063    protected static final String __PARAM_EMAIL_ATTRIBUTE_NAME = "email";
064    /** Name of parameter holding the fields mapping */
065    protected static final String __PARAM_MAPPING = "mapping";
066    /** Name of parameter holding the additional search filter */
067    protected static final String __PARAM_ADDITIONAL_SEARCH_FILTER = "additionalSearchFilter";
068    /** Name of parameter into mapping holding the synchronized property */
069    protected static final String __PARAM_MAPPING_SYNCHRO = "synchro";
070    /** Name of parameter into mapping holding the path of attribute */
071    protected static final String __PARAM_MAPPING_ATTRIBUTE_REF = "metadata-ref";
072    /** Name of parameter into mapping holding the remote attribute */
073    protected static final String __PARAM_MAPPING_ATTRIBUTE_PREFIX = "attribute-";
074
075    /** The logger */
076    protected static final Logger _LOGGER = LoggerFactory.getLogger(UserPopulationSynchronizableContentsCollection.class);
077    
078    private static final int __LDAP_DEFAULT_PAGE_SIZE = 1000;
079
080    /** The DAO for user populations */
081    protected UserPopulationDAO _userPopulationDAO;
082    /** The service manager */
083    protected ServiceManager _manager;
084    /** The JSON utils */
085    protected JSONUtils _jsonUtils;
086    /** The delete user component */
087    protected DeleteUserComponent _deleteUserComponent;
088    
089    /** Mapping of the attributes with source data */
090    protected Map<String, Map<String, List<String>>> _mapping;
091    /** Synchronized fields */
092    protected Set<String> _syncFields;
093    
094    @Override
095    public void service(ServiceManager manager) throws ServiceException
096    {
097        super.service(manager);
098        _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE);
099        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
100        _deleteUserComponent = (DeleteUserComponent) manager.lookup(DeleteUserComponent.ROLE);
101        _manager = manager;
102    }
103    
104    public boolean handleRightAssignmentContext()
105    {
106        return false;
107    }
108    
109    @Override
110    protected void configureDataSource(Configuration configuration) throws ConfigurationException
111    {
112        _mapping = new HashMap<>();
113        _syncFields = new HashSet<>();
114        String mappingAsString = (String) getParameterValues().get(__PARAM_MAPPING);
115        if (StringUtils.isNotEmpty(mappingAsString))
116        {
117            List<Object> mappingAsList = _jsonUtils.convertJsonToList(mappingAsString);
118            for (Object object : mappingAsList)
119            {
120                @SuppressWarnings("unchecked")
121                Map<String, Object> field = (Map<String, Object>) object;
122                String attributeRef = (String) field.get(__PARAM_MAPPING_ATTRIBUTE_REF);
123                
124                String prefix = __PARAM_MAPPING_ATTRIBUTE_PREFIX;
125                for (String prefixedUserDirectoryKey : _getUserDirectoryKeys(field, prefix))
126                {
127                    String userDirectoryKey = prefixedUserDirectoryKey.substring(prefix.length());
128                    if (!_mapping.containsKey(userDirectoryKey))
129                    {
130                        _mapping.put(userDirectoryKey, new HashMap<>());
131                    }
132                    
133                    String[] attributes = ((String) field.get(prefixedUserDirectoryKey)).split(",");
134                    
135                    Map<String, List<String>> userDirectoryMapping = _mapping.get(userDirectoryKey);
136                    userDirectoryMapping.put(attributeRef, Arrays.asList(attributes));
137                }
138
139                boolean isSynchronized = field.containsKey(__PARAM_MAPPING_SYNCHRO) ? (Boolean) field.get(__PARAM_MAPPING_SYNCHRO) : false;
140                if (isSynchronized)
141                {
142                    _syncFields.add(attributeRef);
143                }
144            }
145        }
146    }
147
148    @Override
149    protected void configureSearchModel()
150    {
151        _LOGGER.info("Currently, SCC '{}' cannot define a search model", UserPopulationSynchronizableContentsCollection.class.getName());
152    }
153    
154    @Override
155    protected List<ModifiableContent> _internalPopulate(Logger logger)
156    {
157        List<ModifiableContent> contents = new ArrayList<>();
158        
159        UserPopulation population = _userPopulationDAO.getUserPopulation(getPopulationId());
160        
161        for (UserDirectory userDirectory : population.getUserDirectories())
162        {
163            Map<String, Object> searchParams = new HashMap<>();
164            searchParams.put("userDirectory", userDirectory);
165            contents.addAll(_importOrSynchronizeContents(searchParams, false, logger));
166        }
167        
168        return contents;
169    }
170    
171    /**
172     * Search contents from a LDAP user directory of the population.
173     * To avoid code duplication and useless operations, we return a {@link Map}&lt;{@link String}, {@link Map}&lt;{@link String}, {@link Object}&gt;&gt; 
174     * if getRemoteValues is set to false and {@link Map}&lt;{@link String}, {@link Map}&lt;{@link String}, {@link List}&lt;{@link Object}&gt;&gt;&gt; 
175     * if remoteValues is true.
176     * Without this operation, we have to duplicate the code of searchLDAP and _internalSearch methods.
177     * @param userDirectory The LDAP user directory
178     * @param searchParameters Parameters for the search
179     * @param offset Begin of the search
180     * @param limit Number of results
181     * @param logger The logger
182     * @param getRemoteValues if <code>true</code>, values are organized by the attribute mapping
183     * @return Contents found in LDAP
184     */
185    @SuppressWarnings("unchecked")
186    protected Map<String, Map<String, Object>> searchLDAP(LdapUserDirectory userDirectory, Map<String, Object> searchParameters, int offset, int limit, Logger logger, boolean getRemoteValues)
187    {
188        Map<String, Map<String, Object>> results = new HashMap<>();
189        
190        Map<String, Object> ldapParameterValues = userDirectory.getParameterValues();
191        String dataSourceId = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_DATASOURCE_ID);
192        String relativeDN = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_RELATIVE_DN);
193        String filter = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_OBJECT_FILTER);
194        String searchScope = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_SEARCH_SCOPE);
195        String loginAttr = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_LOGIN_ATTRIBUTE);
196        
197        Map<String, List<String>> udMapping = _mapping.getOrDefault(userDirectory.getId(), new HashMap<>());
198        udMapping.put(getLoginAttributeName(), List.of(loginAttr));
199        
200        // If first name attribute is set
201        String firstNameAttribute = getFirstNameAttributeName();
202        if (StringUtils.isNotBlank(firstNameAttribute))
203        {
204            String firstNameAttr = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_FIRSTNAME_ATTRIBUTE);
205            udMapping.put(firstNameAttribute, List.of(firstNameAttr));
206        }
207        
208        // If last name attribute is set
209        String lastNameAttribute = getLastNameAttributeName();
210        if (StringUtils.isNotBlank(lastNameAttribute))
211        {
212            String lastNameAttr = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_LASTNAME_ATTRIBUTE);
213            udMapping.put(lastNameAttribute, List.of(lastNameAttr));
214        }
215        
216        // If email attribute is set
217        String emailAttribute = getEmailAttributeName();
218        if (StringUtils.isNotBlank(emailAttribute))
219        {
220            String emailAttr = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_EMAIL_ATTRIBUTE);
221            udMapping.put(emailAttribute, List.of(emailAttr));
222        }
223        
224        try
225        {
226            LDAPCollectionHelper ldapHelper = (LDAPCollectionHelper) _manager.lookup(LDAPCollectionHelper.ROLE);
227            ldapHelper._delayedInitialize(dataSourceId);
228            
229            List<String> filters = new ArrayList<>();
230            if (StringUtils.isNotEmpty(filter))
231            {
232                filters.add(filter);
233            }
234            
235            if (searchParameters != null)
236            {
237                for (String parameterName : searchParameters.keySet())
238                {
239                    filters.add(parameterName + "=" + searchParameters.get(parameterName));
240                }
241            }
242            
243            String additionalSearchFilter = getAdditionalSearchFilter();
244            if (StringUtils.isNotEmpty(additionalSearchFilter))
245            {
246                filters.add(additionalSearchFilter);
247            }
248            
249            String filtersReduced = filters.stream()
250                                           .filter(StringUtils::isNotEmpty)
251                                           .map(s -> "(" + s + ")")
252                                           .reduce("", (s1, s2) -> s1 + s2);
253            if (!filtersReduced.isEmpty())
254            {
255                filtersReduced = "(&" + filtersReduced + ")";
256            }
257            
258            results = ldapHelper.search(getId(), __LDAP_DEFAULT_PAGE_SIZE, relativeDN, filtersReduced, searchScope, offset, limit, udMapping, loginAttr, logger);
259            if (getRemoteValues)
260            {
261                Map<String, Map<String, List<Object>>> organizedResults = _sccHelper.organizeRemoteValuesByAttribute(results, udMapping);
262                ldapHelper.transformTypedAttributes(organizedResults, getContentType(), udMapping.keySet());
263                results = (Map<String, Map<String, Object>>) (Object) organizedResults;
264            }
265            
266            _nbError = ldapHelper.getNbErrors();
267            _hasGlobalError = ldapHelper.hasGlobalError();
268        }
269        catch (Exception e)
270        {
271            throw new RuntimeException("An error occured when importing from LDAP UserDirectory", e);
272        }
273        
274        return results;
275    }
276    
277    @Override
278    protected Map<String, Map<String, Object>> internalSearch(Map<String, Object> searchParameters, int offset, int limit, List<Object> sort, Logger logger)
279    {
280        return _internalSearch(searchParameters, offset, limit, sort, logger, false);
281    }
282
283    /**
284     * Internal search
285     * @param searchParameters the search parameters
286     * @param offset starting index
287     * @param limit max number of results
288     * @param sort not used
289     * @param logger the logger
290     * @param getRemoteValues to get remote values or not
291     * @return The search result
292     */
293    private Map<String, Map<String, Object>> _internalSearch(Map<String, Object> searchParameters, int offset, int limit, List<Object> sort, Logger logger, boolean getRemoteValues)
294    {
295        Map<String, Map<String, Object>> results = new LinkedHashMap<>();
296
297        List<UserDirectory> userDirectories = new ArrayList<>();
298        if (searchParameters.containsKey("userDirectory"))
299        {
300            userDirectories.add((UserDirectory) searchParameters.get("userDirectory"));
301            searchParameters.remove("userDirectory");
302        }
303        else
304        {
305            UserPopulation population = _userPopulationDAO.getUserPopulation(getPopulationId());
306            userDirectories = population.getUserDirectories();
307        }
308
309        for (UserDirectory userDirectory : userDirectories)
310        {
311            if (userDirectory instanceof LdapUserDirectory)
312            {
313                // Sort is ignored for LDAP
314                results.putAll(searchLDAP((LdapUserDirectory) userDirectory, searchParameters, offset, limit, logger, getRemoteValues));
315            }
316            else if (userDirectory instanceof JdbcUserDirectory)
317            {
318                // TODO handle SQL case
319                // remoteValuesByContent = searchSQL((JdbcUserDirectory) userDirectory, logger);
320                logger.warn("Population with SQL datasource is not implemented.");
321            }
322        }
323        
324        return results;
325    }
326    
327    @Override
328    @SuppressWarnings("unchecked")
329    protected Map<String, Map<String, List<Object>>> getRemoteValues(Map<String, Object> searchParameters, Logger logger)
330    {
331        return (Map<String, Map<String, List<Object>>>) (Object) _internalSearch(searchParameters, 0, Integer.MAX_VALUE, null, logger, true);
332    }
333    
334    @Override
335    protected Map<String, Object> getAdditionalAttributeValues(String idValue, Content content, Map<String, Object> additionalParameters, boolean create, Logger logger)
336    {
337        Map<String, Object> additionalRemoteValues = super.getAdditionalAttributeValues(idValue, content, additionalParameters, create, logger);
338        UserIdentity user = new UserIdentity(idValue, getPopulationId());
339        additionalRemoteValues.put(UserSCCConstants.USER_ATTRIBUTE_NAME, user);
340        return additionalRemoteValues;
341    }
342    
343    /**
344     * Get the id of the user population
345     * @return The id of user population
346     */
347    public String getPopulationId()
348    {
349        return (String) getParameterValues().get(__PARAM_POPULATION_ID);
350    }
351    
352    @Override
353    public String getIdField()
354    {
355        return getLoginAttributeName();
356    }
357    
358    /**
359     * Get the attribute name for the login
360     * @return The the attribute name for the login
361     */
362    public String getLoginAttributeName()
363    {
364        return UserSCCConstants.USER_UNIQUE_ID_ATTRIBUTE_NAME;
365    }
366    
367    /**
368     * Get the attribute name for the first name
369     * @return The the attribute name for the first name
370     */
371    public String getFirstNameAttributeName()
372    {
373        return (String) getParameterValues().get(__PARAM_FIRSTNAME_ATTRIBUTE_NAME);
374    }
375    
376    /**
377     * Get the attribute name for the last name
378     * @return The the attribute name for the last name
379     */
380    public String getLastNameAttributeName()
381    {
382        return (String) getParameterValues().get(__PARAM_LASTNAME_ATTRIBUTE_NAME);
383    }
384    
385    /**
386     * Get the attribute name for the email
387     * @return The the attribute name for the email
388     */
389    public String getEmailAttributeName()
390    {
391        return (String) getParameterValues().get(__PARAM_EMAIL_ATTRIBUTE_NAME);
392    }
393    
394    /**
395     * Get the additional filter for searching
396     * @return The additional filter for searching
397     */
398    public String getAdditionalSearchFilter()
399    {
400        return (String) getParameterValues().get(__PARAM_ADDITIONAL_SEARCH_FILTER);
401    }
402    
403    @Override
404    public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters)
405    {
406        return _syncFields;
407    }
408    
409    private Set<String> _getUserDirectoryKeys(Map<String, Object> field, String prefix)
410    {
411        return field.keySet().stream()
412                .filter(name -> name.startsWith(prefix))
413                .collect(Collectors.toSet());
414    }
415    
416    @Override
417    protected Map<String, Object> putIdParameter(String idValue)
418    {
419        Map<String, Object> parameters = new HashMap<>();
420
421        for (String userDirectory : _mapping.keySet())
422        {
423            List<String> remoteKeys = _mapping.get(userDirectory).get(getIdField());
424            if (remoteKeys != null && remoteKeys.size() > 0)
425            {
426                parameters.put(userDirectory + "$" + remoteKeys.get(0), idValue);
427            }
428        }
429        
430        return parameters;
431    }
432    
433    @Override
434    protected int _deleteContents(List<Content> contentsToRemove, Logger logger)
435    {
436        return _deleteUserComponent.deleteContentsWithLog(contentsToRemove, Map.of(DeleteOrgUnitComponent.SCC_ID_PARAMETERS_KEY, getId()), Map.of(), logger);
437    }
438}