diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68e966f56e..45f306318e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,7 @@ jobs: working-directory: downloaded-artifacts - name: Upload Coverage with retry - uses: Wandalen/wretry.action@v2.1.0 + uses: Wandalen/wretry.action@v3.1.0 with: attempt_limit: 5 attempt_delay: 2000 @@ -246,6 +246,8 @@ jobs: - run: ./gradlew clean assemble -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}-SNAPSHOT.zip + - run: ./gradlew clean publishPluginZipPublicationToZipStagingRepository && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.zip && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.pom + - name: List files in the build directory if there was an error run: ls -al ./build/distributions/ if: failure() diff --git a/README.md b/README.md index fe698a12d9..0d5d81e109 100644 --- a/README.md +++ b/README.md @@ -129,8 +129,7 @@ plugins.security.system_indices.indices: [".plugins-ml-model", ".plugins-ml-task The demo configuration can be modified in the following files to add a new system index to the demo configuration: -- https://github.com/opensearch-project/security/blob/main/tools/install_demo_configuration.sh -- https://github.com/opensearch-project/security/blob/main/tools/install_demo_configuration.bat +- https://github.com/opensearch-project/security/blob/main/src/main/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurer.java ## Contributing diff --git a/build.gradle b/build.gradle index ea392a8eac..51d9fedb7a 100644 --- a/build.gradle +++ b/build.gradle @@ -407,11 +407,6 @@ opensearchplugin { // This requires an additional Jar not published as part of build-tools loggerUsageCheck.enabled = false -// No need to validate pom, as we do not upload to maven/sonatype -tasks.matching {it.path in [":validateMavenPom", ":validateNebulaPom", ":validatePluginZipPom"]}.all { task -> - task.dependsOn ':generatePomFileForNebulaPublication', ':generatePomFileForPluginZipPublication', ':generatePomFileForMavenPublication' -} - publishing { publications { pluginZip(MavenPublication) { publication -> @@ -618,6 +613,7 @@ dependencies { //OpenSAML implementation 'net.shibboleth.utilities:java-support:8.4.1' + runtimeOnly "io.dropwizard.metrics:metrics-core:4.2.25" implementation "com.onelogin:java-saml:${one_login_java_saml}" implementation "com.onelogin:java-saml-core:${one_login_java_saml}" implementation "org.opensaml:opensaml-core:${open_saml_version}" @@ -635,7 +631,7 @@ dependencies { runtimeOnly "org.opensaml:opensaml-soap-impl:${open_saml_version}" implementation "org.opensaml:opensaml-storage-api:${open_saml_version}" - implementation "com.nulab-inc:zxcvbn:1.8.2" + implementation "com.nulab-inc:zxcvbn:1.9.0" runtimeOnly 'com.google.guava:failureaccess:1.0.2' runtimeOnly 'org.apache.commons:commons-text:1.11.0' @@ -643,13 +639,12 @@ dependencies { runtimeOnly 'com.google.j2objc:j2objc-annotations:2.8' compileOnly 'com.google.code.findbugs:jsr305:3.0.2' runtimeOnly 'org.lz4:lz4-java:1.8.0' - runtimeOnly 'io.dropwizard.metrics:metrics-core:4.2.25' runtimeOnly 'org.slf4j:slf4j-api:1.7.36' runtimeOnly "org.apache.logging.log4j:log4j-slf4j-impl:${versions.log4j}" runtimeOnly 'org.xerial.snappy:snappy-java:1.1.10.5' runtimeOnly 'org.codehaus.woodstox:stax2-api:4.2.2' runtimeOnly "org.glassfish.jaxb:txw2:${jaxb_version}" - runtimeOnly 'com.fasterxml.woodstox:woodstox-core:6.6.1' + runtimeOnly 'com.fasterxml.woodstox:woodstox-core:6.6.2' runtimeOnly 'org.apache.ws.xmlschema:xmlschema-core:2.3.1' runtimeOnly 'org.apache.santuario:xmlsec:2.3.4' runtimeOnly "com.github.luben:zstd-jni:${versions.zstd}" @@ -704,12 +699,12 @@ dependencies { exclude(group:'org.springframework', module: 'spring-jcl' ) } testRuntimeOnly 'org.scala-lang:scala-library:2.13.13' - testRuntimeOnly 'com.yammer.metrics:metrics-core:2.2.0' testRuntimeOnly 'com.typesafe.scala-logging:scala-logging_3:3.9.5' testRuntimeOnly('org.apache.zookeeper:zookeeper:3.9.2') { exclude(group:'ch.qos.logback', module: 'logback-classic' ) exclude(group:'ch.qos.logback', module: 'logback-core' ) } + testRuntimeOnly 'com.yammer.metrics:metrics-core:2.2.0' testRuntimeOnly "org.apache.kafka:kafka-metadata:${kafka_version}" testRuntimeOnly "org.apache.kafka:kafka-storage:${kafka_version}" @@ -725,7 +720,7 @@ dependencies { integrationTestImplementation 'junit:junit:4.13.2' integrationTestImplementation "org.opensearch.plugin:reindex-client:${opensearch_version}" integrationTestImplementation "org.opensearch.plugin:percolator-client:${opensearch_version}" - integrationTestImplementation 'commons-io:commons-io:2.15.1' + integrationTestImplementation 'commons-io:commons-io:2.16.0' integrationTestImplementation "org.apache.logging.log4j:log4j-core:${versions.log4j}" integrationTestImplementation "org.apache.logging.log4j:log4j-jul:${versions.log4j}" integrationTestImplementation 'org.hamcrest:hamcrest:2.2' @@ -742,7 +737,7 @@ dependencies { integrationTestImplementation "org.apache.httpcomponents:httpasyncclient:4.1.5" //spotless - implementation('com.google.googlejavaformat:google-java-format:1.21.0') { + implementation('com.google.googlejavaformat:google-java-format:1.22.0') { exclude group: 'com.google.guava' } } diff --git a/config/roles.yml b/config/roles.yml index e90a4e62a6..1b4c13745f 100644 --- a/config/roles.yml +++ b/config/roles.yml @@ -272,25 +272,39 @@ cross_cluster_search_remote_full_access: - 'indices:data/read/search' # Allow users to operate query assistant -ml_query_assistant_access: +query_assistant_access: reserved: true cluster_permissions: + - 'cluster:admin/opensearch/ml/config/get' - 'cluster:admin/opensearch/ml/execute' - - 'cluster:admin/opensearch/ml/memory/conversation/create' - - 'cluster:admin/opensearch/ml/memory/interaction/create' - 'cluster:admin/opensearch/ml/predict' + - 'cluster:admin/opensearch/ppl' # Allow users to read ML stats/models/tasks ml_read_access: reserved: true cluster_permissions: + - 'cluster:admin/opensearch/ml/config/get' - 'cluster:admin/opensearch/ml/connectors/get' - 'cluster:admin/opensearch/ml/connectors/search' + - 'cluster:admin/opensearch/ml/controllers/get' + - 'cluster:admin/opensearch/ml/memory/conversation/get' + - 'cluster:admin/opensearch/ml/memory/conversation/interaction/search' + - 'cluster:admin/opensearch/ml/memory/conversation/list' + - 'cluster:admin/opensearch/ml/memory/conversation/search' + - 'cluster:admin/opensearch/ml/memory/interaction/get' + - 'cluster:admin/opensearch/ml/memory/interaction/list' + - 'cluster:admin/opensearch/ml/memory/trace/get' + - 'cluster:admin/opensearch/ml/model_groups/get' - 'cluster:admin/opensearch/ml/model_groups/search' - 'cluster:admin/opensearch/ml/models/get' - 'cluster:admin/opensearch/ml/models/search' + - 'cluster:admin/opensearch/ml/profile/nodes' + - 'cluster:admin/opensearch/ml/stats/nodes' - 'cluster:admin/opensearch/ml/tasks/get' - 'cluster:admin/opensearch/ml/tasks/search' + - 'cluster:admin/opensearch/ml/tools/get' + - 'cluster:admin/opensearch/ml/tools/list' # Allows users to use all ML functionality ml_full_access: diff --git a/release-notes/opensearch-security.release-notes-2.13.0.0.md b/release-notes/opensearch-security.release-notes-2.13.0.0.md index 42ccd8fff0..48ecfa9b87 100644 --- a/release-notes/opensearch-security.release-notes-2.13.0.0.md +++ b/release-notes/opensearch-security.release-notes-2.13.0.0.md @@ -1,4 +1,4 @@ -## 2024-03-19 Version 2.13.0.0 +## Version 2.13.0.0 Compatible with OpenSearch 2.13.0 diff --git a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java b/src/integrationTest/java/org/opensearch/security/AbstractDefaultConfigurationTests.java similarity index 69% rename from src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java rename to src/integrationTest/java/org/opensearch/security/AbstractDefaultConfigurationTests.java index eb028c74e4..5387b3e516 100644 --- a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java +++ b/src/integrationTest/java/org/opensearch/security/AbstractDefaultConfigurationTests.java @@ -1,12 +1,12 @@ /* -* Copyright OpenSearch Contributors -* 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. -* -*/ + * Copyright OpenSearch Contributors + * 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. + * + */ package org.opensearch.security; import java.io.IOException; @@ -19,17 +19,16 @@ import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import com.fasterxml.jackson.databind.JsonNode; import org.apache.commons.io.FileUtils; +import org.apache.http.HttpStatus; import org.awaitility.Awaitility; import org.junit.AfterClass; -import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; -import org.opensearch.test.framework.TestSecurityConfig.User; -import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.security.state.SecurityMetadata; +import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; -import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.aMapWithSize; @@ -37,29 +36,22 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) -public class DefaultConfigurationTests { - - private final static Path configurationFolder = ConfigurationFiles.createConfigurationDirectory(); - private static final User ADMIN_USER = new User("admin"); - private static final User NEW_USER = new User("new-user"); - private static final User LIMITED_USER = new User("limited-user"); - - @ClassRule - public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) - .nodeSettings( - Map.of( - "plugins.security.allow_default_init_securityindex", - true, - "plugins.security.restapi.roles_enabled", - List.of("user_admin__all_access") - ) - ) - .defaultConfigurationInitDirectory(configurationFolder.toString()) - .loadConfigurationIntoIndex(false) - .build(); +public abstract class AbstractDefaultConfigurationTests { + public final static Path configurationFolder = ConfigurationFiles.createConfigurationDirectory(); + private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin"); + private static final TestSecurityConfig.User NEW_USER = new TestSecurityConfig.User("new-user"); + private static final TestSecurityConfig.User LIMITED_USER = new TestSecurityConfig.User("limited-user"); + + private final LocalCluster cluster; + + protected AbstractDefaultConfigurationTests(LocalCluster cluster) { + this.cluster = cluster; + } @AfterClass public static void cleanConfigurationDirectory() throws IOException { @@ -73,18 +65,43 @@ public void shouldLoadDefaultConfiguration() { } try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { client.confirmCorrectCredentials(ADMIN_USER.getName()); - HttpResponse response = client.get("_plugins/_security/api/internalusers"); - response.assertStatusCode(200); + TestRestClient.HttpResponse response = client.get("_plugins/_security/api/internalusers"); + response.assertStatusCode(HttpStatus.SC_OK); Map users = response.getBodyAs(Map.class); assertThat( + response.getBody(), users, allOf(aMapWithSize(3), hasKey(ADMIN_USER.getName()), hasKey(NEW_USER.getName()), hasKey(LIMITED_USER.getName())) ); } } + void assertClusterState(final TestRestClient client) { + if (cluster.node().settings().getAsBoolean("plugins.security.allow_default_init_securityindex.use_cluster_state", false)) { + final TestRestClient.HttpResponse response = client.get("_cluster/state"); + response.assertStatusCode(HttpStatus.SC_OK); + final var clusterState = response.getBodyAs(Map.class); + assertTrue(response.getBody(), clusterState.containsKey(SecurityMetadata.TYPE)); + @SuppressWarnings("unchecked") + final var securityClusterState = (Map) clusterState.get(SecurityMetadata.TYPE); + @SuppressWarnings("unchecked") + final var securityConfiguration = (Map) ((Map) clusterState.get(SecurityMetadata.TYPE)).get( + "configuration" + ); + assertTrue(response.getBody(), securityClusterState.containsKey("created")); + assertNotNull(response.getBody(), securityClusterState.get("created")); + + for (final var k : securityConfiguration.keySet()) { + @SuppressWarnings("unchecked") + final var sc = (Map) securityConfiguration.get(k); + assertTrue(response.getBody(), sc.containsKey("hash")); + assertTrue(response.getBody(), sc.containsKey("last_modified")); + } + } + } + @Test - public void securityRolesUgrade() throws Exception { + public void securityRolesUpgrade() throws Exception { try (var client = cluster.getRestClient(ADMIN_USER)) { // Setup: Make sure the config is ready before starting modifications Awaitility.await().alias("Load default configuration").until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); @@ -159,4 +176,5 @@ private Set extractFieldNames(final JsonNode json) { json.fieldNames().forEachRemaining(set::add); return set; } + } diff --git a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterTests.java b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterTests.java new file mode 100644 index 0000000000..704e2c7255 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterTests.java @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ +package org.opensearch.security; + +import java.util.List; +import java.util.Map; + +import org.junit.ClassRule; + +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +public class DefaultConfigurationMultiNodeClusterTests extends AbstractDefaultConfigurationTests { + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .nodeSettings( + Map.of( + "plugins.security.allow_default_init_securityindex", + true, + "plugins.security.restapi.roles_enabled", + List.of("user_admin__all_access") + ) + ) + .defaultConfigurationInitDirectory(configurationFolder.toString()) + .loadConfigurationIntoIndex(false) + .build(); + + public DefaultConfigurationMultiNodeClusterTests() { + super(cluster); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterUseClusterStateTests.java b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterUseClusterStateTests.java new file mode 100644 index 0000000000..8abffac9cf --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterUseClusterStateTests.java @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ +package org.opensearch.security; + +import java.util.List; +import java.util.Map; + +import org.junit.ClassRule; + +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +public class DefaultConfigurationMultiNodeClusterUseClusterStateTests extends AbstractDefaultConfigurationTests { + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .nodeSettings( + Map.of( + "plugins.security.allow_default_init_securityindex", + true, + "plugins.security.allow_default_init_securityindex.use_cluster_state", + true, + "plugins.security.restapi.roles_enabled", + List.of("user_admin__all_access") + ) + ) + .defaultConfigurationInitDirectory(configurationFolder.toString()) + .loadConfigurationIntoIndex(false) + .build(); + + public DefaultConfigurationMultiNodeClusterUseClusterStateTests() { + super(cluster); + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterTests.java b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterTests.java new file mode 100644 index 0000000000..362245db5e --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterTests.java @@ -0,0 +1,44 @@ +/* +* Copyright OpenSearch Contributors +* 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. +* +*/ +package org.opensearch.security; + +import java.util.List; +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.ClassRule; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class DefaultConfigurationSingleNodeClusterTests extends AbstractDefaultConfigurationTests { + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .nodeSettings( + Map.of( + "plugins.security.allow_default_init_securityindex", + true, + "plugins.security.restapi.roles_enabled", + List.of("user_admin__all_access") + ) + ) + .defaultConfigurationInitDirectory(configurationFolder.toString()) + .loadConfigurationIntoIndex(false) + .build(); + + public DefaultConfigurationSingleNodeClusterTests() { + super(cluster); + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterUseClusterStateTests.java b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterUseClusterStateTests.java new file mode 100644 index 0000000000..e05005e912 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterUseClusterStateTests.java @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ +package org.opensearch.security; + +import java.util.List; +import java.util.Map; + +import org.junit.ClassRule; + +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +public class DefaultConfigurationSingleNodeClusterUseClusterStateTests extends AbstractDefaultConfigurationTests { + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .nodeSettings( + Map.of( + "plugins.security.allow_default_init_securityindex", + true, + "plugins.security.allow_default_init_securityindex.use_cluster_state", + true, + "plugins.security.restapi.roles_enabled", + List.of("user_admin__all_access") + ) + ) + .defaultConfigurationInitDirectory(configurationFolder.toString()) + .loadConfigurationIntoIndex(false) + .build(); + + public DefaultConfigurationSingleNodeClusterUseClusterStateTests() { + super(cluster); + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/SecurityConfigurationBootstrapTests.java b/src/integrationTest/java/org/opensearch/security/SecurityConfigurationBootstrapTests.java index 5b83e0d6d0..e6af5d58bb 100644 --- a/src/integrationTest/java/org/opensearch/security/SecurityConfigurationBootstrapTests.java +++ b/src/integrationTest/java/org/opensearch/security/SecurityConfigurationBootstrapTests.java @@ -124,6 +124,7 @@ public void shouldStillLoadSecurityConfigDuringBootstrapAndActiveConfigUpdateReq .put("action_groups.yml", CType.ACTIONGROUPS) .put("config.yml", CType.CONFIG) .put("roles.yml", CType.ROLES) + .put("roles_mapping.yml", CType.ROLESMAPPING) .put("tenants.yml", CType.TENANTS) .build(); @@ -146,7 +147,7 @@ public void shouldStillLoadSecurityConfigDuringBootstrapAndActiveConfigUpdateReq // After the configuration has been loaded, the rest clients should be able to connect successfully cluster.triggerConfigurationReloadForCTypes( internalNodeClient, - List.of(CType.ACTIONGROUPS, CType.CONFIG, CType.ROLES, CType.TENANTS), + List.of(CType.ACTIONGROUPS, CType.CONFIG, CType.ROLES, CType.ROLESMAPPING, CType.TENANTS), true ); try (final TestRestClient freshClient = cluster.getRestClient(USER_ADMIN)) { diff --git a/src/integrationTest/java/org/opensearch/security/http/AnonymousAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/AnonymousAuthenticationTest.java index b1c13aeedc..99f96a388e 100644 --- a/src/integrationTest/java/org/opensearch/security/http/AnonymousAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/AnonymousAuthenticationTest.java @@ -17,7 +17,6 @@ import org.junit.Test; import org.junit.runner.RunWith; -import org.opensearch.test.framework.RolesMapping; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; @@ -45,9 +44,9 @@ public class AnonymousAuthenticationTest { /** * Maps {@link #ANONYMOUS_USER_CUSTOM_ROLE} to {@link #DEFAULT_ANONYMOUS_USER_BACKEND_ROLE_NAME} */ - private static final RolesMapping ANONYMOUS_USER_CUSTOM_ROLE_MAPPING = new RolesMapping(ANONYMOUS_USER_CUSTOM_ROLE).backendRoles( - DEFAULT_ANONYMOUS_USER_BACKEND_ROLE_NAME - ); + private static final TestSecurityConfig.RoleMapping ANONYMOUS_USER_CUSTOM_ROLE_MAPPING = new TestSecurityConfig.RoleMapping( + ANONYMOUS_USER_CUSTOM_ROLE.getName() + ).backendRoles(DEFAULT_ANONYMOUS_USER_BACKEND_ROLE_NAME); /** * User who is stored in the internal user database and can authenticate diff --git a/src/integrationTest/java/org/opensearch/security/http/AsyncTests.java b/src/integrationTest/java/org/opensearch/security/http/AsyncTests.java index 8103d50e74..514d2c45d1 100644 --- a/src/integrationTest/java/org/opensearch/security/http/AsyncTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/AsyncTests.java @@ -26,7 +26,6 @@ import org.opensearch.security.IndexOperationsHelper; import org.opensearch.security.support.ConfigConstants; import org.opensearch.test.framework.AsyncActions; -import org.opensearch.test.framework.RolesMapping; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -44,7 +43,7 @@ public class AsyncTests { public static LocalCluster cluster = new LocalCluster.Builder().singleNode() .authc(AUTHC_HTTPBASIC_INTERNAL) .users(ADMIN_USER) - .rolesMapping(new RolesMapping(ALL_ACCESS).backendRoles("admin")) + .rolesMapping(new TestSecurityConfig.RoleMapping(ALL_ACCESS.getName()).backendRoles("admin")) .anonymousAuth(false) .nodeSettings(Map.of(ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED, List.of(ALL_ACCESS.getName()))) .build(); diff --git a/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java index 975ce25efb..18ae232a91 100644 --- a/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java @@ -17,7 +17,7 @@ import org.junit.Test; import org.junit.runner.RunWith; -import org.opensearch.test.framework.RolesMapping; +import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.HttpAuthenticator; import org.opensearch.test.framework.TestSecurityConfig.Role; @@ -69,7 +69,7 @@ public class CertificateAuthenticationTest { .authc(AUTHC_HTTPBASIC_INTERNAL) .roles(ROLE_ALL_INDEX_SEARCH) .users(USER_ADMIN) - .rolesMapping(new RolesMapping(ROLE_ALL_INDEX_SEARCH).backendRoles(BACKEND_ROLE_BRIDGE)) + .rolesMapping(new TestSecurityConfig.RoleMapping(ROLE_ALL_INDEX_SEARCH.getName()).backendRoles(BACKEND_ROLE_BRIDGE)) .build(); private static final TestCertificates TEST_CERTIFICATES = cluster.getTestCertificates(); diff --git a/src/integrationTest/java/org/opensearch/security/http/CommonProxyAuthenticationTests.java b/src/integrationTest/java/org/opensearch/security/http/CommonProxyAuthenticationTests.java index d98acf8895..48ed08ac22 100644 --- a/src/integrationTest/java/org/opensearch/security/http/CommonProxyAuthenticationTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/CommonProxyAuthenticationTests.java @@ -13,7 +13,6 @@ import java.net.InetAddress; import java.util.List; -import org.opensearch.test.framework.RolesMapping; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -84,13 +83,13 @@ abstract class CommonProxyAuthenticationTests { .indexPermissions("indices:data/read/search") .on(PERSONAL_INDEX_NAME_PATTERN); - protected static final RolesMapping ROLES_MAPPING_CAPTAIN = new RolesMapping(ROLE_PERSONAL_INDEX_SEARCH).backendRoles( - BACKEND_ROLE_CAPTAIN - ); + protected static final TestSecurityConfig.RoleMapping ROLES_MAPPING_CAPTAIN = new TestSecurityConfig.RoleMapping( + ROLE_PERSONAL_INDEX_SEARCH.getName() + ).backendRoles(BACKEND_ROLE_CAPTAIN); - protected static final RolesMapping ROLES_MAPPING_FIRST_MATE = new RolesMapping(ROLE_ALL_INDEX_SEARCH).backendRoles( - BACKEND_ROLE_FIRST_MATE - ); + protected static final TestSecurityConfig.RoleMapping ROLES_MAPPING_FIRST_MATE = new TestSecurityConfig.RoleMapping( + ROLE_ALL_INDEX_SEARCH.getName() + ).backendRoles(BACKEND_ROLE_FIRST_MATE); protected abstract LocalCluster getCluster(); diff --git a/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java index 7339808d8c..090762af21 100644 --- a/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java @@ -27,7 +27,6 @@ import org.opensearch.test.framework.AuthzDomain; import org.opensearch.test.framework.LdapAuthenticationConfigBuilder; import org.opensearch.test.framework.LdapAuthorizationConfigBuilder; -import org.opensearch.test.framework.RolesMapping; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AuthenticationBackend; @@ -118,7 +117,7 @@ public class LdapAuthenticationTest { ) .authc(AUTHC_HTTPBASIC_INTERNAL) .users(ADMIN_USER) - .rolesMapping(new RolesMapping(ALL_ACCESS).backendRoles(CN_GROUP_ADMIN)) + .rolesMapping(new TestSecurityConfig.RoleMapping(ALL_ACCESS.getName()).backendRoles(CN_GROUP_ADMIN)) .authz( new AuthzDomain("ldap_roles").httpEnabled(true) .authorizationBackend( diff --git a/src/integrationTest/java/org/opensearch/security/http/LdapTlsAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/LdapTlsAuthenticationTest.java index 32265f4b81..e5c1012bdc 100644 --- a/src/integrationTest/java/org/opensearch/security/http/LdapTlsAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/LdapTlsAuthenticationTest.java @@ -30,7 +30,7 @@ import org.opensearch.test.framework.AuthzDomain; import org.opensearch.test.framework.LdapAuthenticationConfigBuilder; import org.opensearch.test.framework.LdapAuthorizationConfigBuilder; -import org.opensearch.test.framework.RolesMapping; +import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AuthenticationBackend; import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.HttpAuthenticator; @@ -151,8 +151,8 @@ public class LdapTlsAuthenticationTest { .users(ADMIN_USER) .roles(ROLE_INDEX_ADMINISTRATOR, ROLE_PERSONAL_INDEX_ACCESS) .rolesMapping( - new RolesMapping(ROLE_INDEX_ADMINISTRATOR).backendRoles(CN_GROUP_ADMIN), - new RolesMapping(ROLE_PERSONAL_INDEX_ACCESS).backendRoles(CN_GROUP_CREW) + new TestSecurityConfig.RoleMapping(ROLE_INDEX_ADMINISTRATOR.getName()).backendRoles(CN_GROUP_ADMIN), + new TestSecurityConfig.RoleMapping(ROLE_PERSONAL_INDEX_ACCESS.getName()).backendRoles(CN_GROUP_CREW) ) .authz( new AuthzDomain("ldap_roles").httpEnabled(true) diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index 7210c53ad4..1dbb10b1f8 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -33,7 +33,6 @@ import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; import org.opensearch.test.framework.OnBehalfOfConfig; -import org.opensearch.test.framework.RolesMapping; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; @@ -139,7 +138,7 @@ private static OnBehalfOfConfig defaultOnBehalfOfConfig() { ) ) .authc(AUTHC_HTTPBASIC_INTERNAL) - .rolesMapping(new RolesMapping(HOST_MAPPING_ROLE).hostIPs(HOST_MAPPING_IP)) + .rolesMapping(new TestSecurityConfig.RoleMapping(HOST_MAPPING_ROLE.getName()).hosts(HOST_MAPPING_IP)) .onBehalfOf(defaultOnBehalfOfConfig()) .build(); diff --git a/src/integrationTest/java/org/opensearch/security/http/RolesMappingTests.java b/src/integrationTest/java/org/opensearch/security/http/RolesMappingTests.java new file mode 100644 index 0000000000..828790e47b --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/RolesMappingTests.java @@ -0,0 +1,85 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ +package org.opensearch.security.http; + +import java.util.List; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.apache.http.HttpStatus.SC_OK; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class RolesMappingTests { + static final TestSecurityConfig.User USER_A = new TestSecurityConfig.User("userA").password("s3cret").backendRoles("mapsToRoleA"); + static final TestSecurityConfig.User USER_B = new TestSecurityConfig.User("userB").password("P@ssw0rd").backendRoles("mapsToRoleB"); + + private static final TestSecurityConfig.Role ROLE_A = new TestSecurityConfig.Role("roleA").clusterPermissions("cluster_all"); + + private static final TestSecurityConfig.Role ROLE_B = new TestSecurityConfig.Role("roleB").clusterPermissions("cluster_all"); + + public static final TestSecurityConfig.AuthcDomain AUTHC_DOMAIN = new TestSecurityConfig.AuthcDomain("basic", 0) + .httpAuthenticatorWithChallenge("basic") + .backend("internal"); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .authc(AUTHC_DOMAIN) + .roles(ROLE_A, ROLE_B) + .rolesMapping( + new TestSecurityConfig.RoleMapping(ROLE_A.getName()).backendRoles("mapsToRoleA"), + new TestSecurityConfig.RoleMapping(ROLE_B.getName()).backendRoles("mapsToRoleB") + ) + .users(USER_A, USER_B) + .build(); + + @Test + public void testBackendRoleToRoleMapping() { + try (TestRestClient client = cluster.getRestClient(USER_A)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + assertThat(response, is(notNullValue())); + List roles = response.getTextArrayFromJsonBody("/roles"); + List backendRoles = response.getTextArrayFromJsonBody("/backend_roles"); + assertThat(roles, contains(ROLE_A.getName())); + assertThat(roles, not(contains(ROLE_B.getName()))); + assertThat(backendRoles, contains("mapsToRoleA")); + response.assertStatusCode(SC_OK); + } + + try (TestRestClient client = cluster.getRestClient(USER_B)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + assertThat(response, is(notNullValue())); + List roles = response.getTextArrayFromJsonBody("/roles"); + List backendRoles = response.getTextArrayFromJsonBody("/backend_roles"); + assertThat(roles, contains(ROLE_B.getName())); + assertThat(roles, not(contains(ROLE_A.getName()))); + assertThat(backendRoles, contains("mapsToRoleB")); + response.assertStatusCode(SC_OK); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/RolesMapping.java b/src/integrationTest/java/org/opensearch/test/framework/RolesMapping.java deleted file mode 100644 index 997e7e128b..0000000000 --- a/src/integrationTest/java/org/opensearch/test/framework/RolesMapping.java +++ /dev/null @@ -1,108 +0,0 @@ -/* -* Copyright OpenSearch Contributors -* 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. -* -*/ -package org.opensearch.test.framework; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.test.framework.TestSecurityConfig.Role; - -import static java.util.Objects.requireNonNull; - -/** -* The class represents mapping between backend roles {@link #backendRoles} to OpenSearch role defined by field {@link #roleName}. The -* class provides convenient builder-like methods and can be serialized to JSON. Serialization to JSON is required to store the class -* in an OpenSearch index which contains Security plugin configuration. -*/ -public class RolesMapping implements ToXContentObject { - - /** - * OpenSearch role name - */ - private String roleName; - - /** - * Backend role names - */ - private List backendRoles; - private List hostIPs; - - private boolean reserved = false; - - /** - * Creates roles mapping to OpenSearch role defined by parameter role - * @param role OpenSearch role, must not be null. - */ - public RolesMapping(Role role) { - requireNonNull(role); - this.roleName = requireNonNull(role.getName()); - this.backendRoles = new ArrayList<>(); - this.hostIPs = new ArrayList<>(); - } - - /** - * Defines backend role names - * @param backendRoles backend roles names - * @return current {@link RolesMapping} instance - */ - public RolesMapping backendRoles(String... backendRoles) { - this.backendRoles.addAll(Arrays.asList(backendRoles)); - return this; - } - - /** - * Defines host IP address - * @param hostIPs host IP address - * @return current {@link RolesMapping} instance - */ - public RolesMapping hostIPs(String... hostIPs) { - this.hostIPs.addAll(Arrays.asList(hostIPs)); - return this; - } - - /** - * Determines if role is reserved - * @param reserved true for reserved roles - * @return current {@link RolesMapping} instance - */ - public RolesMapping reserved(boolean reserved) { - this.reserved = reserved; - return this; - } - - /** - * Returns OpenSearch role name - * @return role name - */ - public String getRoleName() { - return roleName; - } - - /** - * Controls serialization to JSON - * @param xContentBuilder must not be null - * @param params not used parameter, but required by the interface {@link ToXContentObject} - * @return builder form parameter xContentBuilder - * @throws IOException denotes error during serialization to JSON - */ - @Override - public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { - xContentBuilder.startObject(); - xContentBuilder.field("reserved", reserved); - xContentBuilder.field("backend_roles", backendRoles); - xContentBuilder.field("hosts", hostIPs); - xContentBuilder.endObject(); - return xContentBuilder; - } -} diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 7957d1cfa4..22aab70521 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -53,6 +53,7 @@ import org.opensearch.action.update.UpdateRequest; import org.opensearch.client.Client; import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.core.common.Strings; import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.core.xcontent.XContentBuilder; @@ -81,7 +82,9 @@ public class TestSecurityConfig { private Map internalUsers = new LinkedHashMap<>(); private Map roles = new LinkedHashMap<>(); private AuditConfiguration auditConfiguration; - private Map rolesMapping = new LinkedHashMap<>(); + private Map rolesMapping = new LinkedHashMap<>(); + + private Map actionGroups = new LinkedHashMap<>(); private String indexName = ".opendistro_security"; @@ -159,9 +162,9 @@ public TestSecurityConfig audit(AuditConfiguration auditConfiguration) { return this; } - public TestSecurityConfig rolesMapping(RolesMapping... mappings) { - for (RolesMapping mapping : mappings) { - String roleName = mapping.getRoleName(); + public TestSecurityConfig rolesMapping(RoleMapping... mappings) { + for (RoleMapping mapping : mappings) { + String roleName = mapping.name(); if (rolesMapping.containsKey(roleName)) { throw new IllegalArgumentException("Role mapping " + roleName + " already exists"); } @@ -170,6 +173,13 @@ public TestSecurityConfig rolesMapping(RolesMapping... mappings) { return this; } + public TestSecurityConfig actionGroups(ActionGroup... groups) { + for (final var group : groups) { + this.actionGroups.put(group.name, group); + } + return this; + } + public static class Config implements ToXContentObject { private boolean anonymousAuth; @@ -252,7 +262,88 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params } } - public static class User implements UserCredentialsHolder, ToXContentObject { + public static final class ActionGroup implements ToXContentObject { + + public enum Type { + + INDEX, + + CLUSTER; + + public String type() { + return name().toLowerCase(); + } + + } + + private final String name; + + private final String description; + + private final Type type; + + private final List allowedActions; + + private Boolean hidden = null; + + private Boolean reserved = null; + + public ActionGroup(String name, Type type, String... allowedActions) { + this(name, null, type, allowedActions); + } + + public ActionGroup(String name, String description, Type type, String... allowedActions) { + this.name = name; + this.description = description; + this.type = type; + this.allowedActions = Arrays.asList(allowedActions); + } + + public String name() { + return name; + } + + public ActionGroup hidden(boolean hidden) { + this.hidden = hidden; + return this; + } + + public ActionGroup reserved(boolean reserved) { + this.reserved = reserved; + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (hidden != null) builder.field("hidden", hidden); + if (reserved != null) builder.field("reserved", reserved); + builder.field("type", type.type()); + builder.field("allowed_actions", allowedActions); + if (description != null) builder.field("description", description); + return builder.endObject(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ActionGroup that = (ActionGroup) o; + return Objects.equals(name, that.name) + && Objects.equals(description, that.description) + && type == that.type + && Objects.equals(allowedActions, that.allowedActions) + && Objects.equals(hidden, that.hidden) + && Objects.equals(reserved, that.reserved); + } + + @Override + public int hashCode() { + return Objects.hash(name, description, type, allowedActions, hidden, reserved); + } + } + + public static final class User implements UserCredentialsHolder, ToXContentObject { public final static TestSecurityConfig.User USER_ADMIN = new TestSecurityConfig.User("admin").roles( new Role("allaccess").indexPermissions("*").on("*").clusterPermissions("*") @@ -265,9 +356,20 @@ public static class User implements UserCredentialsHolder, ToXContentObject { String requestedTenant; private Map attributes = new HashMap<>(); + private Boolean hidden = null; + + private Boolean reserved = null; + + private String description; + public User(String name) { + this(name, null); + } + + public User(String name, String description) { this.name = name; this.password = "secret"; + this.description = description; } public User password(String password) { @@ -289,6 +391,16 @@ public User backendRoles(String... backendRoles) { return this; } + public User reserved(boolean reserved) { + this.reserved = reserved; + return this; + } + + public User hidden(boolean hidden) { + this.hidden = hidden; + return this; + } + public User attr(String key, String value) { this.attributes.put(key, value); return this; @@ -330,9 +442,33 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params xContentBuilder.field("attributes", attributes); } + if (hidden != null) xContentBuilder.field("hidden", hidden); + if (reserved != null) xContentBuilder.field("reserved", reserved); + if (!Strings.isNullOrEmpty(description)) xContentBuilder.field("description", description); xContentBuilder.endObject(); return xContentBuilder; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + User user = (User) o; + return Objects.equals(name, user.name) + && Objects.equals(password, user.password) + && Objects.equals(roles, user.roles) + && Objects.equals(backendRoles, user.backendRoles) + && Objects.equals(requestedTenant, user.requestedTenant) + && Objects.equals(attributes, user.attributes) + && Objects.equals(hidden, user.hidden) + && Objects.equals(reserved, user.reserved) + && Objects.equals(description, user.description); + } + + @Override + public int hashCode() { + return Objects.hash(name, password, roles, backendRoles, requestedTenant, attributes, hidden, reserved, description); + } } public static class Role implements ToXContentObject { @@ -343,8 +479,19 @@ public static class Role implements ToXContentObject { private List indexPermissions = new ArrayList<>(); + private Boolean hidden; + + private Boolean reserved; + + private String description; + public Role(String name) { + this(name, null); + } + + public Role(String name, String description) { this.name = name; + this.description = description; } public Role clusterPermissions(String... clusterPermissions) { @@ -365,6 +512,16 @@ public String getName() { return name; } + public Role hidden(boolean hidden) { + this.hidden = hidden; + return this; + } + + public Role reserved(boolean reserved) { + this.reserved = reserved; + return this; + } + public Role clone() { Role role = new Role(this.name); role.clusterPermissions.addAll(this.clusterPermissions); @@ -383,9 +540,117 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params if (!indexPermissions.isEmpty()) { xContentBuilder.field("index_permissions", indexPermissions); } + if (hidden != null) { + xContentBuilder.field("hidden", hidden); + } + if (reserved != null) { + xContentBuilder.field("reserved", reserved); + } + if (!Strings.isNullOrEmpty(description)) xContentBuilder.field("description", description); + return xContentBuilder.endObject(); + } - xContentBuilder.endObject(); - return xContentBuilder; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Role role = (Role) o; + return Objects.equals(name, role.name) + && Objects.equals(clusterPermissions, role.clusterPermissions) + && Objects.equals(indexPermissions, role.indexPermissions) + && Objects.equals(hidden, role.hidden) + && Objects.equals(reserved, role.reserved) + && Objects.equals(description, role.description); + } + + @Override + public int hashCode() { + return Objects.hash(name, clusterPermissions, indexPermissions, hidden, reserved, description); + } + } + + public static class RoleMapping implements ToXContentObject { + + private List users = new ArrayList<>(); + private List hosts = new ArrayList<>(); + + private final String name; + + private Boolean hidden; + + private Boolean reserved; + + private final String description; + + private List backendRoles = new ArrayList<>(); + + public RoleMapping(final String name) { + this(name, null); + } + + public RoleMapping(final String name, final String description) { + this.name = name; + this.description = description; + } + + public String name() { + return name; + } + + public RoleMapping hidden(boolean hidden) { + this.hidden = hidden; + return this; + } + + public RoleMapping reserved(boolean reserved) { + this.reserved = reserved; + return this; + } + + public RoleMapping users(String... users) { + this.users.addAll(Arrays.asList(users)); + return this; + } + + public RoleMapping hosts(String... hosts) { + this.users.addAll(Arrays.asList(hosts)); + return this; + } + + public RoleMapping backendRoles(String... backendRoles) { + this.backendRoles.addAll(Arrays.asList(backendRoles)); + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (hidden != null) builder.field("hidden", hidden); + if (reserved != null) builder.field("reserved", reserved); + if (users != null && !users.isEmpty()) builder.field("users", users); + if (hosts != null && !hosts.isEmpty()) builder.field("hosts", hosts); + if (description != null) builder.field("description", description); + builder.field("backend_roles", backendRoles); + return builder.endObject(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RoleMapping that = (RoleMapping) o; + return Objects.equals(users, that.users) + && Objects.equals(hosts, that.hosts) + && Objects.equals(name, that.name) + && Objects.equals(hidden, that.hidden) + && Objects.equals(reserved, that.reserved) + && Objects.equals(description, that.description) + && Objects.equals(backendRoles, that.backendRoles); + } + + @Override + public int hashCode() { + return Objects.hash(users, hosts, name, hidden, reserved, description, backendRoles); } } diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java index 217ce99a81..135f1fb481 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -56,7 +56,6 @@ import org.opensearch.test.framework.AuthFailureListeners; import org.opensearch.test.framework.AuthzDomain; import org.opensearch.test.framework.OnBehalfOfConfig; -import org.opensearch.test.framework.RolesMapping; import org.opensearch.test.framework.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; @@ -442,7 +441,7 @@ public Builder roles(Role... roles) { return this; } - public Builder rolesMapping(RolesMapping... mappings) { + public Builder rolesMapping(TestSecurityConfig.RoleMapping... mappings) { testSecurityConfig.rolesMapping(mappings); return this; } diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java index b228fed388..5e9fd75326 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java @@ -213,17 +213,19 @@ public void stop() { for (Node node : nodes) { stopFutures.add(node.stop(2, TimeUnit.SECONDS)); } - CompletableFuture.allOf(stopFutures.toArray(size -> new CompletableFuture[size])).join(); + CompletableFuture.allOf(stopFutures.toArray(CompletableFuture[]::new)).join(); } public void destroy() { - stop(); - nodes.clear(); - try { - FileUtils.deleteDirectory(clusterHomeDir); - } catch (IOException e) { - log.warn("Error while deleting " + clusterHomeDir, e); + stop(); + nodes.clear(); + } finally { + try { + FileUtils.deleteDirectory(clusterHomeDir); + } catch (IOException e) { + log.warn("Error while deleting " + clusterHomeDir, e); + } } } diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java index b7da92b270..bd625a5c31 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java @@ -417,6 +417,14 @@ public T getBodyAs(Class authInfoClass) { } } + public JsonNode bodyAsJsonNode() { + try { + return DefaultObjectMapper.readTree(getBody()); + } catch (IOException e) { + throw new RuntimeException("Cannot parse response body", e); + } + } + public void assertStatusCode(int expectedHttpStatus) { String reason = format("Expected status code is '%d', but was '%d'. Response body '%s'.", expectedHttpStatus, statusCode, body); assertThat(reason, statusCode, equalTo(expectedHttpStatus)); diff --git a/src/main/java/com/amazon/dlic/auth/http/saml/AuthTokenProcessorHandler.java b/src/main/java/com/amazon/dlic/auth/http/saml/AuthTokenProcessorHandler.java index 32e01b9e2f..6abe934925 100644 --- a/src/main/java/com/amazon/dlic/auth/http/saml/AuthTokenProcessorHandler.java +++ b/src/main/java/com/amazon/dlic/auth/http/saml/AuthTokenProcessorHandler.java @@ -137,7 +137,9 @@ private AuthTokenProcessorAction.Response handleImpl( String samlResponseBase64, String samlRequestId, String acsEndpoint, - Saml2Settings saml2Settings + Saml2Settings saml2Settings, + String requestPath // the parameter will be removed in the future as soon as we will read of legacy paths aka + // /_opendistro/_security/... ) { if (token_log.isDebugEnabled()) { try { @@ -156,7 +158,7 @@ private AuthTokenProcessorAction.Response handleImpl( final SamlResponse samlResponse = new SamlResponse(saml2Settings, acsEndpoint, samlResponseBase64); if (!samlResponse.isValid(samlRequestId)) { - log.warn("Error while validating SAML response in /_opendistro/_security/api/authtoken"); + log.warn("Error while validating SAML response in {}", requestPath); return null; } @@ -178,17 +180,14 @@ private Optional handleLowLevel(RestRequest restRequest) throw if (restRequest.getMediaType() != XContentType.JSON) { throw new OpenSearchSecurityException( - "/_opendistro/_security/api/authtoken expects content with type application/json", + restRequest.path() + " expects content with type application/json", RestStatus.UNSUPPORTED_MEDIA_TYPE ); } if (restRequest.method() != Method.POST) { - throw new OpenSearchSecurityException( - "/_opendistro/_security/api/authtoken expects POST requests", - RestStatus.METHOD_NOT_ALLOWED - ); + throw new OpenSearchSecurityException(restRequest.path() + " expects POST requests", RestStatus.METHOD_NOT_ALLOWED); } Saml2Settings saml2Settings = this.saml2SettingsProvider.getCached(); @@ -218,7 +217,13 @@ private Optional handleLowLevel(RestRequest restRequest) throw acsEndpoint = getAbsoluteAcsEndpoint(((ObjectNode) jsonRoot).get("acsEndpoint").textValue()); } - AuthTokenProcessorAction.Response responseBody = this.handleImpl(samlResponseBase64, samlRequestId, acsEndpoint, saml2Settings); + AuthTokenProcessorAction.Response responseBody = this.handleImpl( + samlResponseBase64, + samlRequestId, + acsEndpoint, + saml2Settings, + restRequest.path() + ); if (responseBody == null) { return Optional.empty(); @@ -228,7 +233,7 @@ private Optional handleLowLevel(RestRequest restRequest) throw return Optional.of(new SecurityResponse(HttpStatus.SC_OK, null, responseBodyString, XContentType.JSON.mediaType())); } catch (JsonProcessingException e) { - log.warn("Error while parsing JSON for /_opendistro/_security/api/authtoken", e); + log.warn("Error while parsing JSON for {}", restRequest.path(), e); return Optional.of(new SecurityResponse(HttpStatus.SC_BAD_REQUEST, "JSON could not be parsed")); } } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 688b797e85..6e3c22e695 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -46,6 +46,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; @@ -72,6 +73,8 @@ import org.opensearch.action.search.SearchScrollAction; import org.opensearch.action.support.ActionFilter; import org.opensearch.client.Client; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.NamedDiff; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.node.DiscoveryNode; import org.opensearch.cluster.node.DiscoveryNodes; @@ -104,6 +107,7 @@ import org.opensearch.extensions.ExtensionsManager; import org.opensearch.http.HttpServerTransport; import org.opensearch.http.HttpServerTransport.Dispatcher; +import org.opensearch.http.netty4.ssl.SecureNetty4HttpServerTransport; import org.opensearch.identity.Subject; import org.opensearch.identity.noop.NoopSubject; import org.opensearch.index.IndexModule; @@ -114,6 +118,9 @@ import org.opensearch.plugins.ExtensionAwarePlugin; import org.opensearch.plugins.IdentityPlugin; import org.opensearch.plugins.MapperPlugin; +import org.opensearch.plugins.SecureHttpTransportSettingsProvider; +import org.opensearch.plugins.SecureSettingsFactory; +import org.opensearch.plugins.SecureTransportSettingsProvider; import org.opensearch.repositories.RepositoriesService; import org.opensearch.rest.RestController; import org.opensearch.rest.RestHandler; @@ -150,8 +157,7 @@ import org.opensearch.security.dlic.rest.validation.PasswordValidator; import org.opensearch.security.filter.SecurityFilter; import org.opensearch.security.filter.SecurityRestFilter; -import org.opensearch.security.http.SecurityHttpServerTransport; -import org.opensearch.security.http.SecurityNonSslHttpServerTransport; +import org.opensearch.security.http.NonSslHttpServerTransport; import org.opensearch.security.http.XFFResolver; import org.opensearch.security.identity.SecurityTokenManager; import org.opensearch.security.privileges.PrivilegesEvaluator; @@ -167,12 +173,13 @@ import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.setting.TransportPassiveAuthSetting; +import org.opensearch.security.ssl.OpenSearchSecureSettingsFactory; import org.opensearch.security.ssl.OpenSearchSecuritySSLPlugin; import org.opensearch.security.ssl.SslExceptionHandler; import org.opensearch.security.ssl.http.netty.ValidatingDispatcher; import org.opensearch.security.ssl.transport.DefaultPrincipalExtractor; -import org.opensearch.security.ssl.transport.SecuritySSLNettyTransport; import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.state.SecurityMetadata; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.GuardedSearchOperationWrapper; import org.opensearch.security.support.HeaderHelper; @@ -199,11 +206,14 @@ import org.opensearch.transport.TransportRequestOptions; import org.opensearch.transport.TransportResponseHandler; import org.opensearch.transport.TransportService; +import org.opensearch.transport.netty4.ssl.SecureNetty4Transport; import org.opensearch.watcher.ResourceWatcherService; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE; import static org.opensearch.security.setting.DeprecatedSettings.checkForDeprecatedSetting; +import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX; +import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE; import static org.opensearch.security.support.ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION; // CS-ENFORCE-SINGLE @@ -230,7 +240,6 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile PrivilegesEvaluator evaluator; private volatile UserService userService; private volatile RestLayerPrivilegesEvaluator restLayerEvaluator; - private volatile ThreadPool threadPool; private volatile ConfigurationRepository cr; private volatile AdminDNs adminDns; private volatile ClusterService cs; @@ -284,6 +293,10 @@ private static boolean isDisabled(final Settings settings) { return settings.getAsBoolean(ConfigConstants.SECURITY_DISABLED, false); } + private static boolean useClusterStateToInitSecurityConfig(final Settings settings) { + return settings.getAsBoolean(SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE, false); + } + /** * SSL Cert Reload will be enabled only if security is not disabled and not in we are not using sslOnly mode. * @param settings Elastic configuration settings @@ -858,25 +871,27 @@ public void sendRequest( } @Override - public Map> getTransports( + public Map> getSecureTransports( Settings settings, ThreadPool threadPool, PageCacheRecycler pageCacheRecycler, CircuitBreakerService circuitBreakerService, NamedWriteableRegistry namedWriteableRegistry, NetworkService networkService, + SecureTransportSettingsProvider secureTransportSettingsProvider, Tracer tracer ) { Map> transports = new HashMap>(); if (SSLConfig.isSslOnlyMode()) { - return super.getTransports( + return super.getSecureTransports( settings, threadPool, pageCacheRecycler, circuitBreakerService, namedWriteableRegistry, networkService, + secureTransportSettingsProvider, tracer ); } @@ -884,18 +899,16 @@ public Map> getTransports( if (transportSSLEnabled) { transports.put( "org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport", - () -> new SecuritySSLNettyTransport( - settings, + () -> new SecureNetty4Transport( + migrateSettings(settings), Version.CURRENT, threadPool, networkService, pageCacheRecycler, namedWriteableRegistry, circuitBreakerService, - sks, - evaluateSslExceptionHandler(), sharedGroupFactory, - SSLConfig, + secureTransportSettingsProvider, tracer ) ); @@ -904,7 +917,7 @@ public Map> getTransports( } @Override - public Map> getHttpTransports( + public Map> getSecureHttpTransports( Settings settings, ThreadPool threadPool, BigArrays bigArrays, @@ -914,11 +927,12 @@ public Map> getHttpTransports( NetworkService networkService, Dispatcher dispatcher, ClusterSettings clusterSettings, + SecureHttpTransportSettingsProvider secureHttpTransportSettingsProvider, Tracer tracer ) { if (SSLConfig.isSslOnlyMode()) { - return super.getHttpTransports( + return super.getSecureHttpTransports( settings, threadPool, bigArrays, @@ -928,6 +942,7 @@ public Map> getHttpTransports( networkService, dispatcher, clusterSettings, + secureHttpTransportSettingsProvider, tracer ); } @@ -943,27 +958,25 @@ public Map> getHttpTransports( evaluateSslExceptionHandler() ); // TODO close odshst - final SecurityHttpServerTransport odshst = new SecurityHttpServerTransport( - settings, + final SecureNetty4HttpServerTransport odshst = new SecureNetty4HttpServerTransport( + migrateSettings(settings), networkService, bigArrays, threadPool, - sks, - evaluateSslExceptionHandler(), xContentRegistry, validatingDispatcher, clusterSettings, sharedGroupFactory, - tracer, - securityRestHandler + secureHttpTransportSettingsProvider, + tracer ); return Collections.singletonMap("org.opensearch.security.http.SecurityHttpServerTransport", () -> odshst); } else if (!client) { return Collections.singletonMap( "org.opensearch.security.http.SecurityHttpServerTransport", - () -> new SecurityNonSslHttpServerTransport( - settings, + () -> new NonSslHttpServerTransport( + migrateSettings(settings), networkService, bigArrays, threadPool, @@ -971,8 +984,8 @@ public Map> getHttpTransports( dispatcher, clusterSettings, sharedGroupFactory, - tracer, - securityRestHandler + secureHttpTransportSettingsProvider, + tracer ) ); } @@ -1166,11 +1179,23 @@ public Collection createComponents( components.add(si); components.add(dcf); components.add(userService); - + final var allowDefaultInit = settings.getAsBoolean(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false); + final var useClusterState = useClusterStateToInitSecurityConfig(settings); + if (!SSLConfig.isSslOnlyMode() && !isDisabled(settings) && allowDefaultInit && useClusterState) { + clusterService.addListener(cr); + } return components; } + @Override + public List getNamedWriteables() { + return List.of( + new NamedWriteableRegistry.Entry(ClusterState.Custom.class, SecurityMetadata.TYPE, SecurityMetadata::new), + new NamedWriteableRegistry.Entry(NamedDiff.class, SecurityMetadata.TYPE, SecurityMetadata::readDiffFrom) + ); + } + @Override public Settings additionalSettings() { @@ -1311,9 +1336,8 @@ public List> getSettings() { settings.add( Setting.boolSetting(ConfigConstants.SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES, false, Property.NodeScope, Property.Filtered) ); - settings.add( - Setting.boolSetting(ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false, Property.NodeScope, Property.Filtered) - ); + settings.add(Setting.boolSetting(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE, false, Property.NodeScope, Property.Filtered)); settings.add( Setting.boolSetting( ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, @@ -1909,11 +1933,10 @@ public List getSettingsFilter() { @Override public void onNodeStarted(DiscoveryNode localNode) { - log.info("Node started"); - if (!SSLConfig.isSslOnlyMode() && !client && !disabled) { + this.localNode.set(localNode); + if (!SSLConfig.isSslOnlyMode() && !client && !disabled && !useClusterStateToInitSecurityConfig(settings)) { cr.initOnNodeStart(); } - this.localNode.set(localNode); final Set securityModules = ReflectionHelper.getModulesLoaded(); log.info("{} OpenSearch Security modules loaded so far: {}", securityModules.size(), securityModules); } @@ -2005,6 +2028,11 @@ public SecurityTokenManager getTokenManager() { return tokenManager; } + @Override + public Optional getSecureSettingFactory(Settings settings) { + return Optional.of(new OpenSearchSecureSettingsFactory(threadPool, sks, sslExceptionHandler, securityRestHandler)); + } + public static class GuiceHolder implements LifecycleComponent { private static RepositoriesService repositoriesService; diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java index 02b88bbd5c..2e88418acf 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java @@ -32,13 +32,14 @@ import org.opensearch.security.identity.SecurityTokenManager; import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_API_ROUTE_PREFIX; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class CreateOnBehalfOfTokenAction extends BaseRestHandler { private static final List routes = addRoutesPrefix( ImmutableList.of(new NamedRoute.Builder().method(POST).path("/generateonbehalfoftoken").uniqueName("security:obo/create").build()), - "/_plugins/_security/api" + PLUGIN_API_ROUTE_PREFIX ); public static final long OBO_DEFAULT_EXPIRY_SECONDS = 5 * 60; diff --git a/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java b/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java index 353286fc4a..44ba77428f 100644 --- a/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java +++ b/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java @@ -31,6 +31,7 @@ import java.security.AccessController; import java.security.PrivilegedAction; import java.text.SimpleDateFormat; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -38,12 +39,16 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; +import java.util.stream.Collectors; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; @@ -58,13 +63,19 @@ import org.opensearch.action.admin.cluster.health.ClusterHealthResponse; import org.opensearch.action.admin.indices.create.CreateIndexRequest; import org.opensearch.client.Client; +import org.opensearch.cluster.ClusterChangedEvent; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.ClusterStateListener; +import org.opensearch.cluster.ClusterStateUpdateTask; import org.opensearch.cluster.health.ClusterHealthStatus; import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.metadata.MappingMetadata; import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.Priority; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.common.util.concurrent.ThreadContext.StoredContext; +import org.opensearch.core.action.ActionListener; import org.opensearch.core.common.Strings; import org.opensearch.core.rest.RestStatus; import org.opensearch.core.xcontent.MediaTypeRegistry; @@ -75,12 +86,16 @@ import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.ssl.util.ExceptionUtils; +import org.opensearch.security.state.SecurityMetadata; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.ConfigHelper; +import org.opensearch.security.support.SecurityIndexHandler; import org.opensearch.security.support.SecurityUtils; import org.opensearch.threadpool.ThreadPool; -public class ConfigurationRepository { +import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE; + +public class ConfigurationRepository implements ClusterStateListener { private static final Logger LOGGER = LogManager.getLogger(ConfigurationRepository.class); private final String securityIndex; @@ -96,20 +111,27 @@ public class ConfigurationRepository { private DynamicConfigFactory dynamicConfigFactory; public static final int DEFAULT_CONFIG_VERSION = 2; private final CompletableFuture initalizeConfigTask = new CompletableFuture<>(); + private final boolean acceptInvalid; - private ConfigurationRepository( - Settings settings, + private final AtomicBoolean auditHotReloadingEnabled = new AtomicBoolean(false); + + private final AtomicBoolean initializationInProcess = new AtomicBoolean(false); + + private final SecurityIndexHandler securityIndexHandler; + + // visible for testing + protected ConfigurationRepository( + final String securityIndex, + final Settings settings, final Path configPath, - ThreadPool threadPool, - Client client, - ClusterService clusterService, - AuditLog auditLog + final ThreadPool threadPool, + final Client client, + final ClusterService clusterService, + final AuditLog auditLog, + final SecurityIndexHandler securityIndexHandler ) { - this.securityIndex = settings.get( - ConfigConstants.SECURITY_CONFIG_INDEX_NAME, - ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX - ); + this.securityIndex = securityIndex; this.settings = settings; this.configPath = configPath; this.client = client; @@ -119,8 +141,38 @@ private ConfigurationRepository( this.configurationChangedListener = new ArrayList<>(); this.acceptInvalid = settings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_ACCEPT_INVALID_CONFIG, false); cl = new ConfigurationLoaderSecurity7(client, threadPool, settings, clusterService); - configCache = CacheBuilder.newBuilder().build(); + this.securityIndexHandler = securityIndexHandler; + } + + private Path resolveConfigDir() { + return Optional.ofNullable(System.getProperty("security.default_init.dir")) + .map(Path::of) + .orElseGet(() -> new Environment(settings, configPath).configDir().resolve("opensearch-security/")); + } + + @Override + public void clusterChanged(final ClusterChangedEvent event) { + final SecurityMetadata securityMetadata = event.state().custom(SecurityMetadata.TYPE); + // init and upload sec index on the manager node only as soon as + // creation of index and upload config are done a new cluster state will be created. + // in case of failures it repeats attempt after restart + if (nodeSelectedAsManager(event)) { + if (securityMetadata == null) { + initSecurityIndex(event); + } + } + // executes reload of cache on each node on the cluster, + // since sec initialization has been finished + if (securityMetadata != null) { + executeConfigurationInitialization(securityMetadata); + } + } + + private boolean nodeSelectedAsManager(final ClusterChangedEvent event) { + boolean wasClusterManager = event.previousState().nodes().isLocalNodeElectedClusterManager(); + boolean isClusterManager = event.localNodeClusterManager(); + return !wasClusterManager && isClusterManager; } public String getConfigDirectory() { @@ -236,7 +288,7 @@ private void initalizeClusterConfiguration(final boolean installDefaultConfig) { } catch (Exception e) { LOGGER.debug("Unable to load configuration due to {}", String.valueOf(ExceptionUtils.getRootCause(e))); try { - Thread.sleep(3000); + TimeUnit.MILLISECONDS.sleep(3000); } catch (InterruptedException e1) { Thread.currentThread().interrupt(); LOGGER.debug("Thread was interrupted so we cancel initialization"); @@ -244,27 +296,7 @@ private void initalizeClusterConfiguration(final boolean installDefaultConfig) { } } } - - final Set deprecatedAuditKeysInSettings = AuditConfig.getDeprecatedKeys(settings); - if (!deprecatedAuditKeysInSettings.isEmpty()) { - LOGGER.warn( - "Following keys {} are deprecated in opensearch settings. They will be removed in plugin v2.0.0.0", - deprecatedAuditKeysInSettings - ); - } - final boolean isAuditConfigDocPresentInIndex = cl.isAuditConfigDocPresentInIndex(); - if (isAuditConfigDocPresentInIndex) { - if (!deprecatedAuditKeysInSettings.isEmpty()) { - LOGGER.warn("Audit configuration settings found in both index and opensearch settings (deprecated)"); - } - LOGGER.info("Hot-reloading of audit configuration is enabled"); - } else { - LOGGER.info( - "Hot-reloading of audit configuration is disabled. Using configuration with defaults from opensearch settings. Populate the configuration in index using audit.yml or securityadmin to enable it." - ); - auditLog.setConfig(AuditConfig.from(settings)); - } - + setupAuditConfigurationIfAny(cl.isAuditConfigDocPresentInIndex()); LOGGER.info("Node '{}' initialized", clusterService.localNode().getName()); } catch (Exception e) { @@ -272,6 +304,27 @@ private void initalizeClusterConfiguration(final boolean installDefaultConfig) { } } + private void setupAuditConfigurationIfAny(final boolean auditConfigDocPresent) { + final Set deprecatedAuditKeysInSettings = AuditConfig.getDeprecatedKeys(settings); + if (!deprecatedAuditKeysInSettings.isEmpty()) { + LOGGER.warn( + "Following keys {} are deprecated in opensearch settings. They will be removed in plugin v2.0.0.0", + deprecatedAuditKeysInSettings + ); + } + if (auditConfigDocPresent) { + if (!deprecatedAuditKeysInSettings.isEmpty()) { + LOGGER.warn("Audit configuration settings found in both index and opensearch settings (deprecated)"); + } + LOGGER.info("Hot-reloading of audit configuration is enabled"); + } else { + LOGGER.info( + "Hot-reloading of audit configuration is disabled. Using configuration with defaults from opensearch settings. Populate the configuration in index using audit.yml or securityadmin to enable it." + ); + auditLog.setConfig(AuditConfig.from(settings)); + } + } + private boolean createSecurityIndexIfAbsent() { try { final Map indexSettings = ImmutableMap.of("index.number_of_shards", 1, "index.auto_expand_replicas", "0-all"); @@ -304,7 +357,7 @@ private void waitForSecurityIndexToBeAtLeastYellow() { response == null ? "no response" : (response.isTimedOut() ? "timeout" : "other, maybe red cluster") ); try { - Thread.sleep(500); + TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { // ignore Thread.currentThread().interrupt(); @@ -317,6 +370,69 @@ private void waitForSecurityIndexToBeAtLeastYellow() { } } + void initSecurityIndex(final ClusterChangedEvent event) { + if (!event.state().metadata().hasIndex(securityIndex)) { + securityIndexHandler.createIndex( + ActionListener.wrap(r -> uploadDefaultConfiguration0(), e -> LOGGER.error("Couldn't create index {}", securityIndex, e)) + ); + } else { + // in case index was created and cluster state has not been changed (e.g. restart of the node or something) + // just upload default configuration + uploadDefaultConfiguration0(); + } + } + + private void uploadDefaultConfiguration0() { + securityIndexHandler.uploadDefaultConfiguration( + resolveConfigDir(), + ActionListener.wrap( + configuration -> clusterService.submitStateUpdateTask( + "init-security-configuration", + new ClusterStateUpdateTask(Priority.IMMEDIATE) { + @Override + public ClusterState execute(ClusterState clusterState) throws Exception { + return ClusterState.builder(clusterState) + .putCustom(SecurityMetadata.TYPE, new SecurityMetadata(Instant.now(), configuration)) + .build(); + } + + @Override + public void onFailure(String s, Exception e) { + LOGGER.error(s, e); + } + } + ), + e -> LOGGER.error("Couldn't upload default configuration", e) + ) + ); + } + + Future executeConfigurationInitialization(final SecurityMetadata securityMetadata) { + if (!initalizeConfigTask.isDone()) { + if (initializationInProcess.compareAndSet(false, true)) { + return threadPool.generic().submit(() -> { + securityIndexHandler.loadConfiguration(securityMetadata.configuration(), ActionListener.wrap(cTypeConfigs -> { + notifyConfigurationListeners(cTypeConfigs); + final var auditConfigDocPresent = cTypeConfigs.containsKey(CType.AUDIT) && cTypeConfigs.get(CType.AUDIT).notEmpty(); + setupAuditConfigurationIfAny(auditConfigDocPresent); + auditHotReloadingEnabled.getAndSet(auditConfigDocPresent); + initalizeConfigTask.complete(null); + LOGGER.info( + "Security configuration initialized. Applied hashes: {}", + securityMetadata.configuration() + .stream() + .map(c -> String.format("%s:%s", c.type().toLCString(), c.hash())) + .collect(Collectors.toList()) + ); + }, e -> LOGGER.error("Couldn't reload security configuration", e))); + return null; + }); + } + } + return CompletableFuture.completedFuture(null); + } + + @Deprecated public CompletableFuture initOnNodeStart() { final boolean installDefaultConfig = settings.getAsBoolean(ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false); @@ -333,13 +449,15 @@ public CompletableFuture initOnNodeStart() { return startInitialization.get(); } else if (settings.getAsBoolean(ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, true)) { LOGGER.info( - "Will not attempt to create index {} and default configs if they are absent. Use securityadmin to initialize cluster", + "Will not attempt to create index {} and default configs if they are absent." + + " Use securityadmin to initialize cluster", securityIndex ); return startInitialization.get(); } else { LOGGER.info( - "Will not attempt to create index {} and default configs if they are absent. Will not perform background initialization", + "Will not attempt to create index {} and default configs if they are absent. " + + "Will not perform background initialization", securityIndex ); initalizeConfigTask.complete(null); @@ -352,7 +470,11 @@ public CompletableFuture initOnNodeStart() { } public boolean isAuditHotReloadingEnabled() { - return cl.isAuditConfigDocPresentInIndex(); + if (settings.getAsBoolean(SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE, false)) { + return auditHotReloadingEnabled.get(); + } else { + return cl.isAuditConfigDocPresentInIndex(); + } } public static ConfigurationRepository create( @@ -363,15 +485,20 @@ public static ConfigurationRepository create( ClusterService clusterService, AuditLog auditLog ) { - final ConfigurationRepository repository = new ConfigurationRepository( + final var securityIndex = settings.get( + ConfigConstants.SECURITY_CONFIG_INDEX_NAME, + ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX + ); + return new ConfigurationRepository( + securityIndex, settings, configPath, threadPool, client, clusterService, - auditLog + auditLog, + new SecurityIndexHandler(securityIndex, settings, client) ); - return repository; } public void setDynamicConfigFactory(DynamicConfigFactory dynamicConfigFactory) { @@ -403,6 +530,10 @@ private boolean reloadConfiguration(final Collection configTypes, final b LOGGER.warn("Unable to reload configuration, initalization thread has not yet completed."); return false; } + return loadConfigurationWithLock(configTypes); + } + + private boolean loadConfigurationWithLock(Collection configTypes) { try { if (LOCK.tryLock(60, TimeUnit.SECONDS)) { try { @@ -422,8 +553,12 @@ private boolean reloadConfiguration(final Collection configTypes, final b private void reloadConfiguration0(Collection configTypes, boolean acceptInvalid) { final Map> loaded = getConfigurationsFromIndex(configTypes, false, acceptInvalid); - configCache.putAll(loaded); - notifyAboutChanges(loaded); + notifyConfigurationListeners(loaded); + } + + private void notifyConfigurationListeners(final Map> configuration) { + configCache.putAll(configuration); + notifyAboutChanges(configuration); } public synchronized void subscribeOnChange(ConfigurationChangeListener listener) { 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 a38d618da0..157d44da73 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 @@ -241,7 +241,7 @@ protected ValidationResult patchEntities( for (final var entityName : patchEntityNames(patchContent)) { final var beforePatchEntity = configurationAsJson.get(entityName); final var patchedEntity = patchedConfigurationAsJson.get(entityName); - // verify we can process exising or updated entities + // verify we can process existing or updated entities if (beforePatchEntity != null && !Objects.equals(beforePatchEntity, patchedEntity)) { final var checkEntityCanBeProcess = endpointValidator.isAllowedToChangeImmutableEntity( SecurityConfiguration.of(entityName, configuration) diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java index 50fac9b80c..8b25fd5702 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java @@ -141,12 +141,9 @@ public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { @Override public ValidationResult isAllowedToChangeImmutableEntity(SecurityConfiguration securityConfiguration) throws IOException { - return EndpointValidator.super.isAllowedToChangeImmutableEntity(securityConfiguration).map(ignore -> { - if (isCurrentUserAdmin()) { - return ValidationResult.success(securityConfiguration); - } - return isAllowedToChangeEntityWithRestAdminPermissions(securityConfiguration); - }); + return EndpointValidator.super.isAllowedToChangeImmutableEntity(securityConfiguration).map( + ignore -> isAllowedToChangeEntityWithRestAdminPermissions(securityConfiguration) + ); } @Override diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java index b980a1e4ba..bc2e515e09 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java @@ -106,14 +106,11 @@ public ValidationResult isAllowedToChangeImmutableEntity( public ValidationResult isAllowedToChangeRoleMappingWithRestAdminPermissions( SecurityConfiguration securityConfiguration ) throws IOException { - return loadConfiguration(CType.ROLES, false, false).map(rolesConfiguration -> { - if (isCurrentUserAdmin()) { - return ValidationResult.success(securityConfiguration); - } - return isAllowedToChangeEntityWithRestAdminPermissions( + return loadConfiguration(CType.ROLES, false, false).map( + rolesConfiguration -> isAllowedToChangeEntityWithRestAdminPermissions( SecurityConfiguration.of(securityConfiguration.entityName(), rolesConfiguration) - ); - }).map(ignore -> ValidationResult.success(securityConfiguration)); + ) + ).map(ignore -> ValidationResult.success(securityConfiguration)); } @Override diff --git a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java index 74b7cd415a..ee68a629c6 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java +++ b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java @@ -55,9 +55,19 @@ import org.opensearch.security.user.User; import static org.opensearch.core.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; +import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; +import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; public class Utils { + public final static String PLUGIN_ROUTE_PREFIX = "/" + PLUGINS_PREFIX; + + public final static String LEGACY_PLUGIN_ROUTE_PREFIX = "/" + LEGACY_OPENDISTRO_PREFIX; + + public final static String PLUGIN_API_ROUTE_PREFIX = PLUGIN_ROUTE_PREFIX + "/api"; + + public final static String LEGACY_PLUGIN_API_ROUTE_PREFIX = LEGACY_PLUGIN_ROUTE_PREFIX + "/api"; + private static final ObjectMapper internalMapper = new ObjectMapper(); public static Map convertJsonToxToStructuredMap(ToXContent jsonContent) { @@ -217,7 +227,7 @@ public static Set generateFieldResourcePaths(final Set fields, f *Total number of routes is expanded as twice as the number of routes passed in */ public static List addRoutesPrefix(List routes) { - return addRoutesPrefix(routes, "/_opendistro/_security/api", "/_plugins/_security/api"); + return addRoutesPrefix(routes, LEGACY_PLUGIN_API_ROUTE_PREFIX, PLUGIN_API_ROUTE_PREFIX); } /** @@ -248,7 +258,7 @@ public static List addRoutesPrefix(List routes, final String... pr *Total number of routes is expanded as twice as the number of routes passed in */ public static List addDeprecatedRoutesPrefix(List deprecatedRoutes) { - return addDeprecatedRoutesPrefix(deprecatedRoutes, "/_opendistro/_security/api", "/_plugins/_security/api"); + return addDeprecatedRoutesPrefix(deprecatedRoutes, LEGACY_PLUGIN_API_ROUTE_PREFIX, PLUGIN_API_ROUTE_PREFIX); } /** diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/EndpointValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/EndpointValidator.java index 5879272b30..17c8b1f2ba 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/EndpointValidator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/EndpointValidator.java @@ -156,21 +156,19 @@ default ValidationResult> validateRoles( default ValidationResult isAllowedToChangeEntityWithRestAdminPermissions( final SecurityConfiguration securityConfiguration ) throws IOException { + final var configuration = securityConfiguration.configuration(); if (securityConfiguration.entityExists()) { - final var configuration = securityConfiguration.configuration(); final var existingEntity = configuration.getCEntry(securityConfiguration.entityName()); if (restApiAdminPrivilegesEvaluator().containsRestApiAdminPermissions(existingEntity)) { - return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Access denied")); } - } else { - final var configuration = securityConfiguration.configuration(); - final var configEntityContent = Utils.toConfigObject( + } + if (securityConfiguration.requestContent() != null) { + final var newConfigEntityContent = Utils.toConfigObject( securityConfiguration.requestContent(), configuration.getImplementingClass() ); - if (restApiAdminPrivilegesEvaluator().containsRestApiAdminPermissions(configEntityContent)) { - + if (restApiAdminPrivilegesEvaluator().containsRestApiAdminPermissions(newConfigEntityContent)) { return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Access denied")); } } diff --git a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java index b649b8d71f..420211f29b 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java @@ -55,6 +55,7 @@ import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; import org.opensearch.security.securityconf.impl.AllowlistingSettings; import org.opensearch.security.securityconf.impl.WhitelistingSettings; +import org.opensearch.security.ssl.http.netty.Netty4HttpRequestHeaderVerifier; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.util.ExceptionUtils; import org.opensearch.security.ssl.util.SSLRequestHelper; @@ -69,10 +70,6 @@ import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; -import static org.opensearch.security.http.SecurityHttpServerTransport.CONTEXT_TO_RESTORE; -import static org.opensearch.security.http.SecurityHttpServerTransport.EARLY_RESPONSE; -import static org.opensearch.security.http.SecurityHttpServerTransport.IS_AUTHENTICATED; -import static org.opensearch.security.http.SecurityHttpServerTransport.UNCONSUMED_PARAMS; public class SecurityRestFilter { @@ -128,15 +125,18 @@ public AuthczRestHandler(RestHandler original, AdminDNs adminDNs) { @Override public void handleRequest(RestRequest request, RestChannel channel, NodeClient client) throws Exception { - final Optional maybeSavedResponse = NettyAttribute.popFrom(request, EARLY_RESPONSE); + final Optional maybeSavedResponse = NettyAttribute.popFrom( + request, + Netty4HttpRequestHeaderVerifier.EARLY_RESPONSE + ); if (maybeSavedResponse.isPresent()) { - NettyAttribute.clearAttribute(request, CONTEXT_TO_RESTORE); - NettyAttribute.clearAttribute(request, IS_AUTHENTICATED); + NettyAttribute.clearAttribute(request, Netty4HttpRequestHeaderVerifier.CONTEXT_TO_RESTORE); + NettyAttribute.clearAttribute(request, Netty4HttpRequestHeaderVerifier.IS_AUTHENTICATED); channel.sendResponse(maybeSavedResponse.get().asRestResponse()); return; } - NettyAttribute.popFrom(request, CONTEXT_TO_RESTORE).ifPresent(storedContext -> { + NettyAttribute.popFrom(request, Netty4HttpRequestHeaderVerifier.CONTEXT_TO_RESTORE).ifPresent(storedContext -> { // X_OPAQUE_ID will be overritten on restore - save to apply after restoring the saved context final String xOpaqueId = threadContext.getHeader(Task.X_OPAQUE_ID); storedContext.restore(); @@ -145,7 +145,7 @@ public void handleRequest(RestRequest request, RestChannel channel, NodeClient c } }); - NettyAttribute.popFrom(request, UNCONSUMED_PARAMS).ifPresent(unconsumedParams -> { + NettyAttribute.popFrom(request, Netty4HttpRequestHeaderVerifier.UNCONSUMED_PARAMS).ifPresent(unconsumedParams -> { for (String unconsumedParam : unconsumedParams) { // Consume the parameter on the RestRequest request.param(unconsumedParam); @@ -155,7 +155,7 @@ public void handleRequest(RestRequest request, RestChannel channel, NodeClient c final SecurityRequestChannel requestChannel = SecurityRequestFactory.from(request, channel); // Authenticate request - if (!NettyAttribute.popFrom(request, IS_AUTHENTICATED).orElse(false)) { + if (!NettyAttribute.popFrom(request, Netty4HttpRequestHeaderVerifier.IS_AUTHENTICATED).orElse(false)) { // we aren't authenticated so we should skip this step checkAndAuthenticateRequest(requestChannel); } diff --git a/src/main/java/org/opensearch/security/http/SecurityNonSslHttpServerTransport.java b/src/main/java/org/opensearch/security/http/NonSslHttpServerTransport.java similarity index 75% rename from src/main/java/org/opensearch/security/http/SecurityNonSslHttpServerTransport.java rename to src/main/java/org/opensearch/security/http/NonSslHttpServerTransport.java index f37ebb48e8..d0d1d9f09f 100644 --- a/src/main/java/org/opensearch/security/http/SecurityNonSslHttpServerTransport.java +++ b/src/main/java/org/opensearch/security/http/NonSslHttpServerTransport.java @@ -33,22 +33,17 @@ import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.http.HttpHandlingSettings; import org.opensearch.http.netty4.Netty4HttpServerTransport; -import org.opensearch.security.filter.SecurityRestFilter; -import org.opensearch.security.ssl.http.netty.Netty4ConditionalDecompressor; -import org.opensearch.security.ssl.http.netty.Netty4HttpRequestHeaderVerifier; +import org.opensearch.http.netty4.ssl.SecureNetty4HttpServerTransport; +import org.opensearch.plugins.SecureHttpTransportSettingsProvider; import org.opensearch.telemetry.tracing.Tracer; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.SharedGroupFactory; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelInboundHandlerAdapter; -public class SecurityNonSslHttpServerTransport extends Netty4HttpServerTransport { - - private final ChannelInboundHandlerAdapter headerVerifier; - - public SecurityNonSslHttpServerTransport( +public class NonSslHttpServerTransport extends SecureNetty4HttpServerTransport { + public NonSslHttpServerTransport( final Settings settings, final NetworkService networkService, final BigArrays bigArrays, @@ -57,8 +52,8 @@ public SecurityNonSslHttpServerTransport( final Dispatcher dispatcher, final ClusterSettings clusterSettings, final SharedGroupFactory sharedGroupFactory, - final Tracer tracer, - final SecurityRestFilter restFilter + final SecureHttpTransportSettingsProvider secureHttpTransportSettingsProvider, + final Tracer tracer ) { super( settings, @@ -69,9 +64,9 @@ public SecurityNonSslHttpServerTransport( dispatcher, clusterSettings, sharedGroupFactory, + secureHttpTransportSettingsProvider, tracer ); - headerVerifier = new Netty4HttpRequestHeaderVerifier(restFilter, threadPool, settings); } @Override @@ -90,14 +85,4 @@ protected void initChannel(Channel ch) throws Exception { super.initChannel(ch); } } - - @Override - protected ChannelInboundHandlerAdapter createHeaderVerifier() { - return headerVerifier; - } - - @Override - protected ChannelInboundHandlerAdapter createDecompressor() { - return new Netty4ConditionalDecompressor(); - } } diff --git a/src/main/java/org/opensearch/security/http/SecurityHttpServerTransport.java b/src/main/java/org/opensearch/security/http/SecurityHttpServerTransport.java deleted file mode 100644 index eb75f898f4..0000000000 --- a/src/main/java/org/opensearch/security/http/SecurityHttpServerTransport.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2015-2018 _floragunn_ GmbH - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * 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.http; - -import java.util.Set; - -import org.opensearch.common.network.NetworkService; -import org.opensearch.common.settings.ClusterSettings; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.BigArrays; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.security.filter.SecurityResponse; -import org.opensearch.security.filter.SecurityRestFilter; -import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.ssl.SslExceptionHandler; -import org.opensearch.security.ssl.http.netty.SecuritySSLNettyHttpServerTransport; -import org.opensearch.security.ssl.http.netty.ValidatingDispatcher; -import org.opensearch.telemetry.tracing.Tracer; -import org.opensearch.threadpool.ThreadPool; -import org.opensearch.transport.SharedGroupFactory; - -import io.netty.util.AttributeKey; - -public class SecurityHttpServerTransport extends SecuritySSLNettyHttpServerTransport { - - public static final AttributeKey EARLY_RESPONSE = AttributeKey.newInstance("opensearch-http-early-response"); - public static final AttributeKey> UNCONSUMED_PARAMS = AttributeKey.newInstance("opensearch-http-request-consumed-params"); - public static final AttributeKey CONTEXT_TO_RESTORE = AttributeKey.newInstance( - "opensearch-http-request-thread-context" - ); - public static final AttributeKey SHOULD_DECOMPRESS = AttributeKey.newInstance("opensearch-http-should-decompress"); - public static final AttributeKey IS_AUTHENTICATED = AttributeKey.newInstance("opensearch-http-is-authenticated"); - - public SecurityHttpServerTransport( - final Settings settings, - final NetworkService networkService, - final BigArrays bigArrays, - final ThreadPool threadPool, - final SecurityKeyStore odsks, - final SslExceptionHandler sslExceptionHandler, - final NamedXContentRegistry namedXContentRegistry, - final ValidatingDispatcher dispatcher, - final ClusterSettings clusterSettings, - SharedGroupFactory sharedGroupFactory, - Tracer tracer, - SecurityRestFilter restFilter - ) { - super( - settings, - networkService, - bigArrays, - threadPool, - odsks, - namedXContentRegistry, - dispatcher, - sslExceptionHandler, - clusterSettings, - sharedGroupFactory, - tracer, - restFilter - ); - } -} diff --git a/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java b/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java index 070648ed92..3401ac71e8 100644 --- a/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java @@ -50,15 +50,19 @@ import static org.opensearch.rest.RestRequest.Method.GET; import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.security.dlic.rest.support.Utils.LEGACY_PLUGIN_ROUTE_PREFIX; +import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_ROUTE_PREFIX; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class DashboardsInfoAction extends BaseRestHandler { private static final List routes = ImmutableList.builder() .addAll( - addRoutesPrefix(ImmutableList.of(new Route(GET, "/dashboardsinfo"), new Route(POST, "/dashboardsinfo")), "/_plugins/_security") + addRoutesPrefix(ImmutableList.of(new Route(GET, "/dashboardsinfo"), new Route(POST, "/dashboardsinfo")), PLUGIN_ROUTE_PREFIX) + ) + .addAll( + addRoutesPrefix(ImmutableList.of(new Route(GET, "/kibanainfo"), new Route(POST, "/kibanainfo")), LEGACY_PLUGIN_ROUTE_PREFIX) ) - .addAll(addRoutesPrefix(ImmutableList.of(new Route(GET, "/kibanainfo"), new Route(POST, "/kibanainfo")), "/_opendistro/_security")) .build(); private final Logger log = LogManager.getLogger(this.getClass()); diff --git a/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java b/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java index 1b7e788dae..3c57773417 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java @@ -44,13 +44,15 @@ import static org.opensearch.rest.RestRequest.Method.GET; import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.security.dlic.rest.support.Utils.LEGACY_PLUGIN_ROUTE_PREFIX; +import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_ROUTE_PREFIX; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class SecurityHealthAction extends BaseRestHandler { private static final List routes = addRoutesPrefix( ImmutableList.of(new Route(GET, "/health"), new Route(POST, "/health")), - "/_opendistro/_security", - "/_plugins/_security" + LEGACY_PLUGIN_ROUTE_PREFIX, + PLUGIN_ROUTE_PREFIX ); private final BackendRegistry registry; diff --git a/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java b/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java index 94070649fa..64075d5d0e 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java @@ -57,13 +57,15 @@ import static org.opensearch.rest.RestRequest.Method.GET; import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.security.dlic.rest.support.Utils.LEGACY_PLUGIN_ROUTE_PREFIX; +import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_ROUTE_PREFIX; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class SecurityInfoAction extends BaseRestHandler { private static final List routes = addRoutesPrefix( ImmutableList.of(new Route(GET, "/authinfo"), new Route(POST, "/authinfo")), - "/_opendistro/_security", - "/_plugins/_security" + LEGACY_PLUGIN_ROUTE_PREFIX, + PLUGIN_ROUTE_PREFIX ); private final Logger log = LogManager.getLogger(this.getClass()); diff --git a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java index 1b0bdd7f8e..bd911463d4 100644 --- a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java @@ -61,13 +61,15 @@ import static org.opensearch.rest.RestRequest.Method.GET; import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.security.dlic.rest.support.Utils.LEGACY_PLUGIN_ROUTE_PREFIX; +import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_ROUTE_PREFIX; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class TenantInfoAction extends BaseRestHandler { private static final List routes = addRoutesPrefix( ImmutableList.of(new Route(GET, "/tenantinfo"), new Route(POST, "/tenantinfo")), - "/_opendistro/_security", - "/_plugins/_security" + LEGACY_PLUGIN_ROUTE_PREFIX, + PLUGIN_ROUTE_PREFIX ); private final Logger log = LogManager.getLogger(this.getClass()); 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 90508840e7..83553f2de7 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java @@ -68,6 +68,11 @@ public static SecurityDynamicConfiguration empty() { return new SecurityDynamicConfiguration(); } + @JsonIgnore + public boolean notEmpty() { + return !centries.isEmpty(); + } + public static SecurityDynamicConfiguration fromJson(String json, CType ctype, int version, long seqNo, long primaryTerm) throws IOException { return fromJson(json, ctype, version, seqNo, primaryTerm, false); diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java new file mode 100644 index 0000000000..5351eea57e --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java @@ -0,0 +1,135 @@ +/* + * 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.ssl; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; + +import org.opensearch.common.settings.Settings; +import org.opensearch.http.HttpServerTransport; +import org.opensearch.http.netty4.ssl.SecureNetty4HttpServerTransport; +import org.opensearch.plugins.SecureHttpTransportSettingsProvider; +import org.opensearch.plugins.SecureSettingsFactory; +import org.opensearch.plugins.SecureTransportSettingsProvider; +import org.opensearch.plugins.TransportExceptionHandler; +import org.opensearch.security.filter.SecurityRestFilter; +import org.opensearch.security.ssl.http.netty.Netty4ConditionalDecompressor; +import org.opensearch.security.ssl.http.netty.Netty4HttpRequestHeaderVerifier; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.Transport; +import org.opensearch.transport.TransportAdapterProvider; + +import io.netty.channel.ChannelInboundHandlerAdapter; + +public class OpenSearchSecureSettingsFactory implements SecureSettingsFactory { + private final ThreadPool threadPool; + private final SecurityKeyStore sks; + private final SslExceptionHandler sslExceptionHandler; + private final SecurityRestFilter restFilter; + + public OpenSearchSecureSettingsFactory( + ThreadPool threadPool, + SecurityKeyStore sks, + SslExceptionHandler sslExceptionHandler, + SecurityRestFilter restFilter + ) { + this.threadPool = threadPool; + this.sks = sks; + this.sslExceptionHandler = sslExceptionHandler; + this.restFilter = restFilter; + } + + @Override + public Optional getSecureTransportSettingsProvider(Settings settings) { + return Optional.of(new SecureTransportSettingsProvider() { + @Override + public Optional buildServerTransportExceptionHandler(Settings settings, Transport transport) { + return Optional.of(new TransportExceptionHandler() { + @Override + public void onError(Throwable t) { + sslExceptionHandler.logError(t, false); + } + }); + } + + @Override + public Optional buildSecureServerTransportEngine(Settings settings, Transport transport) throws SSLException { + return Optional.of(sks.createServerTransportSSLEngine()); + } + + @Override + public Optional buildSecureClientTransportEngine(Settings settings, String hostname, int port) throws SSLException { + return Optional.of(sks.createClientTransportSSLEngine(hostname, port)); + } + }); + } + + @Override + public Optional getSecureHttpTransportSettingsProvider(Settings settings) { + return Optional.of(new SecureHttpTransportSettingsProvider() { + @Override + public Collection> getHttpTransportAdapterProviders(Settings settings) { + return List.of(new TransportAdapterProvider() { + @Override + public String name() { + return SecureNetty4HttpServerTransport.REQUEST_DECOMPRESSOR; + } + + @SuppressWarnings("unchecked") + @Override + public Optional create(Settings settings, HttpServerTransport transport, Class adapterClass) { + if (transport instanceof SecureNetty4HttpServerTransport + && ChannelInboundHandlerAdapter.class.isAssignableFrom(adapterClass)) { + return Optional.of((C) new Netty4ConditionalDecompressor()); + } else { + return Optional.empty(); + } + } + }, new TransportAdapterProvider() { + @Override + public String name() { + return SecureNetty4HttpServerTransport.REQUEST_HEADER_VERIFIER; + } + + @SuppressWarnings("unchecked") + @Override + public Optional create(Settings settings, HttpServerTransport transport, Class adapterClass) { + if (transport instanceof SecureNetty4HttpServerTransport + && ChannelInboundHandlerAdapter.class.isAssignableFrom(adapterClass)) { + return Optional.of((C) new Netty4HttpRequestHeaderVerifier(restFilter, threadPool, settings)); + } else { + return Optional.empty(); + } + } + }); + } + + @Override + public Optional buildHttpServerExceptionHandler(Settings settings, HttpServerTransport transport) { + return Optional.of(new TransportExceptionHandler() { + @Override + public void onError(Throwable t) { + sslExceptionHandler.logError(t, true); + } + }); + } + + @Override + public Optional buildSecureHttpServerEngine(Settings settings, HttpServerTransport transport) throws SSLException { + return Optional.of(sks.createHTTPSSLEngine()); + } + }); + } +} diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java index e6e4e85b33..073193e9d4 100644 --- a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; @@ -60,8 +61,12 @@ import org.opensearch.env.NodeEnvironment; import org.opensearch.http.HttpServerTransport; import org.opensearch.http.HttpServerTransport.Dispatcher; +import org.opensearch.http.netty4.ssl.SecureNetty4HttpServerTransport; import org.opensearch.plugins.NetworkPlugin; import org.opensearch.plugins.Plugin; +import org.opensearch.plugins.SecureHttpTransportSettingsProvider; +import org.opensearch.plugins.SecureSettingsFactory; +import org.opensearch.plugins.SecureTransportSettingsProvider; import org.opensearch.plugins.SystemIndexPlugin; import org.opensearch.repositories.RepositoriesService; import org.opensearch.rest.RestController; @@ -70,20 +75,20 @@ import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.NonValidatingObjectMapper; import org.opensearch.security.filter.SecurityRestFilter; -import org.opensearch.security.ssl.http.netty.SecuritySSLNettyHttpServerTransport; import org.opensearch.security.ssl.http.netty.ValidatingDispatcher; import org.opensearch.security.ssl.rest.SecuritySSLInfoAction; import org.opensearch.security.ssl.transport.DefaultPrincipalExtractor; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.transport.SSLConfig; -import org.opensearch.security.ssl.transport.SecuritySSLNettyTransport; import org.opensearch.security.ssl.transport.SecuritySSLTransportInterceptor; import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.support.SecuritySettings; import org.opensearch.telemetry.tracing.Tracer; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.SharedGroupFactory; import org.opensearch.transport.Transport; import org.opensearch.transport.TransportInterceptor; +import org.opensearch.transport.netty4.ssl.SecureNetty4Transport; import org.opensearch.watcher.ResourceWatcherService; import io.netty.handler.ssl.OpenSsl; @@ -91,6 +96,21 @@ //For ES5 this class has only effect when SSL only plugin is installed public class OpenSearchSecuritySSLPlugin extends Plugin implements SystemIndexPlugin, NetworkPlugin { + private static final Setting SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION = Setting.boolSetting( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION, + true, + Property.NodeScope, + Property.Filtered, + Property.Deprecated + ); + + private static final Setting SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME = Setting.boolSetting( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME, + true, + Property.NodeScope, + Property.Filtered, + Property.Deprecated + ); private static boolean USE_NETTY_DEFAULT_ALLOCATOR = Booleans.parseBoolean( System.getProperty("opensearch.unsafe.use_netty_default_allocator"), @@ -112,6 +132,7 @@ public class OpenSearchSecuritySSLPlugin extends Plugin implements SystemIndexPl private final static SslExceptionHandler NOOP_SSL_EXCEPTION_HANDLER = new SslExceptionHandler() { }; protected final SSLConfig SSLConfig; + protected volatile ThreadPool threadPool; // public OpenSearchSecuritySSLPlugin(final Settings settings, final Path configPath) { // this(settings, configPath, false); @@ -237,7 +258,7 @@ public Object run() { } @Override - public Map> getHttpTransports( + public Map> getSecureHttpTransports( Settings settings, ThreadPool threadPool, BigArrays bigArrays, @@ -247,6 +268,7 @@ public Map> getHttpTransports( NetworkService networkService, Dispatcher dispatcher, ClusterSettings clusterSettings, + SecureHttpTransportSettingsProvider secureHttpTransportSettingsProvider, Tracer tracer ) { @@ -259,19 +281,17 @@ public Map> getHttpTransports( configPath, NOOP_SSL_EXCEPTION_HANDLER ); - final SecuritySSLNettyHttpServerTransport sgsnht = new SecuritySSLNettyHttpServerTransport( - settings, + final SecureNetty4HttpServerTransport sgsnht = new SecureNetty4HttpServerTransport( + migrateSettings(settings), networkService, bigArrays, threadPool, - sks, xContentRegistry, validatingDispatcher, - NOOP_SSL_EXCEPTION_HANDLER, clusterSettings, sharedGroupFactory, - tracer, - securityRestHandler + secureHttpTransportSettingsProvider, + tracer ); return Collections.singletonMap("org.opensearch.security.ssl.http.netty.SecuritySSLNettyHttpServerTransport", () -> sgsnht); @@ -313,13 +333,14 @@ public List getTransportInterceptors(NamedWriteableRegistr } @Override - public Map> getTransports( + public Map> getSecureTransports( Settings settings, ThreadPool threadPool, PageCacheRecycler pageCacheRecycler, CircuitBreakerService circuitBreakerService, NamedWriteableRegistry namedWriteableRegistry, NetworkService networkService, + SecureTransportSettingsProvider secureTransportSettingsProvider, Tracer tracer ) { @@ -327,18 +348,16 @@ public Map> getTransports( if (transportSSLEnabled) { transports.put( "org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport", - () -> new SecuritySSLNettyTransport( - settings, + () -> new SecureNetty4Transport( + migrateSettings(settings), Version.CURRENT, threadPool, networkService, pageCacheRecycler, namedWriteableRegistry, circuitBreakerService, - sks, - NOOP_SSL_EXCEPTION_HANDLER, sharedGroupFactory, - SSLConfig, + secureTransportSettingsProvider, tracer ) ); @@ -363,6 +382,7 @@ public Collection createComponents( Supplier repositoriesServiceSupplier ) { + this.threadPool = threadPool; final List components = new ArrayList<>(1); if (client) { @@ -436,22 +456,13 @@ public List> getSettings() { Property.Filtered ) ); - settings.add( - Setting.boolSetting( - SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION, - true, - Property.NodeScope, - Property.Filtered - ) - ); - settings.add( - Setting.boolSetting( - SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME, - true, - Property.NodeScope, - Property.Filtered - ) - ); + if (!settings.stream().anyMatch(s -> s.getKey().equalsIgnoreCase(NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY))) { + settings.add(SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION); + } + if (!settings.stream() + .anyMatch(s -> s.getKey().equalsIgnoreCase(NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME_KEY))) { + settings.add(SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME); + } settings.add( Setting.simpleString(SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, Property.NodeScope, Property.Filtered) ); @@ -664,4 +675,63 @@ public List getSettingsFilter() { settingsFilter.add("plugins.security.*"); return settingsFilter; } + + @Override + public Optional getSecureSettingFactory(Settings settings) { + return Optional.of(new OpenSearchSecureSettingsFactory(threadPool, sks, NOOP_SSL_EXCEPTION_HANDLER, securityRestHandler)); + } + + protected Settings migrateSettings(Settings settings) { + final Settings.Builder builder = Settings.builder().put(settings); + + if (!NetworkModule.TRANSPORT_SSL_DUAL_MODE_ENABLED.exists(settings)) { + builder.put(NetworkModule.TRANSPORT_SSL_DUAL_MODE_ENABLED_KEY, SecuritySettings.SSL_DUAL_MODE_SETTING.get(settings)); + } else { + if (SecuritySettings.SSL_DUAL_MODE_SETTING.exists(settings)) { + throw new OpenSearchException( + "Only one of the settings [" + + NetworkModule.TRANSPORT_SSL_DUAL_MODE_ENABLED_KEY + + ", " + + SecuritySettings.SSL_DUAL_MODE_SETTING.getKey() + + " (deprecated)] could be specified but not both" + ); + } + } + + if (!NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME.exists(settings)) { + builder.put( + NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME_KEY, + SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME.get(settings) + ); + } else { + if (SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME.exists(settings)) { + throw new OpenSearchException( + "Only one of the settings [" + + NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME_KEY + + ", " + + SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME.getKey() + + " (deprecated)] could be specified but not both" + ); + } + } + + if (!NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION.exists(settings)) { + builder.put( + NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY, + SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION.get(settings) + ); + } else { + if (SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION.exists(settings)) { + throw new OpenSearchException( + "Only one of the settings [" + + NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY + + ", " + + SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION.getKey() + + " (deprecated)] could be specified but not both" + ); + } + } + + return builder.build(); + } } diff --git a/src/main/java/org/opensearch/security/ssl/http/netty/Netty4ConditionalDecompressor.java b/src/main/java/org/opensearch/security/ssl/http/netty/Netty4ConditionalDecompressor.java index f133d997f9..b36a12da48 100644 --- a/src/main/java/org/opensearch/security/ssl/http/netty/Netty4ConditionalDecompressor.java +++ b/src/main/java/org/opensearch/security/ssl/http/netty/Netty4ConditionalDecompressor.java @@ -13,15 +13,12 @@ import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.HttpContentDecompressor; -import static org.opensearch.security.http.SecurityHttpServerTransport.EARLY_RESPONSE; -import static org.opensearch.security.http.SecurityHttpServerTransport.SHOULD_DECOMPRESS; - public class Netty4ConditionalDecompressor extends HttpContentDecompressor { @Override protected EmbeddedChannel newContentDecoder(String contentEncoding) throws Exception { - final boolean hasAnEarlyReponse = NettyAttribute.peekFrom(ctx, EARLY_RESPONSE).isPresent(); - final boolean shouldDecompress = NettyAttribute.popFrom(ctx, SHOULD_DECOMPRESS).orElse(false); + final boolean hasAnEarlyReponse = NettyAttribute.peekFrom(ctx, Netty4HttpRequestHeaderVerifier.EARLY_RESPONSE).isPresent(); + final boolean shouldDecompress = NettyAttribute.popFrom(ctx, Netty4HttpRequestHeaderVerifier.SHOULD_DECOMPRESS).orElse(false); if (hasAnEarlyReponse || !shouldDecompress) { // If there was an error prompting an early response,... don't decompress // If there is no explicit decompress flag,... don't decompress diff --git a/src/main/java/org/opensearch/security/ssl/http/netty/Netty4HttpRequestHeaderVerifier.java b/src/main/java/org/opensearch/security/ssl/http/netty/Netty4HttpRequestHeaderVerifier.java index e6dec0c213..9afd6b0e22 100644 --- a/src/main/java/org/opensearch/security/ssl/http/netty/Netty4HttpRequestHeaderVerifier.java +++ b/src/main/java/org/opensearch/security/ssl/http/netty/Netty4HttpRequestHeaderVerifier.java @@ -8,6 +8,8 @@ package org.opensearch.security.ssl.http.netty; +import java.util.Set; + import org.opensearch.ExceptionsHelper; import org.opensearch.OpenSearchSecurityException; import org.opensearch.common.settings.Settings; @@ -30,16 +32,19 @@ import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.HttpRequest; +import io.netty.util.AttributeKey; import io.netty.util.ReferenceCountUtil; -import static org.opensearch.security.http.SecurityHttpServerTransport.CONTEXT_TO_RESTORE; -import static org.opensearch.security.http.SecurityHttpServerTransport.EARLY_RESPONSE; -import static org.opensearch.security.http.SecurityHttpServerTransport.IS_AUTHENTICATED; -import static org.opensearch.security.http.SecurityHttpServerTransport.SHOULD_DECOMPRESS; -import static org.opensearch.security.http.SecurityHttpServerTransport.UNCONSUMED_PARAMS; - @Sharable public class Netty4HttpRequestHeaderVerifier extends SimpleChannelInboundHandler { + public static final AttributeKey IS_AUTHENTICATED = AttributeKey.newInstance("opensearch-http-is-authenticated"); + public static final AttributeKey SHOULD_DECOMPRESS = AttributeKey.newInstance("opensearch-http-should-decompress"); + public static final AttributeKey CONTEXT_TO_RESTORE = AttributeKey.newInstance( + "opensearch-http-request-thread-context" + ); + public static final AttributeKey> UNCONSUMED_PARAMS = AttributeKey.newInstance("opensearch-http-request-consumed-params"); + public static final AttributeKey EARLY_RESPONSE = AttributeKey.newInstance("opensearch-http-early-response"); + private final SecurityRestFilter restFilter; private final ThreadPool threadPool; private final SSLConfig sslConfig; @@ -72,8 +77,8 @@ public void channelRead0(ChannelHandlerContext ctx, DefaultHttpRequest msg) thro } // Start by setting this value to false, only requests that meet all the criteria will be decompressed - ctx.channel().attr(SHOULD_DECOMPRESS).set(Boolean.FALSE); - ctx.channel().attr(IS_AUTHENTICATED).set(Boolean.FALSE); + ctx.channel().attr(Netty4HttpRequestHeaderVerifier.SHOULD_DECOMPRESS).set(Boolean.FALSE); + ctx.channel().attr(Netty4HttpRequestHeaderVerifier.IS_AUTHENTICATED).set(Boolean.FALSE); final Netty4HttpChannel httpChannel = ctx.channel().attr(Netty4HttpServerTransport.HTTP_CHANNEL_KEY).get(); @@ -85,24 +90,25 @@ public void channelRead0(ChannelHandlerContext ctx, DefaultHttpRequest msg) thro // If request channel is completed and a response is sent, then there was a failure during authentication restFilter.checkAndAuthenticateRequest(requestChannel); - ctx.channel().attr(UNCONSUMED_PARAMS).set(requestChannel.getUnconsumedParams()); + ctx.channel().attr(Netty4HttpRequestHeaderVerifier.UNCONSUMED_PARAMS).set(requestChannel.getUnconsumedParams()); ThreadContext.StoredContext contextToRestore = threadPool.getThreadContext().newStoredContext(false); - ctx.channel().attr(CONTEXT_TO_RESTORE).set(contextToRestore); + ctx.channel().attr(Netty4HttpRequestHeaderVerifier.CONTEXT_TO_RESTORE).set(contextToRestore); - requestChannel.getQueuedResponse().ifPresent(response -> ctx.channel().attr(EARLY_RESPONSE).set(response)); + requestChannel.getQueuedResponse() + .ifPresent(response -> ctx.channel().attr(Netty4HttpRequestHeaderVerifier.EARLY_RESPONSE).set(response)); boolean shouldSkipAuthentication = SecurityRestUtils.shouldSkipAuthentication(requestChannel); boolean shouldDecompress = !shouldSkipAuthentication && requestChannel.getQueuedResponse().isEmpty(); if (requestChannel.getQueuedResponse().isEmpty() || shouldSkipAuthentication) { // Only allow decompression on authenticated requests that also aren't one of those ^ - ctx.channel().attr(SHOULD_DECOMPRESS).set(Boolean.valueOf(shouldDecompress)); - ctx.channel().attr(IS_AUTHENTICATED).set(Boolean.TRUE); + ctx.channel().attr(Netty4HttpRequestHeaderVerifier.SHOULD_DECOMPRESS).set(Boolean.valueOf(shouldDecompress)); + ctx.channel().attr(Netty4HttpRequestHeaderVerifier.IS_AUTHENTICATED).set(Boolean.TRUE); } } catch (final OpenSearchSecurityException e) { final SecurityResponse earlyResponse = new SecurityResponse(ExceptionsHelper.status(e).getStatus(), e); - ctx.channel().attr(EARLY_RESPONSE).set(earlyResponse); + ctx.channel().attr(Netty4HttpRequestHeaderVerifier.EARLY_RESPONSE).set(earlyResponse); } catch (final SecurityRequestChannelUnsupported srcu) { // Use defaults for unsupported channels } finally { diff --git a/src/main/java/org/opensearch/security/ssl/http/netty/SecuritySSLNettyHttpServerTransport.java b/src/main/java/org/opensearch/security/ssl/http/netty/SecuritySSLNettyHttpServerTransport.java deleted file mode 100644 index fc2f31b2b0..0000000000 --- a/src/main/java/org/opensearch/security/ssl/http/netty/SecuritySSLNettyHttpServerTransport.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2015-2017 floragunn GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package org.opensearch.security.ssl.http.netty; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.common.network.NetworkService; -import org.opensearch.common.settings.ClusterSettings; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.BigArrays; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.http.HttpChannel; -import org.opensearch.http.HttpHandlingSettings; -import org.opensearch.http.netty4.Netty4HttpChannel; -import org.opensearch.http.netty4.Netty4HttpServerTransport; -import org.opensearch.security.filter.SecurityRestFilter; -import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.ssl.SslExceptionHandler; -import org.opensearch.telemetry.tracing.Tracer; -import org.opensearch.threadpool.ThreadPool; -import org.opensearch.transport.SharedGroupFactory; - -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.handler.codec.DecoderException; -import io.netty.handler.ssl.ApplicationProtocolNames; -import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; -import io.netty.handler.ssl.SslHandler; - -public class SecuritySSLNettyHttpServerTransport extends Netty4HttpServerTransport { - private static final Logger logger = LogManager.getLogger(SecuritySSLNettyHttpServerTransport.class); - private final SecurityKeyStore sks; - private final SslExceptionHandler errorHandler; - private final ChannelInboundHandlerAdapter headerVerifier; - - public SecuritySSLNettyHttpServerTransport( - final Settings settings, - final NetworkService networkService, - final BigArrays bigArrays, - final ThreadPool threadPool, - final SecurityKeyStore sks, - final NamedXContentRegistry namedXContentRegistry, - final ValidatingDispatcher dispatcher, - final SslExceptionHandler errorHandler, - ClusterSettings clusterSettings, - SharedGroupFactory sharedGroupFactory, - Tracer tracer, - SecurityRestFilter restFilter - ) { - super( - settings, - networkService, - bigArrays, - threadPool, - namedXContentRegistry, - dispatcher, - clusterSettings, - sharedGroupFactory, - tracer - ); - this.sks = sks; - this.errorHandler = errorHandler; - headerVerifier = new Netty4HttpRequestHeaderVerifier(restFilter, threadPool, settings); - } - - @Override - public ChannelHandler configureServerChannelHandler() { - return new SSLHttpChannelHandler(this, handlingSettings, sks); - } - - @Override - public void onException(HttpChannel channel, Exception cause0) { - Throwable cause = cause0; - - if (cause0 instanceof DecoderException && cause0 != null) { - cause = cause0.getCause(); - } - - errorHandler.logError(cause, true); - logger.error("Exception during establishing a SSL connection: " + cause, cause); - - super.onException(channel, cause0); - } - - protected class SSLHttpChannelHandler extends Netty4HttpServerTransport.HttpChannelHandler { - /** - * Application negotiation handler to select either HTTP 1.1 or HTTP 2 protocol, based - * on client/server ALPN negotiations. - */ - private class Http2OrHttpHandler extends ApplicationProtocolNegotiationHandler { - protected Http2OrHttpHandler() { - super(ApplicationProtocolNames.HTTP_1_1); - } - - @Override - protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception { - if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { - configureDefaultHttp2Pipeline(ctx.pipeline()); - } else if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { - configureDefaultHttpPipeline(ctx.pipeline()); - } else { - throw new IllegalStateException("Unknown application protocol: " + protocol); - } - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - super.exceptionCaught(ctx, cause); - Netty4HttpChannel channel = ctx.channel().attr(HTTP_CHANNEL_KEY).get(); - if (channel != null) { - if (cause instanceof Error) { - onException(channel, new Exception(cause)); - } else { - onException(channel, (Exception) cause); - } - } - } - } - - protected SSLHttpChannelHandler( - Netty4HttpServerTransport transport, - final HttpHandlingSettings handlingSettings, - final SecurityKeyStore odsks - ) { - super(transport, handlingSettings); - } - - @Override - protected void initChannel(Channel ch) throws Exception { - super.initChannel(ch); - final SslHandler sslHandler = new SslHandler(SecuritySSLNettyHttpServerTransport.this.sks.createHTTPSSLEngine()); - ch.pipeline().addFirst("ssl_http", sslHandler); - } - - @Override - protected void configurePipeline(Channel ch) { - ch.pipeline().addLast(new Http2OrHttpHandler()); - } - } - - @Override - protected ChannelInboundHandlerAdapter createHeaderVerifier() { - return headerVerifier; - } - - @Override - protected ChannelInboundHandlerAdapter createDecompressor() { - return new Netty4ConditionalDecompressor(); - } -} diff --git a/src/main/java/org/opensearch/security/ssl/transport/DualModeSSLHandler.java b/src/main/java/org/opensearch/security/ssl/transport/DualModeSSLHandler.java deleted file mode 100644 index a7961f864b..0000000000 --- a/src/main/java/org/opensearch/security/ssl/transport/DualModeSSLHandler.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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.ssl.transport; - -import java.nio.charset.StandardCharsets; -import java.util.List; -import javax.net.ssl.SSLException; - -import com.google.common.annotations.VisibleForTesting; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.ssl.util.SSLConnectionTestUtil; -import org.opensearch.security.ssl.util.TLSUtil; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPipeline; -import io.netty.handler.codec.ByteToMessageDecoder; -import io.netty.handler.ssl.SslHandler; - -/** - * Modifies the current pipeline dynamically to enable TLS - */ -public class DualModeSSLHandler extends ByteToMessageDecoder { - - private static final Logger logger = LogManager.getLogger(DualModeSSLHandler.class); - private final SecurityKeyStore securityKeyStore; - - private final SslHandler providedSSLHandler; - - public DualModeSSLHandler(SecurityKeyStore securityKeyStore) { - this(securityKeyStore, null); - } - - @VisibleForTesting - protected DualModeSSLHandler(SecurityKeyStore securityKeyStore, SslHandler providedSSLHandler) { - this.securityKeyStore = securityKeyStore; - this.providedSSLHandler = providedSSLHandler; - } - - @Override - protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { - // Will use the first six bytes to detect a protocol. - if (in.readableBytes() < 6) { - return; - } - int offset = in.readerIndex(); - if (in.getCharSequence(offset, 6, StandardCharsets.UTF_8).equals(SSLConnectionTestUtil.DUAL_MODE_CLIENT_HELLO_MSG)) { - logger.debug("Received DualSSL Client Hello message"); - ByteBuf responseBuffer = Unpooled.buffer(6); - responseBuffer.writeCharSequence(SSLConnectionTestUtil.DUAL_MODE_SERVER_HELLO_MSG, StandardCharsets.UTF_8); - ctx.writeAndFlush(responseBuffer).addListener(ChannelFutureListener.CLOSE); - return; - } - - if (TLSUtil.isTLS(in)) { - logger.debug("Identified request as SSL request"); - enableSsl(ctx); - } else { - logger.debug("Identified request as non SSL request, running in HTTP mode as dual mode is enabled"); - ctx.pipeline().remove(this); - } - } - - private void enableSsl(ChannelHandlerContext ctx) throws SSLException { - SslHandler sslHandler; - if (providedSSLHandler != null) { - sslHandler = providedSSLHandler; - } else { - sslHandler = new SslHandler(securityKeyStore.createServerTransportSSLEngine()); - } - ChannelPipeline p = ctx.pipeline(); - p.addAfter("port_unification_handler", "ssl_server", sslHandler); - p.remove(this); - logger.debug("Removed port unification handler and added SSL handler as incoming request is SSL"); - } -} diff --git a/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLNettyTransport.java b/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLNettyTransport.java deleted file mode 100644 index 5be3424528..0000000000 --- a/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLNettyTransport.java +++ /dev/null @@ -1,308 +0,0 @@ -/* - * Copyright 2015-2017 floragunn GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -/* - * 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.ssl.transport; - -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.security.AccessController; -import java.security.PrivilegedAction; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLException; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.ExceptionsHelper; -import org.opensearch.OpenSearchSecurityException; -import org.opensearch.Version; -import org.opensearch.cluster.node.DiscoveryNode; -import org.opensearch.common.network.NetworkService; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.PageCacheRecycler; -import org.opensearch.core.common.io.stream.NamedWriteableRegistry; -import org.opensearch.core.indices.breaker.CircuitBreakerService; -import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.ssl.SslExceptionHandler; -import org.opensearch.security.ssl.util.SSLConfigConstants; -import org.opensearch.security.ssl.util.SSLConnectionTestResult; -import org.opensearch.security.ssl.util.SSLConnectionTestUtil; -import org.opensearch.telemetry.tracing.Tracer; -import org.opensearch.threadpool.ThreadPool; -import org.opensearch.transport.SharedGroupFactory; -import org.opensearch.transport.TcpChannel; -import org.opensearch.transport.netty4.Netty4Transport; - -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelOutboundHandlerAdapter; -import io.netty.channel.ChannelPromise; -import io.netty.handler.codec.DecoderException; -import io.netty.handler.ssl.SslHandler; - -public class SecuritySSLNettyTransport extends Netty4Transport { - - private static final Logger logger = LogManager.getLogger(SecuritySSLNettyTransport.class); - private final SecurityKeyStore ossks; - private final SslExceptionHandler errorHandler; - private final SSLConfig SSLConfig; - - public SecuritySSLNettyTransport( - final Settings settings, - final Version version, - final ThreadPool threadPool, - final NetworkService networkService, - final PageCacheRecycler pageCacheRecycler, - final NamedWriteableRegistry namedWriteableRegistry, - final CircuitBreakerService circuitBreakerService, - final SecurityKeyStore ossks, - final SslExceptionHandler errorHandler, - SharedGroupFactory sharedGroupFactory, - final SSLConfig SSLConfig, - final Tracer tracer - ) { - super( - settings, - version, - threadPool, - networkService, - pageCacheRecycler, - namedWriteableRegistry, - circuitBreakerService, - sharedGroupFactory, - tracer - ); - - this.ossks = ossks; - this.errorHandler = errorHandler; - this.SSLConfig = SSLConfig; - } - - // This allows for testing log messages - Logger getLogger() { - return logger; - } - - @Override - public void onException(TcpChannel channel, Exception e) { - - Throwable cause = e; - - if (e instanceof DecoderException && e != null) { - cause = e.getCause(); - } - - errorHandler.logError(cause, false); - getLogger().error("Exception during establishing a SSL connection: " + cause, cause); - - if (channel == null || !channel.isOpen()) { - throw new OpenSearchSecurityException("The provided TCP channel is invalid.", e); - } - super.onException(channel, e); - } - - @Override - protected ChannelHandler getServerChannelInitializer(String name) { - return new SSLServerChannelInitializer(name); - } - - @Override - protected ChannelHandler getClientChannelInitializer(DiscoveryNode node) { - return new SSLClientChannelInitializer(node); - } - - protected class SSLServerChannelInitializer extends Netty4Transport.ServerChannelInitializer { - - public SSLServerChannelInitializer(String name) { - super(name); - } - - @Override - protected void initChannel(Channel ch) throws Exception { - super.initChannel(ch); - - boolean dualModeEnabled = SSLConfig.isDualModeEnabled(); - if (dualModeEnabled) { - logger.info("SSL Dual mode enabled, using port unification handler"); - final ChannelHandler portUnificationHandler = new DualModeSSLHandler(ossks); - ch.pipeline().addFirst("port_unification_handler", portUnificationHandler); - } else { - final SslHandler sslHandler = new SslHandler(ossks.createServerTransportSSLEngine()); - ch.pipeline().addFirst("ssl_server", sslHandler); - } - } - - @Override - public final void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - if (cause instanceof DecoderException && cause != null) { - cause = cause.getCause(); - } - - errorHandler.logError(cause, false); - getLogger().error("Exception during establishing a SSL connection: " + cause, cause); - - super.exceptionCaught(ctx, cause); - } - } - - protected static class ClientSSLHandler extends ChannelOutboundHandlerAdapter { - private final Logger log = LogManager.getLogger(this.getClass()); - private final SecurityKeyStore sks; - private final boolean hostnameVerificationEnabled; - private final boolean hostnameVerificationResovleHostName; - private final SslExceptionHandler errorHandler; - - private ClientSSLHandler( - final SecurityKeyStore sks, - final boolean hostnameVerificationEnabled, - final boolean hostnameVerificationResovleHostName, - final SslExceptionHandler errorHandler - ) { - this.sks = sks; - this.hostnameVerificationEnabled = hostnameVerificationEnabled; - this.hostnameVerificationResovleHostName = hostnameVerificationResovleHostName; - this.errorHandler = errorHandler; - } - - @Override - public final void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - if (cause instanceof DecoderException && cause != null) { - cause = cause.getCause(); - } - - errorHandler.logError(cause, false); - logger.error("Exception during establishing a SSL connection: " + cause, cause); - - super.exceptionCaught(ctx, cause); - } - - @Override - public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) - throws Exception { - SSLEngine engine = null; - try { - if (hostnameVerificationEnabled) { - final InetSocketAddress inetSocketAddress = (InetSocketAddress) remoteAddress; - String hostname = null; - if (hostnameVerificationResovleHostName) { - hostname = inetSocketAddress.getHostName(); - } else { - hostname = inetSocketAddress.getHostString(); - } - - if (log.isDebugEnabled()) { - log.debug( - "Hostname of peer is {} ({}/{}) with hostnameVerificationResovleHostName: {}", - hostname, - inetSocketAddress.getHostName(), - inetSocketAddress.getHostString(), - hostnameVerificationResovleHostName - ); - } - - engine = sks.createClientTransportSSLEngine(hostname, inetSocketAddress.getPort()); - } else { - engine = sks.createClientTransportSSLEngine(null, -1); - } - } catch (final SSLException e) { - throw ExceptionsHelper.convertToOpenSearchException(e); - } - final SslHandler sslHandler = new SslHandler(engine); - ctx.pipeline().replace(this, "ssl_client", sslHandler); - super.connect(ctx, remoteAddress, localAddress, promise); - } - } - - protected class SSLClientChannelInitializer extends Netty4Transport.ClientChannelInitializer { - private final boolean hostnameVerificationEnabled; - private final boolean hostnameVerificationResovleHostName; - private final DiscoveryNode node; - private SSLConnectionTestResult connectionTestResult; - - @SuppressWarnings("removal") - public SSLClientChannelInitializer(DiscoveryNode node) { - this.node = node; - hostnameVerificationEnabled = settings.getAsBoolean( - SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION, - true - ); - hostnameVerificationResovleHostName = settings.getAsBoolean( - SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME, - true - ); - - connectionTestResult = SSLConnectionTestResult.SSL_AVAILABLE; - if (SSLConfig.isDualModeEnabled()) { - SSLConnectionTestUtil sslConnectionTestUtil = new SSLConnectionTestUtil( - node.getAddress().getAddress(), - node.getAddress().getPort() - ); - connectionTestResult = AccessController.doPrivileged( - (PrivilegedAction) sslConnectionTestUtil::testConnection - ); - } - } - - @Override - protected void initChannel(Channel ch) throws Exception { - super.initChannel(ch); - - if (connectionTestResult == SSLConnectionTestResult.OPENSEARCH_PING_FAILED) { - logger.error( - "SSL dual mode is enabled but dual mode handshake and OpenSearch ping has failed during client connection setup, closing channel" - ); - ch.close(); - return; - } - - if (connectionTestResult == SSLConnectionTestResult.SSL_AVAILABLE) { - logger.debug("Connection to {} needs to be ssl, adding ssl handler to the client channel ", node.getHostName()); - ch.pipeline() - .addFirst( - "client_ssl_handler", - new ClientSSLHandler(ossks, hostnameVerificationEnabled, hostnameVerificationResovleHostName, errorHandler) - ); - } else { - logger.debug("Connection to {} needs to be non ssl", node.getHostName()); - } - } - - @Override - public final void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - if (cause instanceof DecoderException && cause != null) { - cause = cause.getCause(); - } - - errorHandler.logError(cause, false); - getLogger().error("Exception during establishing a SSL connection: " + cause, cause); - - super.exceptionCaught(ctx, cause); - } - } -} diff --git a/src/main/java/org/opensearch/security/state/SecurityConfig.java b/src/main/java/org/opensearch/security/state/SecurityConfig.java new file mode 100644 index 0000000000..f8de098365 --- /dev/null +++ b/src/main/java/org/opensearch/security/state/SecurityConfig.java @@ -0,0 +1,124 @@ +/* + * 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.state; + +import java.io.IOException; +import java.time.Instant; +import java.util.Objects; +import java.util.Optional; + +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.common.io.stream.Writeable; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.security.securityconf.impl.CType; + +import static java.time.format.DateTimeFormatter.ISO_INSTANT; + +public class SecurityConfig implements Writeable, ToXContent { + + private final CType type; + + private final Instant lastModified; + + private final String hash; + + public SecurityConfig(final CType type, final String hash, final Instant lastModified) { + this.type = type; + this.hash = hash; + this.lastModified = lastModified; + } + + public SecurityConfig(final StreamInput in) throws IOException { + this.type = in.readEnum(CType.class); + this.hash = in.readString(); + this.lastModified = in.readOptionalInstant(); + } + + public Optional lastModified() { + return Optional.ofNullable(lastModified); + } + + public CType type() { + return type; + } + + public String hash() { + return hash; + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeEnum(type); + out.writeString(hash); + out.writeOptionalInstant(lastModified); + } + + @Override + public XContentBuilder toXContent(final XContentBuilder xContentBuilder, final Params params) throws IOException { + xContentBuilder.startObject(type.toLCString()).field("hash", hash); + if (lastModified != null) { + xContentBuilder.field("last_modified", ISO_INSTANT.format(lastModified)); + } else { + xContentBuilder.nullField("last_modified"); + } + return xContentBuilder.endObject(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SecurityConfig that = (SecurityConfig) o; + return type == that.type && Objects.equals(lastModified, that.lastModified) && Objects.equals(hash, that.hash); + } + + @Override + public int hashCode() { + return Objects.hash(type, lastModified, hash); + } + + public final static class Builder { + + private final CType type; + + private Instant lastModified; + + private String hash; + + Builder(final SecurityConfig securityConfig) { + this.type = securityConfig.type; + this.lastModified = securityConfig.lastModified; + this.hash = securityConfig.hash; + } + + public Builder withHash(final String hash) { + this.hash = hash; + return this; + } + + public Builder withLastModified(final Instant lastModified) { + this.lastModified = lastModified; + return this; + } + + public SecurityConfig build() { + return new SecurityConfig(type, hash, lastModified); + } + + } + + public static SecurityConfig.Builder from(final SecurityConfig securityConfig) { + return new SecurityConfig.Builder(securityConfig); + } + +} diff --git a/src/main/java/org/opensearch/security/state/SecurityMetadata.java b/src/main/java/org/opensearch/security/state/SecurityMetadata.java new file mode 100644 index 0000000000..f8e2e043fd --- /dev/null +++ b/src/main/java/org/opensearch/security/state/SecurityMetadata.java @@ -0,0 +1,128 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ +package org.opensearch.security.state; + +import java.io.IOException; +import java.time.Instant; +import java.util.Comparator; +import java.util.Objects; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedSet; + +import org.opensearch.Version; +import org.opensearch.cluster.AbstractNamedDiffable; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.NamedDiff; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.XContentBuilder; + +import static java.time.format.DateTimeFormatter.ISO_INSTANT; + +public final class SecurityMetadata extends AbstractNamedDiffable implements ClusterState.Custom { + + public final static String TYPE = "security"; + + private final Instant created; + + private final Set configuration; + + public SecurityMetadata(final Instant created, final Set configuration) { + this.created = created; + this.configuration = configuration; + } + + public SecurityMetadata(StreamInput in) throws IOException { + this.created = in.readInstant(); + this.configuration = in.readSet(SecurityConfig::new); + } + + public Instant created() { + return created; + } + + public Set configuration() { + return configuration; + } + + @Override + public Version getMinimalSupportedVersion() { + return Version.CURRENT.minimumCompatibilityVersion(); + } + + @Override + public String getWriteableName() { + return TYPE; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeInstant(created); + out.writeCollection(configuration); + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.field("created", ISO_INSTANT.format(created)); + xContentBuilder.startObject("configuration"); + for (final var securityConfig : configuration) { + securityConfig.toXContent(xContentBuilder, EMPTY_PARAMS); + } + return xContentBuilder.endObject(); + } + + public static NamedDiff readDiffFrom(StreamInput in) throws IOException { + return readDiffFrom(ClusterState.Custom.class, TYPE, in); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SecurityMetadata that = (SecurityMetadata) o; + return Objects.equals(created, that.created) && Objects.equals(configuration, that.configuration); + } + + @Override + public int hashCode() { + return Objects.hash(created, configuration); + } + + public final static class Builder { + + private final Instant created; + + private final ImmutableSet.Builder configuration = new ImmutableSortedSet.Builder<>( + Comparator.comparing(SecurityConfig::type) + ); + + Builder(SecurityMetadata oldMetadata) { + this.created = oldMetadata.created; + this.configuration.addAll(oldMetadata.configuration); + } + + public Builder withSecurityConfig(final SecurityConfig securityConfig) { + this.configuration.add(securityConfig); + return this; + } + + public SecurityMetadata build() { + return new SecurityMetadata(created, configuration.build()); + } + + } + + public static SecurityMetadata.Builder from(final SecurityMetadata securityMetadata) { + return new SecurityMetadata.Builder(securityMetadata); + } + +} diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index 3060e1b2dc..5169d02d20 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -220,9 +220,14 @@ public class ConfigConstants { public static final String SECURITY_NODES_DN = "plugins.security.nodes_dn"; public static final String SECURITY_NODES_DN_DYNAMIC_CONFIG_ENABLED = "plugins.security.nodes_dn_dynamic_config_enabled"; public static final String SECURITY_DISABLED = "plugins.security.disabled"; + public static final String SECURITY_CACHE_TTL_MINUTES = "plugins.security.cache.ttl_minutes"; public static final String SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES = "plugins.security.allow_unsafe_democertificates"; public static final String SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX = "plugins.security.allow_default_init_securityindex"; + + public static final String SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE = + "plugins.security.allow_default_init_securityindex.use_cluster_state"; + public static final String SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST = "plugins.security.background_init_if_securityindex_not_exist"; diff --git a/src/main/java/org/opensearch/security/support/ConfigHelper.java b/src/main/java/org/opensearch/security/support/ConfigHelper.java index 4f310f6af7..e8526478f2 100644 --- a/src/main/java/org/opensearch/security/support/ConfigHelper.java +++ b/src/main/java/org/opensearch/security/support/ConfigHelper.java @@ -57,6 +57,7 @@ import static org.opensearch.core.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; +@Deprecated public class ConfigHelper { private static final Logger LOGGER = LogManager.getLogger(ConfigHelper.class); diff --git a/src/main/java/org/opensearch/security/support/SecurityIndexHandler.java b/src/main/java/org/opensearch/security/support/SecurityIndexHandler.java new file mode 100644 index 0000000000..1ed8a99614 --- /dev/null +++ b/src/main/java/org/opensearch/security/support/SecurityIndexHandler.java @@ -0,0 +1,233 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ +package org.opensearch.security.support; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.hash.Hashing; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.DocWriteRequest; +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.bulk.BulkRequest; +import org.opensearch.action.get.MultiGetRequest; +import org.opensearch.action.get.MultiGetResponse; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.client.Client; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.state.SecurityConfig; + +import static org.opensearch.core.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; +import static org.opensearch.security.configuration.ConfigurationRepository.DEFAULT_CONFIG_VERSION; +import static org.opensearch.security.support.YamlConfigReader.emptyJsonConfigFor; +import static org.opensearch.security.support.YamlConfigReader.yamlContentFor; + +public class SecurityIndexHandler { + + private final static int MINIMUM_HASH_BITS = 128; + + private static final Logger LOGGER = LogManager.getLogger(SecurityIndexHandler.class); + + private final Settings settings; + + private final Client client; + + private final String indexName; + + public SecurityIndexHandler(final String indexName, final Settings settings, final Client client) { + this.indexName = indexName; + this.settings = settings; + this.client = client; + } + + public final static Map INDEX_SETTINGS = Map.of("index.number_of_shards", 1, "index.auto_expand_replicas", "0-all"); + + public void createIndex(ActionListener listener) { + try (final ThreadContext.StoredContext threadContext = client.threadPool().getThreadContext().stashContext()) { + client.admin() + .indices() + .create( + new CreateIndexRequest(indexName).settings(INDEX_SETTINGS).waitForActiveShards(1), + ActionListener.runBefore(ActionListener.wrap(r -> { + if (r.isAcknowledged()) { + listener.onResponse(true); + } else listener.onFailure(new SecurityException("Couldn't create security index " + indexName)); + }, listener::onFailure), threadContext::restore) + ); + } + } + + public void uploadDefaultConfiguration(final Path configDir, final ActionListener> listener) { + try (final ThreadContext.StoredContext threadContext = client.threadPool().getThreadContext().stashContext()) { + AccessController.doPrivileged((PrivilegedAction) () -> { + try { + LOGGER.info("Uploading default security configuration from {}", configDir.toAbsolutePath()); + final var bulkRequest = new BulkRequest().setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); + final var configuration = new ImmutableSortedSet.Builder<>(Comparator.comparing(SecurityConfig::type)); + for (final var cType : CType.values()) { + final var fileExists = Files.exists(cType.configFile(configDir)); + // Audit config is not packaged by default and while list is deprecated + if ((cType == CType.AUDIT || cType == CType.WHITELIST) && !fileExists) continue; + if (cType == CType.WHITELIST) { + LOGGER.warn( + "WHITELIST configuration type is deprecated and will be replaced with ALLOWLIST in the next major version" + ); + } + final var yamlContent = yamlContentFor(cType, configDir); + final var hash = Hashing.goodFastHash(MINIMUM_HASH_BITS).hashBytes(yamlContent.toBytesRef().bytes); + configuration.add(new SecurityConfig(cType, hash.toString(), null)); + bulkRequest.add( + new IndexRequest(indexName).id(cType.toLCString()) + .opType(DocWriteRequest.OpType.INDEX) + .source(cType.toLCString(), yamlContent) + ); + } + client.bulk(bulkRequest, ActionListener.runBefore(ActionListener.wrap(r -> { + if (r.hasFailures()) { + listener.onFailure(new SecurityException(r.buildFailureMessage())); + return; + } + listener.onResponse(configuration.build()); + }, listener::onFailure), threadContext::restore)); + } catch (final IOException ioe) { + listener.onFailure(new SecurityException(ioe)); + } + return null; + }); + } + } + + public void loadConfiguration( + final Set configuration, + final ActionListener>> listener + ) { + try (final ThreadContext.StoredContext threadContext = client.threadPool().getThreadContext().stashContext()) { + client.threadPool().getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); + final var configurationTypes = configuration.stream().map(SecurityConfig::type).collect(Collectors.toUnmodifiableList()); + client.multiGet(newMultiGetRequest(configurationTypes), ActionListener.runBefore(ActionListener.wrap(r -> { + final var cTypeConfigsBuilder = ImmutableMap.>builderWithExpectedSize( + configuration.size() + ); + var hasFailures = false; + for (final var item : r.getResponses()) { + if (item.isFailed()) { + listener.onFailure(new SecurityException(multiGetFailureMessage(item.getId(), item.getFailure()))); + hasFailures = true; + break; + } + final var cType = CType.fromString(item.getId()); + final var cTypeResponse = item.getResponse(); + if (cTypeResponse.isExists() && !cTypeResponse.isSourceEmpty()) { + final var config = buildDynamicConfiguration( + cType, + cTypeResponse.getSourceAsBytesRef(), + cTypeResponse.getSeqNo(), + cTypeResponse.getPrimaryTerm() + ); + if (config.getVersion() != DEFAULT_CONFIG_VERSION) { + listener.onFailure( + new SecurityException("Version " + config.getVersion() + " is not supported for " + cType.name()) + ); + hasFailures = true; + break; + } + cTypeConfigsBuilder.put(cType, config); + } else { + if (!cType.emptyIfMissing()) { + listener.onFailure(new SecurityException("Missing required configuration for type: " + cType)); + hasFailures = true; + break; + } + cTypeConfigsBuilder.put( + cType, + SecurityDynamicConfiguration.fromJson( + emptyJsonConfigFor(cType), + cType, + DEFAULT_CONFIG_VERSION, + cTypeResponse.getSeqNo(), + cTypeResponse.getPrimaryTerm() + ) + ); + } + } + if (!hasFailures) { + listener.onResponse(cTypeConfigsBuilder.build()); + } + }, listener::onFailure), threadContext::restore)); + } + } + + private MultiGetRequest newMultiGetRequest(final List configurationTypes) { + final var request = new MultiGetRequest().realtime(true).refresh(true); + for (final var cType : configurationTypes) { + request.add(indexName, cType.toLCString()); + } + return request; + } + + private SecurityDynamicConfiguration buildDynamicConfiguration( + final CType cType, + final BytesReference bytesRef, + final long seqNo, + final long primaryTerm + ) { + try { + final var source = SecurityUtils.replaceEnvVars(configTypeSource(bytesRef.streamInput()), settings); + final var jsonNode = DefaultObjectMapper.readTree(source); + var version = 1; + if (jsonNode.has("_meta")) { + if (jsonNode.get("_meta").has("config_version")) { + version = jsonNode.get("_meta").get("config_version").asInt(); + } + } + return SecurityDynamicConfiguration.fromJson(source, cType, version, seqNo, primaryTerm); + } catch (IOException e) { + throw new SecurityException("Couldn't parse content for " + cType, e); + } + } + + private String configTypeSource(final InputStream inputStream) throws IOException { + final var jsonContent = XContentType.JSON.xContent(); + try (final var parser = jsonContent.createParser(NamedXContentRegistry.EMPTY, THROW_UNSUPPORTED_OPERATION, inputStream)) { + parser.nextToken(); + parser.nextToken(); + parser.nextToken(); + return new String(parser.binaryValue(), StandardCharsets.UTF_8); + } + } + + private String multiGetFailureMessage(final String cTypeId, final MultiGetResponse.Failure failure) { + return String.format("Failure %s retrieving configuration for %s (index=%s)", failure, cTypeId, indexName); + } + +} diff --git a/src/main/java/org/opensearch/security/support/YamlConfigReader.java b/src/main/java/org/opensearch/security/support/YamlConfigReader.java new file mode 100644 index 0000000000..237e5b5bfb --- /dev/null +++ b/src/main/java/org/opensearch/security/support/YamlConfigReader.java @@ -0,0 +1,95 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ +package org.opensearch.security.support; + +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.Meta; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; + +import static org.opensearch.core.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; +import static org.opensearch.security.configuration.ConfigurationRepository.DEFAULT_CONFIG_VERSION; + +/** + * Read YAML security config files + */ +public final class YamlConfigReader { + + private static final Logger LOGGER = LogManager.getLogger(YamlConfigReader.class); + + public static BytesReference yamlContentFor(final CType cType, final Path configDir) throws IOException { + final var yamlXContent = XContentType.YAML.xContent(); + try ( + final var r = newReader(cType, configDir); + final var parser = yamlXContent.createParser(NamedXContentRegistry.EMPTY, THROW_UNSUPPORTED_OPERATION, r) + ) { + parser.nextToken(); + try (final var xContentBuilder = XContentFactory.jsonBuilder()) { + xContentBuilder.copyCurrentStructure(parser); + final var bytesRef = BytesReference.bytes(xContentBuilder); + validateYamlContent(cType, bytesRef.streamInput()); + return bytesRef; + } + } + } + + public static Reader newReader(final CType cType, final Path configDir) throws IOException { + final var cTypeFile = cType.configFile(configDir); + final var fileExists = Files.exists(cTypeFile); + if (!fileExists && !cType.emptyIfMissing()) { + throw new IOException("Couldn't find configuration file " + cTypeFile.getFileName()); + } + if (fileExists) { + LOGGER.info("Reading {} configuration from {}", cType, cTypeFile.getFileName()); + return new FileReader(cTypeFile.toFile(), StandardCharsets.UTF_8); + } else { + LOGGER.info("Reading empty {} configuration", cType); + return new StringReader(emptyYamlConfigFor(cType)); + } + } + + private static SecurityDynamicConfiguration emptyConfigFor(final CType cType) { + final var emptyConfiguration = SecurityDynamicConfiguration.empty(); + emptyConfiguration.setCType(cType); + emptyConfiguration.set_meta(new Meta()); + emptyConfiguration.get_meta().setConfig_version(DEFAULT_CONFIG_VERSION); + emptyConfiguration.get_meta().setType(cType.toLCString()); + return emptyConfiguration; + } + + public static String emptyJsonConfigFor(final CType cType) throws IOException { + return DefaultObjectMapper.writeValueAsString(emptyConfigFor(cType), false); + } + + public static String emptyYamlConfigFor(final CType cType) throws IOException { + return DefaultObjectMapper.YAML_MAPPER.writeValueAsString(emptyConfigFor(cType)); + } + + private static void validateYamlContent(final CType cType, final InputStream in) throws IOException { + SecurityDynamicConfiguration.fromNode(DefaultObjectMapper.YAML_MAPPER.readTree(in), cType, DEFAULT_CONFIG_VERSION, -1, -1); + } + +} 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 e66215ac9a..72f0247e53 100644 --- a/src/main/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurer.java +++ b/src/main/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurer.java @@ -58,6 +58,7 @@ public class SecuritySettingsConfigurer { ".plugins-ml-conversation-interactions", ".plugins-ml-memory-meta", ".plugins-ml-memory-message", + ".plugins-ml-stop-words", ".opendistro-alerting-config", ".opendistro-alerting-alert*", ".opendistro-anomaly-results*", diff --git a/src/test/java/org/opensearch/security/configuration/ConfigurationRepositoryTest.java b/src/test/java/org/opensearch/security/configuration/ConfigurationRepositoryTest.java index 5ce1873405..30cbbe6a01 100644 --- a/src/test/java/org/opensearch/security/configuration/ConfigurationRepositoryTest.java +++ b/src/test/java/org/opensearch/security/configuration/ConfigurationRepositoryTest.java @@ -13,22 +13,37 @@ import java.io.IOException; import java.nio.file.Path; +import java.time.Instant; +import java.util.Map; +import java.util.Set; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.opensearch.client.Client; +import org.opensearch.cluster.ClusterChangedEvent; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.ClusterStateUpdateTask; +import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.cluster.node.DiscoveryNodes; import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.Priority; import org.opensearch.common.settings.Settings; +import org.opensearch.core.action.ActionListener; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.state.SecurityConfig; +import org.opensearch.security.state.SecurityMetadata; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.SecurityIndexHandler; import org.opensearch.security.transport.SecurityInterceptorTests; import org.opensearch.threadpool.ThreadPool; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnitRunner; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -36,7 +51,22 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; +import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX; +import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +@RunWith(MockitoJUnitRunner.class) public class ConfigurationRepositoryTest { @Mock @@ -50,21 +80,44 @@ public class ConfigurationRepositoryTest { private ThreadPool threadPool; + @Mock + private SecurityIndexHandler securityIndexHandler; + + @Mock + private ClusterChangedEvent event; + @Before public void setUp() { - MockitoAnnotations.openMocks(this); - Settings settings = Settings.builder() .put("node.name", SecurityInterceptorTests.class.getSimpleName()) .put("request.headers.default", "1") .build(); threadPool = new ThreadPool(settings); + + final var previousState = mock(ClusterState.class); + final var previousDiscoveryNodes = mock(DiscoveryNodes.class); + when(previousState.nodes()).thenReturn(previousDiscoveryNodes); + when(event.previousState()).thenReturn(previousState); + + final var newState = mock(ClusterState.class); + when(event.state()).thenReturn(newState); + when(event.state().metadata()).thenReturn(mock(Metadata.class)); + + when(event.state().custom(SecurityMetadata.TYPE)).thenReturn(null); } private ConfigurationRepository createConfigurationRepository(Settings settings) { - - return ConfigurationRepository.create(settings, path, threadPool, localClient, clusterService, auditLog); + return new ConfigurationRepository( + settings.get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX), + settings, + path, + threadPool, + localClient, + clusterService, + auditLog, + securityIndexHandler + ); } @Test @@ -77,7 +130,7 @@ public void create_shouldReturnConfigurationRepository() { @Test public void initOnNodeStart_withSecurityIndexCreationEnabledShouldSetInstallDefaultConfigTrue() { - Settings settings = Settings.builder().put(ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, true).build(); + Settings settings = Settings.builder().put(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, true).build(); ConfigurationRepository configRepository = createConfigurationRepository(settings); @@ -111,4 +164,129 @@ public void getConfiguration_withInvalidConfigurationShouldReturnNewEmptyConfigu assertThat(config.getSeqNo(), is(equalTo(emptyConfig.getSeqNo()))); assertThat(config, is(not(equalTo(emptyConfig)))); } + + @Test + public void testClusterChanged_shouldInitSecurityIndexIfNoSecurityData() { + when(event.previousState().nodes().isLocalNodeElectedClusterManager()).thenReturn(false); + when(event.localNodeClusterManager()).thenReturn(true); + + final var configurationRepository = mock(ConfigurationRepository.class); + doCallRealMethod().when(configurationRepository).clusterChanged(any()); + configurationRepository.clusterChanged(event); + + verify(configurationRepository).initSecurityIndex(any()); + } + + @Test + public void testClusterChanged_shouldExecuteInitialization() { + when(event.state().custom(SecurityMetadata.TYPE)).thenReturn(new SecurityMetadata(Instant.now(), Set.of())); + + final var configurationRepository = mock(ConfigurationRepository.class); + doCallRealMethod().when(configurationRepository).clusterChanged(any()); + configurationRepository.clusterChanged(event); + + verify(configurationRepository).executeConfigurationInitialization(any()); + } + + @Test + public void testClusterChanged_shouldNotExecuteInitialization() { + final var configurationRepository = mock(ConfigurationRepository.class); + doCallRealMethod().when(configurationRepository).clusterChanged(any()); + configurationRepository.clusterChanged(event); + + verify(configurationRepository, never()).executeConfigurationInitialization(any()); + } + + @Test + public void testInitSecurityIndex_shouldCreateIndexAndUploadConfiguration() throws Exception { + System.setProperty("security.default_init.dir", Path.of(".").toString()); + ConfigurationRepository configRepository = createConfigurationRepository(Settings.EMPTY); + + doAnswer(invocation -> { + @SuppressWarnings("unchecked") + final var listener = (ActionListener) invocation.getArgument(0); + listener.onResponse(true); + return null; + }).when(securityIndexHandler).createIndex(any()); + doAnswer(invocation -> { + @SuppressWarnings("unchecked") + final var listener = (ActionListener>) invocation.getArgument(1); + listener.onResponse(Set.of(new SecurityConfig(CType.CONFIG, "aaa", null))); + return null; + }).when(securityIndexHandler).uploadDefaultConfiguration(any(), any()); + when(event.state().metadata().hasIndex(OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX)).thenReturn(false); + configRepository.initSecurityIndex(event); + + final var clusterStateUpdateTaskCaptor = ArgumentCaptor.forClass(ClusterStateUpdateTask.class); + verify(securityIndexHandler).createIndex(any()); + verify(securityIndexHandler).uploadDefaultConfiguration(any(), any()); + verify(clusterService).submitStateUpdateTask(anyString(), clusterStateUpdateTaskCaptor.capture()); + verifyNoMoreInteractions(clusterService, securityIndexHandler); + + assertClusterState(clusterStateUpdateTaskCaptor); + } + + @Test + public void testInitSecurityIndex_shouldUploadConfigIfIndexCreated() throws Exception { + System.setProperty("security.default_init.dir", Path.of(".").toString()); + + doAnswer(invocation -> { + @SuppressWarnings("unchecked") + final var listener = (ActionListener>) invocation.getArgument(1); + listener.onResponse(Set.of(new SecurityConfig(CType.CONFIG, "aaa", null))); + return null; + }).when(securityIndexHandler).uploadDefaultConfiguration(any(), any()); + + when(event.state().metadata().hasIndex(OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX)).thenReturn(true); + + ConfigurationRepository configRepository = createConfigurationRepository(Settings.EMPTY); + configRepository.initSecurityIndex(event); + + final var clusterStateUpdateTaskCaptor = ArgumentCaptor.forClass(ClusterStateUpdateTask.class); + + verify(event.state().metadata()).hasIndex(OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); + verify(clusterService).submitStateUpdateTask(anyString(), clusterStateUpdateTaskCaptor.capture()); + verify(securityIndexHandler, never()).createIndex(any()); + verify(securityIndexHandler).uploadDefaultConfiguration(any(), any()); + verifyNoMoreInteractions(securityIndexHandler, clusterService); + + assertClusterState(clusterStateUpdateTaskCaptor); + } + + @Test + public void testExecuteConfigurationInitialization_executeInitializationOnlyOnce() throws Exception { + doAnswer(invocation -> { + @SuppressWarnings("unchecked") + final var listener = (ActionListener>>) invocation.getArgument(1); + listener.onResponse(Map.of()); + return null; + }).when(securityIndexHandler).loadConfiguration(any(), any()); + + ConfigurationRepository configRepository = createConfigurationRepository(Settings.EMPTY); + configRepository.executeConfigurationInitialization( + new SecurityMetadata(Instant.now(), Set.of(new SecurityConfig(CType.CONFIG, "aaa", null))) + ).get(); + + verify(securityIndexHandler).loadConfiguration(any(), any()); + verifyNoMoreInteractions(securityIndexHandler); + + reset(securityIndexHandler); + + configRepository.executeConfigurationInitialization( + new SecurityMetadata(Instant.now(), Set.of(new SecurityConfig(CType.CONFIG, "aaa", null))) + ).get(); + + verify(securityIndexHandler, never()).loadConfiguration(any(), any()); + verifyNoMoreInteractions(securityIndexHandler); + } + + void assertClusterState(final ArgumentCaptor clusterStateUpdateTaskCaptor) throws Exception { + final var initializedStateUpdate = clusterStateUpdateTaskCaptor.getValue(); + assertEquals(Priority.IMMEDIATE, initializedStateUpdate.priority()); + var clusterState = initializedStateUpdate.execute(ClusterState.EMPTY_STATE); + SecurityMetadata securityMetadata = clusterState.custom(SecurityMetadata.TYPE); + assertNotNull(securityMetadata.created()); + assertNotNull(securityMetadata.configuration()); + } + } diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiTest.java index fb166779ac..fbeb3473fc 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiTest.java @@ -441,25 +441,46 @@ public void testActionGroupsApiForActionGroupsRestApiAdmin() throws Exception { } @Test - public void testCreateActionGroupWithRestAdminPermissionsForbidden() throws Exception { + public void testCreateOrUpdateRestApiAdminActionGroupForbidden() throws Exception { setupWithRestRoles(Settings.builder().put(SECURITY_RESTAPI_ADMIN_ENABLED, true).build()); rh.sendAdminCertificate = false; - final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); - final Header restApiAdminActionGroupsHeader = encodeBasicHeader("rest_api_admin_actiongroups", "rest_api_admin_actiongroups"); - final Header restApiHeader = encodeBasicHeader("test", "test"); + final var userHeaders = List.of( + encodeBasicHeader("admin", "admin"), + encodeBasicHeader("test", "test"), + encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"), + encodeBasicHeader("rest_api_admin_actiongroups", "rest_api_admin_actiongroups") + ); + for (final var userHeader : userHeaders) { + // attempt to create new action group with REST admin permissions + verifyPutForbidden("new_rest_api_admin_group", restAdminAllowedActions(), userHeader); + verifyPatchForbidden(createPatchRestAdminPermissionsPayload("new_rest_api_admin_group", "add"), userHeader); + + // attempt to update existing action group which has REST admin permissions + verifyPutForbidden("rest_admin_action_group", restAdminAllowedActions(), userHeader); + verifyPatchForbidden(createPatchRestAdminPermissionsPayload("rest_admin_action_group", "replace"), userHeader); + + // attempt to update existing action group with REST admin permissions + verifyPutForbidden("OPENDISTRO_SECURITY_CLUSTER_ALL", restAdminAllowedActions(), userHeader); + verifyPatchForbidden(createPatchRestAdminPermissionsPayload("OPENDISTRO_SECURITY_CLUSTER_ALL", "replace"), userHeader); + + // attempt to delete + verifyDeleteForbidden("rest_admin_action_group", userHeader); + verifyPatchForbidden(createPatchRestAdminPermissionsPayload("rest_admin_action_group", "remove"), userHeader); + } + } - HttpResponse response = rh.executePutRequest(ENDPOINT + "/rest_api_admin_group", restAdminAllowedActions(), restApiAdminHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - response = rh.executePutRequest(ENDPOINT + "/rest_api_admin_group", restAdminAllowedActions(), restApiAdminActionGroupsHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - response = rh.executePutRequest(ENDPOINT + "/rest_api_admin_group", restAdminAllowedActions(), restApiHeader); + void verifyPutForbidden(final String actionGroupName, final String payload, final Header... header) { + HttpResponse response = rh.executePutRequest(ENDPOINT + "/" + actionGroupName, payload, header); Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + } - response = rh.executePatchRequest(ENDPOINT, restAdminPatchBody(), restApiAdminHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - response = rh.executePatchRequest(ENDPOINT, restAdminPatchBody(), restApiAdminActionGroupsHeader); + void verifyPatchForbidden(final String payload, final Header... header) { + HttpResponse response = rh.executePatchRequest(ENDPOINT, payload, header); Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - response = rh.executePatchRequest(ENDPOINT, restAdminPatchBody(), restApiHeader); + } + + void verifyDeleteForbidden(final String actionGroupName, final Header... header) { + HttpResponse response = rh.executeDeleteRequest(ENDPOINT + "/" + actionGroupName, header); Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); } @@ -469,13 +490,30 @@ String restAdminAllowedActions() throws JsonProcessingException { return DefaultObjectMapper.objectMapper.writeValueAsString(rootNode); } - String restAdminPatchBody() throws JsonProcessingException { + private String createPatchRestAdminPermissionsPayload(final String actionGroup, final String op) throws JsonProcessingException { final ArrayNode rootNode = DefaultObjectMapper.objectMapper.createArrayNode(); - final ObjectNode opAddRootNode = DefaultObjectMapper.objectMapper.createObjectNode(); + final ObjectNode opAddObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); final ObjectNode allowedActionsNode = DefaultObjectMapper.objectMapper.createObjectNode(); allowedActionsNode.set("allowed_actions", clusterPermissionsForRestAdmin("cluster/*")); - opAddRootNode.put("op", "add").put("path", "/rest_api_admin_group").set("value", allowedActionsNode); - rootNode.add(opAddRootNode); + if ("add".equals(op)) { + opAddObjectNode.put("op", "add").put("path", "/" + actionGroup).set("value", allowedActionsNode); + rootNode.add(opAddObjectNode); + } + + if ("remove".equals(op)) { + final ObjectNode opRemoveObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); + opRemoveObjectNode.put("op", "remove").put("path", "/" + actionGroup); + rootNode.add(opRemoveObjectNode); + } + + if ("replace".equals(op)) { + final ObjectNode replaceRemoveObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); + replaceRemoveObjectNode.put("op", "replace") + .put("path", "/" + actionGroup + "/allowed_actions") + .set("value", clusterPermissionsForRestAdmin("*")); + + rootNode.add(replaceRemoveObjectNode); + } return DefaultObjectMapper.objectMapper.writeValueAsString(rootNode); } diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiActionValidationTest.java index 88a358dcb2..7fe089c0ba 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiActionValidationTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiActionValidationTest.java @@ -28,21 +28,18 @@ public class RolesApiActionValidationTest extends AbstractApiActionValidationTes @Test public void isAllowedToChangeImmutableEntity() throws Exception { - when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.ROLES)).thenReturn(true); - final var role = new RoleV7(); role.setCluster_permissions(restApiAdminPermissions()); - final var rolesApiActionEndpointValidator = new RolesApiAction(clusterService, threadPool, securityApiDependencies).createEndpointValidator(); - final var result = rolesApiActionEndpointValidator.isAllowedToChangeImmutableEntity(SecurityConfiguration.of( "sss", configuration)); + final var rolesApiActionEndpointValidator = new RolesApiAction(clusterService, threadPool, securityApiDependencies) + .createEndpointValidator(); + final var result = rolesApiActionEndpointValidator.isAllowedToChangeImmutableEntity(SecurityConfiguration.of("sss", configuration)); assertTrue(result.isValid()); } @Test public void isNotAllowedRightsToChangeImmutableEntity() throws Exception { - when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.ROLES)).thenReturn(false); - final var role = new RoleV7(); role.setCluster_permissions(restApiAdminPermissions()); @@ -50,8 +47,9 @@ public void isNotAllowedRightsToChangeImmutableEntity() throws Exception { Mockito.when(configuration.getCEntry("sss")).thenReturn(role); when(restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(any(Object.class))).thenCallRealMethod(); - final var rolesApiActionEndpointValidator = new RolesApiAction(clusterService, threadPool, securityApiDependencies).createEndpointValidator(); - final var result = rolesApiActionEndpointValidator.isAllowedToChangeImmutableEntity(SecurityConfiguration.of( "sss", configuration)); + final var rolesApiActionEndpointValidator = new RolesApiAction(clusterService, threadPool, securityApiDependencies) + .createEndpointValidator(); + final var result = rolesApiActionEndpointValidator.isAllowedToChangeImmutableEntity(SecurityConfiguration.of("sss", configuration)); assertFalse(result.isValid()); assertEquals(RestStatus.FORBIDDEN, result.status()); diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiTest.java index bb11bb2226..eb3ad7d4e5 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiTest.java @@ -691,105 +691,74 @@ public void testRolesApiWithRestApiRolePermission() throws Exception { } @Test - public void testCreateOrUpdateRestApiAdminRoleForbiddenForNonSuperAdmin() throws Exception { + public void testCrudRestApiAdminRoleForbidden() throws Exception { setupWithRestRoles(Settings.builder().put(SECURITY_RESTAPI_ADMIN_ENABLED, true).build()); rh.sendAdminCertificate = false; - final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); - final Header adminHeader = encodeBasicHeader("admin", "admin"); - final Header restApiHeader = encodeBasicHeader("test", "test"); - - final String restAdminPermissionsPayload = createRestAdminPermissionsPayload("cluster/*"); - HttpResponse response = rh.executePutRequest( - ENDPOINT + "/roles/new_rest_admin_role", - restAdminPermissionsPayload, - restApiAdminHeader - ); - Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); - response = rh.executePutRequest(ENDPOINT + "/roles/rest_admin_role_to_delete", restAdminPermissionsPayload, restApiAdminHeader); - Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); - - // attempt to create a new rest admin role by admin - response = rh.executePutRequest(ENDPOINT + "/roles/some_rest_admin_role", restAdminPermissionsPayload, adminHeader); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - // attempt to update exiting admin role - response = rh.executePutRequest(ENDPOINT + "/roles/new_rest_admin_role", restAdminPermissionsPayload, adminHeader); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - // attempt to patch exiting admin role - response = rh.executePatchRequest( - ENDPOINT + "/roles/new_rest_admin_role", - createPatchRestAdminPermissionsPayload("replace"), - adminHeader - ); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - // attempt to update exiting admin role - response = rh.executePutRequest(ENDPOINT + "/roles/new_rest_admin_role", restAdminPermissionsPayload, restApiHeader); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - // attempt to create a new rest admin role by admin - response = rh.executePutRequest(ENDPOINT + "/roles/some_rest_admin_role", restAdminPermissionsPayload, restApiHeader); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - // attempt to patch exiting admin role and crate a new one - response = rh.executePatchRequest(ENDPOINT + "/roles", createPatchRestAdminPermissionsPayload("replace"), restApiHeader); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + final var userHeaders = List.of( + encodeBasicHeader("admin", "admin"), + encodeBasicHeader("test", "test"), + encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"), + encodeBasicHeader("rest_api_admin_roles", "rest_api_admin_roles") + ); + for (final var userHeader : userHeaders) { + final String restAdminPermissionsPayload = createRestAdminPermissionsPayload("cluster/*"); + // attempt to create a new role + verifyPutForbidden("new_rest_admin_role", restAdminPermissionsPayload, userHeader); + verifyPatchForbidden(createPatchRestAdminPermissionsPayload("new_rest_admin_role", "add"), userHeader); + + // attempt to update existing rest admin role + verifyPutForbidden("rest_api_admin_full_access", restAdminPermissionsPayload, userHeader); + verifyPatchForbidden(createPatchRestAdminPermissionsPayload("rest_api_admin_full_access", "replace"), userHeader); + + // attempt to update non rest admin role with REST admin permissions + verifyPutForbidden("opendistro_security_role_starfleet_captains", restAdminPermissionsPayload, userHeader); + verifyPatchForbidden( + createPatchRestAdminPermissionsPayload("opendistro_security_role_starfleet_captains", "replace"), + userHeader + ); + + // attempt to remove REST admin role + verifyDeleteForbidden("rest_api_admin_full_access", userHeader); + verifyPatchForbidden(createPatchRestAdminPermissionsPayload("rest_api_admin_full_access", "remove"), userHeader); + } + } - response = rh.executePatchRequest(ENDPOINT + "/roles", createPatchRestAdminPermissionsPayload("add"), restApiHeader); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - response = rh.executePatchRequest(ENDPOINT + "/roles", createPatchRestAdminPermissionsPayload("remove"), restApiHeader); + void verifyPutForbidden(final String roleName, final String restAdminPermissionsPayload, final Header... header) { + HttpResponse response = rh.executePutRequest(ENDPOINT + "/roles/" + roleName, restAdminPermissionsPayload, header); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); } - @Test - public void testDeleteRestApiAdminRoleForbiddenForNonSuperAdmin() throws Exception { - setupWithRestRoles(Settings.builder().put(SECURITY_RESTAPI_ADMIN_ENABLED, true).build()); - rh.sendAdminCertificate = false; - - final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); - final Header adminHeader = encodeBasicHeader("admin", "admin"); - final Header restApiHeader = encodeBasicHeader("test", "test"); - - final String allRestAdminPermissionsPayload = createRestAdminPermissionsPayload("cluster/*"); - - HttpResponse response = rh.executePutRequest( - ENDPOINT + "/roles/new_rest_admin_role", - allRestAdminPermissionsPayload, - restApiAdminHeader - ); - Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); - - // attempt to update exiting admin role - response = rh.executeDeleteRequest(ENDPOINT + "/roles/new_rest_admin_role", adminHeader); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + void verifyPatchForbidden(final String restAdminPermissionsPayload, final Header... header) { + HttpResponse response = rh.executePatchRequest(ENDPOINT + "/roles", restAdminPermissionsPayload, header); + Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + } - // true to change - response = rh.executeDeleteRequest(ENDPOINT + "/roles/new_rest_admin_role", allRestAdminPermissionsPayload, restApiHeader); + void verifyDeleteForbidden(final String roleName, final Header... header) { + HttpResponse response = rh.executeDeleteRequest(ENDPOINT + "/roles/" + roleName, header); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); } - private String createPatchRestAdminPermissionsPayload(final String op) throws JsonProcessingException { + private String createPatchRestAdminPermissionsPayload(final String roleName, final String op) throws JsonProcessingException { final ArrayNode rootNode = DefaultObjectMapper.objectMapper.createArrayNode(); final ObjectNode opAddObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); final ObjectNode clusterPermissionsNode = DefaultObjectMapper.objectMapper.createObjectNode(); clusterPermissionsNode.set("cluster_permissions", clusterPermissionsForRestAdmin("cluster/*")); if ("add".equals(op)) { - opAddObjectNode.put("op", "add").put("path", "/some_rest_admin_role").set("value", clusterPermissionsNode); + opAddObjectNode.put("op", "add").put("path", "/" + roleName).set("value", clusterPermissionsNode); rootNode.add(opAddObjectNode); } if ("remove".equals(op)) { final ObjectNode opRemoveObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); - opRemoveObjectNode.put("op", "remove").put("path", "/rest_admin_role_to_delete"); + opRemoveObjectNode.put("op", "remove").put("path", "/" + roleName); rootNode.add(opRemoveObjectNode); } if ("replace".equals(op)) { final ObjectNode replaceRemoveObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); replaceRemoveObjectNode.put("op", "replace") - .put("path", "/new_rest_admin_role/cluster_permissions") + .put("path", "/" + roleName + "/cluster_permissions") .set("value", clusterPermissionsForRestAdmin("*")); rootNode.add(replaceRemoveObjectNode); diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiActionValidationTest.java index 5c041989a6..f7d1d4da0b 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiActionValidationTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiActionValidationTest.java @@ -35,18 +35,16 @@ public void setupRoles() throws Exception { @Test public void isAllowedRightsToChangeRoleEntity() throws Exception { - when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.ROLESMAPPING)).thenReturn(true); final var rolesMappingApiActionEndpointValidator = new RolesMappingApiAction(clusterService, threadPool, securityApiDependencies) .createEndpointValidator(); final var result = rolesMappingApiActionEndpointValidator.isAllowedToChangeImmutableEntity( - SecurityConfiguration.of("rest_api_admin_role", configuration) + SecurityConfiguration.of("rest_api_admin_role", configuration) ); assertTrue(result.isValid()); } @Test public void isNotAllowedNoRightsToChangeRoleEntity() throws Exception { - when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.ROLESMAPPING)).thenReturn(false); when(restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(any(Object.class))).thenCallRealMethod(); final var rolesApiActionEndpointValidator = @@ -61,7 +59,6 @@ public void isNotAllowedNoRightsToChangeRoleEntity() throws Exception { @Test public void onConfigChangeShouldCheckRoles() throws Exception { - when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.ROLESMAPPING)).thenReturn(false); when(restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(any(Object.class))).thenCallRealMethod(); when(configurationRepository.getConfigurationsFromIndex(List.of(CType.ROLES), false)) .thenReturn(Map.of(CType.ROLES, rolesConfiguration)); diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiTest.java index 077c852466..dc2ba33e6e 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiTest.java @@ -14,6 +14,7 @@ import java.util.List; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.hc.core5.http.Header; @@ -558,97 +559,83 @@ void verifyNonSuperAdminUser(final Header[] header) throws Exception { } @Test - public void testChangeRestApiAdminRoleMappingForbiddenForNonSuperAdmin() throws Exception { + public void testChangeRestApiAdminRoleMappingForbidden() throws Exception { setupWithRestRoles(Settings.builder().put(SECURITY_RESTAPI_ADMIN_ENABLED, true).build()); rh.sendAdminCertificate = false; - final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); - final Header adminHeader = encodeBasicHeader("admin", "admin"); - final Header restApiHeader = encodeBasicHeader("test", "test"); - - HttpResponse response = rh.executePutRequest( - ENDPOINT + "/roles/new_rest_api_role", - createRestAdminPermissionsPayload(), - restApiAdminHeader - ); - Assert.assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); - response = rh.executePutRequest( - ENDPOINT + "/roles/new_rest_api_role_without_mapping", - createRestAdminPermissionsPayload(), - restApiAdminHeader + final var userHeaders = List.of( + encodeBasicHeader("admin", "admin"), + encodeBasicHeader("test", "test"), + encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"), + encodeBasicHeader("rest_api_admin_rolesmapping", "rest_api_admin_rolesmapping") ); - Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); - response = rh.executePutRequest( - ENDPOINT + "/rolesmapping/new_rest_api_role", - createUsersPayload("a", "b", "c"), - restApiAdminHeader - ); - Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); - verifyRestApiPutAndDeleteForNonRestApiAdmin(adminHeader); - verifyRestApiPutAndDeleteForNonRestApiAdmin(restApiHeader); - verifyRestApiPatchForNonRestApiAdmin(adminHeader, false); - verifyRestApiPatchForNonRestApiAdmin(restApiHeader, false); - verifyRestApiPatchForNonRestApiAdmin(adminHeader, true); - verifyRestApiPatchForNonRestApiAdmin(restApiHeader, true); - } + for (final var userHeader : userHeaders) { + // create new mapping for existing group + verifyPutForbidden("rest_api_admin_roles_mapping_test_without_mapping", createUsers("c", "d"), userHeader); + verifyPatchForbidden(createPatchPayload("rest_api_admin_roles_mapping_test_without_mapping", "add"), userHeader); - private void verifyRestApiPutAndDeleteForNonRestApiAdmin(final Header header) throws Exception { - HttpResponse response = rh.executePutRequest( - ENDPOINT + "/rolesmapping/new_rest_api_role", - createUsersPayload("a", "b", "c"), - header - ); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + // update existing mapping with additional users + verifyPutForbidden("rest_api_admin_roles_mapping_test_with_mapping", createUsers("c", "d"), userHeader); + verifyPatchForbidden(createPatchPayload("rest_api_admin_roles_mapping_test_with_mapping", "replace"), userHeader); - response = rh.executeDeleteRequest(ENDPOINT + "/rolesmapping/new_rest_api_role", "", header); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + // delete existing role mapping forbidden + verifyDeleteForbidden("rest_api_admin_roles_mapping_test_with_mapping", userHeader); + verifyPatchForbidden(createPatchPayload("rest_api_admin_roles_mapping_test_with_mapping", "remove"), userHeader); + } } - private void verifyRestApiPatchForNonRestApiAdmin(final Header header, boolean bulk) throws Exception { - String path = ENDPOINT + "/rolesmapping"; - if (!bulk) { - path += "/new_rest_api_role"; - } - HttpResponse response = rh.executePatchRequest(path, createPathPayload("add"), header); - System.err.println(response.getBody()); + void verifyPutForbidden(final String roleMappingName, final String payload, final Header... header) { + HttpResponse response = rh.executePutRequest(ENDPOINT + "/rolesmapping/" + roleMappingName, payload, header); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + } - response = rh.executePatchRequest(path, createPathPayload("replace"), header); + void verifyPatchForbidden(final String payload, final Header... header) { + HttpResponse response = rh.executePatchRequest(ENDPOINT + "/rolesmapping", payload, header); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + } - response = rh.executePatchRequest(path, createPathPayload("remove"), header); + void verifyDeleteForbidden(final String roleMappingName, final Header... header) { + HttpResponse response = rh.executeDeleteRequest(ENDPOINT + "/rolesmapping/" + roleMappingName, header); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); } - private ObjectNode createUsersObjectNode(final String... users) { - final ArrayNode usersArray = DefaultObjectMapper.objectMapper.createArrayNode(); - for (final String user : users) { - usersArray.add(user); + private String createPatchPayload(final String roleName, final String op) throws JsonProcessingException { + final ArrayNode rootNode = DefaultObjectMapper.objectMapper.createArrayNode(); + final ObjectNode opAddObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); + final ObjectNode clusterPermissionsNode = DefaultObjectMapper.objectMapper.createObjectNode(); + clusterPermissionsNode.set("users", createUsersArray("c", "d")); + if ("add".equals(op)) { + opAddObjectNode.put("op", "add").put("path", "/" + roleName).set("value", clusterPermissionsNode); + rootNode.add(opAddObjectNode); } - return DefaultObjectMapper.objectMapper.createObjectNode().set("users", usersArray); - } - private String createUsersPayload(final String... users) throws JsonProcessingException { - return DefaultObjectMapper.objectMapper.writeValueAsString(createUsersObjectNode(users)); - } - - private String createPathPayload(final String op) throws JsonProcessingException { - final ArrayNode arrayNode = DefaultObjectMapper.objectMapper.createArrayNode(); - final ObjectNode opNode = DefaultObjectMapper.objectMapper.createObjectNode(); - opNode.put("op", op); - if ("add".equals(op)) { - opNode.put("path", "/new_rest_api_role_without_mapping"); - opNode.set("value", createUsersObjectNode("d", "e", "f")); + if ("remove".equals(op)) { + final ObjectNode opRemoveObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); + opRemoveObjectNode.put("op", "remove").put("path", "/" + roleName); + rootNode.add(opRemoveObjectNode); } + if ("replace".equals(op)) { - opNode.put("path", "/new_rest_api_role"); - opNode.set("value", createUsersObjectNode("g", "h", "i")); + final ObjectNode replaceRemoveObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); + replaceRemoveObjectNode.put("op", "replace").put("path", "/" + roleName + "/users").set("value", createUsersArray("c", "d")); + + rootNode.add(replaceRemoveObjectNode); } - if ("remove".equals(op)) { - opNode.put("path", "/new_rest_api_role"); + return DefaultObjectMapper.objectMapper.writeValueAsString(rootNode); + } + + private String createUsers(final String... users) throws JsonProcessingException { + final var o = DefaultObjectMapper.objectMapper.createObjectNode().set("users", createUsersArray("c", "d")); + return DefaultObjectMapper.writeValueAsString(o, false); + } + + private JsonNode createUsersArray(final String... users) { + final ArrayNode usersArray = DefaultObjectMapper.objectMapper.createArrayNode(); + for (final String user : users) { + usersArray.add(user); } - return DefaultObjectMapper.objectMapper.writeValueAsString(arrayNode.add(opNode)); + return usersArray; } @Test diff --git a/src/test/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPluginTest.java b/src/test/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPluginTest.java new file mode 100644 index 0000000000..aefb12c0db --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPluginTest.java @@ -0,0 +1,326 @@ +/* + * 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. + */ + +package org.opensearch.security.ssl; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; + +import org.junit.Before; +import org.junit.Test; + +import org.opensearch.OpenSearchException; +import org.opensearch.common.collect.Tuple; +import org.opensearch.common.network.NetworkModule; +import org.opensearch.common.settings.ClusterSettings; +import org.opensearch.common.settings.Settings; +import org.opensearch.http.HttpServerTransport; +import org.opensearch.http.netty4.ssl.SecureNetty4HttpServerTransport; +import org.opensearch.plugins.SecureHttpTransportSettingsProvider; +import org.opensearch.plugins.SecureTransportSettingsProvider; +import org.opensearch.plugins.TransportExceptionHandler; +import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.support.SecuritySettings; +import org.opensearch.security.test.AbstractSecurityUnitTest; +import org.opensearch.security.test.helper.file.FileHelper; +import org.opensearch.telemetry.tracing.noop.NoopTracer; +import org.opensearch.transport.Transport; +import org.opensearch.transport.TransportAdapterProvider; + +import io.netty.channel.ChannelInboundHandlerAdapter; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsMapContaining.hasKey; +import static org.junit.Assert.assertThrows; + +public class OpenSearchSecuritySSLPluginTest extends AbstractSecurityUnitTest { + private Settings settings; + private SecureHttpTransportSettingsProvider secureHttpTransportSettingsProvider; + private SecureTransportSettingsProvider secureTransportSettingsProvider; + private ClusterSettings clusterSettings; + + @Before + public void setUp() { + settings = Settings.builder() + .put( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, + FileHelper.getAbsoluteFilePathFromClassPath("ssl/kirk-keystore.jks") + ) + .put( + SSLConfigConstants.SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH, + FileHelper.getAbsoluteFilePathFromClassPath("ssl/root-ca.pem") + ) + .put( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, + FileHelper.getAbsoluteFilePathFromClassPath("ssl/truststore.jks") + ) + .put( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, + FileHelper.getAbsoluteFilePathFromClassPath("ssl/kirk-keystore.jks") + ) + .put( + SSLConfigConstants.SECURITY_SSL_HTTP_KEYSTORE_FILEPATH, + FileHelper.getAbsoluteFilePathFromClassPath("ssl/node-0-keystore.jks") + ) + .put(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED, true) + .put(OpenSearchSecuritySSLPlugin.CLIENT_TYPE, "node") + .build(); + + secureTransportSettingsProvider = new SecureTransportSettingsProvider() { + @Override + public Optional buildServerTransportExceptionHandler(Settings settings, Transport transport) { + return Optional.empty(); + } + + @Override + public Optional buildSecureServerTransportEngine(Settings settings, Transport transport) throws SSLException { + return Optional.empty(); + } + + @Override + public Optional buildSecureClientTransportEngine(Settings settings, String hostname, int port) throws SSLException { + return Optional.empty(); + } + }; + + secureHttpTransportSettingsProvider = new SecureHttpTransportSettingsProvider() { + @Override + public Optional buildHttpServerExceptionHandler(Settings settings, HttpServerTransport transport) { + return Optional.empty(); + } + + @Override + public Optional buildSecureHttpServerEngine(Settings settings, HttpServerTransport transport) throws SSLException { + return Optional.empty(); + } + }; + + clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); + } + + @Test + public void testRegisterSecureHttpTransport() throws IOException { + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(settings, null, false)) { + final Map> transports = plugin.getSecureHttpTransports( + settings, + MOCK_POOL, + null, + null, + null, + null, + null, + null, + clusterSettings, + secureHttpTransportSettingsProvider, + NoopTracer.INSTANCE + ); + assertThat(transports, hasKey("org.opensearch.security.ssl.http.netty.SecuritySSLNettyHttpServerTransport")); + assertThat( + transports.get("org.opensearch.security.ssl.http.netty.SecuritySSLNettyHttpServerTransport").get(), + not(nullValue()) + ); + } + } + + @Test + public void testRegisterSecureTransport() throws IOException { + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(settings, null, false)) { + final Map> transports = plugin.getSecureTransports( + settings, + MOCK_POOL, + null, + null, + null, + null, + secureTransportSettingsProvider, + NoopTracer.INSTANCE + ); + assertThat(transports, hasKey("org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport")); + assertThat(transports.get("org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport").get(), not(nullValue())); + } + } + + @Test + public void testRegisterSecureTransportWithDeprecatedSecuirtyPluginSettings() throws IOException { + final Settings deprecated = Settings.builder() + .put(settings) + .put(SecuritySettings.SSL_DUAL_MODE_SETTING.getKey(), true) + .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME, false) + .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION, false) + .build(); + + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(deprecated, null, false)) { + final Map> transports = plugin.getSecureTransports( + deprecated, + MOCK_POOL, + null, + null, + null, + null, + secureTransportSettingsProvider, + NoopTracer.INSTANCE + ); + assertThat(transports, hasKey("org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport")); + assertThat(transports.get("org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport").get(), not(nullValue())); + } + } + + @Test + public void testRegisterSecureTransportWithNetworkModuleSettings() throws IOException { + final Settings migrated = Settings.builder() + .put(settings) + .put(NetworkModule.TRANSPORT_SSL_DUAL_MODE_ENABLED_KEY, true) + .put(NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME_KEY, false) + .put(NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY, false) + .build(); + + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(migrated, null, false)) { + final Map> transports = plugin.getSecureTransports( + migrated, + MOCK_POOL, + null, + null, + null, + null, + secureTransportSettingsProvider, + NoopTracer.INSTANCE + ); + assertThat(transports, hasKey("org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport")); + assertThat(transports.get("org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport").get(), not(nullValue())); + } + } + + @Test + public void testRegisterSecureTransportWithDuplicateSettings() throws IOException { + final Collection> duplicates = List.of( + Tuple.tuple(SecuritySettings.SSL_DUAL_MODE_SETTING.getKey(), NetworkModule.TRANSPORT_SSL_DUAL_MODE_ENABLED_KEY), + Tuple.tuple( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME, + NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME_KEY + ), + Tuple.tuple( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION, + NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY + ) + ); + + for (final Tuple duplicate : duplicates) { + final Settings migrated = Settings.builder() + .put(settings) + .put(duplicate.v1(), true) + .put(NetworkModule.TRANSPORT_SSL_DUAL_MODE_ENABLED_KEY, true) + .put(NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME_KEY, false) + .put(NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY, false) + .build(); + + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(migrated, null, false)) { + final Map> transports = plugin.getSecureTransports( + migrated, + MOCK_POOL, + null, + null, + null, + null, + secureTransportSettingsProvider, + NoopTracer.INSTANCE + ); + assertThat(transports, hasKey("org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport")); + final OpenSearchException ex = assertThrows( + OpenSearchException.class, + transports.get("org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport")::get + ); + assertThat( + ex.getMessage(), + containsString( + "Only one of the settings [" + + duplicate.v2() + + ", " + + duplicate.v1() + + " (deprecated)] could be specified but not both" + ) + ); + } + } + } + + @Test + public void testRegisterSecureHttpTransportWithRequestHeaderVerifier() throws IOException { + final AtomicBoolean created = new AtomicBoolean(false); + + class LocalHeaderVerifier extends ChannelInboundHandlerAdapter { + public LocalHeaderVerifier() { + created.set(true); + } + } + + final SecureHttpTransportSettingsProvider provider = new SecureHttpTransportSettingsProvider() { + @Override + public Collection> getHttpTransportAdapterProviders(Settings settings) { + return List.of(new TransportAdapterProvider() { + + @Override + public String name() { + return SecureNetty4HttpServerTransport.REQUEST_HEADER_VERIFIER; + } + + @SuppressWarnings("unchecked") + @Override + public Optional create(Settings settings, HttpServerTransport transport, Class adapterClass) { + return Optional.of((C) new LocalHeaderVerifier()); + } + + }); + } + + @Override + public Optional buildHttpServerExceptionHandler(Settings settings, HttpServerTransport transport) { + return Optional.empty(); + } + + @Override + public Optional buildSecureHttpServerEngine(Settings settings, HttpServerTransport transport) throws SSLException { + return Optional.empty(); + } + }; + + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(settings, null, false)) { + final Map> transports = plugin.getSecureHttpTransports( + settings, + MOCK_POOL, + null, + null, + null, + null, + null, + null, + clusterSettings, + provider, + NoopTracer.INSTANCE + ); + assertThat(transports, hasKey("org.opensearch.security.ssl.http.netty.SecuritySSLNettyHttpServerTransport")); + + assertThat( + transports.get("org.opensearch.security.ssl.http.netty.SecuritySSLNettyHttpServerTransport").get(), + not(nullValue()) + ); + + assertThat(created.get(), is(true)); + } + } +} diff --git a/src/test/java/org/opensearch/security/ssl/transport/DualModeSSLHandlerTests.java b/src/test/java/org/opensearch/security/ssl/transport/DualModeSSLHandlerTests.java deleted file mode 100644 index e71e77d414..0000000000 --- a/src/test/java/org/opensearch/security/ssl/transport/DualModeSSLHandlerTests.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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.ssl.transport; - -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.ssl.util.SSLConnectionTestUtil; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPipeline; -import io.netty.handler.ssl.SslHandler; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; - -import static org.opensearch.transport.NettyAllocator.getAllocator; - -public class DualModeSSLHandlerTests { - - public static final int TLS_MAJOR_VERSION = 3; - public static final int TLS_MINOR_VERSION = 0; - private static final ByteBufAllocator ALLOCATOR = getAllocator(); - - private SecurityKeyStore securityKeyStore; - private ChannelPipeline pipeline; - private ChannelHandlerContext ctx; - private SslHandler sslHandler; - - @Before - public void setup() { - pipeline = Mockito.mock(ChannelPipeline.class); - ctx = Mockito.mock(ChannelHandlerContext.class); - Mockito.when(ctx.pipeline()).thenReturn(pipeline); - - securityKeyStore = Mockito.mock(SecurityKeyStore.class); - sslHandler = Mockito.mock(SslHandler.class); - } - - @Test - public void testInvalidMessage() throws Exception { - DualModeSSLHandler handler = new DualModeSSLHandler(securityKeyStore); - - handler.decode(ctx, ALLOCATOR.buffer(4), null); - // ensure pipeline is not fetched and manipulated - Mockito.verify(ctx, Mockito.times(0)).pipeline(); - } - - @Test - public void testValidTLSMessage() throws Exception { - DualModeSSLHandler handler = new DualModeSSLHandler(securityKeyStore, sslHandler); - - ByteBuf buffer = ALLOCATOR.buffer(6); - buffer.writeByte(20); - buffer.writeByte(TLS_MAJOR_VERSION); - buffer.writeByte(TLS_MINOR_VERSION); - buffer.writeByte(100); - buffer.writeByte(0); - buffer.writeByte(0); - - handler.decode(ctx, buffer, null); - // ensure ssl handler is added - Mockito.verify(ctx, Mockito.times(1)).pipeline(); - Mockito.verify(pipeline, Mockito.times(1)).addAfter("port_unification_handler", "ssl_server", sslHandler); - Mockito.verify(pipeline, Mockito.times(1)).remove(handler); - } - - @Test - public void testNonTLSMessage() throws Exception { - DualModeSSLHandler handler = new DualModeSSLHandler(securityKeyStore, sslHandler); - - ByteBuf buffer = ALLOCATOR.buffer(6); - - for (int i = 0; i < 6; i++) { - buffer.writeByte(1); - } - - handler.decode(ctx, buffer, null); - // ensure ssl handler is added - Mockito.verify(ctx, Mockito.times(1)).pipeline(); - Mockito.verify(pipeline, Mockito.times(0)).addAfter("port_unification_handler", "ssl_server", sslHandler); - Mockito.verify(pipeline, Mockito.times(1)).remove(handler); - } - - @Test - public void testDualModeClientHelloMessage() throws Exception { - ChannelFuture channelFuture = Mockito.mock(ChannelFuture.class); - Mockito.when(ctx.writeAndFlush(Mockito.any())).thenReturn(channelFuture); - Mockito.when(channelFuture.addListener(Mockito.any())).thenReturn(channelFuture); - - ByteBuf buffer = ALLOCATOR.buffer(6); - buffer.writeCharSequence(SSLConnectionTestUtil.DUAL_MODE_CLIENT_HELLO_MSG, StandardCharsets.UTF_8); - - DualModeSSLHandler handler = new DualModeSSLHandler(securityKeyStore, sslHandler); - List decodedObjs = new ArrayList<>(); - handler.decode(ctx, buffer, decodedObjs); - - ArgumentCaptor serverHelloReplyBuffer = ArgumentCaptor.forClass(ByteBuf.class); - Mockito.verify(ctx, Mockito.times(1)).writeAndFlush(serverHelloReplyBuffer.capture()); - - String actualReply = serverHelloReplyBuffer.getValue().getCharSequence(0, 6, StandardCharsets.UTF_8).toString(); - Assert.assertEquals(SSLConnectionTestUtil.DUAL_MODE_SERVER_HELLO_MSG, actualReply); - } -} diff --git a/src/test/java/org/opensearch/security/ssl/transport/SecuritySSLNettyTransportTests.java b/src/test/java/org/opensearch/security/ssl/transport/SecuritySSLNettyTransportTests.java deleted file mode 100644 index 32e0f48fac..0000000000 --- a/src/test/java/org/opensearch/security/ssl/transport/SecuritySSLNettyTransportTests.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * 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.ssl.transport; - -import java.util.Collections; - -import org.apache.logging.log4j.Logger; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; - -import org.opensearch.OpenSearchSecurityException; -import org.opensearch.Version; -import org.opensearch.cluster.node.DiscoveryNode; -import org.opensearch.common.network.NetworkService; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.PageCacheRecycler; -import org.opensearch.core.common.io.stream.NamedWriteableRegistry; -import org.opensearch.core.indices.breaker.CircuitBreakerService; -import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.ssl.SslExceptionHandler; -import org.opensearch.security.ssl.transport.SecuritySSLNettyTransport.SSLClientChannelInitializer; -import org.opensearch.security.ssl.transport.SecuritySSLNettyTransport.SSLServerChannelInitializer; -import org.opensearch.telemetry.tracing.Tracer; -import org.opensearch.threadpool.ThreadPool; -import org.opensearch.transport.FakeTcpChannel; -import org.opensearch.transport.SharedGroupFactory; -import org.opensearch.transport.TcpChannel; - -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.DecoderException; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.junit.Assert.assertThrows; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class SecuritySSLNettyTransportTests { - - @Mock - private Version version; - @Mock - private ThreadPool threadPool; - @Mock - private PageCacheRecycler pageCacheRecycler; - @Mock - private NamedWriteableRegistry namedWriteableRegistry; - @Mock - private CircuitBreakerService circuitBreakerService; - @Mock - private Tracer trace; - @Mock - private SecurityKeyStore ossks; - @Mock - private SslExceptionHandler sslExceptionHandler; - @Mock - private DiscoveryNode discoveryNode; - - // This initializes all the above mocks - @Rule - public MockitoRule rule = MockitoJUnit.rule(); - - private NetworkService networkService; - private SharedGroupFactory sharedGroupFactory; - private Logger mockLogger; - private SSLConfig sslConfig; - private SecuritySSLNettyTransport securitySSLNettyTransport; - Throwable testCause = new Throwable("Test Cause"); - - @Before - public void setup() { - - networkService = new NetworkService(Collections.emptyList()); - sharedGroupFactory = new SharedGroupFactory(Settings.EMPTY); - - sslConfig = new SSLConfig(Settings.EMPTY); - mockLogger = mock(Logger.class); - - securitySSLNettyTransport = spy( - new SecuritySSLNettyTransport( - Settings.EMPTY, - version, - threadPool, - networkService, - pageCacheRecycler, - namedWriteableRegistry, - circuitBreakerService, - ossks, - sslExceptionHandler, - sharedGroupFactory, - sslConfig, - trace - ) - ); - } - - @Test - public void OnException_withNullChannelShouldThrowException() { - - OpenSearchSecurityException exception = new OpenSearchSecurityException("The provided TCP channel is invalid"); - assertThrows(OpenSearchSecurityException.class, () -> securitySSLNettyTransport.onException(null, exception)); - } - - @Test - public void OnException_withClosedChannelShouldThrowException() { - - TcpChannel channel = new FakeTcpChannel(); - channel.close(); - OpenSearchSecurityException exception = new OpenSearchSecurityException("The provided TCP channel is invalid"); - assertThrows(OpenSearchSecurityException.class, () -> securitySSLNettyTransport.onException(channel, exception)); - } - - @Test - public void OnException_withNullExceptionShouldSucceed() { - - TcpChannel channel = new FakeTcpChannel(); - securitySSLNettyTransport.onException(channel, null); - verify(securitySSLNettyTransport, times(1)).onException(channel, null); - channel.close(); - } - - @Test - public void OnException_withDecoderExceptionShouldGetCause() { - - when(securitySSLNettyTransport.getLogger()).thenReturn(mockLogger); - DecoderException exception = new DecoderException("Test Exception", testCause); - TcpChannel channel = new FakeTcpChannel(); - securitySSLNettyTransport.onException(channel, exception); - verify(mockLogger, times(1)).error("Exception during establishing a SSL connection: " + exception.getCause(), exception.getCause()); - } - - @Test - public void getServerChannelInitializer_shouldReturnValidServerChannel() { - - ChannelHandler channelHandler = securitySSLNettyTransport.getServerChannelInitializer("test-server-channel"); - assertThat(channelHandler, is(notNullValue())); - assertThat(channelHandler, is(instanceOf(SSLServerChannelInitializer.class))); - } - - @Test - public void getClientChannelInitializer_shouldReturnValidClientChannel() { - ChannelHandler channelHandler = securitySSLNettyTransport.getClientChannelInitializer(discoveryNode); - assertThat(channelHandler, is(notNullValue())); - assertThat(channelHandler, is(instanceOf(SSLClientChannelInitializer.class))); - } - - @Test - public void exceptionWithServerChannelHandlerContext_nonNullDecoderExceptionShouldGetCause() throws Exception { - when(securitySSLNettyTransport.getLogger()).thenReturn(mockLogger); - Throwable exception = new DecoderException("Test Exception", testCause); - ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); - securitySSLNettyTransport.getServerChannelInitializer(discoveryNode.getName()).exceptionCaught(ctx, exception); - verify(mockLogger, times(1)).error("Exception during establishing a SSL connection: " + exception.getCause(), exception.getCause()); - } - - @Test - public void exceptionWithServerChannelHandlerContext_nonNullCauseOnlyShouldNotGetCause() throws Exception { - when(securitySSLNettyTransport.getLogger()).thenReturn(mockLogger); - Throwable exception = new OpenSearchSecurityException("Test Exception", testCause); - ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); - securitySSLNettyTransport.getServerChannelInitializer(discoveryNode.getName()).exceptionCaught(ctx, exception); - verify(mockLogger, times(1)).error("Exception during establishing a SSL connection: " + exception, exception); - } - - @Test - public void exceptionWithClientChannelHandlerContext_nonNullDecoderExceptionShouldGetCause() throws Exception { - when(securitySSLNettyTransport.getLogger()).thenReturn(mockLogger); - Throwable exception = new DecoderException("Test Exception", testCause); - ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); - securitySSLNettyTransport.getClientChannelInitializer(discoveryNode).exceptionCaught(ctx, exception); - verify(mockLogger, times(1)).error("Exception during establishing a SSL connection: " + exception.getCause(), exception.getCause()); - } - - @Test - public void exceptionWithClientChannelHandlerContext_nonNullCauseOnlyShouldNotGetCause() throws Exception { - when(securitySSLNettyTransport.getLogger()).thenReturn(mockLogger); - Throwable exception = new OpenSearchSecurityException("Test Exception", testCause); - ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); - securitySSLNettyTransport.getClientChannelInitializer(discoveryNode).exceptionCaught(ctx, exception); - verify(mockLogger, times(1)).error("Exception during establishing a SSL connection: " + exception, exception); - } -} diff --git a/src/test/java/org/opensearch/security/state/SecurityMetadataSerializationTestCase.java b/src/test/java/org/opensearch/security/state/SecurityMetadataSerializationTestCase.java new file mode 100644 index 0000000000..c52f37cf54 --- /dev/null +++ b/src/test/java/org/opensearch/security/state/SecurityMetadataSerializationTestCase.java @@ -0,0 +1,154 @@ +/* + * 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.state; + +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import com.carrotsearch.randomizedtesting.RandomizedContext; +import com.carrotsearch.randomizedtesting.RandomizedRunner; +import com.carrotsearch.randomizedtesting.RandomizedTest; +import com.google.common.collect.ImmutableSortedSet; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.Version; +import org.opensearch.cluster.ClusterState; +import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.core.common.io.stream.NamedWriteableAwareStreamInput; +import org.opensearch.core.common.io.stream.NamedWriteableRegistry; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.Writeable; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.test.DiffableTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; + +@RunWith(RandomizedRunner.class) +public class SecurityMetadataSerializationTestCase extends RandomizedTest { + + protected ClusterState.Custom createTestInstance() { + final var configuration = new ImmutableSortedSet.Builder<>(Comparator.comparing(SecurityConfig::type)); + for (final var c : CType.values()) { + configuration.add(new SecurityConfig(c, randomAsciiAlphanumOfLength(128), null)); + } + return new SecurityMetadata(randomInstant(), configuration.build()); + } + + protected ClusterState.Custom makeTestChanges(ClusterState.Custom custom) { + final var securityMetadata = (SecurityMetadata) custom; + + if (randomBoolean()) { + final var configuration = securityMetadata.configuration(); + int leaveElements = randomIntBetween(0, configuration.size() - 1); + final var randomConfigs = randomSubsetOf(leaveElements, configuration); + final var securityMetadataBuilder = SecurityMetadata.from(securityMetadata); + for (final var config : randomConfigs) { + securityMetadataBuilder.withSecurityConfig( + SecurityConfig.from(config).withLastModified(randomInstant()).withHash(randomAsciiAlphanumOfLength(128)).build() + ); + } + return securityMetadataBuilder.build(); + } + + return securityMetadata; + } + + public static List randomSubsetOf(int size, Collection collection) { + if (size > collection.size()) { + throw new IllegalArgumentException( + "Can't pick " + size + " random objects from a collection of " + collection.size() + " objects" + ); + } + List tempList = new ArrayList<>(collection); + Collections.shuffle(tempList, RandomizedContext.current().getRandom()); + return tempList.subList(0, size); + } + + protected Instant randomInstant() { + return Instant.ofEpochSecond(randomLongBetween(0L, 3000000000L), randomLongBetween(0L, 999999999L)); + } + + @Test + public void testSerialization() throws IOException { + for (int runs = 0; runs < 20; runs++) { + ClusterState.Custom testInstance = createTestInstance(); + assertSerialization(testInstance); + } + } + + void assertSerialization(ClusterState.Custom testInstance) throws IOException { + assertSerialization(testInstance, Version.CURRENT); + } + + void assertSerialization(ClusterState.Custom testInstance, Version version) throws IOException { + ClusterState.Custom deserializedInstance = copyInstance(testInstance, version); + assertEqualInstances(testInstance, deserializedInstance); + } + + void assertEqualInstances(ClusterState.Custom expectedInstance, ClusterState.Custom newInstance) { + assertNotSame(newInstance, expectedInstance); + assertEquals(expectedInstance, newInstance); + assertEquals(expectedInstance.hashCode(), newInstance.hashCode()); + } + + @Test + public void testDiffableSerialization() throws IOException { + DiffableTestUtils.testDiffableSerialization( + this::createTestInstance, + this::makeTestChanges, + getNamedWriteableRegistry(), + SecurityMetadata::new, + SecurityMetadata::readDiffFrom + ); + } + + protected NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(Collections.emptyList()); + } + + protected final ClusterState.Custom copyInstance(ClusterState.Custom instance, Version version) throws IOException { + return copyWriteable(instance, getNamedWriteableRegistry(), SecurityMetadata::new, version); + } + + public static T copyWriteable( + T original, + NamedWriteableRegistry namedWriteableRegistry, + Writeable.Reader reader, + Version version + ) throws IOException { + return copyInstance(original, namedWriteableRegistry, (out, value) -> value.writeTo(out), reader, version); + } + + protected static T copyInstance( + T original, + NamedWriteableRegistry namedWriteableRegistry, + Writeable.Writer writer, + Writeable.Reader reader, + Version version + ) throws IOException { + try (BytesStreamOutput output = new BytesStreamOutput()) { + output.setVersion(version); + writer.write(output, original); + try (StreamInput in = new NamedWriteableAwareStreamInput(output.bytes().streamInput(), namedWriteableRegistry)) { + in.setVersion(version); + return reader.read(in); + } + } + } + +} diff --git a/src/test/java/org/opensearch/security/support/ConfigReaderTest.java b/src/test/java/org/opensearch/security/support/ConfigReaderTest.java new file mode 100644 index 0000000000..189b92ff68 --- /dev/null +++ b/src/test/java/org/opensearch/security/support/ConfigReaderTest.java @@ -0,0 +1,63 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ +package org.opensearch.security.support; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; + +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.securityconf.impl.CType; + +import static org.opensearch.security.configuration.ConfigurationRepository.DEFAULT_CONFIG_VERSION; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class ConfigReaderTest { + + @ClassRule + public static TemporaryFolder folder = new TemporaryFolder(); + + private static File configDir; + + @BeforeClass + public static void createConfigFile() throws IOException { + configDir = folder.newFolder("config"); + } + + @Test + public void testThrowsIOExceptionForMandatoryCTypes() { + for (final var cType : CType.REQUIRED_CONFIG_FILES) { + assertThrows(IOException.class, () -> YamlConfigReader.newReader(cType, configDir.toPath())); + } + } + + @Test + public void testCreateReaderForNonMandatoryCTypes() throws IOException { + final var yamlMapper = DefaultObjectMapper.YAML_MAPPER; + for (final var cType : CType.NOT_REQUIRED_CONFIG_FILES) { + try (final var reader = new BufferedReader(YamlConfigReader.newReader(cType, configDir.toPath()))) { + final var emptyYaml = yamlMapper.readTree(reader); + assertTrue(emptyYaml.has("_meta")); + + final var meta = emptyYaml.get("_meta"); + assertEquals(cType.toLCString(), meta.get("type").asText()); + assertEquals(DEFAULT_CONFIG_VERSION, meta.get("config_version").asInt()); + } + } + } + +} diff --git a/src/test/java/org/opensearch/security/support/SecurityIndexHandlerTest.java b/src/test/java/org/opensearch/security/support/SecurityIndexHandlerTest.java new file mode 100644 index 0000000000..170f0a9853 --- /dev/null +++ b/src/test/java/org/opensearch/security/support/SecurityIndexHandlerTest.java @@ -0,0 +1,510 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ + +package org.opensearch.security.support; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +import org.opensearch.action.DocWriteRequest; +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.admin.indices.create.CreateIndexResponse; +import org.opensearch.action.bulk.BulkItemResponse; +import org.opensearch.action.bulk.BulkRequest; +import org.opensearch.action.bulk.BulkResponse; +import org.opensearch.action.get.GetResponse; +import org.opensearch.action.get.MultiGetItemResponse; +import org.opensearch.action.get.MultiGetRequest; +import org.opensearch.action.get.MultiGetResponse; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.support.ActiveShardCount; +import org.opensearch.client.AdminClient; +import org.opensearch.client.Client; +import org.opensearch.client.IndicesAdminClient; +import org.opensearch.common.CheckedSupplier; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.common.bytes.BytesArray; +import org.opensearch.index.get.GetResult; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.state.SecurityConfig; +import org.opensearch.threadpool.ThreadPool; + +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.opensearch.security.configuration.ConfigurationRepository.DEFAULT_CONFIG_VERSION; +import static org.opensearch.security.support.YamlConfigReader.emptyYamlConfigFor; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class SecurityIndexHandlerTest { + + final static String INDEX_NAME = "some_index"; + + final static String CONFIG_YAML = "_meta: \n" + + " type: \"config\"\n" + + " config_version: 2\n" + + "config:\n" + + " dynamic:\n" + + " http:\n" + + " anonymous_auth_enabled: false\n"; + + final static String USERS_YAML = "_meta:\n" + + " type: \"internalusers\"\n" + + " config_version: 2\n" + + "admin:\n" + + " hash: \"$2y$12$erlkZeSv7eRMa1vs3UgDl.xoqu1P9GY94Toj1BwdvJiq7eKTOjQjS\"\n" + + " reserved: true\n" + + " backend_roles:\n" + + " - \"admin\"\n" + + " description: \"Some admin user\"\n"; + + final static String ROLES_YAML = "_meta:\n" + " type: \"roles\"\n" + " config_version: 2\n" + "some_role:\n" + " reserved: true\n"; + + final static String ROLES_MAPPING_YAML = "_meta:\n" + + " type: \"rolesmapping\"\n" + + " config_version: 2\n" + + "all_access: \n" + + " reserved: false\n"; + + static final Map> YAML = Map.of( + CType.ACTIONGROUPS, + () -> emptyYamlConfigFor(CType.ACTIONGROUPS), + CType.ALLOWLIST, + () -> emptyYamlConfigFor(CType.ALLOWLIST), + CType.AUDIT, + () -> emptyYamlConfigFor(CType.AUDIT), + CType.CONFIG, + () -> CONFIG_YAML, + CType.INTERNALUSERS, + () -> USERS_YAML, + CType.NODESDN, + () -> emptyYamlConfigFor(CType.NODESDN), + CType.ROLES, + () -> ROLES_YAML, + CType.ROLESMAPPING, + () -> ROLES_MAPPING_YAML, + CType.TENANTS, + () -> emptyYamlConfigFor(CType.TENANTS), + CType.WHITELIST, + () -> emptyYamlConfigFor(CType.WHITELIST) + ); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Mock + private Client client; + + @Mock + private ThreadPool threadPool; + + @Mock + private IndicesAdminClient indicesAdminClient; + + private Path configFolder; + + private ThreadContext threadContext; + + private SecurityIndexHandler securityIndexHandler; + + @Before + public void setupClient() throws IOException { + when(client.admin()).thenReturn(mock(AdminClient.class)); + when(client.admin().indices()).thenReturn(indicesAdminClient); + when(client.threadPool()).thenReturn(threadPool); + threadContext = new ThreadContext(Settings.EMPTY); + when(client.threadPool()).thenReturn(threadPool); + when(threadPool.getThreadContext()).thenReturn(threadContext); + configFolder = temporaryFolder.newFolder("config").toPath(); + securityIndexHandler = new SecurityIndexHandler(INDEX_NAME, Settings.EMPTY, client); + } + + @Test + public void testCreateIndex_shouldCreateIndex() { + doAnswer(invocation -> { + ActionListener actionListener = invocation.getArgument(1); + actionListener.onResponse(new CreateIndexResponse(true, true, "some_index")); + return null; + }).when(indicesAdminClient).create(any(), any()); + + securityIndexHandler.createIndex(ActionListener.wrap(Assert::assertTrue, Assert::assertNull)); + + final var requestCaptor = ArgumentCaptor.forClass(CreateIndexRequest.class); + + verify(indicesAdminClient).create(requestCaptor.capture(), any()); + + final var createRequest = requestCaptor.getValue(); + assertEquals(INDEX_NAME, createRequest.index()); + for (final var setting : SecurityIndexHandler.INDEX_SETTINGS.entrySet()) + assertEquals(setting.getValue().toString(), createRequest.settings().get(setting.getKey())); + + assertEquals(ActiveShardCount.ONE, createRequest.waitForActiveShards()); + } + + @Test + public void testCreateIndex_shouldReturnSecurityExceptionIfItCanNotCreateIndex() { + + final var listener = spy(ActionListener.wrap(r -> fail("Unexpected behave"), e -> { + assertEquals(SecurityException.class, e.getClass()); + assertEquals("Couldn't create security index " + INDEX_NAME, e.getMessage()); + })); + + doAnswer(invocation -> { + ActionListener actionListener = invocation.getArgument(1); + actionListener.onResponse(new CreateIndexResponse(false, false, "some_index")); + return null; + }).when(indicesAdminClient).create(any(), any()); + + securityIndexHandler.createIndex(listener); + + verify(indicesAdminClient).create(isA(CreateIndexRequest.class), any()); + verify(listener).onFailure(any()); + } + + @Test + public void testUploadDefaultConfiguration_shouldFailIfRequiredConfigFilesAreMissing() { + final var listener = spy(ActionListener.>wrap(r -> fail("Unexpected behave"), e -> { + assertEquals(SecurityException.class, e.getClass()); + assertThat(e.getMessage(), containsString("Couldn't find configuration file")); + })); + securityIndexHandler.uploadDefaultConfiguration(configFolder, listener); + + verify(listener).onFailure(any()); + } + + @Test + public void testUploadDefaultConfiguration_shouldFailIfBulkHasFailures() throws IOException { + final var failedBulkResponse = new BulkResponse( + new BulkItemResponse[] { + new BulkItemResponse(1, DocWriteRequest.OpType.CREATE, new BulkItemResponse.Failure("a", "b", new Exception())) }, + 100L + ); + final var listener = spy(ActionListener.>wrap(r -> fail("Unexpected behave"), e -> { + assertEquals(SecurityException.class, e.getClass()); + assertEquals(e.getMessage(), failedBulkResponse.buildFailureMessage()); + })); + doAnswer(invocation -> { + ActionListener actionListener = invocation.getArgument(1); + actionListener.onResponse(failedBulkResponse); + return null; + }).when(client).bulk(any(BulkRequest.class), any()); + for (final var c : CType.REQUIRED_CONFIG_FILES) { + try (final var io = Files.newBufferedWriter(c.configFile(configFolder))) { + io.write(YAML.get(c).get()); + io.flush(); + } + } + securityIndexHandler.uploadDefaultConfiguration(configFolder, listener); + verify(listener).onFailure(any()); + } + + @Test + public void testUploadDefaultConfiguration_shouldCreateSetOfSecurityConfigs() throws IOException { + + final var listener = spy(ActionListener.>wrap(configuration -> { + for (final var sc : configuration) { + assertTrue(sc.lastModified().isEmpty()); + assertNotNull(sc.hash()); + } + }, e -> fail("Unexpected behave"))); + + for (final var c : CType.REQUIRED_CONFIG_FILES) { + try (final var io = Files.newBufferedWriter(c.configFile(configFolder))) { + final var source = YAML.get(c).get(); + io.write(source); + io.flush(); + } + } + + final var bulkRequestCaptor = ArgumentCaptor.forClass(BulkRequest.class); + + doAnswer(invocation -> { + ActionListener actionListener = invocation.getArgument(1); + final var r = mock(BulkResponse.class); + when(r.hasFailures()).thenReturn(false); + actionListener.onResponse(r); + return null; + }).when(client).bulk(bulkRequestCaptor.capture(), any()); + securityIndexHandler.uploadDefaultConfiguration(configFolder, listener); + + final var bulkRequest = bulkRequestCaptor.getValue(); + for (final var r : bulkRequest.requests()) { + final var indexRequest = (IndexRequest) r; + assertEquals(INDEX_NAME, r.index()); + assertEquals(DocWriteRequest.OpType.INDEX, indexRequest.opType()); + } + verify(listener).onResponse(any()); + } + + @Test + public void testUploadDefaultConfiguration_shouldSkipAudit() throws IOException { + final var listener = spy( + ActionListener.>wrap( + configuration -> assertFalse(configuration.stream().anyMatch(sc -> sc.type() == CType.AUDIT)), + e -> fail("Unexpected behave") + ) + ); + + for (final var c : CType.REQUIRED_CONFIG_FILES) { + if (c == CType.AUDIT) continue; + try (final var io = Files.newBufferedWriter(c.configFile(configFolder))) { + final var source = YAML.get(c).get(); + io.write(source); + io.flush(); + } + } + doAnswer(invocation -> { + ActionListener actionListener = invocation.getArgument(1); + final var r = mock(BulkResponse.class); + when(r.hasFailures()).thenReturn(false); + actionListener.onResponse(r); + return null; + }).when(client).bulk(any(BulkRequest.class), any()); + + securityIndexHandler.uploadDefaultConfiguration(configFolder, listener); + verify(listener).onResponse(any()); + } + + @Test + public void testUploadDefaultConfiguration_shouldSkipWhitelist() throws IOException { + final var listener = spy( + ActionListener.>wrap( + configuration -> assertFalse(configuration.stream().anyMatch(sc -> sc.type() == CType.WHITELIST)), + e -> fail("Unexpected behave") + ) + ); + + for (final var c : CType.REQUIRED_CONFIG_FILES) { + if (c == CType.WHITELIST) continue; + try (final var io = Files.newBufferedWriter(c.configFile(configFolder))) { + final var source = YAML.get(c).get(); + io.write(source); + io.flush(); + } + } + doAnswer(invocation -> { + ActionListener actionListener = invocation.getArgument(1); + final var r = mock(BulkResponse.class); + when(r.hasFailures()).thenReturn(false); + actionListener.onResponse(r); + return null; + }).when(client).bulk(any(BulkRequest.class), any()); + + securityIndexHandler.uploadDefaultConfiguration(configFolder, listener); + verify(listener).onResponse(any()); + } + + @Test + public void testLoadConfiguration_shouldFailIfResponseHasFailures() { + final var listener = spy( + ActionListener.>>wrap( + r -> fail("Unexpected behave"), + e -> assertEquals(SecurityException.class, e.getClass()) + ) + ); + + doAnswer(invocation -> { + ActionListener actionListener = invocation.getArgument(1); + final var r = mock(MultiGetResponse.class); + final var mr = mock(MultiGetItemResponse.class); + when(mr.isFailed()).thenReturn(true); + when(mr.getFailure()).thenReturn(new MultiGetResponse.Failure("a", "id", new Exception())); + when(r.getResponses()).thenReturn(new MultiGetItemResponse[] { mr }); + actionListener.onResponse(r); + return null; + }).when(client).multiGet(any(MultiGetRequest.class), any()); + + securityIndexHandler.loadConfiguration(configuration(), listener); + verify(listener).onFailure(any()); + } + + @Test + public void testLoadConfiguration_shouldFailIfNoRequiredConfigInResponse() { + final var listener = spy( + ActionListener.>>wrap( + r -> fail("Unexpected behave"), + e -> assertEquals("Missing required configuration for type: CONFIG", e.getMessage()) + ) + ); + doAnswer(invocation -> { + ActionListener actionListener = invocation.getArgument(1); + final var getResult = mock(GetResult.class); + final var r = new MultiGetResponse(new MultiGetItemResponse[] { new MultiGetItemResponse(new GetResponse(getResult), null) }); + when(getResult.getId()).thenReturn(CType.CONFIG.toLCString()); + when(getResult.isExists()).thenReturn(false); + actionListener.onResponse(r); + return null; + }).when(client).multiGet(any(MultiGetRequest.class), any()); + + securityIndexHandler.loadConfiguration(configuration(), listener); + + verify(listener).onFailure(any()); + } + + @Test + public void testLoadConfiguration_shouldFailForUnsupportedVersion() { + final var listener = spy( + ActionListener.>>wrap( + r -> fail("Unexpected behave"), + e -> assertEquals("Version 1 is not supported for CONFIG", e.getMessage()) + ) + ); + doAnswer(invocation -> { + + final var objectMapper = DefaultObjectMapper.objectMapper; + + ActionListener actionListener = invocation.getArgument(1); + final var getResult = mock(GetResult.class); + final var r = new MultiGetResponse(new MultiGetItemResponse[] { new MultiGetItemResponse(new GetResponse(getResult), null) }); + when(getResult.getId()).thenReturn(CType.CONFIG.toLCString()); + when(getResult.isExists()).thenReturn(true); + + final var oldVersionJson = objectMapper.createObjectNode() + .set("opendistro_security", objectMapper.createObjectNode().set("dynamic", objectMapper.createObjectNode())) + .toString() + .getBytes(StandardCharsets.UTF_8); + final var configResponse = objectMapper.createObjectNode().put(CType.CONFIG.toLCString(), oldVersionJson); + final var source = objectMapper.writeValueAsBytes(configResponse); + when(getResult.sourceRef()).thenReturn(new BytesArray(source, 0, source.length)); + actionListener.onResponse(r); + return null; + }).when(client).multiGet(any(MultiGetRequest.class), any()); + securityIndexHandler.loadConfiguration(configuration(), listener); + + verify(listener).onFailure(any()); + } + + @Test + public void testLoadConfiguration_shouldFailForUnparseableConfig() { + final var listener = spy( + ActionListener.>>wrap( + r -> fail("Unexpected behave"), + e -> assertEquals("Couldn't parse content for CONFIG", e.getMessage()) + ) + ); + doAnswer(invocation -> { + + final var objectMapper = DefaultObjectMapper.objectMapper; + + ActionListener actionListener = invocation.getArgument(1); + final var getResult = mock(GetResult.class); + final var r = new MultiGetResponse(new MultiGetItemResponse[] { new MultiGetItemResponse(new GetResponse(getResult), null) }); + when(getResult.getId()).thenReturn(CType.CONFIG.toLCString()); + when(getResult.isExists()).thenReturn(true); + + final var configResponse = objectMapper.createObjectNode() + .put( + CType.CONFIG.toLCString(), + objectMapper.createObjectNode() + .set("_meta", objectMapper.createObjectNode().put("type", CType.CONFIG.toLCString())) + .toString() + .getBytes(StandardCharsets.UTF_8) + ); + final var source = objectMapper.writeValueAsBytes(configResponse); + when(getResult.sourceRef()).thenReturn(new BytesArray(source, 0, source.length)); + actionListener.onResponse(r); + return null; + }).when(client).multiGet(any(MultiGetRequest.class), any()); + securityIndexHandler.loadConfiguration(configuration(), listener); + + verify(listener).onFailure(any()); + } + + @Test + public void testLoadConfiguration_shouldBuildSecurityConfig() { + final var listener = spy(ActionListener.>>wrap(config -> { + assertEquals(CType.values().length, config.keySet().size()); + for (final var c : CType.values()) { + assertTrue(c.toLCString(), config.containsKey(c)); + } + }, e -> fail("Unexpected behave"))); + doAnswer(invocation -> { + final var objectMapper = DefaultObjectMapper.objectMapper; + ActionListener actionListener = invocation.getArgument(1); + + final var responses = new MultiGetItemResponse[CType.values().length]; + var counter = 0; + for (final var c : CType.values()) { + final var getResult = mock(GetResult.class); + if (!c.emptyIfMissing()) { + when(getResult.getId()).thenReturn(c.toLCString()); + when(getResult.isExists()).thenReturn(true); + + final var minimumRequiredConfig = minimumRequiredConfig(c); + if (c == CType.CONFIG) minimumRequiredConfig.set( + "config", + objectMapper.createObjectNode().set("dynamic", objectMapper.createObjectNode()) + ); + + final var source = objectMapper.writeValueAsBytes( + objectMapper.createObjectNode() + .put(c.toLCString(), minimumRequiredConfig.toString().getBytes(StandardCharsets.UTF_8)) + ); + + when(getResult.sourceRef()).thenReturn(new BytesArray(source, 0, source.length)); + + responses[counter] = new MultiGetItemResponse(new GetResponse(getResult), null); + } else { + when(getResult.getId()).thenReturn(c.toLCString()); + when(getResult.isExists()).thenReturn(false); + responses[counter] = new MultiGetItemResponse(new GetResponse(getResult), null); + } + counter++; + } + actionListener.onResponse(new MultiGetResponse(responses)); + return null; + }).when(client).multiGet(any(MultiGetRequest.class), any()); + securityIndexHandler.loadConfiguration(configuration(), listener); + + verify(listener).onResponse(any()); + } + + private ObjectNode minimumRequiredConfig(final CType cType) { + final var objectMapper = DefaultObjectMapper.objectMapper; + return objectMapper.createObjectNode() + .set("_meta", objectMapper.createObjectNode().put("type", cType.toLCString()).put("config_version", DEFAULT_CONFIG_VERSION)); + } + + private Set configuration() { + return Set.of(new SecurityConfig(CType.CONFIG, "aaa", null), new SecurityConfig(CType.AUDIT, "bbb", null)); + } + +} diff --git a/src/test/resources/restapi/action_groups.yml b/src/test/resources/restapi/action_groups.yml index 638f65f72f..4ec858a69e 100644 --- a/src/test/resources/restapi/action_groups.yml +++ b/src/test/resources/restapi/action_groups.yml @@ -132,3 +132,16 @@ OPENDISTRO_SECURITY_SUGGEST: - "indices:data/read/suggest*" type: "index" description: "Migrated from v6" +rest_admin_action_group: + allowed_actions: + - 'restapi:admin/actiongroups' + - 'restapi:admin/allowlist' + - 'restapi:admin/config/update' + - 'restapi:admin/internalusers' + - 'restapi:admin/nodesdn' + - 'restapi:admin/roles' + - 'restapi:admin/rolesmapping' + - 'restapi:admin/ssl/certs/info' + - 'restapi:admin/ssl/certs/reload' + - 'restapi:admin/tenants' + type: "cluster" diff --git a/src/test/resources/restapi/roles.yml b/src/test/resources/restapi/roles.yml index 1c3756cb4d..925b051bb3 100644 --- a/src/test/resources/restapi/roles.yml +++ b/src/test/resources/restapi/roles.yml @@ -393,6 +393,32 @@ opendistro_security_role_starfleet_captains: allowed_actions: - "CRUD_UT" tenant_permissions: [] +rest_api_admin_roles_mapping_test_without_mapping: + reserved: true + cluster_permissions: + - 'restapi:admin/actiongroups' + - 'restapi:admin/allowlist' + - 'restapi:admin/config/update' + - 'restapi:admin/internalusers' + - 'restapi:admin/nodesdn' + - 'restapi:admin/roles' + - 'restapi:admin/rolesmapping' + - 'restapi:admin/ssl/certs/info' + - 'restapi:admin/ssl/certs/reload' + - 'restapi:admin/tenants' +rest_api_admin_roles_mapping_test_with_mapping: + reserved: true + cluster_permissions: + - 'restapi:admin/actiongroups' + - 'restapi:admin/allowlist' + - 'restapi:admin/config/update' + - 'restapi:admin/internalusers' + - 'restapi:admin/nodesdn' + - 'restapi:admin/roles' + - 'restapi:admin/rolesmapping' + - 'restapi:admin/ssl/certs/info' + - 'restapi:admin/ssl/certs/reload' + - 'restapi:admin/tenants' rest_api_admin_full_access: reserved: true cluster_permissions: diff --git a/src/test/resources/restapi/roles_mapping.yml b/src/test/resources/restapi/roles_mapping.yml index 8bfe826247..73673ae3ff 100644 --- a/src/test/resources/restapi/roles_mapping.yml +++ b/src/test/resources/restapi/roles_mapping.yml @@ -217,6 +217,9 @@ opendistro_security_role_host2: - "opendistro_security_host_localhost" and_backend_roles: [] description: "Migrated from v6" +rest_api_admin_roles_mapping_test_with_mapping: + reserved: true + users: [a, b] rest_api_admin_full_access: reserved: false hidden: true