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.io.FileOutputStream; 022import java.io.IOException; 023import java.io.OutputStream; 024import java.util.ArrayList; 025import java.util.Base64; 026import java.util.Collection; 027import java.util.Comparator; 028import java.util.HashMap; 029import java.util.List; 030import java.util.Map; 031import java.util.Properties; 032import java.util.TreeMap; 033import java.util.concurrent.locks.Lock; 034import java.util.concurrent.locks.ReentrantLock; 035 036import javax.xml.transform.OutputKeys; 037import javax.xml.transform.TransformerFactory; 038import javax.xml.transform.sax.SAXTransformerFactory; 039import javax.xml.transform.sax.TransformerHandler; 040import javax.xml.transform.stream.StreamResult; 041 042import org.apache.avalon.framework.component.Component; 043import org.apache.avalon.framework.configuration.Configuration; 044import org.apache.avalon.framework.configuration.ConfigurationException; 045import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 046import org.apache.avalon.framework.configuration.DefaultConfigurationSerializer; 047import org.apache.avalon.framework.service.ServiceException; 048import org.apache.avalon.framework.service.ServiceManager; 049import org.apache.avalon.framework.service.Serviceable; 050import org.apache.commons.io.IOUtils; 051import org.apache.commons.lang3.StringUtils; 052import org.apache.http.client.methods.CloseableHttpResponse; 053import org.apache.http.client.methods.HttpGet; 054import org.apache.http.impl.client.CloseableHttpClient; 055import org.apache.xml.serializer.OutputPropertiesFactory; 056 057import org.ametys.core.captcha.Captcha; 058import org.ametys.core.captcha.CaptchaExtensionPoint; 059import org.ametys.core.captcha.CaptchaHelper; 060import org.ametys.core.datasource.LDAPDataSourceManager; 061import org.ametys.core.datasource.SQLDataSourceManager; 062import org.ametys.core.user.population.UserPopulationDAO; 063import org.ametys.runtime.config.Config; 064import org.ametys.runtime.config.ConfigManager; 065import org.ametys.runtime.exception.ServiceUnavailableException; 066import org.ametys.runtime.model.type.ElementType; 067import org.ametys.runtime.plugin.component.AbstractLogEnabled; 068import org.ametys.runtime.servlet.RuntimeServlet; 069import org.ametys.runtime.util.AmetysHomeHelper; 070import org.ametys.site.BackOfficeRequestHelper; 071 072/** 073 * A cache for site information provided by the Back-Office. 074 */ 075public class SiteInformationCache extends AbstractLogEnabled implements Serviceable, Component 076{ 077 /** Avalon Role */ 078 public static final String ROLE = SiteInformationCache.class.getName(); 079 080 /** Prefix for backoffice synchronized userpopulations */ 081 public static final String BACKOFFICE_PREFIX_IDENTIFIER = "bo-"; 082 083 private final Lock _syncLock = new ReentrantLock(); 084 085 private Map<SiteUrl, Site> _sites; 086 087 private LDAPDataSourceManager _ldapDataSourceManager; 088 private SQLDataSourceManager _sqlDataSourceManager; 089 private UserPopulationDAO _userPopulationDAO; 090 private CaptchaExtensionPoint _captchaExtensionPoint; 091 092 public void service(ServiceManager manager) throws ServiceException 093 { 094 _sqlDataSourceManager = (SQLDataSourceManager) manager.lookup(SQLDataSourceManager.ROLE); 095 _ldapDataSourceManager = (LDAPDataSourceManager) manager.lookup(LDAPDataSourceManager.ROLE); 096 _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE); 097 _captchaExtensionPoint = (CaptchaExtensionPoint) manager.lookup(CaptchaExtensionPoint.ROLE); 098 } 099 100 /** 101 * Clear cached informations. 102 */ 103 public void resetSitesCache() 104 { 105 _sites = null; 106 } 107 108 /** 109 * Returns the cached informations. 110 * @return the cached informations. 111 */ 112 public Map<SiteUrl, Site> getSites() 113 { 114 _synchronize(); 115 return _sites; 116 } 117 118 private void _synchronize() 119 { 120 if (_sites == null) 121 { 122 if (_syncLock.tryLock()) 123 { 124 try 125 { 126 _synchronizeSites(); 127 _synchronizePopulationsAndDatasources(); 128 } 129 catch (Exception e) 130 { 131 throw new RuntimeException("Unable to synchronize sites data", e); 132 } 133 finally 134 { 135 _syncLock.unlock(); 136 } 137 } 138 else 139 { 140 // some is already filling _sites for us... we just have to wait 141 _syncLock.lock(); 142 _syncLock.unlock(); 143 } 144 } 145 } 146 147 private void _synchronizeSites() throws ConfigurationException 148 { 149 Collection<Site> sites = new ArrayList<>(); 150 151 Configuration boConfiguration = _getBackofficeConfiguration("/_sites.xml"); 152 _configureSites (boConfiguration, sites); 153 154 TreeMap<SiteUrl, Site> sortedSites = new TreeMap<>(new SiteUrlComparator()); 155 156 for (Site site : sites) 157 { 158 for (SiteUrl url : site.getSiteUrls()) 159 { 160 sortedSites.put(url, site); 161 } 162 } 163 164 _sites = sortedSites; 165 } 166 167 private void _configureSites (Configuration conf, Collection<Site> sites) throws ConfigurationException 168 { 169 Configuration[] sitesConf = conf.getChildren("site"); 170 for (Configuration siteConf : sitesConf) 171 { 172 String name = siteConf.getAttribute("name"); 173 String title = siteConf.getAttribute("title"); 174 175 List<String> populationIds = new ArrayList<>(); 176 for (Configuration populationConf : siteConf.getChild("populations").getChildren()) 177 { 178 populationIds.add(populationConf.getValue()); 179 } 180 181 List<SiteUrl> urls = new ArrayList<>(); 182 for (Configuration urlConf : siteConf.getChildren("url")) 183 { 184 String serverName = urlConf.getAttribute("serverName"); 185 String serverPort = urlConf.getAttribute("serverPort"); 186 String serverPath = urlConf.getAttribute("serverPath"); 187 188 urls.add(new SiteUrl(serverName, serverPort, serverPath)); 189 } 190 191 List<String> languages = new ArrayList<>(); 192 for (Configuration langConf : siteConf.getChild("languages").getChildren()) 193 { 194 languages.add(langConf.getName()); 195 } 196 197 // Get sign-up pages 198 Map<String, List<SignupPage>> signupPages = _configureSignupPages(siteConf, name); 199 200 // Get weak password url 201 Map<String, String> weakPasswordUrls = _configureWeakPasswordUrls(siteConf); 202 203 // Add the site to the sites 204 sites.add(new Site(name, title, urls, languages, populationIds, signupPages, weakPasswordUrls)); 205 206 // Sub sites 207 _configureSites (siteConf, sites); 208 } 209 } 210 211 private Map<String , List<SignupPage>> _configureSignupPages(Configuration siteConf, String siteName) throws ConfigurationException 212 { 213 Map<String , List<SignupPage>> signupPagesByLang = new HashMap<>(); 214 Configuration signupPagesConf = siteConf.getChild("signupPages"); 215 216 boolean publicSignupAllowed = signupPagesConf.getAttributeAsBoolean("publicSignupAllowed", false); 217 if (publicSignupAllowed) 218 { 219 // For every child available for "signupPages" 220 for (Configuration langConf : signupPagesConf.getChildren()) 221 { 222 String lang = langConf.getName(); 223 224 List<SignupPage> pages = new ArrayList<>(); 225 226 for (Configuration pageConf : langConf.getChildren("page")) 227 { 228 String url = pageConf.getChild("url").getValue(); 229 230 Map<String, String> popAndUserDirIds = new HashMap<>(); 231 for (Configuration userDirectoryConf : pageConf.getChildren("userDirectory")) 232 { 233 popAndUserDirIds.put(userDirectoryConf.getAttribute("populationId"), userDirectoryConf.getAttribute("id")); 234 } 235 236 pages.add(new SignupPage(url, siteName, lang, popAndUserDirIds)); 237 } 238 239 signupPagesByLang.put(lang, pages); 240 } 241 } 242 243 return signupPagesByLang; 244 } 245 246 private Map<String, String> _configureWeakPasswordUrls(Configuration siteConf) throws ConfigurationException 247 { 248 Configuration weakPasswordUrlConf = siteConf.getChild("weakPasswordUrls"); 249 250 Map<String, String> weakPasswordUrls = new HashMap<>(); 251 252 for (Configuration langConf : weakPasswordUrlConf.getChildren()) 253 { 254 weakPasswordUrls.put(langConf.getName(), langConf.getValue()); 255 } 256 257 return weakPasswordUrls; 258 259 } 260 261 class SiteUrlComparator implements Comparator<SiteUrl> 262 { 263 @Override 264 public int compare(SiteUrl url1, SiteUrl url2) 265 { 266 int result = url2.getServerName().compareTo(url1.getServerName()); 267 268 if (result != 0) 269 { 270 return result; 271 } 272 273 result = url2.getServerPort().compareTo(url1.getServerPort()); 274 275 if (result != 0) 276 { 277 return result; 278 } 279 280 return url2.getServerPath().compareTo(url1.getServerPath()); 281 } 282 } 283 284 private Configuration _getBackofficeConfiguration(String url) 285 { 286 // Get site names and URLs from the CMS 287 String cmsURL = Config.getInstance().getValue("org.ametys.site.bo"); 288 289 try (CloseableHttpClient httpClient = BackOfficeRequestHelper.getHttpClient()) 290 { 291 HttpGet httpGet = new HttpGet(cmsURL + url); 292 httpGet.addHeader("X-Ametys-FO", "true"); 293 294 try (CloseableHttpResponse response = httpClient.execute(httpGet); ByteArrayOutputStream os = new ByteArrayOutputStream()) 295 { 296 switch (response.getStatusLine().getStatusCode()) 297 { 298 case 200: 299 break; 300 301 case 403: 302 throw new IllegalStateException("The CMS back-office refused the connection"); 303 304 case 503: 305 BackOfficeRequestHelper.switchOnMaintenanceIfNeeded(response); 306 throw new ServiceUnavailableException(); 307 308 case 500: 309 default: 310 throw new IllegalStateException("The CMS back-office returned an error"); 311 } 312 313 response.getEntity().writeTo(os); 314 try (ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray())) 315 { 316 Configuration conf = new DefaultConfigurationBuilder().build(is); 317 return conf; 318 } 319 } 320 } 321 catch (Exception e) 322 { 323 throw new RuntimeException("Unable to synchronize site data", e); 324 } 325 } 326 327 /** 328 * Synchronize the local user populations and datasources with backoffice 329 * @throws Exception If an unexpected error occurred 330 */ 331 private void _synchronizePopulationsAndDatasources() throws Exception 332 { 333 Configuration configuration = _getBackofficeConfiguration("/_sites-populations.xml"); 334 335 // Peppers 336 _synchronizePeppers(configuration); 337 338 // Multifactor authentication files 339 _synchronizeMultifactorAuthenticationFiles(configuration); 340 341 // First, we need to remove the files because they will be read before we reinitialize them by the DataSourceConsumerEP 342 _userPopulationDAO.getConfigurationFile().delete(); 343 _sqlDataSourceManager.getFileConfiguration().delete(); 344 _ldapDataSourceManager.getFileConfiguration().delete(); 345 346 // Then we can destroy what is in memory 347 _userPopulationDAO.dispose(); 348 _sqlDataSourceManager.dispose(true); 349 _ldapDataSourceManager.dispose(); 350 351 // And finally we can transfer data 352 353 // SQL datasources 354 Configuration sqlDatasources = configuration.getChild("SQLDatasources").getChild("datasources"); 355 356 _serialize(sqlDatasources, _sqlDataSourceManager.getFileConfiguration()); 357 _sqlDataSourceManager.initialize(true); 358 359 // LDAP Datasources 360 _serialize(configuration.getChild("LDAPDatasources").getChild("datasources"), _ldapDataSourceManager.getFileConfiguration()); 361 _ldapDataSourceManager.initialize(); 362 363 // User populations 364 _serialize(configuration.getChild("UserPopulations").getChild("userPopulations"), _userPopulationDAO.getConfigurationFile()); 365 _userPopulationDAO.initialize(); 366 367 // Config values 368 _synchronizeConfigValues(configuration); 369 } 370 371 private void _synchronizePeppers(Configuration configuration) throws ConfigurationException, IOException 372 { 373 Configuration peppersConfiguration = configuration.getChild("Peppers"); 374 375 for (Configuration pepperConfiguration : peppersConfiguration.getChildren()) 376 { 377 String name = pepperConfiguration.getName(); 378 String value = pepperConfiguration.getValue(); 379 380 File pepperFile = new File(AmetysHomeHelper.getAmetysHomeData() + "/auth", name); 381 pepperFile.createNewFile(); 382 383 byte[] pepperBytes = Base64.getDecoder().decode(value); 384 try (FileOutputStream fileOutputStream = new FileOutputStream(pepperFile)) 385 { 386 IOUtils.write(pepperBytes, fileOutputStream); 387 } 388 } 389 390 } 391 392 private void _synchronizeMultifactorAuthenticationFiles(Configuration configuration) throws ConfigurationException, IOException 393 { 394 Configuration mfaConfigurations = configuration.getChild("MultifactorAuthentication"); 395 396 for (Configuration mfaConfiguration : mfaConfigurations.getChildren()) 397 { 398 String name = mfaConfiguration.getName(); 399 String value = mfaConfiguration.getValue(); 400 401 if (name.startsWith("mfa_")) 402 { 403 File mfaFile = new File(AmetysHomeHelper.getAmetysHomeConfig() + "/mfa", name); 404 mfaFile.createNewFile(); 405 406 byte[] mfaBytes = Base64.getDecoder().decode(value); 407 try (FileOutputStream fileOutputStream = new FileOutputStream(mfaFile)) 408 { 409 IOUtils.write(mfaBytes, fileOutputStream); 410 } 411 } 412 } 413 } 414 415 private void _serialize(Configuration configuration, File file) throws Exception 416 { 417 if (!file.exists()) 418 { 419 file.getParentFile().mkdirs(); 420 file.createNewFile(); 421 } 422 423 // create a transformer for saving sax into a file 424 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 425 426 // create the result where to write 427 try (OutputStream os = new FileOutputStream(file)) 428 { 429 StreamResult sResult = new StreamResult(os); 430 th.setResult(sResult); 431 432 // create the format of result 433 Properties format = new Properties(); 434 format.put(OutputKeys.METHOD, "xml"); 435 format.put(OutputKeys.INDENT, "yes"); 436 format.put(OutputKeys.ENCODING, "UTF-8"); 437 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4"); 438 th.getTransformer().setOutputProperties(format); 439 440 new DefaultConfigurationSerializer().serialize(th, configuration); 441 } 442 } 443 444 private void _synchronizeConfigValues(Configuration boConfiguration) 445 { 446 Map<String, Object> valuesToChange = new HashMap<>(); 447 448 _synchronizeMonitory(boConfiguration, valuesToChange); 449 _synchronizeCaptcha(boConfiguration, valuesToChange); 450 _synchronizeUpload(boConfiguration, valuesToChange); 451 _synchronizeMultifactorAuthenticationDatasource(boConfiguration, valuesToChange); 452 453 // UPDATE CONFIG 454 if (!valuesToChange.isEmpty()) 455 { 456 try 457 { 458 Map<String, Object> existingValues = Config.getInstance().getValues(); 459 existingValues.putAll(valuesToChange); 460 ConfigManager.getInstance().save(existingValues, new File(AmetysHomeHelper.getAmetysHomeConfig(), RuntimeServlet.CONFIG_FILE_NAME).getCanonicalPath()); 461 // Need a restart to be taken in account 462 } 463 catch (Exception e) 464 { 465 getLogger().error("The monitoring/captcha synchronization failed", e); 466 } 467 } 468 } 469 470 private void _synchronizeUpload(Configuration boConfiguration, Map<String, Object> valuesToChange) 471 { 472 Configuration uploadConfiguration = boConfiguration.getChild("UploadMaxSize"); 473 Long uploadMaxSize = uploadConfiguration.getValueAsLong(-1); 474 475 if (uploadMaxSize != Config.getInstance().getValue("runtime.upload.max-size")) 476 { 477 valuesToChange.put("runtime.upload.max-size", uploadMaxSize); 478 } 479 } 480 481 private void _synchronizeCaptcha(Configuration boConfiguration, Map<String, Object> valuesToChange) 482 { 483 Configuration captchaConfiguration = boConfiguration.getChild("Captcha"); 484 String captchaType = captchaConfiguration.getAttribute("type", null); 485 486 if (!StringUtils.equals(captchaType, Config.getInstance().getValue("runtime.captcha.type"))) 487 { 488 // If the captcha has changed, reinitialize the captcha from CaptchaHelper 489 CaptchaHelper.staticDispose(); 490 valuesToChange.put("runtime.captcha.type", captchaType); 491 } 492 493 Captcha newCaptcha = _captchaExtensionPoint.getExtension(captchaType); 494 if (newCaptcha == null) 495 { 496 throw new IllegalArgumentException("The captcha extension '" + captchaType + "' is unknown on the site. Missing plugin?"); 497 } 498 499 Map<String, Object> parameterValues = new HashMap<>(); 500 501 boolean valueChanged = false; 502 for (String parameter : newCaptcha.getConfigParameters()) 503 { 504 ElementType elementType = (ElementType) ConfigManager.getInstance().getModelItem(parameter).getType(); 505 Object newValue = elementType.castValue(captchaConfiguration.getAttribute(parameter, null)); 506 // Store the captcha parameter 507 parameterValues.put(parameter, newValue); 508 // Check if a parameter value has changed 509 if (!valueChanged) 510 { 511 Object oldValue = elementType.castValue(Config.getInstance().getValue(parameter)); 512 // We have a new value different from the previous one or no more value when one was present 513 valueChanged = newValue != null && !newValue.equals(oldValue) || newValue == null && oldValue != null; 514 } 515 } 516 517 // If any parameters has changed, we update them all 518 if (valueChanged) 519 { 520 valuesToChange.putAll(parameterValues); 521 } 522 } 523 524 private void _synchronizeMonitory(Configuration boConfiguration, Map<String, Object> valuesToChange) 525 { 526 Configuration monitoringConfiguration = boConfiguration.getChild("Monitoring"); 527 boolean enabled = monitoringConfiguration.getAttributeAsBoolean("enabled", false); 528 boolean wasEnabled = Config.getInstance().getValue("front.cache.monitoring.schedulers.enable"); 529 String datasourceId = monitoringConfiguration.getChild("Datasource").getValue(""); 530 531 if (enabled != wasEnabled) 532 { 533 valuesToChange.put("front.cache.monitoring.schedulers.enable", enabled); 534 } 535 if (enabled && !StringUtils.equals(datasourceId, Config.getInstance().getValue("front.cache.monitoring.datasource.jdbc.pool"))) 536 { 537 valuesToChange.put("front.cache.monitoring.datasource.jdbc.pool", datasourceId); 538 } 539 540 } 541 542 private void _synchronizeMultifactorAuthenticationDatasource(Configuration boConfiguration, Map<String, Object> valuesToChange) 543 { 544 Configuration mfaDatasourceConfiguration = boConfiguration.getChild("MultifactorAuthentication") 545 .getChild("Datasource"); 546 String mfaDatasourceId = mfaDatasourceConfiguration.getValue(""); 547 548 if (!StringUtils.equals(mfaDatasourceId, Config.getInstance().getValue("runtime.assignments.multifactorauthentication"))) 549 { 550 valuesToChange.put("runtime.assignments.multifactorauthentication", mfaDatasourceId); 551 } 552 } 553 554 /** 555 * Record of the SignupPage parameters 556 * @param url The sign-up page URL 557 * @param siteName the page site name 558 * @param lang the page language 559 * @param userDirByPop The userDirectories Ids and populations Ids 560 */ 561 public record SignupPage (String url, String siteName, String lang, Map<String, String> userDirByPop) { /*Nothing*/ } 562 563}