diff --git a/src/main/java/org/opensearch/security/tools/democonfig/Installer.java b/src/main/java/org/opensearch/security/tools/democonfig/Installer.java index 864607a9c6..f1ee81f84e 100644 --- a/src/main/java/org/opensearch/security/tools/democonfig/Installer.java +++ b/src/main/java/org/opensearch/security/tools/democonfig/Installer.java @@ -14,6 +14,7 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileReader; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -97,7 +98,7 @@ public static Installer getInstance() { * Installs the demo security configuration * @param options the options passed to the script */ - public void installDemoConfiguration(String[] options) { + public void installDemoConfiguration(String[] options) throws IOException { readOptions(options); printScriptHeaders(); gatherUserInputs(); @@ -108,7 +109,7 @@ public void installDemoConfiguration(String[] options) { finishScriptExecution(); } - public static void main(String[] options) { + public static void main(String[] options) throws IOException { Installer installer = Installer.getInstance(); installer.buildOptions(); installer.installDemoConfiguration(options); diff --git a/src/main/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurer.java b/src/main/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurer.java index 5b497d0f20..92a7915114 100644 --- a/src/main/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurer.java +++ b/src/main/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurer.java @@ -12,24 +12,22 @@ package org.opensearch.security.tools.democonfig; import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.File; +import java.io.FileInputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; +import org.bouncycastle.crypto.generators.OpenBSDBCrypt; import org.opensearch.common.settings.Settings; import org.opensearch.core.common.Strings; -import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.dlic.rest.validation.PasswordValidator; import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.support.ConfigConstants; @@ -38,6 +36,7 @@ import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.Yaml; +import static org.opensearch.security.DefaultObjectMapper.YAML_MAPPER; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX; @@ -81,6 +80,7 @@ public class SecuritySettingsConfigurer { static String ADMIN_USERNAME = "admin"; private final Installer installer; + static final String DEFAULT_ADMIN_PASSWORD = "admin"; public SecuritySettingsConfigurer(Installer installer) { this.installer = installer; @@ -92,7 +92,7 @@ public SecuritySettingsConfigurer(Installer installer) { * 2. Sets the custom admin password (Generates one if none is provided) * 3. Write the security config to opensearch.yml */ - public void configureSecuritySettings() { + public void configureSecuritySettings() throws IOException { checkIfSecurityPluginIsAlreadyConfigured(); updateAdminPassword(); writeSecurityConfigToOpenSearchYML(); @@ -125,9 +125,17 @@ void checkIfSecurityPluginIsAlreadyConfigured() { /** * Replaces the admin password in internal_users.yml with the custom or generated password */ - void updateAdminPassword() { + void updateAdminPassword() throws IOException { String INTERNAL_USERS_FILE_PATH = installer.OPENSEARCH_CONF_DIR + "opensearch-security" + File.separator + "internal_users.yml"; boolean shouldValidatePassword = installer.environment.equals(ExecutionEnvironment.DEMO); + + // check if the password `admin` is present, if not skip updating admin password + if (!isAdminPasswordSetToAdmin(INTERNAL_USERS_FILE_PATH)) { + System.out.println("Admin password seems to be custom configured. Skipping update to admin password."); + return; + } + + // if hashed value for default password "admin" is found, update it with the custom password. try { final PasswordValidator passwordValidator = PasswordValidator.of( Settings.builder() @@ -169,17 +177,29 @@ void updateAdminPassword() { System.exit(-1); } - // Print an update to the logs - System.out.println("Admin password set successfully."); - + // Update the custom password in internal_users.yml file writePasswordToInternalUsersFile(ADMIN_PASSWORD, INTERNAL_USERS_FILE_PATH); + System.out.println("Admin password set successfully."); + } catch (IOException e) { System.out.println("Exception updating the admin password : " + e.getMessage()); System.exit(-1); } } + /** + * Check if the password for admin user was already updated. (Possibly via a custom internal_users.yml) + * @param internalUsersFile Path to internal_users.yml file + * @return true if password was already updated, false otherwise + * @throws IOException if there was an error while reading the file + */ + private boolean isAdminPasswordSetToAdmin(String internalUsersFile) throws IOException { + JsonNode internalUsers = YAML_MAPPER.readTree(new FileInputStream(internalUsersFile)); + return internalUsers.has("admin") + && OpenBSDBCrypt.checkPassword(internalUsers.get("admin").get("hash").asText(), DEFAULT_ADMIN_PASSWORD.toCharArray()); + } + /** * Generate password hash and update it in the internal_users.yml file * @param adminPassword the password to be hashed and updated @@ -190,31 +210,24 @@ void writePasswordToInternalUsersFile(String adminPassword, String internalUsers String hashedAdminPassword = Hasher.hash(adminPassword.toCharArray()); if (hashedAdminPassword.isEmpty()) { - System.out.println("Hash the admin password failure, see console for details"); + System.out.println("Failure while hashing the admin password, see console for details."); System.exit(-1); } - Path tempFilePath = Paths.get(internalUsersFile + ".tmp"); - Path internalUsersPath = Paths.get(internalUsersFile); - - try ( - BufferedReader reader = new BufferedReader(new FileReader(internalUsersFile, StandardCharsets.UTF_8)); - BufferedWriter writer = new BufferedWriter(new FileWriter(tempFilePath.toFile(), StandardCharsets.UTF_8)) - ) { - String line; - while ((line = reader.readLine()) != null) { - if (line.matches(" *hash: *\"\\$2a\\$12\\$VcCDgh2NDk07JGN0rjGbM.Ad41qVR/YFJcgHp0UGns5JDymv..TOG\"")) { - line = line.replace( - "\"$2a$12$VcCDgh2NDk07JGN0rjGbM.Ad41qVR/YFJcgHp0UGns5JDymv..TOG\"", - "\"" + hashedAdminPassword + "\"" - ); - } - writer.write(line + System.lineSeparator()); + try { + var map = YAML_MAPPER.readValue(new File(internalUsersFile), new TypeReference>>() { + }); + var admin = map.get("admin"); + if (admin != null) { + // Replace the password since the default password was found via the check: isAdminPasswordSetToAdmin(..) + admin.put("hash", hashedAdminPassword); } + + // Write the updated map back to the internal_users.yml file + YAML_MAPPER.writeValue(new File(internalUsersFile), map); } catch (IOException e) { throw new IOException("Unable to update the internal users file with the hashed password."); } - Files.move(tempFilePath, internalUsersPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING); } /** @@ -329,7 +342,7 @@ static boolean isNodeMaxLocalStorageNodesAlreadyPresent(String filePath) { static boolean isKeyPresentInYMLFile(String filePath, String key) throws IOException { JsonNode node; try { - node = DefaultObjectMapper.YAML_MAPPER.readTree(new File(filePath)); + node = YAML_MAPPER.readTree(new File(filePath)); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java b/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java index 50a65e7fa2..f4b56e6f76 100644 --- a/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java +++ b/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java @@ -21,16 +21,22 @@ import java.io.PrintStream; import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.tools.Hasher; import org.opensearch.security.tools.democonfig.util.NoExitSecurityManager; import static org.hamcrest.MatcherAssert.assertThat; @@ -39,6 +45,7 @@ import static org.hamcrest.Matchers.is; import static org.opensearch.security.dlic.rest.validation.RequestContentValidator.ValidationError.INVALID_PASSWORD_INVALID_REGEX; import static org.opensearch.security.dlic.rest.validation.RequestContentValidator.ValidationError.INVALID_PASSWORD_TOO_SHORT; +import static org.opensearch.security.tools.democonfig.SecuritySettingsConfigurer.DEFAULT_ADMIN_PASSWORD; import static org.opensearch.security.tools.democonfig.SecuritySettingsConfigurer.DEFAULT_PASSWORD_MIN_LENGTH; import static org.opensearch.security.tools.democonfig.SecuritySettingsConfigurer.REST_ENABLED_ROLES; import static org.opensearch.security.tools.democonfig.SecuritySettingsConfigurer.SYSTEM_INDICES; @@ -66,13 +73,14 @@ public class SecuritySettingsConfigurerTests { private static Installer installer; @Before - public void setUp() { + public void setUp() throws IOException { System.setOut(new PrintStream(outContent)); System.setErr(new PrintStream(outContent)); installer = Installer.getInstance(); installer.buildOptions(); securitySettingsConfigurer = new SecuritySettingsConfigurer(installer); setUpConf(); + setUpInternalUsersYML(); } @After @@ -87,7 +95,7 @@ public void tearDown() throws NoSuchFieldException, IllegalAccessException { } @Test - public void testUpdateAdminPasswordWithCustomPassword() throws NoSuchFieldException, IllegalAccessException { + public void testUpdateAdminPasswordWithCustomPassword() throws NoSuchFieldException, IllegalAccessException, IOException { String customPassword = "myStrongPassword123"; setEnv(adminPasswordKey, customPassword); @@ -104,7 +112,7 @@ public void testUpdateAdminPassword_noPasswordSupplied() { try { System.setSecurityManager(new NoExitSecurityManager()); securitySettingsConfigurer.updateAdminPassword(); - } catch (SecurityException e) { + } catch (SecurityException | IOException e) { assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing.")); } finally { System.setSecurityManager(null); @@ -125,7 +133,7 @@ public void testUpdateAdminPasswordWithWeakPassword() throws NoSuchFieldExceptio try { System.setSecurityManager(new NoExitSecurityManager()); securitySettingsConfigurer.updateAdminPassword(); - } catch (SecurityException e) { + } catch (SecurityException | IOException e) { assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing.")); } finally { System.setSecurityManager(null); @@ -148,7 +156,7 @@ public void testUpdateAdminPasswordWithShortPassword() throws NoSuchFieldExcepti try { System.setSecurityManager(new NoExitSecurityManager()); securitySettingsConfigurer.updateAdminPassword(); - } catch (SecurityException e) { + } catch (SecurityException | IOException e) { assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing.")); } finally { System.setSecurityManager(null); @@ -160,7 +168,8 @@ public void testUpdateAdminPasswordWithShortPassword() throws NoSuchFieldExcepti } @Test - public void testUpdateAdminPasswordWithWeakPassword_skipPasswordValidation() throws NoSuchFieldException, IllegalAccessException { + public void testUpdateAdminPasswordWithWeakPassword_skipPasswordValidation() throws NoSuchFieldException, IllegalAccessException, + IOException { setEnv(adminPasswordKey, "weakpassword"); installer.environment = ExecutionEnvironment.TEST; securitySettingsConfigurer.updateAdminPassword(); @@ -170,6 +179,49 @@ public void testUpdateAdminPasswordWithWeakPassword_skipPasswordValidation() thr verifyStdOutContainsString("Admin password set successfully."); } + @Test + public void testUpdateAdminPasswordWithCustomInternalUsersYML() throws IOException { + String internalUsersFile = installer.OPENSEARCH_CONF_DIR + "opensearch-security" + File.separator + "internal_users.yml"; + Path internalUsersFilePath = Paths.get(internalUsersFile); + + List newContent = Arrays.asList( + "_meta:", + " type: \"internalusers\"", + " config_version: 2", + "admin:", + " hash: " + Hasher.hash(RandomStringUtils.randomAlphanumeric(16).toCharArray()), + " backend_roles:", + " - \"admin\"" + ); + // overwriting existing content + Files.write(internalUsersFilePath, newContent, StandardCharsets.UTF_8); + + securitySettingsConfigurer.updateAdminPassword(); + + verifyStdOutContainsString("Admin password seems to be custom configured. Skipping update to admin password."); + } + + @Test + public void testUpdateAdminPasswordWithDefaultInternalUsersYml() { + + SecuritySettingsConfigurer.ADMIN_PASSWORD = ""; // to ensure 0 flaky-ness + try { + System.setSecurityManager(new NoExitSecurityManager()); + securitySettingsConfigurer.updateAdminPassword(); + } catch (SecurityException | IOException e) { + assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing.")); + } finally { + System.setSecurityManager(null); + } + + verifyStdOutContainsString( + String.format( + "No custom admin password found. Please provide a password via the environment variable %s.", + ConfigConstants.OPENSEARCH_INITIAL_ADMIN_PASSWORD + ) + ); + } + @Test public void testSecurityPluginAlreadyConfigured() { securitySettingsConfigurer.writeSecurityConfigToOpenSearchYML(); @@ -353,4 +405,21 @@ void setUpConf() { private void verifyStdOutContainsString(String s) { assertThat(outContent.toString(), containsString(s)); } + + private void setUpInternalUsersYML() throws IOException { + String internalUsersFile = installer.OPENSEARCH_CONF_DIR + "opensearch-security" + File.separator + "internal_users.yml"; + Path internalUsersFilePath = Paths.get(internalUsersFile); + List defaultContent = Arrays.asList( + "_meta:", + " type: \"internalusers\"", + " config_version: 2", + "admin:", + " hash: " + Hasher.hash(DEFAULT_ADMIN_PASSWORD.toCharArray()), + " reserved: " + true, + " backend_roles:", + " - \"admin\"", + " description: Demo admin user" + ); + Files.write(internalUsersFilePath, defaultContent, StandardCharsets.UTF_8); + } }