001/* 002 * Copyright 2017 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.plugins.site; 017 018import java.io.ByteArrayInputStream; 019import java.io.ByteArrayOutputStream; 020import java.io.File; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.Comparator; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.TreeMap; 028import java.util.concurrent.locks.Lock; 029import java.util.concurrent.locks.ReentrantLock; 030 031import org.apache.avalon.framework.component.Component; 032import org.apache.avalon.framework.configuration.Configuration; 033import org.apache.avalon.framework.configuration.ConfigurationException; 034import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 035import org.apache.avalon.framework.service.ServiceException; 036import org.apache.avalon.framework.service.ServiceManager; 037import org.apache.avalon.framework.service.Serviceable; 038import org.apache.commons.lang.StringUtils; 039import org.apache.http.client.methods.CloseableHttpResponse; 040import org.apache.http.client.methods.HttpGet; 041import org.apache.http.impl.client.CloseableHttpClient; 042 043import org.ametys.core.authentication.CredentialProviderFactory; 044import org.ametys.core.authentication.CredentialProviderModel; 045import org.ametys.core.datasource.AbstractDataSourceManager.DataSourceDefinition; 046import org.ametys.core.datasource.LDAPDataSourceManager; 047import org.ametys.core.datasource.SQLDataSourceManager; 048import org.ametys.core.user.directory.UserDirectoryFactory; 049import org.ametys.core.user.directory.UserDirectoryModel; 050import org.ametys.core.user.population.UserPopulation; 051import org.ametys.core.user.population.UserPopulationDAO; 052import org.ametys.runtime.config.Config; 053import org.ametys.runtime.config.ConfigManager; 054import org.ametys.runtime.i18n.I18nizableText; 055import org.ametys.runtime.parameter.Parameter; 056import org.ametys.runtime.parameter.ParameterHelper.ParameterType; 057import org.ametys.runtime.plugin.component.AbstractLogEnabled; 058import org.ametys.runtime.servlet.RuntimeServlet; 059import org.ametys.runtime.util.AmetysHomeHelper; 060import org.ametys.site.BackOfficeRequestHelper; 061 062/** 063 * A cache for site information provided by the Back-Office. 064 */ 065public class SiteInformationCache extends AbstractLogEnabled implements Serviceable, Component 066{ 067 /** Avalon Role */ 068 public static final String ROLE = SiteInformationCache.class.getName(); 069 070 /** Prefix for backoffice synchronized userpopulations */ 071 public static final String BACKOFFICE_PREFIX_IDENTIFIER = "bo-"; 072 073 private final Lock _syncLock = new ReentrantLock(); 074 075 private Map<SiteUrl, Site> _sites; 076 077 private LDAPDataSourceManager _ldapDataSourceManager; 078 private SQLDataSourceManager _sqlDataSourceManager; 079 private UserPopulationDAO _userPopulationDAO; 080 private UserDirectoryFactory _userDirectoryFactory; 081 private CredentialProviderFactory _credentialProviderFactory; 082 083 public void service(ServiceManager manager) throws ServiceException 084 { 085 _sqlDataSourceManager = (SQLDataSourceManager) manager.lookup(SQLDataSourceManager.ROLE); 086 _ldapDataSourceManager = (LDAPDataSourceManager) manager.lookup(LDAPDataSourceManager.ROLE); 087 _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE); 088 _userDirectoryFactory = (UserDirectoryFactory) manager.lookup(UserDirectoryFactory.ROLE); 089 _credentialProviderFactory = (CredentialProviderFactory) manager.lookup(CredentialProviderFactory.ROLE); 090 } 091 092 /** 093 * Clear cached informations. 094 */ 095 public void resetSitesCache() 096 { 097 _sites = null; 098 } 099 100 /** 101 * Returns the cached informations. 102 * @return the cached informations. 103 */ 104 public Map<SiteUrl, Site> getSites() 105 { 106 _synchronize(); 107 return _sites; 108 } 109 110 private void _synchronize() 111 { 112 if (_sites == null) 113 { 114 if (_syncLock.tryLock()) 115 { 116 try 117 { 118 _synchronizeSites(); 119 _synchronizePopulationsAndDatasources(); 120 } 121 catch (Exception e) 122 { 123 throw new RuntimeException("Unable to synchronize sites data", e); 124 } 125 finally 126 { 127 _syncLock.unlock(); 128 } 129 } 130 else 131 { 132 // some is already filling _sites for us... we just have to wait 133 _syncLock.lock(); 134 _syncLock.unlock(); 135 } 136 } 137 } 138 139 private void _synchronizeSites() throws ConfigurationException 140 { 141 Collection<Site> sites = new ArrayList<>(); 142 143 Configuration boConfiguration = _getBackofficeConfiguration("/_sites.xml"); 144 _configureSites (boConfiguration, sites); 145 146 TreeMap<SiteUrl, Site> sortedSites = new TreeMap<>(new SiteUrlComparator()); 147 148 for (Site site : sites) 149 { 150 for (SiteUrl url : site.getSiteUrls()) 151 { 152 sortedSites.put(url, site); 153 } 154 } 155 156 _sites = sortedSites; 157 } 158 159 private void _configureSites (Configuration conf, Collection<Site> sites) throws ConfigurationException 160 { 161 Configuration[] sitesConf = conf.getChildren("site"); 162 for (Configuration siteConf : sitesConf) 163 { 164 String name = siteConf.getAttribute("name"); 165 166 List<String> populationIds = new ArrayList<>(); 167 for (Configuration populationConf : siteConf.getChild("populations").getChildren()) 168 { 169 populationIds.add(populationConf.getValue()); 170 } 171 172 List<SiteUrl> urls = new ArrayList<>(); 173 for (Configuration urlConf : siteConf.getChildren("url")) 174 { 175 String serverName = urlConf.getAttribute("serverName"); 176 String serverPort = urlConf.getAttribute("serverPort"); 177 String serverPath = urlConf.getAttribute("serverPath"); 178 179 urls.add(new SiteUrl(serverName, serverPort, serverPath)); 180 } 181 182 List<String> languages = new ArrayList<>(); 183 for (Configuration langConf : siteConf.getChild("languages").getChildren()) 184 { 185 languages.add(langConf.getName()); 186 } 187 188 sites.add(new Site(name, urls, languages, populationIds)); 189 190 // Sub sites 191 _configureSites (siteConf, sites); 192 } 193 } 194 195 class SiteUrlComparator implements Comparator<SiteUrl> 196 { 197 @Override 198 public int compare(SiteUrl url1, SiteUrl url2) 199 { 200 int result = url2.getServerName().compareTo(url1.getServerName()); 201 202 if (result != 0) 203 { 204 return result; 205 } 206 207 result = url2.getServerPort().compareTo(url1.getServerPort()); 208 209 if (result != 0) 210 { 211 return result; 212 } 213 214 return url2.getServerPath().compareTo(url1.getServerPath()); 215 } 216 } 217 218 private Configuration _getBackofficeConfiguration(String url) 219 { 220 // Get site names and URLs from the CMS 221 String cmsURL = Config.getInstance().getValueAsString("org.ametys.site.bo"); 222 223 try (CloseableHttpClient httpClient = BackOfficeRequestHelper.getHttpClient()) 224 { 225 HttpGet httpGet = new HttpGet(cmsURL + url); 226 httpGet.addHeader("X-Ametys-FO", "true"); 227 228 try (CloseableHttpResponse response = httpClient.execute(httpGet); ByteArrayOutputStream os = new ByteArrayOutputStream()) 229 { 230 switch (response.getStatusLine().getStatusCode()) 231 { 232 case 200: 233 break; 234 235 case 403: 236 throw new IllegalStateException("The CMS back-office refused the connection"); 237 238 case 500: 239 default: 240 throw new IllegalStateException("The CMS back-office returned an error"); 241 } 242 243 response.getEntity().writeTo(os); 244 try (ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray())) 245 { 246 Configuration conf = new DefaultConfigurationBuilder().build(is); 247 return conf; 248 } 249 } 250 } 251 catch (Exception e) 252 { 253 throw new RuntimeException("Unable to synchronize site data", e); 254 } 255 } 256 257 /** 258 * Synchronize the local user populations and datasources with backoffice 259 */ 260 private void _synchronizePopulationsAndDatasources() 261 { 262 Configuration boConfiguration = _getBackofficeConfiguration("/_sites-populations.xml"); 263 264 _removeBackOfficeSynchronizedElements(); 265 266 _addBackOfficeSynchronizedElements(boConfiguration); 267 } 268 269 private void _synchronizeMonitoringAndCaptcha(Configuration boConfiguration, Map<String, String> mapping) 270 { 271 // MONITORING 272 Configuration monitoringConfiguration = boConfiguration.getChild("Monitoring"); 273 boolean enabled = monitoringConfiguration.getAttributeAsBoolean("enabled", false); 274 boolean wasEnabled = Config.getInstance().getValueAsBoolean("front.cache.monitoring.schedulers.enable"); 275 String datasourceId = monitoringConfiguration.getChild("Datasource").getValue(""); 276 277 Map<String, String> valuesToChange = new HashMap<>(); 278 if (enabled != wasEnabled) 279 { 280 valuesToChange.put("front.cache.monitoring.schedulers.enable", enabled ? "true" : "false"); 281 } 282 if (enabled && !StringUtils.equals(datasourceId, Config.getInstance().getValueAsString("front.cache.monitoring.datasource.jdbc.pool"))) 283 { 284 valuesToChange.put("front.cache.monitoring.datasource.jdbc.pool", mapping.get(datasourceId)); 285 } 286 287 // CAPTCHA 288 Configuration captchaConfiguration = boConfiguration.getChild("Captcha"); 289 String captchaType = captchaConfiguration.getAttribute("type", null); 290 String captchaPublicKey = captchaConfiguration.getAttribute("publicKey", null); 291 String captchaSecretKey = captchaConfiguration.getAttribute("secretKey", null); 292 293 if (!StringUtils.equals(captchaType, Config.getInstance().getValueAsString("runtime.captcha.type")) 294 || !StringUtils.equals(captchaPublicKey, Config.getInstance().getValueAsString("runtime.captcha.recaptcha.publickey")) 295 || !!StringUtils.equals(captchaSecretKey, Config.getInstance().getValueAsString("runtime.captcha.recaptcha.secretkey"))) 296 { 297 valuesToChange.put("runtime.captcha.type", captchaType); 298 valuesToChange.put("runtime.captcha.recaptcha.publickey", captchaPublicKey); 299 valuesToChange.put("runtime.captcha.recaptcha.secretkey", captchaSecretKey); 300 } 301 302 // UPDATE CONFIG 303 if (!valuesToChange.isEmpty()) 304 { 305 try 306 { 307 Map<String, String> existingValues = Config.read(); 308 existingValues.putAll(valuesToChange); 309 ConfigManager.getInstance().save(existingValues, new File(AmetysHomeHelper.getAmetysHomeConfig(), RuntimeServlet.CONFIG_FILE_NAME).getCanonicalPath()); 310 // Need a restart to be taken in account 311 } 312 catch (Exception e) 313 { 314 getLogger().error("The monitoring/captcha synchronization failed", e); 315 } 316 } 317 } 318 319 private void _addBackOfficeSynchronizedElements(Configuration configuration) 320 { 321 Map<String, String> mapping = _addBackofficeDatasources(configuration); 322 _addBackOfficeUserPopulations(configuration, mapping); 323 _synchronizeMonitoringAndCaptcha(configuration, mapping); 324 } 325 326 private Map<String, String> _addBackofficeDatasources(Configuration configuration) 327 { 328 Map<String, String> mapping = new HashMap<>(); 329 mapping.putAll(_addBackofficeSQLDatasources(configuration)); 330 mapping.putAll(_addBackofficeLDAPDatasources(configuration)); 331 return mapping; 332 } 333 334 private Map<String, String> _addBackofficeSQLDatasources(Configuration configuration) 335 { 336 Map<String, String> mapping = new HashMap<>(); 337 338 for (Configuration datasourceConfiguration : configuration.getChild("SQLDatasources").getChild("datasources").getChildren("datasource")) 339 { 340 String id = datasourceConfiguration.getAttribute("id", ""); 341 342 String defaultStatus = datasourceConfiguration.getAttribute("default", "false"); 343 String privateStatus = datasourceConfiguration.getAttribute("private", "false"); 344 String name = "DO NOT SELECT OR USE Back-office Synchronized Datasource (" + datasourceConfiguration.getChild("name").getValue("") + ")"; 345 String description = datasourceConfiguration.getChild("description").getValue(""); 346 347 Map<String, Object> parameters = new HashMap<>(); 348 for (Configuration parameterConfiguration : datasourceConfiguration.getChild("parameters").getChildren()) 349 { 350 // Assuming all parameters are String (that is what .add is doing) 351 parameters.put(parameterConfiguration.getName(), parameterConfiguration.getValue("")); 352 } 353 354 DataSourceDefinition newDatasource = _sqlDataSourceManager.add(new I18nizableText(name), new I18nizableText(description), parameters, "true".equals(privateStatus)); 355 mapping.put(id, newDatasource.getId()); 356 if ("true".equals(defaultStatus)) 357 { 358 mapping.put(_sqlDataSourceManager.getDefaultDataSourceId(), newDatasource.getId()); 359 } 360 } 361 362 return mapping; 363 } 364 365 private Map<String, String> _addBackofficeLDAPDatasources(Configuration configuration) 366 { 367 Map<String, String> mapping = new HashMap<>(); 368 369 for (Configuration datasourceConfiguration : configuration.getChild("LDAPDatasources").getChild("datasources").getChildren("datasource")) 370 { 371 String id = datasourceConfiguration.getAttribute("id", ""); 372 373 String defaultStatus = datasourceConfiguration.getAttribute("default", "false"); 374 String privateStatus = datasourceConfiguration.getAttribute("private", "false"); 375 String name = "DO NOT SELECT OR USE Back-office Synchronized Datasource (" + datasourceConfiguration.getChild("name").getValue("") + ")"; 376 String description = datasourceConfiguration.getChild("description").getValue(""); 377 378 Map<String, Object> parameters = new HashMap<>(); 379 for (Configuration parameterConfiguration : datasourceConfiguration.getChild("parameters").getChildren()) 380 { 381 // Assuming all parameters are String (that is what .add is doing) 382 parameters.put(parameterConfiguration.getName(), parameterConfiguration.getValue("")); 383 } 384 385 DataSourceDefinition newDatasource = _ldapDataSourceManager.add(new I18nizableText(name), new I18nizableText(description), parameters, "true".equals(privateStatus)); 386 mapping.put(id, newDatasource.getId()); 387 if ("true".equals(defaultStatus)) 388 { 389 mapping.put(_ldapDataSourceManager.getDefaultDataSourceId(), newDatasource.getId()); 390 } 391 } 392 393 return mapping; 394 } 395 396 private void _addBackOfficeUserPopulations(Configuration configuration, Map<String, String> mapping) 397 { 398 for (Configuration userPopulationConfiguration : configuration.getChild("UserPopulations").getChild("userPopulations").getChildren("userPopulation")) 399 { 400 String id = userPopulationConfiguration.getAttribute("id", ""); 401 String enabled = userPopulationConfiguration.getAttribute("enabled", "true"); 402 if ("true".equals(enabled)) 403 { 404 String label = userPopulationConfiguration.getChild("label").getValue(""); 405 406 // USER DIRECTORIES 407 List<Map<String, String>> userDirectories = new ArrayList<>(); 408 for (Configuration userDirectoryConfiguration : userPopulationConfiguration.getChild("userDirectories").getChildren("userDirectory")) 409 { 410 Map<String, String> userDirectory = new HashMap<>(); 411 userDirectories.add(userDirectory); 412 413 String udid = userDirectoryConfiguration.getAttribute("id", ""); 414 userDirectory.put("id", udid); 415 416 String modelId = userDirectoryConfiguration.getAttribute("modelId", ""); 417 userDirectory.put("udModelId", modelId); 418 419 String additionalLabel = userDirectoryConfiguration.getAttribute("label", ""); 420 userDirectory.put("label", additionalLabel); 421 422 UserDirectoryModel userDirectoryModel = _userDirectoryFactory.getExtension(modelId); 423 424 Map<String, ? extends Parameter<ParameterType>> parameters = userDirectoryModel.getParameters(); 425 for (String parameterId : parameters.keySet()) 426 { 427 Parameter<ParameterType> parameter = parameters.get(parameterId); 428 429 String value = userDirectoryConfiguration.getChild(parameterId).getValue(""); 430 431 if (parameter.getType() == ParameterType.DATASOURCE) 432 { 433 value = mapping.get(value); 434 } 435 436 userDirectory.put(modelId + "$" + parameterId, value); 437 } 438 } 439 440 // CREDENTIAL PROVIDERS 441 List<Map<String, String>> credentialProviders = new ArrayList<>(); 442 for (Configuration credentialProviderConfiguration : userPopulationConfiguration.getChild("credentialProviders").getChildren("credentialProvider")) 443 { 444 Map<String, String> credentialProvider = new HashMap<>(); 445 credentialProviders.add(credentialProvider); 446 447 String cpid = credentialProviderConfiguration.getAttribute("id", ""); 448 credentialProvider.put("id", cpid); 449 450 String modelId = credentialProviderConfiguration.getAttribute("modelId", ""); 451 credentialProvider.put("cpModelId", modelId); 452 453 String additionalLabel = credentialProviderConfiguration.getAttribute("label", ""); 454 credentialProvider.put("label", additionalLabel); 455 456 CredentialProviderModel credentialProviderModel = _credentialProviderFactory.getExtension(modelId); 457 458 Map<String, ? extends Parameter<ParameterType>> parameters = credentialProviderModel.getParameters(); 459 for (String parameterId : parameters.keySet()) 460 { 461 Parameter<ParameterType> parameter = parameters.get(parameterId); 462 463 String value = credentialProviderConfiguration.getChild(parameterId).getValue(""); 464 465 if (parameter.getType() == ParameterType.DATASOURCE) 466 { 467 value = mapping.get(value); 468 } 469 470 credentialProvider.put(modelId + "$" + parameterId, value); 471 } 472 } 473 474 _userPopulationDAO.add(id, label, userDirectories, credentialProviders); 475 } 476 } 477 } 478 479 480 481 private void _removeBackOfficeSynchronizedElements() 482 { 483 _removeBackOfficeUserPopulations(); 484 485 if (!_sqlDataSourceManager.getDefaultDataSourceId().equals(Config.getInstance().getValueAsString("front.cache.monitoring.datasource.jdbc.pool"))) 486 { 487 try 488 { 489 Map<String, String> existingValues = Config.read(); 490 existingValues.put("front.cache.monitoring.datasource.jdbc.pool", _sqlDataSourceManager.getDefaultDataSourceId()); 491 ConfigManager.getInstance().save(existingValues, new File(AmetysHomeHelper.getAmetysHomeConfig(), RuntimeServlet.CONFIG_FILE_NAME).getCanonicalPath()); 492 } 493 catch (Exception e) 494 { 495 getLogger().error("Could not reset config parameter 'front.cache.monitoring.datasource.jdbc.pool' to the default value internal-datasource"); 496 } 497 } 498 499 _removeDatasources(); 500 } 501 502 private void _removeBackOfficeUserPopulations() 503 { 504 for (UserPopulation userPopulation : _userPopulationDAO.getUserPopulations(false)) 505 { 506 _userPopulationDAO.remove(userPopulation.getId(), true); 507 } 508 } 509 510 private void _removeDatasources() 511 { 512 _sqlDataSourceManager.delete(_sqlDataSourceManager.getDataSourceDefinitions(true, false, false).keySet(), true); 513 _ldapDataSourceManager.delete(_ldapDataSourceManager.getDataSourceDefinitions(true, false, false).keySet(), true); 514 } 515}