From 1d2ea3aa4ad9fcd19e7e615c3bc3f4028063a43b Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 19:14:13 -0400 Subject: [PATCH 01/17] [Backport 3.2] Bumps spotbugs to 6.2.4 and checkstyle to 11.0.0 (#5556) Signed-off-by: Darshit Chanpura Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] Co-authored-by: Darshit Chanpura --- CHANGELOG.md | 1 + build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bddbcf990d..397deac7d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Bump `net.minidev:accessors-smart` from 2.5.2 to 2.6.0 ([#5535](https://github.com/opensearch-project/security/pull/5535)) - Bump `commons-codec:commons-codec` from 1.18.0 to 1.19.0 ([#5534](https://github.com/opensearch-project/security/pull/5534)) - Bump `commons-cli:commons-cli` from 1.9.0 to 1.10.0 ([#5533](https://github.com/opensearch-project/security/pull/5533)) +- Bump `checkstyle` to 11.0.0 and `spotbugs` to 6.2.4 ([#5555](https://github.com/opensearch-project/security/pull/5555)) ### Documentation diff --git a/build.gradle b/build.gradle index 1f9fcbfeb3..45c517f4c5 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,7 @@ plugins { id 'com.netflix.nebula.ospackage' version "11.11.2" id "org.gradle.test-retry" version "1.6.2" id 'eclipse' - id "com.github.spotbugs" version "6.2.3" + id "com.github.spotbugs" version "6.2.4" id "com.google.osdetector" version "1.7.3" } @@ -376,7 +376,7 @@ jacocoTestReport { } checkstyle { - toolVersion "10.26.1" + toolVersion "11.0.0" showViolations true configDirectory.set(rootProject.file("checkstyle/")) } From d9369b6bcb010456b1e9db05c64ac4b171123ff8 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 15:55:32 -0400 Subject: [PATCH 02/17] [Backport 3.2] Remove `commons-io` and `commons-lang3` maven metadata from being shaded in opensaml jar (#5559) Signed-off-by: Darshit Chanpura Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] --- CHANGELOG.md | 1 + libs/opensaml/build.gradle | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 397deac7d5..73865839af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Bump `commons-codec:commons-codec` from 1.18.0 to 1.19.0 ([#5534](https://github.com/opensearch-project/security/pull/5534)) - Bump `commons-cli:commons-cli` from 1.9.0 to 1.10.0 ([#5533](https://github.com/opensearch-project/security/pull/5533)) - Bump `checkstyle` to 11.0.0 and `spotbugs` to 6.2.4 ([#5555](https://github.com/opensearch-project/security/pull/5555)) +- Removes `commons-io` and `commons-lang3` maven metadata from shaded opensaml jar to fix CVE-2024-47554 ([#5558](https://github.com/opensearch-project/security/pull/5558)) ### Documentation diff --git a/libs/opensaml/build.gradle b/libs/opensaml/build.gradle index a316aed41b..974f461108 100644 --- a/libs/opensaml/build.gradle +++ b/libs/opensaml/build.gradle @@ -69,6 +69,8 @@ tasks.shadowJar { exclude 'org/publicsuffix/**' exclude 'org/slf4j/**' exclude 'javax/**' + exclude 'META-INF/maven/commons-io/commons-io/**' + exclude 'META-INF/maven/org.apache.commons/commons-lang3/**' exclude 'META-INF/versions/**/org/bouncycastle/**' exclude 'META-INF/services/org.opensaml.security.crypto.ec.NamedCurve' } From b2ef519bc405a6181d974aff6da98648b87b2dce Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 28 Aug 2025 15:18:40 -0400 Subject: [PATCH 03/17] Skip all types of write requests when user has document restrictions Signed-off-by: Craig Perkins --- .../security/configuration/DlsFlsValveImpl.java | 12 ++++++------ .../opensearch/security/filter/SecurityFilter.java | 3 ++- .../security/privileges/PrivilegesEvaluator.java | 1 + .../privileges/PrivilegesEvaluatorResponse.java | 8 ++++++++ 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 9762bfdc64..ff8cb97b3d 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -279,12 +279,12 @@ public boolean invoke(PrivilegesEvaluationContext context, final ActionListener< if (request instanceof BulkShardRequest) { for (BulkItemRequest inner : ((BulkShardRequest) request).items()) { - if (inner.request() instanceof UpdateRequest) { - listener.onFailure( - new OpenSearchSecurityException("Update is not supported when FLS or DLS or Fieldmasking is activated") - ); - return false; - } + listener.onFailure( + new OpenSearchSecurityException( + inner.request().getClass().getSimpleName() + " is not supported when FLS or DLS or Fieldmasking is activated" + ) + ); + return false; } } diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index c731c3545c..653b9fff81 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -451,7 +451,7 @@ private void ap if (pres.isAllowed()) { auditLog.logGrantedPrivileges(action, request, task); auditLog.logIndexEvent(action, request, task); - if (!dlsFlsValve.invoke(context, listener)) { + if (!pres.shouldSkipDlsValve() && !dlsFlsValve.invoke(context, listener)) { return; } final CreateIndexRequestBuilder createIndexRequestBuilder = pres.getCreateIndexRequestBuilder(); @@ -465,6 +465,7 @@ private void ap createIndexRequest.index(), alias2Name(createIndexRequest.aliases()) ); + threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); createIndexRequestBuilder.execute(ActionListener.wrap(createIndexResponse -> { if (createIndexResponse.isAcknowledged()) { log.debug( diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 65c98d7165..d09653ecbd 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -537,6 +537,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) if (replaceResult.accessDenied) { auditLog.logMissingPrivileges(action0, request, task); } else { + presponse.shouldSkipDlsValve = true; presponse.allowed = true; presponse.createIndexRequestBuilder = replaceResult.createIndexRequestBuilder; } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java index d072ec301c..1747df9fcb 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java @@ -48,6 +48,7 @@ public class PrivilegesEvaluatorResponse { private CheckTable indexToActionCheckTable; private String privilegeMatrix; private String reason; + boolean shouldSkipDlsValve = false; /** * Contains issues that were encountered during privilege evaluation. Can be used for logging. @@ -61,6 +62,13 @@ public boolean isAllowed() { return allowed; } + /** + * Returns true if the request is only for dashboards indices + */ + public boolean shouldSkipDlsValve() { + return shouldSkipDlsValve; + } + /** * Returns true if the request can be allowed if the referenced indices are reduced (aka "do not fail on forbidden"). * See getAvailableIndices() for the indices for which we have privileges. From 1dd991801401c8070e1cc3e05db76f783a2a6d70 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 29 Aug 2025 11:00:02 -0400 Subject: [PATCH 04/17] Fix CHANGELOG Signed-off-by: Craig Perkins --- CHANGELOG.md | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c0e6bcfaf..03b4ccb57d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,32 +21,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Maintenance -- Bump `org.eclipse.platform:org.eclipse.core.runtime` from 3.33.0 to 3.33.100 ([#5400](https://github.com/opensearch-project/security/pull/5400)) -- Bump `org.eclipse.platform:org.eclipse.equinox.common` from 3.20.0 to 3.20.100 ([#5402](https://github.com/opensearch-project/security/pull/5402)) -- Bump `spring_version` from 6.2.7 to 6.2.9 ([#5403](https://github.com/opensearch-project/security/pull/5403), [#5493](https://github.com/opensearch-project/security/pull/5493)) -- Bump `stefanzweifel/git-auto-commit-action` from 5 to 6 ([#5401](https://github.com/opensearch-project/security/pull/5401)) -- Bump `com.github.spotbugs` from 5.2.5 to 6.2.3 ([#5409](https://github.com/opensearch-project/security/pull/5409), [#5450](https://github.com/opensearch-project/security/pull/5450), [#5474](https://github.com/opensearch-project/security/pull/5474), [#5536](https://github.com/opensearch-project/security/pull/5536)) -- Bump `org.codehaus.plexus:plexus-utils` from 3.3.0 to 3.6.0 ([#5429](https://github.com/opensearch-project/security/pull/5429)) -- Bump `net.bytebuddy:byte-buddy` from 1.17.5 to 1.17.6 ([#5427](https://github.com/opensearch-project/security/pull/5427)) -- Bump `io.dropwizard.metrics:metrics-core` from 4.2.32 to 4.2.33 ([#5428](https://github.com/opensearch-project/security/pull/5428)) -- Bump `org.junit.jupiter:junit-jupiter-api` from 5.13.1 to 5.13.2 ([#5446](https://github.com/opensearch-project/security/pull/5446)) -- Bump `com.google.errorprone:error_prone_annotations` from 2.38.0 to 2.41.0 ([#5447](https://github.com/opensearch-project/security/pull/5447), [#5477](https://github.com/opensearch-project/security/pull/5477), [#5512](https://github.com/opensearch-project/security/pull/5512), [#5532](https://github.com/opensearch-project/security/pull/5532)) -- Bump `io.dropwizard.metrics:metrics-core` from 4.2.32 to 4.2.33 ([#5428](https://github.com/opensearch-project/security/pull/5428)) -- Bump `org.junit.jupiter:junit-jupiter` from 5.13.2 to 5.13.4 ([#5460](https://github.com/opensearch-project/security/pull/5460), [#5513](https://github.com/opensearch-project/security/pull/5513)) -- Bump `org.checkerframework:checker-qual` from 3.49.4 to 3.49.5 ([#5462](https://github.com/opensearch-project/security/pull/5462)) -- Bump `com.google.googlejavaformat:google-java-format` from 1.27.0 to 1.28.0 ([#5475](https://github.com/opensearch-project/security/pull/5475)) -- Bump `commons-validator:commons-validator` from 1.9.0 to 1.10.0 ([#5476](https://github.com/opensearch-project/security/pull/5476)) -- Bumps checkstyle to 10.26.1 that fixes CVE-2025-48734 ([#5485](https://github.com/opensearch-project/security/pull/5485)) -- Bump `commons-io:commons-io` from 2.19.0 to 2.20.0 ([#5494](https://github.com/opensearch-project/security/pull/5494)) -- Bump `org.xerial.snappy:snappy-java` from 1.1.10.7 to 1.1.10.8 ([#5495](https://github.com/opensearch-project/security/pull/5495)) -- Bump `org.apache.commons:commons-text` from 1.13.1 to 1.14.0 ([#5511](https://github.com/opensearch-project/security/pull/5511)) -- Bump `org.springframework.kafka:spring-kafka-test` from 4.0.0-M2 to 4.0.0-M3 ([#5514](https://github.com/opensearch-project/security/pull/5514)) -- Bumps opensearch-protobufs plugin version to 0.6.0 ([#5529](https://github.com/opensearch-project/security/pull/5529)) -- Bump `net.minidev:accessors-smart` from 2.5.2 to 2.6.0 ([#5535](https://github.com/opensearch-project/security/pull/5535)) -- Bump `commons-codec:commons-codec` from 1.18.0 to 1.19.0 ([#5534](https://github.com/opensearch-project/security/pull/5534)) -- Bump `commons-cli:commons-cli` from 1.9.0 to 1.10.0 ([#5533](https://github.com/opensearch-project/security/pull/5533)) -- Bump `checkstyle` to 11.0.0 and `spotbugs` to 6.2.4 ([#5555](https://github.com/opensearch-project/security/pull/5555)) -- Removes `commons-io` and `commons-lang3` maven metadata from shaded opensaml jar to fix CVE-2024-47554 ([#5558](https://github.com/opensearch-project/security/pull/5558)) - Update delete_backport_branch workflow to include release-chores branches ([#5548](https://github.com/opensearch-project/security/pull/5548)) - Bump `1password/load-secrets-action` from 2 to 3 ([#5573](https://github.com/opensearch-project/security/pull/5573)) - Bump `jjwt_version` from 0.12.6 to 0.13.0 ([#5568](https://github.com/opensearch-project/security/pull/5568), [#5581](https://github.com/opensearch-project/security/pull/5581)) From ec66b0add471b9a5d067e332889507be38b786d6 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 29 Aug 2025 11:28:26 -0400 Subject: [PATCH 05/17] Fix bwc tests Signed-off-by: Craig Perkins --- .../bwc/SecurityBackwardsCompatibilityIT.java | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java b/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java index f32051967c..46100d66eb 100644 --- a/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java +++ b/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java @@ -337,7 +337,7 @@ private void ingestData(String index) throws IOException { bulkRequestBody.append(Song.randomSong().asJson() + "\n"); } List responses = RestHelper.requestAgainstAllNodes( - testUserRestClient, + adminClient(), "POST", "_bulk?refresh=wait_for", new StringEntity(bulkRequestBody.toString(), APPLICATION_NDJSON) @@ -413,30 +413,31 @@ private boolean resourceExists(String url) throws IOException { */ private void createTestRoleIfNotExists(String role) throws IOException { String url = "_plugins/_security/api/roles/" + role; - String roleSettings = "{\n" - + " \"cluster_permissions\": [\n" - + " \"unlimited\"\n" - + " ],\n" - + " \"index_permissions\": [\n" - + " {\n" - + " \"index_patterns\": [\n" - + " \"test_index*\"\n" - + " ],\n" - + " \"dls\": \"{ \\\"bool\\\": { \\\"must\\\": { \\\"match\\\": { \\\"genre\\\": \\\"rock\\\" } } } }\",\n" - + " \"fls\": [\n" - + " \"~lyrics\"\n" - + " ],\n" - + " \"masked_fields\": [\n" - + " \"artist\"\n" - + " ],\n" - + " \"allowed_actions\": [\n" - + " \"read\",\n" - + " \"write\"\n" - + " ]\n" - + " }\n" - + " ],\n" - + " \"tenant_permissions\": []\n" - + "}\n"; + String roleSettings = """ + { + "cluster_permissions": [ + "unlimited" + ], + "index_permissions": [ + { + "index_patterns": [ + "test_index*" + ], + "dls": "{ \"bool\": { \"must\": { \"match\": { \"genre\": \"rock\" } } } }", + "fls": [ + "~lyrics" + ], + "masked_fields": [ + "artist" + ], + "allowed_actions": [ + "read", + ] + } + ], + "tenant_permissions": [] + } + """; Response response = RestHelper.makeRequest(adminClient(), "PUT", url, RestHelper.toHttpEntity(roleSettings)); assertThat(response.getStatusLine().getStatusCode(), anyOf(equalTo(200), equalTo(201))); From a5d84da834ac735a97a09575cdf8ac709d7d0a4e Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 29 Aug 2025 11:57:22 -0400 Subject: [PATCH 06/17] Remove trailing comma Signed-off-by: Craig Perkins --- .../security/bwc/SecurityBackwardsCompatibilityIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java b/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java index 46100d66eb..764ce133dd 100644 --- a/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java +++ b/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java @@ -431,7 +431,7 @@ private void createTestRoleIfNotExists(String role) throws IOException { "artist" ], "allowed_actions": [ - "read", + "read" ] } ], From 63419cd26ed1e771da41e2cd93d0f07665ccdcaa Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 29 Aug 2025 12:59:12 -0400 Subject: [PATCH 07/17] Use 3 backslashes Signed-off-by: Craig Perkins --- .../security/bwc/SecurityBackwardsCompatibilityIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java b/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java index 764ce133dd..ab4abb6f55 100644 --- a/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java +++ b/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java @@ -423,7 +423,7 @@ private void createTestRoleIfNotExists(String role) throws IOException { "index_patterns": [ "test_index*" ], - "dls": "{ \"bool\": { \"must\": { \"match\": { \"genre\": \"rock\" } } } }", + "dls": "{ \\\"bool\\\": { \\\"must\\\": { \\\"match\\\": { \\\"genre\\\": \\\"rock\\\" } } } }", "fls": [ "~lyrics" ], From 442b84a12a008a8f66773f5aaf7eb14d39092ccc Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 2 Dec 2025 12:25:38 -0500 Subject: [PATCH 08/17] Remove shouldSkipDlsValve Signed-off-by: Craig Perkins --- .../org/opensearch/security/filter/SecurityFilter.java | 2 +- .../security/privileges/PrivilegesEvaluatorImpl.java | 1 - .../security/privileges/PrivilegesEvaluatorResponse.java | 8 -------- 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index 339bb20330..bb4aaa9685 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -448,7 +448,7 @@ private void ap if (pres.isAllowed()) { auditLog.logGrantedPrivileges(action, request, task); auditLog.logIndexEvent(action, request, task); - if (!pres.shouldSkipDlsValve() && !dlsFlsValve.invoke(context, listener)) { + if (!dlsFlsValve.invoke(context, listener)) { return; } final CreateIndexRequestBuilder createIndexRequestBuilder = pres.getCreateIndexRequestBuilder(); diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorImpl.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorImpl.java index 14a8a97097..558fb6e6ad 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorImpl.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorImpl.java @@ -413,7 +413,6 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) if (!replaceResult.continueEvaluation) { if (replaceResult.accessDenied) { - presponse.shouldSkipDlsValve = true; auditLog.logMissingPrivileges(action0, request, task); } else { return PrivilegesEvaluatorResponse.ok().with(replaceResult.createIndexRequestBuilder); diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java index b13943b590..574200b3d7 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java @@ -44,7 +44,6 @@ public class PrivilegesEvaluatorResponse { private final CheckTable indexToActionCheckTable; private String privilegeMatrix; private final String reason; - boolean shouldSkipDlsValve = false; /** * Contains issues that were encountered during privilege evaluation. Can be used for logging. @@ -90,13 +89,6 @@ public boolean isAllowed() { return allowed; } - /** - * Returns true if the request is only for dashboards indices - */ - public boolean shouldSkipDlsValve() { - return shouldSkipDlsValve; - } - /** * Returns true if the request can be allowed if the referenced indices are reduced (aka "do not fail on forbidden"). * See getAvailableIndices() for the indices for which we have privileges. From 4647d540558ef31455a21f7d816a433402efa6a6 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 2 Dec 2025 14:02:24 -0500 Subject: [PATCH 09/17] Remove unnecessary changes Signed-off-by: Craig Perkins --- CHANGELOG.md | 13 ------------- .../opensearch/security/filter/SecurityFilter.java | 1 - 2 files changed, 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad2fb9015c..016d8195e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,19 +50,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Maintenance -- Update delete_backport_branch workflow to include release-chores branches ([#5548](https://github.com/opensearch-project/security/pull/5548)) -- Bump `1password/load-secrets-action` from 2 to 3 ([#5573](https://github.com/opensearch-project/security/pull/5573)) -- Bump `jjwt_version` from 0.12.6 to 0.13.0 ([#5568](https://github.com/opensearch-project/security/pull/5568), [#5581](https://github.com/opensearch-project/security/pull/5581)) -- Bump `org.mockito:mockito-core` from 5.18.0 to 5.19.0 ([#5566](https://github.com/opensearch-project/security/pull/5566)) -- Bump `open_saml_version` from 5.1.4 to 5.1.5 ([#5567](https://github.com/opensearch-project/security/pull/5567)) -- Bump `com.google.j2objc:j2objc-annotations` from 3.0.0 to 3.1 ([#5570](https://github.com/opensearch-project/security/pull/5570)) -- Bump `spring_version` from 6.2.9 to 6.2.10 ([#5569](https://github.com/opensearch-project/security/pull/5569)) -- Bump `com.github.spotbugs` from 6.2.4 to 6.2.5 ([#5584](https://github.com/opensearch-project/security/pull/5584)) -- Bump `open_saml_shib_version` from 9.1.4 to 9.1.5 ([#5585](https://github.com/opensearch-project/security/pull/5585)) -- Bump `org.springframework.kafka:spring-kafka-test` from 4.0.0-M3 to 4.0.0-M4 ([#5583](https://github.com/opensearch-project/security/pull/5583)) -- Bump `net.bytebuddy:byte-buddy` from 1.17.6 to 1.17.7 ([#5586](https://github.com/opensearch-project/security/pull/5586)) -- Bump `io.dropwizard.metrics:metrics-core` from 4.2.33 to 4.2.34 ([#5589](https://github.com/opensearch-project/security/pull/5589)) -- Bump `com.nimbusds:nimbus-jose-jwt:9.48` from 9.48 to 10.4.2 ([#5595](https://github.com/opensearch-project/security/pull/5595)) - Bump `org.junit.jupiter:junit-jupiter` from 5.13.4 to 5.14.1 ([#5678](https://github.com/opensearch-project/security/pull/5678), [#5764](https://github.com/opensearch-project/security/pull/5764)) - Bump `ch.qos.logback:logback-classic` from 1.5.18 to 1.5.20 ([#5680](https://github.com/opensearch-project/security/pull/5680), [#5724](https://github.com/opensearch-project/security/pull/5724)) - Bump `org.scala-lang:scala-library` from 2.13.16 to 2.13.18 ([#5682](https://github.com/opensearch-project/security/pull/5682), [#5809](https://github.com/opensearch-project/security/pull/5809)) diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index bb4aaa9685..f4e005d6a2 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -462,7 +462,6 @@ private void ap createIndexRequest.index(), alias2Name(createIndexRequest.aliases()) ); - threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); createIndexRequestBuilder.execute(ActionListener.wrap(createIndexResponse -> { if (createIndexResponse.isAcknowledged()) { log.debug( From a745520d439439bdfdb3c25bb2a0f345f2f04b52 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 2 Dec 2025 14:10:14 -0500 Subject: [PATCH 10/17] Introduce dynamic cluster setting Signed-off-by: Craig Perkins --- .../security/OpenSearchSecurityPlugin.java | 1 + .../configuration/DlsFlsValveImpl.java | 32 +++++++++++++++---- .../security/support/ConfigConstants.java | 2 ++ .../security/support/SecuritySettings.java | 7 ++++ 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 57af519311..8ed0d6de9b 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -2269,6 +2269,7 @@ public List> getSettings() { ); settings.add(SecuritySettings.USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING); + settings.add(SecuritySettings.DLS_WRITE_BLOCKED); } return settings; diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 1701164dc8..122ef92c86 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -85,11 +85,14 @@ import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.HeaderHelper; +import org.opensearch.security.support.SecuritySettings; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.client.Client; import static org.opensearch.security.privileges.PrivilegesEvaluatorImpl.isClusterPerm; +import static org.opensearch.security.support.ConfigConstants.SECURITY_DLS_WRITE_BLOCKED; +import static org.opensearch.security.support.ConfigConstants.SECURITY_DLS_WRITE_BLOCKED_ENABLED_DEFAULT; public class DlsFlsValveImpl implements DlsFlsRequestValve { @@ -109,6 +112,7 @@ public class DlsFlsValveImpl implements DlsFlsRequestValve { private final AdminDNs adminDNs; private final OpensearchDynamicSetting resourceSharingEnabledSetting; private final ResourcePluginInfo resourcePluginInfo; + private volatile boolean dlsWriteBlockedEnabled; public DlsFlsValveImpl( Settings settings, @@ -142,6 +146,12 @@ public DlsFlsValveImpl( config.updateClusterStateMetadataAsync(clusterService, threadPool); } }); + this.dlsWriteBlockedEnabled = settings.getAsBoolean(SECURITY_DLS_WRITE_BLOCKED, SECURITY_DLS_WRITE_BLOCKED_ENABLED_DEFAULT); + if (clusterService.getClusterSettings() != null) { + clusterService.getClusterSettings().addSettingsUpdateConsumer(SecuritySettings.DLS_WRITE_BLOCKED, newDlsWriteBlockedEnabled -> { + dlsWriteBlockedEnabled = newDlsWriteBlockedEnabled; + }); + } this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; } @@ -333,12 +343,22 @@ public boolean invoke(PrivilegesEvaluationContext context, final ActionListener< if (request instanceof BulkShardRequest) { for (BulkItemRequest inner : ((BulkShardRequest) request).items()) { - listener.onFailure( - new OpenSearchSecurityException( - inner.request().getClass().getSimpleName() + " is not supported when FLS or DLS or Fieldmasking is activated" - ) - ); - return false; + if (dlsWriteBlockedEnabled) { + listener.onFailure( + new OpenSearchSecurityException( + inner.request().getClass().getSimpleName() + + " is not supported when FLS or DLS or Fieldmasking is activated" + ) + ); + return false; + } else { + if (inner.request() instanceof UpdateRequest) { + listener.onFailure( + new OpenSearchSecurityException("Update is not supported when FLS or DLS or Fieldmasking is activated") + ); + return false; + } + } } } diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index a7a271c758..37eb41a771 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -340,6 +340,8 @@ public enum RolesMappingResolution { public static final String SECURITY_FILTER_SECURITYINDEX_FROM_ALL_REQUESTS = SECURITY_SETTINGS_PREFIX + "filter_securityindex_from_all_requests"; public static final String SECURITY_DLS_MODE = SECURITY_SETTINGS_PREFIX + "dls.mode"; + public static final String SECURITY_DLS_WRITE_BLOCKED = SECURITY_SETTINGS_PREFIX + "dls.write_blocked"; + public static final boolean SECURITY_DLS_WRITE_BLOCKED_ENABLED_DEFAULT = false; // REST API public static final String SECURITY_RESTAPI_ROLES_ENABLED = SECURITY_SETTINGS_PREFIX + "restapi.roles_enabled"; public static final String SECURITY_RESTAPI_ADMIN_ENABLED = SECURITY_SETTINGS_PREFIX + "restapi.admin.enabled"; diff --git a/src/main/java/org/opensearch/security/support/SecuritySettings.java b/src/main/java/org/opensearch/security/support/SecuritySettings.java index 4a442c9316..cb5e6c1cd1 100644 --- a/src/main/java/org/opensearch/security/support/SecuritySettings.java +++ b/src/main/java/org/opensearch/security/support/SecuritySettings.java @@ -42,4 +42,11 @@ public class SecuritySettings { Setting.Property.NodeScope, Setting.Property.Dynamic ); // Not filtered + + public static final Setting DLS_WRITE_BLOCKED = Setting.boolSetting( + ConfigConstants.SECURITY_DLS_WRITE_BLOCKED, + false, + Setting.Property.NodeScope, + Setting.Property.Dynamic + ); } From 328199d033bfaad50c1d72f212e7ce1020b44ef3 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 2 Dec 2025 14:32:48 -0500 Subject: [PATCH 11/17] Add integrationTest suite Signed-off-by: Craig Perkins --- CHANGELOG.md | 1 - .../DlsWriteBlockedIntegrationTest.java | 183 ++++++++++++++++++ 2 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 src/integrationTest/java/org/opensearch/security/dlsfls/DlsWriteBlockedIntegrationTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 016d8195e3..2f32bacb66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,7 +49,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Remove reflective call to getInnerChannel ([#5816](https://github.com/opensearch-project/security/pull/5816)) ### Maintenance - - Bump `org.junit.jupiter:junit-jupiter` from 5.13.4 to 5.14.1 ([#5678](https://github.com/opensearch-project/security/pull/5678), [#5764](https://github.com/opensearch-project/security/pull/5764)) - Bump `ch.qos.logback:logback-classic` from 1.5.18 to 1.5.20 ([#5680](https://github.com/opensearch-project/security/pull/5680), [#5724](https://github.com/opensearch-project/security/pull/5724)) - Bump `org.scala-lang:scala-library` from 2.13.16 to 2.13.18 ([#5682](https://github.com/opensearch-project/security/pull/5682), [#5809](https://github.com/opensearch-project/security/pull/5809)) diff --git a/src/integrationTest/java/org/opensearch/security/dlsfls/DlsWriteBlockedIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/dlsfls/DlsWriteBlockedIntegrationTest.java new file mode 100644 index 0000000000..3f88c00a8e --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/dlsfls/DlsWriteBlockedIntegrationTest.java @@ -0,0 +1,183 @@ +/* + * 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.dlsfls; + +import java.io.IOException; +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.support.WriteRequest.RefreshPolicy; +import org.opensearch.client.RestHighLevelClient; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.TestSecurityConfig.User; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.transport.client.Client; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.is; +import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; +import static org.opensearch.client.RequestOptions.DEFAULT; +import static org.opensearch.core.rest.RestStatus.INTERNAL_SERVER_ERROR; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; +import static org.opensearch.test.framework.matcher.ExceptionMatcherAssert.assertThatThrownBy; +import static org.opensearch.test.framework.matcher.OpenSearchExceptionMatchers.errorMessageContain; +import static org.opensearch.test.framework.matcher.OpenSearchExceptionMatchers.statusException; + +/** + * Integration tests for DLS_WRITE_BLOCKED setting which blocks write operations + * when users have DLS, FLS, or Field Masking restrictions. + */ +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class DlsWriteBlockedIntegrationTest { + + private static final String DLS_INDEX = "dls_index"; + private static final String FLS_INDEX = "fls_index"; + private static final String NO_RESTRICTION_INDEX = "no_restriction_index"; + + static final User ADMIN_USER = new User("admin").roles(ALL_ACCESS); + + static final User DLS_USER = new User("dls_user").roles( + new Role("dls_role").clusterPermissions("*").indexPermissions("*").dls("{\"term\": {\"dept\": \"sales\"}}").on(DLS_INDEX) + ); + + static final User FLS_USER = new User("fls_user").roles( + new Role("fls_role").clusterPermissions("*").indexPermissions("*").fls("public").on(FLS_INDEX) + ); + + @ClassRule + public static final LocalCluster clusterWithoutDlsWriteBlocked = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER, DLS_USER, FLS_USER) + .build(); + + @ClassRule + public static final LocalCluster clusterWithDlsWriteBlocked = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER, DLS_USER, FLS_USER) + .nodeSettings(Map.of(ConfigConstants.SECURITY_DLS_WRITE_BLOCKED, true)) + .build(); + + @BeforeClass + public static void createTestData() { + try (Client client = clusterWithoutDlsWriteBlocked.getInternalNodeClient()) { + client.index(new IndexRequest(DLS_INDEX).id("1").setRefreshPolicy(IMMEDIATE).source(Map.of("dept", "sales", "amount", 100))) + .actionGet(); + client.index( + new IndexRequest(FLS_INDEX).id("1").setRefreshPolicy(IMMEDIATE).source(Map.of("public", "data", "secret", "hidden")) + ).actionGet(); + client.index(new IndexRequest(NO_RESTRICTION_INDEX).id("1").setRefreshPolicy(IMMEDIATE).source(Map.of("data", "value1"))) + .actionGet(); + } + + try (Client client = clusterWithDlsWriteBlocked.getInternalNodeClient()) { + client.index(new IndexRequest(DLS_INDEX).id("1").setRefreshPolicy(IMMEDIATE).source(Map.of("dept", "sales", "amount", 100))) + .actionGet(); + client.index( + new IndexRequest(FLS_INDEX).id("1").setRefreshPolicy(IMMEDIATE).source(Map.of("public", "data", "secret", "hidden")) + ).actionGet(); + client.index(new IndexRequest(NO_RESTRICTION_INDEX).id("1").setRefreshPolicy(IMMEDIATE).source(Map.of("data", "value1"))) + .actionGet(); + } + } + + @Test + public void testDlsUser_CanWrite_WhenSettingDisabled() throws IOException { + try (RestHighLevelClient client = clusterWithoutDlsWriteBlocked.getRestHighLevelClient(DLS_USER)) { + IndexRequest request = new IndexRequest(DLS_INDEX).id("new1") + .setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source(Map.of("dept", "sales", "amount", 400)); + + var response = client.index(request, DEFAULT); + + assertThat(response.status().getStatus(), is(201)); + } + } + + @Test + public void testDlsUser_CannotWrite_WhenSettingEnabled() throws IOException { + try (RestHighLevelClient client = clusterWithDlsWriteBlocked.getRestHighLevelClient(DLS_USER)) { + IndexRequest request = new IndexRequest(DLS_INDEX).id("new1") + .setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source(Map.of("dept", "sales", "amount", 400)); + + assertThatThrownBy( + () -> client.index(request, DEFAULT), + allOf( + statusException(INTERNAL_SERVER_ERROR), + errorMessageContain("is not supported when FLS or DLS or Fieldmasking is activated") + ) + ); + } + } + + @Test + public void testFlsUser_CanWrite_WhenSettingDisabled() throws IOException { + try (RestHighLevelClient client = clusterWithoutDlsWriteBlocked.getRestHighLevelClient(FLS_USER)) { + IndexRequest request = new IndexRequest(FLS_INDEX).id("new1") + .setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source(Map.of("public", "new_data", "secret", "new_secret")); + + var response = client.index(request, DEFAULT); + + assertThat(response.status().getStatus(), is(201)); + } + } + + @Test + public void testFlsUser_CannotWrite_WhenSettingEnabled() throws IOException { + try (RestHighLevelClient client = clusterWithDlsWriteBlocked.getRestHighLevelClient(FLS_USER)) { + IndexRequest request = new IndexRequest(FLS_INDEX).id("new1") + .setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source(Map.of("public", "new_data", "secret", "new_secret")); + + assertThatThrownBy( + () -> client.index(request, DEFAULT), + allOf( + statusException(INTERNAL_SERVER_ERROR), + errorMessageContain("is not supported when FLS or DLS or Fieldmasking is activated") + ) + ); + } + } + + @Test + public void testAdminUser_CanWrite_WhenSettingEnabled() throws IOException { + try (RestHighLevelClient client = clusterWithDlsWriteBlocked.getRestHighLevelClient(ADMIN_USER)) { + IndexRequest request = new IndexRequest(DLS_INDEX).id("admin_doc") + .setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source(Map.of("dept", "admin", "amount", 999)); + + var response = client.index(request, DEFAULT); + + assertThat(response.status().getStatus(), is(201)); + } + } + + @Test + public void testDlsUser_CanRead_WhenSettingEnabled() throws IOException { + try (RestHighLevelClient client = clusterWithDlsWriteBlocked.getRestHighLevelClient(DLS_USER)) { + var response = client.search(new org.opensearch.action.search.SearchRequest(DLS_INDEX), DEFAULT); + + assertThat(response.status().getStatus(), is(200)); + } + } +} From f2ca6002c787053411e7931020644604c91f8db3 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 2 Dec 2025 14:45:39 -0500 Subject: [PATCH 12/17] Toggle setting dynamically Signed-off-by: Craig Perkins --- .../DlsWriteBlockedIntegrationTest.java | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/dlsfls/DlsWriteBlockedIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/dlsfls/DlsWriteBlockedIntegrationTest.java index 3f88c00a8e..c4225e6ba1 100644 --- a/src/integrationTest/java/org/opensearch/security/dlsfls/DlsWriteBlockedIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/dlsfls/DlsWriteBlockedIntegrationTest.java @@ -25,6 +25,7 @@ import org.opensearch.test.framework.TestSecurityConfig.User; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.transport.client.Client; import static org.hamcrest.MatcherAssert.assertThat; @@ -62,23 +63,15 @@ public class DlsWriteBlockedIntegrationTest { ); @ClassRule - public static final LocalCluster clusterWithoutDlsWriteBlocked = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) .anonymousAuth(false) .authc(AUTHC_HTTPBASIC_INTERNAL) .users(ADMIN_USER, DLS_USER, FLS_USER) .build(); - @ClassRule - public static final LocalCluster clusterWithDlsWriteBlocked = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) - .anonymousAuth(false) - .authc(AUTHC_HTTPBASIC_INTERNAL) - .users(ADMIN_USER, DLS_USER, FLS_USER) - .nodeSettings(Map.of(ConfigConstants.SECURITY_DLS_WRITE_BLOCKED, true)) - .build(); - @BeforeClass public static void createTestData() { - try (Client client = clusterWithoutDlsWriteBlocked.getInternalNodeClient()) { + try (Client client = cluster.getInternalNodeClient()) { client.index(new IndexRequest(DLS_INDEX).id("1").setRefreshPolicy(IMMEDIATE).source(Map.of("dept", "sales", "amount", 100))) .actionGet(); client.index( @@ -87,22 +80,22 @@ public static void createTestData() { client.index(new IndexRequest(NO_RESTRICTION_INDEX).id("1").setRefreshPolicy(IMMEDIATE).source(Map.of("data", "value1"))) .actionGet(); } + } - try (Client client = clusterWithDlsWriteBlocked.getInternalNodeClient()) { - client.index(new IndexRequest(DLS_INDEX).id("1").setRefreshPolicy(IMMEDIATE).source(Map.of("dept", "sales", "amount", 100))) - .actionGet(); - client.index( - new IndexRequest(FLS_INDEX).id("1").setRefreshPolicy(IMMEDIATE).source(Map.of("public", "data", "secret", "hidden")) - ).actionGet(); - client.index(new IndexRequest(NO_RESTRICTION_INDEX).id("1").setRefreshPolicy(IMMEDIATE).source(Map.of("data", "value1"))) - .actionGet(); + private void setDlsWriteBlocked(boolean enabled) throws IOException { + try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { + client.putJson( + "_cluster/settings", + String.format("{\"transient\":{\"%s\":%b}}", ConfigConstants.SECURITY_DLS_WRITE_BLOCKED, enabled) + ); } } @Test public void testDlsUser_CanWrite_WhenSettingDisabled() throws IOException { - try (RestHighLevelClient client = clusterWithoutDlsWriteBlocked.getRestHighLevelClient(DLS_USER)) { - IndexRequest request = new IndexRequest(DLS_INDEX).id("new1") + setDlsWriteBlocked(false); + try (RestHighLevelClient client = cluster.getRestHighLevelClient(DLS_USER)) { + IndexRequest request = new IndexRequest(DLS_INDEX).id("test1") .setRefreshPolicy(RefreshPolicy.IMMEDIATE) .source(Map.of("dept", "sales", "amount", 400)); @@ -114,8 +107,9 @@ public void testDlsUser_CanWrite_WhenSettingDisabled() throws IOException { @Test public void testDlsUser_CannotWrite_WhenSettingEnabled() throws IOException { - try (RestHighLevelClient client = clusterWithDlsWriteBlocked.getRestHighLevelClient(DLS_USER)) { - IndexRequest request = new IndexRequest(DLS_INDEX).id("new1") + setDlsWriteBlocked(true); + try (RestHighLevelClient client = cluster.getRestHighLevelClient(DLS_USER)) { + IndexRequest request = new IndexRequest(DLS_INDEX).id("test2") .setRefreshPolicy(RefreshPolicy.IMMEDIATE) .source(Map.of("dept", "sales", "amount", 400)); @@ -131,8 +125,9 @@ public void testDlsUser_CannotWrite_WhenSettingEnabled() throws IOException { @Test public void testFlsUser_CanWrite_WhenSettingDisabled() throws IOException { - try (RestHighLevelClient client = clusterWithoutDlsWriteBlocked.getRestHighLevelClient(FLS_USER)) { - IndexRequest request = new IndexRequest(FLS_INDEX).id("new1") + setDlsWriteBlocked(false); + try (RestHighLevelClient client = cluster.getRestHighLevelClient(FLS_USER)) { + IndexRequest request = new IndexRequest(FLS_INDEX).id("test3") .setRefreshPolicy(RefreshPolicy.IMMEDIATE) .source(Map.of("public", "new_data", "secret", "new_secret")); @@ -144,8 +139,9 @@ public void testFlsUser_CanWrite_WhenSettingDisabled() throws IOException { @Test public void testFlsUser_CannotWrite_WhenSettingEnabled() throws IOException { - try (RestHighLevelClient client = clusterWithDlsWriteBlocked.getRestHighLevelClient(FLS_USER)) { - IndexRequest request = new IndexRequest(FLS_INDEX).id("new1") + setDlsWriteBlocked(true); + try (RestHighLevelClient client = cluster.getRestHighLevelClient(FLS_USER)) { + IndexRequest request = new IndexRequest(FLS_INDEX).id("test4") .setRefreshPolicy(RefreshPolicy.IMMEDIATE) .source(Map.of("public", "new_data", "secret", "new_secret")); @@ -161,8 +157,9 @@ public void testFlsUser_CannotWrite_WhenSettingEnabled() throws IOException { @Test public void testAdminUser_CanWrite_WhenSettingEnabled() throws IOException { - try (RestHighLevelClient client = clusterWithDlsWriteBlocked.getRestHighLevelClient(ADMIN_USER)) { - IndexRequest request = new IndexRequest(DLS_INDEX).id("admin_doc") + setDlsWriteBlocked(true); + try (RestHighLevelClient client = cluster.getRestHighLevelClient(ADMIN_USER)) { + IndexRequest request = new IndexRequest(DLS_INDEX).id("test6") .setRefreshPolicy(RefreshPolicy.IMMEDIATE) .source(Map.of("dept", "admin", "amount", 999)); @@ -174,7 +171,8 @@ public void testAdminUser_CanWrite_WhenSettingEnabled() throws IOException { @Test public void testDlsUser_CanRead_WhenSettingEnabled() throws IOException { - try (RestHighLevelClient client = clusterWithDlsWriteBlocked.getRestHighLevelClient(DLS_USER)) { + setDlsWriteBlocked(true); + try (RestHighLevelClient client = cluster.getRestHighLevelClient(DLS_USER)) { var response = client.search(new org.opensearch.action.search.SearchRequest(DLS_INDEX), DEFAULT); assertThat(response.status().getStatus(), is(200)); From 6ffa2525a1d78d214aaf33c9f17fea2baa18cb77 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 2 Dec 2025 14:57:32 -0500 Subject: [PATCH 13/17] Use rest client Signed-off-by: Craig Perkins --- .../DlsWriteBlockedIntegrationTest.java | 97 ++++++------------- 1 file changed, 28 insertions(+), 69 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/dlsfls/DlsWriteBlockedIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/dlsfls/DlsWriteBlockedIntegrationTest.java index c4225e6ba1..c6468d6280 100644 --- a/src/integrationTest/java/org/opensearch/security/dlsfls/DlsWriteBlockedIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/dlsfls/DlsWriteBlockedIntegrationTest.java @@ -9,7 +9,6 @@ package org.opensearch.security.dlsfls; import java.io.IOException; -import java.util.Map; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.junit.BeforeClass; @@ -17,28 +16,18 @@ import org.junit.Test; import org.junit.runner.RunWith; -import org.opensearch.action.index.IndexRequest; -import org.opensearch.action.support.WriteRequest.RefreshPolicy; -import org.opensearch.client.RestHighLevelClient; import org.opensearch.security.support.ConfigConstants; import org.opensearch.test.framework.TestSecurityConfig.Role; import org.opensearch.test.framework.TestSecurityConfig.User; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; -import org.opensearch.transport.client.Client; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; -import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; -import static org.opensearch.client.RequestOptions.DEFAULT; -import static org.opensearch.core.rest.RestStatus.INTERNAL_SERVER_ERROR; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; -import static org.opensearch.test.framework.matcher.ExceptionMatcherAssert.assertThatThrownBy; -import static org.opensearch.test.framework.matcher.OpenSearchExceptionMatchers.errorMessageContain; -import static org.opensearch.test.framework.matcher.OpenSearchExceptionMatchers.statusException; /** * Integration tests for DLS_WRITE_BLOCKED setting which blocks write operations @@ -70,15 +59,11 @@ public class DlsWriteBlockedIntegrationTest { .build(); @BeforeClass - public static void createTestData() { - try (Client client = cluster.getInternalNodeClient()) { - client.index(new IndexRequest(DLS_INDEX).id("1").setRefreshPolicy(IMMEDIATE).source(Map.of("dept", "sales", "amount", 100))) - .actionGet(); - client.index( - new IndexRequest(FLS_INDEX).id("1").setRefreshPolicy(IMMEDIATE).source(Map.of("public", "data", "secret", "hidden")) - ).actionGet(); - client.index(new IndexRequest(NO_RESTRICTION_INDEX).id("1").setRefreshPolicy(IMMEDIATE).source(Map.of("data", "value1"))) - .actionGet(); + public static void createTestData() throws IOException { + try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { + client.putJson(DLS_INDEX + "/_doc/1?refresh=true", "{\"dept\":\"sales\",\"amount\":100}"); + client.putJson(FLS_INDEX + "/_doc/1?refresh=true", "{\"public\":\"data\",\"secret\":\"hidden\"}"); + client.putJson(NO_RESTRICTION_INDEX + "/_doc/1?refresh=true", "{\"data\":\"value1\"}"); } } @@ -94,88 +79,62 @@ private void setDlsWriteBlocked(boolean enabled) throws IOException { @Test public void testDlsUser_CanWrite_WhenSettingDisabled() throws IOException { setDlsWriteBlocked(false); - try (RestHighLevelClient client = cluster.getRestHighLevelClient(DLS_USER)) { - IndexRequest request = new IndexRequest(DLS_INDEX).id("test1") - .setRefreshPolicy(RefreshPolicy.IMMEDIATE) - .source(Map.of("dept", "sales", "amount", 400)); - - var response = client.index(request, DEFAULT); + try (TestRestClient client = cluster.getRestClient(DLS_USER)) { + var response = client.putJson(DLS_INDEX + "/_doc/test1?refresh=true", "{\"dept\":\"sales\",\"amount\":400}"); - assertThat(response.status().getStatus(), is(201)); + assertThat(response.getStatusCode(), is(201)); } } @Test public void testDlsUser_CannotWrite_WhenSettingEnabled() throws IOException { setDlsWriteBlocked(true); - try (RestHighLevelClient client = cluster.getRestHighLevelClient(DLS_USER)) { - IndexRequest request = new IndexRequest(DLS_INDEX).id("test2") - .setRefreshPolicy(RefreshPolicy.IMMEDIATE) - .source(Map.of("dept", "sales", "amount", 400)); - - assertThatThrownBy( - () -> client.index(request, DEFAULT), - allOf( - statusException(INTERNAL_SERVER_ERROR), - errorMessageContain("is not supported when FLS or DLS or Fieldmasking is activated") - ) - ); + try (TestRestClient client = cluster.getRestClient(DLS_USER)) { + var response = client.putJson(DLS_INDEX + "/_doc/test2?refresh=true", "{\"dept\":\"sales\",\"amount\":400}"); + + assertThat(response.getStatusCode(), is(500)); + assertThat(response.getBody(), containsString("is not supported when FLS or DLS or Fieldmasking is activated")); } } @Test public void testFlsUser_CanWrite_WhenSettingDisabled() throws IOException { setDlsWriteBlocked(false); - try (RestHighLevelClient client = cluster.getRestHighLevelClient(FLS_USER)) { - IndexRequest request = new IndexRequest(FLS_INDEX).id("test3") - .setRefreshPolicy(RefreshPolicy.IMMEDIATE) - .source(Map.of("public", "new_data", "secret", "new_secret")); - - var response = client.index(request, DEFAULT); + try (TestRestClient client = cluster.getRestClient(FLS_USER)) { + var response = client.putJson(FLS_INDEX + "/_doc/test3?refresh=true", "{\"public\":\"new_data\",\"secret\":\"new_secret\"}"); - assertThat(response.status().getStatus(), is(201)); + assertThat(response.getStatusCode(), is(201)); } } @Test public void testFlsUser_CannotWrite_WhenSettingEnabled() throws IOException { setDlsWriteBlocked(true); - try (RestHighLevelClient client = cluster.getRestHighLevelClient(FLS_USER)) { - IndexRequest request = new IndexRequest(FLS_INDEX).id("test4") - .setRefreshPolicy(RefreshPolicy.IMMEDIATE) - .source(Map.of("public", "new_data", "secret", "new_secret")); - - assertThatThrownBy( - () -> client.index(request, DEFAULT), - allOf( - statusException(INTERNAL_SERVER_ERROR), - errorMessageContain("is not supported when FLS or DLS or Fieldmasking is activated") - ) - ); + try (TestRestClient client = cluster.getRestClient(FLS_USER)) { + var response = client.putJson(FLS_INDEX + "/_doc/test4?refresh=true", "{\"public\":\"new_data\",\"secret\":\"new_secret\"}"); + + assertThat(response.getStatusCode(), is(500)); + assertThat(response.getBody(), containsString("is not supported when FLS or DLS or Fieldmasking is activated")); } } @Test public void testAdminUser_CanWrite_WhenSettingEnabled() throws IOException { setDlsWriteBlocked(true); - try (RestHighLevelClient client = cluster.getRestHighLevelClient(ADMIN_USER)) { - IndexRequest request = new IndexRequest(DLS_INDEX).id("test6") - .setRefreshPolicy(RefreshPolicy.IMMEDIATE) - .source(Map.of("dept", "admin", "amount", 999)); - - var response = client.index(request, DEFAULT); + try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { + var response = client.putJson(DLS_INDEX + "/_doc/test6?refresh=true", "{\"dept\":\"admin\",\"amount\":999}"); - assertThat(response.status().getStatus(), is(201)); + assertThat(response.getStatusCode(), is(201)); } } @Test public void testDlsUser_CanRead_WhenSettingEnabled() throws IOException { setDlsWriteBlocked(true); - try (RestHighLevelClient client = cluster.getRestHighLevelClient(DLS_USER)) { - var response = client.search(new org.opensearch.action.search.SearchRequest(DLS_INDEX), DEFAULT); + try (TestRestClient client = cluster.getRestClient(DLS_USER)) { + var response = client.get(DLS_INDEX + "/_search"); - assertThat(response.status().getStatus(), is(200)); + assertThat(response.getStatusCode(), is(200)); } } } From b668bc3102814bf23a5b3fd0d74d76c171bf82cb Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 5 Jan 2026 14:02:11 -0500 Subject: [PATCH 14/17] Add to CHANGELOG Signed-off-by: Craig Perkins --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f30002744..f3f10a5e3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Features ### Enhancements +- Introduce new dynamic setting (plugins.security.dls.write_blocked) to block all writes when restrictions apply ([#5828](https://github.com/opensearch-project/security/pull/5828)) ### Bug Fixes - Fix IllegalArgumentException when resolved indices are empty in PrivilegesEvaluator ([#5770](https://github.com/opensearch-project/security/pull/5797)) From ef029b08d143c1d7c0884180a7f03097c0a3c871 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 6 Jan 2026 11:58:52 -0500 Subject: [PATCH 15/17] Address comments Signed-off-by: Craig Perkins --- .../dlsfls/DlsWriteBlockedIntegrationTest.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/dlsfls/DlsWriteBlockedIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/dlsfls/DlsWriteBlockedIntegrationTest.java index c6468d6280..8cfb95de87 100644 --- a/src/integrationTest/java/org/opensearch/security/dlsfls/DlsWriteBlockedIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/dlsfls/DlsWriteBlockedIntegrationTest.java @@ -10,13 +10,10 @@ import java.io.IOException; -import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.opensearch.security.support.ConfigConstants; import org.opensearch.test.framework.TestSecurityConfig.Role; import org.opensearch.test.framework.TestSecurityConfig.User; import org.opensearch.test.framework.cluster.ClusterManager; @@ -28,13 +25,12 @@ import static org.hamcrest.Matchers.is; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; +import static org.opensearch.test.framework.matcher.RestMatchers.isCreated; /** * Integration tests for DLS_WRITE_BLOCKED setting which blocks write operations * when users have DLS, FLS, or Field Masking restrictions. */ -@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) -@ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class DlsWriteBlockedIntegrationTest { private static final String DLS_INDEX = "dls_index"; @@ -69,10 +65,7 @@ public static void createTestData() throws IOException { private void setDlsWriteBlocked(boolean enabled) throws IOException { try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { - client.putJson( - "_cluster/settings", - String.format("{\"transient\":{\"%s\":%b}}", ConfigConstants.SECURITY_DLS_WRITE_BLOCKED, enabled) - ); + client.putJson("_cluster/settings", String.format("{\"transient\":{\"plugins.security.dls.write_blocked\":%b}}", enabled)); } } @@ -82,7 +75,7 @@ public void testDlsUser_CanWrite_WhenSettingDisabled() throws IOException { try (TestRestClient client = cluster.getRestClient(DLS_USER)) { var response = client.putJson(DLS_INDEX + "/_doc/test1?refresh=true", "{\"dept\":\"sales\",\"amount\":400}"); - assertThat(response.getStatusCode(), is(201)); + assertThat(response, isCreated()); } } From f05045fb937020b91afa88ecfae299cb402eeb3b Mon Sep 17 00:00:00 2001 From: Nils Bandener <33570290+nibix@users.noreply.github.com> Date: Wed, 7 Jan 2026 06:00:36 +0100 Subject: [PATCH 16/17] New tests for protected indices feature (#5865) Signed-off-by: Nils Bandener --- .../privileges/int_tests/ClusterConfig.java | 50 ++- .../CrossClusterAuthorizationIntTests.java | 13 +- .../DashboardMultiTenancyIntTests.java | 14 +- ...taStreamAuthorizationReadOnlyIntTests.java | 14 +- ...aStreamAuthorizationReadWriteIntTests.java | 14 +- .../IndexAuthorizationReadOnlyIntTests.java | 14 +- .../IndexAuthorizationReadWriteIntTests.java | 14 +- ...uthorizationWithClosedIndicesIntTests.java | 10 +- .../ProtectedIndexAuthorizationIntTests.java | 338 ++++++++++++++++++ .../SnapshotAuthorizationIntTests.java | 14 +- .../test/framework/cluster/LocalCluster.java | 11 + 11 files changed, 429 insertions(+), 77 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/privileges/int_tests/ProtectedIndexAuthorizationIntTests.java diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/ClusterConfig.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/ClusterConfig.java index 2d3a1627e0..cfed0b4262 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/ClusterConfig.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/ClusterConfig.java @@ -12,9 +12,12 @@ package org.opensearch.security.privileges.int_tests; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Supplier; +import org.junit.rules.ExternalResource; + import org.opensearch.test.framework.cluster.LocalCluster; /** @@ -46,8 +49,6 @@ public enum ClusterConfig { final boolean systemIndexPrivilegeEnabled; final boolean allowsEmptyResultSets; - private LocalCluster cluster; - ClusterConfig( String name, Function clusterConfiguration, @@ -62,25 +63,38 @@ public enum ClusterConfig { this.allowsEmptyResultSets = allowsEmptyResultSets; } - LocalCluster cluster(Supplier clusterBuilder) { - if (cluster == null) { - cluster = this.clusterConfiguration.apply(clusterBuilder.get()).build(); - cluster.before(); - } - return cluster; + @Override + public String toString() { + return name; } - void shutdown() { - if (cluster != null) { - try { - cluster.close(); - } catch (Exception e) {} - cluster = null; + public static class ClusterInstances extends ExternalResource { + private final Supplier clusterBuilder; + + public ClusterInstances(Supplier clusterBuilder) { + this.clusterBuilder = clusterBuilder; } - } - @Override - public String toString() { - return name; + private Map configToInstanceMap = new ConcurrentHashMap<>(); + + public LocalCluster get(ClusterConfig config) { + LocalCluster cluster = configToInstanceMap.get(config); + if (cluster == null) { + cluster = config.clusterConfiguration.apply(clusterBuilder.get()).build(); + cluster.before(); + configToInstanceMap.put(config, cluster); + } + + return cluster; + } + + @Override + protected void after() { + for (Map.Entry entry : configToInstanceMap.entrySet()) { + entry.getValue().stopSafe(); + } + configToInstanceMap.clear(); + }; + } } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/CrossClusterAuthorizationIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/CrossClusterAuthorizationIntTests.java index be24358308..76f28dd96a 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/CrossClusterAuthorizationIntTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/CrossClusterAuthorizationIntTests.java @@ -16,7 +16,6 @@ import java.util.List; import com.google.common.collect.ImmutableList; -import org.junit.AfterClass; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; @@ -189,12 +188,10 @@ static LocalCluster.Builder clusterBuilder() { .indices(LocalIndices.index_a1, LocalIndices.index_a2); } - @AfterClass - public static void stopClusters() { - for (ClusterConfig clusterConfig : ClusterConfig.values()) { - clusterConfig.shutdown(); - } - } + @ClassRule + public static final ClusterConfig.ClusterInstances clusters = new ClusterConfig.ClusterInstances( + CrossClusterAuthorizationIntTests::clusterBuilder + ); final TestSecurityConfig.User user; final LocalCluster cluster; @@ -479,7 +476,7 @@ public static Collection params() { public CrossClusterAuthorizationIntTests(ClusterConfig clusterConfig, TestSecurityConfig.User user, String description) throws Exception { this.user = user; - this.cluster = clusterConfig.cluster(CrossClusterAuthorizationIntTests::clusterBuilder); + this.cluster = clusters.get(clusterConfig); this.clusterConfig = clusterConfig; } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DashboardMultiTenancyIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DashboardMultiTenancyIntTests.java index cc7ab237c1..5b67d241e2 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DashboardMultiTenancyIntTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DashboardMultiTenancyIntTests.java @@ -17,7 +17,7 @@ import java.util.UUID; import org.apache.hc.core5.http.message.BasicHeader; -import org.junit.AfterClass; +import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -361,12 +361,10 @@ static LocalCluster.Builder clusterBuilder() { ); } - @AfterClass - public static void stopClusters() { - for (ClusterConfig clusterConfig : ClusterConfig.values()) { - clusterConfig.shutdown(); - } - } + @ClassRule + public static final ClusterConfig.ClusterInstances clusterInstances = new ClusterConfig.ClusterInstances( + DashboardMultiTenancyIntTests::clusterBuilder + ); final TestSecurityConfig.User user; final LocalCluster cluster; @@ -763,7 +761,7 @@ public DashboardMultiTenancyIntTests( @SuppressWarnings("unused") String description ) { this.user = user; - this.cluster = clusterConfig.cluster(DashboardMultiTenancyIntTests::clusterBuilder); + this.cluster = clusterInstances.get(clusterConfig); this.clusterConfig = clusterConfig; } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DataStreamAuthorizationReadOnlyIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DataStreamAuthorizationReadOnlyIntTests.java index 2eaf0b5012..a43be48ca6 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DataStreamAuthorizationReadOnlyIntTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DataStreamAuthorizationReadOnlyIntTests.java @@ -15,7 +15,7 @@ import java.util.Collection; import java.util.List; -import org.junit.AfterClass; +import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -228,12 +228,10 @@ static LocalCluster.Builder clusterBuilder() { .indices(index_c1); } - @AfterClass - public static void stopClusters() { - for (ClusterConfig clusterConfig : ClusterConfig.values()) { - clusterConfig.shutdown(); - } - } + @ClassRule + public static final ClusterConfig.ClusterInstances clusterInstances = new ClusterConfig.ClusterInstances( + DataStreamAuthorizationReadOnlyIntTests::clusterBuilder + ); final TestSecurityConfig.User user; final LocalCluster cluster; @@ -875,7 +873,7 @@ public static Collection params() { public DataStreamAuthorizationReadOnlyIntTests(ClusterConfig clusterConfig, TestSecurityConfig.User user, String description) throws Exception { this.user = user; - this.cluster = clusterConfig.cluster(DataStreamAuthorizationReadOnlyIntTests::clusterBuilder); + this.cluster = clusterInstances.get(clusterConfig); this.clusterConfig = clusterConfig; } } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DataStreamAuthorizationReadWriteIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DataStreamAuthorizationReadWriteIntTests.java index a378609ff4..b43111158e 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DataStreamAuthorizationReadWriteIntTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DataStreamAuthorizationReadWriteIntTests.java @@ -19,7 +19,7 @@ import com.google.common.collect.ImmutableList; import org.junit.After; -import org.junit.AfterClass; +import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -420,12 +420,10 @@ static LocalCluster.Builder clusterBuilder() { .plugin(IndexAuthorizationReadOnlyIntTests.SystemIndexTestPlugin.class); } - @AfterClass - public static void stopClusters() { - for (ClusterConfig clusterConfig : ClusterConfig.values()) { - clusterConfig.shutdown(); - } - } + @ClassRule + public static final ClusterConfig.ClusterInstances clusterInstances = new ClusterConfig.ClusterInstances( + DataStreamAuthorizationReadWriteIntTests::clusterBuilder + ); final TestSecurityConfig.User user; final LocalCluster cluster; @@ -601,7 +599,7 @@ public static Collection params() { public DataStreamAuthorizationReadWriteIntTests(ClusterConfig clusterConfig, TestSecurityConfig.User user, String description) throws Exception { this.user = user; - this.cluster = clusterConfig.cluster(DataStreamAuthorizationReadWriteIntTests::clusterBuilder); + this.cluster = clusterInstances.get(clusterConfig); this.clusterConfig = clusterConfig; } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationReadOnlyIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationReadOnlyIntTests.java index 75c35ebbf5..1642398de3 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationReadOnlyIntTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationReadOnlyIntTests.java @@ -19,7 +19,7 @@ import java.util.stream.Stream; import com.google.common.collect.ImmutableList; -import org.junit.AfterClass; +import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -395,12 +395,10 @@ static LocalCluster.Builder clusterBuilder() { .plugin(SystemIndexTestPlugin.class, MustacheModulePlugin.class); } - @AfterClass - public static void stopClusters() { - for (ClusterConfig clusterConfig : ClusterConfig.values()) { - clusterConfig.shutdown(); - } - } + @ClassRule + public static final ClusterConfig.ClusterInstances clusterInstances = new ClusterConfig.ClusterInstances( + IndexAuthorizationReadOnlyIntTests::clusterBuilder + ); final TestSecurityConfig.User user; final LocalCluster cluster; @@ -1878,7 +1876,7 @@ public static Collection params() { public IndexAuthorizationReadOnlyIntTests(ClusterConfig clusterConfig, TestSecurityConfig.User user, String description) throws Exception { this.user = user; - this.cluster = clusterConfig.cluster(IndexAuthorizationReadOnlyIntTests::clusterBuilder); + this.cluster = clusterInstances.get(clusterConfig); this.clusterConfig = clusterConfig; } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationReadWriteIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationReadWriteIntTests.java index 1dbbee4a78..db46b42820 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationReadWriteIntTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationReadWriteIntTests.java @@ -18,7 +18,7 @@ import com.google.common.collect.ImmutableList; import org.junit.After; -import org.junit.AfterClass; +import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -570,12 +570,10 @@ static LocalCluster.Builder clusterBuilder() { .plugin(IndexAuthorizationReadOnlyIntTests.SystemIndexTestPlugin.class); } - @AfterClass - public static void stopClusters() { - for (ClusterConfig clusterConfig : ClusterConfig.values()) { - clusterConfig.shutdown(); - } - } + @ClassRule + public static final ClusterConfig.ClusterInstances clusterInstances = new ClusterConfig.ClusterInstances( + IndexAuthorizationReadWriteIntTests::clusterBuilder + ); final TestSecurityConfig.User user; final LocalCluster cluster; @@ -1157,7 +1155,7 @@ public static Collection params() { public IndexAuthorizationReadWriteIntTests(ClusterConfig clusterConfig, TestSecurityConfig.User user, String description) throws Exception { this.user = user; - this.cluster = clusterConfig.cluster(IndexAuthorizationReadWriteIntTests::clusterBuilder); + this.cluster = clusterInstances.get(clusterConfig); this.clusterConfig = clusterConfig; } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationWithClosedIndicesIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationWithClosedIndicesIntTests.java index e94d377704..eddf0cb69e 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationWithClosedIndicesIntTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationWithClosedIndicesIntTests.java @@ -20,6 +20,7 @@ import org.apache.logging.log4j.Logger; import org.junit.After; import org.junit.Before; +import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -140,9 +141,13 @@ static LocalCluster.Builder clusterBuilder() { .plugin(MustacheModulePlugin.class); } + @ClassRule + public static final ClusterConfig.ClusterInstances clusterInstances = new ClusterConfig.ClusterInstances( + IndexAuthorizationWithClosedIndicesIntTests::clusterBuilder + ); + private final TestSecurityConfig.User user; private final LocalCluster cluster; - private final ClusterConfig clusterConfig; @Parameters(name = "{0}, {2}") public static Collection params() { @@ -159,8 +164,7 @@ public static Collection params() { public IndexAuthorizationWithClosedIndicesIntTests(ClusterConfig clusterConfig, TestSecurityConfig.User user, String description) throws Exception { this.user = user; - this.cluster = clusterConfig.cluster(IndexAuthorizationWithClosedIndicesIntTests::clusterBuilder); - this.clusterConfig = clusterConfig; + this.cluster = clusterInstances.get(clusterConfig); } @Before diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/ProtectedIndexAuthorizationIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/ProtectedIndexAuthorizationIntTests.java new file mode 100644 index 0000000000..2a6e887069 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/ProtectedIndexAuthorizationIntTests.java @@ -0,0 +1,338 @@ +/* + * 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.privileges.int_tests; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableList; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +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.data.TestAlias; +import org.opensearch.test.framework.data.TestIndex; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.cluster.TestRestClient.json; +import static org.opensearch.test.framework.matcher.RestIndexMatchers.IndexMatcher; +import static org.opensearch.test.framework.matcher.RestIndexMatchers.OnResponseIndexMatcher.containsExactly; +import static org.opensearch.test.framework.matcher.RestIndexMatchers.OnUserIndexMatcher.limitedTo; +import static org.opensearch.test.framework.matcher.RestMatchers.isCreated; +import static org.opensearch.test.framework.matcher.RestMatchers.isForbidden; +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; + +/** + * This class tests protected indices functionality with different cluster configurations. + * It uses the following dimensions: + *
    + *
  • ClusterConfig: Tests with protected indices enabled and disabled
  • + *
  • TestSecurityConfig.User: Different users with different protected index role assignments
  • + *
  • Test methods: Different operations (search, create, delete, update, etc.)
  • + *
+ */ +@RunWith(Parameterized.class) +public class ProtectedIndexAuthorizationIntTests { + + // ------------------------------------------------------------------------------------------------------- + // Test indices used by this test suite + // ------------------------------------------------------------------------------------------------------- + + static final TestIndex protected_index1 = TestIndex.name("protected_index1").documentCount(10).seed(1).build(); + static final TestIndex protected_index2 = TestIndex.name("protected_index2").documentCount(10).seed(2).build(); + static final TestIndex unprotected_index = TestIndex.name("unprotected_index").documentCount(10).seed(4).build(); + + static final TestAlias alias_protected = new TestAlias("alias_protected").on(protected_index1); + + static final TestSecurityConfig.User.MetadataKey ALLOWED = new TestSecurityConfig.User.MetadataKey<>( + "allowed", + IndexMatcher.class + ); + + // ------------------------------------------------------------------------------------------------------- + // Test users with different privilege configurations + // ------------------------------------------------------------------------------------------------------- + + static final TestSecurityConfig.Role PROTECTED_INDEX_ROLE = new TestSecurityConfig.Role("protected_index_role"); + + /** + * User with all index permissions but NOT member of protected index roles. + * When protected indices are enabled, they should NOT have access to protected indices. + */ + static final TestSecurityConfig.User NORMAL_USER = new TestSecurityConfig.User("normal_user")// + .description("all_access but no protected role")// + .roles( + new TestSecurityConfig.Role("all_access_role")// + .clusterPermissions("*") + .indexPermissions("*") + .on("*") + )// + .reference(ALLOWED, limitedTo(unprotected_index)); + + /** + * User with all index permissions AND member of protected_index_role. + * When protected indices are enabled, they SHOULD have full access to protected indices. + */ + static final TestSecurityConfig.User PROTECTED_INDEX_USER = new TestSecurityConfig.User("protected_index_user")// + .description("all_access with protected role")// + .roles( + new TestSecurityConfig.Role("all_access_role")// + .clusterPermissions("*") + .indexPermissions("*") + .on("*") + ) + .referencedRoles(PROTECTED_INDEX_ROLE)// + .reference(ALLOWED, limitedTo(protected_index1, protected_index2, unprotected_index)); + + static final List USERS = ImmutableList.of(NORMAL_USER, PROTECTED_INDEX_USER); + + static LocalCluster.Builder clusterBuilder() { + return new LocalCluster.Builder().singleNode() + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USERS) + .indices(protected_index1, protected_index2, unprotected_index) + .aliases(alias_protected) + .roles(PROTECTED_INDEX_ROLE) + .nodeSettings( + Map.of( + "plugins.security.protected_indices.enabled", + true, + "plugins.security.protected_indices.indices", + "protected_index*", + "plugins.security.protected_indices.roles", + "protected_index_role" + ) + ); + } + + @ClassRule + public static final ClusterConfig.ClusterInstances clusterInstances = new ClusterConfig.ClusterInstances( + ProtectedIndexAuthorizationIntTests::clusterBuilder + ); + + final TestSecurityConfig.User user; + final LocalCluster cluster; + final ClusterConfig clusterConfig; + + @Test + public void search_protectedIndex() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + String matchAllQuery = "{\"query\": {\"match_all\": {}}}"; + + TestRestClient.HttpResponse response = restClient.postJson("protected_index1/_search", matchAllQuery); + if (user == PROTECTED_INDEX_USER) { + assertThat(response, isOk()); + assertThat(response, containsExactly(protected_index1).at("hits.hits[*]._index")); + } else if (clusterConfig.legacyPrivilegeEvaluation) { + assertThat(response, isOk()); + assertThat(response, containsExactly().at("hits.hits[*]._index")); + } else { + // Thew new privilege evaluation just forbids this request; this follows the normal index reduction semantics + assertThat(response, isForbidden()); + } + } + } + + @Test + public void search_unprotectedIndex() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + String matchAllQuery = "{\"query\": {\"match_all\": {}}}"; + + TestRestClient.HttpResponse response = restClient.postJson("unprotected_index/_search", matchAllQuery); + assertThat(response, isOk()); + assertThat(response, containsExactly(unprotected_index).at("hits.hits[*]._index")); + } + } + + @Test + public void search_protectedIndexPattern() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + String matchAllQuery = "{\"query\": {\"match_all\": {}}}"; + + TestRestClient.HttpResponse response = restClient.postJson("protected_index*/_search?size=100", matchAllQuery); + + if (user == PROTECTED_INDEX_USER) { + assertThat(response, isOk()); + assertThat(response, containsExactly(protected_index1, protected_index2).at("hits.hits[*]._index")); + } else { + assertThat(response, isOk()); + assertThat(response, containsExactly().at("hits.hits[*]._index")); + } + } + } + + @Test + public void search_aliasContainingProtectedIndices() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + String matchAllQuery = "{\"query\": {\"match_all\": {}}}"; + + TestRestClient.HttpResponse response = restClient.postJson("alias_protected/_search?size=100", matchAllQuery); + + if (user == PROTECTED_INDEX_USER) { + assertThat(response, isOk()); + assertThat(response, containsExactly(protected_index1).at("hits.hits[*]._index")); + } else { + assertThat(response, isOk()); + assertThat(response, containsExactly().at("hits.hits[*]._index")); + } + } + } + + @Test + public void createDocument_protectedIndex() { + String docId = "protected_index1/_doc/create_test_doc"; + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse response = restClient.put(docId, json("foo", "bar")); + if (user == PROTECTED_INDEX_USER) { + assertThat(response, isCreated()); + } else { + assertThat(response, isForbidden()); + } + } finally { + delete(docId); + } + } + + @Test + public void deleteDocument_protectedIndex() { + String docId = "protected_index1/_doc/create_test_doc"; + try (TestRestClient restClient = cluster.getRestClient(user); TestRestClient adminRestClient = cluster.getAdminCertRestClient()) { + + // Initialization: Create document as admin + { + TestRestClient.HttpResponse httpResponse = adminRestClient.put(docId + "?refresh=true", json("foo", "bar")); + assertThat(httpResponse, isCreated()); + } + + TestRestClient.HttpResponse response = restClient.delete(docId); + if (user == PROTECTED_INDEX_USER) { + assertThat(response, isOk()); + } else { + assertThat(response, isForbidden()); + } + } finally { + delete(docId); + } + } + + @Test + public void updateMappings_protectedIndex() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + String newMappings = "{\"properties\": {\"user_name\": {\"type\": \"text\"}}}"; + TestRestClient.HttpResponse response = restClient.putJson("protected_index1/_mapping", newMappings); + if (user == PROTECTED_INDEX_USER) { + assertThat(response, isOk()); + } else { + assertThat(response, isForbidden()); + } + } + } + + @Test + public void closeIndex_protectedIndex() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse response = restClient.post("protected_index2/_close"); + if (user == PROTECTED_INDEX_USER) { + assertThat(response, isOk()); + } else { + assertThat(response, isForbidden()); + } + } finally { + try (TestRestClient adminRestClient = cluster.getAdminCertRestClient()) { + adminRestClient.post("protected_index2/_open"); + } + } + } + + @Test + public void updateSettings_protectedIndex() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + String indexSettings = "{\"index\": {\"refresh_interval\": \"5s\"}}"; + TestRestClient.HttpResponse response = restClient.putJson("protected_index1/_settings", indexSettings); + if (user == PROTECTED_INDEX_USER) { + assertThat(response, isOk()); + } else { + assertThat(response, isForbidden()); + } + } finally { + try (TestRestClient adminRestClient = cluster.getAdminCertRestClient()) { + adminRestClient.putJson("protected_index1/_settings", "{\"index\": {\"refresh_interval\": null}}"); + } + } + } + + @Test + public void aliasOperations_protectedIndex() { + String aliasName = "test_alias_protected"; + try (TestRestClient restClient = cluster.getRestClient(user)) { + String addAliasBody = """ + {"actions": [{"add": {"index": "protected_index1", "alias": "%s"}}]} + """.formatted(aliasName); + + TestRestClient.HttpResponse response = restClient.postJson("_aliases", addAliasBody); + + if (user == PROTECTED_INDEX_USER) { + assertThat(response, isOk()); + } else { + assertThat(response, isForbidden()); + } + } finally { + // Cleanup - remove alias + try (TestRestClient adminRestClient = cluster.getAdminCertRestClient()) { + String removeAliasBody = """ + {"actions": [{"remove": {"index": "protected_index1", "alias": "%s"}}]} + """.formatted(aliasName); + adminRestClient.postJson("_aliases", removeAliasBody); + } + } + } + + @Parameterized.Parameters(name = "{0}, {2}") + public static Collection params() { + List result = new ArrayList<>(); + + for (ClusterConfig clusterConfig : ClusterConfig.values()) { + for (TestSecurityConfig.User user : USERS) { + result.add(new Object[] { clusterConfig, user, user.getDescription() }); + } + } + return result; + } + + public ProtectedIndexAuthorizationIntTests( + ClusterConfig clusterConfig, + TestSecurityConfig.User user, + @SuppressWarnings("unused") String description + ) { + this.user = user; + this.cluster = clusterInstances.get(clusterConfig); + this.clusterConfig = clusterConfig; + } + + private void delete(String... paths) { + try (TestRestClient adminRestClient = cluster.getAdminCertRestClient()) { + for (String path : paths) { + TestRestClient.HttpResponse response = adminRestClient.delete(path); + if (response.getStatusCode() != 200 && response.getStatusCode() != 404) { + throw new RuntimeException("Error while deleting " + path + "\n" + response.getBody()); + } + } + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/SnapshotAuthorizationIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/SnapshotAuthorizationIntTests.java index 3f999cbc69..bf0b4b9e4f 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/SnapshotAuthorizationIntTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/SnapshotAuthorizationIntTests.java @@ -18,7 +18,7 @@ import com.google.common.collect.ImmutableList; import org.apache.hc.core5.http.HttpEntity; import org.junit.After; -import org.junit.AfterClass; +import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -195,12 +195,10 @@ static LocalCluster.Builder clusterBuilder() { .plugin(IndexAuthorizationReadOnlyIntTests.SystemIndexTestPlugin.class); } - @AfterClass - public static void stopClusters() { - for (ClusterConfig clusterConfig : ClusterConfig.values()) { - clusterConfig.shutdown(); - } - } + @ClassRule + public static final ClusterConfig.ClusterInstances clusterInstances = new ClusterConfig.ClusterInstances( + SnapshotAuthorizationIntTests::clusterBuilder + ); final TestSecurityConfig.User user; final LocalCluster cluster; @@ -354,7 +352,7 @@ public static Collection params() { public SnapshotAuthorizationIntTests(ClusterConfig clusterConfig, TestSecurityConfig.User user, String description) throws Exception { this.user = user; - this.cluster = clusterConfig.cluster(SnapshotAuthorizationIntTests::clusterBuilder); + this.cluster = clusterInstances.get(clusterConfig); this.clusterConfig = clusterConfig; } 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 c84601dcc5..1be176ec8c 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -201,6 +201,17 @@ public void close() { } } + /** + * Stops the cluster without throwing an exception in case of an error. + */ + public void stopSafe() { + try { + close(); + } catch (Exception e) { + log.error("Error while stopping LocalCluster", e); + } + }; + @Override public String getClusterName() { return clusterName; From 1f79d790637c34437a2abe823d12f647c675e862 Mon Sep 17 00:00:00 2001 From: Nils Bandener <33570290+nibix@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:56:12 +0100 Subject: [PATCH 17/17] Reduce log spam caused by TlsHostnameVerificationTests (#5868) Signed-off-by: Nils Bandener Co-authored-by: Shikhar Jain <8859327+shikharj05@users.noreply.github.com> --- .../TlsHostnameVerificationTests.java | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/TlsHostnameVerificationTests.java b/src/integrationTest/java/org/opensearch/security/TlsHostnameVerificationTests.java index 3316888029..6b3768834f 100644 --- a/src/integrationTest/java/org/opensearch/security/TlsHostnameVerificationTests.java +++ b/src/integrationTest/java/org/opensearch/security/TlsHostnameVerificationTests.java @@ -10,48 +10,66 @@ package org.opensearch.security; import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; -import org.junit.Assert; import org.junit.Rule; import org.junit.Test; -import org.opensearch.security.support.ConfigConstants; import org.opensearch.test.framework.certificate.TestCertificates; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.log.LogCapturingAppender; import org.opensearch.test.framework.log.LogsRule; -import static org.opensearch.common.network.NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY; +import static java.util.concurrent.Executors.newSingleThreadExecutor; +import static org.awaitility.Awaitility.await; public class TlsHostnameVerificationTests { @Rule public LogsRule logsRule = new LogsRule("org.opensearch.transport.netty4.ssl.SecureNetty4Transport"); - public LocalCluster.Builder clusterBuilder = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + public LocalCluster.Builder clusterBuilder = new LocalCluster.Builder().clusterManager(ClusterManager.DEFAULT) .anonymousAuth(false) .loadConfigurationIntoIndex(false) - .nodeSettings(Map.of(ConfigConstants.SECURITY_SSL_ONLY, true, TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY, true)) + .nodeSettings( + Map.of("plugins.security.ssl_only", true, "transport.ssl.enforce_hostname_verification", true, "cluster.join.timeout", "10s") + ) .sslOnly(true); @Test public void clusterShouldStart_nodesSanIpsAreValid() { // Note: We cannot use hostnames in this environment. However, IP addresses also work as valid SANs which are also // subject to hostname verification. Thus, we use here certificates with IP SANs - TestCertificates testCertificates = new TestCertificates(ClusterManager.THREE_CLUSTER_MANAGERS.getNodes(), "127.0.0.1"); + TestCertificates testCertificates = new TestCertificates(ClusterManager.DEFAULT.getNodes(), "127.0.0.1"); try (LocalCluster cluster = clusterBuilder.testCertificates(testCertificates).build()) { cluster.before(); - } catch (Exception e) { - Assert.fail("Cluster should start, no exception expected but got: " + e.getMessage()); } } @Test public void clusterShouldNotStart_nodesSanIpsAreInvalid() { - TestCertificates testCertificates = new TestCertificates(ClusterManager.THREE_CLUSTER_MANAGERS.getNodes(), "127.0.0.2"); - try (LocalCluster cluster = clusterBuilder.testCertificates(testCertificates).build()) { - cluster.before(); - Assert.fail("Cluster should not start, an exception expected"); + TestCertificates testCertificates = new TestCertificates(ClusterManager.DEFAULT.getNodes(), "127.0.0.2"); + try ( + LocalCluster cluster = clusterBuilder.testCertificates(testCertificates).build(); + ExecutorService executorService = newSingleThreadExecutor() + ) { + Future clusterFuture = executorService.submit(() -> { + cluster.before(); + return null; + }); + await().alias("expect error message about hostname verification") + .pollDelay(10, TimeUnit.MILLISECONDS) + .until( + () -> LogCapturingAppender.getLogMessagesAsString() + .stream() + .anyMatch( + message -> message.contains("(certificate_unknown) No subject alternative names matching IP address 127.0.0.1") + ) + ); + clusterFuture.cancel(true); } catch (Exception e) { logsRule.assertThatContain("No subject alternative names matching IP address 127.0.0.1 found"); }