/* ==================================================================
* SettingsUserService.java - 27/07/2016 8:09:01 AM
*
* Copyright 2007-2016 SolarNetwork.net Dev Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
* ==================================================================
*/
package net.solarnetwork.node.setup.security;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import net.solarnetwork.node.IdentityService;
import net.solarnetwork.node.Setting;
import net.solarnetwork.node.dao.BasicBatchOptions;
import net.solarnetwork.node.dao.BatchableDao.BatchCallback;
import net.solarnetwork.node.dao.BatchableDao.BatchCallbackResult;
import net.solarnetwork.node.dao.SettingDao;
import net.solarnetwork.node.setup.UserProfile;
import net.solarnetwork.node.setup.UserService;
/**
* {@link UserDetailsService} that uses {@link SettingDao} for users and roles.
*
* @author matt
* @version 1.0
*/
public class SettingsUserService implements UserService, UserDetailsService {
public static final String SETTING_TYPE_USER = "solarnode.user";
public static final String SETTING_TYPE_ROLE = "solarnode.role";
public static final String GRANTED_AUTH_USER = "ROLE_USER";
private final SettingDao settingDao;
private final IdentityService identityService;
private final PasswordEncoder passwordEncoder;
/**
* Constructor.
*
* @param settingDao
* The setting DAO to use.
* @param identityService
* The {@link IdentityService} to use.
* @param passwordEncoder
* The {@link PasswordEncoder} to use for legacy user support.
*/
public SettingsUserService(SettingDao settingDao, IdentityService identityService,
PasswordEncoder passwordEncoder) {
super();
this.settingDao = settingDao;
this.identityService = identityService;
this.passwordEncoder = passwordEncoder;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails result = null;
String password = settingDao.getSetting(username, SETTING_TYPE_USER);
if ( password == null && identityService != null && passwordEncoder != null
&& !someUserExists() ) {
// for backwards-compat with nodes created before user auth, provide a default
Long nodeId = identityService.getNodeId();
if ( nodeId != null && nodeId.toString().equalsIgnoreCase(username) ) {
password = passwordEncoder.encode("solar");
GrantedAuthority auth = new SimpleGrantedAuthority(GRANTED_AUTH_USER);
result = new User(username, password, Collections.singleton(auth));
}
} else if ( password != null ) {
Collection<GrantedAuthority> auths;
String role = settingDao.getSetting(username, SETTING_TYPE_ROLE);
if ( role != null ) {
GrantedAuthority auth = new SimpleGrantedAuthority(role);
auths = Collections.singleton(auth);
} else {
auths = Collections.emptySet();
}
result = new User(username, password, auths);
}
if ( result == null ) {
throw new UsernameNotFoundException(username);
}
return result;
}
@Override
public boolean someUserExists() {
final AtomicBoolean result = new AtomicBoolean(false);
settingDao.batchProcess(new BatchCallback<Setting>() {
@Override
public BatchCallbackResult handle(Setting domainObject) {
if ( domainObject.getType().equals(SETTING_TYPE_USER) ) {
result.set(true);
return BatchCallbackResult.STOP;
}
return BatchCallbackResult.CONTINUE;
}
}, new BasicBatchOptions("FindUser"));
return result.get();
}
/**
* Update the active user's password.
*
* @param existingPassword
* The existing password.
* @param newPassword
* The new password to set.
* @param newPasswordAgain
* The new password, repeated.
* @throws InsufficientAuthenticationException
* If an active user is not available.
* @throws UsernameNotFoundException
* If the active user cannot be found in settings.
* @throws BadCredentialsException
* If {@code existingPassword} does not match the password in
* settings.
* @throws IllegalArgumentException
* if the {@code newPassword} and {@code newPasswordAgain} values do
* not match, or are <em>null</em>
*/
@Override
public void changePassword(final String existingPassword, final String newPassword,
final String newPasswordAgain) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
UserDetails activeUser = (auth == null ? null : (UserDetails) auth.getPrincipal());
if ( activeUser == null ) {
throw new InsufficientAuthenticationException("Active user not found.");
}
UserDetails dbUser = loadUserByUsername(activeUser.getUsername());
if ( dbUser == null ) {
throw new UsernameNotFoundException("User not found");
}
if ( passwordEncoder != null ) {
if ( !passwordEncoder.matches(existingPassword, dbUser.getPassword()) ) {
throw new BadCredentialsException("Existing password does not match.");
}
} else if ( !existingPassword.equals(dbUser.getPassword()) ) {
throw new BadCredentialsException("Existing password does not match.");
}
if ( newPassword == null || newPasswordAgain == null || !newPassword.equals(newPasswordAgain) ) {
throw new IllegalArgumentException(
"New password not provided or does not match repeated password.");
}
String password;
if ( passwordEncoder != null ) {
password = passwordEncoder.encode(newPassword);
} else {
password = newPassword;
}
settingDao.storeSetting(dbUser.getUsername(), SETTING_TYPE_USER, password);
settingDao.storeSetting(dbUser.getUsername(), SETTING_TYPE_ROLE, GRANTED_AUTH_USER);
}
@Override
public void changeUsername(final String newUsername, final String newUsernameAgain) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
UserDetails activeUser = (auth == null ? null : (UserDetails) auth.getPrincipal());
if ( activeUser == null ) {
throw new InsufficientAuthenticationException("Active user not found.");
}
final UserDetails dbUser = loadUserByUsername(activeUser.getUsername());
if ( dbUser == null ) {
throw new UsernameNotFoundException("User not found");
}
if ( newUsername == null || newUsernameAgain == null || !newUsername.equals(newUsernameAgain) ) {
throw new IllegalArgumentException(
"New username not provided or does not match repeated username.");
}
final AtomicBoolean updatedUsername = new AtomicBoolean(false);
final AtomicBoolean updatedRole = new AtomicBoolean(false);
settingDao.batchProcess(new BatchCallback<Setting>() {
@Override
public BatchCallbackResult handle(Setting domainObject) {
if ( domainObject.getType().equals(SETTING_TYPE_USER)
&& domainObject.getKey().equals(dbUser.getUsername()) ) {
updatedUsername.set(true);
domainObject.setKey(newUsername);
return (updatedRole.get() ? BatchCallbackResult.UPDATE_STOP
: BatchCallbackResult.UPDATE);
} else if ( domainObject.getType().equals(SETTING_TYPE_ROLE)
&& domainObject.getKey().equals(dbUser.getUsername()) ) {
updatedRole.set(true);
domainObject.setKey(newUsername);
return (updatedUsername.get() ? BatchCallbackResult.UPDATE_STOP
: BatchCallbackResult.UPDATE);
}
return BatchCallbackResult.CONTINUE;
}
}, new BasicBatchOptions("UpdateUser", BasicBatchOptions.DEFAULT_BATCH_SIZE, true, null));
if ( !updatedUsername.get() ) {
// no username exists, treat as a legacy node whose password was "solar"
UserProfile newProfile = new UserProfile();
newProfile.setUsername(newUsername);
newProfile.setPassword("solar");
newProfile.setPasswordAgain("solar");
storeUserProfile(newProfile);
}
// update active user details to new usenrame
User newUser = new User(newUsername, "",
Collections.singleton(new SimpleGrantedAuthority(GRANTED_AUTH_USER)));
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
newUser, null, newUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
@Override
public void storeUserProfile(UserProfile profile) {
if ( profile.getUsername() == null || profile.getPassword() == null
|| !profile.getPassword().equals(profile.getPasswordAgain()) ) {
throw new IllegalArgumentException(
"Username, password, and repeated password must be provided.");
}
String password;
if ( passwordEncoder != null ) {
password = passwordEncoder.encode(profile.getPassword());
} else {
password = profile.getPassword();
}
settingDao.storeSetting(profile.getUsername(), SETTING_TYPE_USER, password);
settingDao.storeSetting(profile.getUsername(), SETTING_TYPE_ROLE, GRANTED_AUTH_USER);
}
/**
* Get the configured {@link SettingDao}.
*
* @return The DAO.
*/
public SettingDao getSettingDao() {
return settingDao;
}
/**
* Get the configured password encoder.
*
* @return The password encoder.
*/
public PasswordEncoder getPasswordEncoder() {
return passwordEncoder;
}
}