diff --git a/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java index eecd4f16a1..2b5c801e8b 100644 --- a/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java @@ -41,6 +41,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; import static org.opensearch.security.http.CertificateAuthenticationTest.POINTER_BACKEND_ROLES; import static org.opensearch.security.http.DirectoryInformationTrees.CN_GROUP_ADMIN; import static org.opensearch.security.http.DirectoryInformationTrees.DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG; @@ -55,6 +56,8 @@ import static org.opensearch.security.http.DirectoryInformationTrees.USER_KIRK; import static org.opensearch.security.http.DirectoryInformationTrees.USER_SEARCH; import static org.opensearch.security.http.DirectoryInformationTrees.USER_SPOCK; +import static org.opensearch.security.support.ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.BASIC_AUTH_DOMAIN_ORDER; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; @@ -83,7 +86,16 @@ public class LdapAuthenticationTest { public static LocalCluster cluster = new LocalCluster.Builder().testCertificates(TEST_CERTIFICATES) .clusterManager(ClusterManager.SINGLENODE) .anonymousAuth(false) - .nodeSettings(Map.of(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + "." + ADMIN_USER.getName(), List.of(USER_KIRK))) + .nodeSettings( + Map.of( + ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + "." + ADMIN_USER.getName(), + List.of(USER_KIRK), + SECURITY_RESTAPI_ROLES_ENABLED, + List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()), + SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, + false + ) + ) .authc( new AuthcDomain("ldap", BASIC_AUTH_DOMAIN_ORDER + 1, true).httpAuthenticator(new HttpAuthenticator("basic").challenge(false)) .backend( @@ -190,4 +202,15 @@ public void testShouldCreateScrollWithLdapUserAndImpersonateWithAdmin() { scrollResponse.assertStatusCode(200); } } + + @Test + public void testShouldRedactPasswordWhenGettingSecurityConfig() { + try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { + TestRestClient.HttpResponse response = client.get("_plugins/_security/api/securityconfig"); + + response.assertStatusCode(200); + String redactedPassword = response.getTextFromJsonBody("/config/dynamic/authc/ldap/authentication_backend/config/password"); + assertThat("******", equalTo(redactedPassword)); + } + } } diff --git a/src/main/java/org/opensearch/security/DefaultObjectMapper.java b/src/main/java/org/opensearch/security/DefaultObjectMapper.java index 48aa09541a..2d18667c54 100644 --- a/src/main/java/org/opensearch/security/DefaultObjectMapper.java +++ b/src/main/java/org/opensearch/security/DefaultObjectMapper.java @@ -35,6 +35,7 @@ import com.google.common.collect.ImmutableSet; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; @@ -42,14 +43,40 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.exc.InvalidFormatException; import com.fasterxml.jackson.databind.exc.MismatchedInputException; import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import org.opensearch.SpecialPermission; +class ConfigMapSerializer extends StdSerializer> { + private static final Set SENSITIVE_CONFIG_KEYS = Set.of("password"); + + @SuppressWarnings("unchecked") + public ConfigMapSerializer() { + // Pass Map.class to the superclass + super((Class>) (Class) Map.class); + } + + @Override + public void serialize(Map value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + for (Map.Entry entry : value.entrySet()) { + if (SENSITIVE_CONFIG_KEYS.contains(entry.getKey())) { + gen.writeStringField(entry.getKey(), "******"); // Redact + } else { + gen.writeObjectField(entry.getKey(), entry.getValue()); + } + } + gen.writeEndObject(); + } +} + public class DefaultObjectMapper { public static final ObjectMapper objectMapper = new ObjectMapper(); public final static ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory()); @@ -180,6 +207,27 @@ public static String writeValueAsString(Object value, boolean omitDefaults) thro } + @SuppressWarnings("removal") + public static String writeValueAsStringAndRedactSensitive(Object value) throws JsonProcessingException { + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + SimpleModule module = new SimpleModule(); + module.addSerializer(new ConfigMapSerializer()); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(module); + + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> mapper.writeValueAsString(value)); + } catch (final PrivilegedActionException e) { + throw (JsonProcessingException) e.getCause(); + } + + } + @SuppressWarnings("removal") public static T readValue(String string, TypeReference tr) throws IOException { diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java index 04148e8b99..ef8a00d700 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java @@ -372,7 +372,12 @@ protected final ValidationResult> loadConfigurat boolean omitSensitiveData, final boolean logComplianceEvent ) { - final var configuration = load(cType, logComplianceEvent); + SecurityDynamicConfiguration configuration; + if (omitSensitiveData) { + configuration = loadAndRedact(cType, logComplianceEvent); + } else { + configuration = load(cType, logComplianceEvent); + } if (configuration.getSeqNo() < 0) { return ValidationResult.error( @@ -448,6 +453,14 @@ protected final SecurityDynamicConfiguration load(final CType config, boolean return DynamicConfigFactory.addStatics(loaded); } + protected final SecurityDynamicConfiguration loadAndRedact(final CType config, boolean logComplianceEvent) { + SecurityDynamicConfiguration loaded = securityApiDependencies.configurationRepository() + .getConfigurationsFromIndex(List.of(config), logComplianceEvent) + .get(config) + .deepCloneWithRedaction(); + return DynamicConfigFactory.addStatics(loaded); + } + protected boolean ensureIndexExists() { return clusterService.state().metadata().hasConcreteIndex(securityApiDependencies.securityIndexName()); } diff --git a/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java b/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java index bba44b5e28..90508840e7 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java @@ -303,6 +303,15 @@ public SecurityDynamicConfiguration deepClone() { } } + @JsonIgnore + public SecurityDynamicConfiguration deepCloneWithRedaction() { + try { + return fromJson(DefaultObjectMapper.writeValueAsStringAndRedactSensitive(this), ctype, version, seqNo, primaryTerm); + } catch (Exception e) { + throw ExceptionsHelper.convertToOpenSearchException(e); + } + } + @JsonIgnore public void remove(String key) { synchronized (modificationLock) {