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.core.user.population;
017
018import java.io.File;
019import java.io.FileOutputStream;
020import java.io.IOException;
021import java.io.OutputStream;
022import java.lang.reflect.Array;
023import java.nio.file.Files;
024import java.nio.file.StandardCopyOption;
025import java.sql.Connection;
026import java.time.Instant;
027import java.time.temporal.ChronoUnit;
028import java.util.ArrayList;
029import java.util.Collections;
030import java.util.HashMap;
031import java.util.HashSet;
032import java.util.LinkedHashMap;
033import java.util.List;
034import java.util.Map;
035import java.util.Optional;
036import java.util.Properties;
037import java.util.Set;
038import java.util.regex.Pattern;
039import java.util.stream.Collectors;
040
041import javax.xml.transform.OutputKeys;
042import javax.xml.transform.TransformerConfigurationException;
043import javax.xml.transform.TransformerFactory;
044import javax.xml.transform.TransformerFactoryConfigurationError;
045import javax.xml.transform.sax.SAXTransformerFactory;
046import javax.xml.transform.sax.TransformerHandler;
047import javax.xml.transform.stream.StreamResult;
048
049import org.apache.avalon.framework.activity.Disposable;
050import org.apache.avalon.framework.activity.Initializable;
051import org.apache.avalon.framework.component.Component;
052import org.apache.avalon.framework.configuration.Configuration;
053import org.apache.avalon.framework.configuration.ConfigurationException;
054import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
055import org.apache.avalon.framework.service.ServiceException;
056import org.apache.avalon.framework.service.ServiceManager;
057import org.apache.avalon.framework.service.Serviceable;
058import org.apache.cocoon.xml.AttributesImpl;
059import org.apache.cocoon.xml.XMLUtils;
060import org.apache.commons.lang3.StringUtils;
061import org.apache.xml.serializer.OutputPropertiesFactory;
062import org.xml.sax.SAXException;
063
064import org.ametys.core.ObservationConstants;
065import org.ametys.core.authentication.CredentialProvider;
066import org.ametys.core.authentication.CredentialProviderFactory;
067import org.ametys.core.authentication.CredentialProviderModel;
068import org.ametys.core.datasource.ConnectionHelper;
069import org.ametys.core.datasource.SQLDataSourceManager;
070import org.ametys.core.observation.Event;
071import org.ametys.core.observation.ObservationManager;
072import org.ametys.core.script.SQLScriptHelper;
073import org.ametys.core.ui.Callable;
074import org.ametys.core.user.CurrentUserProvider;
075import org.ametys.core.user.InvalidModificationException;
076import org.ametys.core.user.User.UserCreationOrigin;
077import org.ametys.core.user.UserIdentity;
078import org.ametys.core.user.directory.ModifiableUserDirectory;
079import org.ametys.core.user.directory.UserDirectory;
080import org.ametys.core.user.directory.UserDirectoryFactory;
081import org.ametys.core.user.directory.UserDirectoryModel;
082import org.ametys.core.util.I18nUtils;
083import org.ametys.core.util.ReadXMLDataHelper;
084import org.ametys.plugins.core.authentication.MultifactorAuthenticationManager;
085import org.ametys.plugins.core.impl.user.directory.JdbcUserDirectory;
086import org.ametys.plugins.core.impl.user.directory.JdbcUserDirectory.StrongPasswordRequirements;
087import org.ametys.plugins.core.impl.user.directory.StaticUserDirectory;
088import org.ametys.runtime.i18n.I18nizableText;
089import org.ametys.runtime.model.DefinitionContext;
090import org.ametys.runtime.model.ElementDefinition;
091import org.ametys.runtime.model.checker.ItemCheckerDescriptor;
092import org.ametys.runtime.model.type.ElementType;
093import org.ametys.runtime.model.type.ModelItemTypeConstants;
094import org.ametys.runtime.model.type.xml.XMLElementType;
095import org.ametys.runtime.plugin.PluginsManager;
096import org.ametys.runtime.plugin.PluginsManager.Status;
097import org.ametys.runtime.plugin.component.AbstractLogEnabled;
098import org.ametys.runtime.util.AmetysHomeHelper;
099
100/**
101 * DAO for accessing {@link UserPopulation}
102 */
103public class UserPopulationDAO extends AbstractLogEnabled implements Component, Serviceable, Initializable, Disposable
104{
105    /** Avalon Role */
106    public static final String ROLE = UserPopulationDAO.class.getName();
107    
108    /** The id of the "admin" population */
109    public static final String ADMIN_POPULATION_ID = "admin_population";
110    
111    /** The id of the "user system" login */
112    public static final String SYSTEM_USER_LOGIN = "system-user";
113    /** The id of the "user system" population */
114    public static final UserIdentity SYSTEM_USER_IDENTITY = new UserIdentity(SYSTEM_USER_LOGIN, ADMIN_POPULATION_ID);
115    
116    /** The sql table for admin users */
117    private static final String __ADMIN_TABLENAME = "AdminUsers";
118
119    /** The path of the XML file containing the user populations */
120    private static File __USER_POPULATIONS_FILE;
121    
122    /** The regular expression for an id of a user population */
123    private static final String __ID_REGEX = "^[a-z][a-z0-9_-]*";
124    
125    /** The date (as a long) of the last time the {@link #__USER_POPULATIONS_FILE Populations file} was read (last update) */
126    private long _lastFileReading;
127    
128    /** The whole user populations of the application */
129    private Map<String, UserPopulation> _userPopulations;
130    /** The misconfigured user populations */
131    private Set<String> _misconfiguredUserPopulations;
132    
133    /** The list of population ids which are declared in the user population file but were not instanciated since their configuration led to an error */
134    private Set<String> _ignoredPopulations;
135    
136    /** The population admin */
137    private UserPopulation _adminUserPopulation;
138    
139    /** The user directories factory  */
140    private UserDirectoryFactory _userDirectoryFactory;
141
142    /** The credential providers factory  */
143    private CredentialProviderFactory _credentialProviderFactory;
144    
145    /** The extension point for population consumers */
146    private PopulationConsumerExtensionPoint _populationConsumerEP;
147
148    private ObservationManager _observationManager;
149
150    private CurrentUserProvider _currentUserProvider;
151
152    private I18nUtils _i18nutils;
153    
154    private MultifactorAuthenticationManager _multifactorAuthenticationManager;
155
156    private ReadXMLDataHelper _readXMLDataHelper;
157    
158    private ServiceManager _manager;
159
160    @Override
161    public void initialize()
162    {
163        __USER_POPULATIONS_FILE = new File(AmetysHomeHelper.getAmetysHome(), "config" + File.separator + "user-populations.xml");
164        _userPopulations = new LinkedHashMap<>();
165        _misconfiguredUserPopulations = new HashSet<>();
166        _ignoredPopulations = new HashSet<>();
167        _lastFileReading = 0;
168    }
169    
170    private PopulationConsumerExtensionPoint _getPopulationConsumerExtensionPoint()
171    {
172        if (_populationConsumerEP == null)
173        {
174            try
175            {
176                _populationConsumerEP = (PopulationConsumerExtensionPoint) _manager.lookup(PopulationConsumerExtensionPoint.ROLE);
177            }
178            catch (ServiceException e)
179            {
180                throw new RuntimeException("Failed to retrieve PopulationConsumerExtensionPoint", e);
181            }
182        }
183        return _populationConsumerEP;
184    }
185    
186    private ObservationManager _getObservationManager()
187    {
188        if (_observationManager == null)
189        {
190            try
191            {
192                _observationManager = (ObservationManager) _manager.lookup(ObservationManager.ROLE);
193            }
194            catch (ServiceException e)
195            {
196                // Not a safe component... ignore it
197                return null;
198            }
199        }
200        return _observationManager;
201    }
202    
203    private UserDirectoryFactory _getUserDirectoryFactory()
204    {
205        if (_userDirectoryFactory == null)
206        {
207            try
208            {
209                _userDirectoryFactory = (UserDirectoryFactory) _manager.lookup(UserDirectoryFactory.ROLE);
210            }
211            catch (ServiceException e)
212            {
213                throw new RuntimeException("Failed to retrieve UserDirectoryFactory", e);
214            }
215        }
216        return _userDirectoryFactory;
217    }
218    
219    private CredentialProviderFactory _getCredentialProviderFactory()
220    {
221        if (_credentialProviderFactory == null)
222        {
223            try
224            {
225                _credentialProviderFactory = (CredentialProviderFactory) _manager.lookup(CredentialProviderFactory.ROLE);
226            }
227            catch (ServiceException e)
228            {
229                throw new RuntimeException("Failed to retrieve CredentialProviderFactory", e);
230            }
231        }
232        return _credentialProviderFactory;
233    }
234    
235    private CurrentUserProvider _getCurrentUserProvider()
236    {
237        if (_currentUserProvider == null)
238        {
239            try
240            {
241                _currentUserProvider = (CurrentUserProvider) _manager.lookup(CurrentUserProvider.ROLE);
242            }
243            catch (ServiceException e)
244            {
245                throw new RuntimeException("Failed to retrieve CurrentUserProvider", e);
246            }
247        }
248        return _currentUserProvider;
249    }
250    
251    private I18nUtils _getI18nUtils()
252    {
253        if (_i18nutils == null)
254        {
255            try
256            {
257                _i18nutils = (I18nUtils) _manager.lookup(I18nUtils.ROLE);
258            }
259            catch (ServiceException e)
260            {
261                throw new RuntimeException("Failed to retrieve I18nUtils", e);
262            }
263        }
264        return _i18nutils;
265    }
266    
267    private MultifactorAuthenticationManager _getMultifactorAuthenticationManager()
268    {
269        if (_multifactorAuthenticationManager == null)
270        {
271            try
272            {
273                _multifactorAuthenticationManager = (MultifactorAuthenticationManager) _manager.lookup(MultifactorAuthenticationManager.ROLE);
274            }
275            catch (ServiceException e)
276            {
277                throw new RuntimeException("Failed to retrieve multifactor authentication manager", e);
278            }
279        }
280        return _multifactorAuthenticationManager;
281    }
282    
283    private ReadXMLDataHelper _getReadXMLDataHelper()
284    {
285        if (_readXMLDataHelper == null)
286        {
287            try
288            {
289                _readXMLDataHelper = (ReadXMLDataHelper) _manager.lookup(ReadXMLDataHelper.ROLE);
290            }
291            catch (ServiceException e)
292            {
293                throw new RuntimeException("Failed to retrieve read XML data helper", e);
294            }
295        }
296        return _readXMLDataHelper;
297    }
298    
299    @Override
300    public void service(ServiceManager manager) throws ServiceException
301    {
302        _manager =  manager;
303    }
304    
305    /**
306     * Gets all the populations to JSON format
307     * @param withAdmin True to include the "admin" population
308     * @return A list of object representing the {@link UserPopulation}s
309     */
310    public List<Object> getUserPopulationsAsJson(boolean withAdmin)
311    {
312        return getUserPopulations(withAdmin).stream().map(this::getUserPopulationAsJson).collect(Collectors.toList());
313    }
314    
315    /**
316     * Gets a population to JSON format
317     * @param userPopulation The user population to get
318     * @return An object representing a {@link UserPopulation}
319     */
320    public Map<String, Object> getUserPopulationAsJson(UserPopulation userPopulation)
321    {
322        Map<String, Object> result = new LinkedHashMap<>();
323        result.put("id", userPopulation.getId());
324        result.put("label", userPopulation.getLabel());
325        result.put("enabled", userPopulation.isEnabled());
326        result.put("valid", isValid(userPopulation.getId()));
327        result.put("isInUse", _getPopulationConsumerExtensionPoint().isInUse(userPopulation.getId()));
328        
329        List<Object> userDirectories = new ArrayList<>();
330        for (UserDirectory ud : userPopulation.getUserDirectories())
331        {
332            String udModelId = ud.getUserDirectoryModelId();
333            UserDirectoryModel udModel = _getUserDirectoryFactory().getExtension(udModelId);
334            
335            Map<String, Object> directory = new HashMap<>();
336            directory.put("id", ud.getId());
337            
338            if (StringUtils.isNotBlank(ud.getLabel()))
339            {
340                directory.put("label", _getI18nUtils().translate(udModel.getLabel()) + " (" + ud.getLabel() + ")");
341            }
342            else
343            {
344                directory.put("label", udModel.getLabel());
345            }
346            directory.put("modifiable", ud instanceof ModifiableUserDirectory);
347            userDirectories.add(directory);
348        }
349        result.put("userDirectories", userDirectories);
350        
351        List<Object> credentialProviders = new ArrayList<>();
352        for (CredentialProvider cp : userPopulation.getCredentialProviders())
353        {
354            String cpModelId = cp.getCredentialProviderModelId();
355            CredentialProviderModel cpModel = _getCredentialProviderFactory().getExtension(cpModelId);
356            
357            Map<String, Object> credentialProvider = new HashMap<>();
358            credentialProvider.put("id", cp.getId());
359            
360            if (StringUtils.isNotBlank(cp.getLabel()))
361            {
362                credentialProvider.put("label", _getI18nUtils().translate(cpModel.getLabel()) + " (" + cp.getLabel() + ")");
363            }
364            else
365            {
366                credentialProvider.put("label", cpModel.getLabel());
367            }
368            credentialProviders.add(credentialProvider);
369        }
370        result.put("credentialProviders", credentialProviders);
371        
372        return result;
373    }
374    
375    /**
376     * Gets all the populations of this application
377     * @param includeAdminPopulation True to include the "admin" population
378     * @return A list of {@link UserPopulation}
379     */
380    public List<UserPopulation> getUserPopulations(boolean includeAdminPopulation)
381    {
382        List<UserPopulation> result = new ArrayList<>();
383        if (includeAdminPopulation)
384        {
385            result.add(getAdminPopulation());
386        }
387        
388        // Don't read in safe mode, we know that only the admin population is needed in this case and we want to prevent some warnings in the logs for non-safe features not found
389        if (Status.OK.equals(PluginsManager.getInstance().getStatus()))
390        {
391            _readPopulations(false);
392            result.addAll(_userPopulations.values());
393        }
394        
395        return result;
396    }
397    
398    /**
399     * Gets all the enabled populations of this application
400     * @param withAdmin True to include the "admin" population
401     * @return A list of enabled {@link UserPopulation}
402     */
403    public List<UserPopulation> getEnabledUserPopulations(boolean withAdmin)
404    {
405        return getUserPopulations(withAdmin).stream().filter(UserPopulation::isEnabled).collect(Collectors.toList());
406    }
407    
408    /**
409     * Gets a population with its id.
410     * @param id The id of the population
411     * @return The {@link UserPopulation}, or null if not found
412     */
413    public UserPopulation getUserPopulation(String id)
414    {
415        if (ADMIN_POPULATION_ID.equals(id))
416        {
417            return getAdminPopulation();
418        }
419        
420        _readPopulations(false);
421        return _userPopulations.get(id);
422    }
423    
424    /**
425     * Gets the list of the ids of all the population of the application
426     * @return The list of the ids of all the populations
427     */
428    @Callable
429    public List<String> getUserPopulationsIds()
430    {
431        _readPopulations(false);
432        return new ArrayList<>(_userPopulations.keySet());
433    }
434    
435    /**
436     * Returns the id of population which have a fatal invalid configuration.
437     * These populations can NOT be used by application.
438     * @return The ignored populations
439     */
440    public Set<String> getIgnoredPopulations()
441    {
442        _readPopulations(false);
443        return _ignoredPopulations;
444    }
445    
446    /**
447     * Returns the id of population which have at least one user directory or one credential provider misconfigured
448     * These populations can be used by application.
449     * @return The misconfigured populations.
450     */
451    public Set<String> getMisconfiguredPopulations()
452    {
453        _readPopulations(false);
454        return _misconfiguredUserPopulations;
455    }
456    
457    /**
458     * Return the configuration file.
459     * @return the configuration file.
460     */
461    public File getConfigurationFile()
462    {
463        return __USER_POPULATIONS_FILE;
464    }
465    
466    /**
467     * Gets the configuration for creating/editing a user population.
468     * @return A map containing information about what is needed to create/edit a user population
469     * @throws Exception If an error occurs.
470     */
471    @Callable
472    public Map<String, Object> getEditionConfiguration() throws Exception
473    {
474        Map<String, Object> result = new LinkedHashMap<>();
475        DefinitionContext context = DefinitionContext.newInstance().withEdition(true);
476        
477        List<Object> userDirectoryModels = new ArrayList<>();
478        for (String extensionId : _getUserDirectoryFactory().getExtensionsIds())
479        {
480            UserDirectoryModel udModel = _getUserDirectoryFactory().getExtension(extensionId);
481            if (udModel == null)
482            {
483                throw new IllegalStateException("The user population configuration refers to an unexisting extension for the user directory '" + extensionId + "'");
484            }
485            
486            Map<String, Object> udMap = new LinkedHashMap<>();
487            udMap.put("id", extensionId);
488            udMap.put("label", udModel.getLabel());
489            udMap.put("description", udModel.getDescription());
490            
491            Map<String, Object> params = new LinkedHashMap<>();
492            for (String paramId : udModel.getParameters().keySet())
493            {
494                // prefix in case of two parameters from two different models have the same id which can lead to some errorsin client-side
495                params.put(extensionId + "$" + paramId, udModel.getParameters().get(paramId).toJSON(context));
496            }
497            udMap.put("parameters", params);
498            
499            Map<String, Object> paramCheckers = new LinkedHashMap<>();
500            for (String paramCheckerId : udModel.getParameterCheckers().keySet())
501            {
502                ItemCheckerDescriptor paramChecker = udModel.getParameterCheckers().get(paramCheckerId);
503                paramCheckers.put(extensionId + "$" + paramCheckerId, paramChecker.toJSON());
504            }
505            udMap.put("parameterCheckers", paramCheckers);
506            
507            userDirectoryModels.add(udMap);
508        }
509        result.put("userDirectoryModels", userDirectoryModels);
510        
511        List<Object> credentialProviderModels = new ArrayList<>();
512        for (String extensionId : _getCredentialProviderFactory().getExtensionsIds())
513        {
514            CredentialProviderModel cpModel = _getCredentialProviderFactory().getExtension(extensionId);
515            if (cpModel == null)
516            {
517                throw new IllegalStateException("The user population configuration refers to an unexisting extension for the credential provider '" + extensionId + "'");
518            }
519
520            Map<String, Object> cpMap = new LinkedHashMap<>();
521            cpMap.put("id", extensionId);
522            cpMap.put("label", cpModel.getLabel());
523            cpMap.put("description", cpModel.getDescription());
524            
525            Map<String, Object> params = new LinkedHashMap<>();
526            for (String paramId : cpModel.getParameters().keySet())
527            {
528                // prefix in case of two parameters from two different models have the same id which can lead to some errors in client-side
529                params.put(extensionId + "$" + paramId, cpModel.getParameters().get(paramId).toJSON(context));
530            }
531            cpMap.put("parameters", params);
532            
533            Map<String, Object> paramCheckers = new LinkedHashMap<>();
534            for (String paramCheckerId : cpModel.getParameterCheckers().keySet())
535            {
536                ItemCheckerDescriptor paramChecker = cpModel.getParameterCheckers().get(paramCheckerId);
537                paramCheckers.put(extensionId + "$" + paramCheckerId, paramChecker.toJSON());
538            }
539            cpMap.put("parameterCheckers", paramCheckers);
540            
541            credentialProviderModels.add(cpMap);
542        }
543        result.put("credentialProviderModels", credentialProviderModels);
544        
545        return result;
546    }
547    
548    /**
549     * Gets the values of the parameters of the given population
550     * @param id The id of the population
551     * @return The values of the parameters
552     */
553    @Callable
554    public Map<String, Object> getPopulationParameterValues(String id)
555    {
556        Map<String, Object> result = new LinkedHashMap<>();
557        
558        _readPopulations(false);
559        UserPopulation up = _userPopulations.get(id);
560        
561        if (up == null)
562        {
563            getLogger().error("The UserPopulation of id '{}' does not exists.", id);
564            result.put("error", "unknown");
565            return result;
566        }
567        
568        // Population Label
569        result.put("label", up.getLabel());
570        result.put("id", up.getId());
571        
572        // User Directories
573        List<Map<String, Object>> userDirectories = new ArrayList<>();
574        
575        for (UserDirectory ud : up.getUserDirectories())
576        {
577            Map<String, Object> ud2json = new HashMap<>();
578            String udModelId = ud.getUserDirectoryModelId();
579            UserDirectoryModel model = _getUserDirectoryFactory().getExtension(udModelId);
580            if (model == null)
581            {
582                throw new IllegalStateException("The user population configuration refers to an unexisting extension for the user directory '" + udModelId + "'");
583            }
584
585            ud2json.put("id", ud.getId());
586            ud2json.put("udModelId", udModelId);
587            ud2json.put("label", ud.getLabel());
588            Map<String, Object> params = new HashMap<>();
589            for (String key : ud.getParameterValues().keySet())
590            {
591                ElementDefinition parameter = model.getParameters().get(key);
592                params.put(udModelId + "$" + key, ModelItemTypeConstants.PASSWORD_ELEMENT_TYPE_ID.equals(parameter.getType().getId()) ? "PASSWORD" : ud.getParameterValues().get(key));
593            }
594            ud2json.put("params", params);
595            userDirectories.add(ud2json);
596        }
597        result.put("userDirectories", userDirectories);
598        
599        // Credential Providers
600        List<Map<String, Object>> credentialProviders = new ArrayList<>();
601        
602        for (CredentialProvider cp : up.getCredentialProviders())
603        {
604            Map<String, Object> cp2json = new HashMap<>();
605            String cpModelId = cp.getCredentialProviderModelId();
606            CredentialProviderModel model = _getCredentialProviderFactory().getExtension(cpModelId);
607            if (model == null)
608            {
609                throw new IllegalStateException("The user population configuration refers to an unexisting extension for the credential provider '" + cpModelId + "'");
610            }
611
612            cp2json.put("id", cp.getId());
613            cp2json.put("cpModelId", cpModelId);
614            cp2json.put("label", cp.getLabel());
615            Map<String, Object> params = new HashMap<>();
616            for (String key : cp.getParameterValues().keySet())
617            {
618                ElementDefinition parameter = model.getParameters().get(key);
619                params.put(cpModelId + "$" + key, ModelItemTypeConstants.PASSWORD_ELEMENT_TYPE_ID.equals(parameter.getType().getId()) ? "PASSWORD" : cp.getParameterValues().get(key));
620            }
621            cp2json.put("params", params);
622            credentialProviders.add(cp2json);
623        }
624        result.put("credentialProviders", credentialProviders);
625        
626        return result;
627    }
628    
629    /**
630     * Gets the "admin" population
631     * @return The "admin" population
632     */
633    public synchronized UserPopulation getAdminPopulation()
634    {
635        if (_adminUserPopulation != null)
636        {
637            return _adminUserPopulation;
638        }
639        
640        _adminUserPopulation = new UserPopulation();
641        _adminUserPopulation.setId(ADMIN_POPULATION_ID);
642        
643        Map<String, Object> userDirectory1 = new HashMap<>();
644        userDirectory1.put("udModelId", "org.ametys.plugins.core.user.directory.Static");
645        userDirectory1.put("id", "static");
646        userDirectory1.put("org.ametys.plugins.core.user.directory.Static$runtime.users.static.users", SYSTEM_USER_LOGIN + ":System:User:");
647        
648        Map<String, Object> userDirectory2 = new HashMap<>();
649        String udModelId = "org.ametys.plugins.core.user.directory.Jdbc";
650        userDirectory2.put("udModelId", udModelId);
651        userDirectory2.put("id", "admin-jdbc");
652        
653        userDirectory2.put(udModelId + "$" + JdbcUserDirectory.DATASOURCE_PARAM_NAME, SQLDataSourceManager.AMETYS_INTERNAL_DATASOURCE_ID);
654        userDirectory2.put(udModelId + "$" + JdbcUserDirectory.USERS_TABLE_PARAM_NAME, __ADMIN_TABLENAME);
655        userDirectory2.put(udModelId + "$" + JdbcUserDirectory.USE_STRONG_PASSWORD_PARAM_NAME, true);
656        userDirectory2.put(udModelId + "$" + JdbcUserDirectory.USE_STRONG_PASSWORD_MIN_LENGTH_PARAM_NAME, 8);
657        userDirectory2.put(udModelId + "$" + JdbcUserDirectory.USE_STRONG_PASSWORD_MIN_LOWERCASE_PARAM_NAME, 1);
658        userDirectory2.put(udModelId + "$" + JdbcUserDirectory.USE_STRONG_PASSWORD_MIN_UPPERCASE_PARAM_NAME, 1);
659        userDirectory2.put(udModelId + "$" + JdbcUserDirectory.USE_STRONG_PASSWORD_MIN_NUMBERS_PARAM_NAME, 1);
660        userDirectory2.put(udModelId + "$" + JdbcUserDirectory.USE_STRONG_PASSWORD_MIN_SPECIALS_PARAM_NAME, 1);
661        userDirectory2.put(udModelId + "$" + JdbcUserDirectory.USE_STRONG_PASSWORD_SPECIAL_CHARACTERS_PARAM_NAME, StrongPasswordRequirements.DEFAULT_SPECIAL_CHARACTERS);
662        
663        Map<String, Object> credentialProvider = new HashMap<>();
664        String cpModelId = "org.ametys.core.authentication.FormBased";
665        credentialProvider.put("cpModelId", cpModelId);
666        credentialProvider.put(cpModelId + "$" + "runtime.authentication.form.cookies", "false");
667        credentialProvider.put(cpModelId + "$" + "runtime.authentication.form.store-password-in-session", "false");
668        credentialProvider.put(cpModelId + "$" + "runtime.authentication.form.captcha", "true");
669        credentialProvider.put(cpModelId + "$" + "runtime.authentication.form.login-by-email", "false");
670        credentialProvider.put(cpModelId + "$" + "runtime.authentication.form.security.storage", SQLDataSourceManager.AMETYS_INTERNAL_DATASOURCE_ID);
671        credentialProvider.put(cpModelId + "$" + "runtime.authentication.form.multifactor", "true");
672
673        // We may need to create the admin user
674        boolean wasExisting = false;
675        try (Connection connection = ConnectionHelper.getInternalSQLDataSourceConnection())
676        {
677            wasExisting = SQLScriptHelper.tableExists(connection, __ADMIN_TABLENAME);
678        }
679        catch (Exception e)
680        {
681            throw new RuntimeException("Cannot test if " + __ADMIN_TABLENAME + " table exists in internal database", e);
682        }
683        
684        List<UserDirectory> userDirectories = _getUserDirectories(ADMIN_POPULATION_ID, List.of(userDirectory1, userDirectory2));
685        List<CredentialProvider> credentialProviders = _getCredentialProviders(List.of(credentialProvider));
686        
687        _updateUserPopulation(_adminUserPopulation, new I18nizableText("plugin.core", "PLUGINS_CORE_USER_POPULATION_ADMIN_LABEL"), userDirectories, credentialProviders);
688        
689        ((StaticUserDirectory) _adminUserPopulation.getUserDirectory("static")).setGrantAllCredentials(false);
690        
691        if (!wasExisting)
692        {
693            Map<String, String> adminUserInformations = new HashMap<>();
694            adminUserInformations.put("login", "admin");
695            adminUserInformations.put("password", "admin");
696            adminUserInformations.put("firstname", "User");
697            adminUserInformations.put("lastname", "Administrator");
698            adminUserInformations.put("email", "");
699            
700            JdbcUserDirectory adminUserDirectory = (JdbcUserDirectory) _adminUserPopulation.getUserDirectories().get(1);
701            try
702            {
703                adminUserDirectory.forceStrongPassword(false);
704                adminUserDirectory.add(adminUserInformations, UserCreationOrigin.ADMIN);
705                adminUserDirectory.forceStrongPassword(true);
706            }
707            catch (InvalidModificationException e)
708            {
709                throw new RuntimeException("Cannot create the 'admin' user", e);
710            }
711        }
712        
713        return _adminUserPopulation;
714    }
715    
716    /**
717     * Adds a new population
718     * @param id The unique id of the population
719     * @param label The label of the population
720     * @param userDirectoriesParameters A list of user directory parameters
721     * @param credentialProvidersParameters A list of credential provider parameters
722     * @return A map containing the id of the created population, or the kind of error that occured
723     */
724    @Callable
725    public Map<String, Object> add(String id, String label, List<Map<String, Object>> userDirectoriesParameters, List<Map<String, Object>> credentialProvidersParameters)
726    {
727        _readPopulations(false);
728        
729        Map<String, Object> result = new LinkedHashMap<>();
730        
731        if (!_isCorrectId(id))
732        {
733            return null;
734        }
735        
736        UserPopulation userPopulation = new UserPopulation();
737        userPopulation.setId(id);
738        
739        List<UserDirectory> userDirectories = _getUserDirectories(id, userDirectoriesParameters);
740        List<CredentialProvider> credentialProviders = _getCredentialProviders(credentialProvidersParameters);
741        
742        _updateUserPopulation(userPopulation, new I18nizableText(label), userDirectories, credentialProviders);
743        
744        _userPopulations.put(id, userPopulation);
745        if (_writePopulations())
746        {
747            getLogger().error("An error occured when writing the configuration file which contains the user populations.", id);
748            result.put("error", "server");
749            return result;
750        }
751        
752        if (_getObservationManager() != null)
753        {
754            Map<String, Object> eventParams = new HashMap<>();
755            eventParams.put(ObservationConstants.ARGS_USERPOPULATION_ID, id);
756            _getObservationManager().notify(new Event(ObservationConstants.EVENT_USERPOPULATION_ADDED, _getCurrentUserProvider().getUser(), eventParams));
757        }
758        
759        result.put("id", id);
760        return result;
761    }
762    
763    private boolean _isCorrectId(String id)
764    {
765        if (_userPopulations.get(id) != null || ADMIN_POPULATION_ID.equals(id))
766        {
767            getLogger().error("The id '{}' is already used for a population.", id);
768            return false;
769        }
770        
771        if (!Pattern.matches(__ID_REGEX, id))
772        {
773            getLogger().error("The id '{}' is not a correct id for a user population.", id);
774            return false;
775        }
776        
777        return true;
778    }
779    
780    /**
781     * Edits the given population.
782     * @param id The id of the population to edit
783     * @param label The label of the population
784     * @param userDirectoriesParameters A list of user directory parameters
785     * @param credentialProvidersParameters A list of credential provider parameters
786     * @return A map containing the id of the edited population, or the kind of error that occured
787     */
788    @Callable
789    public Map<String, Object> edit(String id, String label, List<Map<String, Object>> userDirectoriesParameters, List<Map<String, Object>> credentialProvidersParameters)
790    {
791        _readPopulations(false);
792        
793        Map<String, Object> result = new LinkedHashMap<>();
794        
795        UserPopulation userPopulation = _userPopulations.get(id);
796        if (userPopulation == null)
797        {
798            getLogger().error("The UserPopulation with id '{}' does not exist, it cannot be edited.", id);
799            result.put("error", "unknown");
800            return result;
801        }
802        
803        // User directories
804        Map<String, Map<String, Object>> existingUserDirectoriesParameters = userPopulation.getUserDirectories().stream()
805                                                                               .collect(Collectors.toMap(UserDirectory::getId, UserDirectory::getParameterValues));
806        Map<String, String> userDirectoriesModels = userPopulation.getUserDirectories().stream().collect(Collectors.toMap(UserDirectory::getId, UserDirectory::getUserDirectoryModelId));
807        List<UserDirectory> userDirectories = _getUserDirectories(id, userDirectoriesParameters, existingUserDirectoriesParameters, userDirectoriesModels);
808
809        // Credential providers
810        Map<String, Map<String, Object>> existingCredentialProvidersParameters = userPopulation.getCredentialProviders().stream()
811                                                                                   .collect(Collectors.toMap(CredentialProvider::getId, CredentialProvider::getParameterValues));
812        Map<String, String> credentialProvidersModels = userPopulation.getCredentialProviders().stream().collect(Collectors.toMap(CredentialProvider::getId, CredentialProvider::getCredentialProviderModelId));
813        List<CredentialProvider> credentialProviders = _getCredentialProviders(credentialProvidersParameters, existingCredentialProvidersParameters, credentialProvidersModels);
814        
815        userPopulation.dispose();
816        _updateUserPopulation(userPopulation, new I18nizableText(label), userDirectories, credentialProviders);
817        
818        if (_writePopulations())
819        {
820            getLogger().error("An error occured when writing the configuration file which contains the user populations.", id);
821            result.put("error", "server");
822            return result;
823        }
824        
825        if (_getObservationManager() != null)
826        {
827            Map<String, Object> eventParams = new HashMap<>();
828            eventParams.put(ObservationConstants.ARGS_USERPOPULATION_ID, id);
829            _getObservationManager().notify(new Event(ObservationConstants.EVENT_USERPOPULATION_UPDATED, _getCurrentUserProvider().getUser(), eventParams));
830        }
831        
832        result.put("id", id);
833        return result;
834    }
835    
836    private List<UserDirectory> _getUserDirectories(String userPopulationId, List<Map<String, Object>> userDirectories)
837    {
838        return _getUserDirectories(userPopulationId, userDirectories, Collections.emptyMap(), Collections.emptyMap());
839    }
840    
841    private List<UserDirectory> _getUserDirectories(String userPopulationId, List<Map<String, Object>> userDirectories, Map<String, Map<String, Object>> existingUdParameters, Map<String, String> udModels)
842    {
843        // Create the user directories
844        List<UserDirectory> uds = new ArrayList<>();
845        for (Map<String, Object> userDirectoryParameters : userDirectories)
846        {
847            String id = (String) userDirectoryParameters.remove("id");
848            String modelId = (String) userDirectoryParameters.remove("udModelId");
849            String additionnalLabel = (String) userDirectoryParameters.remove("label");
850            Map<String, Object> typedParamValues = _getTypedUDParameters(userDirectoryParameters, modelId);
851            
852            if (StringUtils.isBlank(id))
853            {
854                id = org.ametys.core.util.StringUtils.generateKey();
855            }
856            else
857            {
858                _keepExistingUserDirectoryPassword(modelId, typedParamValues, existingUdParameters.getOrDefault(id, Collections.emptyMap()), udModels.get(id));
859            }
860            
861            uds.add(_getUserDirectoryFactory().createUserDirectory(id, modelId, typedParamValues, userPopulationId, additionnalLabel));
862        }
863        
864        return uds;
865    }
866    
867    private List<CredentialProvider> _getCredentialProviders(List<Map<String, Object>> credentialProviders)
868    {
869        return _getCredentialProviders(credentialProviders, Collections.emptyMap(), Collections.emptyMap());
870    }
871    
872    private List<CredentialProvider> _getCredentialProviders(List<Map<String, Object>> credentialProviders, Map<String, Map<String, Object>> existingCpParameters, Map<String, String> cpModels)
873    {
874        // Create the credential providers
875        List<CredentialProvider> cps = new ArrayList<>();
876        for (Map<String, Object> credentialProviderParameters : credentialProviders)
877        {
878            String id = (String) credentialProviderParameters.remove("id");
879            String modelId = (String) credentialProviderParameters.remove("cpModelId");
880            String additionnalLabel = (String) credentialProviderParameters.remove("label");
881            Map<String, Object> typedParamValues = _getTypedCPParameters(credentialProviderParameters, modelId);
882            
883            if (StringUtils.isBlank(id))
884            {
885                id = org.ametys.core.util.StringUtils.generateKey();
886            }
887            else
888            {
889                _keepExistingCredentialProviderPassword(modelId, typedParamValues, existingCpParameters.getOrDefault(id, Collections.emptyMap()), cpModels.get(id));
890            }
891            
892            CredentialProvider credentialProvider = _getCredentialProviderFactory().createCredentialProvider(id, modelId, typedParamValues, additionnalLabel);
893            if (credentialProvider != null)
894            {
895                cps.add(credentialProvider);
896            }
897        }
898        
899        return cps;
900    }
901    
902    private void _updateUserPopulation(UserPopulation userPopulation, I18nizableText label, List<UserDirectory> userDirectories, List<CredentialProvider> credentialProviders)
903    {
904        userPopulation.setLabel(label);
905        userPopulation.setUserDirectories(userDirectories);
906        userPopulation.setCredentialProviders(credentialProviders);
907        
908        _getMultifactorAuthenticationManager().initializeMFACryptoComponent(userPopulation);
909    }
910    
911    private void _keepExistingUserDirectoryPassword(String modelId, Map<String, Object> typedParamValues, Map<String, Object> existingUdParameters, String existingModelId)
912    {
913        // An existing id means an existing user directory
914        if (StringUtils.equals(modelId, existingModelId))
915        {
916            // The model did not changed, we want to keep the unmodified passwords
917            UserDirectoryModel userDirectoryModel = _getUserDirectoryFactory().getExtension(modelId);
918            for (Map.Entry<String, ? extends ElementDefinition> parameterEntry : userDirectoryModel.getParameters().entrySet())
919            {
920                ElementDefinition parameter = parameterEntry.getValue();
921                // If the parameter is a password AND has no value
922                if (ModelItemTypeConstants.PASSWORD_ELEMENT_TYPE_ID.equals(parameter.getType().getId()) && typedParamValues.get(parameterEntry.getKey()) == null)
923                {
924                    // Update the submitted data with the existing password
925                    typedParamValues.put(parameterEntry.getKey(), existingUdParameters.get(parameterEntry.getKey()));
926                }
927            }
928        }
929    }
930
931    private void _keepExistingCredentialProviderPassword(String modelId, Map<String, Object> typedParamValues, Map<String, Object> existingCpParameters, String existingModelId)
932    {
933        // An existing id means an existing credential provider
934        if (StringUtils.equals(modelId, existingModelId))
935        {
936            // The model did not changed, we want to keep the unmodified passwords
937            CredentialProviderModel credentialProviderModel = _getCredentialProviderFactory().getExtension(modelId);
938            for (Map.Entry<String, ? extends ElementDefinition> parameterEntry : credentialProviderModel.getParameters().entrySet())
939            {
940                ElementDefinition parameter = parameterEntry.getValue();
941                // If the parameter is a password AND has no value
942                if (ModelItemTypeConstants.PASSWORD_ELEMENT_TYPE_ID.equals(parameter.getType().getId()) && typedParamValues.get(parameterEntry.getKey()) == null)
943                {
944                    // Update the submitted data with the existing password
945                    typedParamValues.put(parameterEntry.getKey(), existingCpParameters.get(parameterEntry.getKey()));
946                }
947            }
948        }
949    }
950    
951    private Map<String, Object> _getTypedUDParameters(Map<String, Object> parameters, String modelId)
952    {
953        Map<String, Object> resultParameters = new LinkedHashMap<>();
954        
955        UserDirectoryModel model = _getUserDirectoryFactory().getExtension(modelId);
956        if (model == null)
957        {
958            throw new IllegalStateException("The user population configuration refers to an unexisting extension for the user directory '" + modelId + "'");
959        }
960
961        Map<String, ? extends ElementDefinition> declaredParameters = model.getParameters();
962        for (String paramNameWithPrefix : parameters.keySet())
963        {
964            String[] splitStr = paramNameWithPrefix.split("\\$", 2);
965            String prefix = splitStr[0];
966            String paramName = splitStr[1];
967            if (prefix.equals(modelId) && declaredParameters.containsKey(paramName))
968            {
969                Object originalValue = parameters.get(paramNameWithPrefix);
970                
971                ElementDefinition parameter = declaredParameters.get(paramName);
972                ElementType type = parameter.getType();
973                
974                if (parameter.isMultiple())
975                {
976                    @SuppressWarnings("unchecked")
977                    List<Object> multipleValue = originalValue instanceof List
978                            ? (List<Object>) originalValue
979                            : List.of(originalValue);
980                    
981                    Object[] typedValues = multipleValue.stream()
982                            .map(val -> type.castValue(val))
983                            .toArray(s -> (Object[]) Array.newInstance(type.getManagedClass(), s));
984                    
985                    resultParameters.put(paramName, typedValues);
986                }
987                else
988                {
989                    Object typedValue = type.castValue(originalValue);
990                    resultParameters.put(paramName, typedValue);
991                }
992            }
993            else if (prefix.equals(modelId))
994            {
995                getLogger().warn("The parameter {} is not declared in extension {}. It will be ignored", paramName, modelId);
996            }
997        }
998        
999        return resultParameters;
1000    }
1001    
1002    private Map<String, Object> _getTypedCPParameters(Map<String, Object> parameters, String modelId)
1003    {
1004        Map<String, Object> resultParameters = new LinkedHashMap<>();
1005        
1006        CredentialProviderModel model = _getCredentialProviderFactory().getExtension(modelId);
1007        if (model == null)
1008        {
1009            throw new IllegalStateException("The user population configuration refers to an unexisting extension for the credential provider '" + modelId + "'");
1010        }
1011        
1012        Map<String, ? extends ElementDefinition> declaredParameters = model.getParameters();
1013        for (String paramNameWithPrefix : parameters.keySet())
1014        {
1015            String[] splitStr = paramNameWithPrefix.split("\\$", 2);
1016            String prefix = splitStr[0];
1017            String paramName = splitStr[1];
1018            if (prefix.equals(modelId) && declaredParameters.containsKey(paramName))
1019            {
1020                Object originalValue = parameters.get(paramNameWithPrefix);
1021                
1022                ElementDefinition parameter = declaredParameters.get(paramName);
1023                ElementType type = parameter.getType();
1024                
1025                if (parameter.isMultiple())
1026                {
1027                    @SuppressWarnings("unchecked")
1028                    List<Object> multipleValue = originalValue instanceof List
1029                            ? (List<Object>) originalValue
1030                            : List.of(originalValue);
1031                    
1032                    Object[] typedValues = multipleValue.stream()
1033                            .map(val -> type.castValue(val))
1034                            .toArray(s -> (Object[]) Array.newInstance(type.getManagedClass(), s));
1035                    
1036                    resultParameters.put(paramName, typedValues);
1037                }
1038                else
1039                {
1040                    Object typedValue = type.castValue(originalValue);
1041                    resultParameters.put(paramName, typedValue);
1042                }
1043            }
1044            else if (prefix.equals(modelId))
1045            {
1046                getLogger().warn("The parameter {} is not declared in extension {}. It will be ignored", paramName, modelId);
1047            }
1048        }
1049        
1050        return resultParameters;
1051    }
1052
1053    /**
1054     * Removes the given population.
1055     * @param id The id of the population to remove
1056     * @return A map containing the id of the removed population
1057     */
1058    @Callable
1059    public Map<String, Object> remove(String id)
1060    {
1061        return remove(id, false);
1062    }
1063    
1064    /**
1065     * Removes the given population.
1066     * @param id The id of the population to remove
1067     * @param forceDeletion Delete the population even if it is still in use
1068     * @return A map containing the id of the removed population
1069     */
1070    public Map<String, Object> remove(String id, boolean forceDeletion)
1071    {
1072        Map<String, Object> result = new LinkedHashMap<>();
1073        
1074        // Check if the population is not the admin population
1075        if (ADMIN_POPULATION_ID.equals("id"))
1076        {
1077            return null;
1078        }
1079        
1080        // Check if the population is used
1081        if (!forceDeletion && _getPopulationConsumerExtensionPoint().isInUse(id))
1082        {
1083            getLogger().error("The UserPopulation with id '{}' is used, it cannot be removed.", id);
1084            result.put("error", "used");
1085            return result;
1086        }
1087        
1088        _readPopulations(false);
1089        
1090        UserPopulation up = _userPopulations.remove(id);
1091        
1092        if (up == null)
1093        {
1094            getLogger().error("The UserPopulation with id '{}' does not exist, it cannot be removed.", id);
1095            result.put("error", "unknown");
1096            return result;
1097        }
1098        
1099        up.dispose();
1100        
1101        if (_writePopulations())
1102        {
1103            result.put("error", "server");
1104            return result;
1105        }
1106        
1107        if (_getObservationManager() != null)
1108        {
1109            Map<String, Object> eventParams = new HashMap<>();
1110            eventParams.put(ObservationConstants.ARGS_USERPOPULATION_ID, id);
1111            _getObservationManager().notify(new Event(ObservationConstants.EVENT_USERPOPULATION_DELETED, _getCurrentUserProvider().getUser(), eventParams));
1112        }
1113        
1114        result.put("id", id);
1115        return result;
1116    }
1117    
1118    /**
1119     * Enables/Disables the given population
1120     * @param populationId The id of the population to enable/disable
1121     * @param enabled True to enable the population, false to disable it.
1122     * @return A map containing the id of the enabled/disabled population, or with an error.
1123     */
1124    @Callable
1125    public Map<String, Object> enable(String populationId, boolean enabled)
1126    {
1127        Map<String, Object> result = new LinkedHashMap<>();
1128        
1129        UserPopulation population = getUserPopulation(populationId);
1130        if (population != null)
1131        {
1132            population.enable(enabled);
1133            result.put("id", populationId);
1134        }
1135        else
1136        {
1137            getLogger().error("The UserPopulation with id '{}' does not exist, it cannot be enabled/disabled.", populationId);
1138            result.put("error", "unknown");
1139        }
1140        
1141        if (_writePopulations())
1142        {
1143            result.put("error", "server");
1144            return result;
1145        }
1146        
1147        return result;
1148    }
1149    
1150    /**
1151     * Determines if a population has a valid configuration
1152     * @param populationId The id of the population to retrieve state
1153     * @return A map, with the response as a boolean, or an error.
1154     */
1155    @Callable
1156    public boolean isValid(String populationId)
1157    {
1158        return !_misconfiguredUserPopulations.contains(populationId);
1159    }
1160    
1161    /**
1162     * Returns the enabled state of the given population
1163     * @param populationId The id of the population to retrieve state
1164     * @return A map, with the response as a booolean, or an error.
1165     */
1166    @Callable
1167    public Map<String, Object> isEnabled(String populationId)
1168    {
1169        Map<String, Object> result = new LinkedHashMap<>();
1170        
1171        UserPopulation population = getUserPopulation(populationId);
1172        if (population != null)
1173        {
1174            result.put("enabled", population.isEnabled());
1175        }
1176        else
1177        {
1178            result.put("error", "unknown");
1179        }
1180        
1181        return result;
1182    }
1183    
1184    /**
1185     * Determines if a population can be removed
1186     * @param populationId The id of the population
1187     * @return A map, with the response as a boolean, or an error.
1188     */
1189    @Callable
1190    public Map<String, Object> canRemove(String populationId)
1191    {
1192        Map<String, Object> result = new HashMap<>();
1193        
1194        //Can remove only if not used, event if the population is disabled
1195        result.put("canRemove", !_getPopulationConsumerExtensionPoint().isInUse(populationId));
1196        
1197        return result;
1198    }
1199    
1200    /**
1201     * If needed, reads the config file representing the populations and then
1202     * reinitializes and updates the internal representation of the populations.
1203     * @param forceRead True to avoid the use of the cache and force the reading of the file
1204     */
1205    private synchronized void _readPopulations(boolean forceRead)
1206    {
1207        try
1208        {
1209            if (!__USER_POPULATIONS_FILE.exists())
1210            {
1211                if (getLogger().isDebugEnabled())
1212                {
1213                    getLogger().debug("No user population file found at {}. Creating a new one.", __USER_POPULATIONS_FILE.getAbsolutePath());
1214                }
1215                _createPopulationsFile(__USER_POPULATIONS_FILE);
1216            }
1217            
1218            // In Linux file systems, the precision of java.io.File.lastModified() is the second, so we need here to always have
1219            // this (bad!) precision by doing the truncation to second precision (/1000 * 1000) on the millis time value.
1220            // Therefore, the boolean outdated is computed with '>=' operator, and not '>', which will lead to sometimes (but rarely) unnecessarily re-read the file.
1221            long fileLastModified = (__USER_POPULATIONS_FILE.lastModified() / 1000) * 1000;
1222            if (forceRead || fileLastModified >= _lastFileReading)
1223            {
1224                getLogger().debug("Reading user population file at {}", __USER_POPULATIONS_FILE.getAbsolutePath());
1225                
1226                long lastFileReading = Instant.now().truncatedTo(ChronoUnit.SECONDS).toEpochMilli();
1227                Map<String, UserPopulation> userPopulations = new LinkedHashMap<>();
1228                Set<String> ignoredPopulations = new HashSet<>();
1229                Set<String> misconfiguredUserPopulations = new HashSet<>();
1230
1231                Configuration cfg = new DefaultConfigurationBuilder().buildFromFile(__USER_POPULATIONS_FILE);
1232                for (Configuration childCfg : cfg.getChildren("userPopulation"))
1233                {
1234                    try
1235                    {
1236                        UserPopulation up = _configurePopulation(childCfg, misconfiguredUserPopulations);
1237                        userPopulations.put(up.getId(), up);
1238                    }
1239                    catch (ConfigurationException e)
1240                    {
1241                        getLogger().error("Fatal configuration error for population of id '{}'. The population will be ignored.", childCfg.getAttribute("id", ""), e);
1242                        ignoredPopulations.add(childCfg.getAttribute("id", ""));
1243                    }
1244                }
1245                
1246                getLogger().debug("User population file read. Found {} valid population(s), {} invalid population(s) and {} misconfigured population(s)", userPopulations.size(), ignoredPopulations.size(), misconfiguredUserPopulations.size());
1247                
1248                // Release previous components
1249                this.dispose();
1250                
1251                _lastFileReading = lastFileReading;
1252                _userPopulations = userPopulations;
1253                _ignoredPopulations = ignoredPopulations;
1254                _misconfiguredUserPopulations = misconfiguredUserPopulations;
1255            }
1256            else if (getLogger().isDebugEnabled())
1257            {
1258                getLogger().debug("No need to reload the user population file. The file modified at {} was not modified since {}", fileLastModified, _lastFileReading);
1259            }
1260        }
1261        catch (Exception e)
1262        {
1263            getLogger().error("Failed to retrieve user populations from the configuration file " + __USER_POPULATIONS_FILE, e);
1264        }
1265    }
1266    
1267    private void _createPopulationsFile(File file) throws IOException, TransformerConfigurationException, SAXException
1268    {
1269        file.createNewFile();
1270        try (OutputStream os = new FileOutputStream(file))
1271        {
1272            // create a transformer for saving sax into a file
1273            TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
1274            
1275            StreamResult result = new StreamResult(os);
1276            th.setResult(result);
1277
1278            // create the format of result
1279            Properties format = new Properties();
1280            format.put(OutputKeys.METHOD, "xml");
1281            format.put(OutputKeys.INDENT, "yes");
1282            format.put(OutputKeys.ENCODING, "UTF-8");
1283            format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4");
1284            th.getTransformer().setOutputProperties(format);
1285            th.startDocument();
1286            XMLUtils.createElement(th, "userPopulations");
1287            th.endDocument();
1288        }
1289        
1290        if (getLogger().isDebugEnabled())
1291        {
1292            getLogger().debug("Empty file for user populations created");
1293        }
1294    }
1295    
1296    private UserPopulation _configurePopulation(Configuration configuration, Set<String> misconfiguredUserPopulations) throws ConfigurationException
1297    {
1298        UserPopulation up = new UserPopulation();
1299        
1300        String upId = configuration.getAttribute("id");
1301        up.setId(upId);
1302        
1303        List<UserDirectory> userDirectories = _configureUserDirectories(configuration, upId, misconfiguredUserPopulations);
1304        List<CredentialProvider> credentialProviders = _configureCredentialProviders(configuration, upId);
1305
1306        _updateUserPopulation(up, new I18nizableText(configuration.getChild("label").getValue()), userDirectories, credentialProviders);
1307        
1308        boolean enabled = configuration.getAttributeAsBoolean("enabled", true) && userDirectories.size() > 0 && credentialProviders.size() > 0;
1309        up.enable(enabled);
1310        
1311        return up;
1312    }
1313    
1314    private List<UserDirectory> _configureUserDirectories(Configuration configuration, String upId, Set<String> misconfiguredUserPopulations) throws ConfigurationException
1315    {
1316        List<UserDirectory> userDirectories = new ArrayList<>();
1317        
1318        Configuration[] userDirectoriesConf = configuration.getChild("userDirectories").getChildren("userDirectory");
1319        for (Configuration userDirectoryConf : userDirectoriesConf)
1320        {
1321            String id = userDirectoryConf.getAttribute("id");
1322            String modelId = userDirectoryConf.getAttribute("modelId");
1323            String label = userDirectoryConf.getAttribute("label", null);
1324            
1325            try
1326            {
1327                Map<String, Object> paramValues = _getUDParametersFromConfiguration(userDirectoryConf, modelId, upId);
1328                UserDirectory ud = _getUserDirectoryFactory().createUserDirectory(id, modelId, paramValues, upId, label);
1329                if (ud != null)
1330                {
1331                    userDirectories.add(ud);
1332                }
1333            }
1334            catch (Exception e)
1335            {
1336                getLogger().warn("The population of id '" + upId + "' declares a user directory with an invalid configuration", e);
1337                misconfiguredUserPopulations.add(upId);
1338            }
1339        }
1340
1341        if (userDirectories.isEmpty())
1342        {
1343            misconfiguredUserPopulations.add(upId);
1344            getLogger().warn("The population of id '" + upId + "' does not have user directory with a valid configuration. It will be disabled until it will be fixed.");
1345        }
1346        
1347        return userDirectories;
1348    }
1349    
1350    private List<CredentialProvider> _configureCredentialProviders(Configuration configuration, String upId) throws ConfigurationException
1351    {
1352        List<CredentialProvider> credentialProviders = new ArrayList<>();
1353        
1354        Configuration[] credentialProvidersConf = configuration.getChild("credentialProviders").getChildren("credentialProvider");
1355        for (Configuration credentialProviderConf : credentialProvidersConf)
1356        {
1357            String id = credentialProviderConf.getAttribute("id");
1358            String modelId = credentialProviderConf.getAttribute("modelId");
1359            String additionnalLabel = credentialProviderConf.getAttribute("label", null);
1360            
1361            try
1362            {
1363                Map<String, Object> paramValues = _getCPParametersFromConfiguration(credentialProviderConf, modelId, upId);
1364                CredentialProvider cp = _getCredentialProviderFactory().createCredentialProvider(id, modelId, paramValues, additionnalLabel);
1365                if (cp != null)
1366                {
1367                    credentialProviders.add(cp);
1368                }
1369            }
1370            catch (Exception e)
1371            {
1372                getLogger().warn("The population of id '" + upId + "' declares a credential provider with an invalid configuration", e);
1373                _misconfiguredUserPopulations.add(upId);
1374            }
1375        }
1376        
1377        if (credentialProviders.isEmpty())
1378        {
1379            _misconfiguredUserPopulations.add(upId);
1380            getLogger().warn("The population of id '" + upId + "' does not have credential provider with a valid configuration. It will be disabled until it will be fixed.");
1381        }
1382        
1383        return credentialProviders;
1384    }
1385    
1386    /**
1387     * Get the typed parameters of a user directory used by a population
1388     * @param conf The user directory's configuration.
1389     * @param modelId The id of user directory model
1390     * @param populationId The id of population
1391     * @return The typed parameters
1392     * @throws IllegalArgumentException if the configured user directory references a non-existing user directory model
1393     * @throws ConfigurationException if a parameter is missing
1394     */
1395    private Map<String, Object> _getUDParametersFromConfiguration(Configuration conf, String modelId, String populationId) throws ConfigurationException, IllegalArgumentException
1396    {
1397        if (!_getUserDirectoryFactory().hasExtension(modelId))
1398        {
1399            throw new IllegalArgumentException(String.format("The population of id '%s' declares a non-existing user directory model with id '%s'. It will be ignored.", populationId, modelId));
1400        }
1401        
1402        Map<String, ? extends ElementDefinition> declaredParameters = _getUserDirectoryFactory().getExtension(modelId).getParameters();
1403        
1404        Map<String, List<I18nizableText>> errors = new HashMap<>();
1405        Map<String, Object> parameters = _getReadXMLDataHelper().readAndValidateXMLData(conf, modelId + "$", declaredParameters, errors);
1406        if (errors.isEmpty())
1407        {
1408            return parameters;
1409        }
1410        else
1411        {
1412            StringBuilder sb = new StringBuilder(String.format("The population of id '%s' declares a user directory model with id '%s' but some parameters are not valid:", populationId, modelId));
1413            for (String dataName : errors.keySet())
1414            {
1415                for (I18nizableText error : errors.get(dataName))
1416                {
1417                    sb.append("\n* '").append(dataName).append("': ").append(_getI18nUtils().translate(error));
1418                }
1419            }
1420            sb.append("\nThis user directory will be ignored.");
1421            
1422            throw new ConfigurationException(sb.toString(), conf);
1423        }
1424    }
1425    
1426    /**
1427     * Get the typed parameters of a credential provider used by a population
1428     * @param conf The credential provider's configuration.
1429     * @param modelId The id of credential provider model
1430     * @param populationId The id of population
1431     * @return The typed parameters
1432     * @throws IllegalArgumentException if the configured credential provider references a non-existing credential provider model
1433     * @throws ConfigurationException if a parameter is missing
1434     */
1435    private Map<String, Object> _getCPParametersFromConfiguration(Configuration conf, String modelId, String populationId) throws ConfigurationException, IllegalArgumentException
1436    {
1437        if (!_getCredentialProviderFactory().hasExtension(modelId))
1438        {
1439            throw new IllegalArgumentException(String.format("The population of id '%s' declares a non-existing credential provider model with id '%s'. It will be ignored.", populationId, modelId));
1440        }
1441        
1442        Map<String, ? extends ElementDefinition> declaredParameters = _getCredentialProviderFactory().getExtension(modelId).getParameters();
1443        
1444        Map<String, List<I18nizableText>> errors = new HashMap<>();
1445        Map<String, Object> parameters = _getReadXMLDataHelper().readAndValidateXMLData(conf, modelId + "$", declaredParameters, errors);
1446        if (errors.isEmpty())
1447        {
1448            return parameters;
1449        }
1450        else
1451        {
1452            StringBuilder sb = new StringBuilder(String.format("The population of id '%s' declares a credential provider model with id '%s' but some parameters are not valid:", populationId, modelId));
1453            for (String dataName : errors.keySet())
1454            {
1455                for (I18nizableText error : errors.get(dataName))
1456                {
1457                    sb.append("\n* '").append(dataName).append("': ").append(_getI18nUtils().translate(error));
1458                }
1459            }
1460            sb.append("\nThis credential provider will be ignored.");
1461            
1462            throw new ConfigurationException(sb.toString(), conf);
1463        }
1464    }
1465    
1466    /**
1467     * Erases the config file representing the populations and rebuild it 
1468     * from the internal representation of the populations.
1469     * @return True if an error occured
1470     */
1471    private boolean _writePopulations()
1472    {
1473        File backup = new File(__USER_POPULATIONS_FILE.getPath() + ".tmp");
1474        boolean errorOccured = false;
1475        
1476        // Create a backup file
1477        try
1478        {
1479            Files.copy(__USER_POPULATIONS_FILE.toPath(), backup.toPath());
1480        }
1481        catch (IOException e)
1482        {
1483            getLogger().error("Error when creating backup '" + __USER_POPULATIONS_FILE + "' file", e);
1484        }
1485        
1486        // Do writing
1487        try (OutputStream os = new FileOutputStream(__USER_POPULATIONS_FILE))
1488        {
1489            // create a transformer for saving sax into a file
1490            TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
1491            
1492            StreamResult result = new StreamResult(os);
1493            th.setResult(result);
1494
1495            // create the format of result
1496            Properties format = new Properties();
1497            format.put(OutputKeys.METHOD, "xml");
1498            format.put(OutputKeys.INDENT, "yes");
1499            format.put(OutputKeys.ENCODING, "UTF-8");
1500            format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4");
1501            th.getTransformer().setOutputProperties(format);
1502
1503            // sax the config
1504            try
1505            {
1506                _toSAX(th);
1507            }
1508            catch (Exception e)
1509            {
1510                getLogger().error("Error when saxing the userPopulations", e);
1511                errorOccured = true;
1512            }
1513        }
1514        catch (IOException | TransformerConfigurationException | TransformerFactoryConfigurationError e)
1515        {
1516            getLogger().error("Error when trying to modify the user populations with the configuration file " + __USER_POPULATIONS_FILE, e);
1517        }
1518        
1519        // Restore the file if an error previously occured
1520        try
1521        {
1522            if (errorOccured)
1523            {
1524                // An error occured, restore the original
1525                Files.copy(backup.toPath(), __USER_POPULATIONS_FILE.toPath(), StandardCopyOption.REPLACE_EXISTING);
1526                // Force to reread the file
1527                _readPopulations(true);
1528            }
1529            Files.deleteIfExists(backup.toPath());
1530        }
1531        catch (IOException e)
1532        {
1533            getLogger().error("Error when restoring backup '" + __USER_POPULATIONS_FILE + "' file", e);
1534        }
1535        
1536        return errorOccured;
1537    }
1538    
1539    private void _toSAX(TransformerHandler handler)
1540    {
1541        try
1542        {
1543            handler.startDocument();
1544            XMLUtils.startElement(handler, "userPopulations");
1545            for (UserPopulation up : _userPopulations.values())
1546            {
1547                _saxUserPopulation(up, handler);
1548            }
1549            
1550            XMLUtils.endElement(handler, "userPopulations");
1551            handler.endDocument();
1552        }
1553        catch (SAXException e)
1554        {
1555            getLogger().error("Error when saxing the userPopulations", e);
1556        }
1557    }
1558    
1559    private void _saxUserPopulation(UserPopulation userPopulation, TransformerHandler handler)
1560    {
1561        try
1562        {
1563            AttributesImpl atts = new AttributesImpl();
1564            atts.addCDATAAttribute("id", userPopulation.getId());
1565            atts.addCDATAAttribute("enabled", Boolean.toString(userPopulation.isEnabled()));
1566            XMLUtils.startElement(handler, "userPopulation", atts);
1567            
1568            userPopulation.getLabel().toSAX(handler, "label");
1569            
1570            // SAX user directories
1571            XMLUtils.startElement(handler, "userDirectories");
1572            for (UserDirectory ud : userPopulation.getUserDirectories())
1573            {
1574                AttributesImpl attr = new AttributesImpl();
1575                attr.addCDATAAttribute("id", ud.getId());
1576                attr.addCDATAAttribute("modelId", ud.getUserDirectoryModelId());
1577                attr.addCDATAAttribute("label", ud.getLabel() != null ? ud.getLabel() : "");
1578                XMLUtils.startElement(handler, "userDirectory", attr);
1579                
1580                UserDirectoryModel udModel = _getUserDirectoryFactory().getExtension(ud.getUserDirectoryModelId());
1581                _writeParameterValues(udModel.getParameters(), ud.getParameterValues(), handler);
1582
1583                XMLUtils.endElement(handler, "userDirectory");
1584            }
1585            XMLUtils.endElement(handler, "userDirectories");
1586            
1587            // SAX credential providers
1588            XMLUtils.startElement(handler, "credentialProviders");
1589            for (CredentialProvider cp : userPopulation.getCredentialProviders())
1590            {
1591                AttributesImpl attr = new AttributesImpl();
1592                attr.addCDATAAttribute("id", cp.getId());
1593                attr.addCDATAAttribute("modelId", cp.getCredentialProviderModelId());
1594                attr.addCDATAAttribute("label", cp.getLabel() != null ? cp.getLabel() : "");
1595                XMLUtils.startElement(handler, "credentialProvider", attr);
1596
1597                CredentialProviderModel cpModel = _getCredentialProviderFactory().getExtension(cp.getCredentialProviderModelId());
1598                _writeParameterValues(cpModel.getParameters(), cp.getParameterValues(), handler);
1599
1600                XMLUtils.endElement(handler, "credentialProvider");
1601            }
1602            XMLUtils.endElement(handler, "credentialProviders");
1603            
1604            XMLUtils.endElement(handler, "userPopulation");
1605        }
1606        catch (SAXException e)
1607        {
1608            getLogger().error("Error when saxing the userPopulation " + userPopulation, e);
1609        }
1610    }
1611    
1612    private void _writeParameterValues(Map<String, ? extends ElementDefinition> definitions, Map<String, Object> paramValues, TransformerHandler handler) throws SAXException
1613    {
1614        for (String paramName : paramValues.keySet())
1615        {
1616            Object value = paramValues.get(paramName);
1617            ElementDefinition definition = definitions.get(paramName);
1618            
1619            Optional<XMLElementType> type = Optional.ofNullable(definition)
1620                    .map(ElementDefinition::getType)
1621                    .filter(XMLElementType.class::isInstance)
1622                    .map(XMLElementType.class::cast);
1623            
1624            if (type.isPresent())
1625            {
1626                type.get().write(handler, paramName, value);
1627            }
1628        }
1629    }
1630    
1631    @Override
1632    public void dispose()
1633    {
1634        for (UserPopulation up : _userPopulations.values())
1635        {
1636            up.dispose();
1637        }
1638        
1639        _userPopulations.clear();
1640        _lastFileReading = 0;
1641    }
1642}