Skip to content

Commit

Permalink
Updates admin password string only if correct hash is present (opense…
Browse files Browse the repository at this point in the history
…arch-project#4100)

Signed-off-by: Darshit Chanpura <[email protected]>
  • Loading branch information
DarshitChanpura authored Mar 19, 2024
1 parent 25e2e51 commit 91efc3b
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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<Map<String, LinkedHashMap<String, Object>>>() {
});
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);
}

/**
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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);

Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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();
Expand All @@ -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<String> 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();
Expand Down Expand Up @@ -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<String> 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);
}
}

0 comments on commit 91efc3b

Please sign in to comment.