Skip to content

Commit 91efc3b

Browse files
Updates admin password string only if correct hash is present (opensearch-project#4100)
Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
1 parent 25e2e51 commit 91efc3b

File tree

3 files changed

+120
-37
lines changed

3 files changed

+120
-37
lines changed

src/main/java/org/opensearch/security/tools/democonfig/Installer.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.io.BufferedReader;
1515
import java.io.File;
1616
import java.io.FileReader;
17+
import java.io.IOException;
1718
import java.nio.charset.StandardCharsets;
1819
import java.nio.file.Files;
1920
import java.nio.file.Path;
@@ -97,7 +98,7 @@ public static Installer getInstance() {
9798
* Installs the demo security configuration
9899
* @param options the options passed to the script
99100
*/
100-
public void installDemoConfiguration(String[] options) {
101+
public void installDemoConfiguration(String[] options) throws IOException {
101102
readOptions(options);
102103
printScriptHeaders();
103104
gatherUserInputs();
@@ -108,7 +109,7 @@ public void installDemoConfiguration(String[] options) {
108109
finishScriptExecution();
109110
}
110111

111-
public static void main(String[] options) {
112+
public static void main(String[] options) throws IOException {
112113
Installer installer = Installer.getInstance();
113114
installer.buildOptions();
114115
installer.installDemoConfiguration(options);

src/main/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurer.java

+42-29
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,22 @@
1212
package org.opensearch.security.tools.democonfig;
1313

1414
import java.io.BufferedReader;
15-
import java.io.BufferedWriter;
1615
import java.io.File;
16+
import java.io.FileInputStream;
1717
import java.io.FileReader;
1818
import java.io.FileWriter;
1919
import java.io.IOException;
2020
import java.nio.charset.StandardCharsets;
21-
import java.nio.file.Files;
22-
import java.nio.file.Path;
23-
import java.nio.file.Paths;
2421
import java.util.LinkedHashMap;
2522
import java.util.List;
2623
import java.util.Map;
2724

25+
import com.fasterxml.jackson.core.type.TypeReference;
2826
import com.fasterxml.jackson.databind.JsonNode;
27+
import org.bouncycastle.crypto.generators.OpenBSDBCrypt;
2928

3029
import org.opensearch.common.settings.Settings;
3130
import org.opensearch.core.common.Strings;
32-
import org.opensearch.security.DefaultObjectMapper;
3331
import org.opensearch.security.dlic.rest.validation.PasswordValidator;
3432
import org.opensearch.security.dlic.rest.validation.RequestContentValidator;
3533
import org.opensearch.security.support.ConfigConstants;
@@ -38,6 +36,7 @@
3836
import org.yaml.snakeyaml.DumperOptions;
3937
import org.yaml.snakeyaml.Yaml;
4038

39+
import static org.opensearch.security.DefaultObjectMapper.YAML_MAPPER;
4140
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH;
4241
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX;
4342

@@ -81,6 +80,7 @@ public class SecuritySettingsConfigurer {
8180
static String ADMIN_USERNAME = "admin";
8281

8382
private final Installer installer;
83+
static final String DEFAULT_ADMIN_PASSWORD = "admin";
8484

8585
public SecuritySettingsConfigurer(Installer installer) {
8686
this.installer = installer;
@@ -92,7 +92,7 @@ public SecuritySettingsConfigurer(Installer installer) {
9292
* 2. Sets the custom admin password (Generates one if none is provided)
9393
* 3. Write the security config to opensearch.yml
9494
*/
95-
public void configureSecuritySettings() {
95+
public void configureSecuritySettings() throws IOException {
9696
checkIfSecurityPluginIsAlreadyConfigured();
9797
updateAdminPassword();
9898
writeSecurityConfigToOpenSearchYML();
@@ -125,9 +125,17 @@ void checkIfSecurityPluginIsAlreadyConfigured() {
125125
/**
126126
* Replaces the admin password in internal_users.yml with the custom or generated password
127127
*/
128-
void updateAdminPassword() {
128+
void updateAdminPassword() throws IOException {
129129
String INTERNAL_USERS_FILE_PATH = installer.OPENSEARCH_CONF_DIR + "opensearch-security" + File.separator + "internal_users.yml";
130130
boolean shouldValidatePassword = installer.environment.equals(ExecutionEnvironment.DEMO);
131+
132+
// check if the password `admin` is present, if not skip updating admin password
133+
if (!isAdminPasswordSetToAdmin(INTERNAL_USERS_FILE_PATH)) {
134+
System.out.println("Admin password seems to be custom configured. Skipping update to admin password.");
135+
return;
136+
}
137+
138+
// if hashed value for default password "admin" is found, update it with the custom password.
131139
try {
132140
final PasswordValidator passwordValidator = PasswordValidator.of(
133141
Settings.builder()
@@ -169,17 +177,29 @@ void updateAdminPassword() {
169177
System.exit(-1);
170178
}
171179

172-
// Print an update to the logs
173-
System.out.println("Admin password set successfully.");
174-
180+
// Update the custom password in internal_users.yml file
175181
writePasswordToInternalUsersFile(ADMIN_PASSWORD, INTERNAL_USERS_FILE_PATH);
176182

183+
System.out.println("Admin password set successfully.");
184+
177185
} catch (IOException e) {
178186
System.out.println("Exception updating the admin password : " + e.getMessage());
179187
System.exit(-1);
180188
}
181189
}
182190

191+
/**
192+
* Check if the password for admin user was already updated. (Possibly via a custom internal_users.yml)
193+
* @param internalUsersFile Path to internal_users.yml file
194+
* @return true if password was already updated, false otherwise
195+
* @throws IOException if there was an error while reading the file
196+
*/
197+
private boolean isAdminPasswordSetToAdmin(String internalUsersFile) throws IOException {
198+
JsonNode internalUsers = YAML_MAPPER.readTree(new FileInputStream(internalUsersFile));
199+
return internalUsers.has("admin")
200+
&& OpenBSDBCrypt.checkPassword(internalUsers.get("admin").get("hash").asText(), DEFAULT_ADMIN_PASSWORD.toCharArray());
201+
}
202+
183203
/**
184204
* Generate password hash and update it in the internal_users.yml file
185205
* @param adminPassword the password to be hashed and updated
@@ -190,31 +210,24 @@ void writePasswordToInternalUsersFile(String adminPassword, String internalUsers
190210
String hashedAdminPassword = Hasher.hash(adminPassword.toCharArray());
191211

192212
if (hashedAdminPassword.isEmpty()) {
193-
System.out.println("Hash the admin password failure, see console for details");
213+
System.out.println("Failure while hashing the admin password, see console for details.");
194214
System.exit(-1);
195215
}
196216

197-
Path tempFilePath = Paths.get(internalUsersFile + ".tmp");
198-
Path internalUsersPath = Paths.get(internalUsersFile);
199-
200-
try (
201-
BufferedReader reader = new BufferedReader(new FileReader(internalUsersFile, StandardCharsets.UTF_8));
202-
BufferedWriter writer = new BufferedWriter(new FileWriter(tempFilePath.toFile(), StandardCharsets.UTF_8))
203-
) {
204-
String line;
205-
while ((line = reader.readLine()) != null) {
206-
if (line.matches(" *hash: *\"\\$2a\\$12\\$VcCDgh2NDk07JGN0rjGbM.Ad41qVR/YFJcgHp0UGns5JDymv..TOG\"")) {
207-
line = line.replace(
208-
"\"$2a$12$VcCDgh2NDk07JGN0rjGbM.Ad41qVR/YFJcgHp0UGns5JDymv..TOG\"",
209-
"\"" + hashedAdminPassword + "\""
210-
);
211-
}
212-
writer.write(line + System.lineSeparator());
217+
try {
218+
var map = YAML_MAPPER.readValue(new File(internalUsersFile), new TypeReference<Map<String, LinkedHashMap<String, Object>>>() {
219+
});
220+
var admin = map.get("admin");
221+
if (admin != null) {
222+
// Replace the password since the default password was found via the check: isAdminPasswordSetToAdmin(..)
223+
admin.put("hash", hashedAdminPassword);
213224
}
225+
226+
// Write the updated map back to the internal_users.yml file
227+
YAML_MAPPER.writeValue(new File(internalUsersFile), map);
214228
} catch (IOException e) {
215229
throw new IOException("Unable to update the internal users file with the hashed password.");
216230
}
217-
Files.move(tempFilePath, internalUsersPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
218231
}
219232

220233
/**
@@ -329,7 +342,7 @@ static boolean isNodeMaxLocalStorageNodesAlreadyPresent(String filePath) {
329342
static boolean isKeyPresentInYMLFile(String filePath, String key) throws IOException {
330343
JsonNode node;
331344
try {
332-
node = DefaultObjectMapper.YAML_MAPPER.readTree(new File(filePath));
345+
node = YAML_MAPPER.readTree(new File(filePath));
333346
} catch (IOException e) {
334347
throw new RuntimeException(e);
335348
}

src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java

+75-6
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,22 @@
2121
import java.io.PrintStream;
2222
import java.lang.reflect.Field;
2323
import java.nio.charset.StandardCharsets;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
26+
import java.nio.file.Paths;
27+
import java.util.Arrays;
2428
import java.util.Collections;
2529
import java.util.List;
2630
import java.util.Map;
2731

32+
import org.apache.commons.lang3.RandomStringUtils;
2833
import org.junit.After;
2934
import org.junit.Before;
3035
import org.junit.Test;
3136
import org.junit.runner.RunWith;
3237

3338
import org.opensearch.security.support.ConfigConstants;
39+
import org.opensearch.security.tools.Hasher;
3440
import org.opensearch.security.tools.democonfig.util.NoExitSecurityManager;
3541

3642
import static org.hamcrest.MatcherAssert.assertThat;
@@ -39,6 +45,7 @@
3945
import static org.hamcrest.Matchers.is;
4046
import static org.opensearch.security.dlic.rest.validation.RequestContentValidator.ValidationError.INVALID_PASSWORD_INVALID_REGEX;
4147
import static org.opensearch.security.dlic.rest.validation.RequestContentValidator.ValidationError.INVALID_PASSWORD_TOO_SHORT;
48+
import static org.opensearch.security.tools.democonfig.SecuritySettingsConfigurer.DEFAULT_ADMIN_PASSWORD;
4249
import static org.opensearch.security.tools.democonfig.SecuritySettingsConfigurer.DEFAULT_PASSWORD_MIN_LENGTH;
4350
import static org.opensearch.security.tools.democonfig.SecuritySettingsConfigurer.REST_ENABLED_ROLES;
4451
import static org.opensearch.security.tools.democonfig.SecuritySettingsConfigurer.SYSTEM_INDICES;
@@ -66,13 +73,14 @@ public class SecuritySettingsConfigurerTests {
6673
private static Installer installer;
6774

6875
@Before
69-
public void setUp() {
76+
public void setUp() throws IOException {
7077
System.setOut(new PrintStream(outContent));
7178
System.setErr(new PrintStream(outContent));
7279
installer = Installer.getInstance();
7380
installer.buildOptions();
7481
securitySettingsConfigurer = new SecuritySettingsConfigurer(installer);
7582
setUpConf();
83+
setUpInternalUsersYML();
7684
}
7785

7886
@After
@@ -87,7 +95,7 @@ public void tearDown() throws NoSuchFieldException, IllegalAccessException {
8795
}
8896

8997
@Test
90-
public void testUpdateAdminPasswordWithCustomPassword() throws NoSuchFieldException, IllegalAccessException {
98+
public void testUpdateAdminPasswordWithCustomPassword() throws NoSuchFieldException, IllegalAccessException, IOException {
9199
String customPassword = "myStrongPassword123";
92100
setEnv(adminPasswordKey, customPassword);
93101

@@ -104,7 +112,7 @@ public void testUpdateAdminPassword_noPasswordSupplied() {
104112
try {
105113
System.setSecurityManager(new NoExitSecurityManager());
106114
securitySettingsConfigurer.updateAdminPassword();
107-
} catch (SecurityException e) {
115+
} catch (SecurityException | IOException e) {
108116
assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing."));
109117
} finally {
110118
System.setSecurityManager(null);
@@ -125,7 +133,7 @@ public void testUpdateAdminPasswordWithWeakPassword() throws NoSuchFieldExceptio
125133
try {
126134
System.setSecurityManager(new NoExitSecurityManager());
127135
securitySettingsConfigurer.updateAdminPassword();
128-
} catch (SecurityException e) {
136+
} catch (SecurityException | IOException e) {
129137
assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing."));
130138
} finally {
131139
System.setSecurityManager(null);
@@ -148,7 +156,7 @@ public void testUpdateAdminPasswordWithShortPassword() throws NoSuchFieldExcepti
148156
try {
149157
System.setSecurityManager(new NoExitSecurityManager());
150158
securitySettingsConfigurer.updateAdminPassword();
151-
} catch (SecurityException e) {
159+
} catch (SecurityException | IOException e) {
152160
assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing."));
153161
} finally {
154162
System.setSecurityManager(null);
@@ -160,7 +168,8 @@ public void testUpdateAdminPasswordWithShortPassword() throws NoSuchFieldExcepti
160168
}
161169

162170
@Test
163-
public void testUpdateAdminPasswordWithWeakPassword_skipPasswordValidation() throws NoSuchFieldException, IllegalAccessException {
171+
public void testUpdateAdminPasswordWithWeakPassword_skipPasswordValidation() throws NoSuchFieldException, IllegalAccessException,
172+
IOException {
164173
setEnv(adminPasswordKey, "weakpassword");
165174
installer.environment = ExecutionEnvironment.TEST;
166175
securitySettingsConfigurer.updateAdminPassword();
@@ -170,6 +179,49 @@ public void testUpdateAdminPasswordWithWeakPassword_skipPasswordValidation() thr
170179
verifyStdOutContainsString("Admin password set successfully.");
171180
}
172181

182+
@Test
183+
public void testUpdateAdminPasswordWithCustomInternalUsersYML() throws IOException {
184+
String internalUsersFile = installer.OPENSEARCH_CONF_DIR + "opensearch-security" + File.separator + "internal_users.yml";
185+
Path internalUsersFilePath = Paths.get(internalUsersFile);
186+
187+
List<String> newContent = Arrays.asList(
188+
"_meta:",
189+
" type: \"internalusers\"",
190+
" config_version: 2",
191+
"admin:",
192+
" hash: " + Hasher.hash(RandomStringUtils.randomAlphanumeric(16).toCharArray()),
193+
" backend_roles:",
194+
" - \"admin\""
195+
);
196+
// overwriting existing content
197+
Files.write(internalUsersFilePath, newContent, StandardCharsets.UTF_8);
198+
199+
securitySettingsConfigurer.updateAdminPassword();
200+
201+
verifyStdOutContainsString("Admin password seems to be custom configured. Skipping update to admin password.");
202+
}
203+
204+
@Test
205+
public void testUpdateAdminPasswordWithDefaultInternalUsersYml() {
206+
207+
SecuritySettingsConfigurer.ADMIN_PASSWORD = ""; // to ensure 0 flaky-ness
208+
try {
209+
System.setSecurityManager(new NoExitSecurityManager());
210+
securitySettingsConfigurer.updateAdminPassword();
211+
} catch (SecurityException | IOException e) {
212+
assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing."));
213+
} finally {
214+
System.setSecurityManager(null);
215+
}
216+
217+
verifyStdOutContainsString(
218+
String.format(
219+
"No custom admin password found. Please provide a password via the environment variable %s.",
220+
ConfigConstants.OPENSEARCH_INITIAL_ADMIN_PASSWORD
221+
)
222+
);
223+
}
224+
173225
@Test
174226
public void testSecurityPluginAlreadyConfigured() {
175227
securitySettingsConfigurer.writeSecurityConfigToOpenSearchYML();
@@ -353,4 +405,21 @@ void setUpConf() {
353405
private void verifyStdOutContainsString(String s) {
354406
assertThat(outContent.toString(), containsString(s));
355407
}
408+
409+
private void setUpInternalUsersYML() throws IOException {
410+
String internalUsersFile = installer.OPENSEARCH_CONF_DIR + "opensearch-security" + File.separator + "internal_users.yml";
411+
Path internalUsersFilePath = Paths.get(internalUsersFile);
412+
List<String> defaultContent = Arrays.asList(
413+
"_meta:",
414+
" type: \"internalusers\"",
415+
" config_version: 2",
416+
"admin:",
417+
" hash: " + Hasher.hash(DEFAULT_ADMIN_PASSWORD.toCharArray()),
418+
" reserved: " + true,
419+
" backend_roles:",
420+
" - \"admin\"",
421+
" description: Demo admin user"
422+
);
423+
Files.write(internalUsersFilePath, defaultContent, StandardCharsets.UTF_8);
424+
}
356425
}

0 commit comments

Comments
 (0)