/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.embedded.ssh.internal;
import java.io.IOException;
import java.util.SortedMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mindrot.jbcrypt.BCrypt;
import de.rcenvironment.core.configuration.ConfigurationException;
import de.rcenvironment.core.configuration.ConfigurationSegment;
import de.rcenvironment.core.configuration.ConfigurationService;
import de.rcenvironment.core.configuration.WritableConfigurationSegment;
import de.rcenvironment.core.embedded.ssh.api.SshAccount;
import de.rcenvironment.core.embedded.ssh.api.SshAccountConfigurationService;
import de.rcenvironment.core.utils.common.StringUtils;
/**
* Default {@link SshAccountConfigurationService} implementation.
*
* @author Robert Mischke
*/
public class SshAccountConfigurationServiceImpl implements SshAccountConfigurationService {
private static final String CONFIGURATION_PATH_SEPARATOR = "/";
private static final String MAIN_SSH_CONFIGURATION_PATH = "sshServer";
private static final String SSH_ACCOUNTS_SUB_CONFIGURATION_PATH = "accounts";
private static final String ACCOUNT_ROLES_RA_SUB_CONFIGURATION_PATH = "roles/remote access";
private static final String FIELD_NAME_PASSWORD = "password";
private static final String FIELD_NAME_ENABLED = "enabled";
private static final String FIELD_NAME_PW_HASH = "passwordHash";
private static final String FIELD_NAME_ROLE = "role";
private static final String DEFAULT_RA_ROLE_COMMAND_PATTERN = "ra .+";
private static final String EXPECTED_RA_ROLE_ALLOWED_COMMAND_PATTERN_REGEXP =
"help|exit|(" + DEFAULT_RA_ROLE_COMMAND_PATTERN + ")";
// TODO add length restriction?
private static final String VALID_ACCOUNT_NAME_REGEXP = "^[a-zA-Z][a-zA-Z0-9_\\-]*$";
// TODO imperfect wording: does not exclude non-ASCII characters
private static final String VALID_ACCOUNT_NAME_VIOLATION_MESSAGE =
"The login name must begin with a letter, followed by letters, digits, or the characters \"_\" and \"-\".";
// somewhat arbitrary; maximum Linux account name length seems to be 32, but that may break the UI
private static final int MAX_LOGIN_NAME_LENGTH = 20;
private static final String LOGIN_NAME_TOO_LONG_MESSAGE = "The login name must not be longer than "
+ MAX_LOGIN_NAME_LENGTH + " characters";
private ConfigurationService configurationService;
private SshConfiguration sshConfiguration;
private SshAuthenticationManager sshAuthenticationManager;
private final Log log = LogFactory.getLog(getClass());
@Override
public String verifyExpectedStateForConfigurationEditing() {
if (!configurationService.isUsingIntendedProfileDirectory()) {
return "This configuration mode does not seem to run in the intended profile directory "
+ " - is the profile being used by another RCE instance?\n\nIntended profile location: "
+ configurationService.getOriginalProfileDirectory().getAbsolutePath();
}
if (configurationService.isUsingDefaultConfigurationValues()) {
return "The current configuration file is either invalid, or there was an error creating it. "
+ "Please check the file for errors. If the file does not "
+ "exist, make sure you have write access to the containing directory.\n\n"
+ "Intended configuration file location: "
+ configurationService.getProfileConfigurationFile().getAbsolutePath();
}
final ConfigurationSegment sshConfigurationSegment = loadSshConfigurationSegment();
try {
readAsParsedConfigurationData(sshConfigurationSegment);
} catch (ConfigurationException e) {
return "Error reading SSH account data: " + e.getMessage();
}
// no SSH configuration yet -> ok
if (!sshConfigurationSegment.isPresentInCurrentConfiguration()) {
return null;
}
// SshAccountRole raRole = sshConfiguration.getRoleByName("remote access");
// raAccountRoleIsPresent = (raRole != null);
// // verify the role if it is already present
// if (raRole != null) {
// if (!EXPECTED_RA_ROLE_ALLOWED_COMMAND_PATTERN_REGEXP.equals(raRole.getAllowedCommandRegEx())) {
// return "The SSH Account role \"remote access\" exists, but has an unexpected "
// + "configuration. Please fix this manually (for example, by deleting the "
// + "existing \"role\" entry) and restart.";
// }
// }
return null; // all ok
}
@Override
public SortedMap<String, SshAccount> getAllAccountsByLoginName() throws ConfigurationException {
return sshAuthenticationManager.getAllAcountsByLoginName();
}
@Override
public SshAccount getAccount(String account) {
return sshAuthenticationManager.getAccountByLoginName(account, true); // true = allow disabled accounts
}
@Override
public void createAccount(String loginName, String password) throws ConfigurationException {
if (StringUtils.isNullorEmpty(loginName)) {
throw new ConfigurationException("The login name must not be empty!");
}
if (getAccount(loginName) != null) {
throw new ConfigurationException("An account with this login name already exists.");
}
validateLoginName(loginName);
if (StringUtils.isNullorEmpty(password)) {
throw new ConfigurationException("The password must not be empty!");
}
String passwordHash = generatePasswordHash(password);
WritableConfigurationSegment accountsSegment = getWritableConfigurationSegmentForAccountsList();
try {
final WritableConfigurationSegment accountElement = accountsSegment.createElement(loginName);
// write account data
accountElement.setString(FIELD_NAME_PW_HASH, passwordHash);
accountElement.setString(FIELD_NAME_ROLE, SshConstants.ROLE_NAME_REMOTE_ACCESS_USER);
accountElement.setBoolean(FIELD_NAME_ENABLED, true);
} catch (ConfigurationException e) {
log.error("Failed to add SSH account", e);
throw new ConfigurationException("Failed to add the new account;"
+ " most likely, there is already an account with that login name");
}
writeConfigurationChanges();
log.debug(StringUtils.format("Created SSH account '%s' (using password hash authentication)", loginName));
}
@Override
public void updatePasswordHash(String loginName, String plainTextPassword) throws ConfigurationException {
WritableConfigurationSegment accountSegment = getWritableSegmentForAccount(loginName);
accountSegment.setString(FIELD_NAME_PW_HASH, generatePasswordHash(plainTextPassword));
if (accountSegment.getString(FIELD_NAME_PASSWORD) != null) {
// erase existing plain-text password
accountSegment.setString(FIELD_NAME_PASSWORD, null);
}
writeConfigurationChanges();
log.debug(StringUtils.format("Updated password hash for SSH account '%s'", loginName));
}
@Override
public void updateRole(String loginName, String role) throws ConfigurationException {
WritableConfigurationSegment accountSegment = getWritableSegmentForAccount(loginName);
accountSegment.setString(FIELD_NAME_ROLE, role);
writeConfigurationChanges();
log.debug(StringUtils.format("Set role for SSH account '%s' to '%s'", loginName, role));
}
@Override
public void setAccountEnabled(String loginName, boolean enabled) throws ConfigurationException {
WritableConfigurationSegment accountSegment = getWritableSegmentForAccount(loginName);
accountSegment.setBoolean(FIELD_NAME_ENABLED, enabled);
writeConfigurationChanges();
log.debug(StringUtils.format("Set SSH account '%s' to enabled=%s", loginName, enabled));
}
@Override
public void deleteAccount(String loginName) throws ConfigurationException {
WritableConfigurationSegment accountsSegment = getWritableConfigurationSegmentForAccountsList();
if (!accountsSegment.deleteElement(loginName)) {
throw new ConfigurationException(
"Internal consistency error: Requested account deletion, but no matching configuration node was found");
}
writeConfigurationChanges();
log.debug(StringUtils.format("SSH account '%s' deleted", loginName));
}
@Override
public String generatePasswordHash(final String password) {
// TODO move to common utilities; check iteration count
return BCrypt.hashpw(password, BCrypt.gensalt(10));
}
protected void bindConfigurationService(ConfigurationService newConfigurationService) {
this.configurationService = newConfigurationService;
}
// note: overridden in unit test
protected ConfigurationSegment loadSshConfigurationSegment() {
return configurationService.getConfigurationSegment(MAIN_SSH_CONFIGURATION_PATH);
}
private void readAsParsedConfigurationData(final ConfigurationSegment sshConfigurationSegment) throws ConfigurationException {
try {
sshConfiguration = new SshConfiguration(sshConfigurationSegment);
sshAuthenticationManager = new SshAuthenticationManager(sshConfiguration);
} catch (ConfigurationException | IOException e) {
throw new ConfigurationException("Error reading SSH configuration data: " + e.getMessage());
}
}
private void reloadConfiguration() throws ConfigurationException {
readAsParsedConfigurationData(loadSshConfigurationSegment());
}
private WritableConfigurationSegment getWritableConfigurationSegmentAndWrapErrors(String path) throws ConfigurationException {
WritableConfigurationSegment segment;
try {
segment = configurationService.getOrCreateWritableConfigurationSegment(path);
} catch (ConfigurationException e) {
// internal error; the best that can be done here is to exit with this message
throw new ConfigurationException("Failed to access configuration data: " + e.getMessage());
}
return segment;
}
private WritableConfigurationSegment getWritableConfigurationSegmentForAccountsList() throws ConfigurationException {
WritableConfigurationSegment accountsSegment =
getWritableConfigurationSegmentAndWrapErrors(MAIN_SSH_CONFIGURATION_PATH + CONFIGURATION_PATH_SEPARATOR
+ SSH_ACCOUNTS_SUB_CONFIGURATION_PATH);
return accountsSegment;
}
private WritableConfigurationSegment getWritableSegmentForAccount(String loginName) throws ConfigurationException {
WritableConfigurationSegment accountSegment =
getWritableConfigurationSegmentAndWrapErrors(MAIN_SSH_CONFIGURATION_PATH + CONFIGURATION_PATH_SEPARATOR
+ SSH_ACCOUNTS_SUB_CONFIGURATION_PATH + CONFIGURATION_PATH_SEPARATOR + loginName);
return accountSegment;
}
private void writeConfigurationChanges() throws ConfigurationException {
try {
try {
configurationService.writeConfigurationChanges();
} catch (IOException e) {
throw new ConfigurationException("There was an error writing the configuration changes to the profile folder: "
+ e.getMessage());
}
} finally {
// always reload to ensure consistent in-memory data
reloadConfiguration();
}
}
private void validateLoginName(String loginName) throws ConfigurationException {
// note: rarely used, so not precompiling the regexp
if (!loginName.matches(VALID_ACCOUNT_NAME_REGEXP)) {
throw new ConfigurationException(VALID_ACCOUNT_NAME_VIOLATION_MESSAGE);
}
if (loginName.length() > MAX_LOGIN_NAME_LENGTH) {
throw new ConfigurationException(LOGIN_NAME_TOO_LONG_MESSAGE);
}
}
}