diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3b3a442b83..ffa2e33444 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @cliu123 @cwperks @DarshitChanpura @peternied @RyanL1997 @scrawfor99 @reta @willyborankin +* @cwperks @DarshitChanpura @nibix @peternied @RyanL1997 @stephen-crawford @reta @willyborankin diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e5aa40ec2e..088aa4b4e6 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,7 +7,7 @@ ### Issues Resolved [List any issues this PR will resolve] -Is this a backport? If so, please add backport PR # and/or commits # +Is this a backport? If so, please add backport PR # and/or commits #, and remove `backport-failed` label from the original PR. Do these changes introduce new permission(s) to be displayed in the static dropdown on the front-end? If so, please open a draft PR in the security dashboards plugin and link the draft PR here diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 472f32e886..b9d1022757 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,8 +86,9 @@ jobs: attempt_limit: 5 attempt_delay: 2000 action: codecov/codecov-action@v4 + env: | + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: | - token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true verbose: true diff --git a/MAINTAINERS.md b/MAINTAINERS.md index bafadc11bd..be4a41dc03 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -13,21 +13,22 @@ This document contains a list of maintainers in this repo. See [opensearch-proje ## Current Maintainers | Maintainer | GitHub ID | Affiliation | -| ---------------- | ----------------------------------------------------- | ----------- | -| Chang Liu | [cliu123](https://github.com/cliu123) | Amazon | +|------------------|-------------------------------------------------------|-------------| | Darshit Chanpura | [DarshitChanpura](https://github.com/DarshitChanpura) | Amazon | | Peter Nied | [peternied](https://github.com/peternied) | Amazon | | Craig Perkins | [cwperks](https://github.com/cwperks) | Amazon | | Ryan Liang | [RyanL1997](https://github.com/RyanL1997) | Amazon | -| Stephen Crawford | [scrawfor99](https://github.com/scrawfor99) | Amazon | +| Stephen Crawford | [scrawfor99](https://github.com/stephen-crawford) | Amazon | | Andriy Redko | [reta](https://github.com/reta) | Aiven | | Andrey Pleskach | [willyborankin](https://github.com/willyborankin) | Aiven | +| Nils Bandener | [nibix](https://github.com/nibix) | Eliatra | ## Emeritus -| Maintainer | GitHub ID | Affiliation | -| ------------- | --------------------------------------------------- | ----------- | -| Dave Lago | [davidlago](https://github.com/davidlago) | Contributor | +| Maintainer | GitHub ID | Affiliation | +|------------|-------------------------------------------|-------------| +| Dave Lago | [davidlago](https://github.com/davidlago) | Contributor | +| Chang Liu | [cliu123](https://github.com/cliu123) | Amazon | ## Practices diff --git a/build.gradle b/build.gradle index 76b95df7eb..19829a4a76 100644 --- a/build.gradle +++ b/build.gradle @@ -27,13 +27,12 @@ buildscript { common_utils_version = System.getProperty("common_utils.version", '3.0.0.0-SNAPSHOT') kafka_version = '3.7.1' - apache_cxf_version = '4.0.4' open_saml_version = '4.3.2' one_login_java_saml = '2.9.0' jjwt_version = '0.12.6' guava_version = '32.1.3-jre' jaxb_version = '2.3.9' - spring_version = '5.3.37' + spring_version = '5.3.39' if (buildVersionQualifier) { opensearch_build += "-${buildVersionQualifier}" @@ -65,8 +64,8 @@ plugins { id 'maven-publish' id 'com.diffplug.spotless' version '6.25.0' id 'checkstyle' - id 'com.netflix.nebula.ospackage' version "11.9.1" - id "org.gradle.test-retry" version "1.5.9" + id 'com.netflix.nebula.ospackage' version "11.10.0" + id "org.gradle.test-retry" version "1.5.10" id 'eclipse' id "com.github.spotbugs" version "5.2.5" id "com.google.osdetector" version "1.7.3" @@ -470,7 +469,7 @@ bundlePlugin { configurations { all { resolutionStrategy { - force 'commons-codec:commons-codec:1.17.0' + force 'commons-codec:commons-codec:1.17.1' force 'org.slf4j:slf4j-api:1.7.36' force 'org.scala-lang:scala-library:2.13.14' force "com.fasterxml.jackson:jackson-bom:${versions.jackson}" @@ -483,7 +482,7 @@ configurations { force "io.netty:netty-transport:${versions.netty}" force "io.netty:netty-transport-native-unix-common:${versions.netty}" force "com.github.luben:zstd-jni:${versions.zstd}" - force "org.xerial.snappy:snappy-java:1.1.10.5" + force "org.xerial.snappy:snappy-java:1.1.10.6" force "com.google.guava:guava:${guava_version}" // for spotbugs dependency conflict @@ -496,9 +495,9 @@ configurations { // For integrationTest force "org.apache.httpcomponents:httpclient:4.5.14" force "org.apache.httpcomponents:httpcore:4.4.16" - force "com.google.errorprone:error_prone_annotations:2.28.0" - force "org.checkerframework:checker-qual:3.45.0" - force "ch.qos.logback:logback-classic:1.5.6" + force "com.google.errorprone:error_prone_annotations:2.30.0" + force "org.checkerframework:checker-qual:3.46.0" + force "ch.qos.logback:logback-classic:1.5.7" } } @@ -578,7 +577,7 @@ dependencies { implementation "org.apache.httpcomponents:httpasyncclient:${versions.httpasyncclient}" implementation "com.google.guava:guava:${guava_version}" implementation 'org.greenrobot:eventbus-java:3.3.1' - implementation 'commons-cli:commons-cli:1.8.0' + implementation 'commons-cli:commons-cli:1.9.0' implementation "org.bouncycastle:bcprov-jdk18on:${versions.bouncycastle}" implementation 'org.ldaptive:ldaptive:1.2.3' implementation 'com.nimbusds:nimbus-jose-jwt:9.40' @@ -593,21 +592,17 @@ dependencies { implementation 'org.apache.commons:commons-collections4:4.4' //Password generation - implementation 'org.passay:passay:1.6.4' + implementation 'org.passay:passay:1.6.5' implementation "org.apache.kafka:kafka-clients:${kafka_version}" runtimeOnly 'net.minidev:accessors-smart:2.5.1' - runtimeOnly "org.apache.cxf:cxf-core:${apache_cxf_version}" - implementation "org.apache.cxf:cxf-rt-rs-json-basic:${apache_cxf_version}" - runtimeOnly "org.apache.cxf:cxf-rt-security:${apache_cxf_version}" - runtimeOnly 'com.sun.activation:jakarta.activation:1.2.2' runtimeOnly 'com.eclipsesource.minimal-json:minimal-json:0.9.5' - runtimeOnly 'commons-codec:commons-codec:1.17.0' - runtimeOnly 'org.cryptacular:cryptacular:1.2.6' - compileOnly 'com.google.errorprone:error_prone_annotations:2.28.0' + runtimeOnly 'commons-codec:commons-codec:1.17.1' + runtimeOnly 'org.cryptacular:cryptacular:1.2.7' + compileOnly 'com.google.errorprone:error_prone_annotations:2.30.0' runtimeOnly 'com.sun.istack:istack-commons-runtime:4.2.0' runtimeOnly 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.2' runtimeOnly 'org.ow2.asm:asm:9.7' @@ -616,7 +611,7 @@ dependencies { //OpenSAML implementation 'net.shibboleth.utilities:java-support:8.4.2' - runtimeOnly "io.dropwizard.metrics:metrics-core:4.2.26" + runtimeOnly "io.dropwizard.metrics:metrics-core:4.2.27" 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}" @@ -644,14 +639,14 @@ dependencies { runtimeOnly 'org.lz4:lz4-java:1.8.0' 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.xerial.snappy:snappy-java:1.1.10.6' runtimeOnly 'org.codehaus.woodstox:stax2-api:4.2.2' runtimeOnly "org.glassfish.jaxb:txw2:${jaxb_version}" runtimeOnly 'com.fasterxml.woodstox:woodstox-core:6.7.0' 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}" - runtimeOnly 'org.checkerframework:checker-qual:3.45.0' + runtimeOnly 'org.checkerframework:checker-qual:3.46.0' runtimeOnly "org.bouncycastle:bcpkix-jdk18on:${versions.bouncycastle}" runtimeOnly 'org.scala-lang.modules:scala-java8-compat_3:1.0.2' @@ -684,9 +679,9 @@ dependencies { testImplementation 'commons-validator:commons-validator:1.9.0' testImplementation 'org.springframework.kafka:spring-kafka-test:2.9.13' testImplementation "org.springframework:spring-beans:${spring_version}" - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.3' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.3' - testImplementation('org.awaitility:awaitility:4.2.1') { + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.0' + testImplementation('org.awaitility:awaitility:4.2.2') { exclude(group: 'org.hamcrest', module: 'hamcrest') } // Only osx-x86_64, osx-aarch_64, linux-x86_64, linux-aarch_64, windows-x86_64 are available @@ -729,7 +724,7 @@ dependencies { integrationTestImplementation 'org.hamcrest:hamcrest:2.2' integrationTestImplementation "org.bouncycastle:bcpkix-jdk18on:${versions.bouncycastle}" integrationTestImplementation "org.bouncycastle:bcutil-jdk18on:${versions.bouncycastle}" - integrationTestImplementation('org.awaitility:awaitility:4.2.1') { + integrationTestImplementation('org.awaitility:awaitility:4.2.2') { exclude(group: 'org.hamcrest', module: 'hamcrest') } integrationTestImplementation 'com.unboundid:unboundid-ldapsdk:4.0.14' @@ -741,7 +736,7 @@ dependencies { integrationTestImplementation "org.apache.httpcomponents:httpasyncclient:4.1.5" //spotless - implementation('com.google.googlejavaformat:google-java-format:1.22.0') { + implementation('com.google.googlejavaformat:google-java-format:1.23.0') { exclude group: 'com.google.guava' } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd49177..a4b76b9530 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8a1f6b97f4..2b189974c2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=a4b4158601f8636cdeeab09bd76afb640030bb5b144aafe261a5e8af027dc612 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionSha256Sum=5b9c5eb3f9fc2c94abaea57d90bd78747ca117ddbbf96c859d3741181a12bf2a +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a4269..f5feea6d6b 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 7101f8e467..9b42019c79 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## diff --git a/release-notes/opensearch-security.release-notes-1.3.18.0.md b/release-notes/opensearch-security.release-notes-1.3.18.0.md index e717a8e2fd..43ef02fcef 100644 --- a/release-notes/opensearch-security.release-notes-1.3.18.0.md +++ b/release-notes/opensearch-security.release-notes-1.3.18.0.md @@ -5,3 +5,4 @@ Compatible with OpenSearch 1.3.18 ### Maintenance * Bump bouncycastle to 1.78.1 and kafka to 3.7.0 ([#4437](https://github.com/opensearch-project/security/pull/4437)) +* Bump jose to address CVE ([#4549](https://github.com/opensearch-project/security/pull/4549)) diff --git a/release-notes/opensearch-security.release-notes-2.16.0.0.md b/release-notes/opensearch-security.release-notes-2.16.0.0.md new file mode 100644 index 0000000000..e35300f061 --- /dev/null +++ b/release-notes/opensearch-security.release-notes-2.16.0.0.md @@ -0,0 +1,41 @@ +## Version 2.16.0 Release Notes + +Compatible with OpenSearch and OpenSearch Dashboards version 2.16.0 + +### Enhancements +* Add support for PBKDF2 for password hashing & add support for configuring BCrypt and PBKDF2 ([#4524](https://github.com/opensearch-project/security/pull/4524)) +* Use SystemIndexRegistry from core to determine if request contains system indices ([#4471](https://github.com/opensearch-project/security/pull/4471)) +* Separated DLS/FLS privilege evaluation from action privilege evaluation ([#4490](https://github.com/opensearch-project/security/pull/4490)) +* Update PULL_REQUEST_TEMPLATE to include an API spec change in the checklist. ([#4533](https://github.com/opensearch-project/security/pull/4533)) +* Update PATCH API to fail validation if nothing changes ([#4530](https://github.com/opensearch-project/security/pull/4530)) +* Refactor InternalUsers REST API test ([#4481](https://github.com/opensearch-project/security/pull/4481)) +* Refactor Role Mappings REST API test ([#4450](https://github.com/opensearch-project/security/pull/4450)) +* Remove special handling for do_not_fail_on_forbidden on cluster actions ([#4486](https://github.com/opensearch-project/security/pull/4486)) +* Add Tenants REST API test and partial fix ([#4166](https://github.com/opensearch-project/security/pull/4166)) +* Refactor Roles REST API test and partial fix #4166 ([#4433](https://github.com/opensearch-project/security/pull/4433)) +* New algorithm for resolving action groups ([#4448](https://github.com/opensearch-project/security/pull/4448)) +* Check block request only if system index ([#4430](https://github.com/opensearch-project/security/pull/4430)) +* Replaced uses of SecurityRoles by Set mappedRoles where the SecurityRoles functionality is not needed ([#4432](https://github.com/opensearch-project/security/pull/4432)) + +### Bug Fixes +* Fixed test failures in FlsAndFieldMaskingTests ([#4548](https://github.com/opensearch-project/security/pull/4548)) +* Typo in securityadmin.sh hint ([#4526](https://github.com/opensearch-project/security/pull/4526)) +* Fix NPE getting metaFields from mapperService on a close index request ([#4497](https://github.com/opensearch-project/security/pull/4497)) +* Fixes flaky integration tests ([#4452](https://github.com/opensearch-project/security/pull/4452)) + +### Maintenance +* Remove unused dependancy Apache CXF ([#4580](https://github.com/opensearch-project/security/pull/4580)) +* Remove unnecessary return statements ([#4558](https://github.com/opensearch-project/security/pull/4558)) +* Pass set to SystemIndexRegistry.matchesSystemIndexPattern ([#4569](https://github.com/opensearch-project/security/pull/4569)) +* Refactor and update existing ml roles ([#4151](https://github.com/opensearch-project/security/pull/4151)) +* Replace JUnit assertEquals() with Hamcrest matchers assertThat() ([#4544](https://github.com/opensearch-project/security/pull/4544)) +* Update Gradle to 8.9 ([#4553](https://github.com/opensearch-project/security/pull/4553)) +* Bump org.checkerframework:checker-qual from 3.44.0 to 3.45.0 ([#4531](https://github.com/opensearch-project/security/pull/4531)) +* Add security analytics threat intel action ([#4498](https://github.com/opensearch-project/security/pull/4498)) +* Bump kafka_version from 3.7.0 to 3.7.1 ([#4501](https://github.com/opensearch-project/security/pull/4501)) +* Bump org.junit.jupiter:junit-jupiter from 5.10.2 to 5.10.3 ([#4503](https://github.com/opensearch-project/security/pull/4503)) +* Bump com.fasterxml.woodstox:woodstox-core from 6.6.2 to 6.7.0 ([#4483](https://github.com/opensearch-project/security/pull/4483)) +* Bump jjwt_version from 0.12.5 to 0.12.6 ([#4484](https://github.com/opensearch-project/security/pull/4484)) +* Bump org.eclipse.platform:org.eclipse.core.runtime from 3.31.0 to 3.3.1.100 ([#4467](https://github.com/opensearch-project/security/pull/4467)) +* Bump spring_version from 5.3.36 to 5.3.37 ([#4466](https://github.com/opensearch-project/security/pull/4466)) +* Update to Gradle 8.8 ([#4459](https://github.com/opensearch-project/security/pull/4459)) diff --git a/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java index 31f320a654..45ce33a346 100644 --- a/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java +++ b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java @@ -9,6 +9,8 @@ */ package org.opensearch.security; +import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; @@ -28,6 +30,10 @@ import static org.apache.http.HttpStatus.SC_OK; import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.opensearch.security.api.AbstractApiIntegrationTest.configJsonArray; +import static org.opensearch.security.api.PatchPayloadHelper.patch; +import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; +import static org.opensearch.security.support.ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; import static org.opensearch.test.framework.cluster.TestRestClientConfiguration.userWithSourceIp; @@ -40,6 +46,7 @@ public class IpBruteForceAttacksPreventionTests { public static final int ALLOWED_TRIES = 3; public static final int TIME_WINDOW_SECONDS = 3; + public static final int BLOCK_SECONDS = 5; public static final String CLIENT_IP_2 = "127.0.0.2"; public static final String CLIENT_IP_3 = "127.0.0.3"; @@ -49,14 +56,18 @@ public class IpBruteForceAttacksPreventionTests { public static final String CLIENT_IP_7 = "127.0.0.7"; public static final String CLIENT_IP_8 = "127.0.0.8"; public static final String CLIENT_IP_9 = "127.0.0.9"; + public static final String CLIENT_IP_10 = "127.0.0.10"; + public static final String CLIENT_IP_11 = "127.0.0.11"; + public static final String CLIENT_IP_12 = "127.0.0.12"; protected static final AuthFailureListeners listener = new AuthFailureListeners().addRateLimit( - new RateLimiting("internal_authentication_backend_limiting").type("ip") + new RateLimiting("ip_rate_limiting").type("ip") .allowedTries(ALLOWED_TRIES) .timeWindowSeconds(TIME_WINDOW_SECONDS) - .blockExpirySeconds(2) + .blockExpirySeconds(BLOCK_SECONDS) .maxBlockedClients(500) .maxTrackedClients(500) + .ignoreHosts(List.of(CLIENT_IP_10)) ); @Rule @@ -68,6 +79,7 @@ public LocalCluster createCluster() { .authFailureListeners(listener) .authc(AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE) .users(USER_1, USER_2) + .nodeSettings(Map.of(SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true)) .build(); } @@ -84,6 +96,48 @@ public void shouldAuthenticateUserWhenBlockadeIsNotActive() { } } + @Test + public void shouldAllowIpAddressIfMatchesIgnoreHost() { + authenticateUserWithIncorrectPassword(CLIENT_IP_10, USER_2, ALLOWED_TRIES); + try (TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_2, CLIENT_IP_10))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_OK); + } + + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + HttpResponse patchResponse = client.patch( + "_plugins/_security/api/securityconfig", + patch( + replaceOp( + "/config/dynamic/auth_failure_listeners/ip_rate_limiting/ignore_hosts", + configJsonArray(CLIENT_IP_10, CLIENT_IP_11) + ) + ) + ); + patchResponse.assertStatusCode(SC_OK); + } + + authenticateUserWithIncorrectPassword(CLIENT_IP_11, USER_1, ALLOWED_TRIES); + try (TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_11))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_OK); + } + + // Verify other ip addresses are still blocked + authenticateUserWithIncorrectPassword(CLIENT_IP_12, USER_1, ALLOWED_TRIES); + try (TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_12))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_UNAUTHORIZED); + logsRule.assertThatContain("Rejecting REST request because of blocked address: /" + CLIENT_IP_12); + } + } + @Test public void shouldBlockIpAddress() { authenticateUserWithIncorrectPassword(CLIENT_IP_3, USER_2, ALLOWED_TRIES); @@ -144,7 +198,7 @@ public void shouldBlockIpWhenFailureAuthenticationCountIsGreaterThanAllowedTries @Test public void shouldReleaseIpAddressLock() throws InterruptedException { authenticateUserWithIncorrectPassword(CLIENT_IP_9, USER_1, ALLOWED_TRIES * 2); - TimeUnit.SECONDS.sleep(TIME_WINDOW_SECONDS); + TimeUnit.SECONDS.sleep(BLOCK_SECONDS); try (TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_9))) { HttpResponse response = client.getAuthInfo(); diff --git a/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionWithDomainChallengeTests.java b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionWithDomainChallengeTests.java index cd2c577d17..61d5a651b8 100644 --- a/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionWithDomainChallengeTests.java +++ b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionWithDomainChallengeTests.java @@ -10,12 +10,15 @@ package org.opensearch.security; +import java.util.Map; + import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.junit.runner.RunWith; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; +import static org.opensearch.security.support.ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @@ -28,6 +31,7 @@ public LocalCluster createCluster() { .authFailureListeners(listener) .authc(AUTHC_HTTPBASIC_INTERNAL) .users(USER_1, USER_2) + .nodeSettings(Map.of(SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true)) .build(); } } diff --git a/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java index 40aabeb7b2..297eeb38f9 100644 --- a/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java @@ -394,7 +394,7 @@ void assertResponseBody(final String responseBody, final String expectedMessage) assertThat(responseBody, containsString(expectedMessage)); } - static ToXContentObject configJsonArray(final String... values) { + public static ToXContentObject configJsonArray(final String... values) { return (builder, params) -> { builder.startArray(); if (values != null) { diff --git a/src/integrationTest/java/org/opensearch/security/api/CertificatesRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/CertificatesRestApiIntegrationTest.java index ebac5b80a5..8a69406bff 100644 --- a/src/integrationTest/java/org/opensearch/security/api/CertificatesRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/CertificatesRestApiIntegrationTest.java @@ -90,6 +90,17 @@ public void availableForRestAdmin() throws Exception { withUser(REST_API_ADMIN_SSL_INFO, this::verifySSLCertsInfo); } + @Test + public void timeoutTest() throws Exception { + withUser(REST_ADMIN_USER, this::verifyTimeoutRequest); + } + + private void verifyTimeoutRequest(final TestRestClient client) throws Exception { + TestRestClient.HttpResponse response = ok(() -> client.get(sslCertsPath() + "?timeout=0")); + final var body = response.bodyAsJsonNode(); + assertThat(body.get("nodes").size(), is(0)); + } + private void verifySSLCertsInfo(final TestRestClient client) throws Exception { assertSSLCertsInfo( localCluster.nodes(), diff --git a/src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java index 1ea18c4363..18769949fe 100644 --- a/src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java @@ -440,6 +440,18 @@ void assertFilterByUsers(final HttpResponse response, final boolean hasServiceUs assertThat(response.getBody(), response.bodyAsJsonNode().has(NEW_USER), is(hasInternalUser)); } + @Test + public void verifyPOSTOnlyForAuthTokenEndpoint() throws Exception { + withUser(ADMIN_USER_NAME, client -> { + badRequest(() -> client.post(apiPath(ADMIN_USER_NAME, "authtoken"))); + ok(() -> client.post(apiPath(SERVICE_ACCOUNT_USER, "authtoken"))); + /* + should be notImplement but the call doesn't reach {@link org.opensearch.security.dlic.rest.api.InternalUsersApiAction#withAuthTokenPath(RestRequest)} + */ + methodNotAllowed(() -> client.post(apiPath("randomPath"))); + }); + } + @Test public void userApiWithDotsInName() throws Exception { withUser(ADMIN_USER_NAME, client -> { diff --git a/src/integrationTest/java/org/opensearch/security/api/PatchPayloadHelper.java b/src/integrationTest/java/org/opensearch/security/api/PatchPayloadHelper.java index d7bae323bd..9e4b470402 100644 --- a/src/integrationTest/java/org/opensearch/security/api/PatchPayloadHelper.java +++ b/src/integrationTest/java/org/opensearch/security/api/PatchPayloadHelper.java @@ -15,7 +15,7 @@ import org.opensearch.core.xcontent.ToXContentObject; -interface PatchPayloadHelper extends ToXContentObject { +public interface PatchPayloadHelper extends ToXContentObject { enum Op { ADD, diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java index 659d7c178e..72aa2ef941 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java @@ -90,15 +90,25 @@ public class JwtAuthenticationTests { public static final String QA_SONG_INDEX_NAME = String.format("song_lyrics_%s", QA_DEPARTMENT); - private static final KeyPair KEY_PAIR = Keys.keyPairFor(SignatureAlgorithm.RS256); - private static final String PUBLIC_KEY = new String(Base64.getEncoder().encode(KEY_PAIR.getPublic().getEncoded()), US_ASCII); + private static final KeyPair KEY_PAIR1 = Keys.keyPairFor(SignatureAlgorithm.RS256); + private static final String PUBLIC_KEY1 = new String(Base64.getEncoder().encode(KEY_PAIR1.getPublic().getEncoded()), US_ASCII); + + private static final KeyPair KEY_PAIR2 = Keys.keyPairFor(SignatureAlgorithm.RS256); + private static final String PUBLIC_KEY2 = new String(Base64.getEncoder().encode(KEY_PAIR2.getPublic().getEncoded()), US_ASCII); static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); private static final String JWT_AUTH_HEADER = "jwt-auth"; - private static final JwtAuthorizationHeaderFactory tokenFactory = new JwtAuthorizationHeaderFactory( - KEY_PAIR.getPrivate(), + private static final JwtAuthorizationHeaderFactory tokenFactory1 = new JwtAuthorizationHeaderFactory( + KEY_PAIR1.getPrivate(), + CLAIM_USERNAME, + CLAIM_ROLES, + JWT_AUTH_HEADER + ); + + private static final JwtAuthorizationHeaderFactory tokenFactory2 = new JwtAuthorizationHeaderFactory( + KEY_PAIR2.getPrivate(), CLAIM_USERNAME, CLAIM_ROLES, JWT_AUTH_HEADER @@ -108,7 +118,10 @@ public class JwtAuthenticationTests { "jwt", BASIC_AUTH_DOMAIN_ORDER - 1 ).jwtHttpAuthenticator( - new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER).signingKey(PUBLIC_KEY).subjectKey(CLAIM_USERNAME).rolesKey(CLAIM_ROLES) + new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER) + .signingKey(List.of(PUBLIC_KEY1, PUBLIC_KEY2)) + .subjectKey(CLAIM_USERNAME) + .rolesKey(CLAIM_ROLES) ).backend("noop"); public static final String SONG_ID_1 = "song-id-01"; @@ -143,7 +156,7 @@ public static void createTestData() { @Test public void shouldAuthenticateWithJwtToken_positive() { - try (TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USER_SUPERHERO))) { + try (TestRestClient client = cluster.getRestClient(tokenFactory1.generateValidToken(USER_SUPERHERO))) { HttpResponse response = client.getAuthInfo(); @@ -155,7 +168,7 @@ public void shouldAuthenticateWithJwtToken_positive() { @Test public void shouldAuthenticateWithJwtToken_positiveWithAnotherUsername() { - try (TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USERNAME_ROOT))) { + try (TestRestClient client = cluster.getRestClient(tokenFactory1.generateValidToken(USERNAME_ROOT))) { HttpResponse response = client.getAuthInfo(); @@ -167,7 +180,7 @@ public void shouldAuthenticateWithJwtToken_positiveWithAnotherUsername() { @Test public void shouldAuthenticateWithJwtToken_failureLackingUserName() { - try (TestRestClient client = cluster.getRestClient(tokenFactory.generateTokenWithoutPreferredUsername(USER_SUPERHERO))) { + try (TestRestClient client = cluster.getRestClient(tokenFactory1.generateTokenWithoutPreferredUsername(USER_SUPERHERO))) { HttpResponse response = client.getAuthInfo(); @@ -178,7 +191,7 @@ public void shouldAuthenticateWithJwtToken_failureLackingUserName() { @Test public void shouldAuthenticateWithJwtToken_failureExpiredToken() { - try (TestRestClient client = cluster.getRestClient(tokenFactory.generateExpiredToken(USER_SUPERHERO))) { + try (TestRestClient client = cluster.getRestClient(tokenFactory1.generateExpiredToken(USER_SUPERHERO))) { HttpResponse response = client.getAuthInfo(); @@ -202,7 +215,7 @@ public void shouldAuthenticateWithJwtToken_failureIncorrectFormatOfToken() { @Test public void shouldAuthenticateWithJwtToken_failureIncorrectSignature() { KeyPair incorrectKeyPair = Keys.keyPairFor(SignatureAlgorithm.RS256); - Header header = tokenFactory.generateTokenSignedWithKey(incorrectKeyPair.getPrivate(), USER_SUPERHERO); + Header header = tokenFactory1.generateTokenSignedWithKey(incorrectKeyPair.getPrivate(), USER_SUPERHERO); try (TestRestClient client = cluster.getRestClient(header)) { HttpResponse response = client.getAuthInfo(); @@ -214,7 +227,7 @@ public void shouldAuthenticateWithJwtToken_failureIncorrectSignature() { @Test public void shouldReadRolesFromToken_positiveFirstRoleSet() { - Header header = tokenFactory.generateValidToken(USER_SUPERHERO, ROLE_ADMIN, ROLE_DEVELOPER, ROLE_QA); + Header header = tokenFactory1.generateValidToken(USER_SUPERHERO, ROLE_ADMIN, ROLE_DEVELOPER, ROLE_QA); try (TestRestClient client = cluster.getRestClient(header)) { HttpResponse response = client.getAuthInfo(); @@ -228,7 +241,7 @@ public void shouldReadRolesFromToken_positiveFirstRoleSet() { @Test public void shouldReadRolesFromToken_positiveSecondRoleSet() { - Header header = tokenFactory.generateValidToken(USER_SUPERHERO, ROLE_CTO, ROLE_CEO, ROLE_VP); + Header header = tokenFactory1.generateValidToken(USER_SUPERHERO, ROLE_CTO, ROLE_CEO, ROLE_VP); try (TestRestClient client = cluster.getRestClient(header)) { HttpResponse response = client.getAuthInfo(); @@ -244,7 +257,7 @@ public void shouldReadRolesFromToken_positiveSecondRoleSet() { public void shouldExposeTokenClaimsAsUserAttributes_positive() throws IOException { String[] roles = { ROLE_VP }; Map additionalClaims = Map.of(CLAIM_DEPARTMENT, QA_DEPARTMENT); - Header header = tokenFactory.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); + Header header = tokenFactory1.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); try (RestHighLevelClient client = cluster.getRestHighLevelClient(List.of(header))) { SearchRequest searchRequest = queryStringQueryRequest(QA_SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS); @@ -261,11 +274,36 @@ public void shouldExposeTokenClaimsAsUserAttributes_positive() throws IOExceptio public void shouldExposeTokenClaimsAsUserAttributes_negative() throws IOException { String[] roles = { ROLE_VP }; Map additionalClaims = Map.of(CLAIM_DEPARTMENT, "department-without-access-to-qa-song-index"); - Header header = tokenFactory.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); + Header header = tokenFactory1.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); try (RestHighLevelClient client = cluster.getRestHighLevelClient(List.of(header))) { SearchRequest searchRequest = queryStringQueryRequest(QA_SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS); assertThatThrownBy(() -> client.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); } } + + @Test + public void secondKeypairShouldAuthenticateWithJwtToken_positive() { + try (TestRestClient client = cluster.getRestClient(tokenFactory2.generateValidToken(USER_SUPERHERO))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USER_SUPERHERO)); + } + } + + @Test + public void secondKeypairShouldAuthenticateWithJwtToken_positiveWithAnotherUsername() { + try (TestRestClient client = cluster.getRestClient(tokenFactory2.generateValidToken(USERNAME_ROOT))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USERNAME_ROOT)); + } + } + } diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationWithUrlParamTests.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationWithUrlParamTests.java index e10ad82e8c..43a342dcfd 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationWithUrlParamTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationWithUrlParamTests.java @@ -72,7 +72,10 @@ public class JwtAuthenticationWithUrlParamTests { "jwt", BASIC_AUTH_DOMAIN_ORDER - 1 ).jwtHttpAuthenticator( - new JwtConfigBuilder().jwtUrlParameter(TOKEN_URL_PARAM).signingKey(PUBLIC_KEY).subjectKey(CLAIM_USERNAME).rolesKey(CLAIM_ROLES) + new JwtConfigBuilder().jwtUrlParameter(TOKEN_URL_PARAM) + .signingKey(List.of(PUBLIC_KEY)) + .subjectKey(CLAIM_USERNAME) + .rolesKey(CLAIM_ROLES) ).backend("noop"); @Rule diff --git a/src/integrationTest/java/org/opensearch/test/framework/JwtConfigBuilder.java b/src/integrationTest/java/org/opensearch/test/framework/JwtConfigBuilder.java index 88297bacd2..f5984bad8f 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/JwtConfigBuilder.java +++ b/src/integrationTest/java/org/opensearch/test/framework/JwtConfigBuilder.java @@ -9,6 +9,7 @@ */ package org.opensearch.test.framework; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -19,7 +20,7 @@ public class JwtConfigBuilder { private String jwtHeader; private String jwtUrlParameter; - private String signingKey; + private List signingKeys; private String subjectKey; private String rolesKey; @@ -33,8 +34,8 @@ public JwtConfigBuilder jwtUrlParameter(String jwtUrlParameter) { return this; } - public JwtConfigBuilder signingKey(String signingKey) { - this.signingKey = signingKey; + public JwtConfigBuilder signingKey(List signingKeys) { + this.signingKeys = signingKeys; return this; } @@ -50,10 +51,10 @@ public JwtConfigBuilder rolesKey(String rolesKey) { public Map build() { Builder builder = new Builder<>(); - if (Objects.isNull(signingKey)) { + if (Objects.isNull(signingKeys)) { throw new IllegalStateException("Signing key is required."); } - builder.put("signing_key", signingKey); + builder.put("signing_key", signingKeys); if (isNoneBlank(jwtHeader)) { builder.put("jwt_header", jwtHeader); } diff --git a/src/integrationTest/java/org/opensearch/test/framework/RateLimiting.java b/src/integrationTest/java/org/opensearch/test/framework/RateLimiting.java index bd38aac1e5..494ea47a11 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/RateLimiting.java +++ b/src/integrationTest/java/org/opensearch/test/framework/RateLimiting.java @@ -10,6 +10,7 @@ package org.opensearch.test.framework; import java.io.IOException; +import java.util.List; import java.util.Objects; import org.opensearch.core.xcontent.ToXContentObject; @@ -20,6 +21,7 @@ public class RateLimiting implements ToXContentObject { private final String name; private String type; private String authenticationBackend; + private List ignoreHosts; private Integer allowedTries; private Integer timeWindowSeconds; private Integer blockExpirySeconds; @@ -44,6 +46,11 @@ public RateLimiting authenticationBackend(String authenticationBackend) { return this; } + public RateLimiting ignoreHosts(List ignoreHosts) { + this.ignoreHosts = ignoreHosts; + return this; + } + public RateLimiting allowedTries(Integer allowedTries) { this.allowedTries = allowedTries; return this; @@ -79,6 +86,7 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params xContentBuilder.field("block_expiry_seconds", blockExpirySeconds); xContentBuilder.field("max_blocked_clients", maxBlockedClients); xContentBuilder.field("max_tracked_clients", maxTrackedClients); + xContentBuilder.field("ignore_hosts", ignoreHosts); 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 81604d1376..a1ea3720ba 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -815,7 +815,7 @@ public static class AuthcDomain implements ToXContentObject { ).httpAuthenticator("basic").backend("internal"); public final static AuthcDomain JWT_AUTH_DOMAIN = new TestSecurityConfig.AuthcDomain("jwt", 1).jwtHttpAuthenticator( - new JwtConfigBuilder().jwtHeader(AUTHORIZATION).signingKey(PUBLIC_KEY) + new JwtConfigBuilder().jwtHeader(AUTHORIZATION).signingKey(List.of(PUBLIC_KEY)) ).backend("noop"); private final String id; diff --git a/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java b/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java index 08eaeed65e..b3cb7bfe8c 100644 --- a/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java +++ b/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java @@ -14,6 +14,7 @@ import java.nio.file.Path; import java.security.AccessController; import java.security.PrivilegedAction; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -54,7 +55,7 @@ public class HTTPJwtAuthenticator implements HTTPAuthenticator { private static final Pattern BASIC = Pattern.compile("^\\s*Basic\\s.*", Pattern.CASE_INSENSITIVE); private static final String BEARER = "bearer "; - private final JwtParser jwtParser; + private final List jwtParsers = new ArrayList<>(); private final String jwtHeaderName; private final boolean isDefaultAuthHeader; private final String jwtUrlParameter; @@ -67,7 +68,8 @@ public class HTTPJwtAuthenticator implements HTTPAuthenticator { public HTTPJwtAuthenticator(final Settings settings, final Path configPath) { super(); - String signingKey = settings.get("signing_key"); + List signingKeys = settings.getAsList("signing_key"); + jwtUrlParameter = settings.get("jwt_url_parameter"); jwtHeaderName = settings.get("jwt_header", AUTHORIZATION); isDefaultAuthHeader = AUTHORIZATION.equalsIgnoreCase(jwtHeaderName); @@ -83,19 +85,23 @@ public HTTPJwtAuthenticator(final Settings settings, final Path configPath) { ); } - final JwtParserBuilder jwtParserBuilder = KeyUtils.createJwtParserBuilderFromSigningKey(signingKey, log); - if (jwtParserBuilder == null) { - jwtParser = null; - } else { - if (requireIssuer != null) { - jwtParserBuilder.requireIssuer(requireIssuer); - } - - final SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - sm.checkPermission(new SpecialPermission()); + for (String key : signingKeys) { + JwtParser jwtParser; + final JwtParserBuilder jwtParserBuilder = KeyUtils.createJwtParserBuilderFromSigningKey(key, log); + if (jwtParserBuilder == null) { + jwtParser = null; + } else { + if (requireIssuer != null) { + jwtParserBuilder.requireIssuer(requireIssuer); + } + + final SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + jwtParser = AccessController.doPrivileged((PrivilegedAction) jwtParserBuilder::build); } - jwtParser = AccessController.doPrivileged((PrivilegedAction) jwtParserBuilder::build); + jwtParsers.add(jwtParser); } } @@ -120,7 +126,8 @@ public AuthCredentials run() { } private AuthCredentials extractCredentials0(final SecurityRequest request) { - if (jwtParser == null) { + + if (jwtParsers.isEmpty() || jwtParsers.getFirst() == null) { log.error("Missing Signing Key. JWT authentication will not work"); return null; } @@ -157,39 +164,43 @@ private AuthCredentials extractCredentials0(final SecurityRequest request) { } } - try { - final Claims claims = jwtParser.parseClaimsJws(jwtToken).getBody(); + for (JwtParser jwtParser : jwtParsers) { + try { - if (!requiredAudience.isEmpty()) { - assertValidAudienceClaim(claims); - } + final Claims claims = jwtParser.parseClaimsJws(jwtToken).getBody(); - final String subject = extractSubject(claims, request); + if (!requiredAudience.isEmpty()) { + assertValidAudienceClaim(claims); + } - if (subject == null) { - log.error("No subject found in JWT token"); - return null; - } + final String subject = extractSubject(claims, request); - final String[] roles = extractRoles(claims, request); + if (subject == null) { + log.error("No subject found in JWT token"); + return null; + } - final AuthCredentials ac = new AuthCredentials(subject, roles).markComplete(); + final String[] roles = extractRoles(claims, request); - for (Entry claim : claims.entrySet()) { - ac.addAttribute("attr.jwt." + claim.getKey(), String.valueOf(claim.getValue())); - } + final AuthCredentials ac = new AuthCredentials(subject, roles).markComplete(); - return ac; + for (Entry claim : claims.entrySet()) { + ac.addAttribute("attr.jwt." + claim.getKey(), String.valueOf(claim.getValue())); + } - } catch (WeakKeyException e) { - log.error("Cannot authenticate user with JWT because of ", e); - return null; - } catch (Exception e) { - if (log.isDebugEnabled()) { - log.debug("Invalid or expired JWT token.", e); + return ac; + + } catch (WeakKeyException e) { + log.error("Cannot authenticate user with JWT because of ", e); + return null; + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug("Invalid or expired JWT token.", e); + } } - return null; } + log.error("Failed to parse JWT token using any of the available parsers"); + return null; } private void assertValidAudienceClaim(Claims claims) throws BadJWTException { diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 3f1905d281..509b98f12e 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -36,6 +36,7 @@ import java.security.AccessController; import java.security.MessageDigest; import java.security.PrivilegedAction; +import java.security.Provider; import java.security.Security; import java.util.ArrayList; import java.util.Arrays; @@ -63,7 +64,6 @@ import org.apache.logging.log4j.Logger; import org.apache.lucene.search.QueryCachingPolicy; import org.apache.lucene.search.Weight; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.opensearch.OpenSearchException; import org.opensearch.OpenSearchSecurityException; @@ -378,18 +378,14 @@ public OpenSearchSecurityPlugin(final Settings settings, final Path configPath) demoCertHashes.add("a2ce3f577a5031398c1b4f58761444d837b031d0aff7614f8b9b5e4a9d59dbd1"); // esnode demoCertHashes.add("cd708e8dc707ae065f7ad8582979764b497f062e273d478054ab2f49c5469c6"); // root-ca - final SecurityManager sm = System.getSecurityManager(); - - if (sm != null) { - sm.checkPermission(new SpecialPermission()); - } + // updates correct sha256sum + demoCertHashes.add("a3556d6bb61f7bd63cb19b1c8d0078d30c12739dedb0455c5792ac8627782042"); // kirk + demoCertHashes.add("25e34a9a5d4f1dceed1666eb624397bf3fe5787a7133cd32838ace0381bce1f7"); // kirk-key + demoCertHashes.add("a2ce3f577a5031398c1b4f58761444d837b031d0aff7614f8b9b5e4a9d59dbd1"); // esnode + demoCertHashes.add("ba9c5a61065f7f6115188128ffbdaa18fca34562b78b811f082439e2bef1d282"); // esnode-key + demoCertHashes.add("bcd708e8dc707ae065f7ad8582979764b497f062e273d478054ab2f49c5469c6"); // root-ca - AccessController.doPrivileged((PrivilegedAction) () -> { - if (Security.getProvider("BC") == null) { - Security.addProvider(new BouncyCastleProvider()); - } - return null; - }); + tryAddSecurityProvider(); final String advancedModulesEnabledKey = ConfigConstants.SECURITY_ADVANCED_MODULES_ENABLED; if (settings.hasValue(advancedModulesEnabledKey)) { @@ -2121,6 +2117,30 @@ public Optional getSecureSettingFactory(Settings settings return Optional.of(new OpenSearchSecureSettingsFactory(threadPool, sks, sslExceptionHandler, securityRestHandler)); } + @SuppressWarnings("removal") + private void tryAddSecurityProvider() { + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + // Add provider if on the classpath. + AccessController.doPrivileged((PrivilegedAction) () -> { + if (Security.getProvider("BC") == null) { + try { + Class providerClass = Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider"); + Provider provider = (Provider) providerClass.getDeclaredConstructor().newInstance(); + Security.addProvider(provider); + log.debug("Bouncy Castle Provider added"); + } catch (Exception e) { + log.debug("Bouncy Castle Provider could not be added", e); + } + } + return null; + }); + } + public static class GuiceHolder implements LifecycleComponent { private static RepositoriesService repositoriesService; diff --git a/src/main/java/org/opensearch/security/auth/AuthFailureListener.java b/src/main/java/org/opensearch/security/auth/AuthFailureListener.java index b835078aa3..cbc76cc2e0 100644 --- a/src/main/java/org/opensearch/security/auth/AuthFailureListener.java +++ b/src/main/java/org/opensearch/security/auth/AuthFailureListener.java @@ -19,8 +19,11 @@ import java.net.InetAddress; +import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.AuthCredentials; public interface AuthFailureListener { void onAuthFailure(InetAddress remoteAddress, AuthCredentials authCredentials, Object request); + + WildcardMatcher getIgnoreHostsMatcher(); } diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 97c060be35..0e39acf59e 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -28,6 +28,7 @@ import java.net.InetAddress; import java.net.InetSocketAddress; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -64,6 +65,7 @@ import org.opensearch.security.http.XFFResolver; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; @@ -84,6 +86,7 @@ public class BackendRegistry { private Multimap authBackendFailureListeners; private List> ipClientBlockRegistries; private Multimap> authBackendClientBlockRegistries; + private String hostResolverMode; private volatile boolean initialized; private volatile boolean injectedUserEnabled = false; @@ -182,6 +185,7 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { authBackendFailureListeners = dcm.getAuthBackendFailureListeners(); ipClientBlockRegistries = dcm.getIpClientBlockRegistries(); authBackendClientBlockRegistries = dcm.getAuthBackendClientBlockRegistries(); + hostResolverMode = dcm.getHostsResolverMode(); // OpenSearch Security no default authc initialized = !restAuthDomains.isEmpty() || anonymousAuthEnabled || injectedUserEnabled; @@ -197,11 +201,15 @@ public boolean authenticate(final SecurityRequestChannel request) { final boolean isDebugEnabled = log.isDebugEnabled(); final boolean isBlockedBasedOnAddress = request.getRemoteAddress() .map(InetSocketAddress::getAddress) - .map(address -> isBlocked(address)) + .map(this::isBlocked) .orElse(false); if (isBlockedBasedOnAddress) { if (isDebugEnabled) { - log.debug("Rejecting REST request because of blocked address: {}", request.getRemoteAddress().orElse(null)); + InetSocketAddress ipAddress = request.getRemoteAddress().orElse(null); + log.debug( + "Rejecting REST request because of blocked address: {}", + ipAddress != null ? "/" + ipAddress.getAddress().getHostAddress() : null + ); } request.queueForSending(new SecurityResponse(SC_UNAUTHORIZED, "Authentication finally failed")); @@ -680,6 +688,10 @@ private boolean isBlocked(InetAddress address) { } for (ClientBlockRegistry clientBlockRegistry : ipClientBlockRegistries) { + WildcardMatcher ignoreHostsMatcher = ((AuthFailureListener) clientBlockRegistry).getIgnoreHostsMatcher(); + if (matchesHostPatterns(ignoreHostsMatcher, address, hostResolverMode)) { + return false; + } if (clientBlockRegistry.isBlocked(address)) { return true; } @@ -688,6 +700,23 @@ private boolean isBlocked(InetAddress address) { return false; } + public static boolean matchesHostPatterns(WildcardMatcher hostMatcher, InetAddress address, String hostResolverMode) { + if (hostMatcher == null) { + return false; + } + if (address != null) { + List valuesToCheck = new ArrayList<>(List.of(address.getHostAddress())); + if (hostResolverMode != null + && (hostResolverMode.equalsIgnoreCase("ip-hostname") || hostResolverMode.equalsIgnoreCase("ip-hostname-lookup"))) { + final String hostName = address.getHostName(); + valuesToCheck.add(hostName); + } + + return valuesToCheck.stream().anyMatch(hostMatcher); + } + return false; + } + private boolean isBlocked(String authBackend, String userName) { if (this.authBackendClientBlockRegistries == null) { diff --git a/src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java b/src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java index 0fc796d94f..a0028fc1d6 100644 --- a/src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java +++ b/src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java @@ -19,19 +19,25 @@ import java.net.InetAddress; import java.nio.file.Path; +import java.util.Collections; +import java.util.List; import org.opensearch.common.settings.Settings; import org.opensearch.security.auth.AuthFailureListener; import org.opensearch.security.auth.blocking.ClientBlockRegistry; import org.opensearch.security.auth.blocking.HeapBasedClientBlockRegistry; +import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.util.ratetracking.RateTracker; public abstract class AbstractRateLimiter implements AuthFailureListener, ClientBlockRegistry { protected final ClientBlockRegistry clientBlockRegistry; protected final RateTracker rateTracker; + protected final List ignoreHosts; + private WildcardMatcher ignoreHostMatcher; public AbstractRateLimiter(Settings settings, Path configPath, Class clientIdType) { + this.ignoreHosts = settings.getAsList("ignore_hosts", Collections.emptyList()); this.clientBlockRegistry = new HeapBasedClientBlockRegistry<>( settings.getAsInt("block_expiry_seconds", 60 * 10) * 1000, settings.getAsInt("max_blocked_clients", 100_000), @@ -47,6 +53,19 @@ public AbstractRateLimiter(Settings settings, Path configPath, Class withAuthTokenPath(final RestRequest request) throws IOException { return endpointValidator.withRequiredEntityName(nameParam(request)).map(username -> { // Handle auth token fetching - if (!(request.uri().contains("/internalusers/" + username + "/authtoken") && request.uri().endsWith("/authtoken"))) { - return ValidationResult.error(RestStatus.NOT_IMPLEMENTED, methodNotImplementedMessage(request.method())); + if (request.uri().contains("/internalusers/" + username + "/authtoken") && request.uri().endsWith("/authtoken")) { + return ValidationResult.success(username); } - return ValidationResult.success(username); + return ValidationResult.error(RestStatus.NOT_IMPLEMENTED, methodNotImplementedMessage(request.method())); }); } diff --git a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java index 420211f29b..b56f3e951d 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java @@ -254,7 +254,6 @@ void authorizeRequest(RestHandler original, SecurityRequestChannel request, User log.debug(err); request.queueForSending(new SecurityResponse(HttpStatus.SC_UNAUTHORIZED, err)); - return; } } } @@ -309,7 +308,6 @@ public void checkAndAuthenticateRequest(SecurityRequestChannel requestChannel) t if (!registry.authenticate(requestChannel)) { // another roundtrip org.apache.logging.log4j.ThreadContext.remove("user"); - return; } else { // make it possible to filter logs by username org.apache.logging.log4j.ThreadContext.put( diff --git a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java index 0f5ec9a1bc..38825a9bf1 100644 --- a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java @@ -179,7 +179,7 @@ private Set getAllSystemIndices(final Resolved requestedResolved) { .collect(Collectors.toSet()); if (isSystemIndexEnabled) { systemIndices.addAll(systemIndexMatcher.getMatchAny(requestedResolved.getAllIndices(), Collectors.toList())); - systemIndices.addAll(SystemIndexRegistry.matchesSystemIndexPattern(requestedResolved.getAllIndices().toArray(String[]::new))); + systemIndices.addAll(SystemIndexRegistry.matchesSystemIndexPattern(requestedResolved.getAllIndices())); } return systemIndices; } diff --git a/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java b/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java index d06a45726a..cc0bf25b5e 100644 --- a/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java @@ -32,6 +32,11 @@ import org.apache.logging.log4j.Logger; import org.opensearch.action.ActionRequest; +import org.opensearch.action.fieldcaps.FieldCapabilitiesAction; +import org.opensearch.action.get.GetAction; +import org.opensearch.action.get.MultiGetAction; +import org.opensearch.action.search.MultiSearchAction; +import org.opensearch.action.search.SearchAction; import org.opensearch.action.search.SearchRequest; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.service.ClusterService; @@ -49,13 +54,11 @@ public class TermsAggregationEvaluator { protected final Logger log = LogManager.getLogger(this.getClass()); private static final String[] READ_ACTIONS = new String[] { - "indices:data/read/msearch", - "indices:data/read/mget", - "indices:data/read/get", - "indices:data/read/search", - "indices:data/read/field_caps*" - // "indices:admin/mappings/fields/get*" - }; + MultiSearchAction.NAME, + MultiGetAction.NAME, + GetAction.NAME, + SearchAction.NAME, + FieldCapabilitiesAction.NAME }; private static final QueryBuilder NONE_QUERY = new MatchNoneQueryBuilder(); diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java index 84902bba3e..f78c173202 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java @@ -45,7 +45,6 @@ import com.google.common.collect.SetMultimap; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.util.Strings; import org.opensearch.ExceptionsHelper; import org.opensearch.action.support.IndicesOptions; @@ -801,7 +800,7 @@ public Set getResolvedIndexPattern( } } - if (Strings.isNotBlank(unresolved)) { + if (!(unresolved == null || unresolved.isBlank())) { final String[] resolvedIndicesFromPattern = resolver.concreteIndexNames( cs.state(), IndicesOptions.lenientExpandOpen(), diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java index 56415ec1bf..335e5283ba 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java @@ -83,6 +83,7 @@ public class DynamicConfigFactory implements Initializable, ConfigurationChangeL private static SecurityDynamicConfiguration staticTenants = SecurityDynamicConfiguration.empty(); private static final WhitelistingSettings defaultWhitelistingSettings = new WhitelistingSettings(); private static final AllowlistingSettings defaultAllowlistingSettings = new AllowlistingSettings(); + private static final AuditConfig defaultAuditConfig = AuditConfig.from(Settings.EMPTY); static void resetStatics() { staticRoles = SecurityDynamicConfiguration.empty(); @@ -314,7 +315,7 @@ public void onChange(Map> typeToConfig) { eventBus.post(whitelist == null ? defaultWhitelistingSettings : whitelist); eventBus.post(allowlist == null ? defaultAllowlistingSettings : allowlist); if (cr.isAuditHotReloadingEnabled()) { - eventBus.post(audit); + eventBus.post(audit == null ? defaultAuditConfig : audit); } initialized.set(true); diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java index dc9be395b1..0638d7a884 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java @@ -226,6 +226,7 @@ public Map getListeners() { public static class AuthFailureListener { public String type; public String authentication_backend; + public List ignore_hosts; public int allowed_tries = 10; public int time_window_seconds = 60 * 60; public int block_expiry_seconds = 60 * 10; diff --git a/src/main/java/org/opensearch/security/ssl/DefaultSecurityKeyStore.java b/src/main/java/org/opensearch/security/ssl/DefaultSecurityKeyStore.java index b697bbedff..9be2582b7f 100644 --- a/src/main/java/org/opensearch/security/ssl/DefaultSecurityKeyStore.java +++ b/src/main/java/org/opensearch/security/ssl/DefaultSecurityKeyStore.java @@ -34,11 +34,13 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.TreeSet; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -1186,7 +1188,10 @@ public String getSubjectAlternativeNames(X509Certificate cert) { ? cert.getSubjectAlternativeNames() : null; if (altNames != null) { - Collection> sans = new ArrayList<>(); + Comparator> comparator = Comparator.comparing((List altName) -> (Integer) altName.get(0)) + .thenComparing((List altName) -> (String) altName.get(1)); + + Set> sans = new TreeSet<>(comparator); for (List altName : altNames) { Integer type = (Integer) altName.get(0); // otherName requires parsing to string diff --git a/src/main/java/org/opensearch/security/tools/democonfig/Certificates.java b/src/main/java/org/opensearch/security/tools/democonfig/Certificates.java index baff8d7078..3ec18bc373 100644 --- a/src/main/java/org/opensearch/security/tools/democonfig/Certificates.java +++ b/src/main/java/org/opensearch/security/tools/democonfig/Certificates.java @@ -48,7 +48,7 @@ public enum Certificates { "KRVHWCFiR7bZhHGLq3br8hSu0hwjb3oGa1ZI8dui6ujyZt6nm6BoEkau3G/6+zq9", "E6vX3+8Fj4HKCAL6i0SwfGmEpTNp5WUhqibK/fMhhmMT4Mx6MxkT+OFnIjdUU0S/", "e3kgnG8qjficUr38CyEli1U0M7koIXUZI7r+LQ==", - "-----END CERTIFICATE-----" + "-----END CERTIFICATE-----\n" ) ) ), @@ -83,7 +83,7 @@ public enum Certificates { "mQGwy8vIqMjAdHGLrCS35sVYBXG13knS52LJHvbVee39AbD5/LlWvjJGlQMzCLrw", "F7oILW5kXxhb8S73GWcuMbuQMFVHFONbZAZgn+C9FW4l7XyRdkrbR1MRZ2km8YMs", "/AHmo368d4PSNRMMzLHw8Q==", - "-----END PRIVATE KEY-----" + "-----END PRIVATE KEY-----\n" ) ) ), @@ -115,7 +115,7 @@ public enum Certificates { "hUBqIEAYly1EqH/y45APiRt3Nor1yF6zEI4TnL0yNrHw6LyQkUNCHIGMJLfnJQ9L", "camMGIXOx60kXNMTigF9oXXwixWAnDM9y3QT8QXA7hej/4zkbO+vIeV/7lGUdkyg", "PAi92EvyxmsliEMyMR0VINl8emyobvfwa7oMeWMR+hg=", - "-----END CERTIFICATE-----" + "-----END CERTIFICATE-----\n" ) ) ), @@ -150,7 +150,7 @@ public enum Certificates { "tu49A/0KZu4PBjrFMYTSEWGNJez3Fb2VsJwylVl6HivwbP61FhlYfyksCzQQFU71", "+x7Nmybp7PmpEBECr3deoZKQ/acNHn0iwb0It+YqV5+TquQebqgwK6WCLsMuiYKT", "bg/ch9Rhxbq22yrVgWHh6epp", - "-----END PRIVATE KEY-----" + "-----END PRIVATE KEY-----\n" ) ) ), @@ -185,7 +185,7 @@ public enum Certificates { "1yVJon6RkUGtqBqKIuLksKwEr//ELnjmXit4LQKSnqKr0FTCB7seIrKJNyb35Qnq", "qy9a/Unhokrmdda1tr6MbqU8l7HmxLuSd/Ky+L0eDNtYv6YfMewtjg0TtAnFyQov", "rdXmeq1dy9HLo3Ds4AFz3Gx9076TxcRS/iI=", - "-----END CERTIFICATE-----" + "-----END CERTIFICATE-----\n" ) ) ); diff --git a/src/main/java/org/opensearch/security/user/UserService.java b/src/main/java/org/opensearch/security/user/UserService.java index 449c496c37..f380fdb328 100644 --- a/src/main/java/org/opensearch/security/user/UserService.java +++ b/src/main/java/org/opensearch/security/user/UserService.java @@ -27,7 +27,6 @@ import com.google.common.collect.ImmutableList; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -243,14 +242,8 @@ public static String generatePassword() { CharacterRule lowercaseCharacterRule = new CharacterRule(EnglishCharacterData.LowerCase, 1); CharacterRule uppercaseCharacterRule = new CharacterRule(EnglishCharacterData.UpperCase, 1); CharacterRule numericCharacterRule = new CharacterRule(EnglishCharacterData.Digit, 1); - CharacterRule specialCharacterRule = new CharacterRule(EnglishCharacterData.Special, 1); - List rules = Arrays.asList( - lowercaseCharacterRule, - uppercaseCharacterRule, - numericCharacterRule, - specialCharacterRule - ); + List rules = Arrays.asList(lowercaseCharacterRule, uppercaseCharacterRule, numericCharacterRule); PasswordGenerator passwordGenerator = new PasswordGenerator(); Random random = Randomness.get(); @@ -275,17 +268,17 @@ public AuthToken generateAuthToken(String accountName) throws IOException { String authToken = null; try { - final ObjectMapper mapper = DefaultObjectMapper.objectMapper; - JsonNode accountDetails = mapper.readTree(internalUsersConfiguration.getCEntry(accountName).toString()); + final var accountEntry = DefaultObjectMapper.writeValueAsString(internalUsersConfiguration.getCEntry(accountName), false); + JsonNode accountDetails = DefaultObjectMapper.readTree(accountEntry); final ObjectNode contentAsNode = (ObjectNode) accountDetails; SecurityJsonNode securityJsonNode = new SecurityJsonNode(contentAsNode); - Optional.ofNullable(securityJsonNode.get("service")) + Optional.ofNullable(securityJsonNode.get("attributes").get("service")) .map(SecurityJsonNode::asString) .filter("true"::equalsIgnoreCase) .orElseThrow(() -> new UserServiceException(AUTH_TOKEN_GENERATION_MESSAGE)); - Optional.ofNullable(securityJsonNode.get("enabled")) + Optional.ofNullable(securityJsonNode.get("attributes").get("enabled")) .map(SecurityJsonNode::asString) .filter("true"::equalsIgnoreCase) .orElseThrow(() -> new UserServiceException(AUTH_TOKEN_GENERATION_MESSAGE)); @@ -306,7 +299,7 @@ public AuthToken generateAuthToken(String accountName) throws IOException { saveAndUpdateConfigs(getUserConfigName().toString(), client, CType.INTERNALUSERS, internalUsersConfiguration); authToken = Base64.getUrlEncoder().encodeToString((accountName + ":" + plainTextPassword).getBytes(StandardCharsets.UTF_8)); - return new BasicAuthToken(authToken); + return new BasicAuthToken("Basic " + authToken); } catch (JsonProcessingException ex) { throw new UserServiceException(FAILED_ACCOUNT_RETRIEVAL_MESSAGE); diff --git a/src/main/java/org/opensearch/security/util/KeyUtils.java b/src/main/java/org/opensearch/security/util/KeyUtils.java index bdf7bf04e0..920cf198be 100644 --- a/src/main/java/org/opensearch/security/util/KeyUtils.java +++ b/src/main/java/org/opensearch/security/util/KeyUtils.java @@ -54,8 +54,8 @@ public JwtParserBuilder run() { PublicKey key = null; final String minimalKeyFormat = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", "") - .replace("-----END PUBLIC KEY-----", ""); - + .replace("-----END PUBLIC KEY-----", "") + .trim(); final byte[] decoded = Base64.getDecoder().decode(minimalKeyFormat); try { diff --git a/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java b/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java index fcd7dd2160..4214e8ed06 100644 --- a/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java +++ b/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java @@ -14,6 +14,7 @@ import java.nio.charset.StandardCharsets; import java.security.KeyPair; import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; @@ -534,6 +535,306 @@ public void testRequiredIssuerWithIncorrectAudience() { Assert.assertNull(credentials); } + @Test + public void testMultipleSigningKeysParseSuccessfully() throws NoSuchAlgorithmException { + + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + KeyPair pair1 = keyGen.generateKeyPair(); + PrivateKey priv1 = pair1.getPrivate(); + PublicKey pub1 = pair1.getPublic(); + + KeyPair pair2 = keyGen.generateKeyPair(); + PrivateKey priv2 = pair2.getPrivate(); + PublicKey pub2 = pair2.getPublic(); + + String jwsToken1 = Jwts.builder().setSubject("Leonard McCoy").signWith(priv1, SignatureAlgorithm.RS256).compact(); + String jwsToken2 = Jwts.builder().setSubject("Stephen Crawford").signWith(priv2, SignatureAlgorithm.RS256).compact(); + + Settings settings = Settings.builder() + .put( + "signing_key", + "-----BEGIN PUBLIC KEY-----\n" + + BaseEncoding.base64().encode(pub1.getEncoded()) + + "-----END PUBLIC KEY-----,-----BEGIN PUBLIC KEY-----\n" + + BaseEncoding.base64().encode(pub2.getEncoded()) + + "-----END PUBLIC KEY-----" + ) + .build(); + + HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); + Map headers1 = new HashMap(); + headers1.put("Authorization", "Bearer " + jwsToken1); + + AuthCredentials creds1 = jwtAuth.extractCredentials( + new FakeRestRequest(headers1, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds1); + assertThat(creds1.getUsername(), is("Leonard McCoy")); + assertThat(creds1.getBackendRoles().size(), is(0)); + + Map headers2 = new HashMap(); + headers2.put("Authorization", "Bearer " + jwsToken2); + AuthCredentials creds2 = jwtAuth.extractCredentials( + new FakeRestRequest(headers2, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds2); + assertThat(creds2.getUsername(), is("Stephen Crawford")); + assertThat(creds2.getBackendRoles().size(), is(0)); + } + + @Test + public void testMultipleSigningKeysParseWithSpacesSuccessfully() throws NoSuchAlgorithmException { + + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + KeyPair pair1 = keyGen.generateKeyPair(); + PrivateKey priv1 = pair1.getPrivate(); + PublicKey pub1 = pair1.getPublic(); + + KeyPair pair2 = keyGen.generateKeyPair(); + PrivateKey priv2 = pair2.getPrivate(); + PublicKey pub2 = pair2.getPublic(); + + String jwsToken1 = Jwts.builder().setSubject("Leonard McCoy").signWith(priv1, SignatureAlgorithm.RS256).compact(); + String jwsToken2 = Jwts.builder().setSubject("Stephen Crawford").signWith(priv2, SignatureAlgorithm.RS256).compact(); + + Settings settings = Settings.builder() + .put( + "signing_key", + "-----BEGIN PUBLIC KEY-----\n" + + BaseEncoding.base64().encode(pub1.getEncoded()) + + "-----END PUBLIC KEY-----, -----BEGIN PUBLIC KEY-----\n" + + BaseEncoding.base64().encode(pub2.getEncoded()) + + "-----END PUBLIC KEY-----" + ) + .build(); + + HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); + Map headers1 = new HashMap(); + headers1.put("Authorization", "Bearer " + jwsToken1); + + AuthCredentials creds1 = jwtAuth.extractCredentials( + new FakeRestRequest(headers1, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds1); + assertThat(creds1.getUsername(), is("Leonard McCoy")); + assertThat(creds1.getBackendRoles().size(), is(0)); + + Map headers2 = new HashMap(); + headers2.put("Authorization", "Bearer " + jwsToken2); + AuthCredentials creds2 = jwtAuth.extractCredentials( + new FakeRestRequest(headers2, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds2); + assertThat(creds2.getUsername(), is("Stephen Crawford")); + assertThat(creds2.getBackendRoles().size(), is(0)); + } + + @Test + public void testMultipleSigningKeysMixedAlgsParseSuccessfully() throws NoSuchAlgorithmException { + + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + KeyPair pair1 = keyGen.generateKeyPair(); + PrivateKey priv1 = pair1.getPrivate(); + PublicKey pub1 = pair1.getPublic(); + + KeyPairGenerator keyGen2 = KeyPairGenerator.getInstance("EC"); + keyGen2.initialize(521); + KeyPair pair = keyGen2.generateKeyPair(); + PrivateKey priv2 = pair.getPrivate(); + PublicKey pub2 = pair.getPublic(); + + String jwsToken1 = Jwts.builder().setSubject("Leonard McCoy").signWith(priv1, SignatureAlgorithm.RS256).compact(); + + String jwsToken2 = Jwts.builder().setSubject("Stephen Crawford").signWith(priv2, SignatureAlgorithm.ES512).compact(); + + Settings settings = Settings.builder() + .put( + "signing_key", + "-----BEGIN PUBLIC KEY-----\n" + + BaseEncoding.base64().encode(pub1.getEncoded()) + + "-----END PUBLIC KEY-----," + + BaseEncoding.base64().encode(pub2.getEncoded()) + ) + .build(); + + HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); + Map headers1 = new HashMap(); + headers1.put("Authorization", "Bearer " + jwsToken1); + + AuthCredentials creds1 = jwtAuth.extractCredentials( + new FakeRestRequest(headers1, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds1); + assertThat(creds1.getUsername(), is("Leonard McCoy")); + assertThat(creds1.getBackendRoles().size(), is(0)); + + Map headers2 = new HashMap(); + headers2.put("Authorization", "Bearer " + jwsToken2); + AuthCredentials creds2 = jwtAuth.extractCredentials( + new FakeRestRequest(headers2, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds2); + assertThat(creds2.getUsername(), is("Stephen Crawford")); + assertThat(creds2.getBackendRoles().size(), is(0)); + } + + @Test + public void testManyMultipleSigningKeysMixedAlgsParseSuccessfully() throws NoSuchAlgorithmException { + + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + KeyPair pair1 = keyGen.generateKeyPair(); + PrivateKey priv1 = pair1.getPrivate(); + PublicKey pub1 = pair1.getPublic(); + + KeyPairGenerator keyGen2 = KeyPairGenerator.getInstance("EC"); + keyGen2.initialize(521); + KeyPair pair = keyGen2.generateKeyPair(); + PrivateKey priv2 = pair.getPrivate(); + PublicKey pub2 = pair.getPublic(); + + KeyPairGenerator keyGen3 = KeyPairGenerator.getInstance("RSA"); + keyGen3.initialize(2048); + KeyPair pair3 = keyGen3.generateKeyPair(); + PrivateKey priv3 = pair3.getPrivate(); + PublicKey pub3 = pair3.getPublic(); + + KeyPairGenerator keyGen4 = KeyPairGenerator.getInstance("EC"); + keyGen4.initialize(521); + KeyPair pair4 = keyGen4.generateKeyPair(); + PrivateKey priv4 = pair4.getPrivate(); + PublicKey pub4 = pair4.getPublic(); + + String jwsToken1 = Jwts.builder().setSubject("Stephen Crawford").signWith(priv1, SignatureAlgorithm.RS256).compact(); + String jwsToken2 = Jwts.builder().setSubject("Craig Perkins").signWith(priv2, SignatureAlgorithm.ES512).compact(); + String jwsToken3 = Jwts.builder().setSubject("Darshit Chanpura").signWith(priv3, SignatureAlgorithm.RS256).compact(); + String jwsToken4 = Jwts.builder().setSubject("Derek Ho").signWith(priv4, SignatureAlgorithm.ES512).compact(); + + Settings settings = Settings.builder() + .put( + "signing_key", + "-----BEGIN PUBLIC KEY-----\n" + + BaseEncoding.base64().encode(pub1.getEncoded()) + + "-----END PUBLIC KEY-----," + + BaseEncoding.base64().encode(pub2.getEncoded()) + + "," + + "-----BEGIN PUBLIC KEY-----\n" + + BaseEncoding.base64().encode(pub3.getEncoded()) + + "-----END PUBLIC KEY-----," + + BaseEncoding.base64().encode(pub4.getEncoded()) + ) + .build(); + + HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); + Map headers1 = new HashMap(); + headers1.put("Authorization", "Bearer " + jwsToken1); + + AuthCredentials creds1 = jwtAuth.extractCredentials( + new FakeRestRequest(headers1, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds1); + assertThat(creds1.getUsername(), is("Stephen Crawford")); + assertThat(creds1.getBackendRoles().size(), is(0)); + + Map headers2 = new HashMap(); + headers2.put("Authorization", "Bearer " + jwsToken2); + AuthCredentials creds2 = jwtAuth.extractCredentials( + new FakeRestRequest(headers2, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds2); + assertThat(creds2.getUsername(), is("Craig Perkins")); + assertThat(creds2.getBackendRoles().size(), is(0)); + + Map headers3 = new HashMap(); + headers3.put("Authorization", "Bearer " + jwsToken3); + + AuthCredentials creds3 = jwtAuth.extractCredentials( + new FakeRestRequest(headers3, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds3); + assertThat(creds3.getUsername(), is("Darshit Chanpura")); + assertThat(creds3.getBackendRoles().size(), is(0)); + + Map headers4 = new HashMap(); + headers4.put("Authorization", "Bearer " + jwsToken4); + AuthCredentials creds4 = jwtAuth.extractCredentials( + new FakeRestRequest(headers4, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds4); + assertThat(creds4.getUsername(), is("Derek Ho")); + assertThat(creds4.getBackendRoles().size(), is(0)); + } + + @Test + public void testMultipleSigningKeysFailToParseReturnsNull() throws NoSuchAlgorithmException { + + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + KeyPair pair1 = keyGen.generateKeyPair(); + PrivateKey priv1 = pair1.getPrivate(); + PublicKey pub1 = pair1.getPublic(); + + KeyPair pair2 = keyGen.generateKeyPair(); + PrivateKey priv2 = pair2.getPrivate(); + PublicKey pub2 = pair2.getPublic(); + + String invalidJwsToken = "123invalidtoken.."; + + Settings settings = Settings.builder() + .put( + "signing_key", + "-----BEGIN PUBLIC KEY-----\n" + + BaseEncoding.base64().encode(pub1.getEncoded()) + + "-----END PUBLIC KEY-----, -----BEGIN PUBLIC KEY-----\n" + + BaseEncoding.base64().encode(pub2.getEncoded()) + + "-----END PUBLIC KEY-----" + ) + .build(); + + HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); + Map headers1 = new HashMap(); + headers1.put("Authorization", "Bearer " + invalidJwsToken); + + AuthCredentials creds1 = jwtAuth.extractCredentials( + new FakeRestRequest(headers1, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNull(creds1); + + Map headers2 = new HashMap(); + headers2.put("Authorization", "Bearer " + invalidJwsToken); + AuthCredentials creds2 = jwtAuth.extractCredentials( + new FakeRestRequest(headers2, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNull(creds2); + } + /** extracts a default user credential from a request header */ private AuthCredentials extractCredentialsFromJwtHeader(final Settings.Builder settingsBuilder, final JwtBuilder jwtBuilder) { final Settings settings = settingsBuilder.build(); diff --git a/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/CxfTestTools.java b/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/CxfTestTools.java deleted file mode 100644 index b2958193c2..0000000000 --- a/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/CxfTestTools.java +++ /dev/null @@ -1,22 +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 com.amazon.dlic.auth.http.jwt.keybyoidc; - -import org.apache.cxf.jaxrs.json.basic.JsonMapObject; -import org.apache.cxf.jaxrs.json.basic.JsonMapObjectReaderWriter; - -class CxfTestTools { - - static String toJson(JsonMapObject jsonMapObject) { - return new JsonMapObjectReaderWriter().toJson(jsonMapObject); - } -} diff --git a/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/TestJwts.java b/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/TestJwts.java index 4a6d5f97e9..acc6a0dba9 100644 --- a/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/TestJwts.java +++ b/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/TestJwts.java @@ -14,7 +14,6 @@ import java.util.Set; import com.google.common.collect.ImmutableSet; -import org.apache.logging.log4j.util.Strings; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSAlgorithm; @@ -31,7 +30,7 @@ class TestJwts { static final String ROLES_CLAIM = "roles"; static final Set TEST_ROLES = ImmutableSet.of("role1", "role2"); - static final String TEST_ROLES_STRING = Strings.join(TEST_ROLES, ','); + static final String TEST_ROLES_STRING = String.join(",", TEST_ROLES); static final String TEST_AUDIENCE = "TestAudience"; diff --git a/src/test/java/org/opensearch/security/UtilTests.java b/src/test/java/org/opensearch/security/UtilTests.java index 195297a440..2445b560df 100644 --- a/src/test/java/org/opensearch/security/UtilTests.java +++ b/src/test/java/org/opensearch/security/UtilTests.java @@ -26,11 +26,15 @@ package org.opensearch.security; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; import java.util.Map; import org.junit.Test; import org.opensearch.common.settings.Settings; +import org.opensearch.security.auth.BackendRegistry; import org.opensearch.security.hasher.PasswordHasher; import org.opensearch.security.hasher.PasswordHasherFactory; import org.opensearch.security.support.ConfigConstants; @@ -184,4 +188,88 @@ public void testNoEnvReplace() { ); } } + + @Test + public void testHostMatching() throws UnknownHostException { + assertThat(BackendRegistry.matchesHostPatterns(null, null, "ip-only"), is(false)); + assertThat(BackendRegistry.matchesHostPatterns(null, null, null), is(false)); + assertThat(BackendRegistry.matchesHostPatterns(WildcardMatcher.from(List.of("127.0.0.1")), null, "ip-only"), is(false)); + assertThat(BackendRegistry.matchesHostPatterns(null, InetAddress.getByName("127.0.0.1"), "ip-only"), is(false)); + assertThat( + BackendRegistry.matchesHostPatterns(WildcardMatcher.from(List.of("127.0.0.1")), InetAddress.getByName("127.0.0.1"), "ip-only"), + is(true) + ); + assertThat( + BackendRegistry.matchesHostPatterns(WildcardMatcher.from(List.of("127.0.0.*")), InetAddress.getByName("127.0.0.1"), "ip-only"), + is(true) + ); + assertThat( + BackendRegistry.matchesHostPatterns( + WildcardMatcher.from(List.of("127.0.0.1")), + InetAddress.getByName("localhost"), + "ip-hostname" + ), + is(true) + ); + assertThat( + BackendRegistry.matchesHostPatterns(WildcardMatcher.from(List.of("127.0.0.1")), InetAddress.getByName("localhost"), "ip-only"), + is(true) + ); + assertThat( + BackendRegistry.matchesHostPatterns( + WildcardMatcher.from(List.of("127.0.0.1")), + InetAddress.getByName("localhost"), + "ip-hostname" + ), + is(true) + ); + assertThat( + BackendRegistry.matchesHostPatterns( + WildcardMatcher.from(List.of("127.0.0.1")), + InetAddress.getByName("example.org"), + "ip-hostname" + ), + is(false) + ); + assertThat( + BackendRegistry.matchesHostPatterns( + WildcardMatcher.from(List.of("example.org")), + InetAddress.getByName("example.org"), + "ip-hostname" + ), + is(true) + ); + assertThat( + BackendRegistry.matchesHostPatterns( + WildcardMatcher.from(List.of("example.org")), + InetAddress.getByName("example.org"), + "ip-only" + ), + is(false) + ); + assertThat( + BackendRegistry.matchesHostPatterns( + WildcardMatcher.from(List.of("*example.org")), + InetAddress.getByName("example.org"), + "ip-hostname" + ), + is(true) + ); + assertThat( + BackendRegistry.matchesHostPatterns( + WildcardMatcher.from(List.of("example.*")), + InetAddress.getByName("example.org"), + "ip-hostname" + ), + is(true) + ); + assertThat( + BackendRegistry.matchesHostPatterns( + WildcardMatcher.from(List.of("opensearch.org")), + InetAddress.getByName("example.org"), + "ip-hostname" + ), + is(false) + ); + } } diff --git a/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java b/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java index 9669f17c7f..244967cf76 100644 --- a/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java +++ b/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java @@ -54,7 +54,7 @@ public class SecuritySSLReloadCertsActionTests extends SingleClusterTest { "subject_dn", "CN=node-1.example.com,OU=SSL,O=Test,L=Test,C=DE", "san", - "[[8, 1.2.3.4.5.5], [0, [2.5.4.3, node-1.example.com]], [2, node-1.example.com], [2, localhost], [7, 127.0.0.1]]", + "[[0, [2.5.4.3, node-1.example.com]], [2, localhost], [2, node-1.example.com], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]", "not_before", "2023-04-14T13:22:53Z", "not_after", @@ -69,7 +69,7 @@ public class SecuritySSLReloadCertsActionTests extends SingleClusterTest { "subject_dn", "CN=node-1.example.com,OU=SSL,O=Test,L=Test,C=DE", "san", - "[[8, 1.2.3.4.5.5], [0, [2.5.4.3, node-1.example.com]], [2, node-1.example.com], [2, localhost], [7, 127.0.0.1]]", + "[[0, [2.5.4.3, node-1.example.com]], [2, localhost], [2, node-1.example.com], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]", "not_before", "2023-04-14T13:23:00Z", "not_after", diff --git a/src/test/java/org/opensearch/security/tools/democonfig/CertificateGeneratorTests.java b/src/test/java/org/opensearch/security/tools/democonfig/CertificateGeneratorTests.java index 71771f8116..f7f13988c8 100644 --- a/src/test/java/org/opensearch/security/tools/democonfig/CertificateGeneratorTests.java +++ b/src/test/java/org/opensearch/security/tools/democonfig/CertificateGeneratorTests.java @@ -165,7 +165,7 @@ private static String readPEMFile(String pemFilePath) throws Exception { try (BufferedReader reader = new BufferedReader(new FileReader(pemFilePath))) { String line; while ((line = reader.readLine()) != null) { - pemContent.append(line).append("\n"); + pemContent.append(line); } } return pemContent.toString();