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.web.cache;
017
018import java.io.ByteArrayInputStream;
019import java.io.InputStream;
020import java.nio.charset.StandardCharsets;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.LinkedHashSet;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029import java.util.stream.Collectors;
030
031import javax.xml.parsers.DocumentBuilder;
032import javax.xml.parsers.DocumentBuilderFactory;
033
034import org.apache.avalon.framework.service.ServiceException;
035import org.apache.avalon.framework.service.ServiceManager;
036import org.apache.avalon.framework.service.Serviceable;
037import org.apache.commons.collections.CollectionUtils;
038import org.apache.commons.io.IOUtils;
039import org.apache.commons.lang3.StringUtils;
040import org.apache.http.HttpResponse;
041import org.apache.http.NameValuePair;
042import org.apache.http.client.methods.HttpPost;
043import org.apache.http.message.BasicNameValuePair;
044import org.apache.xpath.XPathAPI;
045import org.apache.xpath.objects.XObject;
046import org.w3c.dom.Document;
047
048import org.ametys.core.ObservationConstants;
049import org.ametys.core.authentication.CredentialProvider;
050import org.ametys.core.authentication.CredentialProviderFactory;
051import org.ametys.core.authentication.CredentialProviderModel;
052import org.ametys.core.datasource.AbstractDataSourceManager.DataSourceDefinition;
053import org.ametys.core.datasource.LDAPDataSourceManager;
054import org.ametys.core.datasource.SQLDataSourceManager;
055import org.ametys.core.observation.Event;
056import org.ametys.core.observation.Observer;
057import org.ametys.core.ui.Callable;
058import org.ametys.core.user.directory.UserDirectory;
059import org.ametys.core.user.directory.UserDirectoryFactory;
060import org.ametys.core.user.directory.UserDirectoryModel;
061import org.ametys.core.user.population.PopulationContextHelper;
062import org.ametys.core.user.population.UserPopulation;
063import org.ametys.core.user.population.UserPopulationDAO;
064import org.ametys.core.util.JSONUtils;
065import org.ametys.runtime.i18n.I18nizableText;
066import org.ametys.runtime.parameter.Parameter;
067import org.ametys.runtime.parameter.ParameterHelper.ParameterType;
068import org.ametys.runtime.plugin.component.AbstractLogEnabled;
069import org.ametys.web.repository.site.SiteManager;
070
071/**
072 * This observer asks the front offices to synchronize users populations and datasources 
073 */
074public class SynchronizeUserPopulationsObserver extends AbstractLogEnabled implements Observer, Serviceable
075{
076    private ServiceManager _manager;
077    private UserDirectoryFactory _userDirectoryFactory;
078    private SiteManager _siteManager;
079    private PopulationContextHelper _populationContextHelper;
080    private UserPopulationDAO _userPopulationDAO;
081    private SQLDataSourceManager _sqlDatasourceManager;
082    private LDAPDataSourceManager _ldapDatasourceManager;
083    private JSONUtils _jsonUtils;
084    private CredentialProviderFactory _credentialProviderFactory;
085    
086    public void service(ServiceManager manager) throws ServiceException 
087    {
088        _manager = manager;
089    }
090    
091    private JSONUtils getJSONUtils()
092    {
093        if (_jsonUtils == null)
094        {
095            try
096            {
097                _jsonUtils = (JSONUtils) _manager.lookup(JSONUtils.ROLE);
098            }
099            catch (ServiceException e)
100            {
101                throw new RuntimeException(e);
102            }
103        }
104        return _jsonUtils;
105    }
106    
107    private LDAPDataSourceManager getLDAPDataSourceManager()
108    {
109        if (_ldapDatasourceManager == null)
110        {
111            try
112            {
113                _ldapDatasourceManager = (LDAPDataSourceManager) _manager.lookup(LDAPDataSourceManager.ROLE);
114            }
115            catch (ServiceException e)
116            {
117                throw new RuntimeException(e);
118            }
119        }
120        return _ldapDatasourceManager;
121    }
122    
123    private SQLDataSourceManager getSQLDataSourceManager()
124    {
125        if (_sqlDatasourceManager == null)
126        {
127            try
128            {
129                _sqlDatasourceManager = (SQLDataSourceManager) _manager.lookup(SQLDataSourceManager.ROLE);
130            }
131            catch (ServiceException e)
132            {
133                throw new RuntimeException(e);
134            }
135        }
136        return _sqlDatasourceManager;
137    }
138    
139    private SiteManager getSiteManager()
140    {
141        if (_siteManager == null)
142        {
143            try
144            {
145                _siteManager = (SiteManager) _manager.lookup(SiteManager.ROLE);
146            }
147            catch (ServiceException e)
148            {
149                throw new RuntimeException(e);
150            }
151        }
152        return _siteManager;
153    }
154    
155    private PopulationContextHelper getPopulationContextHelper()
156    {
157        if (_populationContextHelper == null)
158        {
159            try
160            {
161                _populationContextHelper = (PopulationContextHelper) _manager.lookup(PopulationContextHelper.ROLE);
162            }
163            catch (ServiceException e)
164            {
165                throw new RuntimeException(e);
166            }
167        }
168        return _populationContextHelper;
169    }    
170    
171    private UserPopulationDAO getUserPopulationDAO()
172    {
173        if (_userPopulationDAO == null)
174        {
175            try
176            {
177                _userPopulationDAO = (UserPopulationDAO) _manager.lookup(UserPopulationDAO.ROLE);
178            }
179            catch (ServiceException e)
180            {
181                throw new RuntimeException(e);
182            }
183        }
184        return _userPopulationDAO;
185    }
186    
187    private UserDirectoryFactory getUserDirectoryFactory()
188    {
189        if (_userDirectoryFactory == null)
190        {
191            try
192            {
193                _userDirectoryFactory = (UserDirectoryFactory) _manager.lookup(UserDirectoryFactory.ROLE);
194            }
195            catch (ServiceException e)
196            {
197                throw new RuntimeException(e);
198            }
199        }
200        return _userDirectoryFactory;
201    }
202    
203    private CredentialProviderFactory getCredentialProviderFactory()
204    {
205        if (_credentialProviderFactory == null)
206        {
207            try
208            {
209                _credentialProviderFactory = (CredentialProviderFactory) _manager.lookup(CredentialProviderFactory.ROLE);
210            }
211            catch (ServiceException e)
212            {
213                throw new RuntimeException(e);
214            }
215        }
216        return _credentialProviderFactory;
217    }
218    
219    @Override
220    public int getPriority(Event event)
221    {
222        return Observer.MAX_PRIORITY;
223    }
224    
225    @Override
226    public boolean supports(Event event)
227    {
228        String eventType = event.getId();
229        return eventType.equals(ObservationConstants.EVENT_DATASOURCE_UPDATED)
230                || eventType.equals(ObservationConstants.EVENT_DATASOURCE_DELETED)
231                || eventType.equals(ObservationConstants.EVENT_USERPOPULATION_UPDATED)
232                || eventType.equals(ObservationConstants.EVENT_USERPOPULATION_DELETED)
233                || eventType.equals(ObservationConstants.EVENT_USERPOPULATIONS_ASSIGNMENT);
234    }
235    
236    public void observe(Event event, Map<String, Object> transientVars) throws Exception
237    {
238        String eventType = event.getId();
239        if (eventType.equals(ObservationConstants.EVENT_DATASOURCE_UPDATED) 
240                || eventType.equals(ObservationConstants.EVENT_DATASOURCE_DELETED))
241        {
242            // If datasource modified implied in a used population...
243            Map<String, Object> args = event.getArguments();
244            @SuppressWarnings("unchecked")
245            List<String> datasourceIds = (List<String>) args.get(ObservationConstants.ARGS_DATASOURCE_IDS);
246    
247            Set<UserPopulation> usedPopulations = _getPopulationsUsedBySites();
248            Set<String> usedDatasources = _getDatasourcesUsedByPopulations(usedPopulations);
249            
250            if (!CollectionUtils.intersection(datasourceIds, usedDatasources).isEmpty())
251            {
252                CacheHelper.testWS("/_resetCache", getLogger());
253            }
254        }
255        else if (eventType.equals(ObservationConstants.EVENT_USERPOPULATION_UPDATED) 
256                    || eventType.equals(ObservationConstants.EVENT_USERPOPULATION_DELETED))
257        {
258            // If a used population population 
259            Map<String, Object> args = event.getArguments();
260            String populationId = (String) args.get(ObservationConstants.ARGS_USERPOPULATION_ID);
261            Set<String> usedPopulations = _getPopulationsIdsUsedBySites();
262            
263            if (usedPopulations.contains(populationId))
264            {
265                CacheHelper.testWS("/_resetCache", getLogger());
266            }
267        }
268        else if (eventType.equals(ObservationConstants.EVENT_USERPOPULATIONS_ASSIGNMENT))
269        {
270            // If site context assignation modified
271            Map<String, Object> args = event.getArguments();
272            String context = (String) args.get(ObservationConstants.ARGS_USERPOPULATION_CONTEXT);
273            if (context.startsWith("/sites/") || context.startsWith("/sites-fo/"))
274            {
275                CacheHelper.testWS("/_resetCache", getLogger()); // re-synchronize, because _sites.xml contains the association
276            }
277        }
278    }
279    
280    /**
281     * This method will call every front-office and will ask them to test the datasources implied by the chosen populations 
282     * @param populationIds The chosen populations. Cannot be null.
283     * @return The error messages
284     * @throws Exception If an error occurred while testing
285     */
286    @Callable
287    public Map<String, Map<String, Map<String, Object>>> testFrontOfficesDatasources(List<String> populationIds) throws Exception
288    {
289        Map<String, Map<String, Map<String, Object>>> issues = new HashMap<>();
290        
291        Set<UserPopulation> populations = populationIds.stream().map(getUserPopulationDAO()::getUserPopulation).collect(Collectors.toSet());
292        Set<String> datasourcesIds = _getDatasourcesUsedByPopulations(populations);
293        for (String datasourceId : datasourcesIds)
294        {
295            if (datasourceId.startsWith(SQLDataSourceManager.SQL_DATASOURCE_PREFIX))
296            {
297                if (StringUtils.equals(datasourceId, SQLDataSourceManager.AMETYS_INTERNAL_DATASOURCE_ID))
298                {
299                    _addEntry(issues, "*", datasourceId, SQLDataSourceManager.getInternalDataSourceDefinition().getName(), "Internal datasources cannot be used");
300                }
301                else
302                {
303                    DataSourceDefinition dataSourceDefinition = getSQLDataSourceManager().getDataSourceDefinition(datasourceId);
304                    Map<String, String> parameters = dataSourceDefinition.getParameters();
305                    
306                    List<NameValuePair> testParameters = _getSQLTestParameters(parameters);
307    
308                    List<Map<String, Object>> responses = CacheHelper.callWS("/_datasource-test", testParameters , getLogger());
309                    for (Map<String, Object> response : responses)
310                    {
311                        HttpPost request = (HttpPost) response.get("request");
312    
313                        byte[] byteArray = (byte[]) response.get("bodyResponse");
314                        if (byteArray != null)
315                        {
316                            try (InputStream inputstream = new ByteArrayInputStream(byteArray))
317                            {
318                                if (getLogger().isDebugEnabled())
319                                {
320                                    getLogger().debug("This is result from '" + request.getURI() + "'\n" + IOUtils.toString(inputstream));
321                                    inputstream.reset();
322                                }
323                                
324                                DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
325                                Document document = docBuilder.parse(inputstream);
326                                XObject eval = XPathAPI.eval(document, "/ActionResult/sql-connection-checker-datasource/text()");
327                                String errorMessage = eval.str();
328                                if (StringUtils.isNotBlank(errorMessage))
329                                {
330                                    _addEntry(issues, request.getURI().getHost(), datasourceId, dataSourceDefinition.getName(), errorMessage);
331                                }
332                            }
333                        }
334                        else
335                        {
336                            HttpResponse httpresponse = (HttpResponse) response.get("response");
337                            _addEntry(issues, request.getURI().getHost(), datasourceId, dataSourceDefinition.getName(), "Server error code " + (httpresponse != null ? httpresponse.getStatusLine() : "FATAL"));
338                        }
339                    }
340                }
341            }
342            else if (datasourceId.startsWith(LDAPDataSourceManager.LDAP_DATASOURCE_PREFIX))
343            {
344                DataSourceDefinition dataSourceDefinition = getLDAPDataSourceManager().getDataSourceDefinition(datasourceId);
345                Map<String, String> parameters = dataSourceDefinition.getParameters();
346                
347                List<NameValuePair> testParameters = _getLDAPTestParameters(parameters);
348
349                List<Map<String, Object>> responses = CacheHelper.callWS("/_datasource-test", testParameters , getLogger());
350                for (Map<String, Object> response : responses)
351                {
352                    HttpPost request = (HttpPost) response.get("request");
353
354                    byte[] byteArray = (byte[]) response.get("bodyResponse");
355                    if (byteArray != null)
356                    {
357                        try (InputStream inputstream = new ByteArrayInputStream(byteArray))
358                        {
359                            if (getLogger().isDebugEnabled())
360                            {
361                                getLogger().debug("This is result from '" + request.getURI() + "'\n" + IOUtils.toString(inputstream, StandardCharsets.UTF_8));
362                                inputstream.reset();
363                            }
364                            
365                            DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
366                            Document document = docBuilder.parse(inputstream);
367                            XObject eval = XPathAPI.eval(document, "/ActionResult/ldap-connection-checker-datasource/text()");
368                            String errorMessage = eval.str();
369                            if (StringUtils.isNotBlank(errorMessage))
370                            {
371                                _addEntry(issues, request.getURI().getHost(), datasourceId, dataSourceDefinition.getName(), errorMessage);
372                            }
373                        }
374                    }
375                    else
376                    {
377                        HttpResponse httpresponse = (HttpResponse) response.get("response");
378                        _addEntry(issues, request.getURI().getHost(), datasourceId, dataSourceDefinition.getName(), "Server error code " + (httpresponse != null ? httpresponse.getStatusLine() : "FATAL"));
379                    }
380                }
381            }
382        }
383        
384        return issues.isEmpty() ? null : issues;
385    }
386    
387    private void _addEntry(Map<String, Map<String, Map<String, Object>>> issues, String key, String subKey, I18nizableText label, String value)
388    {
389        if (!issues.containsKey(key))
390        {
391            issues.put(key, new HashMap<>());
392            issues.get(key).put(subKey, new HashMap<>());
393        }
394        else if (!issues.get(key).containsKey(subKey))
395        {
396            issues.get(key).put(subKey, new HashMap<>());
397        }
398        
399        issues.get(key).get(subKey).put("label", label);
400        issues.get(key).get(subKey).put("error", value);
401    }
402    
403    private List<NameValuePair> _getSQLTestParameters(Map<String, String> parameters)
404    {
405        List<String> paramNames = new ArrayList<>();
406        paramNames.add("id");
407        paramNames.add("dbtype");
408        paramNames.add("url");
409        paramNames.add("user");
410        paramNames.add("password");
411
412        List<String> values = paramNames.stream().map(parameters::get).collect(Collectors.toList());
413        
414        Map<String, Object> type = new HashMap<>();
415        type.put("testParamsNames", paramNames);
416        type.put("rawTestValues", values);
417
418        Map<String, Object> testArg = new HashMap<>();
419        testArg.put("sql-connection-checker-datasource", type);
420
421        List<NameValuePair> postParameters = new ArrayList<>();
422        postParameters.add(new BasicNameValuePair("fieldCheckersInfo", getJSONUtils().convertObjectToJson(testArg)));
423        return postParameters;
424    }
425    
426    private List<NameValuePair> _getLDAPTestParameters(Map<String, String> parameters)
427    {
428        List<String> paramNames = new ArrayList<>();
429        paramNames.add("id");
430        paramNames.add("baseURL");
431        paramNames.add("baseDN");
432        paramNames.add("useSSL");
433        paramNames.add("followReferrals");
434        paramNames.add("authenticationMethod");
435        paramNames.add("adminDN");
436        paramNames.add("adminPassword");
437
438        List<String> values = paramNames.stream().map(parameters::get).collect(Collectors.toList());
439        
440        Map<String, Object> type = new HashMap<>();
441        type.put("testParamsNames", paramNames);
442        type.put("rawTestValues", values);
443
444        Map<String, Object> testArg = new HashMap<>();
445        testArg.put("ldap-connection-checker-datasource", type);
446
447        List<NameValuePair> postParameters = new ArrayList<>();
448        postParameters.add(new BasicNameValuePair("fieldCheckersInfo", getJSONUtils().convertObjectToJson(testArg)));
449        return postParameters;
450    }
451    
452    private Set<String> _getPopulationsIdsUsedBySites()
453    {
454        // Retrieve the sites to build the contexts to search on
455        Collection<String> siteNames = getSiteManager().getSiteNames();
456        
457        // We return all the populations linked to at least one site
458        List<String> populations = new ArrayList<>();
459        for (String siteName : siteNames)
460        {
461            populations.addAll(getPopulationContextHelper().getUserPopulationsOnContext("/sites/" + siteName, false));
462            populations.addAll(getPopulationContextHelper().getUserPopulationsOnContext("/sites-fo/" + siteName, false));
463        }
464        
465        return new LinkedHashSet<>(populations);
466    }
467    
468    private Set<UserPopulation> _getPopulationsUsedBySites()
469    {
470        return _getPopulationsIdsUsedBySites().stream().map(getUserPopulationDAO()::getUserPopulation).collect(Collectors.toSet());
471    }
472    
473    private Set<String> _getDatasourcesUsedByPopulations(Set<UserPopulation> usedPopulations)
474    {
475        Set<String> datasourcesInUse = new HashSet<>();
476        
477        for (UserPopulation userPopulation : usedPopulations)
478        {
479            for (UserDirectory userDirectory : userPopulation.getUserDirectories())
480            {
481                String userDirectoryModelId = userDirectory.getUserDirectoryModelId();
482                UserDirectoryModel userDirectoryModel = getUserDirectoryFactory().getExtension(userDirectoryModelId);
483                
484                Map<String, Object> parameterValues = userDirectory.getParameterValues();
485                
486                Map<String, ? extends Parameter<ParameterType>> userDirectoryModelParameters = userDirectoryModel.getParameters();
487                for (String userDirectoryModelParameterId : userDirectoryModelParameters.keySet())
488                {
489                    Parameter<ParameterType> userDirectoryModelParameter = userDirectoryModelParameters.get(userDirectoryModelParameterId);
490                    if (ParameterType.DATASOURCE.equals(userDirectoryModelParameter.getType()))
491                    {
492                        String datasourceId = (String) parameterValues.get(userDirectoryModelParameterId);
493                        if (getSQLDataSourceManager().getDefaultDataSourceId().equals(datasourceId))
494                        {
495                            datasourcesInUse.add(getSQLDataSourceManager().getDefaultDataSourceDefinition().getId());
496                        }
497                        else if (getLDAPDataSourceManager().getDefaultDataSourceId().equals(datasourceId))
498                        {
499                            datasourcesInUse.add(getLDAPDataSourceManager().getDefaultDataSourceDefinition().getId());
500                        }
501                        else
502                        {
503                            datasourcesInUse.add(datasourceId);
504                        }
505                    }
506                }
507            }
508
509            for (CredentialProvider credentialProvider : userPopulation.getCredentialProviders())
510            {
511                String credentialProviderModelId = credentialProvider.getCredentialProviderModelId();
512                CredentialProviderModel credentialProviderModel = getCredentialProviderFactory().getExtension(credentialProviderModelId);
513                
514                Map<String, Object> parameterValues = credentialProvider.getParameterValues();
515                
516                Map<String, ? extends Parameter<ParameterType>> credentialProviderModelParameters = credentialProviderModel.getParameters();
517                for (String userDirectoryModelParameterId : credentialProviderModelParameters.keySet())
518                {
519                    Parameter<ParameterType> credentialProviderModelParameter = credentialProviderModelParameters.get(userDirectoryModelParameterId);
520                    if (ParameterType.DATASOURCE.equals(credentialProviderModelParameter.getType()))
521                    {
522                        String datasourceId = (String) parameterValues.get(userDirectoryModelParameterId);
523                        if (getSQLDataSourceManager().getDefaultDataSourceId().equals(datasourceId))
524                        {
525                            datasourcesInUse.add(getSQLDataSourceManager().getDefaultDataSourceDefinition().getId());
526                        }
527                        else if (getLDAPDataSourceManager().getDefaultDataSourceId().equals(datasourceId))
528                        {
529                            datasourcesInUse.add(getLDAPDataSourceManager().getDefaultDataSourceDefinition().getId());
530                        }
531                        else
532                        {
533                            datasourcesInUse.add(datasourceId);
534                        }
535                    }
536                }
537            }
538        }
539        
540        return datasourcesInUse;
541    }
542}