001/*
002 *  Copyright 2017 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.site;
017
018import java.io.ByteArrayInputStream;
019import java.io.ByteArrayOutputStream;
020import java.io.File;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Comparator;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.TreeMap;
028import java.util.concurrent.locks.Lock;
029import java.util.concurrent.locks.ReentrantLock;
030
031import org.apache.avalon.framework.component.Component;
032import org.apache.avalon.framework.configuration.Configuration;
033import org.apache.avalon.framework.configuration.ConfigurationException;
034import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
035import org.apache.avalon.framework.service.ServiceException;
036import org.apache.avalon.framework.service.ServiceManager;
037import org.apache.avalon.framework.service.Serviceable;
038import org.apache.commons.lang.StringUtils;
039import org.apache.http.client.methods.CloseableHttpResponse;
040import org.apache.http.client.methods.HttpGet;
041import org.apache.http.impl.client.CloseableHttpClient;
042
043import org.ametys.core.authentication.CredentialProviderFactory;
044import org.ametys.core.authentication.CredentialProviderModel;
045import org.ametys.core.datasource.AbstractDataSourceManager.DataSourceDefinition;
046import org.ametys.core.datasource.LDAPDataSourceManager;
047import org.ametys.core.datasource.SQLDataSourceManager;
048import org.ametys.core.user.directory.UserDirectoryFactory;
049import org.ametys.core.user.directory.UserDirectoryModel;
050import org.ametys.core.user.population.UserPopulation;
051import org.ametys.core.user.population.UserPopulationDAO;
052import org.ametys.runtime.config.Config;
053import org.ametys.runtime.config.ConfigManager;
054import org.ametys.runtime.i18n.I18nizableText;
055import org.ametys.runtime.parameter.Parameter;
056import org.ametys.runtime.parameter.ParameterHelper.ParameterType;
057import org.ametys.runtime.plugin.component.AbstractLogEnabled;
058import org.ametys.runtime.servlet.RuntimeServlet;
059import org.ametys.runtime.util.AmetysHomeHelper;
060import org.ametys.site.BackOfficeRequestHelper;
061
062/**
063 * A cache for site information provided by the Back-Office.
064 */
065public class SiteInformationCache extends AbstractLogEnabled implements Serviceable, Component
066{
067    /** Avalon Role */ 
068    public static final String ROLE = SiteInformationCache.class.getName();
069    
070    /** Prefix for backoffice synchronized userpopulations */
071    public static final String BACKOFFICE_PREFIX_IDENTIFIER = "bo-";
072    
073    private final Lock _syncLock = new ReentrantLock();
074    
075    private Map<SiteUrl, Site> _sites;
076    
077    private LDAPDataSourceManager _ldapDataSourceManager;
078    private SQLDataSourceManager _sqlDataSourceManager;
079    private UserPopulationDAO _userPopulationDAO;
080    private UserDirectoryFactory _userDirectoryFactory;
081    private CredentialProviderFactory _credentialProviderFactory;
082
083    public void service(ServiceManager manager) throws ServiceException
084    {
085        _sqlDataSourceManager = (SQLDataSourceManager) manager.lookup(SQLDataSourceManager.ROLE);
086        _ldapDataSourceManager = (LDAPDataSourceManager) manager.lookup(LDAPDataSourceManager.ROLE);
087        _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE);
088        _userDirectoryFactory = (UserDirectoryFactory) manager.lookup(UserDirectoryFactory.ROLE);
089        _credentialProviderFactory = (CredentialProviderFactory) manager.lookup(CredentialProviderFactory.ROLE);
090    }
091    
092    /**
093     * Clear cached informations.
094     */
095    public void resetSitesCache()
096    {
097        _sites = null;
098    }
099    
100    /**
101     * Returns the cached informations.
102     * @return the cached informations.
103     */
104    public Map<SiteUrl, Site> getSites()
105    {
106        _synchronize();
107        return _sites;
108    }
109
110    private void _synchronize()
111    {
112        if (_sites == null)
113        {
114            if (_syncLock.tryLock())
115            {
116                try
117                {
118                    _synchronizeSites();
119                    _synchronizePopulationsAndDatasources();
120                }
121                catch (Exception e)
122                {
123                    throw new RuntimeException("Unable to synchronize sites data", e);
124                }
125                finally
126                {
127                    _syncLock.unlock();
128                }
129            }
130            else
131            {
132                // some is already filling _sites for us... we just have to wait
133                _syncLock.lock();
134                _syncLock.unlock();
135            }
136        }
137    }
138    
139    private void _synchronizeSites() throws ConfigurationException
140    {
141        Collection<Site> sites = new ArrayList<>();
142        
143        Configuration boConfiguration = _getBackofficeConfiguration("/_sites.xml");
144        _configureSites (boConfiguration, sites);
145        
146        TreeMap<SiteUrl, Site> sortedSites = new TreeMap<>(new SiteUrlComparator());
147        
148        for (Site site : sites)
149        {
150            for (SiteUrl url : site.getSiteUrls())
151            {
152                sortedSites.put(url, site);
153            }
154        }
155        
156        _sites = sortedSites;
157    }
158    
159    private void _configureSites (Configuration conf, Collection<Site> sites) throws ConfigurationException
160    {
161        Configuration[] sitesConf = conf.getChildren("site");
162        for (Configuration siteConf : sitesConf)
163        {
164            String name = siteConf.getAttribute("name");
165
166            List<String> populationIds = new ArrayList<>();
167            for (Configuration populationConf : siteConf.getChild("populations").getChildren())
168            {
169                populationIds.add(populationConf.getValue());
170            }
171            
172            List<SiteUrl> urls = new ArrayList<>();
173            for (Configuration urlConf : siteConf.getChildren("url"))
174            {
175                String serverName = urlConf.getAttribute("serverName");
176                String serverPort = urlConf.getAttribute("serverPort");
177                String serverPath = urlConf.getAttribute("serverPath");
178                
179                urls.add(new SiteUrl(serverName, serverPort, serverPath));
180            }
181    
182            List<String> languages = new ArrayList<>();
183            for (Configuration langConf : siteConf.getChild("languages").getChildren())
184            {
185                languages.add(langConf.getName());
186            }
187            
188            sites.add(new Site(name, urls, languages, populationIds));
189            
190            // Sub sites
191            _configureSites (siteConf, sites);
192        }
193    }
194    
195    class SiteUrlComparator implements Comparator<SiteUrl>
196    {
197        @Override
198        public int compare(SiteUrl url1, SiteUrl url2)
199        {
200            int result = url2.getServerName().compareTo(url1.getServerName());
201            
202            if (result != 0)
203            {
204                return result;
205            }
206            
207            result = url2.getServerPort().compareTo(url1.getServerPort());
208            
209            if (result != 0)
210            {
211                return result;
212            }
213            
214            return url2.getServerPath().compareTo(url1.getServerPath());
215        }
216    }
217    
218    private Configuration _getBackofficeConfiguration(String url)
219    {
220        // Get site names and URLs from the CMS
221        String cmsURL = Config.getInstance().getValueAsString("org.ametys.site.bo");
222        
223        try (CloseableHttpClient httpClient = BackOfficeRequestHelper.getHttpClient())
224        {
225            HttpGet httpGet = new HttpGet(cmsURL + url);
226            httpGet.addHeader("X-Ametys-FO", "true");    
227            
228            try (CloseableHttpResponse response = httpClient.execute(httpGet); ByteArrayOutputStream os = new ByteArrayOutputStream())
229            {
230                switch (response.getStatusLine().getStatusCode())
231                {
232                    case 200: 
233                        break;
234                    
235                    case 403: 
236                        throw new IllegalStateException("The CMS back-office refused the connection");
237                        
238                    case 500: 
239                    default:
240                        throw new IllegalStateException("The CMS back-office returned an error");
241                }
242                
243                response.getEntity().writeTo(os);
244                try (ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray()))
245                {
246                    Configuration conf = new DefaultConfigurationBuilder().build(is);
247                    return conf;
248                }
249            }
250        }
251        catch (Exception e)
252        {
253            throw new RuntimeException("Unable to synchronize site data", e);
254        }
255    }
256    
257    /**
258     * Synchronize the local user populations and datasources with backoffice
259     */
260    private void _synchronizePopulationsAndDatasources()
261    {
262        Configuration boConfiguration = _getBackofficeConfiguration("/_sites-populations.xml");
263        
264        _removeBackOfficeSynchronizedElements();
265        
266        _addBackOfficeSynchronizedElements(boConfiguration);
267    }
268    
269    private void _synchronizeMonitoringAndCaptcha(Configuration boConfiguration, Map<String, String> mapping)
270    {
271        // MONITORING
272        Configuration monitoringConfiguration = boConfiguration.getChild("Monitoring");
273        boolean enabled = monitoringConfiguration.getAttributeAsBoolean("enabled", false);
274        boolean wasEnabled = Config.getInstance().getValueAsBoolean("front.cache.monitoring.schedulers.enable");
275        String datasourceId = monitoringConfiguration.getChild("Datasource").getValue("");
276        
277        Map<String, String> valuesToChange = new HashMap<>();
278        if (enabled != wasEnabled)
279        {
280            valuesToChange.put("front.cache.monitoring.schedulers.enable", enabled ? "true" : "false");
281        }
282        if (enabled && !StringUtils.equals(datasourceId, Config.getInstance().getValueAsString("front.cache.monitoring.datasource.jdbc.pool")))
283        {
284            valuesToChange.put("front.cache.monitoring.datasource.jdbc.pool", mapping.get(datasourceId));
285        }
286        
287        // CAPTCHA
288        Configuration captchaConfiguration = boConfiguration.getChild("Captcha");
289        String captchaType = captchaConfiguration.getAttribute("type", null);
290        String captchaPublicKey = captchaConfiguration.getAttribute("publicKey", null);
291        String captchaSecretKey = captchaConfiguration.getAttribute("secretKey", null);
292        
293        if (!StringUtils.equals(captchaType, Config.getInstance().getValueAsString("runtime.captcha.type"))
294                || !StringUtils.equals(captchaPublicKey, Config.getInstance().getValueAsString("runtime.captcha.recaptcha.publickey"))
295                || !!StringUtils.equals(captchaSecretKey, Config.getInstance().getValueAsString("runtime.captcha.recaptcha.secretkey")))
296        {
297            valuesToChange.put("runtime.captcha.type", captchaType);
298            valuesToChange.put("runtime.captcha.recaptcha.publickey", captchaPublicKey);
299            valuesToChange.put("runtime.captcha.recaptcha.secretkey", captchaSecretKey);
300        }
301        
302        // UPDATE CONFIG
303        if (!valuesToChange.isEmpty())
304        {
305            try
306            {
307                Map<String, String> existingValues = Config.read();
308                existingValues.putAll(valuesToChange);
309                ConfigManager.getInstance().save(existingValues, new File(AmetysHomeHelper.getAmetysHomeConfig(), RuntimeServlet.CONFIG_FILE_NAME).getCanonicalPath());
310                // Need a restart to be taken in account 
311            }
312            catch (Exception e)
313            {
314                getLogger().error("The monitoring/captcha synchronization failed", e);
315            }
316        }
317    }
318
319    private void _addBackOfficeSynchronizedElements(Configuration configuration)
320    {
321        Map<String, String> mapping = _addBackofficeDatasources(configuration);
322        _addBackOfficeUserPopulations(configuration, mapping);
323        _synchronizeMonitoringAndCaptcha(configuration, mapping);
324    }
325    
326    private Map<String, String> _addBackofficeDatasources(Configuration configuration)
327    {
328        Map<String, String> mapping = new HashMap<>();
329        mapping.putAll(_addBackofficeSQLDatasources(configuration));
330        mapping.putAll(_addBackofficeLDAPDatasources(configuration));
331        return mapping;
332    }
333
334    private Map<String, String> _addBackofficeSQLDatasources(Configuration configuration)
335    {
336        Map<String, String> mapping = new HashMap<>();
337        
338        for (Configuration datasourceConfiguration : configuration.getChild("SQLDatasources").getChild("datasources").getChildren("datasource"))
339        {
340            String id = datasourceConfiguration.getAttribute("id", "");
341            
342            String defaultStatus = datasourceConfiguration.getAttribute("default", "false");
343            String privateStatus = datasourceConfiguration.getAttribute("private", "false");
344            String name = "DO NOT SELECT OR USE Back-office Synchronized Datasource (" + datasourceConfiguration.getChild("name").getValue("") + ")";
345            String description = datasourceConfiguration.getChild("description").getValue("");
346            
347            Map<String, Object> parameters = new HashMap<>();
348            for (Configuration parameterConfiguration : datasourceConfiguration.getChild("parameters").getChildren())
349            {
350                // Assuming all parameters are String (that is what .add is doing)
351                parameters.put(parameterConfiguration.getName(), parameterConfiguration.getValue(""));
352            }
353            
354            DataSourceDefinition newDatasource = _sqlDataSourceManager.add(new I18nizableText(name), new I18nizableText(description), parameters, "true".equals(privateStatus));
355            mapping.put(id, newDatasource.getId());
356            if ("true".equals(defaultStatus))
357            {
358                mapping.put(_sqlDataSourceManager.getDefaultDataSourceId(), newDatasource.getId());
359            }
360        }
361        
362        return mapping;
363    }
364    
365    private Map<String, String> _addBackofficeLDAPDatasources(Configuration configuration)
366    {
367        Map<String, String> mapping = new HashMap<>();
368        
369        for (Configuration datasourceConfiguration : configuration.getChild("LDAPDatasources").getChild("datasources").getChildren("datasource"))
370        {
371            String id = datasourceConfiguration.getAttribute("id", "");
372            
373            String defaultStatus = datasourceConfiguration.getAttribute("default", "false");
374            String privateStatus = datasourceConfiguration.getAttribute("private", "false");
375            String name = "DO NOT SELECT OR USE Back-office Synchronized Datasource (" + datasourceConfiguration.getChild("name").getValue("") + ")";
376            String description = datasourceConfiguration.getChild("description").getValue("");
377            
378            Map<String, Object> parameters = new HashMap<>();
379            for (Configuration parameterConfiguration : datasourceConfiguration.getChild("parameters").getChildren())
380            {
381                // Assuming all parameters are String (that is what .add is doing)
382                parameters.put(parameterConfiguration.getName(), parameterConfiguration.getValue(""));
383            }
384            
385            DataSourceDefinition newDatasource = _ldapDataSourceManager.add(new I18nizableText(name), new I18nizableText(description), parameters, "true".equals(privateStatus));
386            mapping.put(id, newDatasource.getId());
387            if ("true".equals(defaultStatus))
388            {
389                mapping.put(_ldapDataSourceManager.getDefaultDataSourceId(), newDatasource.getId());
390            }
391        }
392        
393        return mapping;
394    }    
395
396    private void _addBackOfficeUserPopulations(Configuration configuration, Map<String, String> mapping)
397    {
398        for (Configuration userPopulationConfiguration : configuration.getChild("UserPopulations").getChild("userPopulations").getChildren("userPopulation"))
399        {
400            String id = userPopulationConfiguration.getAttribute("id", "");
401            String enabled = userPopulationConfiguration.getAttribute("enabled", "true");
402            if ("true".equals(enabled))
403            {
404                String label = userPopulationConfiguration.getChild("label").getValue("");
405                
406                // USER DIRECTORIES
407                List<Map<String, String>> userDirectories = new ArrayList<>();
408                for (Configuration userDirectoryConfiguration : userPopulationConfiguration.getChild("userDirectories").getChildren("userDirectory"))
409                {
410                    Map<String, String> userDirectory = new HashMap<>();
411                    userDirectories.add(userDirectory);
412                    
413                    String udid = userDirectoryConfiguration.getAttribute("id", "");
414                    userDirectory.put("id", udid);
415                    
416                    String modelId = userDirectoryConfiguration.getAttribute("modelId", "");
417                    userDirectory.put("udModelId", modelId);
418
419                    String additionalLabel = userDirectoryConfiguration.getAttribute("label", "");
420                    userDirectory.put("label", additionalLabel);
421
422                    UserDirectoryModel userDirectoryModel = _userDirectoryFactory.getExtension(modelId);
423                    
424                    Map<String, ? extends Parameter<ParameterType>> parameters = userDirectoryModel.getParameters();
425                    for (String parameterId : parameters.keySet())
426                    {
427                        Parameter<ParameterType> parameter = parameters.get(parameterId);
428                        
429                        String value = userDirectoryConfiguration.getChild(parameterId).getValue("");
430                        
431                        if (parameter.getType() == ParameterType.DATASOURCE)
432                        {
433                            value = mapping.get(value);
434                        }
435                        
436                        userDirectory.put(modelId + "$" + parameterId, value);
437                    }
438                }
439                
440                // CREDENTIAL PROVIDERS
441                List<Map<String, String>> credentialProviders = new ArrayList<>();
442                for (Configuration credentialProviderConfiguration : userPopulationConfiguration.getChild("credentialProviders").getChildren("credentialProvider"))
443                {
444                    Map<String, String> credentialProvider = new HashMap<>();
445                    credentialProviders.add(credentialProvider);
446                    
447                    String cpid = credentialProviderConfiguration.getAttribute("id", "");
448                    credentialProvider.put("id", cpid);
449                    
450                    String modelId = credentialProviderConfiguration.getAttribute("modelId", "");
451                    credentialProvider.put("cpModelId", modelId);
452
453                    String additionalLabel = credentialProviderConfiguration.getAttribute("label", "");
454                    credentialProvider.put("label", additionalLabel);
455                    
456                    CredentialProviderModel credentialProviderModel = _credentialProviderFactory.getExtension(modelId);
457                    
458                    Map<String, ? extends Parameter<ParameterType>> parameters = credentialProviderModel.getParameters();
459                    for (String parameterId : parameters.keySet())
460                    {
461                        Parameter<ParameterType> parameter = parameters.get(parameterId);
462                        
463                        String value = credentialProviderConfiguration.getChild(parameterId).getValue("");
464                        
465                        if (parameter.getType() == ParameterType.DATASOURCE)
466                        {
467                            value = mapping.get(value);
468                        }
469                        
470                        credentialProvider.put(modelId + "$" + parameterId, value);
471                    }
472                }
473                
474                _userPopulationDAO.add(id, label, userDirectories, credentialProviders);
475            }
476        }
477    }
478
479
480    
481    private void _removeBackOfficeSynchronizedElements()
482    {
483        _removeBackOfficeUserPopulations();
484        
485        if (!_sqlDataSourceManager.getDefaultDataSourceId().equals(Config.getInstance().getValueAsString("front.cache.monitoring.datasource.jdbc.pool")))
486        {
487            try
488            {
489                Map<String, String> existingValues = Config.read();
490                existingValues.put("front.cache.monitoring.datasource.jdbc.pool", _sqlDataSourceManager.getDefaultDataSourceId());
491                ConfigManager.getInstance().save(existingValues, new File(AmetysHomeHelper.getAmetysHomeConfig(), RuntimeServlet.CONFIG_FILE_NAME).getCanonicalPath());
492            }
493            catch (Exception e)
494            {
495                getLogger().error("Could not reset config parameter 'front.cache.monitoring.datasource.jdbc.pool' to the default value internal-datasource");
496            }
497        }
498        
499        _removeDatasources();
500    }
501    
502    private void _removeBackOfficeUserPopulations()
503    {
504        for (UserPopulation userPopulation : _userPopulationDAO.getUserPopulations(false))
505        {
506            _userPopulationDAO.remove(userPopulation.getId(), true);
507        }
508    }
509    
510    private void _removeDatasources()
511    {
512        _sqlDataSourceManager.delete(_sqlDataSourceManager.getDataSourceDefinitions(true, false, false).keySet(), true);
513        _ldapDataSourceManager.delete(_ldapDataSourceManager.getDataSourceDefinitions(true, false, false).keySet(), true);
514    }
515}