diff --git a/build.gradle b/build.gradle index 125fb5e39e..f5d0ed8488 100644 --- a/build.gradle +++ b/build.gradle @@ -256,6 +256,7 @@ test { jvmArgs += "-Xmx3072m" if (JavaVersion.current() > JavaVersion.VERSION_1_8) { jvmArgs += "--add-opens=java.base/java.io=ALL-UNNAMED" + jvmArgs += "--add-opens=java.base/java.util=ALL-UNNAMED" } retry { failOnPassedAfterRetry = false @@ -303,6 +304,7 @@ def setCommonTestConfig(Test task) { task.jvmArgs += "-Xmx3072m" if (JavaVersion.current() > JavaVersion.VERSION_1_8) { task.jvmArgs += "--add-opens=java.base/java.io=ALL-UNNAMED" + task.jvmArgs += "--add-opens=java.base/java.util=ALL-UNNAMED" } task.retry { failOnPassedAfterRetry = false 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 33af16c66a..104abc7f31 100644 --- a/src/main/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurer.java +++ b/src/main/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurer.java @@ -54,7 +54,6 @@ public static void configureSecuritySettings() { * Replaces the admin password in internal_users.yml with the custom or generated password */ static void updateAdminPassword() { - String initialAdminPassword = System.getenv("initialAdminPassword"); String ADMIN_PASSWORD_FILE_PATH = OPENSEARCH_CONF_DIR + "initialAdminPassword.txt"; String INTERNAL_USERS_FILE_PATH = OPENSEARCH_CONF_DIR + "opensearch-security" + File.separator + "internal_users.yml"; @@ -108,7 +107,7 @@ static void updateAdminPassword() { writePasswordToInternalUsersFile(ADMIN_PASSWORD, INTERNAL_USERS_FILE_PATH); } catch (IOException e) { - System.out.println("Exception: " + e.getMessage()); + System.out.println("Exception updating the admin password : " + e.getMessage()); System.exit(-1); } } diff --git a/src/test/java/org/opensearch/security/tools/democonfig/InstallerTests.java b/src/test/java/org/opensearch/security/tools/democonfig/InstallerTests.java index d54fb8fdd8..427d17d988 100644 --- a/src/test/java/org/opensearch/security/tools/democonfig/InstallerTests.java +++ b/src/test/java/org/opensearch/security/tools/democonfig/InstallerTests.java @@ -28,7 +28,7 @@ import org.junit.Before; import org.junit.Test; -import org.opensearch.security.test.SingleClusterTest; +import org.opensearch.security.tools.democonfig.util.NoExitSecurityManager; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; @@ -64,11 +64,14 @@ import static org.opensearch.security.tools.democonfig.Installer.setOpenSearchVariables; import static org.opensearch.security.tools.democonfig.Installer.setSecurityVariables; import static org.opensearch.security.tools.democonfig.Installer.skip_updates; +import static org.opensearch.security.tools.democonfig.util.DemoConfigHelperUtil.createDirectory; +import static org.opensearch.security.tools.democonfig.util.DemoConfigHelperUtil.createFile; +import static org.opensearch.security.tools.democonfig.util.DemoConfigHelperUtil.deleteDirectoryRecursive; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; -public class InstallerTests extends SingleClusterTest { +public class InstallerTests { private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); private final PrintStream originalOut = System.out; private final InputStream originalIn = System.in; @@ -489,50 +492,12 @@ public void setUpSecurityDirectories() { public void tearDownSecurityDirectories() { // Clean up testing directories or files - deleteFile(OPENSEARCH_PLUGINS_DIR + "opensearch-security" + File.separator + "opensearch-security-version.jar"); - deleteFile(OPENSEARCH_LIB_PATH + "opensearch-osVersion.jar"); - deleteDirectory(OPENSEARCH_PLUGINS_DIR + "opensearch-security"); - deleteDirectory(OPENSEARCH_PLUGINS_DIR); - deleteDirectory(OPENSEARCH_LIB_PATH); - deleteFile(OPENSEARCH_CONF_DIR + File.separator + "securityadmin_demo.sh"); - deleteDirectory(OPENSEARCH_CONF_DIR); + deleteDirectoryRecursive(OPENSEARCH_PLUGINS_DIR); + deleteDirectoryRecursive(OPENSEARCH_LIB_PATH); + deleteDirectoryRecursive(OPENSEARCH_CONF_DIR); } - private void createDirectory(String path) { - File directory = new File(path); - if (!directory.exists() && !directory.mkdirs()) { - throw new RuntimeException("Failed to create directory: " + path); - } - } - - private void createFile(String path) { - try { - File file = new File(path); - if (!file.exists() && !file.createNewFile()) { - throw new RuntimeException("Failed to create file: " + path); - } - } catch (Exception e) { - // without this the catch, we would need to throw exception, - // which would then require modifying caller method signature - throw new RuntimeException("Failed to create file: " + path, e); - } - } - - private void deleteDirectory(String path) { - File directory = new File(path); - if (directory.exists() && !directory.delete()) { - throw new RuntimeException("Failed to delete directory: " + path); - } - } - - private void deleteFile(String path) { - File file = new File(path); - if (file.exists() && !file.delete()) { - throw new RuntimeException("Failed to delete file: " + path); - } - } - - private void setWritePermissions(String filePath) { + static void setWritePermissions(String filePath) { if (!OS.toLowerCase().contains("win")) { Path file = Paths.get(filePath); Set perms = new HashSet<>(); @@ -544,19 +509,4 @@ private void setWritePermissions(String filePath) { } } } - -} - -class NoExitSecurityManager extends SecurityManager { - @Override - public void checkPermission(java.security.Permission perm) { - // Allow everything except System.exit code 0 &b -1 - if (perm instanceof java.lang.RuntimePermission && ("exitVM.0".equals(perm.getName()) || "exitVM.-1".equals(perm.getName()))) { - StringBuilder sb = new StringBuilder(); - sb.append("System.exit("); - sb.append(perm.getName().contains("0") ? 0 : -1); - sb.append(") blocked to allow print statement testing."); - throw new SecurityException(sb.toString()); - } - } } 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 6d2614b1e1..8b02b13bf3 100644 --- a/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java +++ b/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java @@ -1,3 +1,305 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + package org.opensearch.security.tools.democonfig; -public class SecuritySettingsConfigurerTests {} +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Map; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.dlic.rest.validation.PasswordValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; +import org.opensearch.security.tools.democonfig.util.NoExitSecurityManager; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX; +import static org.opensearch.security.tools.democonfig.Installer.FILE_EXTENSION; +import static org.opensearch.security.tools.democonfig.Installer.OPENSEARCH_CONF_DIR; +import static org.opensearch.security.tools.democonfig.Installer.OPENSEARCH_CONF_FILE; +import static org.opensearch.security.tools.democonfig.SecuritySettingsConfigurer.getSecurityAdminCommands; +import static org.opensearch.security.tools.democonfig.SecuritySettingsConfigurer.writeSecurityConfigToOpenSearchYML; +import static org.opensearch.security.tools.democonfig.util.DemoConfigHelperUtil.createDirectory; +import static org.opensearch.security.tools.democonfig.util.DemoConfigHelperUtil.createFile; +import static org.opensearch.security.tools.democonfig.util.DemoConfigHelperUtil.deleteDirectoryRecursive; +import static org.opensearch.security.user.UserService.generatePassword; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +public class SecuritySettingsConfigurerTests { + + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final InputStream originalIn = System.in; + + private final String adminPasswordKey = "initialAdminPassword"; + + @Before + public void setUp() { + System.setOut(new PrintStream(outContent)); + setUpConf(); + } + + @After + public void tearDown() throws NoSuchFieldException, IllegalAccessException { + System.setOut(originalOut); + System.setIn(originalIn); + deleteDirectoryRecursive(OPENSEARCH_CONF_DIR); + Installer.environment = ExecutionEnvironment.DEMO; + unsetEnv(adminPasswordKey); + } + + @Test + public void testUpdateAdminPasswordWithCustomPassword() throws NoSuchFieldException, IllegalAccessException { + String customPassword = generateStrongPassword(); + setEnv(adminPasswordKey, customPassword); + + SecuritySettingsConfigurer.updateAdminPassword(); + + assertThat(customPassword, is(equalTo(SecuritySettingsConfigurer.ADMIN_PASSWORD))); + + assertThat(outContent.toString(), containsString("ADMIN PASSWORD SET TO: " + customPassword)); + } + + @Test + public void testUpdateAdminPasswordWithFilePassword() throws IOException { + String customPassword = generateStrongPassword(); + String initialAdminPasswordTxt = System.getProperty("user.dir") + + File.separator + + "test-conf" + + File.separator + + adminPasswordKey + + ".txt"; + createFile(initialAdminPasswordTxt); + + try (BufferedWriter writer = new BufferedWriter(new FileWriter(initialAdminPasswordTxt, StandardCharsets.UTF_8))) { + writer.write(customPassword); + } catch (IOException e) { + throw new IOException("Unable to update the internal users file with the hashed password."); + } + + SecuritySettingsConfigurer.updateAdminPassword(); + + assertEquals(customPassword, SecuritySettingsConfigurer.ADMIN_PASSWORD); + assertThat(outContent.toString(), containsString("ADMIN PASSWORD SET TO: " + customPassword)); + } + + @Test + public void testUpdateAdminPasswordWithWeakPassword() throws NoSuchFieldException, IllegalAccessException { + + setEnv(adminPasswordKey, "weakpassword"); + try { + System.setSecurityManager(new NoExitSecurityManager()); + + SecuritySettingsConfigurer.updateAdminPassword(); + + assertThat(outContent.toString(), containsString("Password weakpassword is weak. Please re-try with a stronger password.")); + + } catch (SecurityException e) { + assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing.")); + } finally { + System.setSecurityManager(null); + } + } + + @Test + public void testUpdateAdminPasswordWithWeakPassword_skipPasswordValidation() throws NoSuchFieldException, IllegalAccessException { + setEnv(adminPasswordKey, "weakpassword"); + Installer.environment = ExecutionEnvironment.TEST; + SecuritySettingsConfigurer.updateAdminPassword(); + + assertThat("weakpassword", is(equalTo(SecuritySettingsConfigurer.ADMIN_PASSWORD))); + assertThat(outContent.toString(), containsString("ADMIN PASSWORD SET TO: weakpassword")); + } + + @Test + public void testSecurityPluginAlreadyConfigured() { + writeSecurityConfigToOpenSearchYML(); + try { + System.setSecurityManager(new NoExitSecurityManager()); + String expectedMessage = OPENSEARCH_CONF_FILE + " seems to be already configured for Security. Quit."; + + SecuritySettingsConfigurer.checkIfSecurityPluginIsAlreadyConfigured(); + assertThat(outContent.toString(), containsString(expectedMessage)); + } catch (SecurityException e) { + assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing.")); + } finally { + System.setSecurityManager(null); + } + } + + @Test + public void testSecurityPluginNotConfigured() { + try { + SecuritySettingsConfigurer.checkIfSecurityPluginIsAlreadyConfigured(); + } catch (Exception e) { + fail("Expected checkIfSecurityPluginIsAlreadyConfigured to succeed without any errors."); + } + } + + @Test + public void testConfigFileDoesNotExist() { + OPENSEARCH_CONF_FILE = "path/to/nonexistentfile"; + try { + System.setSecurityManager(new NoExitSecurityManager()); + String expectedMessage = "OpenSearch configuration file does not exist. Quit."; + + SecuritySettingsConfigurer.checkIfSecurityPluginIsAlreadyConfigured(); + assertThat(outContent.toString(), containsString(expectedMessage)); + } catch (SecurityException e) { + assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing.")); + } finally { + System.setSecurityManager(null); + } + // reset the file pointer + OPENSEARCH_CONF_FILE = OPENSEARCH_CONF_DIR + "opensearch.yml"; + } + + @Test + public void testBuildSecurityConfigString() { + String actual = SecuritySettingsConfigurer.buildSecurityConfigString(); + + String expected = "\n" + + "######## Start OpenSearch Security Demo Configuration ########\n" + + "# WARNING: revise all the lines below before you go into production\n" + + "plugins.security.ssl.transport.pemcert_filepath: esnode.pem\n" + + "plugins.security.ssl.transport.pemkey_filepath: esnode-key.pem\n" + + "plugins.security.ssl.transport.pemtrustedcas_filepath: root-ca.pem\n" + + "plugins.security.ssl.transport.enforce_hostname_verification: false\n" + + "plugins.security.ssl.http.enabled: true\n" + + "plugins.security.ssl.http.pemcert_filepath: esnode.pem\n" + + "plugins.security.ssl.http.pemkey_filepath: esnode-key.pem\n" + + "plugins.security.ssl.http.pemtrustedcas_filepath: root-ca.pem\n" + + "plugins.security.allow_unsafe_democertificates: true\n" + + "plugins.security.authcz.admin_dn:\n" + + " - CN=kirk,OU=client,O=client,L=test, C=de\n" + + "\n" + + "plugins.security.audit.type: internal_opensearch\n" + + "plugins.security.enable_snapshot_restore_privilege: true\n" + + "plugins.security.check_snapshot_restore_write_privileges: true\n" + + "plugins.security.restapi.roles_enabled: [\"all_access\", \"security_rest_api_access\"]\n" + + "plugins.security.system_indices.enabled: true\n" + + "plugins.security.system_indices.indices: [.plugins-ml-config, .plugins-ml-connector, .plugins-ml-model-group, .plugins-ml-model, .plugins-ml-task, .plugins-ml-conversation-meta, .plugins-ml-conversation-interactions, .opendistro-alerting-config, .opendistro-alerting-alert*, .opendistro-anomaly-results*, .opendistro-anomaly-detector*, .opendistro-anomaly-checkpoints, .opendistro-anomaly-detection-state, .opendistro-reports-*, .opensearch-notifications-*, .opensearch-notebooks, .opensearch-observability, .ql-datasources, .opendistro-asynchronous-search-response*, .replication-metadata-store, .opensearch-knn-models, .geospatial-ip2geo-data*]\n" + + "node.max_local_storage_nodes: 3\n" + + "######## End OpenSearch Security Demo Configuration ########\n"; + assertThat(actual, is(equalTo(expected))); + + Installer.initsecurity = true; + actual = SecuritySettingsConfigurer.buildSecurityConfigString(); + assertThat(actual, containsString("plugins.security.allow_default_init_securityindex: true\n")); + + Installer.cluster_mode = true; + actual = SecuritySettingsConfigurer.buildSecurityConfigString(); + assertThat(actual, containsString("network.host: 0.0.0.0\n")); + assertThat(actual, containsString("node.name: smoketestnode\n")); + assertThat(actual, containsString("cluster.initial_cluster_manager_nodes: smoketestnode\n")); + } + + @Test + public void testCreateSecurityAdminDemoScriptAndGetSecurityAdminCommands() throws IOException { + String demoPath = OPENSEARCH_CONF_DIR + "securityadmin_demo" + FILE_EXTENSION; + SecuritySettingsConfigurer.createSecurityAdminDemoScript("scriptPath", demoPath); + + assertThat(new File(demoPath).exists(), is(equalTo(true))); + + String[] commands = getSecurityAdminCommands("scriptPath"); + + try (BufferedReader reader = new BufferedReader(new FileReader(demoPath, StandardCharsets.UTF_8))) { + assertThat(reader.readLine(), is(commands[0])); + assertThat(reader.readLine(), is(equalTo(commands[1]))); + } + } + + @Test + public void testCreateSecurityAdminDemoScript_invalidPath() { + String demoPath = null; + try { + SecuritySettingsConfigurer.createSecurityAdminDemoScript("scriptPath", demoPath); + fail("Expected to throw Exception"); + } catch (IOException | NullPointerException e) { + // expected + } + } + + @SuppressWarnings("unchecked") + public static void setEnv(String key, String value) throws NoSuchFieldException, IllegalAccessException { + Class[] classes = Collections.class.getDeclaredClasses(); + Map env = System.getenv(); + for (Class cl : classes) { + if ("java.util.Collections$UnmodifiableMap".equals(cl.getName())) { + Field field = cl.getDeclaredField("m"); + field.setAccessible(true); + Object obj = field.get(env); + Map map = (Map) obj; + map.clear(); + map.put(key, value); + } + } + } + + @SuppressWarnings("unchecked") + public static void unsetEnv(String key) throws NoSuchFieldException, IllegalAccessException { + Class[] classes = Collections.class.getDeclaredClasses(); + Map env = System.getenv(); + for (Class cl : classes) { + if ("java.util.Collections$UnmodifiableMap".equals(cl.getName())) { + Field field = cl.getDeclaredField("m"); + field.setAccessible(true); + Object obj = field.get(env); + Map map = (Map) obj; + map.remove(key); + } + } + } + + void setUpConf() { + OPENSEARCH_CONF_DIR = System.getProperty("user.dir") + File.separator + "test-conf" + File.separator; + OPENSEARCH_CONF_FILE = OPENSEARCH_CONF_DIR + "opensearch.yml"; + String securityConfDir = OPENSEARCH_CONF_DIR + "opensearch-security" + File.separator; + createDirectory(securityConfDir); + createFile(securityConfDir + "internal_users.yml"); + createFile(OPENSEARCH_CONF_FILE); + } + + private String generateStrongPassword() { + String password = ""; + final PasswordValidator passwordValidator = PasswordValidator.of( + Settings.builder() + .put(SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, "(?=.*[A-Z])(?=.*[^a-zA-Z\\\\d])(?=.*[0-9])(?=.*[a-z]).{8,}") + .put(SECURITY_RESTAPI_PASSWORD_MIN_LENGTH, 8) + .build() + ); + while (passwordValidator.validate("admin", password) != RequestContentValidator.ValidationError.NONE) { + password = generatePassword(); + } + return password; + } +} diff --git a/src/test/java/org/opensearch/security/tools/democonfig/util/DemoConfigHelperUtil.java b/src/test/java/org/opensearch/security/tools/democonfig/util/DemoConfigHelperUtil.java new file mode 100644 index 0000000000..2ea8ecf3be --- /dev/null +++ b/src/test/java/org/opensearch/security/tools/democonfig/util/DemoConfigHelperUtil.java @@ -0,0 +1,46 @@ +package org.opensearch.security.tools.democonfig.util; + +import java.io.File; + +public class DemoConfigHelperUtil { + public static void createDirectory(String path) { + File directory = new File(path); + if (!directory.exists() && !directory.mkdirs()) { + throw new RuntimeException("Failed to create directory: " + path); + } + } + + public static void createFile(String path) { + try { + File file = new File(path); + if (!file.exists() && !file.createNewFile()) { + throw new RuntimeException("Failed to create file: " + path); + } + } catch (Exception e) { + // without this the catch, we would need to throw exception, + // which would then require modifying caller method signature + throw new RuntimeException("Failed to create file: " + path, e); + } + } + + public static void deleteDirectoryRecursive(String path) { + File directory = new File(path); + if (directory.exists()) { + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteDirectoryRecursive(file.getAbsolutePath()); + } else { + file.delete(); + } + } + } + // Delete the empty directory after all its content is deleted + directory.delete(); + System.out.println("Deleted directory: " + directory.getAbsolutePath()); + } else { + System.out.println("No directory found at: " + directory.getAbsolutePath()); + } + } +} diff --git a/src/test/java/org/opensearch/security/tools/democonfig/util/NoExitSecurityManager.java b/src/test/java/org/opensearch/security/tools/democonfig/util/NoExitSecurityManager.java new file mode 100644 index 0000000000..29d0f54b29 --- /dev/null +++ b/src/test/java/org/opensearch/security/tools/democonfig/util/NoExitSecurityManager.java @@ -0,0 +1,15 @@ +package org.opensearch.security.tools.democonfig.util; + +public class NoExitSecurityManager extends SecurityManager { + @Override + public void checkPermission(java.security.Permission perm) { + // Allow everything except System.exit code 0 &b -1 + if (perm instanceof java.lang.RuntimePermission && ("exitVM.0".equals(perm.getName()) || "exitVM.-1".equals(perm.getName()))) { + StringBuilder sb = new StringBuilder(); + sb.append("System.exit("); + sb.append(perm.getName().contains("0") ? 0 : -1); + sb.append(") blocked to allow print statement testing."); + throw new SecurityException(sb.toString()); + } + } +}