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