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}