From cb237055ac0a650f329f94d9196a78f22adf035b Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Tue, 13 Nov 2007 17:11:29 +0000 Subject: [PATCH] SEC-600: Added Jdbc implementation of UserDetailsManager --- .../userdetails/UserDetailsManager.java | 5 +- .../jdbc/JdbcUserDetailsManager.java | 270 ++++++++++++++++++ .../jdbc/JdbcUserDetailsManagerTests.java | 175 ++++++++++++ 3 files changed, 448 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/org/springframework/security/userdetails/jdbc/JdbcUserDetailsManager.java create mode 100644 core/src/test/java/org/springframework/security/userdetails/jdbc/JdbcUserDetailsManagerTests.java diff --git a/core/src/main/java/org/springframework/security/userdetails/UserDetailsManager.java b/core/src/main/java/org/springframework/security/userdetails/UserDetailsManager.java index e4a45bc637..1daea3a711 100644 --- a/core/src/main/java/org/springframework/security/userdetails/UserDetailsManager.java +++ b/core/src/main/java/org/springframework/security/userdetails/UserDetailsManager.java @@ -26,8 +26,9 @@ public interface UserDetailsManager extends UserDetailsService { void deleteUser(String username); /** - * Modify the current user's password. - * + * Modify the current user's password. This should change the user's password in + * the persistent user repository (datbase, LDAP etc) and should also modify the + * current security context to contain the new password. * * @param oldPassword current password (for re-authentication if required) * @param newPassword the password to change to diff --git a/core/src/main/java/org/springframework/security/userdetails/jdbc/JdbcUserDetailsManager.java b/core/src/main/java/org/springframework/security/userdetails/jdbc/JdbcUserDetailsManager.java new file mode 100644 index 0000000000..81b2476d8a --- /dev/null +++ b/core/src/main/java/org/springframework/security/userdetails/jdbc/JdbcUserDetailsManager.java @@ -0,0 +1,270 @@ +package org.springframework.security.userdetails.jdbc; + +import org.springframework.security.AccessDeniedException; +import org.springframework.security.Authentication; +import org.springframework.security.AuthenticationException; +import org.springframework.security.AuthenticationManager; +import org.springframework.security.context.SecurityContextHolder; +import org.springframework.security.providers.UsernamePasswordAuthenticationToken; +import org.springframework.security.userdetails.UserDetails; +import org.springframework.security.userdetails.UserDetailsManager; +import org.springframework.context.ApplicationContextException; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.object.MappingSqlQuery; +import org.springframework.jdbc.object.SqlQuery; +import org.springframework.jdbc.object.SqlUpdate; +import org.springframework.util.Assert; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import javax.sql.DataSource; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.List; + +/** + * Jdbc user management service. + * + * @author Luke Taylor + * @version $Id$ + */ +public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsManager { + //~ Static fields/initializers ===================================================================================== + + public static final String DEF_CREATE_USER_SQL = + "insert into users (username, password, enabled) values (?,?,?)"; + public static final String DEF_DELETE_USER_SQL = + "delete from users where username = ?"; + public static final String DEF_UPDATE_USER_SQL = + "update users set password = ?, enabled = ? where username = ?"; + public static final String DEF_INSERT_AUTHORITY_SQL = + "insert into authorities (username, authority) values (?,?)"; + public static final String DEF_DELETE_USER_AUTHORITIES_SQL = + "delete from authorities where username = ?"; + public static final String DEF_USER_EXISTS_SQL = + "select username from users where username = ?"; + public static final String DEF_CHANGE_PASSWORD_SQL = + "update users set password = ? where username = ?"; + + + //~ Instance fields ================================================================================================ + + protected final Log logger = LogFactory.getLog(getClass()); + + private String createUserSql = DEF_CREATE_USER_SQL; + private String deleteUserSql = DEF_DELETE_USER_SQL; + private String updateUserSql = DEF_UPDATE_USER_SQL; + private String createAuthoritySql = DEF_INSERT_AUTHORITY_SQL; + private String deleteUserAuthoritiesSql = DEF_DELETE_USER_AUTHORITIES_SQL; + private String userExistsSql = DEF_USER_EXISTS_SQL; + private String changePasswordSql = DEF_CHANGE_PASSWORD_SQL; + + protected SqlUpdate insertUser; + protected SqlUpdate deleteUser; + protected SqlUpdate updateUser; + protected SqlUpdate insertAuthority; + protected SqlUpdate deleteUserAuthorities; + protected SqlQuery userExistsQuery; + protected SqlUpdate changePassword; + + private AuthenticationManager authenticationManager; + + //~ Methods ======================================================================================================== + + protected void initDao() throws ApplicationContextException { + if (authenticationManager == null) { + logger.info("No authentication manager set. Reauthentication of users when changing passwords will" + + "not be performed."); + } + + insertUser = new InsertUser(getDataSource()); + deleteUser = new DeleteUser(getDataSource()); + updateUser = new UpdateUser(getDataSource()); + insertAuthority = new InsertAuthority(getDataSource()); + deleteUserAuthorities = new DeleteUserAuthorities(getDataSource()); + userExistsQuery = new UserExistsQuery(getDataSource()); + changePassword = new ChangePassword(getDataSource()); + super.initDao(); + } + + public void createUser(UserDetails user) { + insertUser.update(new Object[] {user.getUsername(), user.getPassword(), user.isEnabled()}); + + for (int i=0; i < user.getAuthorities().length; i++) { + insertAuthority.update(user.getUsername(), user.getAuthorities()[i].getAuthority()); + } + } + + public void updateUser(UserDetails user) { + updateUser.update(new Object[] {user.getPassword(), user.isEnabled(), user.getUsername()}); + deleteUserAuthorities.update(user.getUsername()); + + for (int i=0; i < user.getAuthorities().length; i++) { + insertAuthority.update(user.getUsername(), user.getAuthorities()[i].getAuthority()); + } + } + + public void deleteUser(String username) { + deleteUserAuthorities.update(username); + deleteUser.update(username); + } + + public void changePassword(String oldPassword, String newPassword) throws AuthenticationException { + Authentication currentUser = SecurityContextHolder.getContext().getAuthentication(); + + if (currentUser == null) { + // This would indicate bad coding somewhere + throw new AccessDeniedException("Can't change password as no Authentication object found in context " + + "for current user."); + } + + String username = currentUser.getName(); + + // If an authentication manager has been set, reauthenticate the user with the supplied password. + if (authenticationManager != null) { + logger.debug("Reauthenticating user '"+ username + "' for password change request."); + + authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, oldPassword)); + } else { + logger.debug("No authentication manager set. Password won't be re-checked."); + } + + logger.debug("Changing password for user '"+ username + "'"); + + changePassword.update(new String[] {newPassword, username}); + + SecurityContextHolder.getContext().setAuthentication(createNewAuthentication(currentUser, newPassword)); + } + + protected Authentication createNewAuthentication(Authentication currentAuth, String newPassword) { + UserDetails user = loadUserByUsername(currentAuth.getName()); + + UsernamePasswordAuthenticationToken newAuthentication = + new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities()); + newAuthentication.setDetails(currentAuth.getDetails()); + + return newAuthentication; + } + + public boolean userExists(String username) { + List users = userExistsQuery.execute(username); + + if (users.size() > 1) { + throw new IllegalStateException("More than one user found with name '" + username + "'"); + } + + return users.size() == 1; + } + + public void setAuthenticationManager(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + //~ Inner Classes ================================================================================================== + + protected class InsertUser extends SqlUpdate { + + public InsertUser(DataSource ds) { + super(ds, createUserSql); + declareParameter(new SqlParameter(Types.VARCHAR)); + declareParameter(new SqlParameter(Types.VARCHAR)); + declareParameter(new SqlParameter(Types.BOOLEAN)); + compile(); + } + } + + protected class DeleteUser extends SqlUpdate { + public DeleteUser(DataSource ds) { + super(ds, deleteUserSql); + declareParameter(new SqlParameter(Types.VARCHAR)); + compile(); + } + } + + protected class InsertAuthority extends SqlUpdate { + public InsertAuthority(DataSource ds) { + super(ds, createAuthoritySql); + declareParameter(new SqlParameter(Types.VARCHAR)); + declareParameter(new SqlParameter(Types.VARCHAR)); + compile(); + } + } + + protected class DeleteUserAuthorities extends SqlUpdate { + public DeleteUserAuthorities(DataSource ds) { + super(ds, deleteUserAuthoritiesSql); + declareParameter(new SqlParameter(Types.VARCHAR)); + compile(); + } + } + + protected class UpdateUser extends SqlUpdate { + public UpdateUser(DataSource ds) { + super(ds, updateUserSql); + declareParameter(new SqlParameter(Types.VARCHAR)); + declareParameter(new SqlParameter(Types.BOOLEAN)); + declareParameter(new SqlParameter(Types.VARCHAR)); + compile(); + } + } + + protected class ChangePassword extends SqlUpdate { + public ChangePassword(DataSource ds) { + super(ds, changePasswordSql); + declareParameter(new SqlParameter(Types.VARCHAR)); + declareParameter(new SqlParameter(Types.VARCHAR)); + compile(); + } + } + + + protected class UserExistsQuery extends MappingSqlQuery { + + public UserExistsQuery(DataSource ds) { + super(ds, userExistsSql); + declareParameter(new SqlParameter(Types.VARCHAR)); + compile(); + } + + protected Object mapRow(ResultSet rs, int rowNum) throws SQLException { + return rs.getString(1); + } + } + + public void setCreateUserSql(String createUserSql) { + Assert.hasText(createUserSql); + this.createUserSql = createUserSql; + } + + public void setDeleteUserSql(String deleteUserSql) { + Assert.hasText(deleteUserSql); + this.deleteUserSql = deleteUserSql; + } + + public void setUpdateUserSql(String updateUserSql) { + Assert.hasText(updateUserSql); + this.updateUserSql = updateUserSql; + } + + public void setCreateAuthoritySql(String createAuthoritySql) { + Assert.hasText(createAuthoritySql); + this.createAuthoritySql = createAuthoritySql; + } + + public void setDeleteUserAuthoritiesSql(String deleteUserAuthoritiesSql) { + Assert.hasText(deleteUserAuthoritiesSql); + this.deleteUserAuthoritiesSql = deleteUserAuthoritiesSql; + } + + public void setUserExistsSql(String userExistsSql) { + Assert.hasText(userExistsSql); + this.userExistsSql = userExistsSql; + } + + public void setChangePasswordSql(String changePasswordSql) { + Assert.hasText(changePasswordSql); + this.changePasswordSql = changePasswordSql; + } +} diff --git a/core/src/test/java/org/springframework/security/userdetails/jdbc/JdbcUserDetailsManagerTests.java b/core/src/test/java/org/springframework/security/userdetails/jdbc/JdbcUserDetailsManagerTests.java new file mode 100644 index 0000000000..1d193e884f --- /dev/null +++ b/core/src/test/java/org/springframework/security/userdetails/jdbc/JdbcUserDetailsManagerTests.java @@ -0,0 +1,175 @@ +package org.springframework.security.userdetails.jdbc; + +import org.springframework.security.AccessDeniedException; +import org.springframework.security.BadCredentialsException; +import org.springframework.security.MockAuthenticationManager; +import org.springframework.security.context.SecurityContextHolder; +import org.springframework.security.providers.UsernamePasswordAuthenticationToken; +import org.springframework.security.userdetails.User; +import org.springframework.security.userdetails.UserDetails; +import org.springframework.security.util.AuthorityUtils; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import org.junit.After; +import org.junit.AfterClass; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Tests for {@link JdbcUserDetailsManager} + * + * @author Luke Taylor + * @version $Id$ + */ +public class JdbcUserDetailsManagerTests { + private static final String SELECT_JOE_SQL = "select * from users where username = 'joe'"; + private static final String SELECT_JOE_AUTHORITIES_SQL = "select * from authorities where username = 'joe'"; + + private static final UserDetails joe = new User("joe", "password", true, true, true, true, + AuthorityUtils.stringArrayToAuthorityArray(new String[]{"A","B","C"})); + + private static DriverManagerDataSource dataSource; + private JdbcUserDetailsManager manager; + private JdbcTemplate template; + + @BeforeClass + public static void createDataSource() { + dataSource = new DriverManagerDataSource("org.hsqldb.jdbcDriver", "jdbc:hsqldb:mem:tokenrepotest", "sa", ""); + } + + @AfterClass + public static void clearDataSource() { + dataSource = null; + } + + @Before + public void initializeManagerAndCreateTables() { + manager = new JdbcUserDetailsManager(); + manager.setDataSource(dataSource); + manager.setCreateUserSql(JdbcUserDetailsManager.DEF_CREATE_USER_SQL); + manager.setUpdateUserSql(JdbcUserDetailsManager.DEF_UPDATE_USER_SQL); + manager.setUserExistsSql(JdbcUserDetailsManager.DEF_USER_EXISTS_SQL); + manager.setCreateAuthoritySql(JdbcUserDetailsManager.DEF_INSERT_AUTHORITY_SQL); + manager.setDeleteUserAuthoritiesSql(JdbcUserDetailsManager.DEF_DELETE_USER_AUTHORITIES_SQL); + manager.setDeleteUserSql(JdbcUserDetailsManager.DEF_DELETE_USER_SQL); + manager.setChangePasswordSql(JdbcUserDetailsManager.DEF_CHANGE_PASSWORD_SQL); + manager.initDao(); + template = manager.getJdbcTemplate(); + + template.execute("create table users(username varchar(20) not null primary key," + + "password varchar(20) not null, enabled boolean not null)"); + template.execute("create table authorities (username varchar(20) not null, authority varchar(20) not null, " + + "constraint fk_authorities_users foreign key(username) references users(username))"); + } + + @After + public void dropTablesAndClearContext() { + template.execute("drop table authorities"); + template.execute("drop table users"); + SecurityContextHolder.clearContext(); + } + + @Test + public void createUserInsertsCorrectData() { + manager.createUser(joe); + + UserDetails joe2 = manager.loadUserByUsername("joe"); + + assertEquals(joe, joe2); + } + + @Test + public void deleteUserRemovesUserDataAndAuthorities() { + insertJoe(); + manager.deleteUser("joe"); + + assertEquals(0, template.queryForList(SELECT_JOE_SQL).size()); + assertEquals(0, template.queryForList(SELECT_JOE_AUTHORITIES_SQL).size()); + } + + @Test + public void updateUserChangesDataCorrectly() { + insertJoe(); + User newJoe = new User("joe","newpassword",false,true,true,true, + AuthorityUtils.stringArrayToAuthorityArray(new String[]{"D","E","F"})); + + manager.updateUser(newJoe); + + UserDetails joe = manager.loadUserByUsername("joe"); + + assertEquals(newJoe, joe); + } + + @Test + public void userExistsReturnsFalseForNonExistentUsername() { + assertFalse(manager.userExists("joe")); + } + + @Test + public void userExistsReturnsTrueForExistingUsername() { + insertJoe(); + assertTrue(manager.userExists("joe")); + } + + @Test(expected = AccessDeniedException.class) + public void changePasswordFailsForUnauthenticatedUser() { + manager.changePassword("password", "newPassword"); + } + + @Test + public void changePasswordSucceedsWithAuthenticatedUserAndNoAuthenticationManagerSet() { + insertJoe(); + authenticateJoe(); + manager.changePassword("wrongpassword", "newPassword"); + UserDetails newJoe = manager.loadUserByUsername("joe"); + + assertEquals("newPassword", newJoe.getPassword()); + } + + @Test + public void changePasswordSucceedsWithIfReAuthenticationSucceeds() { + insertJoe(); + authenticateJoe(); + manager.setAuthenticationManager(new MockAuthenticationManager(true)); + manager.changePassword("password", "newPassword"); + UserDetails newJoe = manager.loadUserByUsername("joe"); + + assertEquals("newPassword", newJoe.getPassword()); + // The password in the context should also be altered + assertEquals("newPassword", SecurityContextHolder.getContext().getAuthentication().getCredentials()); + } + + @Test + public void changePasswordFailsIfReAuthenticationFails() { + insertJoe(); + authenticateJoe(); + manager.setAuthenticationManager(new MockAuthenticationManager(false)); + + try { + manager.changePassword("password", "newPassword"); + fail("Expected BadCredentialsException"); + } catch (BadCredentialsException expected) { + } + + // Check password hasn't changed. + UserDetails newJoe = manager.loadUserByUsername("joe"); + assertEquals("password", newJoe.getPassword()); + assertEquals("password", SecurityContextHolder.getContext().getAuthentication().getCredentials()); + } + + private void authenticateJoe() { + UsernamePasswordAuthenticationToken auth = + new UsernamePasswordAuthenticationToken("joe","password", joe.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(auth); + } + + private void insertJoe() { + template.execute("insert into users (username, password, enabled) values ('joe','password','true')"); + template.execute("insert into authorities (username, authority) values ('joe','A')"); + template.execute("insert into authorities (username, authority) values ('joe','B')"); + template.execute("insert into authorities (username, authority) values ('joe','C')"); + } +}