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