=== modified file 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/CodeGenerator.java' --- dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/CodeGenerator.java 2011-11-25 18:32:06 +0000 +++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/CodeGenerator.java 2012-10-29 14:34:57 +0000 @@ -43,21 +43,34 @@ public static final int CODESIZE = 11; /** - * Generates a pseudo random string using the allowed characters + * Generates a pseudo random string using the allowed characters. Code is + * 11 characters long. * - * @return the Code + * @param codeSize the number of characters in the code. + * @return the code. */ public static String generateCode() { + return generateCode( CODESIZE ); + } + + /** + * Generates a pseudo random string using the allowed characters. + * + * @param codeSize the number of characters in the code. + * @return the code. + */ + public static String generateCode( int codeSize ) + { // Using the system default algorithm and seed SecureRandom sr = new SecureRandom(); - char[] randomChars = new char[CODESIZE]; + char[] randomChars = new char[codeSize]; // first char should be a letter randomChars[0] = letters.charAt( sr.nextInt( letters.length() ) ); - for ( int i = 1; i < CODESIZE; ++i ) + for ( int i = 1; i < codeSize; ++i ) { randomChars[i] = allowedChars.charAt( sr.nextInt( NUMBER_OF_CODEPOINTS ) ); } === modified file 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserCredentials.java' --- dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserCredentials.java 2012-10-07 19:07:53 +0000 +++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserCredentials.java 2012-10-29 14:34:57 +0000 @@ -27,24 +27,25 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +import java.io.Serializable; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +import org.hisp.dhis.common.BaseIdentifiableObject; +import org.hisp.dhis.common.Dxf2Namespace; +import org.hisp.dhis.common.IdentifiableObjectUtils; +import org.hisp.dhis.common.view.DetailedView; +import org.hisp.dhis.common.view.ExportView; +import org.hisp.dhis.dataset.DataSet; + import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; -import org.hisp.dhis.common.BaseIdentifiableObject; -import org.hisp.dhis.common.Dxf2Namespace; -import org.hisp.dhis.common.IdentifiableObjectUtils; -import org.hisp.dhis.common.view.DetailedView; -import org.hisp.dhis.common.view.ExportView; -import org.hisp.dhis.dataset.DataSet; - -import java.io.Serializable; -import java.util.Collection; -import java.util.Date; -import java.util.HashSet; -import java.util.Set; /** * @author Nguyen Hong Duc @@ -71,13 +72,34 @@ private String username; /** - * Required. + * Required. Will be stored as a hash. */ private String password; + /** + * Set of user roles. + */ private Set userAuthorityGroups = new HashSet(); + /** + * Date of the user's last login. + */ private Date lastLogin; + + /** + * The token used for a user account restore. Will be stored as a hash. + */ + private String restoreToken; + + /** + * The code used for a user account restore. Will be stored as a hash. + */ + private String restoreCode; + + /** + * The timestamp representing when the restore window expires. + */ + private Date restoreExpiry; // ------------------------------------------------------------------------- // Logic @@ -200,6 +222,39 @@ return user != null ? user.getName() : username; } + /** + * Tests whether the given input arguments can perform a valid restore of the + * user account for these credentials. Returns false if any of the input arguments + * are null, or any of the properties on the credentials are null. Returns false + * if the expiry date arguement is after the expiry date of the credentials. + * Returns false if any of the given token or code arguments are not equal to + * the respective properties the the credentials. Returns true otherwise. + * + * @param token the restore token. + * @param code the restore code. + * @param expiry the expiry date. + * @return true or false. + */ + public boolean canRestore( String token, String code, Date date ) + { + if ( this.restoreToken == null || this.restoreCode == null || this.restoreExpiry == null ) + { + return false; + } + + if ( token == null || code == null || date == null ) + { + return false; + } + + if ( date.after( this.restoreExpiry ) ) + { + return false; + } + + return token.equals( this.restoreToken ) && code.equals( this.restoreCode ); + } + // ------------------------------------------------------------------------- // hashCode and equals // ------------------------------------------------------------------------- @@ -313,4 +368,34 @@ { this.lastLogin = lastLogin; } + + public String getRestoreToken() + { + return restoreToken; + } + + public void setRestoreToken( String restoreToken ) + { + this.restoreToken = restoreToken; + } + + public String getRestoreCode() + { + return restoreCode; + } + + public void setRestoreCode( String restoreCode ) + { + this.restoreCode = restoreCode; + } + + public Date getRestoreExpiry() + { + return restoreExpiry; + } + + public void setRestoreExpiry( Date restoreExpiry ) + { + this.restoreExpiry = restoreExpiry; + } } === added file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/DefaultSecurityService.java' --- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/DefaultSecurityService.java 1970-01-01 00:00:00 +0000 +++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/DefaultSecurityService.java 2012-10-29 14:34:57 +0000 @@ -0,0 +1,190 @@ +package org.hisp.dhis.security; + +/* + * Copyright (c) 2004-2012, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.hisp.dhis.common.CodeGenerator; +import org.hisp.dhis.message.MessageSender; +import org.hisp.dhis.period.Cal; +import org.hisp.dhis.system.velocity.VelocityManager; +import org.hisp.dhis.user.User; +import org.hisp.dhis.user.UserCredentials; +import org.hisp.dhis.user.UserService; + +/** + * @author Lars Helge Overland + */ +public class DefaultSecurityService + implements SecurityService +{ + private static final String RESTORE_PATH = "/dhis-web-commons/security/restore.action"; + + // ------------------------------------------------------------------------- + // Dependencies + // ------------------------------------------------------------------------- + + private PasswordManager passwordManager; + + public void setPasswordManager( PasswordManager passwordManager ) + { + this.passwordManager = passwordManager; + } + + private MessageSender emailMessageSender; + + public void setEmailMessageSender( MessageSender emailMessageSender ) + { + this.emailMessageSender = emailMessageSender; + } + + private UserService userService; + + public void setUserService( UserService userService ) + { + this.userService = userService; + } + + // ------------------------------------------------------------------------- + // SecurityService implementation + // ------------------------------------------------------------------------- + + public boolean sendRestoreMessage( String username, String rootPath ) + { + if ( username == null || rootPath == null ) + { + return false; + } + + UserCredentials credentials = userService.getUserCredentialsByUsername( username ); + + if ( credentials == null ) + { + return false; + } + + // TODO check if email is configured + + String[] result = initRestore( credentials ); + + Set users = new HashSet(); + users.add( credentials.getUser() ); + + Map vars = new HashMap(); + vars.put( "rootPath", rootPath ); + vars.put( "restorePath", rootPath + RESTORE_PATH ); + vars.put( "token", result[0] ); + vars.put( "code", result[1] ); + vars.put( "username", username ); + + String text1 = new VelocityManager().render( vars, "restore_message1.vm" ); + String text2 = new VelocityManager().render( vars, "restore_message2.vm" ); + + emailMessageSender.sendMessage( "User account restore confirmation (message 1 of 2)", text1, null, users ); + emailMessageSender.sendMessage( "User account restore confirmation (message 2 of 2)", text2, null, users ); + + return true; + } + + public String[] initRestore( UserCredentials credentials ) + { + String token = CodeGenerator.generateCode( 40 ); + String code = CodeGenerator.generateCode( 15 ); + + String hashedToken = passwordManager.encodePassword( credentials.getUsername(), token ); + String hashedCode = passwordManager.encodePassword( credentials.getUsername(), code ); + + Date expiry = new Cal().now().add( Calendar.HOUR_OF_DAY, 1 ).time(); + + credentials.setRestoreToken( hashedToken ); + credentials.setRestoreCode( hashedCode ); + credentials.setRestoreExpiry( expiry ); + + userService.updateUserCredentials( credentials ); + + String[] result = { token, code }; + return result; + } + + public boolean restore( String username, String token, String code, String newPassword ) + { + if ( username == null || token == null || code == null || newPassword == null ) + { + return false; + } + + UserCredentials credentials = userService.getUserCredentialsByUsername( username ); + + if ( credentials == null ) + { + return false; + } + + token = passwordManager.encodePassword( username, token ); + code = passwordManager.encodePassword( username, code ); + + Date date = new Cal().now().time(); + + if ( !credentials.canRestore( token, code, date ) ) + { + return false; + } + + newPassword = passwordManager.encodePassword( username, newPassword ); + + credentials.setPassword( newPassword ); + + userService.updateUserCredentials( credentials ); + + return true; + } + + public boolean verifyToken( String username, String token ) + { + if ( username == null || token == null ) + { + return false; + } + + UserCredentials credentials = userService.getUserCredentialsByUsername( username ); + + if ( credentials == null || credentials.getRestoreToken() == null ) + { + return false; + } + + token = passwordManager.encodePassword( username, token ); + + return credentials.getRestoreToken().equals( token ); + } +} === added file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/SecurityService.java' --- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/SecurityService.java 1970-01-01 00:00:00 +0000 +++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/SecurityService.java 2012-10-29 14:34:57 +0000 @@ -0,0 +1,87 @@ +package org.hisp.dhis.security; + +/* + * Copyright (c) 2004-2012, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import org.hisp.dhis.user.UserCredentials; + +/** + * @author Lars Helge Overland + */ +public interface SecurityService +{ + /** + * Will invoke the initiateRestore method and dispatch email messages with + * restore information to the user. + * + * @param username the user name of the user to send restore messages. + * @param rootPath the root path of the request. + * @return false if any of the arguments are null or if the user credentials + * identified by the user name does not exist, true otherwise. + */ + boolean sendRestoreMessage( String username, String rootPath ); + + /** + * Will populate the restoreToken and restoreCode property of the given + * credentials with a hashed version of auto-generated values. Will set the + * restoreExpiry property with a date time one hour from now. Changes will be + * persisted. + * + * @param credentials the user credentials. + * @return an array where index 0 is the clear-text token and index 1 the + * clear-text code. + */ + String[] initRestore( UserCredentials credentials ); + + /** + * Tests whether the given token and code are valid for the given user name. + * If true, it will update the user credentials identified by the given user + * name with the new password. In order to succeed, the given token and code + * must match the ones on the credentials, and the current date must be before + * the expiry date time of the credentials. + * + * @param username the user name. + * @param token the token. + * @param code the code. + * @param newPassword the proposed new password. + * @return true or false. + */ + boolean restore( String username, String token, String code, String newPassword ); + + /** + * Tests whether the given token in combination with the given user name is + * valid, i.e. whether the hashed version of the token matches the one on the + * user credentials identified by the given user name. + * + * @param username the user name. + * @param token the token. + * @return false if any of the arguments are null or if the user credentials + * identified by the user name does not exist, true if the arguments + * are valid. + */ + boolean verifyToken( String username, String token ); +} === modified file 'dhis-2/dhis-services/dhis-service-core/src/main/resources/META-INF/dhis/security.xml' --- dhis-2/dhis-services/dhis-service-core/src/main/resources/META-INF/dhis/security.xml 2012-03-05 14:43:22 +0000 +++ dhis-2/dhis-services/dhis-service-core/src/main/resources/META-INF/dhis/security.xml 2012-10-29 14:34:57 +0000 @@ -16,6 +16,12 @@ + + + + + + === modified file 'dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/user/hibernate/UserCredentials.hbm.xml' --- dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/user/hibernate/UserCredentials.hbm.xml 2011-06-11 19:51:41 +0000 +++ dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/user/hibernate/UserCredentials.hbm.xml 2012-10-29 14:34:57 +0000 @@ -30,7 +30,13 @@ - + + + + + + + === added file 'dhis-2/dhis-services/dhis-service-core/src/main/resources/restore_message1.vm' --- dhis-2/dhis-services/dhis-service-core/src/main/resources/restore_message1.vm 1970-01-01 00:00:00 +0000 +++ dhis-2/dhis-services/dhis-service-core/src/main/resources/restore_message1.vm 2012-10-29 14:34:57 +0000 @@ -0,0 +1,7 @@ +Someone, probably you, have asked us to restore your useraccount at ${basePath}. +You have been sent two emails. This is the first email of those two. Please follow +the link below this text. In the next step you will be asked to enter a code +which is sent to you in the other email. You must complete the restore within 1 +hour. + +${restorePath}?username=${username}&token=${token} === added file 'dhis-2/dhis-services/dhis-service-core/src/main/resources/restore_message2.vm' --- dhis-2/dhis-services/dhis-service-core/src/main/resources/restore_message2.vm 1970-01-01 00:00:00 +0000 +++ dhis-2/dhis-services/dhis-service-core/src/main/resources/restore_message2.vm 2012-10-29 14:34:57 +0000 @@ -0,0 +1,7 @@ +Someone, probably you, have asked us to restore your useraccount at ${basePath}. +You have been sent two emails. This is the second email of those two. Please +read the first email and follow the instructions. If you already have done that, +please use the code below to complete the account restore form. You must complete +the restore within 1 hour. + +${code} === added directory 'dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security' === added file 'dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/SecurityServiceTest.java' --- dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/SecurityServiceTest.java 1970-01-01 00:00:00 +0000 +++ dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/SecurityServiceTest.java 2012-10-29 14:34:57 +0000 @@ -0,0 +1,94 @@ +package org.hisp.dhis.security; + +/* + * Copyright (c) 2004-2012, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import static junit.framework.Assert.*; + +import org.hisp.dhis.DhisSpringTest; +import org.hisp.dhis.user.User; +import org.hisp.dhis.user.UserCredentials; +import org.hisp.dhis.user.UserService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @author Lars Helge Overland + */ +public class SecurityServiceTest + extends DhisSpringTest +{ + private UserCredentials credentials; + + @Autowired + private UserService userService; + + @Autowired + private PasswordManager passwordManager; + + @Autowired + private SecurityService securityService; + + @Override + public void setUpTest() + { + credentials = new UserCredentials(); + credentials.setUsername( "johndoe" ); + credentials.setPassword( "" ); + + User user = createUser( 'A' ); + user.setUserCredentials( credentials ); + credentials.setUser( user ); + userService.addUserCredentials( credentials ); + } + + @Test + public void testRestore() + { + String[] result = securityService.initRestore( credentials ); + + assertNotNull( result[0] ); + assertNotNull( result[1] ); + assertNotNull( credentials.getRestoreToken() ); + assertNotNull( credentials.getRestoreCode() ); + assertNotNull( credentials.getRestoreExpiry() ); + + boolean verified = securityService.verifyToken( credentials.getUsername(), result[0] ); + + assertTrue( verified ); + + String password = "NewPassword1"; + + boolean restored = securityService.restore( credentials.getUsername(), result[0], result[1], password ); + + assertTrue( restored ); + + String hashedPassword = passwordManager.encodePassword( credentials.getUsername(), password ); + + assertEquals( hashedPassword, credentials.getPassword() ); + } +}