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