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.parameter.Parameter; 070import org.ametys.runtime.parameter.ParameterHelper.ParameterType; 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, String> 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, String> 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, String> 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<String> 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, String> 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<String> 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 Parameter<ParameterType>> userDirectoryModelParameters = userDirectoryModel.getParameters(); 500 for (String userDirectoryModelParameterId : userDirectoryModelParameters.keySet()) 501 { 502 Parameter<ParameterType> userDirectoryModelParameter = userDirectoryModelParameters.get(userDirectoryModelParameterId); 503 if (ParameterType.DATASOURCE.equals(userDirectoryModelParameter.getType())) 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 Parameter<ParameterType>> credentialProviderModelParameters = credentialProviderModel.getParameters(); 530 for (String userDirectoryModelParameterId : credentialProviderModelParameters.keySet()) 531 { 532 Parameter<ParameterType> credentialProviderModelParameter = credentialProviderModelParameters.get(userDirectoryModelParameterId); 533 if (ParameterType.DATASOURCE.equals(credentialProviderModelParameter.getType())) 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}