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