Skip to content

Add customizable RowMappers for user details and authorities in JdbcUserDetailsManager #16561

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.springframework.core.log.LogMessage;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
Expand Down Expand Up @@ -156,13 +157,48 @@ public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsMa

private UserCache userCache = new NullUserCache();

private RowMapper<UserDetails> userDetailsMapper = this::mapToUser;

private RowMapper<GrantedAuthority> grantedAuthorityMapper = this::mapToGrantedAuthority;

public JdbcUserDetailsManager() {
}

public JdbcUserDetailsManager(DataSource dataSource) {
setDataSource(dataSource);
}

/**
* Sets the {@code RowMapper} to convert each user result row into a
* {@link UserDetails} object.
*
* The default mapper expects columns with names like 'username', 'password',
* 'enabled', etc., and maps them directly to the corresponding UserDetails
* properties.
* @param mapper the {@code RowMapper} to use for mapping rows in the database, must
* not be null
* @since 6.5
*/
public void setUserDetailsMapper(RowMapper<UserDetails> mapper) {
Assert.notNull(mapper, "userDetailsMapper cannot be null");
this.userDetailsMapper = mapper;
}

/**
* Sets the {@code RowMapper} to convert each authority result row into a
* {@link GrantedAuthority} object.
*
* The default mapper expects columns with names like 'authority' or 'role', and maps
* them directly to SimpleGrantedAuthority objects.
* @param mapper the {@code RowMapper} to use for mapping rows in the database to
* GrantedAuthority objects, must not be null
* @since 6.5
*/
public void setGrantedAuthorityMapper(RowMapper<GrantedAuthority> mapper) {
Assert.notNull(mapper, "grantedAuthorityMapper cannot be null");
this.grantedAuthorityMapper = mapper;
}

@Override
protected void initDao() throws ApplicationContextException {
if (this.authenticationManager == null) {
Expand All @@ -178,7 +214,7 @@ protected void initDao() throws ApplicationContextException {
*/
@Override
protected List<UserDetails> loadUsersByUsername(String username) {
return getJdbcTemplate().query(getUsersByUsernameQuery(), this::mapToUser, username);
return getJdbcTemplate().query(getUsersByUsernameQuery(), this.userDetailsMapper, username);
}

private UserDetails mapToUser(ResultSet rs, int rowNum) throws SQLException {
Expand Down Expand Up @@ -387,7 +423,7 @@ public List<GrantedAuthority> findGroupAuthorities(String groupName) {
this.logger.debug("Loading authorities for group '" + groupName + "'");
Assert.hasText(groupName, "groupName should have text");
return getJdbcTemplate().query(this.groupAuthoritiesSql, new String[] { groupName },
this::mapToGrantedAuthority);
this.grantedAuthorityMapper);
}

private GrantedAuthority mapToGrantedAuthority(ResultSet rs, int rowNum) throws SQLException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.springframework.security.provisioning;

import java.sql.SQLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
Expand All @@ -28,6 +29,7 @@
import org.junit.jupiter.api.Test;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.security.PopulatedDatabase;
import org.springframework.security.TestDataSource;
import org.springframework.security.access.AccessDeniedException;
Expand All @@ -48,14 +50,16 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.verify;

/**
* Tests for {@link JdbcUserDetailsManager}
*
* @author Luke Taylor
* @author dae won
*/
public class JdbcUserDetailsManagerTests {

Expand Down Expand Up @@ -365,6 +369,42 @@ public void createNewAuthenticationUsesNullPasswordToKeepPassordsSave() {
assertThat(updatedAuth.getCredentials()).isNull();
}

@Test
public void setUserDetailsMapperWithNullMapperThrowsException() {
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> this.manager.setUserDetailsMapper(null))
.withMessage("userDetailsMapper cannot be null");
}

@Test
public void setUserDetailsMapperWithMockMapper() throws SQLException {
RowMapper<UserDetails> mockMapper = mock(RowMapper.class);
given(mockMapper.mapRow(any(), anyInt())).willReturn(joe);
this.manager.setUserDetailsMapper(mockMapper);
insertJoe();
UserDetails newJoe = this.manager.loadUserByUsername("joe");
assertThat(joe).isEqualTo(newJoe);
verify(mockMapper).mapRow(any(), anyInt());
}

@Test
public void setGrantedAuthorityMapperWithNullMapperThrowsException() {
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> this.manager.setGrantedAuthorityMapper(null))
.withMessage("grantedAuthorityMapper cannot be null");
}

@Test
public void setGrantedAuthorityMapperWithMockMapper() throws SQLException {
RowMapper<GrantedAuthority> mockMapper = mock(RowMapper.class);
GrantedAuthority mockAuthority = new SimpleGrantedAuthority("ROLE_MOCK");
given(mockMapper.mapRow(any(), anyInt())).willReturn(mockAuthority);
this.manager.setGrantedAuthorityMapper(mockMapper);
List<GrantedAuthority> authGroup = this.manager.findGroupAuthorities("GROUP_0");
assertThat(authGroup.get(0)).isEqualTo(mockAuthority);
verify(mockMapper).mapRow(any(), anyInt());
}

private Authentication authenticateJoe() {
UsernamePasswordAuthenticationToken auth = UsernamePasswordAuthenticationToken.authenticated("joe", "password",
joe.getAuthorities());
Expand Down