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.core.user.population; 017 018import java.io.File; 019import java.io.FileOutputStream; 020import java.io.IOException; 021import java.io.OutputStream; 022import java.lang.reflect.Array; 023import java.nio.file.Files; 024import java.nio.file.StandardCopyOption; 025import java.sql.Connection; 026import java.time.Instant; 027import java.time.temporal.ChronoUnit; 028import java.util.ArrayList; 029import java.util.Collections; 030import java.util.HashMap; 031import java.util.HashSet; 032import java.util.LinkedHashMap; 033import java.util.List; 034import java.util.Map; 035import java.util.Optional; 036import java.util.Properties; 037import java.util.Set; 038import java.util.regex.Pattern; 039import java.util.stream.Collectors; 040 041import javax.xml.transform.OutputKeys; 042import javax.xml.transform.TransformerConfigurationException; 043import javax.xml.transform.TransformerFactory; 044import javax.xml.transform.TransformerFactoryConfigurationError; 045import javax.xml.transform.sax.SAXTransformerFactory; 046import javax.xml.transform.sax.TransformerHandler; 047import javax.xml.transform.stream.StreamResult; 048 049import org.apache.avalon.framework.activity.Disposable; 050import org.apache.avalon.framework.activity.Initializable; 051import org.apache.avalon.framework.component.Component; 052import org.apache.avalon.framework.configuration.Configuration; 053import org.apache.avalon.framework.configuration.ConfigurationException; 054import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 055import org.apache.avalon.framework.service.ServiceException; 056import org.apache.avalon.framework.service.ServiceManager; 057import org.apache.avalon.framework.service.Serviceable; 058import org.apache.cocoon.xml.AttributesImpl; 059import org.apache.cocoon.xml.XMLUtils; 060import org.apache.commons.lang3.StringUtils; 061import org.apache.xml.serializer.OutputPropertiesFactory; 062import org.xml.sax.SAXException; 063 064import org.ametys.core.ObservationConstants; 065import org.ametys.core.authentication.CredentialProvider; 066import org.ametys.core.authentication.CredentialProviderFactory; 067import org.ametys.core.authentication.CredentialProviderModel; 068import org.ametys.core.datasource.ConnectionHelper; 069import org.ametys.core.datasource.SQLDataSourceManager; 070import org.ametys.core.observation.Event; 071import org.ametys.core.observation.ObservationManager; 072import org.ametys.core.script.SQLScriptHelper; 073import org.ametys.core.ui.Callable; 074import org.ametys.core.user.CurrentUserProvider; 075import org.ametys.core.user.InvalidModificationException; 076import org.ametys.core.user.User.UserCreationOrigin; 077import org.ametys.core.user.UserIdentity; 078import org.ametys.core.user.directory.ModifiableUserDirectory; 079import org.ametys.core.user.directory.UserDirectory; 080import org.ametys.core.user.directory.UserDirectoryFactory; 081import org.ametys.core.user.directory.UserDirectoryModel; 082import org.ametys.core.util.I18nUtils; 083import org.ametys.core.util.ReadXMLDataHelper; 084import org.ametys.plugins.core.authentication.MultifactorAuthenticationManager; 085import org.ametys.plugins.core.impl.user.directory.JdbcUserDirectory; 086import org.ametys.plugins.core.impl.user.directory.JdbcUserDirectory.StrongPasswordRequirements; 087import org.ametys.plugins.core.impl.user.directory.StaticUserDirectory; 088import org.ametys.runtime.i18n.I18nizableText; 089import org.ametys.runtime.model.DefinitionContext; 090import org.ametys.runtime.model.ElementDefinition; 091import org.ametys.runtime.model.checker.ItemCheckerDescriptor; 092import org.ametys.runtime.model.type.ElementType; 093import org.ametys.runtime.model.type.ModelItemTypeConstants; 094import org.ametys.runtime.model.type.xml.XMLElementType; 095import org.ametys.runtime.plugin.PluginsManager; 096import org.ametys.runtime.plugin.PluginsManager.Status; 097import org.ametys.runtime.plugin.component.AbstractLogEnabled; 098import org.ametys.runtime.util.AmetysHomeHelper; 099 100/** 101 * DAO for accessing {@link UserPopulation} 102 */ 103public class UserPopulationDAO extends AbstractLogEnabled implements Component, Serviceable, Initializable, Disposable 104{ 105 /** Avalon Role */ 106 public static final String ROLE = UserPopulationDAO.class.getName(); 107 108 /** The id of the "admin" population */ 109 public static final String ADMIN_POPULATION_ID = "admin_population"; 110 111 /** The id of the "user system" login */ 112 public static final String SYSTEM_USER_LOGIN = "system-user"; 113 /** The id of the "user system" population */ 114 public static final UserIdentity SYSTEM_USER_IDENTITY = new UserIdentity(SYSTEM_USER_LOGIN, ADMIN_POPULATION_ID); 115 116 /** The sql table for admin users */ 117 private static final String __ADMIN_TABLENAME = "AdminUsers"; 118 119 /** The path of the XML file containing the user populations */ 120 private static File __USER_POPULATIONS_FILE; 121 122 /** The regular expression for an id of a user population */ 123 private static final String __ID_REGEX = "^[a-z][a-z0-9_-]*"; 124 125 /** The date (as a long) of the last time the {@link #__USER_POPULATIONS_FILE Populations file} was read (last update) */ 126 private long _lastFileReading; 127 128 /** The whole user populations of the application */ 129 private Map<String, UserPopulation> _userPopulations; 130 /** The misconfigured user populations */ 131 private Set<String> _misconfiguredUserPopulations; 132 133 /** The list of population ids which are declared in the user population file but were not instanciated since their configuration led to an error */ 134 private Set<String> _ignoredPopulations; 135 136 /** The population admin */ 137 private UserPopulation _adminUserPopulation; 138 139 /** The user directories factory */ 140 private UserDirectoryFactory _userDirectoryFactory; 141 142 /** The credential providers factory */ 143 private CredentialProviderFactory _credentialProviderFactory; 144 145 /** The extension point for population consumers */ 146 private PopulationConsumerExtensionPoint _populationConsumerEP; 147 148 private ObservationManager _observationManager; 149 150 private CurrentUserProvider _currentUserProvider; 151 152 private I18nUtils _i18nutils; 153 154 private MultifactorAuthenticationManager _multifactorAuthenticationManager; 155 156 private ReadXMLDataHelper _readXMLDataHelper; 157 158 private ServiceManager _manager; 159 160 @Override 161 public void initialize() 162 { 163 __USER_POPULATIONS_FILE = new File(AmetysHomeHelper.getAmetysHome(), "config" + File.separator + "user-populations.xml"); 164 _userPopulations = new LinkedHashMap<>(); 165 _misconfiguredUserPopulations = new HashSet<>(); 166 _ignoredPopulations = new HashSet<>(); 167 _lastFileReading = 0; 168 } 169 170 private PopulationConsumerExtensionPoint _getPopulationConsumerExtensionPoint() 171 { 172 if (_populationConsumerEP == null) 173 { 174 try 175 { 176 _populationConsumerEP = (PopulationConsumerExtensionPoint) _manager.lookup(PopulationConsumerExtensionPoint.ROLE); 177 } 178 catch (ServiceException e) 179 { 180 throw new RuntimeException("Failed to retrieve PopulationConsumerExtensionPoint", e); 181 } 182 } 183 return _populationConsumerEP; 184 } 185 186 private ObservationManager _getObservationManager() 187 { 188 if (_observationManager == null) 189 { 190 try 191 { 192 _observationManager = (ObservationManager) _manager.lookup(ObservationManager.ROLE); 193 } 194 catch (ServiceException e) 195 { 196 // Not a safe component... ignore it 197 return null; 198 } 199 } 200 return _observationManager; 201 } 202 203 private UserDirectoryFactory _getUserDirectoryFactory() 204 { 205 if (_userDirectoryFactory == null) 206 { 207 try 208 { 209 _userDirectoryFactory = (UserDirectoryFactory) _manager.lookup(UserDirectoryFactory.ROLE); 210 } 211 catch (ServiceException e) 212 { 213 throw new RuntimeException("Failed to retrieve UserDirectoryFactory", e); 214 } 215 } 216 return _userDirectoryFactory; 217 } 218 219 private CredentialProviderFactory _getCredentialProviderFactory() 220 { 221 if (_credentialProviderFactory == null) 222 { 223 try 224 { 225 _credentialProviderFactory = (CredentialProviderFactory) _manager.lookup(CredentialProviderFactory.ROLE); 226 } 227 catch (ServiceException e) 228 { 229 throw new RuntimeException("Failed to retrieve CredentialProviderFactory", e); 230 } 231 } 232 return _credentialProviderFactory; 233 } 234 235 private CurrentUserProvider _getCurrentUserProvider() 236 { 237 if (_currentUserProvider == null) 238 { 239 try 240 { 241 _currentUserProvider = (CurrentUserProvider) _manager.lookup(CurrentUserProvider.ROLE); 242 } 243 catch (ServiceException e) 244 { 245 throw new RuntimeException("Failed to retrieve CurrentUserProvider", e); 246 } 247 } 248 return _currentUserProvider; 249 } 250 251 private I18nUtils _getI18nUtils() 252 { 253 if (_i18nutils == null) 254 { 255 try 256 { 257 _i18nutils = (I18nUtils) _manager.lookup(I18nUtils.ROLE); 258 } 259 catch (ServiceException e) 260 { 261 throw new RuntimeException("Failed to retrieve I18nUtils", e); 262 } 263 } 264 return _i18nutils; 265 } 266 267 private MultifactorAuthenticationManager _getMultifactorAuthenticationManager() 268 { 269 if (_multifactorAuthenticationManager == null) 270 { 271 try 272 { 273 _multifactorAuthenticationManager = (MultifactorAuthenticationManager) _manager.lookup(MultifactorAuthenticationManager.ROLE); 274 } 275 catch (ServiceException e) 276 { 277 throw new RuntimeException("Failed to retrieve multifactor authentication manager", e); 278 } 279 } 280 return _multifactorAuthenticationManager; 281 } 282 283 private ReadXMLDataHelper _getReadXMLDataHelper() 284 { 285 if (_readXMLDataHelper == null) 286 { 287 try 288 { 289 _readXMLDataHelper = (ReadXMLDataHelper) _manager.lookup(ReadXMLDataHelper.ROLE); 290 } 291 catch (ServiceException e) 292 { 293 throw new RuntimeException("Failed to retrieve read XML data helper", e); 294 } 295 } 296 return _readXMLDataHelper; 297 } 298 299 @Override 300 public void service(ServiceManager manager) throws ServiceException 301 { 302 _manager = manager; 303 } 304 305 /** 306 * Gets all the populations to JSON format 307 * @param withAdmin True to include the "admin" population 308 * @return A list of object representing the {@link UserPopulation}s 309 */ 310 public List<Object> getUserPopulationsAsJson(boolean withAdmin) 311 { 312 return getUserPopulations(withAdmin).stream().map(this::getUserPopulationAsJson).collect(Collectors.toList()); 313 } 314 315 /** 316 * Gets a population to JSON format 317 * @param userPopulation The user population to get 318 * @return An object representing a {@link UserPopulation} 319 */ 320 public Map<String, Object> getUserPopulationAsJson(UserPopulation userPopulation) 321 { 322 Map<String, Object> result = new LinkedHashMap<>(); 323 result.put("id", userPopulation.getId()); 324 result.put("label", userPopulation.getLabel()); 325 result.put("enabled", userPopulation.isEnabled()); 326 result.put("valid", isValid(userPopulation.getId())); 327 result.put("isInUse", _getPopulationConsumerExtensionPoint().isInUse(userPopulation.getId())); 328 329 List<Object> userDirectories = new ArrayList<>(); 330 for (UserDirectory ud : userPopulation.getUserDirectories()) 331 { 332 String udModelId = ud.getUserDirectoryModelId(); 333 UserDirectoryModel udModel = _getUserDirectoryFactory().getExtension(udModelId); 334 335 Map<String, Object> directory = new HashMap<>(); 336 directory.put("id", ud.getId()); 337 338 if (StringUtils.isNotBlank(ud.getLabel())) 339 { 340 directory.put("label", _getI18nUtils().translate(udModel.getLabel()) + " (" + ud.getLabel() + ")"); 341 } 342 else 343 { 344 directory.put("label", udModel.getLabel()); 345 } 346 directory.put("modifiable", ud instanceof ModifiableUserDirectory); 347 userDirectories.add(directory); 348 } 349 result.put("userDirectories", userDirectories); 350 351 List<Object> credentialProviders = new ArrayList<>(); 352 for (CredentialProvider cp : userPopulation.getCredentialProviders()) 353 { 354 String cpModelId = cp.getCredentialProviderModelId(); 355 CredentialProviderModel cpModel = _getCredentialProviderFactory().getExtension(cpModelId); 356 357 Map<String, Object> credentialProvider = new HashMap<>(); 358 credentialProvider.put("id", cp.getId()); 359 360 if (StringUtils.isNotBlank(cp.getLabel())) 361 { 362 credentialProvider.put("label", _getI18nUtils().translate(cpModel.getLabel()) + " (" + cp.getLabel() + ")"); 363 } 364 else 365 { 366 credentialProvider.put("label", cpModel.getLabel()); 367 } 368 credentialProviders.add(credentialProvider); 369 } 370 result.put("credentialProviders", credentialProviders); 371 372 return result; 373 } 374 375 /** 376 * Gets all the populations of this application 377 * @param includeAdminPopulation True to include the "admin" population 378 * @return A list of {@link UserPopulation} 379 */ 380 public List<UserPopulation> getUserPopulations(boolean includeAdminPopulation) 381 { 382 List<UserPopulation> result = new ArrayList<>(); 383 if (includeAdminPopulation) 384 { 385 result.add(getAdminPopulation()); 386 } 387 388 // Don't read in safe mode, we know that only the admin population is needed in this case and we want to prevent some warnings in the logs for non-safe features not found 389 if (Status.OK.equals(PluginsManager.getInstance().getStatus())) 390 { 391 _readPopulations(false); 392 result.addAll(_userPopulations.values()); 393 } 394 395 return result; 396 } 397 398 /** 399 * Gets all the enabled populations of this application 400 * @param withAdmin True to include the "admin" population 401 * @return A list of enabled {@link UserPopulation} 402 */ 403 public List<UserPopulation> getEnabledUserPopulations(boolean withAdmin) 404 { 405 return getUserPopulations(withAdmin).stream().filter(UserPopulation::isEnabled).collect(Collectors.toList()); 406 } 407 408 /** 409 * Gets a population with its id. 410 * @param id The id of the population 411 * @return The {@link UserPopulation}, or null if not found 412 */ 413 public UserPopulation getUserPopulation(String id) 414 { 415 if (ADMIN_POPULATION_ID.equals(id)) 416 { 417 return getAdminPopulation(); 418 } 419 420 _readPopulations(false); 421 return _userPopulations.get(id); 422 } 423 424 /** 425 * Gets the list of the ids of all the population of the application 426 * @return The list of the ids of all the populations 427 */ 428 @Callable 429 public List<String> getUserPopulationsIds() 430 { 431 _readPopulations(false); 432 return new ArrayList<>(_userPopulations.keySet()); 433 } 434 435 /** 436 * Returns the id of population which have a fatal invalid configuration. 437 * These populations can NOT be used by application. 438 * @return The ignored populations 439 */ 440 public Set<String> getIgnoredPopulations() 441 { 442 _readPopulations(false); 443 return _ignoredPopulations; 444 } 445 446 /** 447 * Returns the id of population which have at least one user directory or one credential provider misconfigured 448 * These populations can be used by application. 449 * @return The misconfigured populations. 450 */ 451 public Set<String> getMisconfiguredPopulations() 452 { 453 _readPopulations(false); 454 return _misconfiguredUserPopulations; 455 } 456 457 /** 458 * Return the configuration file. 459 * @return the configuration file. 460 */ 461 public File getConfigurationFile() 462 { 463 return __USER_POPULATIONS_FILE; 464 } 465 466 /** 467 * Gets the configuration for creating/editing a user population. 468 * @return A map containing information about what is needed to create/edit a user population 469 * @throws Exception If an error occurs. 470 */ 471 @Callable 472 public Map<String, Object> getEditionConfiguration() throws Exception 473 { 474 Map<String, Object> result = new LinkedHashMap<>(); 475 DefinitionContext context = DefinitionContext.newInstance().withEdition(true); 476 477 List<Object> userDirectoryModels = new ArrayList<>(); 478 for (String extensionId : _getUserDirectoryFactory().getExtensionsIds()) 479 { 480 UserDirectoryModel udModel = _getUserDirectoryFactory().getExtension(extensionId); 481 if (udModel == null) 482 { 483 throw new IllegalStateException("The user population configuration refers to an unexisting extension for the user directory '" + extensionId + "'"); 484 } 485 486 Map<String, Object> udMap = new LinkedHashMap<>(); 487 udMap.put("id", extensionId); 488 udMap.put("label", udModel.getLabel()); 489 udMap.put("description", udModel.getDescription()); 490 491 Map<String, Object> params = new LinkedHashMap<>(); 492 for (String paramId : udModel.getParameters().keySet()) 493 { 494 // prefix in case of two parameters from two different models have the same id which can lead to some errorsin client-side 495 params.put(extensionId + "$" + paramId, udModel.getParameters().get(paramId).toJSON(context)); 496 } 497 udMap.put("parameters", params); 498 499 Map<String, Object> paramCheckers = new LinkedHashMap<>(); 500 for (String paramCheckerId : udModel.getParameterCheckers().keySet()) 501 { 502 ItemCheckerDescriptor paramChecker = udModel.getParameterCheckers().get(paramCheckerId); 503 paramCheckers.put(extensionId + "$" + paramCheckerId, paramChecker.toJSON()); 504 } 505 udMap.put("parameterCheckers", paramCheckers); 506 507 userDirectoryModels.add(udMap); 508 } 509 result.put("userDirectoryModels", userDirectoryModels); 510 511 List<Object> credentialProviderModels = new ArrayList<>(); 512 for (String extensionId : _getCredentialProviderFactory().getExtensionsIds()) 513 { 514 CredentialProviderModel cpModel = _getCredentialProviderFactory().getExtension(extensionId); 515 if (cpModel == null) 516 { 517 throw new IllegalStateException("The user population configuration refers to an unexisting extension for the credential provider '" + extensionId + "'"); 518 } 519 520 Map<String, Object> cpMap = new LinkedHashMap<>(); 521 cpMap.put("id", extensionId); 522 cpMap.put("label", cpModel.getLabel()); 523 cpMap.put("description", cpModel.getDescription()); 524 525 Map<String, Object> params = new LinkedHashMap<>(); 526 for (String paramId : cpModel.getParameters().keySet()) 527 { 528 // prefix in case of two parameters from two different models have the same id which can lead to some errors in client-side 529 params.put(extensionId + "$" + paramId, cpModel.getParameters().get(paramId).toJSON(context)); 530 } 531 cpMap.put("parameters", params); 532 533 Map<String, Object> paramCheckers = new LinkedHashMap<>(); 534 for (String paramCheckerId : cpModel.getParameterCheckers().keySet()) 535 { 536 ItemCheckerDescriptor paramChecker = cpModel.getParameterCheckers().get(paramCheckerId); 537 paramCheckers.put(extensionId + "$" + paramCheckerId, paramChecker.toJSON()); 538 } 539 cpMap.put("parameterCheckers", paramCheckers); 540 541 credentialProviderModels.add(cpMap); 542 } 543 result.put("credentialProviderModels", credentialProviderModels); 544 545 return result; 546 } 547 548 /** 549 * Gets the values of the parameters of the given population 550 * @param id The id of the population 551 * @return The values of the parameters 552 */ 553 @Callable 554 public Map<String, Object> getPopulationParameterValues(String id) 555 { 556 Map<String, Object> result = new LinkedHashMap<>(); 557 558 _readPopulations(false); 559 UserPopulation up = _userPopulations.get(id); 560 561 if (up == null) 562 { 563 getLogger().error("The UserPopulation of id '{}' does not exists.", id); 564 result.put("error", "unknown"); 565 return result; 566 } 567 568 // Population Label 569 result.put("label", up.getLabel()); 570 result.put("id", up.getId()); 571 572 // User Directories 573 List<Map<String, Object>> userDirectories = new ArrayList<>(); 574 575 for (UserDirectory ud : up.getUserDirectories()) 576 { 577 Map<String, Object> ud2json = new HashMap<>(); 578 String udModelId = ud.getUserDirectoryModelId(); 579 UserDirectoryModel model = _getUserDirectoryFactory().getExtension(udModelId); 580 if (model == null) 581 { 582 throw new IllegalStateException("The user population configuration refers to an unexisting extension for the user directory '" + udModelId + "'"); 583 } 584 585 ud2json.put("id", ud.getId()); 586 ud2json.put("udModelId", udModelId); 587 ud2json.put("label", ud.getLabel()); 588 Map<String, Object> params = new HashMap<>(); 589 for (String key : ud.getParameterValues().keySet()) 590 { 591 ElementDefinition parameter = model.getParameters().get(key); 592 params.put(udModelId + "$" + key, ModelItemTypeConstants.PASSWORD_ELEMENT_TYPE_ID.equals(parameter.getType().getId()) ? "PASSWORD" : ud.getParameterValues().get(key)); 593 } 594 ud2json.put("params", params); 595 userDirectories.add(ud2json); 596 } 597 result.put("userDirectories", userDirectories); 598 599 // Credential Providers 600 List<Map<String, Object>> credentialProviders = new ArrayList<>(); 601 602 for (CredentialProvider cp : up.getCredentialProviders()) 603 { 604 Map<String, Object> cp2json = new HashMap<>(); 605 String cpModelId = cp.getCredentialProviderModelId(); 606 CredentialProviderModel model = _getCredentialProviderFactory().getExtension(cpModelId); 607 if (model == null) 608 { 609 throw new IllegalStateException("The user population configuration refers to an unexisting extension for the credential provider '" + cpModelId + "'"); 610 } 611 612 cp2json.put("id", cp.getId()); 613 cp2json.put("cpModelId", cpModelId); 614 cp2json.put("label", cp.getLabel()); 615 Map<String, Object> params = new HashMap<>(); 616 for (String key : cp.getParameterValues().keySet()) 617 { 618 ElementDefinition parameter = model.getParameters().get(key); 619 params.put(cpModelId + "$" + key, ModelItemTypeConstants.PASSWORD_ELEMENT_TYPE_ID.equals(parameter.getType().getId()) ? "PASSWORD" : cp.getParameterValues().get(key)); 620 } 621 cp2json.put("params", params); 622 credentialProviders.add(cp2json); 623 } 624 result.put("credentialProviders", credentialProviders); 625 626 return result; 627 } 628 629 /** 630 * Gets the "admin" population 631 * @return The "admin" population 632 */ 633 public synchronized UserPopulation getAdminPopulation() 634 { 635 if (_adminUserPopulation != null) 636 { 637 return _adminUserPopulation; 638 } 639 640 _adminUserPopulation = new UserPopulation(); 641 _adminUserPopulation.setId(ADMIN_POPULATION_ID); 642 643 Map<String, Object> userDirectory1 = new HashMap<>(); 644 userDirectory1.put("udModelId", "org.ametys.plugins.core.user.directory.Static"); 645 userDirectory1.put("id", "static"); 646 userDirectory1.put("org.ametys.plugins.core.user.directory.Static$runtime.users.static.users", SYSTEM_USER_LOGIN + ":System:User:"); 647 648 Map<String, Object> userDirectory2 = new HashMap<>(); 649 String udModelId = "org.ametys.plugins.core.user.directory.Jdbc"; 650 userDirectory2.put("udModelId", udModelId); 651 userDirectory2.put("id", "admin-jdbc"); 652 653 userDirectory2.put(udModelId + "$" + JdbcUserDirectory.DATASOURCE_PARAM_NAME, SQLDataSourceManager.AMETYS_INTERNAL_DATASOURCE_ID); 654 userDirectory2.put(udModelId + "$" + JdbcUserDirectory.USERS_TABLE_PARAM_NAME, __ADMIN_TABLENAME); 655 userDirectory2.put(udModelId + "$" + JdbcUserDirectory.USE_STRONG_PASSWORD_PARAM_NAME, true); 656 userDirectory2.put(udModelId + "$" + JdbcUserDirectory.USE_STRONG_PASSWORD_MIN_LENGTH_PARAM_NAME, 8); 657 userDirectory2.put(udModelId + "$" + JdbcUserDirectory.USE_STRONG_PASSWORD_MIN_LOWERCASE_PARAM_NAME, 1); 658 userDirectory2.put(udModelId + "$" + JdbcUserDirectory.USE_STRONG_PASSWORD_MIN_UPPERCASE_PARAM_NAME, 1); 659 userDirectory2.put(udModelId + "$" + JdbcUserDirectory.USE_STRONG_PASSWORD_MIN_NUMBERS_PARAM_NAME, 1); 660 userDirectory2.put(udModelId + "$" + JdbcUserDirectory.USE_STRONG_PASSWORD_MIN_SPECIALS_PARAM_NAME, 1); 661 userDirectory2.put(udModelId + "$" + JdbcUserDirectory.USE_STRONG_PASSWORD_SPECIAL_CHARACTERS_PARAM_NAME, StrongPasswordRequirements.DEFAULT_SPECIAL_CHARACTERS); 662 663 Map<String, Object> credentialProvider = new HashMap<>(); 664 String cpModelId = "org.ametys.core.authentication.FormBased"; 665 credentialProvider.put("cpModelId", cpModelId); 666 credentialProvider.put(cpModelId + "$" + "runtime.authentication.form.cookies", "false"); 667 credentialProvider.put(cpModelId + "$" + "runtime.authentication.form.store-password-in-session", "false"); 668 credentialProvider.put(cpModelId + "$" + "runtime.authentication.form.captcha", "true"); 669 credentialProvider.put(cpModelId + "$" + "runtime.authentication.form.login-by-email", "false"); 670 credentialProvider.put(cpModelId + "$" + "runtime.authentication.form.security.storage", SQLDataSourceManager.AMETYS_INTERNAL_DATASOURCE_ID); 671 credentialProvider.put(cpModelId + "$" + "runtime.authentication.form.multifactor", "true"); 672 673 // We may need to create the admin user 674 boolean wasExisting = false; 675 try (Connection connection = ConnectionHelper.getInternalSQLDataSourceConnection()) 676 { 677 wasExisting = SQLScriptHelper.tableExists(connection, __ADMIN_TABLENAME); 678 } 679 catch (Exception e) 680 { 681 throw new RuntimeException("Cannot test if " + __ADMIN_TABLENAME + " table exists in internal database", e); 682 } 683 684 List<UserDirectory> userDirectories = _getUserDirectories(ADMIN_POPULATION_ID, List.of(userDirectory1, userDirectory2)); 685 List<CredentialProvider> credentialProviders = _getCredentialProviders(List.of(credentialProvider)); 686 687 _updateUserPopulation(_adminUserPopulation, new I18nizableText("plugin.core", "PLUGINS_CORE_USER_POPULATION_ADMIN_LABEL"), userDirectories, credentialProviders); 688 689 ((StaticUserDirectory) _adminUserPopulation.getUserDirectory("static")).setGrantAllCredentials(false); 690 691 if (!wasExisting) 692 { 693 Map<String, String> adminUserInformations = new HashMap<>(); 694 adminUserInformations.put("login", "admin"); 695 adminUserInformations.put("password", "admin"); 696 adminUserInformations.put("firstname", "User"); 697 adminUserInformations.put("lastname", "Administrator"); 698 adminUserInformations.put("email", ""); 699 700 JdbcUserDirectory adminUserDirectory = (JdbcUserDirectory) _adminUserPopulation.getUserDirectories().get(1); 701 try 702 { 703 adminUserDirectory.forceStrongPassword(false); 704 adminUserDirectory.add(adminUserInformations, UserCreationOrigin.ADMIN); 705 adminUserDirectory.forceStrongPassword(true); 706 } 707 catch (InvalidModificationException e) 708 { 709 throw new RuntimeException("Cannot create the 'admin' user", e); 710 } 711 } 712 713 return _adminUserPopulation; 714 } 715 716 /** 717 * Adds a new population 718 * @param id The unique id of the population 719 * @param label The label of the population 720 * @param userDirectoriesParameters A list of user directory parameters 721 * @param credentialProvidersParameters A list of credential provider parameters 722 * @return A map containing the id of the created population, or the kind of error that occured 723 */ 724 @Callable 725 public Map<String, Object> add(String id, String label, List<Map<String, Object>> userDirectoriesParameters, List<Map<String, Object>> credentialProvidersParameters) 726 { 727 _readPopulations(false); 728 729 Map<String, Object> result = new LinkedHashMap<>(); 730 731 if (!_isCorrectId(id)) 732 { 733 return null; 734 } 735 736 UserPopulation userPopulation = new UserPopulation(); 737 userPopulation.setId(id); 738 739 List<UserDirectory> userDirectories = _getUserDirectories(id, userDirectoriesParameters); 740 List<CredentialProvider> credentialProviders = _getCredentialProviders(credentialProvidersParameters); 741 742 _updateUserPopulation(userPopulation, new I18nizableText(label), userDirectories, credentialProviders); 743 744 _userPopulations.put(id, userPopulation); 745 if (_writePopulations()) 746 { 747 getLogger().error("An error occured when writing the configuration file which contains the user populations.", id); 748 result.put("error", "server"); 749 return result; 750 } 751 752 if (_getObservationManager() != null) 753 { 754 Map<String, Object> eventParams = new HashMap<>(); 755 eventParams.put(ObservationConstants.ARGS_USERPOPULATION_ID, id); 756 _getObservationManager().notify(new Event(ObservationConstants.EVENT_USERPOPULATION_ADDED, _getCurrentUserProvider().getUser(), eventParams)); 757 } 758 759 result.put("id", id); 760 return result; 761 } 762 763 private boolean _isCorrectId(String id) 764 { 765 if (_userPopulations.get(id) != null || ADMIN_POPULATION_ID.equals(id)) 766 { 767 getLogger().error("The id '{}' is already used for a population.", id); 768 return false; 769 } 770 771 if (!Pattern.matches(__ID_REGEX, id)) 772 { 773 getLogger().error("The id '{}' is not a correct id for a user population.", id); 774 return false; 775 } 776 777 return true; 778 } 779 780 /** 781 * Edits the given population. 782 * @param id The id of the population to edit 783 * @param label The label of the population 784 * @param userDirectoriesParameters A list of user directory parameters 785 * @param credentialProvidersParameters A list of credential provider parameters 786 * @return A map containing the id of the edited population, or the kind of error that occured 787 */ 788 @Callable 789 public Map<String, Object> edit(String id, String label, List<Map<String, Object>> userDirectoriesParameters, List<Map<String, Object>> credentialProvidersParameters) 790 { 791 _readPopulations(false); 792 793 Map<String, Object> result = new LinkedHashMap<>(); 794 795 UserPopulation userPopulation = _userPopulations.get(id); 796 if (userPopulation == null) 797 { 798 getLogger().error("The UserPopulation with id '{}' does not exist, it cannot be edited.", id); 799 result.put("error", "unknown"); 800 return result; 801 } 802 803 // User directories 804 Map<String, Map<String, Object>> existingUserDirectoriesParameters = userPopulation.getUserDirectories().stream() 805 .collect(Collectors.toMap(UserDirectory::getId, UserDirectory::getParameterValues)); 806 Map<String, String> userDirectoriesModels = userPopulation.getUserDirectories().stream().collect(Collectors.toMap(UserDirectory::getId, UserDirectory::getUserDirectoryModelId)); 807 List<UserDirectory> userDirectories = _getUserDirectories(id, userDirectoriesParameters, existingUserDirectoriesParameters, userDirectoriesModels); 808 809 // Credential providers 810 Map<String, Map<String, Object>> existingCredentialProvidersParameters = userPopulation.getCredentialProviders().stream() 811 .collect(Collectors.toMap(CredentialProvider::getId, CredentialProvider::getParameterValues)); 812 Map<String, String> credentialProvidersModels = userPopulation.getCredentialProviders().stream().collect(Collectors.toMap(CredentialProvider::getId, CredentialProvider::getCredentialProviderModelId)); 813 List<CredentialProvider> credentialProviders = _getCredentialProviders(credentialProvidersParameters, existingCredentialProvidersParameters, credentialProvidersModels); 814 815 userPopulation.dispose(); 816 _updateUserPopulation(userPopulation, new I18nizableText(label), userDirectories, credentialProviders); 817 818 if (_writePopulations()) 819 { 820 getLogger().error("An error occured when writing the configuration file which contains the user populations.", id); 821 result.put("error", "server"); 822 return result; 823 } 824 825 if (_getObservationManager() != null) 826 { 827 Map<String, Object> eventParams = new HashMap<>(); 828 eventParams.put(ObservationConstants.ARGS_USERPOPULATION_ID, id); 829 _getObservationManager().notify(new Event(ObservationConstants.EVENT_USERPOPULATION_UPDATED, _getCurrentUserProvider().getUser(), eventParams)); 830 } 831 832 result.put("id", id); 833 return result; 834 } 835 836 private List<UserDirectory> _getUserDirectories(String userPopulationId, List<Map<String, Object>> userDirectories) 837 { 838 return _getUserDirectories(userPopulationId, userDirectories, Collections.emptyMap(), Collections.emptyMap()); 839 } 840 841 private List<UserDirectory> _getUserDirectories(String userPopulationId, List<Map<String, Object>> userDirectories, Map<String, Map<String, Object>> existingUdParameters, Map<String, String> udModels) 842 { 843 // Create the user directories 844 List<UserDirectory> uds = new ArrayList<>(); 845 for (Map<String, Object> userDirectoryParameters : userDirectories) 846 { 847 String id = (String) userDirectoryParameters.remove("id"); 848 String modelId = (String) userDirectoryParameters.remove("udModelId"); 849 String additionnalLabel = (String) userDirectoryParameters.remove("label"); 850 Map<String, Object> typedParamValues = _getTypedUDParameters(userDirectoryParameters, modelId); 851 852 if (StringUtils.isBlank(id)) 853 { 854 id = org.ametys.core.util.StringUtils.generateKey(); 855 } 856 else 857 { 858 _keepExistingUserDirectoryPassword(modelId, typedParamValues, existingUdParameters.getOrDefault(id, Collections.emptyMap()), udModels.get(id)); 859 } 860 861 uds.add(_getUserDirectoryFactory().createUserDirectory(id, modelId, typedParamValues, userPopulationId, additionnalLabel)); 862 } 863 864 return uds; 865 } 866 867 private List<CredentialProvider> _getCredentialProviders(List<Map<String, Object>> credentialProviders) 868 { 869 return _getCredentialProviders(credentialProviders, Collections.emptyMap(), Collections.emptyMap()); 870 } 871 872 private List<CredentialProvider> _getCredentialProviders(List<Map<String, Object>> credentialProviders, Map<String, Map<String, Object>> existingCpParameters, Map<String, String> cpModels) 873 { 874 // Create the credential providers 875 List<CredentialProvider> cps = new ArrayList<>(); 876 for (Map<String, Object> credentialProviderParameters : credentialProviders) 877 { 878 String id = (String) credentialProviderParameters.remove("id"); 879 String modelId = (String) credentialProviderParameters.remove("cpModelId"); 880 String additionnalLabel = (String) credentialProviderParameters.remove("label"); 881 Map<String, Object> typedParamValues = _getTypedCPParameters(credentialProviderParameters, modelId); 882 883 if (StringUtils.isBlank(id)) 884 { 885 id = org.ametys.core.util.StringUtils.generateKey(); 886 } 887 else 888 { 889 _keepExistingCredentialProviderPassword(modelId, typedParamValues, existingCpParameters.getOrDefault(id, Collections.emptyMap()), cpModels.get(id)); 890 } 891 892 CredentialProvider credentialProvider = _getCredentialProviderFactory().createCredentialProvider(id, modelId, typedParamValues, additionnalLabel); 893 if (credentialProvider != null) 894 { 895 cps.add(credentialProvider); 896 } 897 } 898 899 return cps; 900 } 901 902 private void _updateUserPopulation(UserPopulation userPopulation, I18nizableText label, List<UserDirectory> userDirectories, List<CredentialProvider> credentialProviders) 903 { 904 userPopulation.setLabel(label); 905 userPopulation.setUserDirectories(userDirectories); 906 userPopulation.setCredentialProviders(credentialProviders); 907 908 _getMultifactorAuthenticationManager().initializeMFACryptoComponent(userPopulation); 909 } 910 911 private void _keepExistingUserDirectoryPassword(String modelId, Map<String, Object> typedParamValues, Map<String, Object> existingUdParameters, String existingModelId) 912 { 913 // An existing id means an existing user directory 914 if (StringUtils.equals(modelId, existingModelId)) 915 { 916 // The model did not changed, we want to keep the unmodified passwords 917 UserDirectoryModel userDirectoryModel = _getUserDirectoryFactory().getExtension(modelId); 918 for (Map.Entry<String, ? extends ElementDefinition> parameterEntry : userDirectoryModel.getParameters().entrySet()) 919 { 920 ElementDefinition parameter = parameterEntry.getValue(); 921 // If the parameter is a password AND has no value 922 if (ModelItemTypeConstants.PASSWORD_ELEMENT_TYPE_ID.equals(parameter.getType().getId()) && typedParamValues.get(parameterEntry.getKey()) == null) 923 { 924 // Update the submitted data with the existing password 925 typedParamValues.put(parameterEntry.getKey(), existingUdParameters.get(parameterEntry.getKey())); 926 } 927 } 928 } 929 } 930 931 private void _keepExistingCredentialProviderPassword(String modelId, Map<String, Object> typedParamValues, Map<String, Object> existingCpParameters, String existingModelId) 932 { 933 // An existing id means an existing credential provider 934 if (StringUtils.equals(modelId, existingModelId)) 935 { 936 // The model did not changed, we want to keep the unmodified passwords 937 CredentialProviderModel credentialProviderModel = _getCredentialProviderFactory().getExtension(modelId); 938 for (Map.Entry<String, ? extends ElementDefinition> parameterEntry : credentialProviderModel.getParameters().entrySet()) 939 { 940 ElementDefinition parameter = parameterEntry.getValue(); 941 // If the parameter is a password AND has no value 942 if (ModelItemTypeConstants.PASSWORD_ELEMENT_TYPE_ID.equals(parameter.getType().getId()) && typedParamValues.get(parameterEntry.getKey()) == null) 943 { 944 // Update the submitted data with the existing password 945 typedParamValues.put(parameterEntry.getKey(), existingCpParameters.get(parameterEntry.getKey())); 946 } 947 } 948 } 949 } 950 951 private Map<String, Object> _getTypedUDParameters(Map<String, Object> parameters, String modelId) 952 { 953 Map<String, Object> resultParameters = new LinkedHashMap<>(); 954 955 UserDirectoryModel model = _getUserDirectoryFactory().getExtension(modelId); 956 if (model == null) 957 { 958 throw new IllegalStateException("The user population configuration refers to an unexisting extension for the user directory '" + modelId + "'"); 959 } 960 961 Map<String, ? extends ElementDefinition> declaredParameters = model.getParameters(); 962 for (String paramNameWithPrefix : parameters.keySet()) 963 { 964 String[] splitStr = paramNameWithPrefix.split("\\$", 2); 965 String prefix = splitStr[0]; 966 String paramName = splitStr[1]; 967 if (prefix.equals(modelId) && declaredParameters.containsKey(paramName)) 968 { 969 Object originalValue = parameters.get(paramNameWithPrefix); 970 971 ElementDefinition parameter = declaredParameters.get(paramName); 972 ElementType type = parameter.getType(); 973 974 if (parameter.isMultiple()) 975 { 976 @SuppressWarnings("unchecked") 977 List<Object> multipleValue = originalValue instanceof List 978 ? (List<Object>) originalValue 979 : List.of(originalValue); 980 981 Object[] typedValues = multipleValue.stream() 982 .map(val -> type.castValue(val)) 983 .toArray(s -> (Object[]) Array.newInstance(type.getManagedClass(), s)); 984 985 resultParameters.put(paramName, typedValues); 986 } 987 else 988 { 989 Object typedValue = type.castValue(originalValue); 990 resultParameters.put(paramName, typedValue); 991 } 992 } 993 else if (prefix.equals(modelId)) 994 { 995 getLogger().warn("The parameter {} is not declared in extension {}. It will be ignored", paramName, modelId); 996 } 997 } 998 999 return resultParameters; 1000 } 1001 1002 private Map<String, Object> _getTypedCPParameters(Map<String, Object> parameters, String modelId) 1003 { 1004 Map<String, Object> resultParameters = new LinkedHashMap<>(); 1005 1006 CredentialProviderModel model = _getCredentialProviderFactory().getExtension(modelId); 1007 if (model == null) 1008 { 1009 throw new IllegalStateException("The user population configuration refers to an unexisting extension for the credential provider '" + modelId + "'"); 1010 } 1011 1012 Map<String, ? extends ElementDefinition> declaredParameters = model.getParameters(); 1013 for (String paramNameWithPrefix : parameters.keySet()) 1014 { 1015 String[] splitStr = paramNameWithPrefix.split("\\$", 2); 1016 String prefix = splitStr[0]; 1017 String paramName = splitStr[1]; 1018 if (prefix.equals(modelId) && declaredParameters.containsKey(paramName)) 1019 { 1020 Object originalValue = parameters.get(paramNameWithPrefix); 1021 1022 ElementDefinition parameter = declaredParameters.get(paramName); 1023 ElementType type = parameter.getType(); 1024 1025 if (parameter.isMultiple()) 1026 { 1027 @SuppressWarnings("unchecked") 1028 List<Object> multipleValue = originalValue instanceof List 1029 ? (List<Object>) originalValue 1030 : List.of(originalValue); 1031 1032 Object[] typedValues = multipleValue.stream() 1033 .map(val -> type.castValue(val)) 1034 .toArray(s -> (Object[]) Array.newInstance(type.getManagedClass(), s)); 1035 1036 resultParameters.put(paramName, typedValues); 1037 } 1038 else 1039 { 1040 Object typedValue = type.castValue(originalValue); 1041 resultParameters.put(paramName, typedValue); 1042 } 1043 } 1044 else if (prefix.equals(modelId)) 1045 { 1046 getLogger().warn("The parameter {} is not declared in extension {}. It will be ignored", paramName, modelId); 1047 } 1048 } 1049 1050 return resultParameters; 1051 } 1052 1053 /** 1054 * Removes the given population. 1055 * @param id The id of the population to remove 1056 * @return A map containing the id of the removed population 1057 */ 1058 @Callable 1059 public Map<String, Object> remove(String id) 1060 { 1061 return remove(id, false); 1062 } 1063 1064 /** 1065 * Removes the given population. 1066 * @param id The id of the population to remove 1067 * @param forceDeletion Delete the population even if it is still in use 1068 * @return A map containing the id of the removed population 1069 */ 1070 public Map<String, Object> remove(String id, boolean forceDeletion) 1071 { 1072 Map<String, Object> result = new LinkedHashMap<>(); 1073 1074 // Check if the population is not the admin population 1075 if (ADMIN_POPULATION_ID.equals("id")) 1076 { 1077 return null; 1078 } 1079 1080 // Check if the population is used 1081 if (!forceDeletion && _getPopulationConsumerExtensionPoint().isInUse(id)) 1082 { 1083 getLogger().error("The UserPopulation with id '{}' is used, it cannot be removed.", id); 1084 result.put("error", "used"); 1085 return result; 1086 } 1087 1088 _readPopulations(false); 1089 1090 UserPopulation up = _userPopulations.remove(id); 1091 1092 if (up == null) 1093 { 1094 getLogger().error("The UserPopulation with id '{}' does not exist, it cannot be removed.", id); 1095 result.put("error", "unknown"); 1096 return result; 1097 } 1098 1099 up.dispose(); 1100 1101 if (_writePopulations()) 1102 { 1103 result.put("error", "server"); 1104 return result; 1105 } 1106 1107 if (_getObservationManager() != null) 1108 { 1109 Map<String, Object> eventParams = new HashMap<>(); 1110 eventParams.put(ObservationConstants.ARGS_USERPOPULATION_ID, id); 1111 _getObservationManager().notify(new Event(ObservationConstants.EVENT_USERPOPULATION_DELETED, _getCurrentUserProvider().getUser(), eventParams)); 1112 } 1113 1114 result.put("id", id); 1115 return result; 1116 } 1117 1118 /** 1119 * Enables/Disables the given population 1120 * @param populationId The id of the population to enable/disable 1121 * @param enabled True to enable the population, false to disable it. 1122 * @return A map containing the id of the enabled/disabled population, or with an error. 1123 */ 1124 @Callable 1125 public Map<String, Object> enable(String populationId, boolean enabled) 1126 { 1127 Map<String, Object> result = new LinkedHashMap<>(); 1128 1129 UserPopulation population = getUserPopulation(populationId); 1130 if (population != null) 1131 { 1132 population.enable(enabled); 1133 result.put("id", populationId); 1134 } 1135 else 1136 { 1137 getLogger().error("The UserPopulation with id '{}' does not exist, it cannot be enabled/disabled.", populationId); 1138 result.put("error", "unknown"); 1139 } 1140 1141 if (_writePopulations()) 1142 { 1143 result.put("error", "server"); 1144 return result; 1145 } 1146 1147 return result; 1148 } 1149 1150 /** 1151 * Determines if a population has a valid configuration 1152 * @param populationId The id of the population to retrieve state 1153 * @return A map, with the response as a boolean, or an error. 1154 */ 1155 @Callable 1156 public boolean isValid(String populationId) 1157 { 1158 return !_misconfiguredUserPopulations.contains(populationId); 1159 } 1160 1161 /** 1162 * Returns the enabled state of the given population 1163 * @param populationId The id of the population to retrieve state 1164 * @return A map, with the response as a booolean, or an error. 1165 */ 1166 @Callable 1167 public Map<String, Object> isEnabled(String populationId) 1168 { 1169 Map<String, Object> result = new LinkedHashMap<>(); 1170 1171 UserPopulation population = getUserPopulation(populationId); 1172 if (population != null) 1173 { 1174 result.put("enabled", population.isEnabled()); 1175 } 1176 else 1177 { 1178 result.put("error", "unknown"); 1179 } 1180 1181 return result; 1182 } 1183 1184 /** 1185 * Determines if a population can be removed 1186 * @param populationId The id of the population 1187 * @return A map, with the response as a boolean, or an error. 1188 */ 1189 @Callable 1190 public Map<String, Object> canRemove(String populationId) 1191 { 1192 Map<String, Object> result = new HashMap<>(); 1193 1194 //Can remove only if not used, event if the population is disabled 1195 result.put("canRemove", !_getPopulationConsumerExtensionPoint().isInUse(populationId)); 1196 1197 return result; 1198 } 1199 1200 /** 1201 * If needed, reads the config file representing the populations and then 1202 * reinitializes and updates the internal representation of the populations. 1203 * @param forceRead True to avoid the use of the cache and force the reading of the file 1204 */ 1205 private synchronized void _readPopulations(boolean forceRead) 1206 { 1207 try 1208 { 1209 if (!__USER_POPULATIONS_FILE.exists()) 1210 { 1211 if (getLogger().isDebugEnabled()) 1212 { 1213 getLogger().debug("No user population file found at {}. Creating a new one.", __USER_POPULATIONS_FILE.getAbsolutePath()); 1214 } 1215 _createPopulationsFile(__USER_POPULATIONS_FILE); 1216 } 1217 1218 // In Linux file systems, the precision of java.io.File.lastModified() is the second, so we need here to always have 1219 // this (bad!) precision by doing the truncation to second precision (/1000 * 1000) on the millis time value. 1220 // Therefore, the boolean outdated is computed with '>=' operator, and not '>', which will lead to sometimes (but rarely) unnecessarily re-read the file. 1221 long fileLastModified = (__USER_POPULATIONS_FILE.lastModified() / 1000) * 1000; 1222 if (forceRead || fileLastModified >= _lastFileReading) 1223 { 1224 getLogger().debug("Reading user population file at {}", __USER_POPULATIONS_FILE.getAbsolutePath()); 1225 1226 long lastFileReading = Instant.now().truncatedTo(ChronoUnit.SECONDS).toEpochMilli(); 1227 Map<String, UserPopulation> userPopulations = new LinkedHashMap<>(); 1228 Set<String> ignoredPopulations = new HashSet<>(); 1229 Set<String> misconfiguredUserPopulations = new HashSet<>(); 1230 1231 Configuration cfg = new DefaultConfigurationBuilder().buildFromFile(__USER_POPULATIONS_FILE); 1232 for (Configuration childCfg : cfg.getChildren("userPopulation")) 1233 { 1234 try 1235 { 1236 UserPopulation up = _configurePopulation(childCfg, misconfiguredUserPopulations); 1237 userPopulations.put(up.getId(), up); 1238 } 1239 catch (ConfigurationException e) 1240 { 1241 getLogger().error("Fatal configuration error for population of id '{}'. The population will be ignored.", childCfg.getAttribute("id", ""), e); 1242 ignoredPopulations.add(childCfg.getAttribute("id", "")); 1243 } 1244 } 1245 1246 getLogger().debug("User population file read. Found {} valid population(s), {} invalid population(s) and {} misconfigured population(s)", userPopulations.size(), ignoredPopulations.size(), misconfiguredUserPopulations.size()); 1247 1248 // Release previous components 1249 this.dispose(); 1250 1251 _lastFileReading = lastFileReading; 1252 _userPopulations = userPopulations; 1253 _ignoredPopulations = ignoredPopulations; 1254 _misconfiguredUserPopulations = misconfiguredUserPopulations; 1255 } 1256 else if (getLogger().isDebugEnabled()) 1257 { 1258 getLogger().debug("No need to reload the user population file. The file modified at {} was not modified since {}", fileLastModified, _lastFileReading); 1259 } 1260 } 1261 catch (Exception e) 1262 { 1263 getLogger().error("Failed to retrieve user populations from the configuration file " + __USER_POPULATIONS_FILE, e); 1264 } 1265 } 1266 1267 private void _createPopulationsFile(File file) throws IOException, TransformerConfigurationException, SAXException 1268 { 1269 file.createNewFile(); 1270 try (OutputStream os = new FileOutputStream(file)) 1271 { 1272 // create a transformer for saving sax into a file 1273 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 1274 1275 StreamResult result = new StreamResult(os); 1276 th.setResult(result); 1277 1278 // create the format of result 1279 Properties format = new Properties(); 1280 format.put(OutputKeys.METHOD, "xml"); 1281 format.put(OutputKeys.INDENT, "yes"); 1282 format.put(OutputKeys.ENCODING, "UTF-8"); 1283 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4"); 1284 th.getTransformer().setOutputProperties(format); 1285 th.startDocument(); 1286 XMLUtils.createElement(th, "userPopulations"); 1287 th.endDocument(); 1288 } 1289 1290 if (getLogger().isDebugEnabled()) 1291 { 1292 getLogger().debug("Empty file for user populations created"); 1293 } 1294 } 1295 1296 private UserPopulation _configurePopulation(Configuration configuration, Set<String> misconfiguredUserPopulations) throws ConfigurationException 1297 { 1298 UserPopulation up = new UserPopulation(); 1299 1300 String upId = configuration.getAttribute("id"); 1301 up.setId(upId); 1302 1303 List<UserDirectory> userDirectories = _configureUserDirectories(configuration, upId, misconfiguredUserPopulations); 1304 List<CredentialProvider> credentialProviders = _configureCredentialProviders(configuration, upId); 1305 1306 _updateUserPopulation(up, new I18nizableText(configuration.getChild("label").getValue()), userDirectories, credentialProviders); 1307 1308 boolean enabled = configuration.getAttributeAsBoolean("enabled", true) && userDirectories.size() > 0 && credentialProviders.size() > 0; 1309 up.enable(enabled); 1310 1311 return up; 1312 } 1313 1314 private List<UserDirectory> _configureUserDirectories(Configuration configuration, String upId, Set<String> misconfiguredUserPopulations) throws ConfigurationException 1315 { 1316 List<UserDirectory> userDirectories = new ArrayList<>(); 1317 1318 Configuration[] userDirectoriesConf = configuration.getChild("userDirectories").getChildren("userDirectory"); 1319 for (Configuration userDirectoryConf : userDirectoriesConf) 1320 { 1321 String id = userDirectoryConf.getAttribute("id"); 1322 String modelId = userDirectoryConf.getAttribute("modelId"); 1323 String label = userDirectoryConf.getAttribute("label", null); 1324 1325 try 1326 { 1327 Map<String, Object> paramValues = _getUDParametersFromConfiguration(userDirectoryConf, modelId, upId); 1328 UserDirectory ud = _getUserDirectoryFactory().createUserDirectory(id, modelId, paramValues, upId, label); 1329 if (ud != null) 1330 { 1331 userDirectories.add(ud); 1332 } 1333 } 1334 catch (Exception e) 1335 { 1336 getLogger().warn("The population of id '" + upId + "' declares a user directory with an invalid configuration", e); 1337 misconfiguredUserPopulations.add(upId); 1338 } 1339 } 1340 1341 if (userDirectories.isEmpty()) 1342 { 1343 misconfiguredUserPopulations.add(upId); 1344 getLogger().warn("The population of id '" + upId + "' does not have user directory with a valid configuration. It will be disabled until it will be fixed."); 1345 } 1346 1347 return userDirectories; 1348 } 1349 1350 private List<CredentialProvider> _configureCredentialProviders(Configuration configuration, String upId) throws ConfigurationException 1351 { 1352 List<CredentialProvider> credentialProviders = new ArrayList<>(); 1353 1354 Configuration[] credentialProvidersConf = configuration.getChild("credentialProviders").getChildren("credentialProvider"); 1355 for (Configuration credentialProviderConf : credentialProvidersConf) 1356 { 1357 String id = credentialProviderConf.getAttribute("id"); 1358 String modelId = credentialProviderConf.getAttribute("modelId"); 1359 String additionnalLabel = credentialProviderConf.getAttribute("label", null); 1360 1361 try 1362 { 1363 Map<String, Object> paramValues = _getCPParametersFromConfiguration(credentialProviderConf, modelId, upId); 1364 CredentialProvider cp = _getCredentialProviderFactory().createCredentialProvider(id, modelId, paramValues, additionnalLabel); 1365 if (cp != null) 1366 { 1367 credentialProviders.add(cp); 1368 } 1369 } 1370 catch (Exception e) 1371 { 1372 getLogger().warn("The population of id '" + upId + "' declares a credential provider with an invalid configuration", e); 1373 _misconfiguredUserPopulations.add(upId); 1374 } 1375 } 1376 1377 if (credentialProviders.isEmpty()) 1378 { 1379 _misconfiguredUserPopulations.add(upId); 1380 getLogger().warn("The population of id '" + upId + "' does not have credential provider with a valid configuration. It will be disabled until it will be fixed."); 1381 } 1382 1383 return credentialProviders; 1384 } 1385 1386 /** 1387 * Get the typed parameters of a user directory used by a population 1388 * @param conf The user directory's configuration. 1389 * @param modelId The id of user directory model 1390 * @param populationId The id of population 1391 * @return The typed parameters 1392 * @throws IllegalArgumentException if the configured user directory references a non-existing user directory model 1393 * @throws ConfigurationException if a parameter is missing 1394 */ 1395 private Map<String, Object> _getUDParametersFromConfiguration(Configuration conf, String modelId, String populationId) throws ConfigurationException, IllegalArgumentException 1396 { 1397 if (!_getUserDirectoryFactory().hasExtension(modelId)) 1398 { 1399 throw new IllegalArgumentException(String.format("The population of id '%s' declares a non-existing user directory model with id '%s'. It will be ignored.", populationId, modelId)); 1400 } 1401 1402 Map<String, ? extends ElementDefinition> declaredParameters = _getUserDirectoryFactory().getExtension(modelId).getParameters(); 1403 1404 Map<String, List<I18nizableText>> errors = new HashMap<>(); 1405 Map<String, Object> parameters = _getReadXMLDataHelper().readAndValidateXMLData(conf, modelId + "$", declaredParameters, errors); 1406 if (errors.isEmpty()) 1407 { 1408 return parameters; 1409 } 1410 else 1411 { 1412 StringBuilder sb = new StringBuilder(String.format("The population of id '%s' declares a user directory model with id '%s' but some parameters are not valid:", populationId, modelId)); 1413 for (String dataName : errors.keySet()) 1414 { 1415 for (I18nizableText error : errors.get(dataName)) 1416 { 1417 sb.append("\n* '").append(dataName).append("': ").append(_getI18nUtils().translate(error)); 1418 } 1419 } 1420 sb.append("\nThis user directory will be ignored."); 1421 1422 throw new ConfigurationException(sb.toString(), conf); 1423 } 1424 } 1425 1426 /** 1427 * Get the typed parameters of a credential provider used by a population 1428 * @param conf The credential provider's configuration. 1429 * @param modelId The id of credential provider model 1430 * @param populationId The id of population 1431 * @return The typed parameters 1432 * @throws IllegalArgumentException if the configured credential provider references a non-existing credential provider model 1433 * @throws ConfigurationException if a parameter is missing 1434 */ 1435 private Map<String, Object> _getCPParametersFromConfiguration(Configuration conf, String modelId, String populationId) throws ConfigurationException, IllegalArgumentException 1436 { 1437 if (!_getCredentialProviderFactory().hasExtension(modelId)) 1438 { 1439 throw new IllegalArgumentException(String.format("The population of id '%s' declares a non-existing credential provider model with id '%s'. It will be ignored.", populationId, modelId)); 1440 } 1441 1442 Map<String, ? extends ElementDefinition> declaredParameters = _getCredentialProviderFactory().getExtension(modelId).getParameters(); 1443 1444 Map<String, List<I18nizableText>> errors = new HashMap<>(); 1445 Map<String, Object> parameters = _getReadXMLDataHelper().readAndValidateXMLData(conf, modelId + "$", declaredParameters, errors); 1446 if (errors.isEmpty()) 1447 { 1448 return parameters; 1449 } 1450 else 1451 { 1452 StringBuilder sb = new StringBuilder(String.format("The population of id '%s' declares a credential provider model with id '%s' but some parameters are not valid:", populationId, modelId)); 1453 for (String dataName : errors.keySet()) 1454 { 1455 for (I18nizableText error : errors.get(dataName)) 1456 { 1457 sb.append("\n* '").append(dataName).append("': ").append(_getI18nUtils().translate(error)); 1458 } 1459 } 1460 sb.append("\nThis credential provider will be ignored."); 1461 1462 throw new ConfigurationException(sb.toString(), conf); 1463 } 1464 } 1465 1466 /** 1467 * Erases the config file representing the populations and rebuild it 1468 * from the internal representation of the populations. 1469 * @return True if an error occured 1470 */ 1471 private boolean _writePopulations() 1472 { 1473 File backup = new File(__USER_POPULATIONS_FILE.getPath() + ".tmp"); 1474 boolean errorOccured = false; 1475 1476 // Create a backup file 1477 try 1478 { 1479 Files.copy(__USER_POPULATIONS_FILE.toPath(), backup.toPath()); 1480 } 1481 catch (IOException e) 1482 { 1483 getLogger().error("Error when creating backup '" + __USER_POPULATIONS_FILE + "' file", e); 1484 } 1485 1486 // Do writing 1487 try (OutputStream os = new FileOutputStream(__USER_POPULATIONS_FILE)) 1488 { 1489 // create a transformer for saving sax into a file 1490 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 1491 1492 StreamResult result = new StreamResult(os); 1493 th.setResult(result); 1494 1495 // create the format of result 1496 Properties format = new Properties(); 1497 format.put(OutputKeys.METHOD, "xml"); 1498 format.put(OutputKeys.INDENT, "yes"); 1499 format.put(OutputKeys.ENCODING, "UTF-8"); 1500 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4"); 1501 th.getTransformer().setOutputProperties(format); 1502 1503 // sax the config 1504 try 1505 { 1506 _toSAX(th); 1507 } 1508 catch (Exception e) 1509 { 1510 getLogger().error("Error when saxing the userPopulations", e); 1511 errorOccured = true; 1512 } 1513 } 1514 catch (IOException | TransformerConfigurationException | TransformerFactoryConfigurationError e) 1515 { 1516 getLogger().error("Error when trying to modify the user populations with the configuration file " + __USER_POPULATIONS_FILE, e); 1517 } 1518 1519 // Restore the file if an error previously occured 1520 try 1521 { 1522 if (errorOccured) 1523 { 1524 // An error occured, restore the original 1525 Files.copy(backup.toPath(), __USER_POPULATIONS_FILE.toPath(), StandardCopyOption.REPLACE_EXISTING); 1526 // Force to reread the file 1527 _readPopulations(true); 1528 } 1529 Files.deleteIfExists(backup.toPath()); 1530 } 1531 catch (IOException e) 1532 { 1533 getLogger().error("Error when restoring backup '" + __USER_POPULATIONS_FILE + "' file", e); 1534 } 1535 1536 return errorOccured; 1537 } 1538 1539 private void _toSAX(TransformerHandler handler) 1540 { 1541 try 1542 { 1543 handler.startDocument(); 1544 XMLUtils.startElement(handler, "userPopulations"); 1545 for (UserPopulation up : _userPopulations.values()) 1546 { 1547 _saxUserPopulation(up, handler); 1548 } 1549 1550 XMLUtils.endElement(handler, "userPopulations"); 1551 handler.endDocument(); 1552 } 1553 catch (SAXException e) 1554 { 1555 getLogger().error("Error when saxing the userPopulations", e); 1556 } 1557 } 1558 1559 private void _saxUserPopulation(UserPopulation userPopulation, TransformerHandler handler) 1560 { 1561 try 1562 { 1563 AttributesImpl atts = new AttributesImpl(); 1564 atts.addCDATAAttribute("id", userPopulation.getId()); 1565 atts.addCDATAAttribute("enabled", Boolean.toString(userPopulation.isEnabled())); 1566 XMLUtils.startElement(handler, "userPopulation", atts); 1567 1568 userPopulation.getLabel().toSAX(handler, "label"); 1569 1570 // SAX user directories 1571 XMLUtils.startElement(handler, "userDirectories"); 1572 for (UserDirectory ud : userPopulation.getUserDirectories()) 1573 { 1574 AttributesImpl attr = new AttributesImpl(); 1575 attr.addCDATAAttribute("id", ud.getId()); 1576 attr.addCDATAAttribute("modelId", ud.getUserDirectoryModelId()); 1577 attr.addCDATAAttribute("label", ud.getLabel() != null ? ud.getLabel() : ""); 1578 XMLUtils.startElement(handler, "userDirectory", attr); 1579 1580 UserDirectoryModel udModel = _getUserDirectoryFactory().getExtension(ud.getUserDirectoryModelId()); 1581 _writeParameterValues(udModel.getParameters(), ud.getParameterValues(), handler); 1582 1583 XMLUtils.endElement(handler, "userDirectory"); 1584 } 1585 XMLUtils.endElement(handler, "userDirectories"); 1586 1587 // SAX credential providers 1588 XMLUtils.startElement(handler, "credentialProviders"); 1589 for (CredentialProvider cp : userPopulation.getCredentialProviders()) 1590 { 1591 AttributesImpl attr = new AttributesImpl(); 1592 attr.addCDATAAttribute("id", cp.getId()); 1593 attr.addCDATAAttribute("modelId", cp.getCredentialProviderModelId()); 1594 attr.addCDATAAttribute("label", cp.getLabel() != null ? cp.getLabel() : ""); 1595 XMLUtils.startElement(handler, "credentialProvider", attr); 1596 1597 CredentialProviderModel cpModel = _getCredentialProviderFactory().getExtension(cp.getCredentialProviderModelId()); 1598 _writeParameterValues(cpModel.getParameters(), cp.getParameterValues(), handler); 1599 1600 XMLUtils.endElement(handler, "credentialProvider"); 1601 } 1602 XMLUtils.endElement(handler, "credentialProviders"); 1603 1604 XMLUtils.endElement(handler, "userPopulation"); 1605 } 1606 catch (SAXException e) 1607 { 1608 getLogger().error("Error when saxing the userPopulation " + userPopulation, e); 1609 } 1610 } 1611 1612 private void _writeParameterValues(Map<String, ? extends ElementDefinition> definitions, Map<String, Object> paramValues, TransformerHandler handler) throws SAXException 1613 { 1614 for (String paramName : paramValues.keySet()) 1615 { 1616 Object value = paramValues.get(paramName); 1617 ElementDefinition definition = definitions.get(paramName); 1618 1619 Optional<XMLElementType> type = Optional.ofNullable(definition) 1620 .map(ElementDefinition::getType) 1621 .filter(XMLElementType.class::isInstance) 1622 .map(XMLElementType.class::cast); 1623 1624 if (type.isPresent()) 1625 { 1626 type.get().write(handler, paramName, value); 1627 } 1628 } 1629 } 1630 1631 @Override 1632 public void dispose() 1633 { 1634 for (UserPopulation up : _userPopulations.values()) 1635 { 1636 up.dispose(); 1637 } 1638 1639 _userPopulations.clear(); 1640 _lastFileReading = 0; 1641 } 1642}