/* * 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); } } }