diff --git a/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java b/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java index e77d6a9f73..813b0e39de 100644 --- a/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java +++ b/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java @@ -25,6 +25,11 @@ public static void createRoleMappingFile(File destination) { copyResourceToFile(resource, destination); } + public static void createConfigFile(File destination) { + String resource = "config.yml"; + copyResourceToFile(resource, destination); + } + public static Path createConfigurationDirectory() { try { Path tempDirectory = Files.createTempDirectory("test-security-config"); diff --git a/src/integrationTest/java/org/opensearch/security/SecurityAdminLauncher.java b/src/integrationTest/java/org/opensearch/security/SecurityAdminLauncher.java index 0cd8b23f5d..997f879225 100644 --- a/src/integrationTest/java/org/opensearch/security/SecurityAdminLauncher.java +++ b/src/integrationTest/java/org/opensearch/security/SecurityAdminLauncher.java @@ -38,4 +38,17 @@ public int updateRoleMappings(File roleMappingsConfigurationFile) throws Excepti return SecurityAdmin.execute(commandLineArguments); } + + public int updateConfig(File configFile) throws Exception { + String[] commandLineArguments = {"-cacert", certificates.getRootCertificate().getAbsolutePath(), + "-cert", certificates.getAdminCertificate().getAbsolutePath(), + "-key", certificates.getAdminKey(null).getAbsolutePath(), + "-nhnv", + "-p", String.valueOf(port), + "-f", configFile.getAbsolutePath(), + "-t", "config" + }; + + return SecurityAdmin.execute(commandLineArguments); + } } diff --git a/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java b/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java index 2caf05536b..9409809bc8 100644 --- a/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java +++ b/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java @@ -15,6 +15,7 @@ import java.util.Map; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import com.fasterxml.jackson.databind.JsonNode; import org.awaitility.Awaitility; import org.junit.BeforeClass; import org.junit.ClassRule; @@ -46,7 +47,7 @@ public class SecurityConfigurationTests { private static final User USER_ADMIN = new User("admin").roles(ALL_ACCESS); private static final User LIMITED_USER = new User("limited-user") - .roles(new Role("limited-role").indexPermissions("indices:data/read/search", "indices:data/read/get").on("user-${user.name}")); + .roles(new Role("limited-role").indexPermissions("indices:data/read/search", "indices:data/read/get").on("user-${user.name}")); public static final String LIMITED_USER_INDEX = "user-" + LIMITED_USER.getName(); public static final String ADDITIONAL_USER_1 = "additional00001"; public static final String ADDITIONAL_PASSWORD_1 = ADDITIONAL_USER_1; @@ -61,11 +62,11 @@ public class SecurityConfigurationTests { @ClassRule public static LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) - .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_ADMIN, LIMITED_USER).anonymousAuth(false) - .nodeSettings(Map.of(SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + USER_ADMIN.getName() +"__" + ALL_ACCESS.getName()), - SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, true)) - .build(); + .clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_ADMIN, LIMITED_USER).anonymousAuth(false) + .nodeSettings(Map.of(SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + USER_ADMIN.getName() +"__" + ALL_ACCESS.getName()), + SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, true)) + .build(); @Rule public TemporaryFolder configurationDirectory = new TemporaryFolder(); @@ -82,7 +83,7 @@ public static void initData() { public void shouldCreateUserViaRestApi_success() { try(TestRestClient client = cluster.getRestClient(USER_ADMIN)) { HttpResponse httpResponse = client.putJson(INTERNAL_USERS_RESOURCE + ADDITIONAL_USER_1, String.format(CREATE_USER_BODY, - ADDITIONAL_PASSWORD_1)); + ADDITIONAL_PASSWORD_1)); assertThat(httpResponse.getStatusCode(), equalTo(201)); } @@ -98,7 +99,7 @@ public void shouldCreateUserViaRestApi_success() { public void shouldCreateUserViaRestApi_failure() { try(TestRestClient client = cluster.getRestClient(LIMITED_USER)) { HttpResponse httpResponse = client.putJson(INTERNAL_USERS_RESOURCE + ADDITIONAL_USER_1, String.format(CREATE_USER_BODY, - ADDITIONAL_PASSWORD_1)); + ADDITIONAL_PASSWORD_1)); httpResponse.assertStatusCode(403); } @@ -141,7 +142,7 @@ public void shouldCreateUserViaRestApiWhenAdminIsAuthenticatedViaCertificate_pos try(TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { HttpResponse httpResponse = client.putJson(INTERNAL_USERS_RESOURCE + ADDITIONAL_USER_2, String.format(CREATE_USER_BODY, - ADDITIONAL_PASSWORD_2)); + ADDITIONAL_PASSWORD_2)); httpResponse.assertStatusCode(201); } @@ -158,7 +159,7 @@ public void shouldCreateUserViaRestApiWhenAdminIsAuthenticatedViaCertificate_neg TestCertificates testCertificates = cluster.getTestCertificates(); try(TestRestClient client = cluster.getRestClient(testCertificates.createSelfSignedCertificate("CN=attacker"))) { HttpResponse httpResponse = client.putJson(INTERNAL_USERS_RESOURCE + ADDITIONAL_USER_2, String.format(CREATE_USER_BODY, - ADDITIONAL_PASSWORD_2)); + ADDITIONAL_PASSWORD_2)); httpResponse.assertStatusCode(401); } @@ -209,7 +210,28 @@ public void shouldUseSecurityAdminTool() throws Exception { assertThat(exitCode, equalTo(0)); try(TestRestClient client = cluster.getRestClient(USER_ADMIN)) { Awaitility.await().alias("Waiting for rolemapping 'readall' availability.") - .until(() -> client.get("_plugins/_security/api/rolesmapping/readall").getStatusCode(), equalTo(200)); + .until(() -> client.get("_plugins/_security/api/rolesmapping/readall").getStatusCode(), equalTo(200)); + } + } + + @Test + public void shouldReloadExtensionsConfigurationFromFile() throws Exception { + SecurityAdminLauncher securityAdminLauncher = new SecurityAdminLauncher(cluster.getHttpPort(), cluster.getTestCertificates()); + File config = configurationDirectory.newFile("config.yml"); + ConfigurationFiles.createConfigFile(config); + int exitCode = securityAdminLauncher.updateConfig(config); + assertThat(exitCode, equalTo(0)); + + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Awaitility.await() + .until(() -> + { + HttpResponse httpResponse = client.get("_plugins/_security/api/securityconfig"); + JsonNode jsonNode = DefaultObjectMapper.objectMapper.readTree(httpResponse.getBody()); + return jsonNode.get("config").get("dynamic").get("extensions"); + + }, jsonNode -> jsonNode.get("encryption_key").asText().equals("encryption key") && jsonNode.get("signing_key").asText().equals("signing key") + ); } } } diff --git a/src/integrationTest/java/org/opensearch/test/framework/matcher/GetSettingsResponseContainsIndicesMatcher.java b/src/integrationTest/java/org/opensearch/test/framework/matcher/GetSettingsResponseContainsIndicesMatcher.java index 01f14dd044..d88b65875e 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/matcher/GetSettingsResponseContainsIndicesMatcher.java +++ b/src/integrationTest/java/org/opensearch/test/framework/matcher/GetSettingsResponseContainsIndicesMatcher.java @@ -9,11 +9,12 @@ */ package org.opensearch.test.framework.matcher; +import java.util.Map; + import org.hamcrest.Description; import org.hamcrest.TypeSafeDiagnosingMatcher; import org.opensearch.action.admin.indices.settings.get.GetSettingsResponse; -import org.opensearch.common.collect.ImmutableOpenMap; import org.opensearch.common.settings.Settings; import static java.util.Objects.isNull; @@ -32,12 +33,12 @@ class GetSettingsResponseContainsIndicesMatcher extends TypeSafeDiagnosingMatche @Override protected boolean matchesSafely(GetSettingsResponse response, Description mismatchDescription) { - final ImmutableOpenMap indexToSettings = response.getIndexToSettings(); + final Map indexToSettings = response.getIndexToSettings(); for (String index : expectedIndices) { if (!indexToSettings.containsKey(index)) { mismatchDescription .appendText("Response contains settings of indices: ") - .appendValue(indexToSettings.keys()); + .appendValue(indexToSettings.keySet().toArray()); return false; } } diff --git a/src/integrationTest/resources/config.yml b/src/integrationTest/resources/config.yml index 5e929c0e2a..3d4be02946 100644 --- a/src/integrationTest/resources/config.yml +++ b/src/integrationTest/resources/config.yml @@ -15,3 +15,6 @@ config: authentication_backend: type: "internal" config: {} + extensions: + signing_key: "signing key" + encryption_key: "encryption key" diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java index 3deaf4db95..16d1248820 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java @@ -11,6 +11,7 @@ package org.opensearch.security.authtoken.jwt; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Base64; @@ -29,7 +30,7 @@ public static String encrypt(final String secret, final String data) { // rebuild key using SecretKeySpec SecretKey originalKey = new SecretKeySpec(Arrays.copyOf(decodedKey, 16), "AES"); cipher.init(Cipher.ENCRYPT_MODE, originalKey); - byte[] cipherText = cipher.doFinal(data.getBytes("UTF-8")); + byte[] cipherText = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(cipherText); } catch (Exception e) { throw new RuntimeException( @@ -47,7 +48,7 @@ public static String decrypt(final String secret, final String encryptedString) SecretKey originalKey = new SecretKeySpec(Arrays.copyOf(decodedKey, 16), "AES"); cipher.init(Cipher.DECRYPT_MODE, originalKey); byte[] cipherText = cipher.doFinal(Base64.getDecoder().decode(encryptedString)); - return new String(cipherText); + return new String(cipherText, StandardCharsets.UTF_8); } catch (Exception e) { throw new RuntimeException("Error occured while decrypting data", e); } diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java index f91e768283..0033b3bf5f 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java @@ -38,6 +38,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.common.settings.Settings; import org.opensearch.security.auth.AuthDomain; import org.opensearch.security.auth.AuthFailureListener; import org.opensearch.security.auth.AuthorizationBackend; @@ -80,7 +81,7 @@ public abstract class DynamicConfigModel { public abstract Multimap getAuthBackendFailureListeners(); public abstract List> getIpClientBlockRegistries(); public abstract Multimap> getAuthBackendClientBlockRegistries(); - + public abstract Settings getDynamicExtensionsSettings(); protected final Map authImplMap = new HashMap<>(); public DynamicConfigModel() { diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java index 40b3e3319a..515ad6dcac 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java @@ -188,7 +188,12 @@ public List> getIpClientBlockRegistries() { public Multimap> getAuthBackendClientBlockRegistries() { return Multimaps.unmodifiableMultimap(authBackendClientBlockRegistries); } - + + @Override + public Settings getDynamicExtensionsSettings() { + return Settings.EMPTY; + } + private void buildAAA() { final SortedSet restAuthDomains0 = new TreeSet<>(); diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index 6db5fba0a7..fa914677a7 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -188,7 +188,13 @@ public List> getIpClientBlockRegistries() { public Multimap> getAuthBackendClientBlockRegistries() { return Multimaps.unmodifiableMultimap(authBackendClientBlockRegistries); } - + + @Override + public Settings getDynamicExtensionsSettings() { + return Settings.builder() + .put(Settings.builder().loadFromSource(config.dynamic.extensions.configAsJson(), XContentType.JSON).build()) + .build(); + } private void buildAAA() { diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java b/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java index 6599942d34..99d83d0679 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java @@ -37,6 +37,7 @@ import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import org.opensearch.security.DefaultObjectMapper; @@ -71,11 +72,12 @@ public static class Dynamic { public String hosts_resolver_mode = "ip-only"; public String transport_userrname_attribute; public boolean do_not_fail_on_forbidden_empty; - + public Extensions extensions = new Extensions(); + @Override public String toString() { return "Dynamic [filtered_alias_mode=" + filtered_alias_mode + ", kibana=" + kibana + ", http=" + http + ", authc=" + authc + ", authz=" - + authz + "]"; + + authz + ", extensions=" + extensions + "]"; } } @@ -319,5 +321,32 @@ public String toString() { } - + + public static class Extensions { + @JsonProperty("signing_key") + private String signingKey; + @JsonProperty("encryption_key") + private String encryptionKey; + + public String getSigningKey() { + return signingKey; + } + + public void setSigningKey(String signingKey) { + this.signingKey = signingKey; + } + + public String getEncryptionKey() { + return encryptionKey; + } + + public void setEncryptionKey(String encryptionKey) { + this.encryptionKey = encryptionKey; + } + + @Override + public String toString() { + return "Extensions [signing_key=" + signingKey + ", encryption_key=" + encryptionKey +"]"; + } + } } diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java index 3029b42abf..e8ecd6bd97 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java @@ -37,6 +37,7 @@ import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import org.opensearch.security.DefaultObjectMapper; @@ -125,11 +126,12 @@ public static class Dynamic { public String hosts_resolver_mode = "ip-only"; public String transport_userrname_attribute; public boolean do_not_fail_on_forbidden_empty; + public Extensions extensions = new Extensions(); @Override public String toString() { return "Dynamic [filtered_alias_mode=" + filtered_alias_mode + ", kibana=" + kibana + ", http=" + http + ", authc=" + authc + ", authz=" - + authz + "]"; + + authz + ", extensions=" + extensions + "]"; } } @@ -461,8 +463,43 @@ public String toString() { return "AuthzDomain [http_enabled=" + http_enabled + ", transport_enabled=" + transport_enabled + ", authorization_backend=" + authorization_backend + ", description=" + description + "]"; } - - + + } + + public static class Extensions { + @JsonProperty("signing_key") + private String signingKey; + @JsonProperty("encryption_key") + private String encryptionKey; + + @JsonIgnore + public String configAsJson() { + try { + return DefaultObjectMapper.writeValueAsString(this, false); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public String getSigningKey() { + return signingKey; + } + + public void setSigningKey(String signingKey) { + this.signingKey = signingKey; + } + + public String getEncryptionKey() { + return encryptionKey; + } + + public void setEncryptionKey(String encryptionKey) { + this.encryptionKey = encryptionKey; + } + + @Override + public String toString() { + return "Extensions [signing_key=" + signingKey + ", encryption_key=" + encryptionKey +"]"; + } } - } diff --git a/src/test/resources/config.yml b/src/test/resources/config.yml index 3663b3c706..abdeb86421 100644 --- a/src/test/resources/config.yml +++ b/src/test/resources/config.yml @@ -96,3 +96,6 @@ config: multi_rolespan_enabled: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null + extensions: + signing_key: "signing key" + encryption_key: "encryption key" diff --git a/src/test/resources/restapi/securityconfig.json b/src/test/resources/restapi/securityconfig.json index 4e4b1bba63..3fbc385eb8 100644 --- a/src/test/resources/restapi/securityconfig.json +++ b/src/test/resources/restapi/securityconfig.json @@ -153,7 +153,9 @@ "do_not_fail_on_forbidden":false, "multi_rolespan_enabled":false, "hosts_resolver_mode":"ip-only", - "do_not_fail_on_forbidden_empty":false + "do_not_fail_on_forbidden_empty":false, + "extensions": { + } } } diff --git a/src/test/resources/restapi/securityconfig_nondefault.json b/src/test/resources/restapi/securityconfig_nondefault.json index 6fb297be37..4437941815 100644 --- a/src/test/resources/restapi/securityconfig_nondefault.json +++ b/src/test/resources/restapi/securityconfig_nondefault.json @@ -170,6 +170,10 @@ "do_not_fail_on_forbidden" : false, "multi_rolespan_enabled" : true, "hosts_resolver_mode" : "ip-only", - "do_not_fail_on_forbidden_empty" : false + "do_not_fail_on_forbidden_empty" : false, + "extensions": { + "signing_key": "signing key", + "encryption_key": "encryption key" + } } }