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