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