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.plugins.core.impl.user.directory; 017 018import java.sql.Connection; 019import java.sql.PreparedStatement; 020import java.sql.ResultSet; 021import java.sql.SQLException; 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.Collection; 025import java.util.Collections; 026import java.util.HashMap; 027import java.util.LinkedHashMap; 028import java.util.List; 029import java.util.Map; 030 031import org.apache.avalon.framework.component.Component; 032import org.apache.avalon.framework.context.Context; 033import org.apache.avalon.framework.context.ContextException; 034import org.apache.avalon.framework.context.Contextualizable; 035import org.apache.avalon.framework.service.ServiceException; 036import org.apache.avalon.framework.service.ServiceManager; 037import org.apache.commons.codec.digest.DigestUtils; 038import org.apache.commons.lang3.RandomStringUtils; 039import org.apache.commons.lang3.StringUtils; 040import org.apache.commons.lang3.ArrayUtils; 041import org.apache.excalibur.source.SourceResolver; 042 043import org.ametys.core.ObservationConstants; 044import org.ametys.core.cache.Cache; 045import org.ametys.core.datasource.ConnectionHelper; 046import org.ametys.core.migration.MigrationExtensionPoint; 047import org.ametys.core.migration.storage.VersionStorageExtensionPoint; 048import org.ametys.core.migration.storage.impl.SqlVersionStorage; 049import org.ametys.core.observation.Event; 050import org.ametys.core.observation.ObservationManager; 051import org.ametys.core.script.SQLScriptHelper; 052import org.ametys.core.user.CurrentUserProvider; 053import org.ametys.core.user.InvalidModificationException; 054import org.ametys.core.user.User; 055import org.ametys.core.user.UserIdentity; 056import org.ametys.core.user.directory.ModifiableUserDirectory; 057import org.ametys.core.user.directory.NotUniqueUserException; 058import org.ametys.core.util.mail.SendMailHelper; 059import org.ametys.plugins.core.jdbc.JdbcParameterTypeExtensionPoint; 060import org.ametys.runtime.i18n.I18nizableText; 061import org.ametys.runtime.model.ElementDefinition; 062import org.ametys.runtime.model.ModelHelper; 063import org.ametys.runtime.model.ModelItem; 064import org.ametys.runtime.model.View; 065import org.ametys.runtime.model.type.ModelItemType; 066import org.ametys.runtime.model.type.ModelItemTypeConstants; 067import org.ametys.runtime.parameter.DefaultValidator; 068import org.ametys.runtime.parameter.Errors; 069import org.ametys.runtime.parameter.Validator; 070import org.ametys.runtime.plugin.PluginsManager; 071 072/** 073 * Use a jdbc driver for getting the list of users, modifying them and also 074 * authenticate them.<br> 075 * Passwords need to be encrypted with MD5 and encoded in base64.<br> 076 */ 077public class JdbcUserDirectory extends AbstractCachingUserDirectory implements ModifiableUserDirectory, Component, Contextualizable 078{ 079 /** The base plugin (for i18n key) */ 080 protected static final String BASE_PLUGIN_NAME = "core"; 081 082 static final String[] __COLUMNS = new String[] {"login", "password", "firstname", "lastname", "email"}; 083 static final String[] __ORDERBY_COLUMNS = new String[] {"lastname", "firstname"}; 084 085 /** Name of the parameter holding the datasource id */ 086 private static final String __DATASOURCE_PARAM_NAME = "runtime.users.jdbc.datasource"; 087 /** Name of the parameter holding the table users' name */ 088 private static final String __USERS_TABLE_PARAM_NAME = "runtime.users.jdbc.table"; 089 090 private static final String __COLUMN_LOGIN = "login"; 091 private static final String __COLUMN_PASSWORD = "password"; 092 private static final String __COLUMN_FIRSTNAME = "firstname"; 093 private static final String __COLUMN_LASTNAME = "lastname"; 094 private static final String __COLUMN_EMAIL = "email"; 095 private static final String __COLUMN_SALT = "salt"; 096 097 /** The identifier of data source */ 098 protected String _dataSourceId; 099 /** The name of users' SQL table */ 100 protected String _userTableName; 101 102 /** Model */ 103 protected Map<String, ElementDefinition> _model; 104 105 /** The avalon service manager */ 106 protected ServiceManager _manager; 107 108 /** The avalon context */ 109 protected Context _context; 110 111 /** The cocoon source resolver */ 112 protected SourceResolver _sourceResolver; 113 114 private ObservationManager _observationManager; 115 private CurrentUserProvider _currentUserProvider; 116 117 private boolean _lazyInitialized; 118 119 private JdbcParameterTypeExtensionPoint _jdbcParameterTypeExtensionPoint; 120 121 private View _view; 122 123 private MigrationExtensionPoint _migrationEP; 124 125 private SqlVersionStorage _sqlVersionStorage; 126 127 @Override 128 public void contextualize(Context context) throws ContextException 129 { 130 _context = context; 131 } 132 133 @Override 134 public void service(ServiceManager manager) throws ServiceException 135 { 136 super.service(manager); 137 138 _manager = manager; 139 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 140 _jdbcParameterTypeExtensionPoint = (JdbcParameterTypeExtensionPoint) manager.lookup(JdbcParameterTypeExtensionPoint.ROLE); 141 _migrationEP = (MigrationExtensionPoint) manager.lookup(MigrationExtensionPoint.ROLE); 142 VersionStorageExtensionPoint versionStorageEP = (VersionStorageExtensionPoint) manager.lookup(VersionStorageExtensionPoint.ROLE); 143 _sqlVersionStorage = (SqlVersionStorage) versionStorageEP.getExtension("sql"); 144 } 145 146 public String getFamilyId() 147 { 148 return JdbcUserDirectory.class.getName(); 149 } 150 151 @Override 152 protected String getCacheTypeLabel() 153 { 154 return "SQL"; 155 } 156 157 @Override 158 public void init(String id, String udModelId, Map<String, Object> paramValues, String label) 159 { 160 super.init(id, udModelId, paramValues, label); 161 162 _userTableName = (String) paramValues.get(__USERS_TABLE_PARAM_NAME); 163 _dataSourceId = (String) paramValues.get(__DATASOURCE_PARAM_NAME); 164 165 _initModelParameters(); 166 167 createCaches(); 168 } 169 170 private void _initModelParameters() 171 { 172 _model = new LinkedHashMap<>(); 173 174 I18nizableText invalidLoginText = new I18nizableText("plugin." + BASE_PLUGIN_NAME, "PLUGINS_CORE_USERS_JDBC_FIELD_LOGIN_INVALID"); 175 Validator loginValidator = new DefaultValidator("^[a-zA-Z0-9_\\-\\.@]{3,64}$", invalidLoginText, true); 176 _initModelParameter(__COLUMN_LOGIN, ModelItemTypeConstants.STRING_TYPE_ID, "PLUGINS_CORE_USERS_JDBC_FIELD_LOGIN_LABEL", "PLUGINS_CORE_USERS_JDBC_FIELD_LOGIN_DESCRIPTION", loginValidator); 177 178 _initModelParameter(__COLUMN_PASSWORD, ModelItemTypeConstants.PASSWORD_ELEMENT_TYPE_ID, "PLUGINS_CORE_USERS_JDBC_FIELD_PASSWORD_LABEL", "PLUGINS_CORE_USERS_JDBC_FIELD_PASSWORD_DESCRIPTION", null); 179 180 _initModelParameter(__COLUMN_FIRSTNAME, ModelItemTypeConstants.STRING_TYPE_ID, "PLUGINS_CORE_USERS_JDBC_FIELD_FIRSTNAME_LABEL", "PLUGINS_CORE_USERS_JDBC_FIELD_FIRSTNAME_DESCRIPTION", null); 181 182 _initModelParameter(__COLUMN_LASTNAME, ModelItemTypeConstants.STRING_TYPE_ID, "PLUGINS_CORE_USERS_JDBC_FIELD_LASTNAME_LABEL", "PLUGINS_CORE_USERS_JDBC_FIELD_LASTNAME_DESCRIPTION", null); 183 184 I18nizableText invalidEmailText = new I18nizableText("plugin." + BASE_PLUGIN_NAME, "PLUGINS_CORE_USERS_JDBC_FIELD_EMAIL_INVALID"); 185 Validator emailValidator = new DefaultValidator(SendMailHelper.EMAIL_VALIDATION_REGEXP, invalidEmailText, false); 186 _initModelParameter(__COLUMN_EMAIL, ModelItemTypeConstants.STRING_TYPE_ID, "PLUGINS_CORE_USERS_JDBC_FIELD_EMAIL_LABEL", "PLUGINS_CORE_USERS_JDBC_FIELD_EMAIL_DESCRIPTION", emailValidator); 187 } 188 189 private void _initModelParameter(String name, String parameterType, String labelKey, String descriptionKey, Validator validator) 190 { 191 ModelItemType modelItemType = _jdbcParameterTypeExtensionPoint.getExtension(parameterType); 192 193 ElementDefinition parameter = new ElementDefinition<>(); 194 parameter.setPluginName(BASE_PLUGIN_NAME); 195 parameter.setType(modelItemType); 196 parameter.setName(name); 197 parameter.setLabel(new I18nizableText("plugin." + BASE_PLUGIN_NAME, labelKey)); 198 parameter.setDescription(new I18nizableText("plugin." + BASE_PLUGIN_NAME, descriptionKey)); 199 parameter.setValidator(validator != null ? validator : new DefaultValidator(null, true)); 200 201 _model.put(name, parameter); 202 } 203 204 /** 205 * Lazy lookup the {@link ObservationManager} 206 * @return the observation manager 207 */ 208 protected ObservationManager getObservationManager() 209 { 210 if (_observationManager == null) 211 { 212 try 213 { 214 _observationManager = (ObservationManager) _manager.lookup(ObservationManager.ROLE); 215 } 216 catch (ServiceException e) 217 { 218 // We may be in safe mode 219 if (PluginsManager.getInstance().isSafeMode()) 220 { 221 getLogger().debug("Unable to lookup ObservationManager component in safe mode", e); 222 } 223 else 224 { 225 getLogger().error("Unable to lookup ObservationManager component", e); 226 } 227 } 228 } 229 return _observationManager; 230 } 231 232 /** 233 * Lazy lookup the {@link CurrentUserProvider} 234 * @return the current user provider 235 */ 236 protected CurrentUserProvider getCurrentUserProvider() 237 { 238 if (_currentUserProvider == null) 239 { 240 try 241 { 242 _currentUserProvider = (CurrentUserProvider) _manager.lookup(CurrentUserProvider.ROLE); 243 } 244 catch (ServiceException e) 245 { 246 throw new RuntimeException("Unable to lookup CurrentUserProvider component", e); 247 } 248 } 249 return _currentUserProvider; 250 } 251 252 /** 253 * Get the connection to the database 254 * @return the SQL connection 255 */ 256 @SuppressWarnings("unchecked") 257 protected Connection getSQLConnection() 258 { 259 Connection connection = ConnectionHelper.getConnection(_dataSourceId); 260 261 if (!_lazyInitialized) 262 { 263 try 264 { 265 String componentId = "user-directory.jdbc"; 266 String versionId = "org.ametys.plugins.core.user.directory.Jdbc.upgrade_" + _userTableName; 267 SQLScriptHelper.createTableIfNotExists(_dataSourceId, _userTableName, "plugin:core://scripts/%s/jdbc_users.template.sql", _sourceResolver, 268 (Map) ArrayUtils.toMap(new String[][] {{"%TABLENAME%", _userTableName}}), componentId, versionId, _migrationEP, _sqlVersionStorage); 269 } 270 catch (Exception e) 271 { 272 getLogger().error("The tables requires by the " + this.getClass().getName() + " could not be created. A degraded behavior will occur", e); 273 } 274 275 _lazyInitialized = true; 276 } 277 278 return connection; 279 } 280 281 @SuppressWarnings("unchecked") 282 @Override 283 public Collection<User> getUsers() 284 { 285 return getUsers(Integer.MAX_VALUE, 0, Collections.EMPTY_MAP); 286 } 287 288 @Override 289 public List<User> getUsers(int count, int offset, Map<String, Object> parameters) 290 { 291 String pattern = StringUtils.defaultIfEmpty((String) parameters.get("pattern"), null); 292 int boundedCount = count >= 0 ? count : Integer.MAX_VALUE; 293 int boundedOffset = offset >= 0 ? offset : 0; 294 295 SelectUsersJdbcQueryExecutor<List<User>> queryExecutor = new SelectUsersJdbcQueryExecutor<>(pattern, boundedCount, boundedOffset) 296 { 297 @Override 298 protected List<User> processResultSet(ResultSet rs) throws SQLException 299 { 300 return _getUsersProcessResultSet(rs, true); 301 } 302 }; 303 304 return queryExecutor.run(); 305 } 306 307 @Override 308 public User getUser(String login) 309 { 310 if (isCachingEnabled() && getCacheByLogin().hasKey(login)) 311 { 312 User user = getCacheByLogin().get(login); 313 return user; 314 } 315 316 SelectUserJdbcQueryExecutor<User> queryExecutor = new SelectUserJdbcQueryExecutor<>(login) 317 { 318 @Override 319 protected User processResultSet(ResultSet rs) throws SQLException 320 { 321 Cache<String, User> cache = isCachingEnabled() ? getCacheByLogin() : null; 322 return _getUserProcessResultSet(rs, login, cache); 323 } 324 }; 325 326 return queryExecutor.run(); 327 } 328 329 @Override 330 public User getUserByEmail(String email) throws NotUniqueUserException 331 { 332 if (StringUtils.isBlank(email)) 333 { 334 return null; 335 } 336 337 String lcEmail = email.toLowerCase(); 338 339 if (isCachingEnabled() && getCacheByMail().hasKey(lcEmail)) 340 { 341 User user = getCacheByMail().get(lcEmail); 342 return user; 343 } 344 345 SelectUserJdbcQueryExecutor<List<User>> queryExecutor = new SelectUserJdbcQueryExecutor<>(email, __COLUMN_EMAIL) 346 { 347 @Override 348 protected List<User> processResultSet(ResultSet rs) throws SQLException 349 { 350 return _getUsersProcessResultSet(rs, false); 351 } 352 }; 353 354 List<User> users = queryExecutor.run(); 355 if (users.size() == 1) 356 { 357 return users.get(0); 358 } 359 else if (users.isEmpty()) 360 { 361 return null; 362 } 363 else 364 { 365 throw new NotUniqueUserException("Find " + users.size() + " users matching the email " + email); 366 } 367 } 368 369 @Override 370 public boolean checkCredentials(String login, String password) 371 { 372 boolean updateNeeded = false; 373 374 Connection con = null; 375 PreparedStatement stmt = null; 376 ResultSet rs = null; 377 try 378 { 379 // Connect to the database with connection pool 380 con = getSQLConnection(); 381 382 // Build request for authenticating the user 383 String sql = "SELECT " + __COLUMN_LOGIN + ", " + __COLUMN_PASSWORD + ", " + __COLUMN_SALT + " FROM " + _userTableName + " WHERE " + __COLUMN_LOGIN + " = ?"; 384 if (getLogger().isDebugEnabled()) 385 { 386 getLogger().debug(sql); 387 } 388 389 stmt = con.prepareStatement(sql); 390 stmt.setString(1, login); 391 392 // Do the request 393 rs = stmt.executeQuery(); 394 395 if (rs.next()) 396 { 397 String storedPassword = rs.getString(__COLUMN_PASSWORD); 398 String salt = rs.getString(__COLUMN_SALT); 399 400 if (salt == null && _isMD5Encrypted(storedPassword)) 401 { 402 String encryptedPassword = org.ametys.core.util.StringUtils.md5Base64(password); 403 404 if (encryptedPassword == null) 405 { 406 getLogger().error("Unable to encrypt password"); 407 return false; 408 } 409 410 if (storedPassword.equals(encryptedPassword)) 411 { 412 updateNeeded = true; 413 return true; 414 } 415 416 return false; 417 } 418 else 419 { 420 String encryptedPassword = DigestUtils.sha512Hex(salt + password); 421 422 if (encryptedPassword == null) 423 { 424 getLogger().error("Unable to encrypt password"); 425 return false; 426 } 427 428 return storedPassword.equalsIgnoreCase(encryptedPassword); 429 } 430 } 431 432 return false; 433 } 434 catch (SQLException e) 435 { 436 getLogger().error("Error during the connection to the database", e); 437 return false; 438 } 439 finally 440 { 441 // Close connections 442 ConnectionHelper.cleanup(rs); 443 ConnectionHelper.cleanup(stmt); 444 ConnectionHelper.cleanup(con); 445 446 if (updateNeeded) 447 { 448 _updateToSSHAPassword(login, password); 449 } 450 } 451 } 452 453 @Override 454 public void add(Map<String, String> userInformation) throws InvalidModificationException 455 { 456 Connection con = null; 457 PreparedStatement stmt = null; 458 459 if (getLogger().isDebugEnabled()) 460 { 461 getLogger().debug("Starting adding a new user"); 462 } 463 464 // Check the presence of all parameters 465 Map<String, Errors> errorFields = validate(userInformation); 466 467 if (errorFields.size() > 0) 468 { 469 throw new InvalidModificationException("The creation of user failed because of invalid parameter values", errorFields); 470 } 471 472 String login = userInformation.get("login"); 473 474 try 475 { 476 // Connect to the database with connection pool 477 con = getSQLConnection(); 478 479 stmt = createAddStatement(con, userInformation); 480 481 // Do the request and check the result 482 if (stmt.executeUpdate() != 1) 483 { 484 if (getLogger().isWarnEnabled()) 485 { 486 getLogger().warn("The user to remove '" + login + "' was not removed."); 487 } 488 throw new InvalidModificationException("Error no user inserted"); 489 } 490 491 if (getObservationManager() != null) 492 { 493 // Observation manager can be null in safe mode 494 Map<String, Object> eventParams = new HashMap<>(); 495 eventParams.put(ObservationConstants.ARGS_USER, new UserIdentity(login, getPopulationId())); 496 getObservationManager().notify(new Event(ObservationConstants.EVENT_USER_ADDED, getCurrentUserProvider().getUser(), eventParams)); 497 } 498 } 499 catch (SQLException e) 500 { 501 getLogger().error("Error communication with database", e); 502 throw new InvalidModificationException("Error during the communication with the database", e); 503 } 504 finally 505 { 506 // Close connections 507 ConnectionHelper.cleanup(stmt); 508 ConnectionHelper.cleanup(con); 509 } 510 511 } 512 513 @Override 514 public Map<String, Errors> validate(Map<String, String> userInformation) 515 { 516 Map<String, Errors> errorFields = new HashMap<>(); 517 for (ElementDefinition parameter : _model.values()) 518 { 519 Object typedValue = parameter.getType().castValue(userInformation.get(parameter.getName())); 520 Errors errors = new Errors(); 521 522 List<I18nizableText> errorsList = ModelHelper.validateValue(parameter, typedValue); 523 for (I18nizableText error : errorsList) 524 { 525 errors.addError(error); 526 } 527 528 if (errors.hasErrors()) 529 { 530 if (getLogger().isDebugEnabled()) 531 { 532 getLogger().debug("The field '" + parameter.getName() + "' is not valid"); 533 } 534 errorFields.put(parameter.getName(), errors); 535 } 536 } 537 return errorFields; 538 } 539 540 @Override 541 public void update(Map<String, String> userInformation) throws InvalidModificationException 542 { 543 Connection con = null; 544 PreparedStatement stmt = null; 545 546 Map<String, Errors> errorFields = new HashMap<>(); 547 for (String id : userInformation.keySet()) 548 { 549 ElementDefinition parameter = _model.get(id); 550 if (parameter != null) 551 { 552 Object typedValue = parameter.getType().castValue(userInformation.get(parameter.getName())); 553 List<I18nizableText> errorsList = ModelHelper.validateValue(parameter, typedValue); 554 Errors errors = new Errors(); 555 for (I18nizableText error : errorsList) 556 { 557 errors.addError(error); 558 } 559 560 if (errors.hasErrors()) 561 { 562 if (getLogger().isDebugEnabled()) 563 { 564 getLogger().debug("The field '" + parameter.getName() + "' is not valid"); 565 } 566 errorFields.put(parameter.getName(), errors); 567 } 568 } 569 } 570 571 if (errorFields.size() > 0) 572 { 573 throw new InvalidModificationException("The modification of user failed because of invalid parameter values", errorFields); 574 } 575 576 String login = userInformation.get("login"); 577 if (StringUtils.isEmpty(login)) 578 { 579 throw new InvalidModificationException("Cannot update without login information"); 580 } 581 582 try 583 { 584 // Connect to the database with connection pool 585 con = getSQLConnection(); 586 587 stmt = createModifyStatement(con, userInformation); 588 589 // Do the request 590 if (stmt.executeUpdate() != 1) 591 { 592 throw new InvalidModificationException("Error. User '" + login + "' not updated"); 593 } 594 595 if (getObservationManager() != null) 596 { 597 // Observation manager can be null in safe mode 598 Map<String, Object> eventParams = new HashMap<>(); 599 eventParams.put(ObservationConstants.ARGS_USER, new UserIdentity(login, getPopulationId())); 600 getObservationManager().notify(new Event(ObservationConstants.EVENT_USER_UPDATED, getCurrentUserProvider().getUser(), eventParams)); 601 } 602 603 if (isCachingEnabled()) 604 { 605 getCacheByLogin().invalidate(login); 606 getCacheByMail().invalidateAll(); 607 } 608 } 609 catch (SQLException e) 610 { 611 getLogger().error("Error communication with database", e); 612 throw new InvalidModificationException("Error communication with database", e); 613 } 614 finally 615 { 616 // Close connections 617 ConnectionHelper.cleanup(stmt); 618 ConnectionHelper.cleanup(con); 619 } 620 } 621 622 @Override 623 public void remove(String login) throws InvalidModificationException 624 { 625 Connection con = null; 626 PreparedStatement stmt = null; 627 628 try 629 { 630 // Connect to the database with connection pool 631 con = getSQLConnection(); 632 633 // Build request for removing the user 634 String sqlRequest = "DELETE FROM " + _userTableName + " WHERE " + __COLUMN_LOGIN + " = ?"; 635 if (getLogger().isDebugEnabled()) 636 { 637 getLogger().debug(sqlRequest); 638 } 639 640 stmt = con.prepareStatement(sqlRequest); 641 stmt.setString(1, login); 642 643 // Do the request and check the result 644 if (stmt.executeUpdate() != 1) 645 { 646 throw new InvalidModificationException("Error user was not deleted"); 647 } 648 649 if (getObservationManager() != null) 650 { 651 // Observation manager can be null in safe mode 652 Map<String, Object> eventParams = new HashMap<>(); 653 eventParams.put(ObservationConstants.ARGS_USER, new UserIdentity(login, getPopulationId())); 654 getObservationManager().notify(new Event(ObservationConstants.EVENT_USER_DELETED, getCurrentUserProvider().getUser(), eventParams)); 655 } 656 657 if (isCachingEnabled()) 658 { 659 getCacheByLogin().invalidate(login); 660 getCacheByMail().invalidateAll(); 661 } 662 } 663 catch (SQLException e) 664 { 665 throw new InvalidModificationException("Error during the communication with the database", e); 666 } 667 finally 668 { 669 // Close connections 670 ConnectionHelper.cleanup(stmt); 671 ConnectionHelper.cleanup(con); 672 } 673 } 674 675 public Collection< ? extends ModelItem> getModelItems() 676 { 677 return Collections.unmodifiableCollection(_model.values()); 678 } 679 680 /** 681 * Get the mandatory predicate to use when querying users by pattern. 682 * @param pattern The pattern to match, can be null. 683 * @return a {@link JdbcPredicate}, can be null. 684 */ 685 protected JdbcPredicate _getMandatoryPredicate(String pattern) 686 { 687 return null; 688 } 689 690 /** 691 * Get the pattern to match user login 692 * @param pattern the pattern 693 * @return the pattern to match user login 694 */ 695 protected String _getPatternToMatch(String pattern) 696 { 697 if (pattern != null) 698 { 699 return "%" + pattern + "%"; 700 } 701 return null; 702 } 703 704 /** 705 * Determines if the password is encrypted with MD5 algorithm 706 * @param password The encrypted password 707 * @return true if the password is encrypted with MD5 algorithm 708 */ 709 protected boolean _isMD5Encrypted(String password) 710 { 711 return password.length() == 24; 712 } 713 714 /** 715 * Generate a salt key and encrypt the password with the sha2 algorithm 716 * @param login The user login 717 * @param password The user pasword 718 */ 719 protected void _updateToSSHAPassword(String login, String password) 720 { 721 Connection con = null; 722 PreparedStatement stmt = null; 723 ResultSet rs = null; 724 725 try 726 { 727 con = getSQLConnection(); 728 729 String generateSaltKey = RandomStringUtils.randomAlphanumeric(48); 730 String newEncryptedPassword = DigestUtils.sha512Hex(generateSaltKey + password); 731 732 String sqlUpdate = "UPDATE " + _userTableName + " SET " + __COLUMN_PASSWORD + " = ?, " + __COLUMN_SALT + " = ? WHERE " + __COLUMN_LOGIN + " = ?"; 733 if (getLogger().isDebugEnabled()) 734 { 735 getLogger().debug(sqlUpdate); 736 } 737 738 stmt = con.prepareStatement(sqlUpdate); 739 stmt.setString(1, newEncryptedPassword); 740 stmt.setString(2, generateSaltKey); 741 stmt.setString(3, login); 742 743 stmt.execute(); 744 } 745 catch (SQLException e) 746 { 747 getLogger().error("Error during the connection to the database", e); 748 } 749 finally 750 { 751 // Close connections 752 ConnectionHelper.cleanup(rs); 753 ConnectionHelper.cleanup(stmt); 754 ConnectionHelper.cleanup(con); 755 } 756 } 757 758 /** 759 * Create Add statement 760 * @param con The sql connection 761 * @param userInformation the user informations 762 * @return The statement 763 * @throws SQLException if an error occurred 764 */ 765 protected PreparedStatement createAddStatement(Connection con, Map<String, String> userInformation) throws SQLException 766 { 767 String beginClause = "INSERT INTO " + _userTableName + " ("; 768 String middleClause = ") VALUES ("; 769 String endClause = ")"; 770 771 StringBuffer intoClause = new StringBuffer(); 772 StringBuffer valueClause = new StringBuffer(); 773 774 intoClause.append(__COLUMN_SALT); 775 valueClause.append("?"); 776 777 for (String column : __COLUMNS) 778 { 779 intoClause.append(", " + column); 780 valueClause.append(", ?"); 781 } 782 783 String sqlRequest = beginClause + intoClause.toString() + middleClause + valueClause + endClause; 784 if (getLogger().isDebugEnabled()) 785 { 786 getLogger().debug(sqlRequest); 787 } 788 789 PreparedStatement stmt = con.prepareStatement(sqlRequest); 790 791 int i = 1; 792 boolean clearText = !userInformation.containsKey("clearText") || !"false".equals(userInformation.get("clearText")) || !userInformation.containsKey(__COLUMN_SALT); 793 String generatedSaltKey = clearText ? RandomStringUtils.randomAlphanumeric(48) : userInformation.get(__COLUMN_SALT); 794 795 stmt.setString(i++, generatedSaltKey); 796 797 for (String column : __COLUMNS) 798 { 799 if ("password".equals(column)) 800 { 801 String encryptedPassword; 802 if (clearText) 803 { 804 encryptedPassword = DigestUtils.sha512Hex(generatedSaltKey + userInformation.get(column)); 805 if (encryptedPassword == null) 806 { 807 String message = "Cannot encode password"; 808 getLogger().error(message); 809 throw new SQLException(message); 810 } 811 } 812 else 813 { 814 encryptedPassword = userInformation.get(column); 815 } 816 stmt.setString(i++, encryptedPassword); 817 } 818 else 819 { 820 stmt.setString(i++, userInformation.get(column)); 821 } 822 } 823 824 return stmt; 825 } 826 827 /** 828 * Create statement to update database 829 * @param con The sql connection 830 * @param userInformation The user information 831 * @return The statement 832 * @throws SQLException if an error occurred 833 */ 834 protected PreparedStatement createModifyStatement(Connection con, Map<String, String> userInformation) throws SQLException 835 { 836 // Build request for editing the user 837 String beginClause = "UPDATE " + _userTableName + " SET "; 838 String endClause = " WHERE " + __COLUMN_LOGIN + " = ?"; 839 840 StringBuffer columnNames = new StringBuffer(""); 841 842 boolean passwordUpdate = false; 843 for (String id : userInformation.keySet()) 844 { 845 if (ArrayUtils.contains(__COLUMNS, id) && !"login".equals(id) && !("password".equals(id) && userInformation.get(id) == null)) 846 { 847 if ("password".equals(id)) 848 { 849 passwordUpdate = true; 850 } 851 852 if (columnNames.length() > 0) 853 { 854 columnNames.append(", "); 855 } 856 columnNames.append(id + " = ?"); 857 } 858 } 859 860 if (passwordUpdate) 861 { 862 columnNames.append(", " + __COLUMN_SALT + " = ?"); 863 } 864 865 String sqlRequest = beginClause + columnNames.toString() + endClause; 866 if (getLogger().isDebugEnabled()) 867 { 868 getLogger().debug(sqlRequest); 869 } 870 871 PreparedStatement stmt = con.prepareStatement(sqlRequest); 872 _fillModifyStatement(stmt, userInformation); 873 874 return stmt; 875 } 876 877 /** 878 * Fill the statement with the user informations 879 * @param stmt The statement of the sql request 880 * @param userInformation the user informations 881 * @throws SQLException if an error occurred 882 */ 883 protected void _fillModifyStatement(PreparedStatement stmt, Map<String, String> userInformation) throws SQLException 884 { 885 int index = 1; 886 887 boolean clearText = !userInformation.containsKey("clearText") || !"false".equals(userInformation.get("clearText")) || !userInformation.containsKey(__COLUMN_SALT); 888 String generatedSaltKey = clearText ? RandomStringUtils.randomAlphanumeric(48) : userInformation.get(__COLUMN_SALT); 889 890 boolean passwordUpdate = false; 891 892 for (String id : userInformation.keySet()) 893 { 894 if (ArrayUtils.contains(__COLUMNS, id) && !"login".equals(id)) 895 { 896 if ("password".equals(id)) 897 { 898 if (userInformation.get(id) != null) 899 { 900 String encryptedPassword; 901 if (clearText) 902 { 903 encryptedPassword = DigestUtils.sha512Hex(generatedSaltKey + userInformation.get(id)); 904 if (encryptedPassword == null) 905 { 906 String message = "Cannot encrypt password"; 907 getLogger().error(message); 908 throw new SQLException(message); 909 } 910 } 911 else 912 { 913 encryptedPassword = userInformation.get(id); 914 } 915 stmt.setString(index++, encryptedPassword); 916 passwordUpdate = true; 917 } 918 } 919 else 920 { 921 stmt.setString(index++, userInformation.get(id)); 922 } 923 } 924 } 925 926 if (passwordUpdate) 927 { 928 stmt.setString(index++, generatedSaltKey); 929 } 930 931 stmt.setString(index++, userInformation.get("login")); 932 } 933 934 /** 935 * Populate the user list with the result set 936 * @param rs The result set 937 * @param isLogin true for login, false for email 938 * @return The user list 939 * @throws SQLException If an SQL exception occurs 940 */ 941 protected List<User> _getUsersProcessResultSet(ResultSet rs, boolean isLogin) throws SQLException 942 { 943 List<User> users = new ArrayList<>(); 944 945 while (rs.next()) 946 { 947 User user = null; 948 949 // Try to get in cache 950 Cache<String, User> cache = null; 951 if (isCachingEnabled()) 952 { 953 if (isLogin) 954 { 955 cache = getCacheByLogin(); 956 String login = rs.getString(__COLUMN_LOGIN); 957 user = cache.hasKey(login) ? cache.get(login) : null; 958 } 959 else 960 { 961 cache = getCacheByMail(); 962 String email = rs.getString(__COLUMN_EMAIL).toLowerCase(); 963 user = cache.hasKey(email) ? cache.get(email) : null; 964 } 965 } 966 967 // Or create from result set 968 if (user == null) 969 { 970 user = _createUserFromResultSet(rs); 971 972 if (cache != null) 973 { 974 if (isLogin) 975 { 976 cache.put(user.getIdentity().getLogin(), user); 977 } 978 else 979 { 980 cache.put(user.getEmail().toLowerCase(), user); // Email should not be empty here, since user was searched by email 981 } 982 } 983 } 984 985 users.add(user); 986 } 987 988 return users; 989 } 990 991 /** 992 * Create the user implementation from the result set of the request 993 * 994 * @param rs The result set where you can use get methods 995 * @return The user reflecting the current cursor position in the result set 996 * @throws SQLException if an error occurred 997 */ 998 protected User _createUserFromResultSet(ResultSet rs) throws SQLException 999 { 1000 String login = rs.getString(__COLUMN_LOGIN); 1001 String lastName = rs.getString(__COLUMN_LASTNAME); 1002 String firstName = rs.getString(__COLUMN_FIRSTNAME); 1003 String email = rs.getString(__COLUMN_EMAIL); 1004 1005 return new User(new UserIdentity(login, getPopulationId()), lastName, firstName, email, this); 1006 } 1007 1008 /** 1009 * Retrieve an user from a result set 1010 * @param rs The result set 1011 * @param login The user login 1012 * @param cache the cache to use. Is null if caching is not enabled 1013 * @return The retrieved user or null if not found 1014 * @throws SQLException If an SQL Exception occurs 1015 */ 1016 protected User _getUserProcessResultSet(ResultSet rs, String login, Cache<String, User> cache) throws SQLException 1017 { 1018 if (rs.next()) 1019 { 1020 // Retrieve information of the user 1021 User user = _createUserFromResultSet(rs); 1022 1023 if (isCachingEnabled()) 1024 { 1025 cache.put(login, user); 1026 } 1027 1028 return user; 1029 } 1030 else 1031 { 1032 // no user with this login in the database 1033 return null; 1034 } 1035 } 1036 1037 @Override 1038 public View getView() 1039 { 1040 if (_view == null) 1041 { 1042 _view = View.of(this, __COLUMNS); 1043 } 1044 return _view; 1045 } 1046 1047 // ------------------------------ 1048 // INNER CLASSE 1049 // ------------------------------ 1050 /** 1051 * An internal query executor. 1052 * @param <T> The type of the queried object 1053 */ 1054 protected abstract class AbstractJdbcQueryExecutor<T> 1055 { 1056 /** 1057 * Main function, run the query process. Will not throw exception. Use 1058 * runWithException to catch non SQL exception thrown by 1059 * {@link #processResultSet(ResultSet)} 1060 * @return The queried object or null 1061 */ 1062 @SuppressWarnings("synthetic-access") 1063 public T run() 1064 { 1065 try 1066 { 1067 return runWithException(); 1068 } 1069 catch (Exception e) 1070 { 1071 getLogger().error("Exception during a query execution", e); 1072 throw new RuntimeException("Exception during a query execution", e); 1073 } 1074 } 1075 1076 /** 1077 * Main function, run the query process. 1078 * @return The queried object or null 1079 * @throws Exception All non SQLException will be thrown 1080 */ 1081 @SuppressWarnings("synthetic-access") 1082 public T runWithException() throws Exception 1083 { 1084 Connection connection = null; 1085 PreparedStatement stmt = null; 1086 ResultSet rs = null; 1087 1088 try 1089 { 1090 connection = getSQLConnection(); 1091 1092 String sql = getSqlQuery(connection); 1093 1094 if (getLogger().isDebugEnabled()) 1095 { 1096 getLogger().debug("Executing SQL query: " + sql); 1097 } 1098 1099 stmt = prepareStatement(connection, sql); 1100 rs = executeQuery(stmt); 1101 1102 return processResultSet(rs); 1103 } 1104 catch (SQLException e) 1105 { 1106 getLogger().error("Error during the communication with the database", e); 1107 throw new RuntimeException("Error during the communication with the database", e); 1108 } 1109 finally 1110 { 1111 ConnectionHelper.cleanup(rs); 1112 ConnectionHelper.cleanup(stmt); 1113 ConnectionHelper.cleanup(connection); 1114 } 1115 } 1116 1117 /** 1118 * Must return the SQL query to execute 1119 * @param connection The pool connection 1120 * @return The SQL query 1121 */ 1122 protected abstract String getSqlQuery(Connection connection); 1123 1124 /** 1125 * Prepare the statement to execute 1126 * @param connection The pool connection 1127 * @param sql The SQL query 1128 * @return The prepared statement, ready to be executed 1129 * @throws SQLException If an SQL Exception occurs 1130 */ 1131 protected PreparedStatement prepareStatement(Connection connection, String sql) throws SQLException 1132 { 1133 return connection.prepareStatement(sql); 1134 } 1135 1136 /** 1137 * Execute the prepared statement and retrieves the result set. 1138 * @param stmt The prepared statement 1139 * @return The result set 1140 * @throws SQLException If an SQL Exception occurs 1141 */ 1142 protected ResultSet executeQuery(PreparedStatement stmt) throws SQLException 1143 { 1144 return stmt.executeQuery(); 1145 } 1146 1147 /** 1148 * Process the result set 1149 * @param rs The result set 1150 * @return The queried object or null 1151 * @throws SQLException If an SQL exception occurs 1152 * @throws Exception Other exception will be thrown when using {@link #runWithException()} 1153 */ 1154 protected T processResultSet(ResultSet rs) throws SQLException, Exception 1155 { 1156 return null; 1157 } 1158 } 1159 1160 /** 1161 * Query executor in order to select an user 1162 * @param <T> The type of the queried object 1163 */ 1164 protected class SelectUserJdbcQueryExecutor<T> extends AbstractJdbcQueryExecutor<T> 1165 { 1166 /** The user login */ 1167 protected String _value; 1168 /** The search column */ 1169 protected String _searchColumn; 1170 1171 /** 1172 * The constructor 1173 * @param value The strict value to search for 1174 */ 1175 protected SelectUserJdbcQueryExecutor(String value) 1176 { 1177 _value = value; 1178 _searchColumn = __COLUMN_LOGIN; 1179 } 1180 1181 /** 1182 * The constructor 1183 * @param value The strict value to search for 1184 * @param searchColumn The name of search column 1185 */ 1186 protected SelectUserJdbcQueryExecutor(String value, String searchColumn) 1187 { 1188 _value = value; 1189 _searchColumn = searchColumn; 1190 } 1191 1192 @Override 1193 protected String getSqlQuery(Connection connection) 1194 { 1195 // Build SQL request 1196 StringBuilder selectClause = new StringBuilder(); 1197 for (String id : __COLUMNS) 1198 { 1199 if (selectClause.length() > 0) 1200 { 1201 selectClause.append(", "); 1202 } 1203 selectClause.append(id); 1204 } 1205 1206 StringBuilder sql = new StringBuilder("SELECT "); 1207 sql.append(selectClause).append(" FROM ").append(_userTableName); 1208 sql.append(" WHERE ").append(_searchColumn).append(" = ?"); 1209 1210 return sql.toString(); 1211 } 1212 1213 @Override 1214 protected PreparedStatement prepareStatement(Connection connection, String sql) throws SQLException 1215 { 1216 PreparedStatement stmt = super.prepareStatement(connection, sql); 1217 1218 stmt.setString(1, _value); 1219 return stmt; 1220 } 1221 } 1222 1223 /** 1224 * Query executor in order to select users 1225 * @param <T> The type of the queried object 1226 */ 1227 protected class SelectUsersJdbcQueryExecutor<T> extends AbstractJdbcQueryExecutor<T> 1228 { 1229 /** The pattern to match (none if null) */ 1230 protected String _pattern; 1231 /** The maximum number of users to select */ 1232 protected int _length; 1233 /** The offset to start with, first is 0 */ 1234 protected int _offset; 1235 1236 /** The mandatory predicate to use when querying users by pattern */ 1237 protected JdbcPredicate _mandatoryPredicate; 1238 /** The pattern to match, extracted from the pattern */ 1239 protected String _patternToMatch; 1240 1241 /** 1242 * The constructor 1243 * @param pattern The pattern to match (none if null). 1244 * @param length The maximum number of users to select. 1245 * @param offset The offset to start with, first is 0. 1246 */ 1247 protected SelectUsersJdbcQueryExecutor(String pattern, int length, int offset) 1248 { 1249 _pattern = pattern; 1250 _length = length; 1251 _offset = offset; 1252 } 1253 1254 @Override 1255 protected String getSqlQuery(Connection connection) 1256 { 1257 // Build SQL request 1258 StringBuilder selectClause = new StringBuilder(); 1259 for (String column : __COLUMNS) 1260 { 1261 if (selectClause.length() > 0) 1262 { 1263 selectClause.append(", "); 1264 } 1265 selectClause.append(column); 1266 } 1267 1268 StringBuilder sql = new StringBuilder("SELECT "); 1269 sql.append(selectClause).append(" FROM ").append(_userTableName); 1270 1271 // Add the pattern 1272 _mandatoryPredicate = _getMandatoryPredicate(_pattern); 1273 if (_mandatoryPredicate != null) 1274 { 1275 sql.append(" WHERE ").append(_mandatoryPredicate.getPredicate()); 1276 } 1277 1278 _patternToMatch = _getPatternToMatch(_pattern); 1279 if (_patternToMatch != null) 1280 { 1281 if (ConnectionHelper.DATABASE_DERBY.equals(ConnectionHelper.getDatabaseType(connection))) 1282 { 1283 // The LIKE operator in Derby is case sensitive 1284 sql.append(_mandatoryPredicate != null ? " AND (" : " WHERE ") 1285 .append("UPPER(").append(__COLUMN_LOGIN).append(") LIKE UPPER(?) OR ") 1286 .append("UPPER(").append(__COLUMN_LASTNAME).append(") LIKE UPPER(?) OR ") 1287 .append("UPPER(").append(__COLUMN_FIRSTNAME).append(") LIKE UPPER(?)"); 1288 } 1289 else 1290 { 1291 sql.append(_mandatoryPredicate != null ? " AND (" : " WHERE ") 1292 .append(__COLUMN_LOGIN).append(" LIKE ? OR ") 1293 .append(__COLUMN_LASTNAME).append(" LIKE ? OR ") 1294 .append(__COLUMN_FIRSTNAME).append(" LIKE ?"); 1295 } 1296 1297 if (_mandatoryPredicate != null) 1298 { 1299 sql.append(')'); 1300 } 1301 } 1302 1303 StringBuilder orderByClause = new StringBuilder(); 1304 for (String column : __ORDERBY_COLUMNS) 1305 { 1306 orderByClause.append(orderByClause.length() == 0 ? " ORDER BY " : ", "); 1307 orderByClause.append(column); 1308 } 1309 1310 sql.append(orderByClause); 1311 1312 // Add length filters 1313 sql = _addQuerySize(_length, _offset, connection, selectClause, sql); 1314 1315 return sql.toString(); 1316 } 1317 1318 @SuppressWarnings("synthetic-access") 1319 private StringBuilder _addQuerySize(int length, int offset, Connection con, StringBuilder selectClause, StringBuilder sql) 1320 { 1321 // Do not add anything if not necessary 1322 if (length == Integer.MAX_VALUE && offset == 0) 1323 { 1324 return sql; 1325 } 1326 1327 String dbType = ConnectionHelper.getDatabaseType(con); 1328 1329 if (ConnectionHelper.DATABASE_MYSQL.equals(dbType) || ConnectionHelper.DATABASE_POSTGRES.equals(dbType) || ConnectionHelper.DATABASE_HSQLDB.equals(dbType)) 1330 { 1331 sql.append(" LIMIT " + length + " OFFSET " + offset); 1332 return sql; 1333 } 1334 else if (ConnectionHelper.DATABASE_ORACLE.equals(dbType)) 1335 { 1336 return new StringBuilder("select " + selectClause.toString() + " from (select rownum r, " + selectClause.toString() + " from (" + sql.toString() 1337 + ")) where r BETWEEN " + (offset + 1) + " AND " + (offset + length)); 1338 } 1339 else if (ConnectionHelper.DATABASE_DERBY.equals(dbType)) 1340 { 1341 return new StringBuilder("select ").append(selectClause) 1342 .append(" from (select ROW_NUMBER() OVER () AS ROWNUM, ").append(selectClause.toString()) 1343 .append(" from (").append(sql.toString()).append(") AS TR ) AS TRR where ROWNUM BETWEEN ") 1344 .append(offset + 1).append(" AND ").append(offset + length); 1345 } 1346 else if (getLogger().isWarnEnabled()) 1347 { 1348 getLogger().warn("The request will not have the limit and offset set, since its type is unknown"); 1349 } 1350 1351 return sql; 1352 } 1353 1354 @Override 1355 protected PreparedStatement prepareStatement(Connection connection, String sql) throws SQLException 1356 { 1357 PreparedStatement stmt = super.prepareStatement(connection, sql); 1358 1359 int i = 1; 1360 // Value the parameters if there is a pattern 1361 if (_mandatoryPredicate != null) 1362 { 1363 for (String value : _mandatoryPredicate.getValues()) 1364 { 1365 stmt.setString(i++, value); 1366 } 1367 } 1368 1369 if (_patternToMatch != null) 1370 { 1371 // One for the login, one for the lastname. 1372 stmt.setString(i++, _patternToMatch); 1373 stmt.setString(i++, _patternToMatch); 1374 // FIXME 1375 //if (_parameters.containsKey("firstname")) 1376 //{ 1377 stmt.setString(i++, _patternToMatch); 1378 //} 1379 } 1380 1381 return stmt; 1382 } 1383 } 1384 1385 /** 1386 * Class representing a SQL predicate (to use in a WHERE or HAVING clause), 1387 * with optional string parameters. 1388 */ 1389 public class JdbcPredicate 1390 { 1391 1392 /** The predicate string with optional "?" placeholders. */ 1393 protected String _predicate; 1394 /** The predicate parameter values. */ 1395 protected List<String> _predicateParamValues; 1396 1397 /** 1398 * Build a JDBC predicate. 1399 * @param predicate the predicate string. 1400 * @param values the parameter values. 1401 */ 1402 public JdbcPredicate(String predicate, String... values) 1403 { 1404 this(predicate, Arrays.asList(values)); 1405 } 1406 1407 /** 1408 * Build a JDBC predicate. 1409 * @param predicate the predicate string. 1410 * @param values the parameter values. 1411 */ 1412 public JdbcPredicate(String predicate, List<String> values) 1413 { 1414 this._predicate = predicate; 1415 this._predicateParamValues = values; 1416 } 1417 1418 /** 1419 * Get the predicate. 1420 * @return the predicate 1421 */ 1422 public String getPredicate() 1423 { 1424 return _predicate; 1425 } 1426 1427 /** 1428 * Set the predicate. 1429 * @param predicate the predicate to set 1430 */ 1431 public void setPredicate(String predicate) 1432 { 1433 this._predicate = predicate; 1434 } 1435 1436 /** 1437 * Get the parameter values. 1438 * @return the parameter values. 1439 */ 1440 public List<String> getValues() 1441 { 1442 return _predicateParamValues; 1443 } 1444 1445 /** 1446 * Set the parameter values. 1447 * @param values the parameter values to set. 1448 */ 1449 public void setValues(List<String> values) 1450 { 1451 this._predicateParamValues = values; 1452 } 1453 } 1454}