From 9caa098a52c1e155889d70a80127f92b25ac8a35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 07:37:34 -0400 Subject: [PATCH 01/23] dependabot: bump org.gradle.test-retry from 1.5.4 to 1.5.5 (#3391) Bumps org.gradle.test-retry from 1.5.4 to 1.5.5. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.gradle.test-retry&package-manager=gradle&previous-version=1.5.4&new-version=1.5.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index dc443e97bf..25b9a05f0d 100644 --- a/build.gradle +++ b/build.gradle @@ -64,7 +64,7 @@ plugins { id 'com.diffplug.spotless' version '6.21.0' id 'checkstyle' id 'com.netflix.nebula.ospackage' version "11.4.0" - id "org.gradle.test-retry" version "1.5.4" + id "org.gradle.test-retry" version "1.5.5" id 'eclipse' id "com.github.spotbugs" version "5.1.3" id "com.google.osdetector" version "1.7.3" From dfecc00dbcf16c8fe462c1b1c9b8794988b2fd12 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 07:38:06 -0400 Subject: [PATCH 02/23] dependabot: bump org.xerial.snappy:snappy-java from 1.1.10.3 to 1.1.10.4 (#3392) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [org.xerial.snappy:snappy-java](https://github.com/xerial/snappy-java) from 1.1.10.3 to 1.1.10.4.
Release notes

Sourced from org.xerial.snappy:snappy-java's releases.

v1.1.10.4

What's Changed

Security Fix

  • Fixed SnappyInputStream so as not to allocate too large memory when decompressing data with an extremely large chunk size by @​tunnelshade (code change)
    • This does not affect users only using Snappy.compress/uncompress methods

🚀 Features

🔗 Dependency Updates

🛠 Internal Updates

Other Changes

Full Changelog: https://github.com/xerial/snappy-java/compare/v1.1.10.3...v1.1.10.4

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.xerial.snappy:snappy-java&package-manager=gradle&previous-version=1.1.10.3&new-version=1.1.10.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 25b9a05f0d..3a342a71a4 100644 --- a/build.gradle +++ b/build.gradle @@ -430,7 +430,7 @@ configurations { force "io.netty:netty-transport-native-unix-common:${versions.netty}" force "org.apache.bcel:bcel:6.7.0" // This line should be removed once Spotbugs is upgraded to 4.7.4 force "com.github.luben:zstd-jni:${versions.zstd}" - force "org.xerial.snappy:snappy-java:1.1.10.3" + force "org.xerial.snappy:snappy-java:1.1.10.4" force "com.google.guava:guava:${guava_version}" } } @@ -559,7 +559,7 @@ dependencies { runtimeOnly 'io.dropwizard.metrics:metrics-core:4.2.19' 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.3' + runtimeOnly 'org.xerial.snappy:snappy-java:1.1.10.4' runtimeOnly 'org.codehaus.woodstox:stax2-api:4.2.1' runtimeOnly "org.glassfish.jaxb:txw2:${jaxb_version}" runtimeOnly 'com.fasterxml.woodstox:woodstox-core:6.5.1' From 826bdebc8c1c017868f5226191e9ff419e42642f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 07:38:34 -0400 Subject: [PATCH 03/23] dependabot: bump com.google.errorprone:error_prone_annotations from 2.21.1 to 2.22.0 (#3393) Bumps [com.google.errorprone:error_prone_annotations](https://github.com/google/error-prone) from 2.21.1 to 2.22.0.
Release notes

Sourced from com.google.errorprone:error_prone_annotations's releases.

Error Prone 2.22.0

We are considering raising the minimum supported JDK from JDK 11 to JDK 17 in a future release of Error Prone, see #3803. Note that using a newer JDK version to run javac during the build doesn't prevent building code that is deployed to earlier versions, for example it's supported to use the JDK 17 javac and pass --release 11 to compile Java 11 code that is deployed to a JDK 11 runtime. If you have feedback, please comment on #3803.

New checks:

Bug fixes and improvements:

  • Don't complain about literal IP addresses in AddressSelection (https://github.com/google/error-prone/commit/44b65527debbc57892f21ca3ba458b16771e423e)
  • Prevent SuggestedFixes#renameMethod from modifying return type declaration (#4043)
  • Fix UnusedVariable false positives for private record parameters (#2713)
  • When running in conservative mode, no longer assume that implementations of Map.get, etc. return null (#2910)
  • CanIgnoreReturnValueSuggester: Support additional exempting method annotations (#4009)
  • UnusedVariable: exclude junit5's @RegisterExtension (#3892)
  • Support running all available patch checks (#947)
  • Upgrade java-diff-utils 4.0 -> 4.12 (#4081)
  • Flag unused Refaster template parameters (#4060)
  • Support @SuppressWarnings("all") (#4065)
  • Prevent Refaster UMemberSelect from matching method parameters (#2456)
  • MissingDefault : Don't require // fall out comments on expression switches (#2709)
  • Skip UnnecessaryLambda findings for usages in enhanced for loops (#2518)
  • Fix bug where nested MissingBraces violations' suggested fixes result in broken code (#3797)
  • Add support for specifying exemptPrefixes/exemptNames for UnusedVariable via flags (#2753)
  • UnusedMethod: Added exempting variable annotations (#2881)

Full Changelog: https://github.com/google/error-prone/compare/v2.21.1...v2.22.0

Commits
  • 7114c31 Release Error Prone 2.22.0
  • c94d74d Update release.yml - temporarily disable sonatype deployments
  • 080411e Added exempting variable annotations
  • 737dec0 Remove DiffNotApplicableException
  • f3a2bf8 Update ci.yml for JDK 21 release
  • 1d2bc93 Introduce ErrorProneFlags.get{Set,List}OrEmpty, because basically every cal...
  • 1bec842 Fix a crash in UnnecessaryAsync
  • d2ee28e Fix a crash in TimeUnitConversionChecker
  • ac424d0 Tighten the return types in Fix.
  • 58e5bb8 Update and wordsmith the @CompileTimeConstant documentation to reflect chan...
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.google.errorprone:error_prone_annotations&package-manager=gradle&previous-version=2.21.1&new-version=2.22.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3a342a71a4..9fea54298d 100644 --- a/build.gradle +++ b/build.gradle @@ -522,7 +522,7 @@ dependencies { runtimeOnly 'com.eclipsesource.minimal-json:minimal-json:0.9.5' runtimeOnly 'commons-codec:commons-codec:1.16.0' runtimeOnly 'org.cryptacular:cryptacular:1.2.6' - runtimeOnly 'com.google.errorprone:error_prone_annotations:2.21.1' + runtimeOnly 'com.google.errorprone:error_prone_annotations:2.22.0' runtimeOnly 'com.sun.istack:istack-commons-runtime:4.2.0' runtimeOnly 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0' runtimeOnly 'org.ow2.asm:asm:9.5' From b52e762bc449f67d5332a9702040c0f18b2eb986 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 07:44:17 -0400 Subject: [PATCH 04/23] dependabot: bump org.springframework:spring-core from 5.3.29 to 5.3.30 (#3390) Bumps [org.springframework:spring-core](https://github.com/spring-projects/spring-framework) from 5.3.29 to 5.3.30.
Release notes

Sourced from org.springframework:spring-core's releases.

v5.3.30

:star: New Features

  • Optimize ClassUtils#getMostSpecificMethod #31100
  • Optimize whitespace checks in StringUtils #31069
  • Align validation metadata handling in PayloadMethodArgumentResolver #31056
  • Register an override for an existing adapter in ReactiveAdapterRegistry #31048
  • Make bean initialization deterministic for multiple @Autowired methods on same bean class #30994
  • Performance bottlenecks while creating scoped bean instances #30892

:lady_beetle: Bug Fixes

  • Possible classloader leak through incomplete clearing of annotation caches #31176
  • Spring LogFactory implementation deviates from original Apache LogFactory in terms of abstract method declarations #31167
  • Bean injection fails due to nullSafeConciseToString() invoking isEmpty() on a Map/Collection proxy #31156
  • SpelExpressionParser throws IllegalStateException instead of ParseException for invalid expression #31099
  • @DynamicPropertySource in @Nested test class cannot override dynamic properties from enclosing class #31085
  • TransactionalApplicationListenerMethodAdapter should find @TransactionalEventListener on target class method #31037
  • ScheduledAnnotationBeanPostProcessor: graceful shutdown should not interrupt currently running jobs #31020
  • Permgen memory leak due to ClassInfo caching in java.beans.Introspector on JDK 11/17 #31005
  • MethodIntrospector.selectMethods(?) fails to find methods in case of special bridge method arrangement #30907

:notebook_with_decorative_cover: Documentation

  • Fix documentation: Passing in Lists of Values for IN Clause does not work with JdbcTemplate #31229
  • Refine CORS documentation for wildcard processing #31168
  • Propagation REQUIRES_NEW may cause connection pool deadlock #31040
  • Clarify R2DBC ConnectionAccessor and DatabasePopulator exception declarations #30933
  • Doc: Avoid deadlock in @PostConstruct through SmartInitializingSingleton or ContextRefreshedEvent #30889
Commits
  • e5d99ec Release v5.3.30
  • f7bf243 Clarify IN clause resolution with List/Iterable parameter
  • 40678bb Refine CORS documentation for wildcard processing
  • 75faf69 Refine CORS documentation for wildcard processing
  • 39c225c AnnotationUtils.clearCache() includes all annotation caches
  • 0c3d8d7 Align abstract method signatures with original Commons Logging API
  • ddcae04 Do not invoke [Map|Collection].isEmpty() in nullSafeConciseToString()
  • 994bbec Polishing
  • afb378a Consistently throw ParseException instead of IllegalStateException
  • a4fc7d3 Optimize ClassUtils#getMostSpecificMethod
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.springframework:spring-core&package-manager=gradle&previous-version=5.3.29&new-version=5.3.30)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9fea54298d..0a2771ba8e 100644 --- a/build.gradle +++ b/build.gradle @@ -605,7 +605,7 @@ dependencies { testCompileOnly 'org.apiguardian:apiguardian-api:1.1.2' // Kafka test execution testRuntimeOnly 'org.springframework.retry:spring-retry:1.3.4' - testRuntimeOnly ('org.springframework:spring-core:5.3.29') { + testRuntimeOnly ('org.springframework:spring-core:5.3.30') { exclude(group:'org.springframework', module: 'spring-jcl' ) } testRuntimeOnly 'org.scala-lang:scala-library:2.13.12' From 55fe7cb7403f525dcf8414fd94c23faad2890697 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 07:44:25 -0400 Subject: [PATCH 05/23] dependabot: bump org.passay:passay from 1.6.3 to 1.6.4 (#3394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [org.passay:passay](https://github.com/vt-middleware/passay) from 1.6.3 to 1.6.4.
Release notes

Sourced from org.passay:passay's releases.

v1.6.4 Release

See https://www.passay.org/download.html for change log and binaries.

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.passay:passay&package-manager=gradle&previous-version=1.6.3&new-version=1.6.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0a2771ba8e..d3b3846edd 100644 --- a/build.gradle +++ b/build.gradle @@ -505,7 +505,7 @@ dependencies { implementation 'org.apache.commons:commons-collections4:4.4' //Password generation - implementation 'org.passay:passay:1.6.3' + implementation 'org.passay:passay:1.6.4' implementation "org.apache.kafka:kafka-clients:${kafka_version}" From 8eb821b78858d19419d47e58a790f74972fea4fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 08:05:51 -0400 Subject: [PATCH 06/23] dependabot: bump tibdex/github-app-token from 2.0.0 to 2.1.0 (#3395) Bumps [tibdex/github-app-token](https://github.com/tibdex/github-app-token) from 2.0.0 to 2.1.0.
Release notes

Sourced from tibdex/github-app-token's releases.

v2.1.0

Projects willing to keep the token alive after the end of the job can pass revoke: false as an input.

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tibdex/github-app-token&package-manager=github_actions&previous-version=2.0.0&new-version=2.1.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto-release.yml | 2 +- .github/workflows/backport.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 51f9aa8299..f9a5854011 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -13,7 +13,7 @@ jobs: steps: - name: GitHub App token id: github_app_token - uses: tibdex/github-app-token@v2.0.0 + uses: tibdex/github-app-token@v2.1.0 with: app_id: ${{ secrets.APP_ID }} private_key: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 030c51d9cc..374d4d986c 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -16,7 +16,7 @@ jobs: steps: - name: GitHub App token id: github_app_token - uses: tibdex/github-app-token@v2.0.0 + uses: tibdex/github-app-token@v2.1.0 with: app_id: ${{ secrets.APP_ID }} private_key: ${{ secrets.APP_PRIVATE_KEY }} From f09a6aa65fcf363d36052b386c95019719211c07 Mon Sep 17 00:00:00 2001 From: Joshua Palis Date: Mon, 25 Sep 2023 10:30:18 -0700 Subject: [PATCH 07/23] removing opendistro-job-scheduler-lock from list of protected indices (#3383) ### Description Coming from https://github.com/opensearch-project/job-scheduler/issues/492 As mitigation, removing `.opendistro-job-scheduler-lock` from list of protected indices for 3.x/2.x. The lock index will still remain as a system index. ### Check List - [ ] New functionality includes testing - [ ] New functionality has been documented - [X] Commits are signed per the DCO using --signoff By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. For more information on following Developer Certificate of Origin and signing off your commits, please check [here](https://github.com/opensearch-project/OpenSearch/blob/main/CONTRIBUTING.md#developer-certificate-of-origin). Signed-off-by: Joshua Palis --- tools/install_demo_configuration.bat | 2 +- tools/install_demo_configuration.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/install_demo_configuration.bat b/tools/install_demo_configuration.bat index 6bb115fb3e..b08c3bdfae 100755 --- a/tools/install_demo_configuration.bat +++ b/tools/install_demo_configuration.bat @@ -317,7 +317,7 @@ echo plugins.security.enable_snapshot_restore_privilege: true >> "%OPENSEARCH_CO echo plugins.security.check_snapshot_restore_write_privileges: true >> "%OPENSEARCH_CONF_FILE%" echo plugins.security.restapi.roles_enabled: ["all_access", "security_rest_api_access"] >> "%OPENSEARCH_CONF_FILE%" echo plugins.security.system_indices.enabled: true >> "%OPENSEARCH_CONF_FILE%" -echo plugins.security.system_indices.indices: [".plugins-ml-config", ".plugins-ml-connector", ".plugins-ml-model-group", ".plugins-ml-model", ".plugins-ml-task", ".plugins-ml-conversation-meta", ".plugins-ml-conversation-interactions", ".opendistro-alerting-config", ".opendistro-alerting-alert*", ".opendistro-anomaly-results*", ".opendistro-anomaly-detector*", ".opendistro-anomaly-checkpoints", ".opendistro-anomaly-detection-state", ".opendistro-reports-*", ".opensearch-notifications-*", ".opensearch-notebooks", ".opensearch-observability", ".ql-datasources", ".opendistro-asynchronous-search-response*", ".replication-metadata-store", ".opensearch-knn-models", ".geospatial-ip2geo-data*", ".opendistro-job-scheduler-lock"] >> "%OPENSEARCH_CONF_FILE%" +echo plugins.security.system_indices.indices: [".plugins-ml-config", ".plugins-ml-connector", ".plugins-ml-model-group", ".plugins-ml-model", ".plugins-ml-task", ".plugins-ml-conversation-meta", ".plugins-ml-conversation-interactions", ".opendistro-alerting-config", ".opendistro-alerting-alert*", ".opendistro-anomaly-results*", ".opendistro-anomaly-detector*", ".opendistro-anomaly-checkpoints", ".opendistro-anomaly-detection-state", ".opendistro-reports-*", ".opensearch-notifications-*", ".opensearch-notebooks", ".opensearch-observability", ".ql-datasources", ".opendistro-asynchronous-search-response*", ".replication-metadata-store", ".opensearch-knn-models", ".geospatial-ip2geo-data*"] >> "%OPENSEARCH_CONF_FILE%" :: network.host >nul findstr /b /c:"network.host" "%OPENSEARCH_CONF_FILE%" && ( diff --git a/tools/install_demo_configuration.sh b/tools/install_demo_configuration.sh index 7428ea7b14..9fce14aee9 100755 --- a/tools/install_demo_configuration.sh +++ b/tools/install_demo_configuration.sh @@ -385,7 +385,7 @@ echo "plugins.security.enable_snapshot_restore_privilege: true" | $SUDO_CMD tee echo "plugins.security.check_snapshot_restore_write_privileges: true" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null echo 'plugins.security.restapi.roles_enabled: ["all_access", "security_rest_api_access"]' | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null echo 'plugins.security.system_indices.enabled: true' | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null -echo 'plugins.security.system_indices.indices: [".plugins-ml-config", ".plugins-ml-connector", ".plugins-ml-model-group", ".plugins-ml-model", ".plugins-ml-task", ".plugins-ml-conversation-meta", ".plugins-ml-conversation-interactions", ".opendistro-alerting-config", ".opendistro-alerting-alert*", ".opendistro-anomaly-results*", ".opendistro-anomaly-detector*", ".opendistro-anomaly-checkpoints", ".opendistro-anomaly-detection-state", ".opendistro-reports-*", ".opensearch-notifications-*", ".opensearch-notebooks", ".opensearch-observability", ".ql-datasources", ".opendistro-asynchronous-search-response*", ".replication-metadata-store", ".opensearch-knn-models", ".geospatial-ip2geo-data*", ".opendistro-job-scheduler-lock"]' | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null +echo 'plugins.security.system_indices.indices: [".plugins-ml-config", ".plugins-ml-connector", ".plugins-ml-model-group", ".plugins-ml-model", ".plugins-ml-task", ".plugins-ml-conversation-meta", ".plugins-ml-conversation-interactions", ".opendistro-alerting-config", ".opendistro-alerting-alert*", ".opendistro-anomaly-results*", ".opendistro-anomaly-detector*", ".opendistro-anomaly-checkpoints", ".opendistro-anomaly-detection-state", ".opendistro-reports-*", ".opensearch-notifications-*", ".opensearch-notebooks", ".opensearch-observability", ".ql-datasources", ".opendistro-asynchronous-search-response*", ".replication-metadata-store", ".opensearch-knn-models", ".geospatial-ip2geo-data*"]' | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null #network.host if $SUDO_CMD grep --quiet -i "^network.host" "$OPENSEARCH_CONF_FILE"; then From 22f00fa6dfb6c6c722b4574f495ab80f6bf72676 Mon Sep 17 00:00:00 2001 From: Andrey Pleskach Date: Mon, 25 Sep 2023 23:10:19 +0200 Subject: [PATCH 08/23] [Refactor] Change HTTP routes for Audit and Config PUT methods (#3359) Change routs for audit and security configuration PUT methods. The previous configuration used the `{name}` parameter which is confusing since `config` the only allowed value for this parameter. This PR changes routes' configuration and removes useless validation for them. Signed-off-by: Andrey Pleskach --- .../dlic/rest/api/AbstractApiAction.java | 9 +++++-- .../dlic/rest/api/AuditApiAction.java | 16 ++++-------- .../rest/api/SecurityConfigApiAction.java | 25 +++++++------------ .../api/AuditApiActionValidationTest.java | 12 --------- ....java => SecurityConfigApiActionTest.java} | 4 +-- ...SecurityConfigApiActionValidationTest.java | 15 ----------- ...> LegacySecurityConfigApiActionTests.java} | 4 +-- 7 files changed, 25 insertions(+), 60 deletions(-) rename src/test/java/org/opensearch/security/dlic/rest/api/{SecurityConfigApiTest.java => SecurityConfigApiActionTest.java} (98%) rename src/test/java/org/opensearch/security/dlic/rest/api/legacy/{LegacySecurityConfigApiTests.java => LegacySecurityConfigApiActionTests.java} (77%) diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java index 6d3710871b..293f934434 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java @@ -309,8 +309,13 @@ protected final Set patchEntityNames(final JsonNode patchRequestContent) } protected final ValidationResult processPutRequest(final RestRequest request) throws IOException { - return endpointValidator.withRequiredEntityName(nameParam(request)) - .map(entityName -> loadConfigurationWithRequestContent(entityName, request)) + return processPutRequest(nameParam(request), request); + } + + protected final ValidationResult processPutRequest(final String entityName, final RestRequest request) + throws IOException { + return endpointValidator.withRequiredEntityName(entityName) + .map(ignore -> loadConfigurationWithRequestContent(entityName, request)) .map(endpointValidator::onConfigChange) .map(this::addEntityToConfig); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AuditApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AuditApiAction.java index 488a72c958..20e424e959 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AuditApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AuditApiAction.java @@ -38,7 +38,6 @@ import java.util.Set; import static org.opensearch.security.dlic.rest.api.RequestHandler.methodNotImplementedHandler; -import static org.opensearch.security.dlic.rest.api.Responses.badRequestMessage; import static org.opensearch.security.dlic.rest.api.Responses.conflictMessage; import static org.opensearch.security.dlic.rest.api.Responses.methodNotImplementedMessage; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; @@ -124,7 +123,7 @@ public class AuditApiAction extends AbstractApiAction { private static final List routes = addRoutesPrefix( ImmutableList.of( new Route(RestRequest.Method.GET, "/audit/"), - new Route(RestRequest.Method.PUT, "/audit/{name}"), + new Route(RestRequest.Method.PUT, "/audit/config"), new Route(RestRequest.Method.PATCH, "/audit/") ) ); @@ -237,6 +236,9 @@ protected CType getConfigType() { return CType.AUDIT; } + @Override + protected void consumeParameters(RestRequest request) {} + private void auditApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { requestHandlersBuilder.onGetRequest( request -> withEnabledAuditApi(request).map(this::processGetRequest).map(securityConfiguration -> { @@ -248,7 +250,7 @@ private void auditApiRequestHandlers(RequestHandler.RequestHandlersBuilder reque .onChangeRequest(RestRequest.Method.PATCH, request -> withEnabledAuditApi(request).map(this::processPatchRequest)) .onChangeRequest( RestRequest.Method.PUT, - request -> withEnabledAuditApi(request).map(this::withConfigEntityNameOnly).map(ignore -> processPutRequest(request)) + request -> withEnabledAuditApi(request).map(ignore -> processPutRequest("config", request)) ) .override(RestRequest.Method.POST, methodNotImplementedHandler) .override(RestRequest.Method.DELETE, methodNotImplementedHandler); @@ -261,14 +263,6 @@ ValidationResult withEnabledAuditApi(final RestRequest request) { return ValidationResult.success(request); } - ValidationResult withConfigEntityNameOnly(final RestRequest request) { - final var name = nameParam(request); - if (!"config".equals(name)) { - return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("name must be config")); - } - return ValidationResult.success(name); - } - @Override protected EndpointValidator createEndpointValidator() { return new EndpointValidator() { diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiAction.java index 62865cf2e1..6c6ea0f827 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiAction.java @@ -11,12 +11,7 @@ package org.opensearch.security.dlic.rest.api; -import java.util.Collections; -import java.util.List; -import java.util.Map; - import com.google.common.collect.ImmutableList; - import com.google.common.collect.ImmutableMap; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.inject.Inject; @@ -32,8 +27,11 @@ import org.opensearch.security.support.ConfigConstants; import org.opensearch.threadpool.ThreadPool; +import java.util.Collections; +import java.util.List; +import java.util.Map; + import static org.opensearch.security.dlic.rest.api.RequestHandler.methodNotImplementedHandler; -import static org.opensearch.security.dlic.rest.api.Responses.badRequestMessage; import static org.opensearch.security.dlic.rest.api.Responses.methodNotImplementedMessage; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; @@ -43,7 +41,7 @@ public class SecurityConfigApiAction extends AbstractApiAction { private static final List allRoutes = new ImmutableList.Builder().addAll(getRoutes) .addAll( - addRoutesPrefix(ImmutableList.of(new Route(Method.PUT, "/securityconfig/{name}"), new Route(Method.PATCH, "/securityconfig/"))) + addRoutesPrefix(ImmutableList.of(new Route(Method.PUT, "/securityconfig/config"), new Route(Method.PATCH, "/securityconfig/"))) ) .build(); @@ -72,10 +70,13 @@ protected CType getConfigType() { return CType.CONFIG; } + @Override + protected void consumeParameters(RestRequest request) {} + private void securityConfigApiActionRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { requestHandlersBuilder.onChangeRequest( Method.PUT, - request -> withAllowedEndpoint(request).map(this::withConfigEntityNameOnly).map(ignore -> processPutRequest(request)) + request -> withAllowedEndpoint(request).map(ignore -> processPutRequest("config", request)) ) .onChangeRequest(Method.PATCH, request -> withAllowedEndpoint(request).map(this::processPatchRequest)) .override(Method.DELETE, methodNotImplementedHandler) @@ -89,14 +90,6 @@ ValidationResult withAllowedEndpoint(final RestRequest request) { return ValidationResult.success(request); } - ValidationResult withConfigEntityNameOnly(final RestRequest request) { - final var name = nameParam(request); - if (!"config".equals(name)) { - return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("name must be config")); - } - return ValidationResult.success(name); - } - @Override protected EndpointValidator createEndpointValidator() { return new EndpointValidator() { diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AuditApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AuditApiActionValidationTest.java index b42072dcbf..7ffbda2fce 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AuditApiActionValidationTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AuditApiActionValidationTest.java @@ -20,7 +20,6 @@ import org.opensearch.security.util.FakeRestRequest; import java.util.List; -import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -51,17 +50,6 @@ public void enabledAuditApi() { } } - @Test - public void configEntityNameOnly() { - final var auditApiAction = new AuditApiAction(clusterService, threadPool, securityApiDependencies); - var result = auditApiAction.withConfigEntityNameOnly(FakeRestRequest.builder().withParams(Map.of("name", "aaaaa")).build()); - assertFalse(result.isValid()); - assertEquals(RestStatus.BAD_REQUEST, result.status()); - - result = auditApiAction.withConfigEntityNameOnly(FakeRestRequest.builder().withParams(Map.of("name", "config")).build()); - assertTrue(result.isValid()); - } - @Test public void onChangeVerifyReadonlyFields() throws Exception { final var auditApiActionEndpointValidator = new AuditApiAction( diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionTest.java similarity index 98% rename from src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiTest.java rename to src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionTest.java index 8fc6ae1dd8..e0bee59cec 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionTest.java @@ -24,14 +24,14 @@ import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; -public class SecurityConfigApiTest extends AbstractRestApiUnitTest { +public class SecurityConfigApiActionTest extends AbstractRestApiUnitTest { private final String ENDPOINT; protected String getEndpointPrefix() { return PLUGINS_PREFIX; } - public SecurityConfigApiTest() { + public SecurityConfigApiActionTest() { ENDPOINT = getEndpointPrefix() + "/api"; } diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionValidationTest.java index e5993b3698..e3cbfe4120 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionValidationTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionValidationTest.java @@ -17,27 +17,12 @@ import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.util.FakeRestRequest; -import java.util.Map; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class SecurityConfigApiActionValidationTest extends AbstractApiActionValidationTest { - @Test - public void configEntityNameOnly() { - final var securityConfigApiAction = new SecurityConfigApiAction(clusterService, threadPool, securityApiDependencies); - var result = securityConfigApiAction.withConfigEntityNameOnly( - FakeRestRequest.builder().withParams(Map.of("name", "aaaaa")).build() - ); - assertFalse(result.isValid()); - assertEquals(RestStatus.BAD_REQUEST, result.status()); - - result = securityConfigApiAction.withConfigEntityNameOnly(FakeRestRequest.builder().withParams(Map.of("name", "config")).build()); - assertTrue(result.isValid()); - } - @Test public void withAllowedEndpoint() { var securityConfigApiAction = new SecurityConfigApiAction( diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySecurityConfigApiTests.java b/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySecurityConfigApiActionTests.java similarity index 77% rename from src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySecurityConfigApiTests.java rename to src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySecurityConfigApiActionTests.java index 6175809b4a..7d94de03bd 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySecurityConfigApiTests.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySecurityConfigApiActionTests.java @@ -11,11 +11,11 @@ package org.opensearch.security.dlic.rest.api.legacy; -import org.opensearch.security.dlic.rest.api.SecurityConfigApiTest; +import org.opensearch.security.dlic.rest.api.SecurityConfigApiActionTest; import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; -public class LegacySecurityConfigApiTests extends SecurityConfigApiTest { +public class LegacySecurityConfigApiActionTests extends SecurityConfigApiActionTest { @Override protected String getEndpointPrefix() { return LEGACY_OPENDISTRO_PREFIX; From ab891d0cc3b3277f8cbe07f4926d04654d05a70c Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Tue, 26 Sep 2023 07:15:25 -0500 Subject: [PATCH 09/23] Allow for automatic merging of dependabot changes after checks pass (#3408) ### Description Allow for automatic merging of dependabot changes after checks pass ### Issues Resolved - Related https://github.com/opensearch-project/OpenSearch/issues/10211 ### Check List - [ ] New functionality includes testing - [ ] New functionality has been documented - [X] Commits are signed per the DCO using --signoff By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. For more information on following Developer Certificate of Origin and signing off your commits, please check [here](https://github.com/opensearch-project/OpenSearch/blob/main/CONTRIBUTING.md#developer-certificate-of-origin). Signed-off-by: Peter Nied --- .github/workflows/automatic-merges.yml | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/automatic-merges.yml diff --git a/.github/workflows/automatic-merges.yml b/.github/workflows/automatic-merges.yml new file mode 100644 index 0000000000..03c67d9893 --- /dev/null +++ b/.github/workflows/automatic-merges.yml @@ -0,0 +1,31 @@ +name: automatic-merges + +on: + workflow_run: + workflows: + - CI + - Plugin Install + - Code Hygiene + types: completed + +jobs: + automatic-merge-version-bumps: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - id: find-triggering-pr + uses: peternied/find-triggering-pr@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: peternied/discerning-merger@v1 + if: steps.find-triggering-pr.outputs.pr-number != null + with: + token: ${{ secrets.GITHUB_TOKEN }} + pull-request-number: ${{ steps.find-triggering-pr.outputs.pr-number }} + allowed-authors: | + dependabot%5Bbot%5D + allowed-files: | + build.gradle + .github/workflows/*.yml From 8628a89cb2f6f03fa7242390422e845283622391 Mon Sep 17 00:00:00 2001 From: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> Date: Wed, 27 Sep 2023 09:08:18 -0400 Subject: [PATCH 10/23] Demo configuration script requires admin password (#3329) This change requires an alternative to the default credentials for the admin user. The credentials can be provided to the script via: - `initialAdminPassword` environment variable - a file with a single line that contains the password. The admin password for the cluster will be printed to the console output of the `tools/install_demo_configuration.(bat|sh)` Signed-off-by: Stephen Crawford Signed-off-by: Peter Nied Co-authored-by: Peter Nied --- .../action.yml | 13 ++++- .github/workflows/plugin_install.yml | 6 ++- tools/install_demo_configuration.bat | 52 +++++++++++++++++++ tools/install_demo_configuration.sh | 38 ++++++++++++++ 4 files changed, 106 insertions(+), 3 deletions(-) diff --git a/.github/actions/start-opensearch-with-one-plugin/action.yml b/.github/actions/start-opensearch-with-one-plugin/action.yml index fa5681c422..642264f4ec 100644 --- a/.github/actions/start-opensearch-with-one-plugin/action.yml +++ b/.github/actions/start-opensearch-with-one-plugin/action.yml @@ -14,6 +14,10 @@ inputs: description: 'The name of the setup script you want to run i.e. "setup" (do not include file extension). Leave empty to indicate one should not be run.' required: false + admin-password: + description: 'The admin password uses for the cluster' + required: true + runs: using: "composite" steps: @@ -67,6 +71,11 @@ runs: 'y' | .\opensearch-${{ inputs.opensearch-version }}-SNAPSHOT\bin\opensearch-plugin.bat install file:$(pwd)\${{ inputs.plugin-name }}.zip shell: pwsh + - name: Write password to initialAdminPassword location + run: + echo ${{ inputs.admin-password }} >> ./opensearch-${{ env.OPENSEARCH_VERSION }}-SNAPSHOT/config/initialAdminPassword.txt + shell: bash + # Run any configuration scripts - name: Run Setup Script for Linux if: ${{ runner.os == 'Linux' && inputs.setup-script-name != '' }} @@ -101,13 +110,13 @@ runs: # Verify that the server is operational - name: Check OpenSearch Running on Linux if: ${{ runner.os != 'Windows'}} - run: curl https://localhost:9200/_cat/plugins -u 'admin:admin' -k -v + run: curl https://localhost:9200/_cat/plugins -u 'admin:${{ inputs.admin-password }}' -k -v --fail-with-body shell: bash - name: Check OpenSearch Running on Windows if: ${{ runner.os == 'Windows'}} run: | - $credentialBytes = [Text.Encoding]::ASCII.GetBytes("admin:admin") + $credentialBytes = [Text.Encoding]::ASCII.GetBytes("admin:${{ inputs.admin-password }}") $encodedCredentials = [Convert]::ToBase64String($credentialBytes) $baseCredentials = "Basic $encodedCredentials" $Headers = @{ Authorization = $baseCredentials } diff --git a/.github/workflows/plugin_install.yml b/.github/workflows/plugin_install.yml index 5bfce0248b..39901689be 100644 --- a/.github/workflows/plugin_install.yml +++ b/.github/workflows/plugin_install.yml @@ -16,6 +16,9 @@ jobs: runs-on: ${{ matrix.os }} steps: + - id: random-password + uses: peternied/random-name@v1 + - name: Set up JDK uses: actions/setup-java@v3 with: @@ -57,9 +60,10 @@ jobs: opensearch-version: ${{ env.OPENSEARCH_VERSION }} plugin-name: ${{ env.PLUGIN_NAME }} setup-script-name: setup + admin-password: ${{ steps.random-password.outputs.generated_name }} - name: Run sanity tests uses: gradle/gradle-build-action@v2 with: cache-disabled: true - arguments: integTestRemote -Dtests.rest.cluster=localhost:9200 -Dtests.cluster=localhost:9200 -Dtests.clustername="opensearch" -Dhttps=true -Duser=admin -Dpassword=admin + arguments: integTestRemote -Dtests.rest.cluster=localhost:9200 -Dtests.cluster=localhost:9200 -Dtests.clustername="opensearch" -Dhttps=true -Duser=admin -Dpassword=${{ steps.random-password.outputs.generated_name }} -i diff --git a/tools/install_demo_configuration.bat b/tools/install_demo_configuration.bat index b08c3bdfae..d9d30fea2b 100755 --- a/tools/install_demo_configuration.bat +++ b/tools/install_demo_configuration.bat @@ -75,6 +75,7 @@ cd %CUR% echo Basedir: %BASE_DIR% set "OPENSEARCH_CONF_FILE=%BASE_DIR%config\opensearch.yml" +set "INTERNAL_USERS_FILE"=%BASE_DIR%config\opensearch-security\internal_users.yml" set "OPENSEARCH_CONF_DIR=%BASE_DIR%config\" set "OPENSEARCH_BIN_DIR=%BASE_DIR%bin\" set "OPENSEARCH_PLUGINS_DIR=%BASE_DIR%plugins\" @@ -319,6 +320,57 @@ echo plugins.security.restapi.roles_enabled: ["all_access", "security_rest_api_a echo plugins.security.system_indices.enabled: true >> "%OPENSEARCH_CONF_FILE%" echo plugins.security.system_indices.indices: [".plugins-ml-config", ".plugins-ml-connector", ".plugins-ml-model-group", ".plugins-ml-model", ".plugins-ml-task", ".plugins-ml-conversation-meta", ".plugins-ml-conversation-interactions", ".opendistro-alerting-config", ".opendistro-alerting-alert*", ".opendistro-anomaly-results*", ".opendistro-anomaly-detector*", ".opendistro-anomaly-checkpoints", ".opendistro-anomaly-detection-state", ".opendistro-reports-*", ".opensearch-notifications-*", ".opensearch-notebooks", ".opensearch-observability", ".ql-datasources", ".opendistro-asynchronous-search-response*", ".replication-metadata-store", ".opensearch-knn-models", ".geospatial-ip2geo-data*"] >> "%OPENSEARCH_CONF_FILE%" +setlocal enabledelayedexpansion + +set "ADMIN_PASSWORD_FILE=%OPENSEARCH_CONF_DIR%initialAdminPassword.txt" +set "INTERNAL_USERS_FILE=%OPENSEARCH_CONF_DIR%opensearch-security\internal_users.yml" + +echo "what is in the config directory" +dir %OPENSEARCH_CONF_DIR% + +echo "what is in the password file" +type "%ADMIN_PASSWORD_FILE%" + + +if "%initialAdminPassword%" NEQ "" ( + set "ADMIN_PASSWORD=!initialAdminPassword!" +) else ( + for /f %%a in ('type "%ADMIN_PASSWORD_FILE%"') do set "ADMIN_PASSWORD=%%a" +) + +if not defined ADMIN_PASSWORD ( + echo Unable to find the admin password for the cluster. Please set initialAdminPassword or create a file %ADMIN_PASSWORD_FILE% with a single line that contains the password. + exit /b 1 +) + +echo " ***************************************************" +echo " *** ADMIN PASSWORD SET TO: %ADMIN_PASSWORD% ***" +echo " ***************************************************" + +set "HASH_SCRIPT=%OPENSEARCH_PLUGINS_DIR%\opensearch-security\tools\hash.bat" + +REM Run the command and capture its output +for /f %%a in ('%HASH_SCRIPT% -p !ADMIN_PASSWORD!') do ( + set "HASHED_ADMIN_PASSWORD=%%a" +) + +if errorlevel 1 ( + echo Failed to hash the admin password + exit /b 1 +) + +set "default_line= hash: "$2a$12$VcCDgh2NDk07JGN0rjGbM.Ad41qVR/YFJcgHp0UGns5JDymv..TOG"" +set "search=%default_line%" +set "replace= hash: "%HASHED_ADMIN_PASSWORD%"" + +setlocal enableextensions +for /f "delims=" %%i in ('type "%INTERNAL_USERS_FILE%" ^& break ^> "%INTERNAL_USERS_FILE%" ') do ( + set "line=%%i" + setlocal enabledelayedexpansion + >>"%INTERNAL_USERS_FILE%" echo(!line:%search%=%replace%! + endlocal +) + :: network.host >nul findstr /b /c:"network.host" "%OPENSEARCH_CONF_FILE%" && ( echo network.host already present diff --git a/tools/install_demo_configuration.sh b/tools/install_demo_configuration.sh index 9fce14aee9..01bc1bfed1 100755 --- a/tools/install_demo_configuration.sh +++ b/tools/install_demo_configuration.sh @@ -108,6 +108,7 @@ if [ -d "$BASE_DIR" ]; then else echo "DEBUG: basedir does not exist" fi + OPENSEARCH_CONF_FILE="$BASE_DIR/config/opensearch.yml" OPENSEARCH_BIN_DIR="$BASE_DIR/bin" OPENSEARCH_PLUGINS_DIR="$BASE_DIR/plugins" @@ -387,6 +388,43 @@ echo 'plugins.security.restapi.roles_enabled: ["all_access", "security_rest_api_ echo 'plugins.security.system_indices.enabled: true' | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null echo 'plugins.security.system_indices.indices: [".plugins-ml-config", ".plugins-ml-connector", ".plugins-ml-model-group", ".plugins-ml-model", ".plugins-ml-task", ".plugins-ml-conversation-meta", ".plugins-ml-conversation-interactions", ".opendistro-alerting-config", ".opendistro-alerting-alert*", ".opendistro-anomaly-results*", ".opendistro-anomaly-detector*", ".opendistro-anomaly-checkpoints", ".opendistro-anomaly-detection-state", ".opendistro-reports-*", ".opensearch-notifications-*", ".opensearch-notebooks", ".opensearch-observability", ".ql-datasources", ".opendistro-asynchronous-search-response*", ".replication-metadata-store", ".opensearch-knn-models", ".geospatial-ip2geo-data*"]' | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null +## Read the admin password from the file or use the initialAdminPassword if set +ADMIN_PASSWORD_FILE="$OPENSEARCH_CONF_DIR/initialAdminPassword.txt" +INTERNAL_USERS_FILE="$OPENSEARCH_CONF_DIR/opensearch-security/internal_users.yml" + +if [[ -n "$initialAdminPassword" ]]; then + ADMIN_PASSWORD="$initialAdminPassword" +elif [[ -f "$ADMIN_PASSWORD_FILE" && -s "$ADMIN_PASSWORD_FILE" ]]; then + ADMIN_PASSWORD=$(head -n 1 "$ADMIN_PASSWORD_FILE") +else + echo "Unable to find the admin password for the cluster. Please run 'export initialAdminPassword=' or create a file $ADMIN_PASSWORD_FILE with a single line that contains the password." + exit 1 +fi + +echo " ***************************************************" +echo " *** ADMIN PASSWORD SET TO: $ADMIN_PASSWORD ***" +echo " ***************************************************" + +$SUDO_CMD chmod +x "$OPENSEARCH_PLUGINS_DIR/opensearch-security/tools/hash.sh" + +# Use the Hasher script to hash the admin password +HASHED_ADMIN_PASSWORD=$($OPENSEARCH_PLUGINS_DIR/opensearch-security/tools/hash.sh -p "$ADMIN_PASSWORD" | tail -n 1) + +if [ $? -ne 0 ]; then + echo "Hash the admin password failure, see console for details" + exit 1 +fi + +# Find the line number containing 'admin:' in the internal_users.yml file +ADMIN_HASH_LINE=$(grep -n 'admin:' "$INTERNAL_USERS_FILE" | cut -f1 -d:) + +awk -v hashed_admin_password="$HASHED_ADMIN_PASSWORD" ' + /^ *hash: *"\$2a\$12\$VcCDgh2NDk07JGN0rjGbM.Ad41qVR\/YFJcgHp0UGns5JDymv..TOG"/ { + sub(/"\$2a\$12\$VcCDgh2NDk07JGN0rjGbM.Ad41qVR\/YFJcgHp0UGns5JDymv..TOG"/, "\"" hashed_admin_password "\""); + } + { print } +' "$INTERNAL_USERS_FILE" > temp_file && mv temp_file "$INTERNAL_USERS_FILE" + #network.host if $SUDO_CMD grep --quiet -i "^network.host" "$OPENSEARCH_CONF_FILE"; then : #already present From 22f44d3bccf93a4af2562d26694094ec7151669f Mon Sep 17 00:00:00 2001 From: Paras Jain Date: Sat, 30 Sep 2023 04:11:10 +0530 Subject: [PATCH 11/23] Improve serialization speeds (#2802) Use custom serialization in security plugin. - Resolves https://github.com/opensearch-project/security/issues/2780 Signed-off-by: Paras Jain Signed-off-by: Peter Nied Co-authored-by: Paras Jain Co-authored-by: Peter Nied --- .github/workflows/ci.yml | 18 + bwc-test/build.gradle | 4 + .../SecurityBackwardsCompatibilityIT.java | 205 ---------- .../opensearch/security/bwc/ClusterType.java | 28 ++ .../bwc/SecurityBackwardsCompatibilityIT.java | 367 ++++++++++++++++++ .../org/opensearch/security/bwc/Song.java | 117 ++++++ .../security/bwc/helper/RestHelper.java | 90 +++++ .../com/amazon/dlic/auth/ldap/LdapUser.java | 15 + .../auditlog/impl/AbstractAuditLog.java | 6 +- .../security/auth/UserInjector.java | 15 +- .../configuration/DlsFlsValveImpl.java | 16 +- .../security/filter/SecurityFilter.java | 4 + .../transport/SecuritySSLRequestHandler.java | 6 + .../security/support/Base64CustomHelper.java | 225 +++++++++++ .../security/support/Base64Helper.java | 183 ++------- .../security/support/Base64JDKHelper.java | 156 ++++++++ .../security/support/ConfigConstants.java | 5 + .../security/support/HeaderHelper.java | 16 +- .../support/SafeSerializationUtils.java | 81 ++++ .../security/support/SourceFieldsContext.java | 26 +- .../security/support/StreamableRegistry.java | 134 +++++++ .../transport/SecurityInterceptor.java | 31 +- .../transport/SecurityRequestHandler.java | 6 +- .../org/opensearch/security/user/User.java | 9 +- .../support/Base64CustomHelperTest.java | 159 ++++++++ .../security/support/Base64HelperTest.java | 90 +---- .../security/support/Base64JDKHelperTest.java | 128 ++++++ .../support/StreamableRegistryTest.java | 29 ++ .../transport/SecurityInterceptorTests.java | 65 +++- .../SecuritySSLRequestHandlerTests.java | 80 ++++ 30 files changed, 1842 insertions(+), 472 deletions(-) delete mode 100644 bwc-test/src/test/java/SecurityBackwardsCompatibilityIT.java create mode 100644 bwc-test/src/test/java/org/opensearch/security/bwc/ClusterType.java create mode 100644 bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java create mode 100644 bwc-test/src/test/java/org/opensearch/security/bwc/Song.java create mode 100644 bwc-test/src/test/java/org/opensearch/security/bwc/helper/RestHelper.java create mode 100644 src/main/java/org/opensearch/security/support/Base64CustomHelper.java create mode 100644 src/main/java/org/opensearch/security/support/Base64JDKHelper.java create mode 100644 src/main/java/org/opensearch/security/support/SafeSerializationUtils.java create mode 100644 src/main/java/org/opensearch/security/support/StreamableRegistry.java create mode 100644 src/test/java/org/opensearch/security/support/Base64CustomHelperTest.java create mode 100644 src/test/java/org/opensearch/security/support/Base64JDKHelperTest.java create mode 100644 src/test/java/org/opensearch/security/support/StreamableRegistryTest.java create mode 100644 src/test/java/org/opensearch/security/transport/SecuritySSLRequestHandlerTests.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5d5e3430d..9e58ff4b4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,6 +108,24 @@ jobs: arguments: | integrationTest -Dbuild.snapshot=false + backward-compatibility-build: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-java@v3 + with: + distribution: temurin # Temurin is a distribution of adoptium + java-version: 17 + + - name: Checkout Security Repo + uses: actions/checkout@v4 + + - name: Build BWC tests + uses: gradle/gradle-build-action@v2 + with: + cache-disabled: true + arguments: | + -p bwc-test build -x test -x integTest + backward-compatibility: strategy: fail-fast: false diff --git a/bwc-test/build.gradle b/bwc-test/build.gradle index 24cc645ba1..6fb7fc2348 100644 --- a/bwc-test/build.gradle +++ b/bwc-test/build.gradle @@ -47,6 +47,7 @@ buildscript { opensearch_version = System.getProperty("opensearch.version", "3.0.0-SNAPSHOT") opensearch_group = "org.opensearch" common_utils_version = System.getProperty("common_utils.version", '2.9.0.0-SNAPSHOT') + jackson_version = System.getProperty("jackson_version", "2.15.2") } repositories { mavenLocal() @@ -72,6 +73,9 @@ dependencies { testImplementation "org.opensearch.test:framework:${opensearch_version}" testImplementation "org.apache.logging.log4j:log4j-core:${versions.log4j}" testImplementation "org.opensearch:common-utils:${common_utils_version}" + testImplementation "com.fasterxml.jackson.core:jackson-databind:${jackson_version}" + testImplementation "com.fasterxml.jackson.core:jackson-annotations:${jackson_version}" + } loggerUsageCheck.enabled = false diff --git a/bwc-test/src/test/java/SecurityBackwardsCompatibilityIT.java b/bwc-test/src/test/java/SecurityBackwardsCompatibilityIT.java deleted file mode 100644 index 3758b43265..0000000000 --- a/bwc-test/src/test/java/SecurityBackwardsCompatibilityIT.java +++ /dev/null @@ -1,205 +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. - */ -package org.opensearch.security.bwc; - -import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -import org.apache.hc.client5.http.auth.AuthScope; -import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; -import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; -import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; -import org.apache.hc.client5.http.nio.AsyncClientConnectionManager; -import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; -import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; -import org.apache.hc.core5.function.Factory; -import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpHost; -import org.apache.hc.core5.http.message.BasicHeader; -import org.apache.hc.core5.http.nio.ssl.TlsStrategy; -import org.apache.hc.core5.reactor.ssl.TlsDetails; -import org.apache.hc.core5.ssl.SSLContextBuilder; -import org.junit.Assume; -import org.junit.Before; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.test.rest.OpenSearchRestTestCase; - -import org.opensearch.Version; -import org.opensearch.common.settings.Settings; -import org.opensearch.test.rest.OpenSearchRestTestCase; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasItem; - -import org.opensearch.client.RestClient; -import org.opensearch.client.RestClientBuilder; - -import org.junit.Assert; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; - -public class SecurityBackwardsCompatibilityIT extends OpenSearchRestTestCase { - - private ClusterType CLUSTER_TYPE; - private String CLUSTER_NAME; - - @Before - private void testSetup() { - final String bwcsuiteString = System.getProperty("tests.rest.bwcsuite"); - Assume.assumeTrue("Test cannot be run outside the BWC gradle task 'bwcTestSuite' or its dependent tasks", bwcsuiteString != null); - CLUSTER_TYPE = ClusterType.parse(bwcsuiteString); - CLUSTER_NAME = System.getProperty("tests.clustername"); - } - - @Override - protected final boolean preserveClusterUponCompletion() { - return true; - } - - @Override - protected final boolean preserveIndicesUponCompletion() { - return true; - } - - @Override - protected final boolean preserveReposUponCompletion() { - return true; - } - - @Override - protected boolean preserveTemplatesUponCompletion() { - return true; - } - - @Override - protected String getProtocol() { - return "https"; - } - - @Override - protected final Settings restClientSettings() { - return Settings.builder() - .put(super.restClientSettings()) - // increase the timeout here to 90 seconds to handle long waits for a green - // cluster health. the waits for green need to be longer than a minute to - // account for delayed shards - .put(OpenSearchRestTestCase.CLIENT_SOCKET_TIMEOUT, "90s") - .build(); - } - - @Override - protected RestClient buildClient(Settings settings, HttpHost[] hosts) throws IOException { - RestClientBuilder builder = RestClient.builder(hosts); - configureHttpsClient(builder, settings); - boolean strictDeprecationMode = settings.getAsBoolean("strictDeprecationMode", true); - builder.setStrictDeprecationMode(strictDeprecationMode); - return builder.build(); - } - - protected static void configureHttpsClient(RestClientBuilder builder, Settings settings) throws IOException { - Map headers = ThreadContext.buildDefaultHeaders(settings); - Header[] defaultHeaders = new Header[headers.size()]; - int i = 0; - for (Map.Entry entry : headers.entrySet()) { - defaultHeaders[i++] = new BasicHeader(entry.getKey(), entry.getValue()); - } - builder.setDefaultHeaders(defaultHeaders); - builder.setHttpClientConfigCallback(httpClientBuilder -> { - String userName = Optional.ofNullable(System.getProperty("tests.opensearch.username")) - .orElseThrow(() -> new RuntimeException("user name is missing")); - String password = Optional.ofNullable(System.getProperty("tests.opensearch.password")) - .orElseThrow(() -> new RuntimeException("password is missing")); - BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); - credentialsProvider.setCredentials(new AuthScope(null, -1), new UsernamePasswordCredentials(userName, password.toCharArray())); - try { - SSLContext sslContext = SSLContextBuilder.create().loadTrustMaterial(null, (chains, authType) -> true).build(); - - TlsStrategy tlsStrategy = ClientTlsStrategyBuilder.create() - .setSslContext(sslContext) - .setTlsVersions(new String[] { "TLSv1", "TLSv1.1", "TLSv1.2", "SSLv3" }) - .setHostnameVerifier(NoopHostnameVerifier.INSTANCE) - // See please https://issues.apache.org/jira/browse/HTTPCLIENT-2219 - .setTlsDetailsFactory(new Factory() { - @Override - public TlsDetails create(final SSLEngine sslEngine) { - return new TlsDetails(sslEngine.getSession(), sslEngine.getApplicationProtocol()); - } - }) - .build(); - - final AsyncClientConnectionManager cm = PoolingAsyncClientConnectionManagerBuilder.create() - .setTlsStrategy(tlsStrategy) - .build(); - return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider).setConnectionManager(cm); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } - - public void testBasicBackwardsCompatibility() throws Exception { - String round = System.getProperty("tests.rest.bwcsuite_round"); - - if (round.equals("first") || round.equals("old")) { - assertPluginUpgrade("_nodes/" + CLUSTER_NAME + "-0/plugins"); - } else if (round.equals("second")) { - assertPluginUpgrade("_nodes/" + CLUSTER_NAME + "-1/plugins"); - } else if (round.equals("third")) { - assertPluginUpgrade("_nodes/" + CLUSTER_NAME + "-2/plugins"); - } - } - - @SuppressWarnings("unchecked") - public void testWhoAmI() throws Exception { - Map responseMap = (Map) getAsMap("_plugins/_security/whoami"); - Assert.assertTrue(responseMap.containsKey("dn")); - } - - private enum ClusterType { - OLD, - MIXED, - UPGRADED; - - public static ClusterType parse(String value) { - switch (value) { - case "old_cluster": - return OLD; - case "mixed_cluster": - return MIXED; - case "upgraded_cluster": - return UPGRADED; - default: - throw new AssertionError("unknown cluster type: " + value); - } - } - } - - @SuppressWarnings("unchecked") - private void assertPluginUpgrade(String uri) throws Exception { - Map> responseMap = (Map>) getAsMap(uri).get("nodes"); - for (Map response : responseMap.values()) { - List> plugins = (List>) response.get("plugins"); - Set pluginNames = plugins.stream().map(map -> (String) map.get("name")).collect(Collectors.toSet()); - - final Version minNodeVersion = this.minimumNodeVersion(); - - if (minNodeVersion.major <= 1) { - assertThat(pluginNames, hasItem("opensearch_security")); - } else { - assertThat(pluginNames, hasItem("opensearch-security")); - } - - } - } -} diff --git a/bwc-test/src/test/java/org/opensearch/security/bwc/ClusterType.java b/bwc-test/src/test/java/org/opensearch/security/bwc/ClusterType.java new file mode 100644 index 0000000000..7fe849d5b3 --- /dev/null +++ b/bwc-test/src/test/java/org/opensearch/security/bwc/ClusterType.java @@ -0,0 +1,28 @@ +/* + * 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.bwc; + +public enum ClusterType { + OLD, + MIXED, + UPGRADED; + + public static ClusterType parse(String value) { + switch (value) { + case "old_cluster": + return OLD; + case "mixed_cluster": + return MIXED; + case "upgraded_cluster": + return UPGRADED; + default: + throw new AssertionError("unknown cluster type: " + value); + } + } +} 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 new file mode 100644 index 0000000000..1647dbb132 --- /dev/null +++ b/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java @@ -0,0 +1,367 @@ +/* + * 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.bwc; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import javax.net.ssl.SSLContext; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.client5.http.nio.AsyncClientConnectionManager; +import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.hc.core5.http.nio.ssl.TlsStrategy; +import org.apache.hc.core5.reactor.ssl.TlsDetails; +import org.apache.hc.core5.ssl.SSLContextBuilder; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.opensearch.client.Response; +import org.opensearch.client.ResponseException; +import org.opensearch.client.RestClient; +import org.opensearch.client.RestClientBuilder; +import org.opensearch.common.Randomness; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.common.util.io.IOUtils; +import org.opensearch.security.bwc.helper.RestHelper; +import org.opensearch.test.rest.OpenSearchRestTestCase; +import org.opensearch.Version; + +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.equalTo; + +public class SecurityBackwardsCompatibilityIT extends OpenSearchRestTestCase { + + private ClusterType CLUSTER_TYPE; + private String CLUSTER_NAME; + + private final String TEST_USER = "user"; + private final String TEST_PASSWORD = "290735c0-355d-4aaf-9b42-1aaa1f2a3cee"; + private final String TEST_ROLE = "test-dls-fls-role"; + private static RestClient testUserRestClient = null; + + @Before + public void testSetup() { + final String bwcsuiteString = System.getProperty("tests.rest.bwcsuite"); + Assume.assumeTrue("Test cannot be run outside the BWC gradle task 'bwcTestSuite' or its dependent tasks", bwcsuiteString != null); + CLUSTER_TYPE = ClusterType.parse(bwcsuiteString); + CLUSTER_NAME = System.getProperty("tests.clustername"); + if (testUserRestClient == null) { + testUserRestClient = buildClient( + super.restClientSettings(), + super.getClusterHosts().toArray(new HttpHost[0]), + TEST_USER, + TEST_PASSWORD + ); + } + } + + @Override + protected final boolean preserveClusterUponCompletion() { + return true; + } + + @Override + protected final boolean preserveIndicesUponCompletion() { + return true; + } + + @Override + protected final boolean preserveReposUponCompletion() { + return true; + } + + @Override + protected boolean preserveTemplatesUponCompletion() { + return true; + } + + @Override + protected String getProtocol() { + return "https"; + } + + @Override + protected final Settings restClientSettings() { + return Settings.builder() + .put(super.restClientSettings()) + // increase the timeout here to 90 seconds to handle long waits for a green + // cluster health. the waits for green need to be longer than a minute to + // account for delayed shards + .put(OpenSearchRestTestCase.CLIENT_SOCKET_TIMEOUT, "90s") + .build(); + } + + protected RestClient buildClient(Settings settings, HttpHost[] hosts, String username, String password) { + RestClientBuilder builder = RestClient.builder(hosts); + configureHttpsClient(builder, settings, username, password); + boolean strictDeprecationMode = settings.getAsBoolean("strictDeprecationMode", true); + builder.setStrictDeprecationMode(strictDeprecationMode); + return builder.build(); + } + + @Override + protected RestClient buildClient(Settings settings, HttpHost[] hosts) { + String username = Optional.ofNullable(System.getProperty("tests.opensearch.username")) + .orElseThrow(() -> new RuntimeException("user name is missing")); + String password = Optional.ofNullable(System.getProperty("tests.opensearch.password")) + .orElseThrow(() -> new RuntimeException("password is missing")); + return buildClient(super.restClientSettings(), super.getClusterHosts().toArray(new HttpHost[0]), username, password); + } + + private static void configureHttpsClient(RestClientBuilder builder, Settings settings, String userName, String password) { + Map headers = ThreadContext.buildDefaultHeaders(settings); + Header[] defaultHeaders = new Header[headers.size()]; + int i = 0; + for (Map.Entry entry : headers.entrySet()) { + defaultHeaders[i++] = new BasicHeader(entry.getKey(), entry.getValue()); + } + builder.setDefaultHeaders(defaultHeaders); + builder.setHttpClientConfigCallback(httpClientBuilder -> { + BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(new AuthScope(null, -1), new UsernamePasswordCredentials(userName, password.toCharArray())); + try { + SSLContext sslContext = SSLContextBuilder.create().loadTrustMaterial(null, (chains, authType) -> true).build(); + + TlsStrategy tlsStrategy = ClientTlsStrategyBuilder.create() + .setSslContext(sslContext) + .setTlsVersions(new String[] { "TLSv1", "TLSv1.1", "TLSv1.2", "SSLv3" }) + .setHostnameVerifier(NoopHostnameVerifier.INSTANCE) + // See please https://issues.apache.org/jira/browse/HTTPCLIENT-2219 + .setTlsDetailsFactory(sslEngine -> new TlsDetails(sslEngine.getSession(), sslEngine.getApplicationProtocol())) + .build(); + + final AsyncClientConnectionManager cm = PoolingAsyncClientConnectionManagerBuilder.create() + .setTlsStrategy(tlsStrategy) + .build(); + return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider).setConnectionManager(cm); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + public void testWhoAmI() throws Exception { + Map responseMap = getAsMap("_plugins/_security/whoami"); + assertThat(responseMap, hasKey("dn")); + } + + public void testBasicBackwardsCompatibility() throws Exception { + String round = System.getProperty("tests.rest.bwcsuite_round"); + + if (round.equals("first") || round.equals("old")) { + assertPluginUpgrade("_nodes/" + CLUSTER_NAME + "-0/plugins"); + } else if (round.equals("second")) { + assertPluginUpgrade("_nodes/" + CLUSTER_NAME + "-1/plugins"); + } else if (round.equals("third")) { + assertPluginUpgrade("_nodes/" + CLUSTER_NAME + "-2/plugins"); + } + } + + /** + * Tests backward compatibility by created a test user and role with DLS, FLS and masked field settings. Ingests + * data into a test index and runs a matchAll query against the same. + */ + public void testDataIngestionAndSearchBackwardsCompatibility() throws Exception { + String round = System.getProperty("tests.rest.bwcsuite_round"); + String index = "test_index"; + if (round.equals("old")) { + createTestRoleIfNotExists(TEST_ROLE); + createUserIfNotExists(TEST_USER, TEST_PASSWORD, TEST_ROLE); + createIndexIfNotExists(index); + } + ingestData(index); + searchMatchAll(index); + } + + public void testNodeStats() throws IOException { + List responses = RestHelper.requestAgainstAllNodes(client(), "GET", "_nodes/stats", null); + responses.forEach(r -> Assert.assertEquals(200, r.getStatusLine().getStatusCode())); + } + + @SuppressWarnings("unchecked") + private void assertPluginUpgrade(String uri) throws Exception { + Map> responseMap = (Map>) getAsMap(uri).get("nodes"); + for (Map response : responseMap.values()) { + List> plugins = (List>) response.get("plugins"); + Set pluginNames = plugins.stream().map(map -> (String) map.get("name")).collect(Collectors.toSet()); + + final Version minNodeVersion = minimumNodeVersion(); + + if (minNodeVersion.major <= 1) { + assertThat(pluginNames, hasItem("opensearch_security")); // With underscore seperator + } else { + assertThat(pluginNames, hasItem("opensearch-security")); // With dash seperator + } + } + } + + /** + * Ingests data into the test index + * @param index index to ingest data into + */ + + private void ingestData(String index) throws IOException { + StringBuilder bulkRequestBody = new StringBuilder(); + ObjectMapper objectMapper = new ObjectMapper(); + int numberOfRequests = Randomness.get().nextInt(10); + while (numberOfRequests-- > 0) { + for (int i = 0; i < Randomness.get().nextInt(100); i++) { + Map> indexRequest = new HashMap<>(); + indexRequest.put("index", new HashMap<>() { + { + put("_index", index); + } + }); + bulkRequestBody.append(objectMapper.writeValueAsString(indexRequest) + "\n"); + bulkRequestBody.append(objectMapper.writeValueAsString(Song.randomSong().asJson()) + "\n"); + } + List responses = RestHelper.requestAgainstAllNodes( + testUserRestClient, + "POST", + "_bulk?refresh=wait_for", + RestHelper.toHttpEntity(bulkRequestBody.toString()) + ); + responses.forEach(r -> assertEquals(200, r.getStatusLine().getStatusCode())); + } + } + + /** + * Runs a matchAll query against the test index + * @param index index to search + */ + private void searchMatchAll(String index) throws IOException { + String matchAllQuery = "{\n" + " \"query\": {\n" + " \"match_all\": {}\n" + " }\n" + "}"; + int numberOfRequests = Randomness.get().nextInt(10); + while (numberOfRequests-- > 0) { + List responses = RestHelper.requestAgainstAllNodes( + testUserRestClient, + "POST", + index + "/_search", + RestHelper.toHttpEntity(matchAllQuery) + ); + responses.forEach(r -> assertEquals(200, r.getStatusLine().getStatusCode())); + } + } + + /** + * Checks if a resource at the specified URL exists + * @param url of the resource to be checked for existence + * @return true if the resource exists, false otherwise + */ + + private boolean resourceExists(String url) throws IOException { + try { + RestHelper.get(adminClient(), url); + return true; + } catch (ResponseException e) { + if (e.getResponse().getStatusLine().getStatusCode() == 404) { + return false; + } else { + throw e; + } + } + } + + /** + * Creates a test role with DLS, FLS and masked field settings on the test index. + */ + 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"; + Response response = RestHelper.makeRequest(adminClient(), "PUT", url, RestHelper.toHttpEntity(roleSettings)); + + assertThat(response.getStatusLine().getStatusCode(), anyOf(equalTo(200), equalTo(201))); + } + + /** + * Creates a test index if it does not exist already + * @param index index to create + */ + + private void createIndexIfNotExists(String index) throws IOException { + String settings = "{\n" + + " \"settings\": {\n" + + " \"index\": {\n" + + " \"number_of_shards\": 3,\n" + + " \"number_of_replicas\": 1\n" + + " }\n" + + " }\n" + + "}"; + if (!resourceExists(index)) { + Response response = RestHelper.makeRequest(client(), "PUT", index, RestHelper.toHttpEntity(settings)); + assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); + } + } + + /** + * Creates the test user if it does not exist already and maps it to the test role with DLS/FLS settings. + * @param user user to be created + * @param password password for the new user + * @param role roles that the user has to be mapped to + */ + private void createUserIfNotExists(String user, String password, String role) throws IOException { + String url = "_plugins/_security/api/internalusers/" + user; + if (!resourceExists(url)) { + String userSettings = String.format( + Locale.ENGLISH, + "{\n" + " \"password\": \"%s\",\n" + " \"opendistro_security_roles\": [\"%s\"],\n" + " \"backend_roles\": []\n" + "}", + password, + role + ); + Response response = RestHelper.makeRequest(adminClient(), "PUT", url, RestHelper.toHttpEntity(userSettings)); + assertThat(response.getStatusLine().getStatusCode(), equalTo(201)); + } + } + + @AfterClass + public static void cleanUp() throws IOException { + OpenSearchRestTestCase.closeClients(); + IOUtils.close(testUserRestClient); + } +} diff --git a/bwc-test/src/test/java/org/opensearch/security/bwc/Song.java b/bwc-test/src/test/java/org/opensearch/security/bwc/Song.java new file mode 100644 index 0000000000..3cfd2c03e8 --- /dev/null +++ b/bwc-test/src/test/java/org/opensearch/security/bwc/Song.java @@ -0,0 +1,117 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +package org.opensearch.security.bwc; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.opensearch.common.Randomness; + +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +public class Song { + + public static final String FIELD_TITLE = "title"; + public static final String FIELD_ARTIST = "artist"; + public static final String FIELD_LYRICS = "lyrics"; + public static final String FIELD_STARS = "stars"; + public static final String FIELD_GENRE = "genre"; + public static final String ARTIST_FIRST = "First artist"; + public static final String ARTIST_STRING = "String"; + public static final String ARTIST_TWINS = "Twins"; + public static final String TITLE_MAGNUM_OPUS = "Magnum Opus"; + public static final String TITLE_SONG_1_PLUS_1 = "Song 1+1"; + public static final String TITLE_NEXT_SONG = "Next song"; + public static final String ARTIST_NO = "No!"; + public static final String TITLE_POISON = "Poison"; + + public static final String ARTIST_YES = "yes"; + + public static final String TITLE_AFFIRMATIVE = "Affirmative"; + + public static final String ARTIST_UNKNOWN = "unknown"; + public static final String TITLE_CONFIDENTIAL = "confidential"; + + public static final String LYRICS_1 = "Very deep subject"; + public static final String LYRICS_2 = "Once upon a time"; + public static final String LYRICS_3 = "giant nonsense"; + public static final String LYRICS_4 = "Much too much"; + public static final String LYRICS_5 = "Little to little"; + public static final String LYRICS_6 = "confidential secret classified"; + + public static final String GENRE_ROCK = "rock"; + public static final String GENRE_JAZZ = "jazz"; + public static final String GENRE_BLUES = "blues"; + + public static final String QUERY_TITLE_NEXT_SONG = FIELD_TITLE + ":" + "\"" + TITLE_NEXT_SONG + "\""; + public static final String QUERY_TITLE_POISON = FIELD_TITLE + ":" + TITLE_POISON; + public static final String QUERY_TITLE_MAGNUM_OPUS = FIELD_TITLE + ":" + TITLE_MAGNUM_OPUS; + + public static final Song[] SONGS = { + new Song(ARTIST_FIRST, TITLE_MAGNUM_OPUS, LYRICS_1, 1, GENRE_ROCK), + new Song(ARTIST_STRING, TITLE_SONG_1_PLUS_1, LYRICS_2, 2, GENRE_BLUES), + new Song(ARTIST_TWINS, TITLE_NEXT_SONG, LYRICS_3, 3, GENRE_JAZZ), + new Song(ARTIST_NO, TITLE_POISON, LYRICS_4, 4, GENRE_ROCK), + new Song(ARTIST_YES, TITLE_AFFIRMATIVE, LYRICS_5, 5, GENRE_BLUES), + new Song(ARTIST_UNKNOWN, TITLE_CONFIDENTIAL, LYRICS_6, 6, GENRE_JAZZ) }; + + private final String artist; + private final String title; + private final String lyrics; + private final Integer stars; + private final String genre; + + public Song(String artist, String title, String lyrics, Integer stars, String genre) { + this.artist = Objects.requireNonNull(artist, "Artist is required"); + this.title = Objects.requireNonNull(title, "Title is required"); + this.lyrics = Objects.requireNonNull(lyrics, "Lyrics is required"); + this.stars = Objects.requireNonNull(stars, "Stars field is required"); + this.genre = Objects.requireNonNull(genre, "Genre field is required"); + } + + public String getArtist() { + return artist; + } + + public String getTitle() { + return title; + } + + public String getLyrics() { + return lyrics; + } + + public Integer getStars() { + return stars; + } + + public String getGenre() { + return genre; + } + + public Map asMap() { + return Map.of(FIELD_ARTIST, artist, FIELD_TITLE, title, FIELD_LYRICS, lyrics, FIELD_STARS, stars, FIELD_GENRE, genre); + } + + public String asJson() throws JsonProcessingException { + return new ObjectMapper().writeValueAsString(this.asMap()); + } + + public static Song randomSong() { + return new Song( + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + Randomness.get().nextInt(5), + UUID.randomUUID().toString() + ); + } +} diff --git a/bwc-test/src/test/java/org/opensearch/security/bwc/helper/RestHelper.java b/bwc-test/src/test/java/org/opensearch/security/bwc/helper/RestHelper.java new file mode 100644 index 0000000000..3272ac736a --- /dev/null +++ b/bwc-test/src/test/java/org/opensearch/security/bwc/helper/RestHelper.java @@ -0,0 +1,90 @@ +/* + * 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.bwc.helper; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.client.Request; +import org.opensearch.client.RequestOptions; +import org.opensearch.client.Response; +import org.opensearch.client.RestClient; +import org.opensearch.client.WarningsHandler; + +import static org.apache.hc.core5.http.ContentType.APPLICATION_JSON; + +public class RestHelper { + + private static final Logger log = LogManager.getLogger(RestHelper.class); + + public static HttpEntity toHttpEntity(String jsonString) { + return new StringEntity(jsonString, APPLICATION_JSON); + } + + public static Response get(RestClient client, String url) throws IOException { + return makeRequest(client, "GET", url, null, null); + } + + public static Response makeRequest(RestClient client, String method, String endpoint, HttpEntity entity) throws IOException { + return makeRequest(client, method, endpoint, entity, null); + } + + public static Response makeRequest(RestClient client, String method, String endpoint, HttpEntity entity, List
headers) + throws IOException { + log.info("Making request " + method + " " + endpoint + ", with headers " + headers); + + Request request = new Request(method, endpoint); + + RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); + options.setWarningsHandler(WarningsHandler.PERMISSIVE); + if (headers != null) { + headers.forEach(header -> options.addHeader(header.getName(), header.getValue())); + } + request.setOptions(options.build()); + + if (entity != null) { + request.setEntity(entity); + } + + Response response = client.performRequest(request); + log.info("Recieved response " + response.getStatusLine()); + return response; + } + + public static List requestAgainstAllNodes(RestClient client, String method, String endpoint, HttpEntity entity) + throws IOException { + return requestAgainstAllNodes(client, method, endpoint, entity, null); + } + + public static List requestAgainstAllNodes( + RestClient client, + String method, + String endpoint, + HttpEntity entity, + List
headers + ) throws IOException { + int nodeCount = client.getNodes().size(); + List responses = new ArrayList<>(); + while (nodeCount-- > 0) { + responses.add(makeRequest(client, method, endpoint, entity, headers)); + } + return responses; + } + + public static Header getAuthorizationHeader(String username, String password) { + return new BasicHeader("Authorization", "Basic " + username + ":" + password); + } +} diff --git a/src/main/java/com/amazon/dlic/auth/ldap/LdapUser.java b/src/main/java/com/amazon/dlic/auth/ldap/LdapUser.java index 907d605860..f752ce4a49 100755 --- a/src/main/java/com/amazon/dlic/auth/ldap/LdapUser.java +++ b/src/main/java/com/amazon/dlic/auth/ldap/LdapUser.java @@ -11,6 +11,7 @@ package com.amazon.dlic.auth.ldap; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -20,6 +21,8 @@ import com.amazon.dlic.auth.ldap.util.Utils; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.user.User; @@ -45,6 +48,12 @@ public LdapUser( attributes.putAll(extractLdapAttributes(originalUsername, userEntry, customAttrMaxValueLen, allowlistedCustomLdapAttrMatcher)); } + public LdapUser(StreamInput in) throws IOException { + super(in); + userEntry = null; + originalUsername = in.readString(); + } + /** * May return null because ldapEntry is transient * @@ -88,4 +97,10 @@ public static Map extractLdapAttributes( } return Collections.unmodifiableMap(attributes); } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(originalUsername); + } } diff --git a/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java b/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java index 804e0a2114..a8f511be97 100644 --- a/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java +++ b/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java @@ -773,7 +773,8 @@ private TransportAddress getRemoteAddress() { if (address == null && threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER) != null) { address = new TransportAddress( (InetSocketAddress) Base64Helper.deserializeObject( - threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER) + threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER), + threadPool.getThreadContext().getTransient(ConfigConstants.USE_JDK_SERIALIZATION) ) ); } @@ -784,7 +785,8 @@ private String getUser() { User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); if (user == null && threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER) != null) { user = (User) Base64Helper.deserializeObject( - threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER) + threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER), + threadPool.getThreadContext().getTransient(ConfigConstants.USE_JDK_SERIALIZATION) ); } return user == null ? null : user.getName(); diff --git a/src/main/java/org/opensearch/security/auth/UserInjector.java b/src/main/java/org/opensearch/security/auth/UserInjector.java index 3e89a52e93..30df84ef5f 100644 --- a/src/main/java/org/opensearch/security/auth/UserInjector.java +++ b/src/main/java/org/opensearch/security/auth/UserInjector.java @@ -26,6 +26,7 @@ package org.opensearch.security.auth; +import java.io.IOException; import java.io.ObjectStreamException; import java.net.InetAddress; import java.net.UnknownHostException; @@ -36,6 +37,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.common.settings.Settings; import org.opensearch.core.common.transport.TransportAddress; import org.opensearch.rest.RestRequest; @@ -63,13 +66,18 @@ public UserInjector(Settings settings, ThreadPool threadPool, AuditLog auditLog, } - static class InjectedUser extends User { + public static class InjectedUser extends User { private transient TransportAddress transportAddress; public InjectedUser(String name) { super(name); } + public InjectedUser(StreamInput in) throws IOException { + super(in); + this.setInjected(true); + } + private Object writeReplace() throws ObjectStreamException { User user = new User(getName()); user.addRoles(getRoles()); @@ -96,6 +104,11 @@ public void setTransportAddress(String addr) throws UnknownHostException, Illega this.transportAddress = new TransportAddress(iAdress, port); } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + } } public InjectedUser getInjectedUser() { diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 14eaed4e0d..b35137a35d 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -443,7 +443,8 @@ private void setDlsHeaders(EvaluatedDlsFlsConfig dlsFls, ActionRequest request) } else { if (threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER) != null) { Object deserializedDlsQueries = Base64Helper.deserializeObject( - threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER) + threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER), + threadContext.getTransient(ConfigConstants.USE_JDK_SERIALIZATION) ); if (!dlsQueries.equals(deserializedDlsQueries)) { throw new OpenSearchSecurityException( @@ -506,7 +507,10 @@ private void setFlsHeaders(EvaluatedDlsFlsConfig dlsFls, ActionRequest request) if (threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER) != null) { if (!maskedFieldsMap.equals( - Base64Helper.deserializeObject(threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER)) + Base64Helper.deserializeObject( + threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER), + threadContext.getTransient(ConfigConstants.USE_JDK_SERIALIZATION) + ) )) { throw new OpenSearchSecurityException( ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER + " does not match (SG 901D)" @@ -542,7 +546,10 @@ private void setFlsHeaders(EvaluatedDlsFlsConfig dlsFls, ActionRequest request) } else { if (threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER) != null) { if (!flsFields.equals( - Base64Helper.deserializeObject(threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER)) + Base64Helper.deserializeObject( + threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER), + threadContext.getTransient(ConfigConstants.USE_JDK_SERIALIZATION) + ) )) { throw new OpenSearchSecurityException( ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER @@ -550,7 +557,8 @@ private void setFlsHeaders(EvaluatedDlsFlsConfig dlsFls, ActionRequest request) + flsFields + "---" + Base64Helper.deserializeObject( - threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER) + threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER), + threadContext.getTransient(ConfigConstants.USE_JDK_SERIALIZATION) ) ); } else { diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index 06f2fae397..f433a5857d 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -183,6 +183,10 @@ private void ap threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN, Origin.LOCAL.toString()); } + if (threadContext.getTransient(ConfigConstants.USE_JDK_SERIALIZATION) == null) { + threadContext.putTransient(ConfigConstants.USE_JDK_SERIALIZATION, false); + } + final ComplianceConfig complianceConfig = auditLog.getComplianceConfig(); if (complianceConfig != null && complianceConfig.isEnabled()) { attachSourceFieldContext(request); diff --git a/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLRequestHandler.java b/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLRequestHandler.java index 0a1b94548e..c67579e30f 100644 --- a/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLRequestHandler.java +++ b/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLRequestHandler.java @@ -83,8 +83,14 @@ protected ThreadContext getThreadContext() { @Override public final void messageReceived(T request, TransportChannel channel, Task task) throws Exception { + ThreadContext threadContext = getThreadContext(); + threadContext.putTransient( + ConfigConstants.USE_JDK_SERIALIZATION, + channel.getVersion().before(ConfigConstants.FIRST_CUSTOM_SERIALIZATION_SUPPORTED_OS_VERSION) + ); + if (SSLRequestHelper.containsBadHeader(threadContext, "_opendistro_security_ssl_")) { final Exception exception = ExceptionUtils.createBadHeaderException(); channel.sendResponse(exception); diff --git a/src/main/java/org/opensearch/security/support/Base64CustomHelper.java b/src/main/java/org/opensearch/security/support/Base64CustomHelper.java new file mode 100644 index 0000000000..dc66268fcd --- /dev/null +++ b/src/main/java/org/opensearch/security/support/Base64CustomHelper.java @@ -0,0 +1,225 @@ +/* + * 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.support; + +import com.amazon.dlic.auth.ldap.LdapUser; +import com.google.common.base.Preconditions; +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.io.BaseEncoding; +import org.opensearch.OpenSearchException; +import org.opensearch.common.Nullable; +import org.opensearch.core.common.io.stream.BytesStreamInput; +import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.Writeable; +import org.opensearch.core.common.Strings; +import org.opensearch.security.auth.UserInjector; +import org.opensearch.security.user.User; + +import java.io.IOException; +import java.io.Serializable; + +import static org.opensearch.security.support.SafeSerializationUtils.prohibitUnsafeClasses; + +/** + * Provides support for Serialization/Deserialization of objects of supported classes into/from Base64 encoded stream + * using the OpenSearch's custom serialization protocol implemented by the StreamInput/StreamOutput classes. + */ +public class Base64CustomHelper { + + private enum CustomSerializationFormat { + + WRITEABLE(1), + STREAMABLE(2), + GENERIC(3); + + private final int id; + + CustomSerializationFormat(int id) { + this.id = id; + } + + static CustomSerializationFormat fromId(int id) { + switch (id) { + case 1: + return WRITEABLE; + case 2: + return STREAMABLE; + case 3: + return GENERIC; + default: + throw new IllegalArgumentException(String.format("%d is not a valid id", id)); + } + } + + } + + private static final BiMap, Integer> writeableClassToIdMap = HashBiMap.create(); + private static final StreamableRegistry streamableRegistry = StreamableRegistry.getInstance(); + + static { + registerAllWriteables(); + } + + protected static String serializeObject(final Serializable object) { + + Preconditions.checkArgument(object != null, "object must not be null"); + final BytesStreamOutput streamOutput = new SafeBytesStreamOutput(128); + Class clazz = object.getClass(); + try { + prohibitUnsafeClasses(clazz); + CustomSerializationFormat customSerializationFormat = getCustomSerializationMode(clazz); + switch (customSerializationFormat) { + case WRITEABLE: + streamOutput.writeByte((byte) CustomSerializationFormat.WRITEABLE.id); + streamOutput.writeByte((byte) getWriteableClassID(clazz).intValue()); + ((Writeable) object).writeTo(streamOutput); + break; + case STREAMABLE: + streamOutput.writeByte((byte) CustomSerializationFormat.STREAMABLE.id); + streamableRegistry.writeTo(streamOutput, object); + break; + case GENERIC: + streamOutput.writeByte((byte) CustomSerializationFormat.GENERIC.id); + streamOutput.writeGenericValue(object); + break; + default: + throw new IllegalArgumentException( + String.format("Could not determine custom serialization mode for class %s", clazz.getName()) + ); + } + } catch (final Exception e) { + throw new OpenSearchException("Instance {} of class {} is not serializable", e, object, object.getClass()); + } + final byte[] bytes = streamOutput.bytes().toBytesRef().bytes; + streamOutput.close(); + return BaseEncoding.base64().encode(bytes); + } + + protected static Serializable deserializeObject(final String string) { + + Preconditions.checkArgument(!Strings.isNullOrEmpty(string), "object must not be null or empty"); + final byte[] bytes = BaseEncoding.base64().decode(string); + Serializable obj = null; + try (final BytesStreamInput streamInput = new SafeBytesStreamInput(bytes)) { + CustomSerializationFormat serializationFormat = CustomSerializationFormat.fromId(streamInput.readByte()); + switch (serializationFormat) { + case WRITEABLE: + final int classId = streamInput.readByte(); + Class clazz = getWriteableClassFromId(classId); + obj = (Serializable) clazz.getConstructor(StreamInput.class).newInstance(streamInput); + break; + case STREAMABLE: + obj = (Serializable) streamableRegistry.readFrom(streamInput); + break; + case GENERIC: + obj = (Serializable) streamInput.readGenericValue(); + break; + default: + throw new IllegalArgumentException("Could not determine custom deserialization mode"); + } + prohibitUnsafeClasses(obj.getClass()); + return obj; + } catch (final Exception e) { + throw new OpenSearchException(e); + } + } + + private static boolean isWriteable(Class clazz) { + return Writeable.class.isAssignableFrom(clazz); + } + + /** + * Returns integer ID for the registered Writeable class + *
+ * Protected for testing + */ + protected static Integer getWriteableClassID(Class clazz) { + if (!isWriteable(clazz)) { + throw new OpenSearchException("clazz should implement Writeable ", clazz); + } + if (!writeableClassToIdMap.containsKey(clazz)) { + throw new OpenSearchException("Writeable clazz not registered ", clazz); + } + return writeableClassToIdMap.get(clazz); + } + + private static Class getWriteableClassFromId(int id) { + return writeableClassToIdMap.inverse().get(id); + } + + /** + * Registers the given Writeable class for custom serialization by assigning an incrementing integer ID + * IDs are stored in a HashBiMap + * @param clazz class to be registered + */ + private static void registerWriteable(Class clazz) { + if (writeableClassToIdMap.containsKey(clazz)) { + throw new OpenSearchException("writeable clazz is already registered ", clazz.getName()); + } + int id = writeableClassToIdMap.size() + 1; + writeableClassToIdMap.put(clazz, id); + } + + /** + * Registers all Writeable classes for custom serialization support. + * Removing existing classes / changing order of registration will cause a breaking change in the serialization protocol + * as registerWriteable assigns an incrementing integer ID to each of the classes in the order it is called + * starting from 1. + *
+ * New classes can safely be added towards the end. + */ + private static void registerAllWriteables() { + registerWriteable(User.class); + registerWriteable(LdapUser.class); + registerWriteable(UserInjector.InjectedUser.class); + registerWriteable(SourceFieldsContext.class); + } + + private static CustomSerializationFormat getCustomSerializationMode(Class clazz) { + if (isWriteable(clazz)) { + return CustomSerializationFormat.WRITEABLE; + } else if (streamableRegistry.isStreamable(clazz)) { + return CustomSerializationFormat.STREAMABLE; + } else { + return CustomSerializationFormat.GENERIC; + } + } + + private static class SafeBytesStreamOutput extends BytesStreamOutput { + + public SafeBytesStreamOutput(int expectedSize) { + super(expectedSize); + } + + @Override + public void writeGenericValue(@Nullable Object value) throws IOException { + prohibitUnsafeClasses(value.getClass()); + super.writeGenericValue(value); + } + } + + private static class SafeBytesStreamInput extends BytesStreamInput { + + public SafeBytesStreamInput(byte[] bytes) { + super(bytes); + } + + @Override + public Object readGenericValue() throws IOException { + Object object = super.readGenericValue(); + prohibitUnsafeClasses(object.getClass()); + return object; + } + } +} diff --git a/src/main/java/org/opensearch/security/support/Base64Helper.java b/src/main/java/org/opensearch/security/support/Base64Helper.java index 836858decb..a5fbab8515 100644 --- a/src/main/java/org/opensearch/security/support/Base64Helper.java +++ b/src/main/java/org/opensearch/security/support/Base64Helper.java @@ -26,174 +26,47 @@ package org.opensearch.security.support; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InvalidClassException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.ObjectStreamClass; -import java.io.OutputStream; import java.io.Serializable; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Pattern; - -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.common.io.BaseEncoding; -import org.ldaptive.AbstractLdapBean; -import org.ldaptive.LdapAttribute; -import org.ldaptive.LdapEntry; -import org.ldaptive.SearchEntry; - -import com.amazon.dlic.auth.ldap.LdapUser; - -import org.opensearch.OpenSearchException; -import org.opensearch.SpecialPermission; -import org.opensearch.core.common.Strings; -import org.opensearch.security.user.User; public class Base64Helper { - private static final Set> SAFE_CLASSES = ImmutableSet.of( - String.class, - SocketAddress.class, - InetSocketAddress.class, - Pattern.class, - User.class, - SourceFieldsContext.class, - LdapUser.class, - SearchEntry.class, - LdapEntry.class, - AbstractLdapBean.class, - LdapAttribute.class - ); - - private static final List> SAFE_ASSIGNABLE_FROM_CLASSES = ImmutableList.of( - InetAddress.class, - Number.class, - Collection.class, - Map.class, - Enum.class - ); - - private static final Set SAFE_CLASS_NAMES = Collections.singleton("org.ldaptive.LdapAttribute$LdapAttributeValues"); - - private static boolean isSafeClass(Class cls) { - return cls.isArray() - || SAFE_CLASSES.contains(cls) - || SAFE_CLASS_NAMES.contains(cls.getName()) - || SAFE_ASSIGNABLE_FROM_CLASSES.stream().anyMatch(c -> c.isAssignableFrom(cls)); - } - - private final static class SafeObjectOutputStream extends ObjectOutputStream { - - private static final boolean useSafeObjectOutputStream = checkSubstitutionPermission(); - - @SuppressWarnings("removal") - private static boolean checkSubstitutionPermission() { - SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - try { - sm.checkPermission(new SpecialPermission()); - - AccessController.doPrivileged((PrivilegedAction) () -> { - AccessController.checkPermission(SUBSTITUTION_PERMISSION); - return null; - }); - } catch (SecurityException e) { - return false; - } - } - return true; - } - - static ObjectOutputStream create(ByteArrayOutputStream out) throws IOException { - try { - return useSafeObjectOutputStream ? new SafeObjectOutputStream(out) : new ObjectOutputStream(out); - } catch (SecurityException e) { - // As we try to create SafeObjectOutputStream only when necessary permissions are granted, we should - // not reach here, but if we do, we can still return ObjectOutputStream after resetting ByteArrayOutputStream - out.reset(); - return new ObjectOutputStream(out); - } - } - - @SuppressWarnings("removal") - private SafeObjectOutputStream(OutputStream out) throws IOException { - super(out); - - SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - sm.checkPermission(new SpecialPermission()); - } - - AccessController.doPrivileged((PrivilegedAction) () -> enableReplaceObject(true)); - } - - @Override - protected Object replaceObject(Object obj) throws IOException { - Class clazz = obj.getClass(); - if (isSafeClass(clazz)) { - return obj; - } - throw new IOException("Unauthorized serialization attempt " + clazz.getName()); - } + public static String serializeObject(final Serializable object, final boolean useJDKSerialization) { + return useJDKSerialization ? Base64JDKHelper.serializeObject(object) : Base64CustomHelper.serializeObject(object); } public static String serializeObject(final Serializable object) { - - Preconditions.checkArgument(object != null, "object must not be null"); - - final ByteArrayOutputStream bos = new ByteArrayOutputStream(); - try (final ObjectOutputStream out = SafeObjectOutputStream.create(bos)) { - out.writeObject(object); - } catch (final Exception e) { - throw new OpenSearchException("Instance {} of class {} is not serializable", e, object, object.getClass()); - } - final byte[] bytes = bos.toByteArray(); - return BaseEncoding.base64().encode(bytes); + return serializeObject(object, false); } public static Serializable deserializeObject(final String string) { - - Preconditions.checkArgument(!Strings.isNullOrEmpty(string), "string must not be null or empty"); - - final byte[] bytes = BaseEncoding.base64().decode(string); - final ByteArrayInputStream bis = new ByteArrayInputStream(bytes); - try (SafeObjectInputStream in = new SafeObjectInputStream(bis)) { - return (Serializable) in.readObject(); - } catch (final Exception e) { - throw new OpenSearchException(e); - } + return deserializeObject(string, false); } - private final static class SafeObjectInputStream extends ObjectInputStream { - - public SafeObjectInputStream(InputStream in) throws IOException { - super(in); - } - - @Override - protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { - - Class clazz = super.resolveClass(desc); - if (isSafeClass(clazz)) { - return clazz; - } + public static Serializable deserializeObject(final String string, final boolean useJDKDeserialization) { + return useJDKDeserialization ? Base64JDKHelper.deserializeObject(string) : Base64CustomHelper.deserializeObject(string); + } - throw new InvalidClassException("Unauthorized deserialization attempt ", clazz.getName()); + /** + * Ensures that the returned string is JDK serialized. + * + * If the supplied string is a custom serialized representation, will deserialize it and further serialize using + * JDK, otherwise returns the string as is. + * + * @param string original string, can be JDK or custom serialized + * @return jdk serialized string + */ + public static String ensureJDKSerialized(final String string) { + Serializable serializable; + try { + serializable = Base64Helper.deserializeObject(string, false); + } catch (Exception e) { + // We received an exception when de-serializing the given string. It is probably JDK serialized. + // Try to deserialize using JDK + Base64Helper.deserializeObject(string, true); + // Since we could deserialize the object using JDK, the string is already JDK serialized, return as is + return string; } + // If we see an exception now, we want the caller to see it - + return Base64Helper.serializeObject(serializable, true); } } diff --git a/src/main/java/org/opensearch/security/support/Base64JDKHelper.java b/src/main/java/org/opensearch/security/support/Base64JDKHelper.java new file mode 100644 index 0000000000..a4ab87d813 --- /dev/null +++ b/src/main/java/org/opensearch/security/support/Base64JDKHelper.java @@ -0,0 +1,156 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.support; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InvalidClassException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; +import java.io.OutputStream; +import java.io.Serializable; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import com.google.common.base.Preconditions; +import com.google.common.io.BaseEncoding; + +import org.opensearch.OpenSearchException; +import org.opensearch.SpecialPermission; +import org.opensearch.core.common.Strings; + +import static org.opensearch.security.support.SafeSerializationUtils.isSafeClass; + +/** + * Provides support for Serialization/Deserialization of objects of supported classes into/from Base64 encoded stream + * using JDK's in-built serialization protocol implemented by the ObjectOutputStream and ObjectInputStream classes. + */ +public class Base64JDKHelper { + + private final static class SafeObjectOutputStream extends ObjectOutputStream { + + private static final boolean useSafeObjectOutputStream = checkSubstitutionPermission(); + + @SuppressWarnings("removal") + private static boolean checkSubstitutionPermission() { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + try { + sm.checkPermission(new SpecialPermission()); + + AccessController.doPrivileged((PrivilegedAction) () -> { + AccessController.checkPermission(SUBSTITUTION_PERMISSION); + return null; + }); + } catch (SecurityException e) { + return false; + } + } + return true; + } + + static ObjectOutputStream create(ByteArrayOutputStream out) throws IOException { + try { + return useSafeObjectOutputStream ? new SafeObjectOutputStream(out) : new ObjectOutputStream(out); + } catch (SecurityException e) { + // As we try to create SafeObjectOutputStream only when necessary permissions are granted, we should + // not reach here, but if we do, we can still return ObjectOutputStream after resetting ByteArrayOutputStream + out.reset(); + return new ObjectOutputStream(out); + } + } + + @SuppressWarnings("removal") + private SafeObjectOutputStream(OutputStream out) throws IOException { + super(out); + + SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + AccessController.doPrivileged((PrivilegedAction) () -> enableReplaceObject(true)); + } + + @Override + protected Object replaceObject(Object obj) throws IOException { + Class clazz = obj.getClass(); + if (isSafeClass(clazz)) { + return obj; + } + throw new IOException("Unauthorized serialization attempt " + clazz.getName()); + } + } + + public static String serializeObject(final Serializable object) { + + Preconditions.checkArgument(object != null, "object must not be null"); + + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (final ObjectOutputStream out = SafeObjectOutputStream.create(bos)) { + out.writeObject(object); + } catch (final Exception e) { + throw new OpenSearchException("Instance {} of class {} is not serializable", e, object, object.getClass()); + } + final byte[] bytes = bos.toByteArray(); + return BaseEncoding.base64().encode(bytes); + } + + public static Serializable deserializeObject(final String string) { + + Preconditions.checkArgument(!Strings.isNullOrEmpty(string), "object must not be null or empty"); + + final byte[] bytes = BaseEncoding.base64().decode(string); + final ByteArrayInputStream bis = new ByteArrayInputStream(bytes); + try (SafeObjectInputStream in = new SafeObjectInputStream(bis)) { + return (Serializable) in.readObject(); + } catch (final Exception e) { + throw new OpenSearchException(e); + } + } + + private final static class SafeObjectInputStream extends ObjectInputStream { + + public SafeObjectInputStream(InputStream in) throws IOException { + super(in); + } + + @Override + protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { + + Class clazz = super.resolveClass(desc); + if (isSafeClass(clazz)) { + return clazz; + } + + throw new InvalidClassException("Unauthorized deserialization attempt ", clazz.getName()); + } + } +} diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index 8317d65335..9ac73cd579 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -35,6 +35,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import org.opensearch.Version; import org.opensearch.common.settings.Settings; import org.opensearch.security.auditlog.impl.AuditCategory; @@ -242,6 +243,7 @@ public class ConfigConstants { "opendistro_security.compliance.history.write.ignore_users"; public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_EXTERNAL_CONFIG_ENABLED = "opendistro_security.compliance.history.external_config_enabled"; + public static final String OPENDISTRO_SECURITY_SOURCE_FIELD_CONTEXT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "source_field_context"; public static final String SECURITY_COMPLIANCE_DISABLE_ANONYMOUS_AUTHENTICATION = "plugins.security.compliance.disable_anonymous_authentication"; public static final String SECURITY_COMPLIANCE_IMMUTABLE_INDICES = "plugins.security.compliance.immutable_indices"; @@ -323,6 +325,9 @@ public enum RolesMappingResolution { public static final String TENANCY_GLOBAL_TENANT_NAME = "global"; public static final String TENANCY_GLOBAL_TENANT_DEFAULT_NAME = ""; + public static final String USE_JDK_SERIALIZATION = "plugins.security.use_jdk_serialization"; + public static final Version FIRST_CUSTOM_SERIALIZATION_SUPPORTED_OS_VERSION = Version.V_3_0_0; + // On-behalf-of endpoints settings // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings public static final String EXTENSIONS_BWC_PLUGIN_MODE = "bwcPluginMode"; diff --git a/src/main/java/org/opensearch/security/support/HeaderHelper.java b/src/main/java/org/opensearch/security/support/HeaderHelper.java index e8d50346a8..bbb44664fa 100644 --- a/src/main/java/org/opensearch/security/support/HeaderHelper.java +++ b/src/main/java/org/opensearch/security/support/HeaderHelper.java @@ -27,6 +27,8 @@ package org.opensearch.security.support; import java.io.Serializable; +import java.util.Arrays; +import java.util.List; import com.google.common.base.Strings; @@ -68,7 +70,7 @@ public static Serializable deserializeSafeFromHeader(final ThreadContext context final String objectAsBase64 = getSafeFromHeader(context, headerName); if (!Strings.isNullOrEmpty(objectAsBase64)) { - return Base64Helper.deserializeObject(objectAsBase64); + return Base64Helper.deserializeObject(objectAsBase64, context.getTransient(ConfigConstants.USE_JDK_SERIALIZATION)); } return null; @@ -77,4 +79,16 @@ public static Serializable deserializeSafeFromHeader(final ThreadContext context public static boolean isTrustedClusterRequest(final ThreadContext context) { return context.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_TRUSTED_CLUSTER_REQUEST) == Boolean.TRUE; } + + public static List getAllSerializedHeaderNames() { + return Arrays.asList( + ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER, + ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER, + ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER, + ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER, + ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER, + ConfigConstants.OPENDISTRO_SECURITY_DLS_FILTER_LEVEL_QUERY_HEADER, + ConfigConstants.OPENDISTRO_SECURITY_SOURCE_FIELD_CONTEXT + ); + } } diff --git a/src/main/java/org/opensearch/security/support/SafeSerializationUtils.java b/src/main/java/org/opensearch/security/support/SafeSerializationUtils.java new file mode 100644 index 0000000000..c980959f68 --- /dev/null +++ b/src/main/java/org/opensearch/security/support/SafeSerializationUtils.java @@ -0,0 +1,81 @@ +/* + * 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.support; + +import com.amazon.dlic.auth.ldap.LdapUser; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.ldaptive.AbstractLdapBean; +import org.ldaptive.LdapAttribute; +import org.ldaptive.LdapEntry; +import org.ldaptive.SearchEntry; +import org.opensearch.security.auth.UserInjector; +import org.opensearch.security.user.User; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * Provides functionality to verify if a class is categorised to be safe for serialization or + * deserialization by the security plugin. + *
+ * All methods are package private. + */ +public final class SafeSerializationUtils { + + private static final Set> SAFE_CLASSES = ImmutableSet.of( + String.class, + SocketAddress.class, + InetSocketAddress.class, + Pattern.class, + User.class, + UserInjector.InjectedUser.class, + SourceFieldsContext.class, + LdapUser.class, + SearchEntry.class, + LdapEntry.class, + AbstractLdapBean.class, + LdapAttribute.class + ); + + private static final List> SAFE_ASSIGNABLE_FROM_CLASSES = ImmutableList.of( + InetAddress.class, + Number.class, + Collection.class, + Map.class, + Enum.class + ); + + private static final Set SAFE_CLASS_NAMES = Collections.singleton("org.ldaptive.LdapAttribute$LdapAttributeValues"); + + static boolean isSafeClass(Class cls) { + return cls.isArray() + || SAFE_CLASSES.contains(cls) + || SAFE_CLASS_NAMES.contains(cls.getName()) + || SAFE_ASSIGNABLE_FROM_CLASSES.stream().anyMatch(c -> c.isAssignableFrom(cls)); + } + + static void prohibitUnsafeClasses(Class clazz) throws IOException { + if (!isSafeClass(clazz)) { + throw new IOException("Unauthorized serialization attempt " + clazz.getName()); + } + } + +} diff --git a/src/main/java/org/opensearch/security/support/SourceFieldsContext.java b/src/main/java/org/opensearch/security/support/SourceFieldsContext.java index 02f0ad9226..83bbb683e9 100644 --- a/src/main/java/org/opensearch/security/support/SourceFieldsContext.java +++ b/src/main/java/org/opensearch/security/support/SourceFieldsContext.java @@ -26,13 +26,18 @@ package org.opensearch.security.support; +import java.io.IOException; import java.io.Serializable; import java.util.Arrays; +import java.util.Objects; import org.opensearch.action.get.GetRequest; import org.opensearch.action.search.SearchRequest; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.common.io.stream.Writeable; -public class SourceFieldsContext implements Serializable { +public class SourceFieldsContext implements Serializable, Writeable { private String[] includes; private String[] excludes; @@ -77,6 +82,18 @@ public SourceFieldsContext(SearchRequest request) { // } } + public SourceFieldsContext(StreamInput in) throws IOException { + includes = in.readStringArray(); + if (includes.length == 0) { + includes = null; + } + excludes = in.readStringArray(); + if (excludes.length == 0) { + excludes = null; + } + fetchSource = in.readBoolean(); + } + public SourceFieldsContext(GetRequest request) { if (request.fetchSourceContext() != null) { includes = request.fetchSourceContext().includes(); @@ -117,4 +134,11 @@ public String toString() { + fetchSource + "]"; } + + @Override + public void writeTo(StreamOutput streamOutput) throws IOException { + streamOutput.writeStringArray(Objects.requireNonNullElseGet(includes, () -> new String[] {})); + streamOutput.writeStringArray(Objects.requireNonNullElseGet(excludes, () -> new String[] {})); + streamOutput.writeBoolean(fetchSource); + } } diff --git a/src/main/java/org/opensearch/security/support/StreamableRegistry.java b/src/main/java/org/opensearch/security/support/StreamableRegistry.java new file mode 100644 index 0000000000..bfde866376 --- /dev/null +++ b/src/main/java/org/opensearch/security/support/StreamableRegistry.java @@ -0,0 +1,134 @@ +/* + * 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.support; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.Map; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; + +import org.opensearch.OpenSearchException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.common.io.stream.Writeable; + +/** + * Registry for any class that does NOT implement the Writeable interface + * and needs to be serialized over the wire. Supports registration of writer and reader via registerStreamable + * for such classes and provides methods writeTo and readFrom for objects of such registered classes. + *
+ * Methods are protected and intended to be accessed from only within the package. (mostly by Base64Helper) + */ +public class StreamableRegistry { + + private static final StreamableRegistry INSTANCE = new StreamableRegistry(); + public final BiMap, Integer> classToIdMap = HashBiMap.create(); + private final Map idToEntryMap = new HashMap<>(); + + private StreamableRegistry() { + registerAllStreamables(); + } + + private static class Entry { + Writeable.Writer writer; + Writeable.Reader reader; + + Entry(Writeable.Writer writer, Writeable.Reader reader) { + this.writer = writer; + this.reader = reader; + } + } + + private Writeable.Writer getWriter(Class clazz) { + if (!classToIdMap.containsKey(clazz)) { + throw new OpenSearchException(String.format("No writer registered for class %s", clazz.getName())); + } + return idToEntryMap.get(classToIdMap.get(clazz)).writer; + } + + private Writeable.Reader getReader(int id) { + if (!idToEntryMap.containsKey(id)) { + throw new OpenSearchException(String.format("No reader registered for id %s", id)); + } + return idToEntryMap.get(id).reader; + } + + private int getId(Class clazz) { + if (!classToIdMap.containsKey(clazz)) { + throw new OpenSearchException(String.format("No writer registered for class %s", clazz.getName())); + } + return classToIdMap.get(clazz); + } + + protected boolean isStreamable(Class clazz) { + return classToIdMap.containsKey(clazz); + } + + protected void writeTo(StreamOutput out, Object object) throws IOException { + out.writeByte((byte) getId(object.getClass())); + getWriter(object.getClass()).write(out, object); + } + + protected Object readFrom(StreamInput in) throws IOException { + int id = in.readByte(); + return getReader(id).read(in); + } + + protected static StreamableRegistry getInstance() { + return INSTANCE; + } + + protected void registerStreamable(int streamableId, Class clazz, Writeable.Writer writer, Writeable.Reader reader) { + if (Writeable.class.isAssignableFrom(clazz)) { + throw new IllegalArgumentException( + String.format("%s is Writeable and should not be registered as a streamable", clazz.getName()) + ); + } + classToIdMap.put(clazz, streamableId); + idToEntryMap.put(streamableId, new Entry(writer, reader)); + } + + protected int getStreamableID(Class clazz) { + if (!isStreamable(clazz)) { + throw new OpenSearchException(String.format("class %s is in streamable registry", clazz.getName())); + } else { + return classToIdMap.get(clazz); + } + } + + /** + * Register all streamables here. + *
+ * Caution - Register new streamables towards the end. Removing / reordering a registered streamable will change the typeIDs associated with the streamables + * causing a breaking change in the serialization format. + */ + private void registerAllStreamables() { + + // InetSocketAddress + this.registerStreamable(1, InetSocketAddress.class, (o, v) -> { + final InetSocketAddress inetSocketAddress = (InetSocketAddress) v; + o.writeString(inetSocketAddress.getHostString()); + o.writeByteArray(inetSocketAddress.getAddress().getAddress()); + o.writeInt(inetSocketAddress.getPort()); + }, i -> { + String host = i.readString(); + byte[] addressBytes = i.readByteArray(); + int port = i.readInt(); + return new InetSocketAddress(InetAddress.getByAddress(host, addressBytes), port); + }); + } + +} diff --git a/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java b/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java index 0c645c9a00..f064f0af04 100644 --- a/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java +++ b/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java @@ -59,6 +59,7 @@ import org.opensearch.security.ssl.transport.SSLConfig; import org.opensearch.security.support.Base64Helper; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.HeaderHelper; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.Transport.Connection; @@ -147,6 +148,7 @@ public void sendRequestDecorate( final String origCCSTransientMf = getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_CCS); final boolean isDebugEnabled = log.isDebugEnabled(); + final boolean useJDKSerialization = connection.getVersion().before(ConfigConstants.FIRST_CUSTOM_SERIALIZATION_SUPPORTED_OS_VERSION); final boolean isSameNodeRequest = localNode != null && localNode.equals(connection.getNode()); try (ThreadContext.StoredContext stashedContext = getThreadContext().stashContext()) { @@ -224,9 +226,26 @@ && getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_ROL ); } + if (useJDKSerialization) { + Map jdkSerializedHeaders = new HashMap<>(); + HeaderHelper.getAllSerializedHeaderNames() + .stream() + .filter(k -> headerMap.get(k) != null) + .forEach(k -> jdkSerializedHeaders.put(k, Base64Helper.ensureJDKSerialized(headerMap.get(k)))); + headerMap.putAll(jdkSerializedHeaders); + } + getThreadContext().putHeader(headerMap); - ensureCorrectHeaders(remoteAddress0, user0, origin0, injectedUserString, injectedRolesString, isSameNodeRequest); + ensureCorrectHeaders( + remoteAddress0, + user0, + origin0, + injectedUserString, + injectedRolesString, + isSameNodeRequest, + useJDKSerialization + ); if (isActionTraceEnabled()) { getThreadContext().putHeader( @@ -253,7 +272,8 @@ private void ensureCorrectHeaders( final String origin, final String injectedUserString, final String injectedRolesString, - boolean isSameNodeRequest + final boolean isSameNodeRequest, + final boolean useJDKSerialization ) { // keep original address @@ -294,7 +314,7 @@ && getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN_HEADE if (transportAddress != null) { getThreadContext().putHeader( ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER, - Base64Helper.serializeObject(transportAddress.address()) + Base64Helper.serializeObject(transportAddress.address(), useJDKSerialization) ); } @@ -302,7 +322,10 @@ && getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN_HEADE if (userHeader == null) { // put as headers for other requests if (origUser != null) { - getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER, Base64Helper.serializeObject(origUser)); + getThreadContext().putHeader( + ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER, + Base64Helper.serializeObject(origUser, useJDKSerialization) + ); } else if (StringUtils.isNotEmpty(injectedRolesString)) { getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_ROLES_HEADER, injectedRolesString); } else if (StringUtils.isNotEmpty(injectedUserString)) { diff --git a/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java b/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java index 1284ca9781..3ba379dd67 100644 --- a/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java +++ b/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java @@ -107,6 +107,8 @@ protected void messageReceivedDecorate( resolvedActionClass = ((ConcreteShardRequest) request).getRequest().getClass().getSimpleName(); } + final boolean useJDKSerialization = getThreadContext().getTransient(ConfigConstants.USE_JDK_SERIALIZATION); + String initialActionClassValue = getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_INITIAL_ACTION_CLASS_HEADER); final ThreadContext.StoredContext sgContext = getThreadContext().newStoredContext(false); @@ -181,7 +183,7 @@ protected void messageReceivedDecorate( } else { getThreadContext().putTransient( ConfigConstants.OPENDISTRO_SECURITY_USER, - Objects.requireNonNull((User) Base64Helper.deserializeObject(userHeader)) + Objects.requireNonNull((User) Base64Helper.deserializeObject(userHeader, useJDKSerialization)) ); } @@ -190,7 +192,7 @@ protected void messageReceivedDecorate( if (!Strings.isNullOrEmpty(originalRemoteAddress)) { getThreadContext().putTransient( ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, - new TransportAddress((InetSocketAddress) Base64Helper.deserializeObject(originalRemoteAddress)) + new TransportAddress((InetSocketAddress) Base64Helper.deserializeObject(originalRemoteAddress, useJDKSerialization)) ); } else { getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, request.remoteAddress()); diff --git a/src/main/java/org/opensearch/security/user/User.java b/src/main/java/org/opensearch/security/user/User.java index 2642b368d7..394b251271 100644 --- a/src/main/java/org/opensearch/security/user/User.java +++ b/src/main/java/org/opensearch/security/user/User.java @@ -83,6 +83,9 @@ public User(final StreamInput in) throws IOException { name = in.readString(); roles.addAll(in.readList(StreamInput::readString)); requestedTenant = in.readString(); + if (requestedTenant.isEmpty()) { + requestedTenant = null; + } attributes = Collections.synchronizedMap(in.readMap(StreamInput::readString, StreamInput::readString)); securityRoles.addAll(in.readList(StreamInput::readString)); } @@ -167,9 +170,9 @@ public final boolean isUserInRole(final String role) { } /** - * Associate this user with a set of backend roles + * Associate this user with a set of custom attributes * - * @param roles The backend roles + * @param attributes custom attributes */ public final void addAttributes(final Map attributes) { if (attributes != null) { @@ -255,7 +258,7 @@ public final void copyRolesFrom(final User user) { public void writeTo(StreamOutput out) throws IOException { out.writeString(name); out.writeStringCollection(new ArrayList(roles)); - out.writeString(requestedTenant); + out.writeString(requestedTenant == null ? "" : requestedTenant); out.writeMap(attributes, StreamOutput::writeString, StreamOutput::writeString); out.writeStringCollection(securityRoles == null ? Collections.emptyList() : new ArrayList(securityRoles)); } diff --git a/src/test/java/org/opensearch/security/support/Base64CustomHelperTest.java b/src/test/java/org/opensearch/security/support/Base64CustomHelperTest.java new file mode 100644 index 0000000000..e35e1d72ba --- /dev/null +++ b/src/test/java/org/opensearch/security/support/Base64CustomHelperTest.java @@ -0,0 +1,159 @@ +/* + * 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.support; + +import com.amazon.dlic.auth.ldap.LdapUser; +import org.junit.Assert; +import org.junit.Test; +import org.ldaptive.LdapEntry; +import org.opensearch.OpenSearchException; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.common.io.stream.Writeable; +import org.opensearch.security.auth.UserInjector; +import org.opensearch.security.user.AuthCredentials; +import org.opensearch.security.user.User; + +import java.io.Serializable; +import java.net.InetSocketAddress; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.HashMap; + +import static org.opensearch.security.support.Base64CustomHelper.deserializeObject; +import static org.opensearch.security.support.Base64CustomHelper.serializeObject; + +public class Base64CustomHelperTest { + + private static final class NotSafeStreamable implements Serializable { + private static final long serialVersionUID = 5135559266828470092L; + } + + private static final class NotSafeWriteable implements Writeable, Serializable { + @Override + public void writeTo(StreamOutput out) { + + } + } + + private static Serializable ds(Serializable s) { + return deserializeObject(serializeObject(s)); + } + + @Test + public void testString() { + String string = "string"; + Assert.assertEquals(string, ds(string)); + } + + @Test + public void testInteger() { + Integer integer = 0; + Assert.assertEquals(integer, ds(integer)); + } + + @Test + public void testDouble() { + Double number = 0.; + Assert.assertEquals(number, ds(number)); + } + + @Test + public void testInetSocketAddress() { + InetSocketAddress inetSocketAddress = new InetSocketAddress(0); + Assert.assertEquals(inetSocketAddress, ds(inetSocketAddress)); + } + + @Test + public void testUser() { + User user = new User("user"); + Assert.assertEquals(user, ds(user)); + } + + @Test + public void testSourceFieldsContext() { + SourceFieldsContext sourceFieldsContext = new SourceFieldsContext(new SearchRequest("")); + Assert.assertEquals(sourceFieldsContext.toString(), ds(sourceFieldsContext).toString()); + } + + @Test + public void testHashMap() { + HashMap map = new HashMap<>() { + { + put("key", "value"); + } + }; + Assert.assertEquals(map, ds(map)); + } + + @Test + public void testArrayList() { + ArrayList list = new ArrayList<>() { + { + add("value"); + } + }; + Assert.assertEquals(list, ds(list)); + } + + @Test + public void testLdapUser() { + LdapUser ldapUser = new LdapUser( + "username", + "originalusername", + new LdapEntry("dn"), + new AuthCredentials("originalusername", "12345"), + 34, + WildcardMatcher.ANY + ); + Assert.assertEquals(ldapUser, ds(ldapUser)); + } + + @Test + public void testGetWriteableClassID() { + // a need to make a change in this test signifies a breaking change in security plugin's custom serialization + // format + Assert.assertEquals(Integer.valueOf(1), Base64CustomHelper.getWriteableClassID(User.class)); + Assert.assertEquals(Integer.valueOf(2), Base64CustomHelper.getWriteableClassID(LdapUser.class)); + Assert.assertEquals(Integer.valueOf(3), Base64CustomHelper.getWriteableClassID(UserInjector.InjectedUser.class)); + Assert.assertEquals(Integer.valueOf(4), Base64CustomHelper.getWriteableClassID(SourceFieldsContext.class)); + } + + @Test + public void testInjectedUser() { + UserInjector.InjectedUser injectedUser = new UserInjector.InjectedUser("username"); + + // for custom serialization, we expect InjectedUser to be returned on deserialization + UserInjector.InjectedUser deserializedInjecteduser = (UserInjector.InjectedUser) ds(injectedUser); + Assert.assertEquals(injectedUser, deserializedInjecteduser); + Assert.assertTrue(deserializedInjecteduser.isInjected()); + } + + @Test(expected = OpenSearchException.class) + public void testNotSafeStreamable() { + Base64JDKHelper.serializeObject(new NotSafeStreamable()); + } + + @Test(expected = OpenSearchException.class) + public void testNotSafeWriteable() { + Base64JDKHelper.serializeObject(new NotSafeWriteable()); + } + + @Test(expected = OpenSearchException.class) + public void testNotSafeGeneric() { + HashMap map = new HashMap<>(); + map.put(1, ZonedDateTime.now()); + map.put(2, ZonedDateTime.now()); + Base64JDKHelper.serializeObject(map); + } + +} diff --git a/src/test/java/org/opensearch/security/support/Base64HelperTest.java b/src/test/java/org/opensearch/security/support/Base64HelperTest.java index 81c2505985..f55581c7e7 100644 --- a/src/test/java/org/opensearch/security/support/Base64HelperTest.java +++ b/src/test/java/org/opensearch/security/support/Base64HelperTest.java @@ -10,100 +10,44 @@ */ package org.opensearch.security.support; -import java.io.ByteArrayOutputStream; -import java.io.ObjectOutputStream; import java.io.Serializable; -import java.net.InetSocketAddress; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.regex.Pattern; -import com.google.common.io.BaseEncoding; import org.junit.Assert; import org.junit.Test; -import org.opensearch.OpenSearchException; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.security.user.User; - import static org.opensearch.security.support.Base64Helper.deserializeObject; import static org.opensearch.security.support.Base64Helper.serializeObject; public class Base64HelperTest { - private static final class NotSafeSerializable implements Serializable { - private static final long serialVersionUID = 5135559266828470092L; + private static Serializable dsJDK(Serializable s) { + return deserializeObject(serializeObject(s, true), true); } private static Serializable ds(Serializable s) { return deserializeObject(serializeObject(s)); } + /** + * Just one sanity test comprising invocation of JDK and Custom Serialization. + * + * Individual scenarios are covered by Base64CustomHelperTest and Base64JDKHelperTest + */ @Test - public void testString() { - String string = "string"; - Assert.assertEquals(string, ds(string)); - } - - @Test - public void testInteger() { - Integer integer = Integer.valueOf(0); - Assert.assertEquals(integer, ds(integer)); - } - - @Test - public void testDouble() { - Double number = Double.valueOf(0.); - Assert.assertEquals(number, ds(number)); - } - - @Test - public void testInetSocketAddress() { - InetSocketAddress inetSocketAddress = new InetSocketAddress(0); - Assert.assertEquals(inetSocketAddress, ds(inetSocketAddress)); - } - - @Test - public void testPattern() { - Pattern pattern = Pattern.compile(".*"); - Assert.assertEquals(pattern.pattern(), ((Pattern) ds(pattern)).pattern()); - } - - @Test - public void testUser() { - User user = new User("user"); - Assert.assertEquals(user, ds(user)); - } - - @Test - public void testSourceFieldsContext() { - SourceFieldsContext sourceFieldsContext = new SourceFieldsContext(new SearchRequest("")); - Assert.assertEquals(sourceFieldsContext.toString(), ds(sourceFieldsContext).toString()); - } - - @Test - public void testHashMap() { - HashMap map = new HashMap(); - Assert.assertEquals(map, ds(map)); + public void testSerde() { + String test = "string"; + Assert.assertEquals(test, ds(test)); + Assert.assertEquals(test, dsJDK(test)); } @Test - public void testArrayList() { - ArrayList list = new ArrayList(); - Assert.assertEquals(list, ds(list)); - } + public void testEnsureJDKSerialized() { + String test = "string"; + String jdkSerialized = Base64Helper.serializeObject(test, true); + String customSerialized = Base64Helper.serializeObject(test, false); + Assert.assertEquals(jdkSerialized, Base64Helper.ensureJDKSerialized(jdkSerialized)); + Assert.assertEquals(jdkSerialized, Base64Helper.ensureJDKSerialized(customSerialized)); - @Test(expected = OpenSearchException.class) - public void notSafeSerializable() { - serializeObject(new NotSafeSerializable()); } - @Test(expected = OpenSearchException.class) - public void notSafeDeserializable() throws Exception { - final ByteArrayOutputStream bos = new ByteArrayOutputStream(); - try (final ObjectOutputStream out = new ObjectOutputStream(bos)) { - out.writeObject(new NotSafeSerializable()); - } - deserializeObject(BaseEncoding.base64().encode(bos.toByteArray())); - } } diff --git a/src/test/java/org/opensearch/security/support/Base64JDKHelperTest.java b/src/test/java/org/opensearch/security/support/Base64JDKHelperTest.java new file mode 100644 index 0000000000..704f1dc1d7 --- /dev/null +++ b/src/test/java/org/opensearch/security/support/Base64JDKHelperTest.java @@ -0,0 +1,128 @@ +/* + * 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.support; + +import com.amazon.dlic.auth.ldap.LdapUser; +import com.google.common.io.BaseEncoding; +import org.junit.Assert; +import org.junit.Test; +import org.ldaptive.LdapEntry; +import org.opensearch.OpenSearchException; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.security.auth.UserInjector; +import org.opensearch.security.user.AuthCredentials; +import org.opensearch.security.user.User; + +import java.io.ByteArrayOutputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.HashMap; + +public class Base64JDKHelperTest { + private static final class NotSafeSerializable implements Serializable { + private static final long serialVersionUID = 5135559266828470092L; + } + + private static Serializable ds(Serializable s) { + return Base64JDKHelper.deserializeObject(Base64JDKHelper.serializeObject(s)); + } + + @Test + public void testString() { + String string = "string"; + Assert.assertEquals(string, ds(string)); + } + + @Test + public void testInteger() { + Integer integer = 0; + Assert.assertEquals(integer, ds(integer)); + } + + @Test + public void testDouble() { + Double number = 0.0; + Assert.assertEquals(number, ds(number)); + } + + @Test + public void testInetSocketAddress() { + InetSocketAddress inetSocketAddress = new InetSocketAddress(0); + Assert.assertEquals(inetSocketAddress, ds(inetSocketAddress)); + } + + @Test + public void testUser() { + User user = new User("user"); + Assert.assertEquals(user, ds(user)); + } + + @Test + public void testSourceFieldsContext() { + SourceFieldsContext sourceFieldsContext = new SourceFieldsContext(new SearchRequest("")); + Assert.assertEquals(sourceFieldsContext.toString(), ds(sourceFieldsContext).toString()); + } + + @Test + public void testHashMap() { + HashMap map = new HashMap<>(); + map.put("key", "value"); + Assert.assertEquals(map, ds(map)); + } + + @Test + public void testArrayList() { + ArrayList list = new ArrayList<>(); + list.add("value"); + Assert.assertEquals(list, ds(list)); + } + + @Test(expected = OpenSearchException.class) + public void notSafeSerializable() { + Base64JDKHelper.serializeObject(new NotSafeSerializable()); + } + + @Test(expected = OpenSearchException.class) + public void notSafeDeserializable() throws Exception { + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (final ObjectOutputStream out = new ObjectOutputStream(bos)) { + out.writeObject(new NotSafeSerializable()); + } + Base64JDKHelper.deserializeObject(BaseEncoding.base64().encode(bos.toByteArray())); + } + + @Test + public void testLdapUser() { + LdapUser ldapUser = new LdapUser( + "username", + "originalusername", + new LdapEntry("dn"), + new AuthCredentials("originalusername", "12345"), + 34, + WildcardMatcher.ANY + ); + Assert.assertEquals(ldapUser, ds(ldapUser)); + } + + @Test + public void testInjectedUser() { + UserInjector.InjectedUser injectedUser = new UserInjector.InjectedUser("username"); + + // we expect to get User object when deserializing InjectedUser via JDK serialization + User user = new User("username"); + User deserializedUser = (User) ds(injectedUser); + Assert.assertEquals(user, deserializedUser); + Assert.assertTrue(deserializedUser.isInjected()); + } +} diff --git a/src/test/java/org/opensearch/security/support/StreamableRegistryTest.java b/src/test/java/org/opensearch/security/support/StreamableRegistryTest.java new file mode 100644 index 0000000000..13f2448b30 --- /dev/null +++ b/src/test/java/org/opensearch/security/support/StreamableRegistryTest.java @@ -0,0 +1,29 @@ +/* + * 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.support; + +import org.junit.Assert; +import org.junit.Test; +import org.opensearch.OpenSearchException; + +import java.net.InetSocketAddress; + +public class StreamableRegistryTest { + + StreamableRegistry streamableRegistry = StreamableRegistry.getInstance(); + + @Test + public void testStreamableTypeIDs() { + Assert.assertEquals(1, streamableRegistry.getStreamableID(InetSocketAddress.class)); + Assert.assertThrows(OpenSearchException.class, () -> streamableRegistry.getStreamableID(String.class)); + } +} diff --git a/src/test/java/org/opensearch/security/transport/SecurityInterceptorTests.java b/src/test/java/org/opensearch/security/transport/SecurityInterceptorTests.java index d3363c54d8..abc0e314ef 100644 --- a/src/test/java/org/opensearch/security/transport/SecurityInterceptorTests.java +++ b/src/test/java/org/opensearch/security/transport/SecurityInterceptorTests.java @@ -47,9 +47,6 @@ import static java.util.Collections.emptySet; import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; // CS-ENFORCE-SINGLE @@ -110,9 +107,8 @@ public void setup() { ); } - @Test - public void testSendRequestDecorate() { - + private void testSendRequestDecorate(Version remoteNodeVersion) { + boolean useJDKSerialization = remoteNodeVersion.before(ConfigConstants.FIRST_CUSTOM_SERIALIZATION_SUPPORTED_OS_VERSION); ClusterName clusterName = ClusterName.DEFAULT; when(clusterService.getClusterName()).thenReturn(clusterName); @@ -140,7 +136,6 @@ public void testSendRequestDecorate() { User user = new User("John Doe"); threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, user); - AsyncSender sender = mock(AsyncSender.class); String action = "testAction"; TransportRequest request = mock(TransportRequest.class); TransportRequestOptions options = mock(TransportRequestOptions.class); @@ -156,37 +151,65 @@ public void testSendRequestDecorate() { DiscoveryNode localNode = new DiscoveryNode("local-node", new TransportAddress(localAddress, 1234), Version.CURRENT); Connection connection1 = transportService.getConnection(localNode); - DiscoveryNode otherNode = new DiscoveryNode("local-node", new TransportAddress(localAddress, 4321), Version.CURRENT); + DiscoveryNode otherNode = new DiscoveryNode("remote-node", new TransportAddress(localAddress, 4321), remoteNodeVersion); Connection connection2 = transportService.getConnection(otherNode); + // from thread context inside sendRequestDecorate + AsyncSender sender = new AsyncSender() { + @Override + public void sendRequest( + Connection connection, + String action, + TransportRequest request, + TransportRequestOptions options, + TransportResponseHandler handler + ) { + User transientUser = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + assertEquals(transientUser, user); + } + }; // isSameNodeRequest = true securityInterceptor.sendRequestDecorate(sender, connection1, action, request, options, handler, localNode); - // from thread context inside sendRequestDecorate - doAnswer(i -> { - User transientUser = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - assertEquals(transientUser, user); - return null; - }).when(sender).sendRequest(any(Connection.class), eq(action), eq(request), eq(options), eq(handler)); // from original context User transientUser = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); assertEquals(transientUser, user); assertEquals(threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER), null); - // isSameNodeRequest = false - securityInterceptor.sendRequestDecorate(sender, connection2, action, request, options, handler, otherNode); // checking thread context inside sendRequestDecorate - doAnswer(i -> { - String serializedUserHeader = threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER); - assertEquals(serializedUserHeader, Base64Helper.serializeObject(user)); - return null; - }).when(sender).sendRequest(any(Connection.class), eq(action), eq(request), eq(options), eq(handler)); + sender = new AsyncSender() { + @Override + public void sendRequest( + Connection connection, + String action, + TransportRequest request, + TransportRequestOptions options, + TransportResponseHandler handler + ) { + String serializedUserHeader = threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER); + assertEquals(serializedUserHeader, Base64Helper.serializeObject(user, useJDKSerialization)); + } + }; + // isSameNodeRequest = false + securityInterceptor.sendRequestDecorate(sender, connection2, action, request, options, handler, localNode); // from original context User transientUser2 = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); assertEquals(transientUser2, user); assertEquals(threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER), null); + } + + @Test + public void testSendRequestDecorate() { + testSendRequestDecorate(Version.CURRENT); + } + /** + * Tests the scenario when remote node does not implement custom serialization protocol and uses JDK serialization + */ + @Test + public void testSendRequestDecorateWhenRemoteNodeUsesJDKSerde() { + testSendRequestDecorate(Version.V_2_0_0); } } diff --git a/src/test/java/org/opensearch/security/transport/SecuritySSLRequestHandlerTests.java b/src/test/java/org/opensearch/security/transport/SecuritySSLRequestHandlerTests.java new file mode 100644 index 0000000000..23a64e4be3 --- /dev/null +++ b/src/test/java/org/opensearch/security/transport/SecuritySSLRequestHandlerTests.java @@ -0,0 +1,80 @@ +package org.opensearch.security.transport; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.opensearch.Version; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.ssl.SslExceptionHandler; +import org.opensearch.security.ssl.transport.PrincipalExtractor; +import org.opensearch.security.ssl.transport.SSLConfig; +import org.opensearch.security.ssl.transport.SecuritySSLRequestHandler; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.tasks.Task; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.TransportChannel; +import org.opensearch.transport.TransportRequest; +import org.opensearch.transport.TransportRequestHandler; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class SecuritySSLRequestHandlerTests { + + @Mock + TransportRequestHandler actualHandler; + @Mock + SSLConfig sslConfig; + ThreadPool threadPool; + SslExceptionHandler sslExceptionHandler; + Settings settings; + SecuritySSLRequestHandler securitySSLRequestHandler; + String testAction; + + @Mock + private PrincipalExtractor principalExtractor; + + @Before + public void setUp() { + settings = Settings.builder() + .put("node.name", SecurityInterceptorTests.class.getSimpleName()) + .put("request.headers.default", "1") + .build(); + threadPool = new ThreadPool(settings); + testAction = "test_action"; + sslExceptionHandler = mock(SslExceptionHandler.class); + securitySSLRequestHandler = new SecuritySSLRequestHandler<>( + testAction, + actualHandler, + threadPool, + principalExtractor, + sslConfig, + sslExceptionHandler + ); + doNothing().when(sslExceptionHandler) + .logError(any(Exception.class), any(TransportRequest.class), any(String.class), any(Task.class), anyInt()); + } + + @Test + public void testUseJDKSerializationHeaderIsSetOnMessageReceived() throws Exception { + TransportRequest transportRequest = mock(TransportRequest.class); + TransportChannel transportChannel = mock(TransportChannel.class); + Task task = mock(Task.class); + doNothing().when(transportChannel).sendResponse(ArgumentMatchers.any(Exception.class)); + when(transportChannel.getVersion()).thenReturn(Version.V_2_11_0); + when(transportChannel.getChannelType()).thenReturn("transport"); + + Assert.assertThrows(Exception.class, () -> securitySSLRequestHandler.messageReceived(transportRequest, transportChannel, task)); + Assert.assertTrue(threadPool.getThreadContext().getTransient(ConfigConstants.USE_JDK_SERIALIZATION)); + + threadPool.getThreadContext().stashContext(); + when(transportChannel.getVersion()).thenReturn(Version.V_3_0_0); + Assert.assertThrows(Exception.class, () -> securitySSLRequestHandler.messageReceived(transportRequest, transportChannel, task)); + Assert.assertFalse(threadPool.getThreadContext().getTransient(ConfigConstants.USE_JDK_SERIALIZATION)); + } +} From 8ea5e56c3344a5a6b6b9333fedf30c9bf542717f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 12:25:31 -0400 Subject: [PATCH 12/23] dependabot: bump org.xerial.snappy:snappy-java from 1.1.10.4 to 1.1.10.5 (#3435) Bumps [org.xerial.snappy:snappy-java](https://github.com/xerial/snappy-java) from 1.1.10.4 to 1.1.10.5. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index d3b3846edd..8e192900cc 100644 --- a/build.gradle +++ b/build.gradle @@ -430,7 +430,7 @@ configurations { force "io.netty:netty-transport-native-unix-common:${versions.netty}" force "org.apache.bcel:bcel:6.7.0" // This line should be removed once Spotbugs is upgraded to 4.7.4 force "com.github.luben:zstd-jni:${versions.zstd}" - force "org.xerial.snappy:snappy-java:1.1.10.4" + force "org.xerial.snappy:snappy-java:1.1.10.5" force "com.google.guava:guava:${guava_version}" } } @@ -559,7 +559,7 @@ dependencies { runtimeOnly 'io.dropwizard.metrics:metrics-core:4.2.19' 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.4' + runtimeOnly 'org.xerial.snappy:snappy-java:1.1.10.5' runtimeOnly 'org.codehaus.woodstox:stax2-api:4.2.1' runtimeOnly "org.glassfish.jaxb:txw2:${jaxb_version}" runtimeOnly 'com.fasterxml.woodstox:woodstox-core:6.5.1' From 97dea2a9be8c01c3481688f7996a6f9539baef05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 12:28:13 -0400 Subject: [PATCH 13/23] dependabot: bump org.ow2.asm:asm from 9.5 to 9.6 (#3433) Bumps org.ow2.asm:asm from 9.5 to 9.6. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8e192900cc..92b7844252 100644 --- a/build.gradle +++ b/build.gradle @@ -525,7 +525,7 @@ dependencies { runtimeOnly 'com.google.errorprone:error_prone_annotations:2.22.0' runtimeOnly 'com.sun.istack:istack-commons-runtime:4.2.0' runtimeOnly 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0' - runtimeOnly 'org.ow2.asm:asm:9.5' + runtimeOnly 'org.ow2.asm:asm:9.6' testImplementation 'org.apache.camel:camel-xmlsecurity:3.21.0' From 567217cd5b4a28ce4ceb7fe6c8923ce7e1e489c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 13:02:09 -0400 Subject: [PATCH 14/23] dependabot: bump org.apache.camel:camel-xmlsecurity from 3.21.0 to 3.21.1 (#3432) Bumps org.apache.camel:camel-xmlsecurity from 3.21.0 to 3.21.1. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.apache.camel:camel-xmlsecurity&package-manager=gradle&previous-version=3.21.0&new-version=3.21.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 92b7844252..d2e08af0d7 100644 --- a/build.gradle +++ b/build.gradle @@ -527,7 +527,7 @@ dependencies { runtimeOnly 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0' runtimeOnly 'org.ow2.asm:asm:9.6' - testImplementation 'org.apache.camel:camel-xmlsecurity:3.21.0' + testImplementation 'org.apache.camel:camel-xmlsecurity:3.21.1' //OpenSAML implementation 'net.shibboleth.utilities:java-support:8.4.0' From fa52472ad18863716ed201ab2bf1422726ea8fa8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 13:02:40 -0400 Subject: [PATCH 15/23] dependabot: bump com.github.wnameless.json:json-base from 2.4.2 to 2.4.3 (#3434) Bumps [com.github.wnameless.json:json-base](https://github.com/wnameless/json-base) from 2.4.2 to 2.4.3.
Changelog

Sourced from com.github.wnameless.json:json-base's changelog.

Version 1.0.0

  • First release

Version 1.1.0

  • Support Java 9 Module
  • Add isEmpty() to JsonArrayBase and JsonObjectBase
  • Change package name from com.github.wnameless.json to com.github.wnameless.json.base

Version 1.1.1

  • Using "requires static" on Gson and Jackson

Version 1.2.0

  • Add Jsonable interface

Version 2.0.0

  • Add #asBigInteger, #asBigDecimal, #asNumber, #asNull
  • Add #toMap, #toList
  • Add JsonValueCore, JsonObjectCore, JsonArrayCore, JsonCore, JsonSource
  • Add JsonPrinter, JsonValueUtils

Version 2.1.0

  • Fix JsonProter#prettyPrint bug
  • Improve module-info.java

Version 2.2.0

  • Alter all "requires static transitive" to "requires static" in module-info.java to avoid "module not found" error while compiling by other projects

Version 2.2.1

  • Fix JsonPrinter bug on the edge case: having backslash before ending double quotes

Version 2.3.0

  • Add org.json lib support
  • Add Jakarta lib support
  • Change the return type of JsonArrayCore#remove(int) from boolean to JsonArrayCore
  • Increase JUnit code coverage to 100%
  • Remove Cobertura maven dependency

Version 2.4.0

  • Add #stream to JsonArrayBase and JsonObjectBase

Version 2.4.1

  • Improve OrgJsonValue#asNumber
  • Add JsonPrinter#toJsonString

Version 2.4.2

  • Upgrade POM

Version 2.4.3

  • Modify JsonValueUtils#toJavaNumber for preserving precise scale of the float number
Commits
  • a2785b7 [maven-release-plugin] prepare release json-base-2.4.3
  • 849d039 Improve GsonJsonValue implementation
  • 2442050 To preserve precise scale of the float number
  • 10f6d99 [maven-release-plugin] prepare for next development iteration
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.github.wnameless.json:json-base&package-manager=gradle&previous-version=2.4.2&new-version=2.4.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d2e08af0d7..5649e8058a 100644 --- a/build.gradle +++ b/build.gradle @@ -494,7 +494,7 @@ dependencies { implementation "io.jsonwebtoken:jjwt-impl:${jjwt_version}" implementation "io.jsonwebtoken:jjwt-jackson:${jjwt_version}" // JSON flattener - implementation ("com.github.wnameless.json:json-base:2.4.2") { + implementation ("com.github.wnameless.json:json-base:2.4.3") { exclude group: "org.glassfish", module: "jakarta.json" exclude group: "com.google.code.gson", module: "gson" exclude group: "org.json", module: "json" From 702cc3ca4d08bd44c4ae2de3a357524ed08fb10d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 14:20:48 -0400 Subject: [PATCH 16/23] dependabot: bump commons-io:commons-io from 2.13.0 to 2.14.0 (#3431) Bumps commons-io:commons-io from 2.13.0 to 2.14.0. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5649e8058a..741fd41262 100644 --- a/build.gradle +++ b/build.gradle @@ -627,7 +627,7 @@ dependencies { integrationTestImplementation 'junit:junit:4.13.2' integrationTestImplementation "org.opensearch.plugin:reindex-client:${opensearch_version}" integrationTestImplementation "org.opensearch.plugin:percolator-client:${opensearch_version}" - integrationTestImplementation 'commons-io:commons-io:2.13.0' + integrationTestImplementation 'commons-io:commons-io:2.14.0' integrationTestImplementation "org.apache.logging.log4j:log4j-core:${versions.log4j}" integrationTestImplementation "org.apache.logging.log4j:log4j-jul:${versions.log4j}" integrationTestImplementation 'org.hamcrest:hamcrest:2.2' From 1ffa23cc96a6bf96752c33b7f134e4e6139dc828 Mon Sep 17 00:00:00 2001 From: Ryan Liang <109499885+RyanL1997@users.noreply.github.com> Date: Mon, 2 Oct 2023 13:53:06 -0700 Subject: [PATCH 17/23] [Enhancement] Setup auth token utils for obo (#3419) Setup auth token utils for obo (#3419) --------- Signed-off-by: Ryan Liang --- .../security/authtoken/jwt/JwtVendor.java | 10 +-- .../http/OnBehalfOfAuthenticator.java | 6 +- .../securityconf/DynamicConfigModelV7.java | 4 +- .../security/util/AuthTokenUtils.java | 41 ++++++++++ .../authtoken/jwt/AuthTokenUtilsTest.java | 78 +++++++++++++++++++ 5 files changed, 129 insertions(+), 10 deletions(-) create mode 100644 src/main/java/org/opensearch/security/util/AuthTokenUtils.java create mode 100644 src/test/java/org/opensearch/security/authtoken/jwt/AuthTokenUtilsTest.java diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java index 5d3262799f..e68a5ef2d7 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -16,7 +16,6 @@ import java.util.Optional; import java.util.function.LongSupplier; -import com.google.common.base.Strings; import org.apache.cxf.jaxrs.json.basic.JsonMapObjectReaderWriter; import org.apache.cxf.rs.security.jose.jwk.JsonWebKey; import org.apache.cxf.rs.security.jose.jwk.KeyType; @@ -32,6 +31,8 @@ import org.opensearch.common.settings.Settings; import org.opensearch.security.ssl.util.ExceptionUtils; +import static org.opensearch.security.util.AuthTokenUtils.isKeyNull; + public class JwtVendor { private static final Logger logger = LogManager.getLogger(JwtVendor.class); @@ -53,7 +54,7 @@ public JwtVendor(final Settings settings, final Optional timeProvi throw ExceptionUtils.createJwkCreationException(e); } this.jwtProducer = jwtProducer; - if (settings.get("encryption_key") == null) { + if (isKeyNull(settings, "encryption_key")) { throw new IllegalArgumentException("encryption_key cannot be null"); } else { this.claimsEncryptionKey = settings.get("encryption_key"); @@ -73,9 +74,8 @@ public JwtVendor(final Settings settings, final Optional timeProvi * Encryption Algorithm: HS512 * */ static JsonWebKey createJwkFromSettings(Settings settings) throws Exception { - String signingKey = settings.get("signing_key"); - - if (!Strings.isNullOrEmpty(signingKey)) { + if (!isKeyNull(settings, "signing_key")) { + String signingKey = settings.get("signing_key"); JsonWebKey jwk = new JsonWebKey(); diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index 467edd8ac4..c47e850b75 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -43,13 +43,12 @@ import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; +import static org.opensearch.security.util.AuthTokenUtils.isAccessToRestrictedEndpoints; public class OnBehalfOfAuthenticator implements HTTPAuthenticator { private static final String REGEX_PATH_PREFIX = "/(" + LEGACY_OPENDISTRO_PREFIX + "|" + PLUGINS_PREFIX + ")/" + "(.*)"; private static final Pattern PATTERN_PATH_PREFIX = Pattern.compile(REGEX_PATH_PREFIX); - private static final String ON_BEHALF_OF_SUFFIX = "api/generateonbehalfoftoken"; - private static final String ACCOUNT_SUFFIX = "api/account"; protected final Logger log = LogManager.getLogger(this.getClass()); @@ -233,8 +232,7 @@ private void logDebug(String message, Object... args) { public Boolean isRequestAllowed(final RestRequest request) { Matcher matcher = PATTERN_PATH_PREFIX.matcher(request.path()); final String suffix = matcher.matches() ? matcher.group(2) : null; - if (request.method() == RestRequest.Method.POST && ON_BEHALF_OF_SUFFIX.equals(suffix) - || request.method() == RestRequest.Method.PUT && ACCOUNT_SUFFIX.equals(suffix)) { + if (isAccessToRestrictedEndpoints(request, suffix)) { final OpenSearchException exception = ExceptionUtils.invalidUsageOfOBOTokenException(); log.error(exception.toString()); return false; diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index fcbf985f60..0de83f2e2e 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -67,6 +67,8 @@ import org.opensearch.security.securityconf.impl.v7.ConfigV7.AuthzDomain; import org.opensearch.security.support.ReflectionHelper; +import static org.opensearch.security.util.AuthTokenUtils.isKeyNull; + public class DynamicConfigModelV7 extends DynamicConfigModel { private final ConfigV7 config; @@ -383,7 +385,7 @@ private void buildAAA() { * order: -1 - prioritize the OBO authentication when it gets enabled */ Settings oboSettings = getDynamicOnBehalfOfSettings(); - if (oboSettings.get("signing_key") != null && oboSettings.get("encryption_key") != null) { + if (!isKeyNull(oboSettings, "signing_key") && !isKeyNull(oboSettings, "encryption_key")) { final AuthDomain _ad = new AuthDomain( new NoOpAuthenticationBackend(Settings.EMPTY, null), new OnBehalfOfAuthenticator(getDynamicOnBehalfOfSettings(), this.cih.getClusterName()), diff --git a/src/main/java/org/opensearch/security/util/AuthTokenUtils.java b/src/main/java/org/opensearch/security/util/AuthTokenUtils.java new file mode 100644 index 0000000000..30f331d3a7 --- /dev/null +++ b/src/main/java/org/opensearch/security/util/AuthTokenUtils.java @@ -0,0 +1,41 @@ +/* + * 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.util; + +import org.opensearch.common.settings.Settings; +import org.opensearch.rest.RestRequest; + +import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.rest.RestRequest.Method.PUT; + +public class AuthTokenUtils { + private static final String ON_BEHALF_OF_SUFFIX = "api/generateonbehalfoftoken"; + private static final String ACCOUNT_SUFFIX = "api/account"; + + public static Boolean isAccessToRestrictedEndpoints(final RestRequest request, final String suffix) { + if (suffix == null) { + return false; + } + switch (suffix) { + case ON_BEHALF_OF_SUFFIX: + return request.method() == POST; + case ACCOUNT_SUFFIX: + return request.method() == PUT; + default: + return false; + } + } + + public static Boolean isKeyNull(Settings settings, String key) { + return settings.get(key) == null; + } +} diff --git a/src/test/java/org/opensearch/security/authtoken/jwt/AuthTokenUtilsTest.java b/src/test/java/org/opensearch/security/authtoken/jwt/AuthTokenUtilsTest.java new file mode 100644 index 0000000000..d563308e31 --- /dev/null +++ b/src/test/java/org/opensearch/security/authtoken/jwt/AuthTokenUtilsTest.java @@ -0,0 +1,78 @@ +/* + * 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.authtoken.jwt; + +import org.opensearch.common.settings.Settings; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.rest.RestRequest; +import org.opensearch.security.util.AuthTokenUtils; +import org.opensearch.test.rest.FakeRestRequest; +import org.junit.Test; + +import java.util.Collections; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class AuthTokenUtilsTest { + + @Test + public void testIsAccessToRestrictedEndpointsForOnBehalfOfToken() { + NamedXContentRegistry namedXContentRegistry = new NamedXContentRegistry(Collections.emptyList()); + + FakeRestRequest request = new FakeRestRequest.Builder(namedXContentRegistry).withPath("/api/generateonbehalfoftoken") + .withMethod(RestRequest.Method.POST) + .build(); + + assertTrue(AuthTokenUtils.isAccessToRestrictedEndpoints(request, "api/generateonbehalfoftoken")); + } + + @Test + public void testIsAccessToRestrictedEndpointsForAccount() { + NamedXContentRegistry namedXContentRegistry = new NamedXContentRegistry(Collections.emptyList()); + + FakeRestRequest request = new FakeRestRequest.Builder(namedXContentRegistry).withPath("/api/account") + .withMethod(RestRequest.Method.PUT) + .build(); + + assertTrue(AuthTokenUtils.isAccessToRestrictedEndpoints(request, "api/account")); + } + + @Test + public void testIsAccessToRestrictedEndpointsFalseCase() { + NamedXContentRegistry namedXContentRegistry = new NamedXContentRegistry(Collections.emptyList()); + + FakeRestRequest request = new FakeRestRequest.Builder(namedXContentRegistry).withPath("/api/someotherendpoint") + .withMethod(RestRequest.Method.GET) + .build(); + + assertFalse(AuthTokenUtils.isAccessToRestrictedEndpoints(request, "api/someotherendpoint")); + } + + @Test + public void testIsKeyNullWithNullValue() { + Settings settings = Settings.builder().put("someKey", (String) null).build(); + assertTrue(AuthTokenUtils.isKeyNull(settings, "someKey")); + } + + @Test + public void testIsKeyNullWithNonNullValue() { + Settings settings = Settings.builder().put("someKey", "value").build(); + assertFalse(AuthTokenUtils.isKeyNull(settings, "someKey")); + } + + @Test + public void testIsKeyNullWithAbsentKey() { + Settings settings = Settings.builder().build(); + assertTrue(AuthTokenUtils.isKeyNull(settings, "absentKey")); + } +} From 7924da13a57ecbf3352d84e6d020012723b81fa1 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura <35282393+DarshitChanpura@users.noreply.github.com> Date: Tue, 3 Oct 2023 10:32:15 -0400 Subject: [PATCH 18/23] Refactors reRequestAuthentication to call notifyIpAuthFailureListener before sending the response to the channel (#3411) Prior to this change, the ip auth failure listener was not called upon challengeAuthenticator check invocation, which caused AddressBasedRateLimiter to not be invoked. With this change AddressBasedRateLimiter will be invoked upon multiple wrong requests from an ip. Signed-off-by: Darshit Chanpura --- .../IpBruteForceAttacksPreventionTests.java | 27 +++++----- ...cksPreventionWithDomainChallengeTests.java | 32 ++++++++++++ .../cluster/LocalOpenSearchCluster.java | 2 +- .../jwt/AbstractHTTPJwtAuthenticator.java | 6 +-- .../auth/http/jwt/HTTPJwtAuthenticator.java | 6 +-- .../kerberos/HTTPSpnegoAuthenticator.java | 11 ++-- .../http/saml/AuthTokenProcessorHandler.java | 41 ++++----------- .../auth/http/saml/HTTPSamlAuthenticator.java | 19 ++++--- .../security/auth/BackendRegistry.java | 51 ++++++++++-------- .../security/auth/HTTPAuthenticator.java | 15 +++--- .../security/http/HTTPBasicAuthenticator.java | 6 +-- .../http/HTTPClientCertAuthenticator.java | 6 +-- .../security/http/HTTPProxyAuthenticator.java | 6 +-- .../http/OnBehalfOfAuthenticator.java | 6 +-- .../proxy/HTTPExtendedProxyAuthenticator.java | 6 --- .../http/saml/HTTPSamlAuthenticatorTest.java | 52 +++++++++++++------ .../limiting/AddressBasedRateLimiterTest.java | 21 ++++---- .../UserNameBasedRateLimiterTest.java | 21 ++++---- .../cache/DummyHTTPAuthenticator.java | 6 +-- 19 files changed, 181 insertions(+), 159 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionWithDomainChallengeTests.java diff --git a/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java index bb16e0be1b..34e79613f6 100644 --- a/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java +++ b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java @@ -12,7 +12,6 @@ import java.util.concurrent.TimeUnit; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -36,8 +35,8 @@ @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class IpBruteForceAttacksPreventionTests { - private static final User USER_1 = new User("simple-user-1").roles(ALL_ACCESS); - private static final User USER_2 = new User("simple-user-2").roles(ALL_ACCESS); + static final User USER_1 = new User("simple-user-1").roles(ALL_ACCESS); + static final User USER_2 = new User("simple-user-2").roles(ALL_ACCESS); public static final int ALLOWED_TRIES = 3; public static final int TIME_WINDOW_SECONDS = 3; @@ -51,7 +50,7 @@ public class IpBruteForceAttacksPreventionTests { public static final String CLIENT_IP_8 = "127.0.0.8"; public static final String CLIENT_IP_9 = "127.0.0.9"; - private static final AuthFailureListeners listener = new AuthFailureListeners().addRateLimit( + static final AuthFailureListeners listener = new AuthFailureListeners().addRateLimit( new RateLimiting("internal_authentication_backend_limiting").type("ip") .allowedTries(ALLOWED_TRIES) .timeWindowSeconds(TIME_WINDOW_SECONDS) @@ -60,13 +59,17 @@ public class IpBruteForceAttacksPreventionTests { .maxTrackedClients(500) ); - @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) - .anonymousAuth(false) - .authFailureListeners(listener) - .authc(AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE) - .users(USER_1, USER_2) - .build(); + @Rule + public LocalCluster cluster = createCluster(); + + public LocalCluster createCluster() { + return new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .authFailureListeners(listener) + .authc(AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE) + .users(USER_1, USER_2) + .build(); + } @Rule public LogsRule logsRule = new LogsRule("org.opensearch.security.auth.BackendRegistry"); @@ -151,7 +154,7 @@ public void shouldReleaseIpAddressLock() throws InterruptedException { } } - private static void authenticateUserWithIncorrectPassword(String sourceIpAddress, User user, int numberOfRequests) { + void authenticateUserWithIncorrectPassword(String sourceIpAddress, User user, int numberOfRequests) { var clientConfiguration = new TestRestClientConfiguration().username(user.getName()) .password("incorrect password") .sourceInetAddress(sourceIpAddress); diff --git a/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionWithDomainChallengeTests.java b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionWithDomainChallengeTests.java new file mode 100644 index 0000000000..6159599119 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionWithDomainChallengeTests.java @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ + +package org.opensearch.security; + +import 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.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class IpBruteForceAttacksPreventionWithDomainChallengeTests extends IpBruteForceAttacksPreventionTests { + @Override + public LocalCluster createCluster() { + return new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .authFailureListeners(listener) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USER_1, USER_2) + .build(); + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java index c09127e592..77890a4645 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java @@ -344,7 +344,7 @@ public String toString() { String clusterManagerNodes = nodeByTypeToString(CLUSTER_MANAGER); String dataNodes = nodeByTypeToString(DATA); String clientNodes = nodeByTypeToString(CLIENT); - return "\nES Cluster " + return "\nOS Cluster " + clusterName + "\ncluster manager nodes: " + clusterManagerNodes diff --git a/src/main/java/com/amazon/dlic/auth/http/jwt/AbstractHTTPJwtAuthenticator.java b/src/main/java/com/amazon/dlic/auth/http/jwt/AbstractHTTPJwtAuthenticator.java index 4dab3c7740..6dbb7b7676 100644 --- a/src/main/java/com/amazon/dlic/auth/http/jwt/AbstractHTTPJwtAuthenticator.java +++ b/src/main/java/com/amazon/dlic/auth/http/jwt/AbstractHTTPJwtAuthenticator.java @@ -36,7 +36,6 @@ import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.common.Strings; import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auth.HTTPAuthenticator; @@ -236,11 +235,10 @@ public String[] extractRoles(JwtClaims claims) { protected abstract KeyProvider initKeyProvider(Settings settings, Path configPath) throws Exception; @Override - public boolean reRequestAuthentication(RestChannel channel, AuthCredentials authCredentials) { + public BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials authCredentials) { final BytesRestResponse wwwAuthenticateResponse = new BytesRestResponse(RestStatus.UNAUTHORIZED, ""); wwwAuthenticateResponse.addHeader("WWW-Authenticate", "Bearer realm=\"OpenSearch Security\""); - channel.sendResponse(wwwAuthenticateResponse); - return true; + return wwwAuthenticateResponse; } public String getRequiredAudience() { 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 03e385d5c0..338a490037 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 @@ -31,7 +31,6 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auth.HTTPAuthenticator; @@ -171,11 +170,10 @@ private AuthCredentials extractCredentials0(final RestRequest request) { } @Override - public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) { + public BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials credentials) { final BytesRestResponse wwwAuthenticateResponse = new BytesRestResponse(RestStatus.UNAUTHORIZED, ""); wwwAuthenticateResponse.addHeader("WWW-Authenticate", "Bearer realm=\"OpenSearch Security\""); - channel.sendResponse(wwwAuthenticateResponse); - return true; + return wwwAuthenticateResponse; } @Override diff --git a/src/main/java/com/amazon/dlic/auth/http/kerberos/HTTPSpnegoAuthenticator.java b/src/main/java/com/amazon/dlic/auth/http/kerberos/HTTPSpnegoAuthenticator.java index 29f537e899..99bc23b746 100644 --- a/src/main/java/com/amazon/dlic/auth/http/kerberos/HTTPSpnegoAuthenticator.java +++ b/src/main/java/com/amazon/dlic/auth/http/kerberos/HTTPSpnegoAuthenticator.java @@ -49,7 +49,6 @@ import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.env.Environment; import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auth.HTTPAuthenticator; @@ -280,8 +279,7 @@ public GSSCredential run() throws GSSException { } @Override - public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) { - + public BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials credentials) { final BytesRestResponse wwwAuthenticateResponse; XContentBuilder response = getNegotiateResponseBody(); @@ -291,16 +289,15 @@ public boolean reRequestAuthentication(final RestChannel channel, AuthCredential wwwAuthenticateResponse = new BytesRestResponse(RestStatus.UNAUTHORIZED, EMPTY_STRING); } - if (creds == null || creds.getNativeCredentials() == null) { + if (credentials == null || credentials.getNativeCredentials() == null) { wwwAuthenticateResponse.addHeader("WWW-Authenticate", "Negotiate"); } else { wwwAuthenticateResponse.addHeader( "WWW-Authenticate", - "Negotiate " + Base64.getEncoder().encodeToString((byte[]) creds.getNativeCredentials()) + "Negotiate " + Base64.getEncoder().encodeToString((byte[]) credentials.getNativeCredentials()) ); } - channel.sendResponse(wwwAuthenticateResponse); - return true; + return wwwAuthenticateResponse; } @Override diff --git a/src/main/java/com/amazon/dlic/auth/http/saml/AuthTokenProcessorHandler.java b/src/main/java/com/amazon/dlic/auth/http/saml/AuthTokenProcessorHandler.java index 3fee4a9444..7210ed5950 100644 --- a/src/main/java/com/amazon/dlic/auth/http/saml/AuthTokenProcessorHandler.java +++ b/src/main/java/com/amazon/dlic/auth/http/saml/AuthTokenProcessorHandler.java @@ -23,7 +23,6 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; -import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPathExpressionException; import com.fasterxml.jackson.core.JsonParseException; @@ -32,7 +31,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.base.Strings; import com.onelogin.saml2.authn.SamlResponse; -import com.onelogin.saml2.exception.SettingsException; import com.onelogin.saml2.exception.ValidationError; import com.onelogin.saml2.settings.Saml2Settings; import com.onelogin.saml2.util.Util; @@ -49,7 +47,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.joda.time.DateTime; -import org.xml.sax.SAXException; import org.opensearch.OpenSearchSecurityException; import org.opensearch.SpecialPermission; @@ -57,7 +54,6 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentType; import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; import org.opensearch.core.rest.RestStatus; @@ -122,7 +118,7 @@ class AuthTokenProcessorHandler { } @SuppressWarnings("removal") - boolean handle(RestRequest restRequest, RestChannel restChannel) throws Exception { + BytesRestResponse handle(RestRequest restRequest) throws Exception { try { final SecurityManager sm = System.getSecurityManager(); @@ -130,11 +126,10 @@ boolean handle(RestRequest restRequest, RestChannel restChannel) throws Exceptio sm.checkPermission(new SpecialPermission()); } - return AccessController.doPrivileged(new PrivilegedExceptionAction() { + return AccessController.doPrivileged(new PrivilegedExceptionAction() { @Override - public Boolean run() throws XPathExpressionException, SamlConfigException, IOException, ParserConfigurationException, - SAXException, SettingsException { - return handleLowLevel(restRequest, restChannel); + public BytesRestResponse run() throws SamlConfigException, IOException { + return handleLowLevel(restRequest); } }); } catch (PrivilegedActionException e) { @@ -147,13 +142,11 @@ public Boolean run() throws XPathExpressionException, SamlConfigException, IOExc } private AuthTokenProcessorAction.Response handleImpl( - RestRequest restRequest, - RestChannel restChannel, String samlResponseBase64, String samlRequestId, String acsEndpoint, Saml2Settings saml2Settings - ) throws XPathExpressionException, ParserConfigurationException, SAXException, IOException, SettingsException { + ) { if (token_log.isDebugEnabled()) { try { token_log.debug( @@ -188,8 +181,7 @@ private AuthTokenProcessorAction.Response handleImpl( } } - private boolean handleLowLevel(RestRequest restRequest, RestChannel restChannel) throws SamlConfigException, IOException, - XPathExpressionException, ParserConfigurationException, SAXException, SettingsException { + private BytesRestResponse handleLowLevel(RestRequest restRequest) throws SamlConfigException, IOException { try { if (restRequest.getMediaType() != XContentType.JSON) { @@ -234,31 +226,18 @@ private boolean handleLowLevel(RestRequest restRequest, RestChannel restChannel) acsEndpoint = getAbsoluteAcsEndpoint(((ObjectNode) jsonRoot).get("acsEndpoint").textValue()); } - AuthTokenProcessorAction.Response responseBody = this.handleImpl( - restRequest, - restChannel, - samlResponseBase64, - samlRequestId, - acsEndpoint, - saml2Settings - ); + AuthTokenProcessorAction.Response responseBody = this.handleImpl(samlResponseBase64, samlRequestId, acsEndpoint, saml2Settings); if (responseBody == null) { - return false; + return null; } String responseBodyString = DefaultObjectMapper.objectMapper.writeValueAsString(responseBody); - BytesRestResponse authenticateResponse = new BytesRestResponse(RestStatus.OK, "application/json", responseBodyString); - restChannel.sendResponse(authenticateResponse); - - return true; + return new BytesRestResponse(RestStatus.OK, "application/json", responseBodyString); } catch (JsonProcessingException e) { log.warn("Error while parsing JSON for /_opendistro/_security/api/authtoken", e); - - BytesRestResponse authenticateResponse = new BytesRestResponse(RestStatus.BAD_REQUEST, "JSON could not be parsed"); - restChannel.sendResponse(authenticateResponse); - return true; + return new BytesRestResponse(RestStatus.BAD_REQUEST, "JSON could not be parsed"); } } diff --git a/src/main/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticator.java b/src/main/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticator.java index cd6209952f..64d816fabf 100644 --- a/src/main/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticator.java +++ b/src/main/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticator.java @@ -56,7 +56,6 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auth.Destroyable; @@ -171,13 +170,15 @@ public String getType() { } @Override - public boolean reRequestAuthentication(RestChannel restChannel, AuthCredentials authCredentials) { + public BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials credentials) { try { - RestRequest restRequest = restChannel.request(); - Matcher matcher = PATTERN_PATH_PREFIX.matcher(restRequest.path()); + Matcher matcher = PATTERN_PATH_PREFIX.matcher(request.path()); final String suffix = matcher.matches() ? matcher.group(2) : null; - if (API_AUTHTOKEN_SUFFIX.equals(suffix) && this.authTokenProcessorHandler.handle(restRequest, restChannel)) { - return true; + if (API_AUTHTOKEN_SUFFIX.equals(suffix)) { + final BytesRestResponse restResponse = this.authTokenProcessorHandler.handle(request); + if (restResponse != null) { + return restResponse; + } } Saml2Settings saml2Settings = this.saml2SettingsProvider.getCached(); @@ -185,12 +186,10 @@ public boolean reRequestAuthentication(RestChannel restChannel, AuthCredentials authenticateResponse.addHeader("WWW-Authenticate", getWwwAuthenticateHeader(saml2Settings)); - restChannel.sendResponse(authenticateResponse); - - return true; + return authenticateResponse; } catch (Exception e) { log.error("Error in reRequestAuthentication()", e); - return false; + return null; } } diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index ad1406426b..35d4e05c4c 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -230,7 +230,7 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g User authenticatedUser = null; - AuthCredentials authCredenetials = null; + AuthCredentials authCredentials = null; HTTPAuthenticator firstChallengingHttpAuthenticator = null; @@ -272,7 +272,7 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g continue; } - authCredenetials = ac; + authCredentials = ac; if (ac == null) { // no credentials found in request @@ -280,12 +280,18 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g continue; } - if (authDomain.isChallenge() && httpAuthenticator.reRequestAuthentication(channel, null)) { - auditLog.logFailedLogin("", false, null, request); - if (isTraceEnabled) { - log.trace("No 'Authorization' header, send 401 and 'WWW-Authenticate Basic'"); + if (authDomain.isChallenge()) { + final BytesRestResponse restResponse = httpAuthenticator.reRequestAuthentication(request, null); + if (restResponse != null) { + auditLog.logFailedLogin("", false, null, request); + if (isTraceEnabled) { + log.trace("No 'Authorization' header, send 401 and 'WWW-Authenticate Basic'"); + } + notifyIpAuthFailureListeners(request, authCredentials); + channel.sendResponse(restResponse); + return false; } - return false; + } else { // no reRequest possible if (isTraceEnabled) { @@ -296,9 +302,12 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g } else { org.apache.logging.log4j.ThreadContext.put("user", ac.getUsername()); if (!ac.isComplete()) { + final BytesRestResponse restResponse = httpAuthenticator.reRequestAuthentication(request, ac); // credentials found in request but we need another client challenge - if (httpAuthenticator.reRequestAuthentication(channel, ac)) { + if (restResponse != null) { // auditLog.logFailedLogin(ac.getUsername()+" ", request); --noauditlog + notifyIpAuthFailureListeners(request, ac); + channel.sendResponse(restResponse); return false; } else { // no reRequest possible @@ -376,7 +385,7 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g log.debug("User still not authenticated after checking {} auth domains", restAuthDomains.size()); } - if (authCredenetials == null && anonymousAuthEnabled) { + if (authCredentials == null && anonymousAuthEnabled) { final String tenant = Utils.coalesce(request.header("securitytenant"), request.header("security_tenant")); User anonymousUser = new User(User.ANONYMOUS.getName(), new HashSet(User.ANONYMOUS.getRoles()), null); anonymousUser.setRequestedTenant(tenant); @@ -388,6 +397,7 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g } return true; } + BytesRestResponse challengeResponse = null; if (firstChallengingHttpAuthenticator != null) { @@ -395,31 +405,28 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g log.debug("Rerequest with {}", firstChallengingHttpAuthenticator.getClass()); } - if (firstChallengingHttpAuthenticator.reRequestAuthentication(channel, null)) { + challengeResponse = firstChallengingHttpAuthenticator.reRequestAuthentication(request, null); + if (challengeResponse != null) { if (isDebugEnabled) { log.debug("Rerequest {} failed", firstChallengingHttpAuthenticator.getClass()); } - - log.warn( - "Authentication finally failed for {} from {}", - authCredenetials == null ? null : authCredenetials.getUsername(), - remoteAddress - ); - auditLog.logFailedLogin(authCredenetials == null ? null : authCredenetials.getUsername(), false, null, request); - return false; } } log.warn( "Authentication finally failed for {} from {}", - authCredenetials == null ? null : authCredenetials.getUsername(), + authCredentials == null ? null : authCredentials.getUsername(), remoteAddress ); - auditLog.logFailedLogin(authCredenetials == null ? null : authCredenetials.getUsername(), false, null, request); + auditLog.logFailedLogin(authCredentials == null ? null : authCredentials.getUsername(), false, null, request); - notifyIpAuthFailureListeners(request, authCredenetials); + notifyIpAuthFailureListeners(request, authCredentials); - channel.sendResponse(new BytesRestResponse(RestStatus.UNAUTHORIZED, "Authentication finally failed")); + channel.sendResponse( + challengeResponse != null + ? challengeResponse + : new BytesRestResponse(RestStatus.UNAUTHORIZED, "Authentication finally failed") + ); return false; } diff --git a/src/main/java/org/opensearch/security/auth/HTTPAuthenticator.java b/src/main/java/org/opensearch/security/auth/HTTPAuthenticator.java index fa5065ef68..70b94d6dbf 100644 --- a/src/main/java/org/opensearch/security/auth/HTTPAuthenticator.java +++ b/src/main/java/org/opensearch/security/auth/HTTPAuthenticator.java @@ -28,7 +28,7 @@ import org.opensearch.OpenSearchSecurityException; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.rest.RestChannel; +import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestRequest; import org.opensearch.security.user.AuthCredentials; @@ -71,15 +71,14 @@ public interface HTTPAuthenticator { /** * If the {@code extractCredentials()} call was not successful or the authentication flow needs another roundtrip this method - * will be called. If the custom HTTP authenticator does not support this method is a no-op and false should be returned. - * + * will be called. If the custom HTTP authenticator does not support this method is a no-op and null response should be returned. * If the custom HTTP authenticator does support re-request authentication or supports authentication flows with multiple roundtrips - * then the response should be sent (through the channel) and true must be returned. + * then the response will be returned which can then be sent via response channel. * - * @param channel The rest channel to sent back the response via {@code channel.sendResponse()} + * @param request * @param credentials The credentials from the prior authentication attempt - * @return false if re-request is not supported/necessary, true otherwise. - * If true is returned {@code channel.sendResponse()} must be called so that the request completes. + * @return null if re-request is not supported/necessary, response object otherwise. + * If an object is returned {@code channel.sendResponse()} must be called so that the request completes. */ - boolean reRequestAuthentication(final RestChannel channel, AuthCredentials credentials); + BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials credentials); } diff --git a/src/main/java/org/opensearch/security/http/HTTPBasicAuthenticator.java b/src/main/java/org/opensearch/security/http/HTTPBasicAuthenticator.java index 4be83bc2e2..16a822df6d 100644 --- a/src/main/java/org/opensearch/security/http/HTTPBasicAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/HTTPBasicAuthenticator.java @@ -34,7 +34,6 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auth.HTTPAuthenticator; @@ -65,11 +64,10 @@ public AuthCredentials extractCredentials(final RestRequest request, ThreadConte } @Override - public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) { + public BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials credentials) { final BytesRestResponse wwwAuthenticateResponse = new BytesRestResponse(RestStatus.UNAUTHORIZED, "Unauthorized"); wwwAuthenticateResponse.addHeader("WWW-Authenticate", "Basic realm=\"OpenSearch Security\""); - channel.sendResponse(wwwAuthenticateResponse); - return true; + return wwwAuthenticateResponse; } @Override diff --git a/src/main/java/org/opensearch/security/http/HTTPClientCertAuthenticator.java b/src/main/java/org/opensearch/security/http/HTTPClientCertAuthenticator.java index b1e5d4ef40..cea59f7311 100644 --- a/src/main/java/org/opensearch/security/http/HTTPClientCertAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/HTTPClientCertAuthenticator.java @@ -41,7 +41,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.common.Strings; -import org.opensearch.rest.RestChannel; +import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestRequest; import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.support.ConfigConstants; @@ -98,8 +98,8 @@ public AuthCredentials extractCredentials(final RestRequest request, final Threa } @Override - public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) { - return false; + public BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials credentials) { + return null; } @Override diff --git a/src/main/java/org/opensearch/security/http/HTTPProxyAuthenticator.java b/src/main/java/org/opensearch/security/http/HTTPProxyAuthenticator.java index a58a842394..1db7b9769b 100644 --- a/src/main/java/org/opensearch/security/http/HTTPProxyAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/HTTPProxyAuthenticator.java @@ -37,7 +37,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.common.Strings; -import org.opensearch.rest.RestChannel; +import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestRequest; import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.support.ConfigConstants; @@ -89,8 +89,8 @@ public AuthCredentials extractCredentials(final RestRequest request, ThreadConte } @Override - public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) { - return false; + public BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials credentials) { + return null; } @Override diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index c47e850b75..02077bed7c 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -33,7 +33,7 @@ import org.opensearch.SpecialPermission; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.rest.RestChannel; +import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestRequest; import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; @@ -241,8 +241,8 @@ public Boolean isRequestAllowed(final RestRequest request) { } @Override - public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) { - return false; + public BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials credentials) { + return null; } @Override diff --git a/src/main/java/org/opensearch/security/http/proxy/HTTPExtendedProxyAuthenticator.java b/src/main/java/org/opensearch/security/http/proxy/HTTPExtendedProxyAuthenticator.java index ef20374d69..0423fecefe 100644 --- a/src/main/java/org/opensearch/security/http/proxy/HTTPExtendedProxyAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/proxy/HTTPExtendedProxyAuthenticator.java @@ -37,7 +37,6 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.common.Strings; -import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; import org.opensearch.security.http.HTTPProxyAuthenticator; import org.opensearch.security.user.AuthCredentials; @@ -84,11 +83,6 @@ public AuthCredentials extractCredentials(final RestRequest request, ThreadConte return credentials.markComplete(); } - @Override - public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) { - return false; - } - @Override public String getType() { return "extended-proxy"; diff --git a/src/test/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticatorTest.java b/src/test/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticatorTest.java index ff9ec19b09..2594388128 100644 --- a/src/test/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticatorTest.java +++ b/src/test/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticatorTest.java @@ -47,6 +47,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.core.xcontent.MediaType; import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; @@ -141,7 +142,8 @@ public void basicTest() throws Exception { RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - samlAuthenticator.reRequestAuthentication(tokenRestChannel, null); + final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); + tokenRestChannel.sendResponse(authenticatorResponse); String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); HashMap response = DefaultObjectMapper.objectMapper.readValue( @@ -188,7 +190,8 @@ public void decryptAssertionsTest() throws Exception { RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - samlAuthenticator.reRequestAuthentication(tokenRestChannel, null); + final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); + tokenRestChannel.sendResponse(authenticatorResponse); String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); HashMap response = DefaultObjectMapper.objectMapper.readValue( @@ -236,7 +239,8 @@ public void shouldUnescapeSamlEntitiesTest() throws Exception { RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - samlAuthenticator.reRequestAuthentication(tokenRestChannel, null); + final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); + tokenRestChannel.sendResponse(authenticatorResponse); String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); HashMap response = DefaultObjectMapper.objectMapper.readValue( @@ -287,7 +291,8 @@ public void shouldUnescapeSamlEntitiesTest2() throws Exception { RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - samlAuthenticator.reRequestAuthentication(tokenRestChannel, null); + final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); + tokenRestChannel.sendResponse(authenticatorResponse); String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); HashMap response = DefaultObjectMapper.objectMapper.readValue( @@ -338,7 +343,8 @@ public void shouldNotEscapeSamlEntities() throws Exception { RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - samlAuthenticator.reRequestAuthentication(tokenRestChannel, null); + final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); + tokenRestChannel.sendResponse(authenticatorResponse); String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); HashMap response = DefaultObjectMapper.objectMapper.readValue( @@ -389,7 +395,8 @@ public void shouldNotTrimWhitespaceInJwtRoles() throws Exception { RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - samlAuthenticator.reRequestAuthentication(tokenRestChannel, null); + final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); + tokenRestChannel.sendResponse(authenticatorResponse); String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); HashMap response = DefaultObjectMapper.objectMapper.readValue( @@ -436,7 +443,8 @@ public void testMetadataBody() throws Exception { RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - samlAuthenticator.reRequestAuthentication(tokenRestChannel, null); + final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); + tokenRestChannel.sendResponse(authenticatorResponse); String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); HashMap response = DefaultObjectMapper.objectMapper.readValue( @@ -501,7 +509,8 @@ public void unsolicitedSsoTest() throws Exception { ); TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - samlAuthenticator.reRequestAuthentication(tokenRestChannel, null); + final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); + tokenRestChannel.sendResponse(authenticatorResponse); String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); HashMap response = DefaultObjectMapper.objectMapper.readValue( @@ -552,7 +561,8 @@ public void badUnsolicitedSsoTest() throws Exception { ); TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - samlAuthenticator.reRequestAuthentication(tokenRestChannel, null); + final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); + tokenRestChannel.sendResponse(authenticatorResponse); Assert.assertEquals(RestStatus.UNAUTHORIZED, tokenRestChannel.response.status()); } @@ -584,7 +594,8 @@ public void wrongCertTest() throws Exception { RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - samlAuthenticator.reRequestAuthentication(tokenRestChannel, null); + final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); + tokenRestChannel.sendResponse(authenticatorResponse); Assert.assertEquals(401, tokenRestChannel.response.status().getStatus()); } @@ -613,7 +624,8 @@ public void noSignatureTest() throws Exception { RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - samlAuthenticator.reRequestAuthentication(tokenRestChannel, null); + final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); + tokenRestChannel.sendResponse(authenticatorResponse); Assert.assertEquals(401, tokenRestChannel.response.status().getStatus()); } @@ -646,7 +658,8 @@ public void rolesTest() throws Exception { RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - samlAuthenticator.reRequestAuthentication(tokenRestChannel, null); + final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); + tokenRestChannel.sendResponse(authenticatorResponse); String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); HashMap response = DefaultObjectMapper.objectMapper.readValue( @@ -693,7 +706,8 @@ public void idpEndpointWithQueryStringTest() throws Exception { RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - samlAuthenticator.reRequestAuthentication(tokenRestChannel, null); + final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); + tokenRestChannel.sendResponse(authenticatorResponse); String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); HashMap response = DefaultObjectMapper.objectMapper.readValue( @@ -747,7 +761,8 @@ private void commaSeparatedRoles(final String rolesAsString, final Settings.Buil RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - samlAuthenticator.reRequestAuthentication(tokenRestChannel, null); + final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); + tokenRestChannel.sendResponse(authenticatorResponse); String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); HashMap response = DefaultObjectMapper.objectMapper.readValue( @@ -850,7 +865,8 @@ public void initialConnectionFailureTest() throws Exception { RestRequest restRequest = new FakeRestRequest(ImmutableMap.of(), new HashMap()); TestRestChannel restChannel = new TestRestChannel(restRequest); - samlAuthenticator.reRequestAuthentication(restChannel, null); + BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(restRequest, null); + restChannel.sendResponse(authenticatorResponse); Assert.assertNull(restChannel.response); @@ -870,7 +886,8 @@ public void initialConnectionFailureTest() throws Exception { RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - samlAuthenticator.reRequestAuthentication(tokenRestChannel, null); + authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); + tokenRestChannel.sendResponse(authenticatorResponse); String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); HashMap response = DefaultObjectMapper.objectMapper.readValue( @@ -893,7 +910,8 @@ private AuthenticateHeaders getAutenticateHeaders(HTTPSamlAuthenticator samlAuth RestRequest restRequest = new FakeRestRequest(ImmutableMap.of(), new HashMap()); TestRestChannel restChannel = new TestRestChannel(restRequest); - samlAuthenticator.reRequestAuthentication(restChannel, null); + final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(restRequest, null); + restChannel.sendResponse(authenticatorResponse); List wwwAuthenticateHeaders = restChannel.response.getHeaders().get("WWW-Authenticate"); diff --git a/src/test/java/org/opensearch/security/auth/limiting/AddressBasedRateLimiterTest.java b/src/test/java/org/opensearch/security/auth/limiting/AddressBasedRateLimiterTest.java index 827bfa24b6..69ddc5c03a 100644 --- a/src/test/java/org/opensearch/security/auth/limiting/AddressBasedRateLimiterTest.java +++ b/src/test/java/org/opensearch/security/auth/limiting/AddressBasedRateLimiterTest.java @@ -20,28 +20,27 @@ import org.junit.Test; import org.opensearch.common.settings.Settings; -import org.opensearch.security.user.AuthCredentials; + +import java.net.InetAddress; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class AddressBasedRateLimiterTest { - private final static byte[] PASSWORD = new byte[] { '1', '2', '3' }; - @Test public void simpleTest() throws Exception { Settings settings = Settings.builder().put("allowed_tries", 3).build(); - UserNameBasedRateLimiter rateLimiter = new UserNameBasedRateLimiter(settings, null); + AddressBasedRateLimiter rateLimiter = new AddressBasedRateLimiter(settings, null); - assertFalse(rateLimiter.isBlocked("a")); - rateLimiter.onAuthFailure(null, new AuthCredentials("a", PASSWORD), null); - assertFalse(rateLimiter.isBlocked("a")); - rateLimiter.onAuthFailure(null, new AuthCredentials("a", PASSWORD), null); - assertFalse(rateLimiter.isBlocked("a")); - rateLimiter.onAuthFailure(null, new AuthCredentials("a", PASSWORD), null); - assertTrue(rateLimiter.isBlocked("a")); + assertFalse(rateLimiter.isBlocked(InetAddress.getByAddress(new byte[] { 1, 2, 3, 4 }))); + rateLimiter.onAuthFailure(InetAddress.getByAddress(new byte[] { 1, 2, 3, 4 }), null, null); + assertFalse(rateLimiter.isBlocked(InetAddress.getByAddress(new byte[] { 1, 2, 3, 4 }))); + rateLimiter.onAuthFailure(InetAddress.getByAddress(new byte[] { 1, 2, 3, 4 }), null, null); + assertFalse(rateLimiter.isBlocked(InetAddress.getByAddress(new byte[] { 1, 2, 3, 4 }))); + rateLimiter.onAuthFailure(InetAddress.getByAddress(new byte[] { 1, 2, 3, 4 }), null, null); + assertTrue(rateLimiter.isBlocked(InetAddress.getByAddress(new byte[] { 1, 2, 3, 4 }))); } } diff --git a/src/test/java/org/opensearch/security/auth/limiting/UserNameBasedRateLimiterTest.java b/src/test/java/org/opensearch/security/auth/limiting/UserNameBasedRateLimiterTest.java index e42d2bd1b8..a8285c42a7 100644 --- a/src/test/java/org/opensearch/security/auth/limiting/UserNameBasedRateLimiterTest.java +++ b/src/test/java/org/opensearch/security/auth/limiting/UserNameBasedRateLimiterTest.java @@ -17,30 +17,31 @@ package org.opensearch.security.auth.limiting; -import java.net.InetAddress; - import org.junit.Test; import org.opensearch.common.settings.Settings; +import org.opensearch.security.user.AuthCredentials; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class UserNameBasedRateLimiterTest { + private final static byte[] PASSWORD = new byte[] { '1', '2', '3' }; + @Test public void simpleTest() throws Exception { Settings settings = Settings.builder().put("allowed_tries", 3).build(); - AddressBasedRateLimiter rateLimiter = new AddressBasedRateLimiter(settings, null); + UserNameBasedRateLimiter rateLimiter = new UserNameBasedRateLimiter(settings, null); - assertFalse(rateLimiter.isBlocked(InetAddress.getByAddress(new byte[] { 1, 2, 3, 4 }))); - rateLimiter.onAuthFailure(InetAddress.getByAddress(new byte[] { 1, 2, 3, 4 }), null, null); - assertFalse(rateLimiter.isBlocked(InetAddress.getByAddress(new byte[] { 1, 2, 3, 4 }))); - rateLimiter.onAuthFailure(InetAddress.getByAddress(new byte[] { 1, 2, 3, 4 }), null, null); - assertFalse(rateLimiter.isBlocked(InetAddress.getByAddress(new byte[] { 1, 2, 3, 4 }))); - rateLimiter.onAuthFailure(InetAddress.getByAddress(new byte[] { 1, 2, 3, 4 }), null, null); - assertTrue(rateLimiter.isBlocked(InetAddress.getByAddress(new byte[] { 1, 2, 3, 4 }))); + assertFalse(rateLimiter.isBlocked("a")); + rateLimiter.onAuthFailure(null, new AuthCredentials("a", PASSWORD), null); + assertFalse(rateLimiter.isBlocked("a")); + rateLimiter.onAuthFailure(null, new AuthCredentials("a", PASSWORD), null); + assertFalse(rateLimiter.isBlocked("a")); + rateLimiter.onAuthFailure(null, new AuthCredentials("a", PASSWORD), null); + assertTrue(rateLimiter.isBlocked("a")); } } diff --git a/src/test/java/org/opensearch/security/cache/DummyHTTPAuthenticator.java b/src/test/java/org/opensearch/security/cache/DummyHTTPAuthenticator.java index 55c2e789c6..37ac45080b 100644 --- a/src/test/java/org/opensearch/security/cache/DummyHTTPAuthenticator.java +++ b/src/test/java/org/opensearch/security/cache/DummyHTTPAuthenticator.java @@ -16,7 +16,7 @@ import org.opensearch.OpenSearchSecurityException; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.rest.RestChannel; +import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestRequest; import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.user.AuthCredentials; @@ -39,8 +39,8 @@ public AuthCredentials extractCredentials(RestRequest request, ThreadContext con } @Override - public boolean reRequestAuthentication(RestChannel channel, AuthCredentials credentials) { - return false; + public BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials credentials) { + return null; } public static long getCount() { From e4a1b77e9259c019a8ef4728495260468954d6af Mon Sep 17 00:00:00 2001 From: Andrey Pleskach Date: Wed, 4 Oct 2023 22:55:01 +0300 Subject: [PATCH 19/23] Support security config updates on the REST API using permission (#3264) Instead of setting `SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION` settings to update security configuration using `PATCH` or `PUT` a new permission was added: `restapi:admin/config/update`. So far I decided to keep this flag as it is due to a backward compatibility and log a deprecation message that these settings will be removed in the future. Maybe it is better to remove it completely. Besides, added the missed test for `SecurityConfigApiAction` Signed-off-by: Andrey Pleskach --- config/roles.yml | 1 + .../security/OpenSearchSecurityPlugin.java | 91 ++++++------ .../api/RestApiAdminPrivilegesEvaluator.java | 18 +-- .../rest/api/SecurityConfigApiAction.java | 49 ++++--- .../dlic/rest/api/SecurityRestApiActions.java | 2 +- ...on.java => SecuritySSLCertsApiAction.java} | 23 ++-- .../rest/api/SecurityConfigApiActionTest.java | 130 +++++++++++++----- ...SecurityConfigApiActionValidationTest.java | 50 +++++-- ...curitySSLCertsApiActionValidationTest.java | 84 +++++++++++ .../security/dlic/rest/api/UserApiTest.java | 8 +- .../SecurityRolesPermissionsTest.java | 28 +++- src/test/resources/restapi/internal_users.yml | 6 + src/test/resources/restapi/roles.yml | 5 + src/test/resources/restapi/roles_mapping.yml | 5 + 14 files changed, 360 insertions(+), 140 deletions(-) rename src/main/java/org/opensearch/security/dlic/rest/api/{SecuritySSLCertsAction.java => SecuritySSLCertsApiAction.java} (90%) create mode 100644 src/test/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiActionValidationTest.java diff --git a/config/roles.yml b/config/roles.yml index 570168fe10..77906290a0 100644 --- a/config/roles.yml +++ b/config/roles.yml @@ -15,6 +15,7 @@ security_rest_api_full_access: cluster_permissions: - 'restapi:admin/actiongroups' - 'restapi:admin/allowlist' + - 'restapi:admin/config/update' - 'restapi:admin/internalusers' - 'restapi:admin/nodesdn' - 'restapi:admin/roles' diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 8b1e307172..9f2f63e782 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -27,33 +27,6 @@ package org.opensearch.security; // CS-SUPPRESS-SINGLE: RegexpSingleline Extensions manager used to allow/disallow TLS connections to extensions -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.LinkOption; -import java.nio.file.Path; -import java.nio.file.attribute.PosixFilePermission; -import java.security.AccessController; -import java.security.MessageDigest; -import java.security.PrivilegedAction; -import java.security.Security; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; -import java.util.stream.Collectors; -import java.util.stream.Stream; import com.google.common.collect.Lists; import org.apache.logging.log4j.LogManager; @@ -61,13 +34,11 @@ 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; import org.opensearch.SpecialPermission; import org.opensearch.Version; import org.opensearch.action.ActionRequest; -import org.opensearch.core.action.ActionResponse; import org.opensearch.action.search.PitService; import org.opensearch.action.search.SearchScrollAction; import org.opensearch.action.support.ActionFilter; @@ -80,7 +51,6 @@ import org.opensearch.common.lifecycle.Lifecycle; import org.opensearch.common.lifecycle.LifecycleComponent; import org.opensearch.common.lifecycle.LifecycleListener; -import org.opensearch.core.common.io.stream.NamedWriteableRegistry; import org.opensearch.common.logging.DeprecationLogger; import org.opensearch.common.network.NetworkModule; import org.opensearch.common.network.NetworkService; @@ -93,14 +63,18 @@ import org.opensearch.common.util.BigArrays; import org.opensearch.common.util.PageCacheRecycler; import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.NamedWriteableRegistry; +import org.opensearch.core.index.Index; import org.opensearch.core.indices.breaker.CircuitBreakerService; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.core.transport.TransportResponse; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.env.Environment; import org.opensearch.env.NodeEnvironment; import org.opensearch.extensions.ExtensionsManager; import org.opensearch.http.HttpServerTransport; import org.opensearch.http.HttpServerTransport.Dispatcher; -import org.opensearch.core.index.Index; import org.opensearch.index.IndexModule; import org.opensearch.index.cache.query.QueryCache; import org.opensearch.indices.IndicesService; @@ -111,7 +85,6 @@ import org.opensearch.repositories.RepositoriesService; import org.opensearch.rest.RestController; import org.opensearch.rest.RestHandler; -import org.opensearch.core.rest.RestStatus; import org.opensearch.script.ScriptService; import org.opensearch.search.internal.InternalScrollSearchRequest; import org.opensearch.search.internal.ReaderContext; @@ -140,6 +113,7 @@ import org.opensearch.security.configuration.PrivilegesInterceptorImpl; import org.opensearch.security.configuration.Salt; import org.opensearch.security.configuration.SecurityFlsDlsIndexSearcherWrapper; +import org.opensearch.security.dlic.rest.api.Endpoint; import org.opensearch.security.dlic.rest.api.SecurityRestApiActions; import org.opensearch.security.dlic.rest.validation.PasswordValidator; import org.opensearch.security.filter.SecurityFilter; @@ -190,10 +164,42 @@ import org.opensearch.transport.TransportRequest; import org.opensearch.transport.TransportRequestHandler; import org.opensearch.transport.TransportRequestOptions; -import org.opensearch.core.transport.TransportResponse; import org.opensearch.transport.TransportResponseHandler; import org.opensearch.transport.TransportService; import org.opensearch.watcher.ResourceWatcherService; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.security.AccessController; +import java.security.MessageDigest; +import java.security.PrivilegedAction; +import java.security.Security; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS; +import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE; +import static org.opensearch.security.setting.DeprecatedSettings.checkForDeprecatedSetting; +import static org.opensearch.security.support.ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION; // CS-ENFORCE-SINGLE public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin @@ -332,14 +338,11 @@ public OpenSearchSecurityPlugin(final Settings settings, final Path configPath) sm.checkPermission(new SpecialPermission()); } - AccessController.doPrivileged(new PrivilegedAction() { - @Override - public Object run() { - if (Security.getProvider("BC") == null) { - Security.addProvider(new BouncyCastleProvider()); - } - return null; + AccessController.doPrivileged((PrivilegedAction) () -> { + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); } + return null; }); final String advancedModulesEnabledKey = ConfigConstants.SECURITY_ADVANCED_MODULES_ENABLED; @@ -347,6 +350,12 @@ public Object run() { deprecationLogger.deprecate("Setting {} is ignored.", advancedModulesEnabledKey); } + checkForDeprecatedSetting( + settings, + SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, + ENDPOINTS_WITH_PERMISSIONS.get(Endpoint.CONFIG).build(SECURITY_CONFIG_UPDATE) + " permission" + ); + log.info("Clustername: {}", settings.get("cluster.name", "opensearch")); if (!transportSSLEnabled && !SSLConfig.isSslOnlyMode()) { @@ -1788,7 +1797,7 @@ public List> getSettings() { ); settings.add( Setting.boolSetting( - ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, + SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, false, Property.NodeScope, Property.Filtered diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java index d05803be50..a63c496e38 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java @@ -35,9 +35,11 @@ public class RestApiAdminPrivilegesEvaluator { protected final Logger logger = LogManager.getLogger(RestApiAdminPrivilegesEvaluator.class); - public final static String CERTS_INFO_ACTION = "certs"; + public final static String CERTS_INFO_ACTION = "certs/info"; - public final static String RELOAD_CERTS_ACTION = "reloadcerts"; + public final static String RELOAD_CERTS_ACTION = "certs/reload"; + + public final static String SECURITY_CONFIG_UPDATE = "update"; private final static String REST_API_PERMISSION_PREFIX = "restapi:admin"; @@ -61,21 +63,13 @@ default String build() { public final static Map ENDPOINTS_WITH_PERMISSIONS = ImmutableMap.builder() .put(Endpoint.ACTIONGROUPS, action -> buildEndpointPermission(Endpoint.ACTIONGROUPS)) .put(Endpoint.ALLOWLIST, action -> buildEndpointPermission(Endpoint.ALLOWLIST)) + .put(Endpoint.CONFIG, action -> buildEndpointActionPermission(Endpoint.CONFIG, action)) .put(Endpoint.INTERNALUSERS, action -> buildEndpointPermission(Endpoint.INTERNALUSERS)) .put(Endpoint.NODESDN, action -> buildEndpointPermission(Endpoint.NODESDN)) .put(Endpoint.ROLES, action -> buildEndpointPermission(Endpoint.ROLES)) .put(Endpoint.ROLESMAPPING, action -> buildEndpointPermission(Endpoint.ROLESMAPPING)) .put(Endpoint.TENANTS, action -> buildEndpointPermission(Endpoint.TENANTS)) - .put(Endpoint.SSL, action -> { - switch (action) { - case CERTS_INFO_ACTION: - return buildEndpointActionPermission(Endpoint.SSL, "certs/info"); - case RELOAD_CERTS_ACTION: - return buildEndpointActionPermission(Endpoint.SSL, "certs/reload"); - default: - return null; - } - }) + .put(Endpoint.SSL, action -> buildEndpointActionPermission(Endpoint.SSL, action)) .build(); private final ThreadContext threadContext; diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiAction.java index 6c6ea0f827..f71135ce50 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiAction.java @@ -11,42 +11,41 @@ package org.opensearch.security.dlic.rest.api; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.Settings; -import org.opensearch.core.rest.RestStatus; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; import org.opensearch.security.dlic.rest.validation.EndpointValidator; import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.dlic.rest.validation.RequestContentValidator.DataType; -import org.opensearch.security.dlic.rest.validation.ValidationResult; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.support.ConfigConstants; import org.opensearch.threadpool.ThreadPool; -import java.util.Collections; import java.util.List; import java.util.Map; import static org.opensearch.security.dlic.rest.api.RequestHandler.methodNotImplementedHandler; -import static org.opensearch.security.dlic.rest.api.Responses.methodNotImplementedMessage; +import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; public class SecurityConfigApiAction extends AbstractApiAction { - private static final List getRoutes = addRoutesPrefix(Collections.singletonList(new Route(Method.GET, "/securityconfig/"))); - - private static final List allRoutes = new ImmutableList.Builder().addAll(getRoutes) - .addAll( - addRoutesPrefix(ImmutableList.of(new Route(Method.PUT, "/securityconfig/config"), new Route(Method.PATCH, "/securityconfig/"))) + private static final List routes = addRoutesPrefix( + List.of( + new Route(Method.GET, "/securityconfig"), + new Route(Method.PATCH, "/securityconfig"), + new Route(Method.PUT, "/securityconfig/config") ) - .build(); + ); private final boolean allowPutOrPatch; + private final boolean restApiAdminEnabled; + @Inject public SecurityConfigApiAction( final ClusterService clusterService, @@ -57,12 +56,13 @@ public SecurityConfigApiAction( super(Endpoint.CONFIG, clusterService, threadPool, securityApiDependencies); allowPutOrPatch = securityApiDependencies.settings() .getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, false); + this.restApiAdminEnabled = securityApiDependencies.settings().getAsBoolean(SECURITY_RESTAPI_ADMIN_ENABLED, false); this.requestHandlersBuilder.configureRequestHandlers(this::securityConfigApiActionRequestHandlers); } @Override public List routes() { - return allowPutOrPatch ? allRoutes : getRoutes; + return routes; } @Override @@ -74,20 +74,27 @@ protected CType getConfigType() { protected void consumeParameters(RestRequest request) {} private void securityConfigApiActionRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { - requestHandlersBuilder.onChangeRequest( - Method.PUT, - request -> withAllowedEndpoint(request).map(ignore -> processPutRequest("config", request)) - ) - .onChangeRequest(Method.PATCH, request -> withAllowedEndpoint(request).map(this::processPatchRequest)) + requestHandlersBuilder.withAccessHandler(this::accessHandler) + .verifyAccessForAllMethods() + .onChangeRequest(Method.PUT, request -> processPutRequest("config", request)) + .onChangeRequest(Method.PATCH, this::processPatchRequest) .override(Method.DELETE, methodNotImplementedHandler) .override(Method.POST, methodNotImplementedHandler); } - ValidationResult withAllowedEndpoint(final RestRequest request) { - if (!allowPutOrPatch) { - return ValidationResult.error(RestStatus.NOT_IMPLEMENTED, methodNotImplementedMessage(request.method())); + boolean accessHandler(final RestRequest request) { + switch (request.method()) { + case PATCH: + case PUT: + if (!restApiAdminEnabled) { + return allowPutOrPatch; + } else { + return securityApiDependencies.restApiAdminPrivilegesEvaluator() + .isCurrentUserAdminFor(endpoint, SECURITY_CONFIG_UPDATE); + } + default: + return true; } - return ValidationResult.success(request); } @Override diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java index 2eda752a82..78f9ce91df 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java @@ -95,7 +95,7 @@ public static Collection getHandler( new AllowlistApiAction(Endpoint.ALLOWLIST, clusterService, threadPool, securityApiDependencies), new AuditApiAction(clusterService, threadPool, securityApiDependencies), new MultiTenancyConfigApiAction(clusterService, threadPool, securityApiDependencies), - new SecuritySSLCertsAction(clusterService, threadPool, securityKeyStore, certificatesReloadEnabled, securityApiDependencies) + new SecuritySSLCertsApiAction(clusterService, threadPool, securityKeyStore, certificatesReloadEnabled, securityApiDependencies) ); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java similarity index 90% rename from src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java rename to src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java index 639d93c6ab..1dee3d8c84 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java @@ -37,6 +37,8 @@ import static org.opensearch.security.dlic.rest.api.Responses.badRequestMessage; import static org.opensearch.security.dlic.rest.api.Responses.ok; import static org.opensearch.security.dlic.rest.api.Responses.response; +import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION; +import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.RELOAD_CERTS_ACTION; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; /** @@ -45,7 +47,7 @@ * This action serves GET request for _plugins/_security/api/ssl/certs endpoint and * PUT _plugins/_security/api/ssl/{certType}/reloadcerts */ -public class SecuritySSLCertsAction extends AbstractApiAction { +public class SecuritySSLCertsApiAction extends AbstractApiAction { private static final List ROUTES = addRoutesPrefix( ImmutableList.of(new Route(Method.GET, "/ssl/certs"), new Route(Method.PUT, "/ssl/{certType}/reloadcerts/")) ); @@ -56,7 +58,7 @@ public class SecuritySSLCertsAction extends AbstractApiAction { private final boolean httpsEnabled; - public SecuritySSLCertsAction( + public SecuritySSLCertsApiAction( final ClusterService clusterService, final ThreadPool threadPool, final SecurityKeyStore securityKeyStore, @@ -116,18 +118,17 @@ private void securitySSLCertsRequestHandlers(RequestHandler.RequestHandlersBuild }).error((status, toXContent) -> response(channel, status, toXContent))); } - private boolean accessHandler(final RestRequest request) { - switch (request.method()) { - case GET: - return securityApiDependencies.restApiAdminPrivilegesEvaluator().isCurrentUserAdminFor(endpoint, "certs"); - case PUT: - return securityApiDependencies.restApiAdminPrivilegesEvaluator().isCurrentUserAdminFor(endpoint, "reloadcerts"); - default: - return false; + boolean accessHandler(final RestRequest request) { + if (request.method() == Method.GET) { + return securityApiDependencies.restApiAdminPrivilegesEvaluator().isCurrentUserAdminFor(endpoint, CERTS_INFO_ACTION); + } else if (request.method() == Method.PUT) { + return securityApiDependencies.restApiAdminPrivilegesEvaluator().isCurrentUserAdminFor(endpoint, RELOAD_CERTS_ACTION); + } else { + return false; } } - private ValidationResult withSecurityKeyStore() { + ValidationResult withSecurityKeyStore() { if (securityKeyStore == null) { return ValidationResult.error(RestStatus.OK, badRequestMessage("keystore is not initialized")); } diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionTest.java index e0bee59cec..7b98494e1b 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionTest.java @@ -36,31 +36,47 @@ public SecurityConfigApiActionTest() { } @Test - public void testSecurityConfigApiRead() throws Exception { + public void testSecurityConfigApiReadForSuperAdmin() throws Exception { setup(); rh.keystore = "restapi/kirk-keystore.jks"; rh.sendAdminCertificate = true; - HttpResponse response = rh.executeGetRequest(ENDPOINT + "/securityconfig", new Header[0]); + verifyResponsesWithoutPermissionOrUnsupportedFlag(); + } + + @Test + public void testSecurityConfigApiReadRestApiUser() throws Exception { + + setupWithRestRoles(); + + rh.keystore = "restapi/kirk-keystore.jks"; + rh.sendAdminCertificate = false; + + final var restApiHeader = encodeBasicHeader("test", "test"); + verifyResponsesWithoutPermissionOrUnsupportedFlag(restApiHeader); + } + + private void verifyResponsesWithoutPermissionOrUnsupportedFlag(final Header... headers) { + HttpResponse response = rh.executeGetRequest(ENDPOINT + "/securityconfig", headers); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executePutRequest(ENDPOINT + "/securityconfig", "{\"xxx\": 1}", new Header[0]); + response = rh.executePutRequest(ENDPOINT + "/securityconfig", "{\"xxx\": 1}", headers); Assert.assertEquals(HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode()); - response = rh.executePostRequest(ENDPOINT + "/securityconfig", "{\"xxx\": 1}", new Header[0]); + response = rh.executePostRequest(ENDPOINT + "/securityconfig", "{\"xxx\": 1}", headers); Assert.assertEquals(HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode()); - response = rh.executePatchRequest(ENDPOINT + "/securityconfig", "{\"xxx\": 1}", new Header[0]); - Assert.assertEquals(HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode()); + response = rh.executePatchRequest(ENDPOINT + "/securityconfig", "{\"xxx\": 1}", headers); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - response = rh.executeDeleteRequest(ENDPOINT + "/securityconfig", new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT + "/securityconfig", headers); Assert.assertEquals(HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode()); } @Test - public void testSecurityConfigApiWrite() throws Exception { + public void testSecurityConfigApiWriteWithUnsupportedFlagForSuperAdmin() throws Exception { Settings settings = Settings.builder() .put(ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true) @@ -70,52 +86,66 @@ public void testSecurityConfigApiWrite() throws Exception { rh.keystore = "restapi/kirk-keystore.jks"; rh.sendAdminCertificate = true; - HttpResponse response = rh.executeGetRequest(ENDPOINT + "/securityconfig", new Header[0]); + verifyWriteOperations(); + } + + @Test + public void testSecurityConfigApiWriteWithFullListOfPermissions() throws Exception { + + Settings settings = Settings.builder().put(ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED, true).build(); + setupWithRestRoles(settings); + + rh.keystore = "restapi/kirk-keystore.jks"; + rh.sendAdminCertificate = false; + + final var restAdminFullAccess = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); + verifyWriteOperations(restAdminFullAccess); + } + + @Test + public void testSecurityConfigApiWriteWithOnePermission() throws Exception { + Settings settings = Settings.builder().put(ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED, true).build(); + setupWithRestRoles(settings); + rh.keystore = "restapi/kirk-keystore.jks"; + rh.sendAdminCertificate = false; + final var updateOnlyRestApiHeader = encodeBasicHeader("rest_api_admin_config_update", "rest_api_admin_config_update"); + verifyWriteOperations(updateOnlyRestApiHeader); + } + + private void verifyWriteOperations(final Header... header) throws Exception { + HttpResponse response = rh.executeGetRequest(ENDPOINT + "/securityconfig", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executePutRequest( - ENDPOINT + "/securityconfig/xxx", - FileHelper.loadFile("restapi/securityconfig.json"), - new Header[0] - ); + response = rh.executePutRequest(ENDPOINT + "/securityconfig/xxx", FileHelper.loadFile("restapi/securityconfig.json"), header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - response = rh.executePutRequest( - ENDPOINT + "/securityconfig/config", - FileHelper.loadFile("restapi/securityconfig.json"), - new Header[0] - ); + response = rh.executePutRequest(ENDPOINT + "/securityconfig/config", FileHelper.loadFile("restapi/securityconfig.json"), header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executePutRequest( - ENDPOINT + "/securityconfig/config", - FileHelper.loadFile("restapi/invalid_config.json"), - new Header[0] - ); + response = rh.executePutRequest(ENDPOINT + "/securityconfig/config", FileHelper.loadFile("restapi/invalid_config.json"), header); Assert.assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, response.getStatusCode()); Assert.assertTrue(response.getContentType(), response.isJsonContentType()); Assert.assertTrue(response.getBody().contains("Unrecognized field")); - response = rh.executeGetRequest(ENDPOINT + "/securityconfig", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/securityconfig", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executePostRequest(ENDPOINT + "/securityconfig", "{\"xxx\": 1}", new Header[0]); + response = rh.executePostRequest(ENDPOINT + "/securityconfig", "{\"xxx\": 1}", header); Assert.assertEquals(HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode()); response = rh.executePatchRequest( ENDPOINT + "/securityconfig", "[{\"op\": \"replace\",\"path\": \"/config/dynamic/hosts_resolver_mode\",\"value\": \"other\"}]", - new Header[0] + header ); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executeDeleteRequest(ENDPOINT + "/securityconfig", new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT + "/securityconfig", header); Assert.assertEquals(HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode()); - } @Test - public void testSecurityConfigForHTTPPatch() throws Exception { + public void testSecurityConfigForPatchWithUnsupportedFlag() throws Exception { Settings settings = Settings.builder() .put(ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true) @@ -124,28 +154,58 @@ public void testSecurityConfigForHTTPPatch() throws Exception { rh.keystore = "restapi/kirk-keystore.jks"; rh.sendAdminCertificate = true; + verifyPatch(); + } + + @Test + public void testSecurityConfigForPatchWithFullPermissions() throws Exception { + + Settings settings = Settings.builder().put(ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED, true).build(); + setupWithRestRoles(settings); + + rh.keystore = "restapi/kirk-keystore.jks"; + rh.sendAdminCertificate = false; // non-default config + final var restAdminFullAccess = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); + verifyPatch(restAdminFullAccess); + } + + @Test + public void testSecurityConfigForPatchWithOnePermission() throws Exception { + + Settings settings = Settings.builder().put(ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED, true).build(); + setupWithRestRoles(settings); + + rh.keystore = "restapi/kirk-keystore.jks"; + rh.sendAdminCertificate = false; + + // non-default config + final var updateOnlyRestApiHeader = encodeBasicHeader("rest_api_admin_config_update", "rest_api_admin_config_update"); + verifyPatch(updateOnlyRestApiHeader); + } + + private void verifyPatch(final Header... header) throws Exception { String updatedConfig = FileHelper.loadFile("restapi/securityconfig_nondefault.json"); // update config - HttpResponse response = rh.executePutRequest(ENDPOINT + "/securityconfig/config", updatedConfig, new Header[0]); + HttpResponse response = rh.executePutRequest(ENDPOINT + "/securityconfig/config", updatedConfig, header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // make patch request response = rh.executePatchRequest( ENDPOINT + "/securityconfig", "[{\"op\": \"add\",\"path\": \"/config/dynamic/do_not_fail_on_forbidden\",\"value\": \"false\"}]", - new Header[0] + header ); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // get config - response = rh.executeGetRequest(ENDPOINT + "/securityconfig", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/securityconfig", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // verify configs are same Assert.assertEquals(DefaultObjectMapper.readTree(updatedConfig), DefaultObjectMapper.readTree(response.getBody()).get("config")); - } + } diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionValidationTest.java index e3cbfe4120..af80ad3a4d 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionValidationTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionValidationTest.java @@ -13,29 +13,32 @@ import org.junit.Test; import org.opensearch.common.settings.Settings; -import org.opensearch.core.rest.RestStatus; -import org.opensearch.security.support.ConfigConstants; +import org.opensearch.rest.RestRequest; import org.opensearch.security.util.FakeRestRequest; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; +import static org.opensearch.security.support.ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION; public class SecurityConfigApiActionValidationTest extends AbstractApiActionValidationTest { @Test - public void withAllowedEndpoint() { - var securityConfigApiAction = new SecurityConfigApiAction( + public void accessHandlerForDefaultSettings() { + final var securityConfigApiAction = new SecurityConfigApiAction( clusterService, threadPool, new SecurityApiDependencies(null, configurationRepository, null, null, restApiAdminPrivilegesEvaluator, null, Settings.EMPTY) ); + assertTrue(securityConfigApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.GET).build())); + assertFalse(securityConfigApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.PUT).build())); + assertFalse(securityConfigApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.PATCH).build())); + } - var result = securityConfigApiAction.withAllowedEndpoint(FakeRestRequest.builder().build()); - assertFalse(result.isValid()); - assertEquals(RestStatus.NOT_IMPLEMENTED, result.status()); - - securityConfigApiAction = new SecurityConfigApiAction( + @Test + public void accessHandlerForUnsupportedSetting() { + final var securityConfigApiAction = new SecurityConfigApiAction( clusterService, threadPool, new SecurityApiDependencies( @@ -45,11 +48,32 @@ public void withAllowedEndpoint() { null, restApiAdminPrivilegesEvaluator, null, - Settings.builder().put(ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true).build() + Settings.builder().put(SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true).build() ) ); - result = securityConfigApiAction.withAllowedEndpoint(FakeRestRequest.builder().build()); - assertTrue(result.isValid()); + assertTrue(securityConfigApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.GET).build())); + assertTrue(securityConfigApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.PUT).build())); + assertTrue(securityConfigApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.PATCH).build())); } + @Test + public void accessHandlerForRestAdmin() { + when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.CONFIG, RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE)).thenReturn(true); + final var securityConfigApiAction = new SecurityConfigApiAction( + clusterService, + threadPool, + new SecurityApiDependencies( + null, + configurationRepository, + null, + null, + restApiAdminPrivilegesEvaluator, + null, + Settings.builder().put(SECURITY_RESTAPI_ADMIN_ENABLED, true).build() + ) + ); + assertTrue(securityConfigApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.GET).build())); + assertTrue(securityConfigApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.PUT).build())); + assertTrue(securityConfigApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.PATCH).build())); + } } diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiActionValidationTest.java new file mode 100644 index 0000000000..59fa37274b --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiActionValidationTest.java @@ -0,0 +1,84 @@ +/* + * 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.dlic.rest.api; + +import org.junit.Test; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.rest.RestRequest; +import org.opensearch.security.util.FakeRestRequest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; +import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION; +import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.RELOAD_CERTS_ACTION; + +public class SecuritySSLCertsApiActionValidationTest extends AbstractApiActionValidationTest { + + @Test + public void withSecurityKeyStore() { + final var securitySSLCertsApiAction = new SecuritySSLCertsApiAction( + clusterService, + threadPool, + null, + true, + securityApiDependencies + ); + final var result = securitySSLCertsApiAction.withSecurityKeyStore(); + assertFalse(result.isValid()); + assertEquals(RestStatus.OK, result.status()); + } + + @Test + public void accessDenied() { + final var securitySSLCertsApiAction = new SecuritySSLCertsApiAction( + clusterService, + threadPool, + null, + true, + securityApiDependencies + ); + when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.SSL, CERTS_INFO_ACTION)).thenReturn(false); + when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.SSL, RELOAD_CERTS_ACTION)).thenReturn(false); + assertFalse(securitySSLCertsApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.GET).build())); + assertFalse(securitySSLCertsApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.PUT).build())); + + for (final var m : RequestHandler.RequestHandlersBuilder.SUPPORTED_METHODS) { + if (m != RestRequest.Method.GET && m != RestRequest.Method.PUT) { + assertFalse(securitySSLCertsApiAction.accessHandler(FakeRestRequest.builder().withMethod(m).build())); + } + } + } + + @Test + public void hasAccess() { + final var securitySSLCertsApiAction = new SecuritySSLCertsApiAction( + clusterService, + threadPool, + null, + true, + securityApiDependencies + ); + when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.SSL, CERTS_INFO_ACTION)).thenReturn(true); + when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.SSL, RELOAD_CERTS_ACTION)).thenReturn(true); + assertTrue(securitySSLCertsApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.GET).build())); + assertTrue(securitySSLCertsApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.PUT).build())); + + for (final var m : RequestHandler.RequestHandlersBuilder.SUPPORTED_METHODS) { + if (m != RestRequest.Method.GET && m != RestRequest.Method.PUT) { + assertFalse(securitySSLCertsApiAction.accessHandler(FakeRestRequest.builder().withMethod(m).build())); + } + } + } + +} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/UserApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/UserApiTest.java index 3b959b4d26..35cfae3b3b 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/UserApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/UserApiTest.java @@ -54,7 +54,7 @@ protected String getEndpointPrefix() { return PLUGINS_PREFIX; } - final int USER_SETTING_SIZE = 7 * 19; // Lines per account entry * number of accounts + final int USER_SETTING_SIZE = 7 * 20; // Lines per account entry * number of accounts private static final String ENABLED_SERVICE_ACCOUNT_BODY = "{" + " \"attributes\": { \"service\": \"true\", " @@ -98,7 +98,7 @@ public void testSecurityRoles() throws Exception { HttpResponse response = rh.executeGetRequest(ENDPOINT + "/" + CType.INTERNALUSERS.toLCString()); Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(133, settings.size()); + Assert.assertEquals(USER_SETTING_SIZE, settings.size()); response = rh.executePatchRequest( ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/newuser\", " @@ -153,7 +153,7 @@ public void testUserFilters() throws Exception { rh.keystore = "restapi/kirk-keystore.jks"; rh.sendAdminCertificate = true; final int SERVICE_ACCOUNTS_IN_SETTINGS = 1; - final int INTERNAL_ACCOUNTS_IN_SETTINGS = 19; + final int INTERNAL_ACCOUNTS_IN_SETTINGS = 20; final String serviceAccountName = "JohnDoeService"; HttpResponse response; @@ -845,7 +845,7 @@ public void testScoreBasedPasswordRules() throws Exception { HttpResponse response = rh.executeGetRequest("_plugins/_security/api/" + CType.INTERNALUSERS.toLCString()); Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(133, settings.size()); + Assert.assertEquals(USER_SETTING_SIZE, settings.size()); addUserWithPassword( "admin", diff --git a/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java b/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java index 49a9be8a91..9d104381a6 100644 --- a/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java +++ b/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java @@ -53,6 +53,7 @@ import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.RELOAD_CERTS_ACTION; +import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE; public class SecurityRolesPermissionsTest { @@ -78,6 +79,8 @@ static String restAdminApiRoleName(final String endpoint) { new SimpleEntry<>(restAdminApiRoleName(CERTS_INFO_ACTION), role(pb.build(CERTS_INFO_ACTION))), new SimpleEntry<>(restAdminApiRoleName(RELOAD_CERTS_ACTION), role(pb.build(RELOAD_CERTS_ACTION))) ); + } else if (e.getKey() == Endpoint.CONFIG) { + return Stream.of(new SimpleEntry<>(restAdminApiRoleName(SECURITY_CONFIG_UPDATE), role(pb.build(SECURITY_CONFIG_UPDATE)))); } else { return Stream.of(new SimpleEntry<>(restAdminApiRoleName(endpoint), role(pb.build()))); } @@ -95,6 +98,8 @@ static String[] allRestApiPermissions() { return ENDPOINTS_WITH_PERMISSIONS.entrySet().stream().flatMap(entry -> { if (entry.getKey() == Endpoint.SSL) { return Stream.of(entry.getValue().build(CERTS_INFO_ACTION), entry.getValue().build(RELOAD_CERTS_ACTION)); + } else if (entry.getKey() == Endpoint.CONFIG) { + return Stream.of(entry.getValue().build(SECURITY_CONFIG_UPDATE)); } else { return Stream.of(entry.getValue().build()); } @@ -130,6 +135,11 @@ public void hasNoExplicitClusterPermissionPermissionForRestAdmin() { endpoint.name(), securityRolesForRole.hasExplicitClusterPermissionPermission(permissionBuilder.build(RELOAD_CERTS_ACTION)) ); + } else if (endpoint == Endpoint.CONFIG) { + Assert.assertFalse( + endpoint.name(), + securityRolesForRole.hasExplicitClusterPermissionPermission(permissionBuilder.build(SECURITY_CONFIG_UPDATE)) + ); } else { Assert.assertFalse( endpoint.name(), @@ -156,6 +166,11 @@ public void hasExplicitClusterPermissionPermissionForRestAdminWitFullAccess() { endpoint.name() + "/" + CERTS_INFO_ACTION, securityRolesForRole.hasExplicitClusterPermissionPermission(permissionBuilder.build(RELOAD_CERTS_ACTION)) ); + } else if (endpoint == Endpoint.CONFIG) { + Assert.assertTrue( + endpoint.name() + "/" + SECURITY_CONFIG_UPDATE, + securityRolesForRole.hasExplicitClusterPermissionPermission(permissionBuilder.build(SECURITY_CONFIG_UPDATE)) + ); } else { Assert.assertTrue( endpoint.name(), @@ -168,10 +183,10 @@ public void hasExplicitClusterPermissionPermissionForRestAdminWitFullAccess() { @Test public void hasExplicitClusterPermissionPermissionForRestAdmin() { - // verify all endpoint except SSL + // verify all endpoint except SSL and verify CONFIG endpoints final Collection noSslEndpoints = ENDPOINTS_WITH_PERMISSIONS.keySet() .stream() - .filter(e -> e != Endpoint.SSL) + .filter(e -> e != Endpoint.SSL && e != Endpoint.CONFIG) .collect(Collectors.toList()); for (final Endpoint endpoint : noSslEndpoints) { final String permission = ENDPOINTS_WITH_PERMISSIONS.get(endpoint).build(); @@ -190,6 +205,15 @@ public void hasExplicitClusterPermissionPermissionForRestAdmin() { ); assertHasNoPermissionsForRestApiAdminOnePermissionRole(Endpoint.SSL, sslAllowRole); } + // verify CONFIG endpoint with 1 action + final SecurityRoles securityConfigAllowRole = configModel.getSecurityRoles() + .filter(ImmutableSet.of(restAdminApiRoleName(SECURITY_CONFIG_UPDATE))); + final PermissionBuilder permissionBuilder = ENDPOINTS_WITH_PERMISSIONS.get(Endpoint.CONFIG); + Assert.assertTrue( + Endpoint.SSL + "/" + SECURITY_CONFIG_UPDATE, + securityConfigAllowRole.hasExplicitClusterPermissionPermission(permissionBuilder.build(SECURITY_CONFIG_UPDATE)) + ); + assertHasNoPermissionsForRestApiAdminOnePermissionRole(Endpoint.CONFIG, securityConfigAllowRole); } void assertHasNoPermissionsForRestApiAdminOnePermissionRole(final Endpoint allowEndpoint, final SecurityRoles allowOnlyRoleForRole) { diff --git a/src/test/resources/restapi/internal_users.yml b/src/test/resources/restapi/internal_users.yml index 658d3f3aa1..d5d26ef4b5 100644 --- a/src/test/resources/restapi/internal_users.yml +++ b/src/test/resources/restapi/internal_users.yml @@ -127,3 +127,9 @@ rest_api_admin_tenants: hidden: false backend_roles: [] description: "REST API Tenats admin user" +rest_api_admin_config_update: + hash: "$2y$12$capXg1HNP49Vxeb6ijzRnu5BLMUE0ZePq1l3MhF8tjnuxg614uaY6" + reserved: false + hidden: false + backend_roles: [] + description: "REST API Config update admin user" diff --git a/src/test/resources/restapi/roles.yml b/src/test/resources/restapi/roles.yml index f639a21a4e..1c3756cb4d 100644 --- a/src/test/resources/restapi/roles.yml +++ b/src/test/resources/restapi/roles.yml @@ -398,6 +398,7 @@ rest_api_admin_full_access: cluster_permissions: - 'restapi:admin/actiongroups' - 'restapi:admin/allowlist' + - 'restapi:admin/config/update' - 'restapi:admin/internalusers' - 'restapi:admin/nodesdn' - 'restapi:admin/roles' @@ -441,3 +442,7 @@ rest_api_admin_tenants_only: reserved: true cluster_permissions: - 'restapi:admin/tenants' +rest_api_admin_config_update_only: + reserved: true + cluster_permissions: + - 'restapi:admin/config/update' diff --git a/src/test/resources/restapi/roles_mapping.yml b/src/test/resources/restapi/roles_mapping.yml index a87287d5ff..8bfe826247 100644 --- a/src/test/resources/restapi/roles_mapping.yml +++ b/src/test/resources/restapi/roles_mapping.yml @@ -195,6 +195,7 @@ opendistro_security_test: - "rest_api_admin_tenants" - "rest_api_admin_ssl_info" - "rest_api_admin_ssl_reloadcerts" + - "rest_api_admin_config_update" and_backend_roles: [] description: "Migrated from v6" opendistro_security_role_starfleet_captains: @@ -260,3 +261,7 @@ rest_api_admin_tenants_only: reserved: false hidden: true users: [rest_api_admin_tenants] +rest_api_admin_config_update_only: + reserved: false + hidden: true + users: [rest_api_admin_config_update] From 93f79f881a718f35596dcd44e705b29b8c5c25c3 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Wed, 4 Oct 2023 15:48:42 -0500 Subject: [PATCH 20/23] [Build Break] Update custom serialization min supported version (#3458) Update custom serialization min supported version, should be 2.11.0 since the features backport [1] was merged. - [1] https://github.com/opensearch-project/security/pull/3444 Signed-off-by: Peter Nied --- .../org/opensearch/security/support/ConfigConstants.java | 2 +- .../security/transport/SecuritySSLRequestHandlerTests.java | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index 9ac73cd579..1f5728edfb 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -326,7 +326,7 @@ public enum RolesMappingResolution { public static final String TENANCY_GLOBAL_TENANT_DEFAULT_NAME = ""; public static final String USE_JDK_SERIALIZATION = "plugins.security.use_jdk_serialization"; - public static final Version FIRST_CUSTOM_SERIALIZATION_SUPPORTED_OS_VERSION = Version.V_3_0_0; + public static final Version FIRST_CUSTOM_SERIALIZATION_SUPPORTED_OS_VERSION = Version.V_2_11_0; // On-behalf-of endpoints settings // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings diff --git a/src/test/java/org/opensearch/security/transport/SecuritySSLRequestHandlerTests.java b/src/test/java/org/opensearch/security/transport/SecuritySSLRequestHandlerTests.java index 23a64e4be3..2c160dfe31 100644 --- a/src/test/java/org/opensearch/security/transport/SecuritySSLRequestHandlerTests.java +++ b/src/test/java/org/opensearch/security/transport/SecuritySSLRequestHandlerTests.java @@ -66,12 +66,17 @@ public void testUseJDKSerializationHeaderIsSetOnMessageReceived() throws Excepti TransportChannel transportChannel = mock(TransportChannel.class); Task task = mock(Task.class); doNothing().when(transportChannel).sendResponse(ArgumentMatchers.any(Exception.class)); - when(transportChannel.getVersion()).thenReturn(Version.V_2_11_0); + when(transportChannel.getVersion()).thenReturn(Version.V_2_10_0); when(transportChannel.getChannelType()).thenReturn("transport"); Assert.assertThrows(Exception.class, () -> securitySSLRequestHandler.messageReceived(transportRequest, transportChannel, task)); Assert.assertTrue(threadPool.getThreadContext().getTransient(ConfigConstants.USE_JDK_SERIALIZATION)); + threadPool.getThreadContext().stashContext(); + when(transportChannel.getVersion()).thenReturn(Version.V_2_11_0); + Assert.assertThrows(Exception.class, () -> securitySSLRequestHandler.messageReceived(transportRequest, transportChannel, task)); + Assert.assertFalse(threadPool.getThreadContext().getTransient(ConfigConstants.USE_JDK_SERIALIZATION)); + threadPool.getThreadContext().stashContext(); when(transportChannel.getVersion()).thenReturn(Version.V_3_0_0); Assert.assertThrows(Exception.class, () -> securitySSLRequestHandler.messageReceived(transportRequest, transportChannel, task)); From 87520741566eaa4ea609441a1f53f3cef4a9674c Mon Sep 17 00:00:00 2001 From: David Osorno <48450162+davidosorno@users.noreply.github.com> Date: Wed, 4 Oct 2023 17:14:44 -0700 Subject: [PATCH 21/23] Redirect deprecation messages to stderr. (#3454) * Redirect deprecation messages to stderr to differentiate between the two streams to make debugging and troubleshooting more efficient. Signed-off-by: David Osorno --- tools/hash.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/hash.sh b/tools/hash.sh index e4f92b4cdf..2bd5bb1e0e 100755 --- a/tools/hash.sh +++ b/tools/hash.sh @@ -1,9 +1,9 @@ #!/bin/bash -echo "**************************************************************************" -echo "** This tool will be deprecated in the next major release of OpenSearch **" -echo "** https://github.com/opensearch-project/security/issues/1755 **" -echo "**************************************************************************" +echo "**************************************************************************" >&2 +echo "** This tool will be deprecated in the next major release of OpenSearch **" >&2 +echo "** https://github.com/opensearch-project/security/issues/1755 **" >&2 +echo "**************************************************************************" >&2 SCRIPT_PATH="${BASH_SOURCE[0]}" if ! [ -x "$(command -v realpath)" ]; then From bfba97ae932b1397b3389d7b14ccdec3860dbd2e Mon Sep 17 00:00:00 2001 From: Gagan Juneja Date: Thu, 5 Oct 2023 18:42:30 +0530 Subject: [PATCH 22/23] Add tracer to Transport (#3463) ### Description Add tracer to fix the breaking issue. ### Issues Resolved - #3462 ### Check List - [ ] New functionality includes testing - [ ] New functionality has been documented - [x] Commits are signed per the DCO using --signoff By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. For more information on following Developer Certificate of Origin and signing off your commits, please check [here](https://github.com/opensearch-project/OpenSearch/blob/main/CONTRIBUTING.md#developer-certificate-of-origin). --------- Signed-off-by: Gagan Juneja Co-authored-by: Gagan Juneja --- .../opensearch/security/OpenSearchSecurityPlugin.java | 9 ++++++--- .../security/ssl/OpenSearchSecuritySSLPlugin.java | 6 ++++-- .../ssl/transport/SecuritySSLNettyTransport.java | 7 +++++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 9f2f63e782..fd9590e04d 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -824,7 +824,8 @@ public Map> getTransports( PageCacheRecycler pageCacheRecycler, CircuitBreakerService circuitBreakerService, NamedWriteableRegistry namedWriteableRegistry, - NetworkService networkService + NetworkService networkService, + Tracer tracer ) { Map> transports = new HashMap>(); @@ -835,7 +836,8 @@ public Map> getTransports( pageCacheRecycler, circuitBreakerService, namedWriteableRegistry, - networkService + networkService, + tracer ); } @@ -853,7 +855,8 @@ public Map> getTransports( sks, evaluateSslExceptionHandler(), sharedGroupFactory, - SSLConfig + SSLConfig, + tracer ) ); } diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java index bff2cf02d5..18ec7457e9 100644 --- a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java @@ -315,7 +315,8 @@ public Map> getTransports( PageCacheRecycler pageCacheRecycler, CircuitBreakerService circuitBreakerService, NamedWriteableRegistry namedWriteableRegistry, - NetworkService networkService + NetworkService networkService, + Tracer tracer ) { Map> transports = new HashMap>(); @@ -333,7 +334,8 @@ public Map> getTransports( sks, NOOP_SSL_EXCEPTION_HANDLER, sharedGroupFactory, - SSLConfig + SSLConfig, + tracer ) ); diff --git a/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLNettyTransport.java b/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLNettyTransport.java index ad4ebec1c5..7aeebdaf9f 100644 --- a/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLNettyTransport.java +++ b/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLNettyTransport.java @@ -59,6 +59,7 @@ import org.opensearch.security.ssl.util.SSLConfigConstants; import org.opensearch.security.ssl.util.SSLConnectionTestResult; import org.opensearch.security.ssl.util.SSLConnectionTestUtil; +import org.opensearch.telemetry.tracing.Tracer; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.SharedGroupFactory; import org.opensearch.transport.TcpChannel; @@ -82,7 +83,8 @@ public SecuritySSLNettyTransport( final SecurityKeyStore ossks, final SslExceptionHandler errorHandler, SharedGroupFactory sharedGroupFactory, - final SSLConfig SSLConfig + final SSLConfig SSLConfig, + final Tracer tracer ) { super( settings, @@ -92,7 +94,8 @@ public SecuritySSLNettyTransport( pageCacheRecycler, namedWriteableRegistry, circuitBreakerService, - sharedGroupFactory + sharedGroupFactory, + tracer ); this.ossks = ossks; From f435c052a826b15683b8a6a6f2ccd9dd107070f9 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Thu, 5 Oct 2023 19:58:24 -0500 Subject: [PATCH 23/23] Expanding Authentication with SecurityRequest Abstraction (#3430) Introduced a new abstraction, SecurityRequest & SecurityRequestChannel, to streamline and secure the authentication process in the OpenSearch Security plugin. By isolating the essential request components needed for authentication, we minimize potential risks associated with previous designs and provide a more maintainable architecture. Signed-off-by: Peter Nied --- .../jwt/AbstractHTTPJwtAuthenticator.java | 33 ++-- .../auth/http/jwt/HTTPJwtAuthenticator.java | 37 +++-- .../kerberos/HTTPSpnegoAuthenticator.java | 47 +++--- .../http/saml/AuthTokenProcessorHandler.java | 20 +-- .../auth/http/saml/HTTPSamlAuthenticator.java | 48 +++--- .../security/auditlog/AuditLog.java | 14 +- .../auditlog/AuditLogSslExceptionHandler.java | 4 +- .../security/auditlog/NullAuditLog.java | 14 +- .../auditlog/impl/AbstractAuditLog.java | 16 +- .../security/auditlog/impl/AuditLogImpl.java | 14 +- .../security/auditlog/impl/AuditMessage.java | 25 +++- .../security/auth/BackendRegistry.java | 92 ++++++------ .../security/auth/HTTPAuthenticator.java | 14 +- .../security/auth/UserInjector.java | 4 +- .../dlic/rest/api/AbstractApiAction.java | 9 +- .../dlic/rest/api/NodesDnApiAction.java | 1 + .../rest/api/RestApiPrivilegesEvaluator.java | 5 +- .../rest/validation/EndpointValidator.java | 4 + .../security/filter/OpenSearchRequest.java | 91 +++++++++++ .../filter/OpenSearchRequestChannel.java | 97 ++++++++++++ .../security/filter/SecurityFilter.java | 1 + .../security/filter/SecurityRequest.java | 53 +++++++ .../filter/SecurityRequestChannel.java | 29 ++++ .../SecurityRequestChannelUnsupported.java | 17 +++ .../filter/SecurityRequestFactory.java | 31 ++++ .../security/filter/SecurityResponse.java | 44 ++++++ .../security/filter/SecurityRestFilter.java | 99 +++++++----- .../security/http/HTTPBasicAuthenticator.java | 20 +-- .../http/HTTPClientCertAuthenticator.java | 11 +- .../security/http/HTTPProxyAuthenticator.java | 11 +- .../http/OnBehalfOfAuthenticator.java | 18 ++- .../security/http/RemoteIpDetector.java | 19 ++- .../opensearch/security/http/XFFResolver.java | 35 +++-- .../proxy/HTTPExtendedProxyAuthenticator.java | 4 +- .../rest/SecurityConfigUpdateAction.java | 8 +- .../security/rest/SecurityWhoAmIAction.java | 9 +- .../impl/AllowlistingSettings.java | 44 +++--- .../impl/WhitelistingSettings.java | 32 ++-- .../security/ssl/SslExceptionHandler.java | 4 +- .../ssl/http/netty/ValidatingDispatcher.java | 8 +- .../ssl/rest/SecuritySSLInfoAction.java | 9 +- .../security/ssl/util/SSLRequestHelper.java | 21 +-- .../security/support/HTTPHelper.java | 5 +- .../security/util/AuthTokenUtils.java | 4 +- .../http/jwt/HTTPJwtAuthenticatorTest.java | 39 +++-- ...wtKeyByOpenIdConnectAuthenticatorTest.java | 41 ++--- ...wtKeyByOpenIdConnectAuthenticatorTest.java | 27 ++-- .../http/saml/HTTPSamlAuthenticatorTest.java | 141 +++++------------- .../auditlog/helper/MockRestRequest.java | 6 + .../security/auditlog/impl/AuditlogTest.java | 4 +- .../auditlog/impl/DisabledCategoriesTest.java | 13 +- .../authtoken/jwt/AuthTokenUtilsTest.java | 7 +- .../cache/DummyHTTPAuthenticator.java | 12 +- .../http/OnBehalfOfAuthenticatorTest.java | 42 ++++-- .../HTTPExtendedProxyAuthenticatorTest.java | 15 +- .../security/util/FakeRestRequest.java | 5 + 56 files changed, 978 insertions(+), 499 deletions(-) create mode 100644 src/main/java/org/opensearch/security/filter/OpenSearchRequest.java create mode 100644 src/main/java/org/opensearch/security/filter/OpenSearchRequestChannel.java create mode 100644 src/main/java/org/opensearch/security/filter/SecurityRequest.java create mode 100644 src/main/java/org/opensearch/security/filter/SecurityRequestChannel.java create mode 100644 src/main/java/org/opensearch/security/filter/SecurityRequestChannelUnsupported.java create mode 100644 src/main/java/org/opensearch/security/filter/SecurityRequestFactory.java create mode 100644 src/main/java/org/opensearch/security/filter/SecurityResponse.java diff --git a/src/main/java/com/amazon/dlic/auth/http/jwt/AbstractHTTPJwtAuthenticator.java b/src/main/java/com/amazon/dlic/auth/http/jwt/AbstractHTTPJwtAuthenticator.java index 6dbb7b7676..b183593a91 100644 --- a/src/main/java/com/amazon/dlic/auth/http/jwt/AbstractHTTPJwtAuthenticator.java +++ b/src/main/java/com/amazon/dlic/auth/http/jwt/AbstractHTTPJwtAuthenticator.java @@ -11,17 +11,21 @@ package com.amazon.dlic.auth.http.jwt; +import static org.apache.http.HttpHeaders.AUTHORIZATION; + import java.nio.file.Path; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Collection; +import java.util.Map; +import java.util.Optional; import java.util.Map.Entry; import java.util.regex.Pattern; import com.google.common.annotations.VisibleForTesting; import org.apache.cxf.rs.security.jose.jwt.JwtClaims; import org.apache.cxf.rs.security.jose.jwt.JwtToken; -import org.apache.hc.core5.http.HttpHeaders; +import org.apache.http.HttpStatus; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -35,10 +39,10 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.common.Strings; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestRequest; import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auth.HTTPAuthenticator; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.filter.SecurityResponse; import org.opensearch.security.user.AuthCredentials; public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator { @@ -62,8 +66,8 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator public AbstractHTTPJwtAuthenticator(Settings settings, Path configPath) { jwtUrlParameter = settings.get("jwt_url_parameter"); - jwtHeaderName = settings.get("jwt_header", HttpHeaders.AUTHORIZATION); - isDefaultAuthHeader = HttpHeaders.AUTHORIZATION.equalsIgnoreCase(jwtHeaderName); + jwtHeaderName = settings.get("jwt_header", AUTHORIZATION); + isDefaultAuthHeader = AUTHORIZATION.equalsIgnoreCase(jwtHeaderName); rolesKey = settings.get("roles_key"); subjectKey = settings.get("subject_key"); clockSkewToleranceSeconds = settings.getAsInt("jwt_clock_skew_tolerance_seconds", DEFAULT_CLOCK_SKEW_TOLERANCE_SECONDS); @@ -82,7 +86,8 @@ public AbstractHTTPJwtAuthenticator(Settings settings, Path configPath) { @Override @SuppressWarnings("removal") - public AuthCredentials extractCredentials(RestRequest request, ThreadContext context) throws OpenSearchSecurityException { + public AuthCredentials extractCredentials(final SecurityRequest request, final ThreadContext context) + throws OpenSearchSecurityException { final SecurityManager sm = System.getSecurityManager(); if (sm != null) { @@ -99,7 +104,7 @@ public AuthCredentials run() { return creds; } - private AuthCredentials extractCredentials0(final RestRequest request) throws OpenSearchSecurityException { + private AuthCredentials extractCredentials0(final SecurityRequest request) throws OpenSearchSecurityException { String jwtString = getJwtTokenString(request); @@ -140,7 +145,7 @@ private AuthCredentials extractCredentials0(final RestRequest request) throws Op } - protected String getJwtTokenString(RestRequest request) { + protected String getJwtTokenString(SecurityRequest request) { String jwtToken = request.header(jwtHeaderName); if (isDefaultAuthHeader && jwtToken != null && BASIC.matcher(jwtToken).matches()) { jwtToken = null; @@ -148,10 +153,10 @@ protected String getJwtTokenString(RestRequest request) { if (jwtUrlParameter != null) { if (jwtToken == null || jwtToken.isEmpty()) { - jwtToken = request.param(jwtUrlParameter); + jwtToken = request.params().get(jwtUrlParameter); } else { // just consume to avoid "contains unrecognized parameter" - request.param(jwtUrlParameter); + request.params().get(jwtUrlParameter); } } @@ -235,10 +240,10 @@ public String[] extractRoles(JwtClaims claims) { protected abstract KeyProvider initKeyProvider(Settings settings, Path configPath) throws Exception; @Override - public BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials authCredentials) { - final BytesRestResponse wwwAuthenticateResponse = new BytesRestResponse(RestStatus.UNAUTHORIZED, ""); - wwwAuthenticateResponse.addHeader("WWW-Authenticate", "Bearer realm=\"OpenSearch Security\""); - return wwwAuthenticateResponse; + public Optional reRequestAuthentication(final SecurityRequest request, AuthCredentials authCredentials) { + return Optional.of( + new SecurityResponse(HttpStatus.SC_UNAUTHORIZED, Map.of("WWW-Authenticate", "Bearer realm=\"OpenSearch Security\""), "") + ); } public String getRequiredAudience() { 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 338a490037..df1c605167 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 @@ -11,10 +11,14 @@ package com.amazon.dlic.auth.http.jwt; +import static org.apache.http.HttpHeaders.AUTHORIZATION; + import java.nio.file.Path; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Collection; +import java.util.Map; +import java.util.Optional; import java.util.Map.Entry; import java.util.regex.Pattern; @@ -22,7 +26,8 @@ import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.JwtParserBuilder; import io.jsonwebtoken.security.WeakKeyException; -import org.apache.hc.core5.http.HttpHeaders; + +import org.apache.http.HttpStatus; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -30,10 +35,9 @@ import org.opensearch.SpecialPermission; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestRequest; -import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auth.HTTPAuthenticator; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.filter.SecurityResponse; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.util.KeyUtils; @@ -58,8 +62,8 @@ public HTTPJwtAuthenticator(final Settings settings, final Path configPath) { String signingKey = settings.get("signing_key"); jwtUrlParameter = settings.get("jwt_url_parameter"); - jwtHeaderName = settings.get("jwt_header", HttpHeaders.AUTHORIZATION); - isDefaultAuthHeader = HttpHeaders.AUTHORIZATION.equalsIgnoreCase(jwtHeaderName); + jwtHeaderName = settings.get("jwt_header", AUTHORIZATION); + isDefaultAuthHeader = AUTHORIZATION.equalsIgnoreCase(jwtHeaderName); rolesKey = settings.get("roles_key"); subjectKey = settings.get("subject_key"); requireAudience = settings.get("required_audience"); @@ -83,7 +87,8 @@ public HTTPJwtAuthenticator(final Settings settings, final Path configPath) { @Override @SuppressWarnings("removal") - public AuthCredentials extractCredentials(RestRequest request, ThreadContext context) throws OpenSearchSecurityException { + public AuthCredentials extractCredentials(final SecurityRequest request, final ThreadContext context) + throws OpenSearchSecurityException { final SecurityManager sm = System.getSecurityManager(); if (sm != null) { @@ -100,7 +105,7 @@ public AuthCredentials run() { return creds; } - private AuthCredentials extractCredentials0(final RestRequest request) { + private AuthCredentials extractCredentials0(final SecurityRequest request) { if (jwtParser == null) { log.error("Missing Signing Key. JWT authentication will not work"); return null; @@ -112,10 +117,10 @@ private AuthCredentials extractCredentials0(final RestRequest request) { } if ((jwtToken == null || jwtToken.isEmpty()) && jwtUrlParameter != null) { - jwtToken = request.param(jwtUrlParameter); + jwtToken = request.params().get(jwtUrlParameter); } else { // just consume to avoid "contains unrecognized parameter" - request.param(jwtUrlParameter); + request.params().get(jwtUrlParameter); } if (jwtToken == null || jwtToken.length() == 0) { @@ -170,10 +175,10 @@ private AuthCredentials extractCredentials0(final RestRequest request) { } @Override - public BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials credentials) { - final BytesRestResponse wwwAuthenticateResponse = new BytesRestResponse(RestStatus.UNAUTHORIZED, ""); - wwwAuthenticateResponse.addHeader("WWW-Authenticate", "Bearer realm=\"OpenSearch Security\""); - return wwwAuthenticateResponse; + public Optional reRequestAuthentication(final SecurityRequest channel, AuthCredentials creds) { + return Optional.of( + new SecurityResponse(HttpStatus.SC_UNAUTHORIZED, Map.of("WWW-Authenticate", "Bearer realm=\"OpenSearch Security\""), "") + ); } @Override @@ -181,7 +186,7 @@ public String getType() { return "jwt"; } - protected String extractSubject(final Claims claims, final RestRequest request) { + protected String extractSubject(final Claims claims, final SecurityRequest request) { String subject = claims.getSubject(); if (subjectKey != null) { // try to get roles from claims, first as Object to avoid having to catch the ExpectedTypeException @@ -205,7 +210,7 @@ protected String extractSubject(final Claims claims, final RestRequest request) } @SuppressWarnings("unchecked") - protected String[] extractRoles(final Claims claims, final RestRequest request) { + protected String[] extractRoles(final Claims claims, final SecurityRequest request) { // no roles key specified if (rolesKey == null) { return new String[0]; diff --git a/src/main/java/com/amazon/dlic/auth/http/kerberos/HTTPSpnegoAuthenticator.java b/src/main/java/com/amazon/dlic/auth/http/kerberos/HTTPSpnegoAuthenticator.java index 99bc23b746..ad24b8db95 100644 --- a/src/main/java/com/amazon/dlic/auth/http/kerberos/HTTPSpnegoAuthenticator.java +++ b/src/main/java/com/amazon/dlic/auth/http/kerberos/HTTPSpnegoAuthenticator.java @@ -11,6 +11,8 @@ package com.amazon.dlic.auth.http.kerberos; +import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; + import java.io.Serializable; import java.nio.file.Files; import java.nio.file.Path; @@ -22,13 +24,17 @@ import java.security.PrivilegedExceptionAction; import java.util.Base64; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; +import java.util.Optional; import java.util.Set; import javax.security.auth.Subject; import javax.security.auth.login.LoginException; import com.google.common.base.Strings; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.ietf.jgss.GSSContext; @@ -48,15 +54,13 @@ import org.opensearch.common.xcontent.XContentFactory; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.env.Environment; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestRequest; -import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auth.HTTPAuthenticator; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.filter.SecurityResponse; import org.opensearch.security.user.AuthCredentials; public class HTTPSpnegoAuthenticator implements HTTPAuthenticator { - private static final String EMPTY_STRING = ""; private static final Oid[] KRB_OIDS = new Oid[] { KrbConstants.SPNEGO, KrbConstants.KRB5MECH }; protected final Logger log = LogManager.getLogger(this.getClass()); @@ -170,7 +174,7 @@ public Void run() { @Override @SuppressWarnings("removal") - public AuthCredentials extractCredentials(final RestRequest request, ThreadContext threadContext) { + public AuthCredentials extractCredentials(final SecurityRequest request, final ThreadContext threadContext) { final SecurityManager sm = System.getSecurityManager(); if (sm != null) { @@ -187,7 +191,7 @@ public AuthCredentials run() { return creds; } - private AuthCredentials extractCredentials0(final RestRequest request) { + private AuthCredentials extractCredentials0(final SecurityRequest request) { if (acceptorPrincipal == null || acceptorKeyTabPath == null) { log.error("Missing acceptor principal or keytab configuration. Kerberos authentication will not work"); @@ -279,25 +283,22 @@ public GSSCredential run() throws GSSException { } @Override - public BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials credentials) { - final BytesRestResponse wwwAuthenticateResponse; - XContentBuilder response = getNegotiateResponseBody(); - - if (response != null) { - wwwAuthenticateResponse = new BytesRestResponse(RestStatus.UNAUTHORIZED, response); - } else { - wwwAuthenticateResponse = new BytesRestResponse(RestStatus.UNAUTHORIZED, EMPTY_STRING); + public Optional reRequestAuthentication(final SecurityRequest request, AuthCredentials creds) { + final Map headers = new HashMap<>(); + String responseBody = ""; + final String negotiateResponseBody = getNegotiateResponseBody(); + if (negotiateResponseBody != null) { + responseBody = negotiateResponseBody; + headers.putAll(SecurityResponse.CONTENT_TYPE_APP_JSON); } - if (credentials == null || credentials.getNativeCredentials() == null) { - wwwAuthenticateResponse.addHeader("WWW-Authenticate", "Negotiate"); + if (creds == null || creds.getNativeCredentials() == null) { + headers.put("WWW-Authenticate", "Negotiate"); } else { - wwwAuthenticateResponse.addHeader( - "WWW-Authenticate", - "Negotiate " + Base64.getEncoder().encodeToString((byte[]) credentials.getNativeCredentials()) - ); + headers.put("WWW-Authenticate", "Negotiate " + Base64.getEncoder().encodeToString((byte[]) creds.getNativeCredentials())); } - return wwwAuthenticateResponse; + + return Optional.of(new SecurityResponse(SC_UNAUTHORIZED, headers, responseBody)); } @Override @@ -369,7 +370,7 @@ private static String getUsernameFromGSSContext(final GSSContext gssContext, fin return null; } - private XContentBuilder getNegotiateResponseBody() { + private String getNegotiateResponseBody() { try { XContentBuilder negotiateResponseBody = XContentFactory.jsonBuilder(); negotiateResponseBody.startObject(); @@ -381,7 +382,7 @@ private XContentBuilder getNegotiateResponseBody() { negotiateResponseBody.endObject(); negotiateResponseBody.endObject(); negotiateResponseBody.endObject(); - return negotiateResponseBody; + return negotiateResponseBody.toString(); } catch (Exception ex) { log.error("Can't construct response body", ex); return null; diff --git a/src/main/java/com/amazon/dlic/auth/http/saml/AuthTokenProcessorHandler.java b/src/main/java/com/amazon/dlic/auth/http/saml/AuthTokenProcessorHandler.java index 7210ed5950..a344c653f3 100644 --- a/src/main/java/com/amazon/dlic/auth/http/saml/AuthTokenProcessorHandler.java +++ b/src/main/java/com/amazon/dlic/auth/http/saml/AuthTokenProcessorHandler.java @@ -19,6 +19,7 @@ import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.List; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -34,6 +35,7 @@ import com.onelogin.saml2.exception.ValidationError; import com.onelogin.saml2.settings.Saml2Settings; import com.onelogin.saml2.util.Util; + import org.apache.commons.lang3.StringUtils; import org.apache.cxf.jaxrs.json.basic.JsonMapObjectReaderWriter; import org.apache.cxf.rs.security.jose.jwk.JsonWebKey; @@ -44,21 +46,21 @@ import org.apache.cxf.rs.security.jose.jwt.JwtClaims; import org.apache.cxf.rs.security.jose.jwt.JwtToken; import org.apache.cxf.rs.security.jose.jwt.JwtUtils; +import org.apache.http.HttpStatus; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.joda.time.DateTime; - import org.opensearch.OpenSearchSecurityException; import org.opensearch.SpecialPermission; import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentType; -import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; import org.opensearch.core.rest.RestStatus; import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.dlic.rest.api.AuthTokenProcessorAction; +import org.opensearch.security.filter.SecurityResponse; class AuthTokenProcessorHandler { private static final Logger log = LogManager.getLogger(AuthTokenProcessorHandler.class); @@ -118,7 +120,7 @@ class AuthTokenProcessorHandler { } @SuppressWarnings("removal") - BytesRestResponse handle(RestRequest restRequest) throws Exception { + Optional handle(RestRequest restRequest) throws Exception { try { final SecurityManager sm = System.getSecurityManager(); @@ -126,9 +128,9 @@ BytesRestResponse handle(RestRequest restRequest) throws Exception { sm.checkPermission(new SpecialPermission()); } - return AccessController.doPrivileged(new PrivilegedExceptionAction() { + return AccessController.doPrivileged(new PrivilegedExceptionAction>() { @Override - public BytesRestResponse run() throws SamlConfigException, IOException { + public Optional run() throws SamlConfigException, IOException { return handleLowLevel(restRequest); } }); @@ -181,7 +183,7 @@ private AuthTokenProcessorAction.Response handleImpl( } } - private BytesRestResponse handleLowLevel(RestRequest restRequest) throws SamlConfigException, IOException { + private Optional handleLowLevel(RestRequest restRequest) throws SamlConfigException, IOException { try { if (restRequest.getMediaType() != XContentType.JSON) { @@ -229,15 +231,15 @@ private BytesRestResponse handleLowLevel(RestRequest restRequest) throws SamlCon AuthTokenProcessorAction.Response responseBody = this.handleImpl(samlResponseBase64, samlRequestId, acsEndpoint, saml2Settings); if (responseBody == null) { - return null; + return Optional.empty(); } String responseBodyString = DefaultObjectMapper.objectMapper.writeValueAsString(responseBody); - return new BytesRestResponse(RestStatus.OK, "application/json", responseBodyString); + return Optional.of(new SecurityResponse(HttpStatus.SC_OK, SecurityResponse.CONTENT_TYPE_APP_JSON, responseBodyString)); } catch (JsonProcessingException e) { log.warn("Error while parsing JSON for /_opendistro/_security/api/authtoken", e); - return new BytesRestResponse(RestStatus.BAD_REQUEST, "JSON could not be parsed"); + return Optional.of(new SecurityResponse(HttpStatus.SC_BAD_REQUEST, null, "JSON could not be parsed")); } } diff --git a/src/main/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticator.java b/src/main/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticator.java index 64d816fabf..a3f37ba46e 100644 --- a/src/main/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticator.java +++ b/src/main/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticator.java @@ -18,6 +18,8 @@ import java.security.PrivateKey; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; +import java.util.Map; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -35,6 +37,7 @@ import net.shibboleth.utilities.java.support.xml.BasicParserPool; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.cxf.rs.security.jose.jwk.JsonWebKey; +import org.apache.http.HttpStatus; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensaml.core.config.InitializationException; @@ -55,11 +58,13 @@ import org.opensearch.SpecialPermission; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestRequest; -import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auth.Destroyable; import org.opensearch.security.auth.HTTPAuthenticator; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.filter.SecurityRequestChannelUnsupported; +import org.opensearch.security.filter.SecurityResponse; +import org.opensearch.security.filter.OpenSearchRequest; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.PemKeyReader; import org.opensearch.security.user.AuthCredentials; @@ -148,17 +153,18 @@ public HTTPSamlAuthenticator(final Settings settings, final Path configPath) { } @Override - public AuthCredentials extractCredentials(RestRequest restRequest, ThreadContext threadContext) throws OpenSearchSecurityException { - Matcher matcher = PATTERN_PATH_PREFIX.matcher(restRequest.path()); + public AuthCredentials extractCredentials(final SecurityRequest request, final ThreadContext threadContext) + throws OpenSearchSecurityException { + Matcher matcher = PATTERN_PATH_PREFIX.matcher(request.path()); final String suffix = matcher.matches() ? matcher.group(2) : null; if (API_AUTHTOKEN_SUFFIX.equals(suffix)) { return null; } - AuthCredentials authCredentials = this.httpJwtAuthenticator.extractCredentials(restRequest, threadContext); + AuthCredentials authCredentials = this.httpJwtAuthenticator.extractCredentials(request, threadContext); if (AUTHINFO_SUFFIX.equals(suffix)) { - this.initLogoutUrl(restRequest, threadContext, authCredentials); + this.initLogoutUrl(threadContext, authCredentials); } return authCredentials; @@ -170,26 +176,32 @@ public String getType() { } @Override - public BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials credentials) { + public Optional reRequestAuthentication(final SecurityRequest request, final AuthCredentials authCredentials) { try { Matcher matcher = PATTERN_PATH_PREFIX.matcher(request.path()); final String suffix = matcher.matches() ? matcher.group(2) : null; + if (API_AUTHTOKEN_SUFFIX.equals(suffix)) { - final BytesRestResponse restResponse = this.authTokenProcessorHandler.handle(request); - if (restResponse != null) { - return restResponse; + // Verficiation of SAML ASC endpoint only works with RestRequests + if (!(request instanceof OpenSearchRequest)) { + throw new SecurityRequestChannelUnsupported(); + } else { + final OpenSearchRequest openSearchRequest = (OpenSearchRequest) request; + final RestRequest restRequest = openSearchRequest.breakEncapsulationForRequest(); + Optional restResponse = this.authTokenProcessorHandler.handle(restRequest); + if (restResponse.isPresent()) { + return restResponse; + } } } - Saml2Settings saml2Settings = this.saml2SettingsProvider.getCached(); - BytesRestResponse authenticateResponse = new BytesRestResponse(RestStatus.UNAUTHORIZED, ""); - - authenticateResponse.addHeader("WWW-Authenticate", getWwwAuthenticateHeader(saml2Settings)); - - return authenticateResponse; + final Saml2Settings saml2Settings = this.saml2SettingsProvider.getCached(); + return Optional.of( + new SecurityResponse(HttpStatus.SC_UNAUTHORIZED, Map.of("WWW-Authenticate", getWwwAuthenticateHeader(saml2Settings)), "") + ); } catch (Exception e) { log.error("Error in reRequestAuthentication()", e); - return null; + return Optional.empty(); } } @@ -397,7 +409,7 @@ String buildLogoutUrl(AuthCredentials authCredentials) { } - private void initLogoutUrl(RestRequest restRequest, ThreadContext threadContext, AuthCredentials authCredentials) { + private void initLogoutUrl(ThreadContext threadContext, AuthCredentials authCredentials) { threadContext.putTransient(ConfigConstants.SSO_LOGOUT_URL, buildLogoutUrl(authCredentials)); } diff --git a/src/main/java/org/opensearch/security/auditlog/AuditLog.java b/src/main/java/org/opensearch/security/auditlog/AuditLog.java index 612b790686..997b9e4b87 100644 --- a/src/main/java/org/opensearch/security/auditlog/AuditLog.java +++ b/src/main/java/org/opensearch/security/auditlog/AuditLog.java @@ -35,23 +35,23 @@ import org.opensearch.index.engine.Engine.IndexResult; import org.opensearch.index.get.GetResult; import org.opensearch.core.index.shard.ShardId; -import org.opensearch.rest.RestRequest; import org.opensearch.security.auditlog.config.AuditConfig; import org.opensearch.security.compliance.ComplianceConfig; +import org.opensearch.security.filter.SecurityRequest; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportRequest; public interface AuditLog extends Closeable { // login - void logFailedLogin(String effectiveUser, boolean securityadmin, String initiatingUser, RestRequest request); + void logFailedLogin(String effectiveUser, boolean securityadmin, String initiatingUser, SecurityRequest request); - void logSucceededLogin(String effectiveUser, boolean securityadmin, String initiatingUser, RestRequest request); + void logSucceededLogin(String effectiveUser, boolean securityadmin, String initiatingUser, SecurityRequest request); // privs - void logMissingPrivileges(String privilege, String effectiveUser, RestRequest request); + void logMissingPrivileges(String privilege, String effectiveUser, SecurityRequest request); - void logGrantedPrivileges(String effectiveUser, RestRequest request); + void logGrantedPrivileges(String effectiveUser, SecurityRequest request); void logMissingPrivileges(String privilege, TransportRequest request, Task task); @@ -63,13 +63,13 @@ public interface AuditLog extends Closeable { // spoof void logBadHeaders(TransportRequest request, String action, Task task); - void logBadHeaders(RestRequest request); + void logBadHeaders(SecurityRequest request); void logSecurityIndexAttempt(TransportRequest request, String action, Task task); void logSSLException(TransportRequest request, Throwable t, String action, Task task); - void logSSLException(RestRequest request, Throwable t); + void logSSLException(SecurityRequest request, Throwable t); void logDocumentRead(String index, String id, ShardId shardId, Map fieldNameValues); diff --git a/src/main/java/org/opensearch/security/auditlog/AuditLogSslExceptionHandler.java b/src/main/java/org/opensearch/security/auditlog/AuditLogSslExceptionHandler.java index 942f06804f..df96400f96 100644 --- a/src/main/java/org/opensearch/security/auditlog/AuditLogSslExceptionHandler.java +++ b/src/main/java/org/opensearch/security/auditlog/AuditLogSslExceptionHandler.java @@ -27,7 +27,7 @@ package org.opensearch.security.auditlog; import org.opensearch.OpenSearchException; -import org.opensearch.rest.RestRequest; +import org.opensearch.security.filter.SecurityRequestChannel; import org.opensearch.security.ssl.SslExceptionHandler; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportRequest; @@ -42,7 +42,7 @@ public AuditLogSslExceptionHandler(final AuditLog auditLog) { } @Override - public void logError(Throwable t, RestRequest request, int type) { + public void logError(Throwable t, SecurityRequestChannel request, int type) { switch (type) { case 0: auditLog.logSSLException(request, t); diff --git a/src/main/java/org/opensearch/security/auditlog/NullAuditLog.java b/src/main/java/org/opensearch/security/auditlog/NullAuditLog.java index 809951d48a..1ac4492a94 100644 --- a/src/main/java/org/opensearch/security/auditlog/NullAuditLog.java +++ b/src/main/java/org/opensearch/security/auditlog/NullAuditLog.java @@ -35,9 +35,9 @@ import org.opensearch.index.engine.Engine.IndexResult; import org.opensearch.index.get.GetResult; import org.opensearch.core.index.shard.ShardId; -import org.opensearch.rest.RestRequest; import org.opensearch.security.auditlog.config.AuditConfig; import org.opensearch.security.compliance.ComplianceConfig; +import org.opensearch.security.filter.SecurityRequest; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportRequest; @@ -49,12 +49,12 @@ public void close() throws IOException { } @Override - public void logFailedLogin(String effectiveUser, boolean securityadmin, String initiatingUser, RestRequest request) { + public void logFailedLogin(String effectiveUser, boolean securityadmin, String initiatingUser, SecurityRequest request) { // noop, intentionally left empty } @Override - public void logSucceededLogin(String effectiveUser, boolean securityadmin, String initiatingUser, RestRequest request) { + public void logSucceededLogin(String effectiveUser, boolean securityadmin, String initiatingUser, SecurityRequest request) { // noop, intentionally left empty } @@ -79,7 +79,7 @@ public void logBadHeaders(TransportRequest request, String action, Task task) { } @Override - public void logBadHeaders(RestRequest request) { + public void logBadHeaders(SecurityRequest request) { // noop, intentionally left empty } @@ -94,17 +94,17 @@ public void logSSLException(TransportRequest request, Throwable t, String action } @Override - public void logSSLException(RestRequest request, Throwable t) { + public void logSSLException(SecurityRequest request, Throwable t) { // noop, intentionally left empty } @Override - public void logMissingPrivileges(String privilege, String effectiveUser, RestRequest request) { + public void logMissingPrivileges(String privilege, String effectiveUser, SecurityRequest request) { // noop, intentionally left empty } @Override - public void logGrantedPrivileges(String effectiveUser, RestRequest request) { + public void logGrantedPrivileges(String effectiveUser, SecurityRequest request) { // noop, intentionally left empty } diff --git a/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java b/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java index a8f511be97..de53f0b744 100644 --- a/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java +++ b/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java @@ -60,12 +60,12 @@ import org.opensearch.index.engine.Engine.IndexResult; import org.opensearch.index.get.GetResult; import org.opensearch.core.index.shard.ShardId; -import org.opensearch.rest.RestRequest; import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.auditlog.config.AuditConfig; import org.opensearch.security.compliance.ComplianceConfig; import org.opensearch.security.dlic.rest.support.Utils; +import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.support.Base64Helper; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; @@ -139,7 +139,7 @@ public ComplianceConfig getComplianceConfig() { } @Override - public void logFailedLogin(String effectiveUser, boolean securityadmin, String initiatingUser, RestRequest request) { + public void logFailedLogin(String effectiveUser, boolean securityadmin, String initiatingUser, SecurityRequest request) { if (!checkRestFilter(AuditCategory.FAILED_LOGIN, effectiveUser, request)) { return; @@ -157,7 +157,7 @@ public void logFailedLogin(String effectiveUser, boolean securityadmin, String i } @Override - public void logSucceededLogin(String effectiveUser, boolean securityadmin, String initiatingUser, RestRequest request) { + public void logSucceededLogin(String effectiveUser, boolean securityadmin, String initiatingUser, SecurityRequest request) { if (!checkRestFilter(AuditCategory.AUTHENTICATED, effectiveUser, request)) { return; @@ -174,7 +174,7 @@ public void logSucceededLogin(String effectiveUser, boolean securityadmin, Strin } @Override - public void logMissingPrivileges(String privilege, String effectiveUser, RestRequest request) { + public void logMissingPrivileges(String privilege, String effectiveUser, SecurityRequest request) { if (!checkRestFilter(AuditCategory.MISSING_PRIVILEGES, effectiveUser, request)) { return; } @@ -189,7 +189,7 @@ public void logMissingPrivileges(String privilege, String effectiveUser, RestReq } @Override - public void logGrantedPrivileges(String effectiveUser, RestRequest request) { + public void logGrantedPrivileges(String effectiveUser, SecurityRequest request) { if (!checkRestFilter(AuditCategory.GRANTED_PRIVILEGES, effectiveUser, request)) { return; } @@ -348,7 +348,7 @@ public void logBadHeaders(TransportRequest request, String action, Task task) { } @Override - public void logBadHeaders(RestRequest request) { + public void logBadHeaders(SecurityRequest request) { if (!checkRestFilter(AuditCategory.BAD_HEADERS, getUser(), request)) { return; @@ -437,7 +437,7 @@ public void logSSLException(TransportRequest request, Throwable t, String action } @Override - public void logSSLException(RestRequest request, Throwable t) { + public void logSSLException(SecurityRequest request, Throwable t) { if (!checkRestFilter(AuditCategory.SSL_EXCEPTION, getUser(), request)) { return; @@ -898,7 +898,7 @@ private boolean checkComplianceFilter( } @VisibleForTesting - boolean checkRestFilter(final AuditCategory category, final String effectiveUser, RestRequest request) { + boolean checkRestFilter(final AuditCategory category, final String effectiveUser, SecurityRequest request) { final boolean isTraceEnabled = log.isTraceEnabled(); if (isTraceEnabled) { log.trace( diff --git a/src/main/java/org/opensearch/security/auditlog/impl/AuditLogImpl.java b/src/main/java/org/opensearch/security/auditlog/impl/AuditLogImpl.java index c88f1fca3f..8da4b13d4c 100644 --- a/src/main/java/org/opensearch/security/auditlog/impl/AuditLogImpl.java +++ b/src/main/java/org/opensearch/security/auditlog/impl/AuditLogImpl.java @@ -31,9 +31,9 @@ import org.opensearch.index.engine.Engine.IndexResult; import org.opensearch.index.get.GetResult; import org.opensearch.core.index.shard.ShardId; -import org.opensearch.rest.RestRequest; import org.opensearch.security.auditlog.config.AuditConfig; import org.opensearch.security.auditlog.routing.AuditMessageRouter; +import org.opensearch.security.filter.SecurityRequest; import org.opensearch.tasks.Task; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportRequest; @@ -131,28 +131,28 @@ protected void save(final AuditMessage msg) { } @Override - public void logFailedLogin(String effectiveUser, boolean securityAdmin, String initiatingUser, RestRequest request) { + public void logFailedLogin(String effectiveUser, boolean securityAdmin, String initiatingUser, SecurityRequest request) { if (enabled) { super.logFailedLogin(effectiveUser, securityAdmin, initiatingUser, request); } } @Override - public void logSucceededLogin(String effectiveUser, boolean securityAdmin, String initiatingUser, RestRequest request) { + public void logSucceededLogin(String effectiveUser, boolean securityAdmin, String initiatingUser, SecurityRequest request) { if (enabled) { super.logSucceededLogin(effectiveUser, securityAdmin, initiatingUser, request); } } @Override - public void logMissingPrivileges(String privilege, String effectiveUser, RestRequest request) { + public void logMissingPrivileges(String privilege, String effectiveUser, SecurityRequest request) { if (enabled) { super.logMissingPrivileges(privilege, effectiveUser, request); } } @Override - public void logGrantedPrivileges(String effectiveUser, RestRequest request) { + public void logGrantedPrivileges(String effectiveUser, SecurityRequest request) { if (enabled) { super.logGrantedPrivileges(effectiveUser, request); } @@ -187,7 +187,7 @@ public void logBadHeaders(TransportRequest request, String action, Task task) { } @Override - public void logBadHeaders(RestRequest request) { + public void logBadHeaders(SecurityRequest request) { if (enabled) { super.logBadHeaders(request); } @@ -208,7 +208,7 @@ public void logSSLException(TransportRequest request, Throwable t, String action } @Override - public void logSSLException(RestRequest request, Throwable t) { + public void logSSLException(SecurityRequest request, Throwable t) { if (enabled) { super.logSSLException(request, t); } diff --git a/src/main/java/org/opensearch/security/auditlog/impl/AuditMessage.java b/src/main/java/org/opensearch/security/auditlog/impl/AuditMessage.java index 7c3bf5c77f..a41b4625c2 100644 --- a/src/main/java/org/opensearch/security/auditlog/impl/AuditMessage.java +++ b/src/main/java/org/opensearch/security/auditlog/impl/AuditMessage.java @@ -48,6 +48,8 @@ import org.opensearch.security.auditlog.AuditLog.Origin; import org.opensearch.security.auditlog.config.AuditConfig; import org.opensearch.security.dlic.rest.support.Utils; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.filter.OpenSearchRequest; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.support.WildcardMatcher; @@ -369,16 +371,31 @@ void addRestMethod(final RestRequest.Method method) { } } - void addRestRequestInfo(final RestRequest request, final AuditConfig.Filter filter) { + void addRestRequestInfo(final SecurityRequest request, final AuditConfig.Filter filter) { if (request != null) { - final String path = request.path(); + final String path = request.path().toString(); addPath(path); addRestHeaders(request.getHeaders(), filter.shouldExcludeSensitiveHeaders()); addRestParams(request.params()); addRestMethod(request.method()); - if (filter.shouldLogRequestBody() && request.hasContentOrSourceParam()) { + + if (filter.shouldLogRequestBody()) { + + if (!(request instanceof OpenSearchRequest)) { + // The request body is only avaliable on some request sources + return; + } + + final OpenSearchRequest securityRestRequest = (OpenSearchRequest) request; + final RestRequest restRequest = securityRestRequest.breakEncapsulationForRequest(); + + if (!(restRequest.hasContentOrSourceParam())) { + // If there is no content, don't attempt to save any body information + return; + } + try { - final Tuple xContentTuple = request.contentOrSourceParam(); + final Tuple xContentTuple = restRequest.contentOrSourceParam(); final String requestBody = XContentHelper.convertToJson(xContentTuple.v2(), false, xContentTuple.v1()); if (path != null && requestBody != null diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 35d4e05c4c..88e88c3c52 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -32,6 +32,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.SortedSet; import java.util.concurrent.Callable; @@ -43,6 +44,7 @@ import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import com.google.common.collect.Multimap; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.greenrobot.eventbus.Subscribe; @@ -50,15 +52,14 @@ import org.opensearch.OpenSearchSecurityException; import org.opensearch.common.settings.Settings; import org.opensearch.core.common.transport.TransportAddress; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestRequest; import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.auth.blocking.ClientBlockRegistry; import org.opensearch.security.auth.internal.NoOpAuthenticationBackend; import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.filter.SecurityRequestChannel; +import org.opensearch.security.filter.SecurityResponse; import org.opensearch.security.http.OnBehalfOfAuthenticator; import org.opensearch.security.http.XFFResolver; import org.opensearch.security.securityconf.DynamicConfigModel; @@ -68,6 +69,10 @@ import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; +import static org.apache.http.HttpStatus.SC_FORBIDDEN; +import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; +import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; + public class BackendRegistry { protected final Logger log = LogManager.getLogger(this.getClass()); @@ -185,16 +190,18 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { * @return The authenticated user, null means another roundtrip * @throws OpenSearchSecurityException */ - public boolean authenticate(final RestRequest request, final RestChannel channel, final ThreadContext threadContext) { + public boolean authenticate(final SecurityRequestChannel request) { final boolean isDebugEnabled = log.isDebugEnabled(); - if (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress - && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress())) { + final boolean isBlockedBasedOnAddress = request.getRemoteAddress() + .map(InetSocketAddress::getAddress) + .map(address -> isBlocked(address)) + .orElse(false); + if (isBlockedBasedOnAddress) { if (isDebugEnabled) { - log.debug("Rejecting REST request because of blocked address: {}", request.getHttpChannel().getRemoteAddress()); + log.debug("Rejecting REST request because of blocked address: {}", request.getRemoteAddress().orElse(null)); } - channel.sendResponse(new BytesRestResponse(RestStatus.UNAUTHORIZED, "Authentication finally failed")); - + request.queueForSending(new SecurityResponse(SC_UNAUTHORIZED, null, "Authentication finally failed")); return false; } @@ -214,17 +221,17 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g if (!isInitialized()) { log.error("Not yet initialized (you may need to run securityadmin)"); - channel.sendResponse(new BytesRestResponse(RestStatus.SERVICE_UNAVAILABLE, "OpenSearch Security not initialized.")); + request.queueForSending(new SecurityResponse(SC_SERVICE_UNAVAILABLE, null, "OpenSearch Security not initialized.")); return false; } final TransportAddress remoteAddress = xffResolver.resolve(request); final boolean isTraceEnabled = log.isTraceEnabled(); if (isTraceEnabled) { - log.trace("Rest authentication request from {} [original: {}]", remoteAddress, request.getHttpChannel().getRemoteAddress()); + log.trace("Rest authentication request from {} [original: {}]", remoteAddress, request.getRemoteAddress().orElse(null)); } - threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, remoteAddress); + threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, remoteAddress); boolean authenticated = false; @@ -256,7 +263,7 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g } final AuthCredentials ac; try { - ac = httpAuthenticator.extractCredentials(request, threadContext); + ac = httpAuthenticator.extractCredentials(request, threadPool.getThreadContext()); } catch (Exception e1) { if (isDebugEnabled) { log.debug("'{}' extracting credentials from {} http authenticator", e1.toString(), httpAuthenticator.getType(), e1); @@ -281,17 +288,16 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g } if (authDomain.isChallenge()) { - final BytesRestResponse restResponse = httpAuthenticator.reRequestAuthentication(request, null); - if (restResponse != null) { + final Optional restResponse = httpAuthenticator.reRequestAuthentication(request, null); + if (restResponse.isPresent()) { auditLog.logFailedLogin("", false, null, request); if (isTraceEnabled) { log.trace("No 'Authorization' header, send 401 and 'WWW-Authenticate Basic'"); } notifyIpAuthFailureListeners(request, authCredentials); - channel.sendResponse(restResponse); + request.queueForSending(restResponse.get()); return false; } - } else { // no reRequest possible if (isTraceEnabled) { @@ -302,12 +308,11 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g } else { org.apache.logging.log4j.ThreadContext.put("user", ac.getUsername()); if (!ac.isComplete()) { - final BytesRestResponse restResponse = httpAuthenticator.reRequestAuthentication(request, ac); // credentials found in request but we need another client challenge - if (restResponse != null) { - // auditLog.logFailedLogin(ac.getUsername()+" ", request); --noauditlog + final Optional restResponse = httpAuthenticator.reRequestAuthentication(request, ac); + if (restResponse.isPresent()) { notifyIpAuthFailureListeners(request, ac); - channel.sendResponse(restResponse); + request.queueForSending(restResponse.get()); return false; } else { // no reRequest possible @@ -334,9 +339,7 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g authDomain.getBackend().getClass().getName() )) { authFailureListener.onAuthFailure( - (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress) - ? ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress() - : null, + request.getRemoteAddress().map(InetSocketAddress::getAddress).orElse(null), ac, request ); @@ -347,9 +350,10 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g if (adminDns.isAdmin(authenticatedUser)) { log.error("Cannot authenticate rest user because admin user is not permitted to login via HTTP"); auditLog.logFailedLogin(authenticatedUser.getName(), true, null, request); - channel.sendResponse( - new BytesRestResponse( - RestStatus.FORBIDDEN, + request.queueForSending( + new SecurityResponse( + SC_FORBIDDEN, + null, "Cannot authenticate user because admin user is not permitted to login via HTTP" ) ); @@ -370,10 +374,8 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g if (authenticated) { final User impersonatedUser = impersonate(request, authenticatedUser); - threadContext.putTransient( - ConfigConstants.OPENDISTRO_SECURITY_USER, - impersonatedUser == null ? authenticatedUser : impersonatedUser - ); + threadPool.getThreadContext() + .putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, impersonatedUser == null ? authenticatedUser : impersonatedUser); auditLog.logSucceededLogin( (impersonatedUser == null ? authenticatedUser : impersonatedUser).getName(), false, @@ -390,14 +392,15 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g User anonymousUser = new User(User.ANONYMOUS.getName(), new HashSet(User.ANONYMOUS.getRoles()), null); anonymousUser.setRequestedTenant(tenant); - threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, anonymousUser); + threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, anonymousUser); auditLog.logSucceededLogin(anonymousUser.getName(), false, null, request); if (isDebugEnabled) { log.debug("Anonymous User is authenticated"); } return true; } - BytesRestResponse challengeResponse = null; + + Optional challengeResponse = Optional.empty(); if (firstChallengingHttpAuthenticator != null) { @@ -406,7 +409,7 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g } challengeResponse = firstChallengingHttpAuthenticator.reRequestAuthentication(request, null); - if (challengeResponse != null) { + if (challengeResponse.isPresent()) { if (isDebugEnabled) { log.debug("Rerequest {} failed", firstChallengingHttpAuthenticator.getClass()); } @@ -422,25 +425,16 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g notifyIpAuthFailureListeners(request, authCredentials); - channel.sendResponse( - challengeResponse != null - ? challengeResponse - : new BytesRestResponse(RestStatus.UNAUTHORIZED, "Authentication finally failed") + request.queueForSending( + challengeResponse.orElseGet(() -> new SecurityResponse(SC_UNAUTHORIZED, null, "Authentication finally failed")) ); return false; } - return authenticated; } - private void notifyIpAuthFailureListeners(RestRequest request, AuthCredentials authCredentials) { - notifyIpAuthFailureListeners( - (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress) - ? ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress() - : null, - authCredentials, - request - ); + private void notifyIpAuthFailureListeners(SecurityRequestChannel request, AuthCredentials authCredentials) { + notifyIpAuthFailureListeners(request.getRemoteAddress().map(InetSocketAddress::getAddress).orElse(null), authCredentials, request); } private void notifyIpAuthFailureListeners(InetAddress remoteAddress, AuthCredentials authCredentials, Object request) { @@ -587,7 +581,7 @@ public User call() throws Exception { } } - private User impersonate(final RestRequest request, final User originalUser) throws OpenSearchSecurityException { + private User impersonate(final SecurityRequest request, final User originalUser) throws OpenSearchSecurityException { final String impersonatedUserHeader = request.header("opendistro_security_impersonate_as"); diff --git a/src/main/java/org/opensearch/security/auth/HTTPAuthenticator.java b/src/main/java/org/opensearch/security/auth/HTTPAuthenticator.java index 70b94d6dbf..f5a4df34b5 100644 --- a/src/main/java/org/opensearch/security/auth/HTTPAuthenticator.java +++ b/src/main/java/org/opensearch/security/auth/HTTPAuthenticator.java @@ -26,10 +26,13 @@ package org.opensearch.security.auth; +import java.util.Optional; + import org.opensearch.OpenSearchSecurityException; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestRequest; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.filter.SecurityResponse; import org.opensearch.security.user.AuthCredentials; /** @@ -67,7 +70,7 @@ public interface HTTPAuthenticator { * If the authentication flow needs another roundtrip with the request originator do not mark it as complete. * @throws OpenSearchSecurityException */ - AuthCredentials extractCredentials(RestRequest request, ThreadContext context) throws OpenSearchSecurityException; + AuthCredentials extractCredentials(final SecurityRequest request, final ThreadContext context) throws OpenSearchSecurityException; /** * If the {@code extractCredentials()} call was not successful or the authentication flow needs another roundtrip this method @@ -75,10 +78,9 @@ public interface HTTPAuthenticator { * If the custom HTTP authenticator does support re-request authentication or supports authentication flows with multiple roundtrips * then the response will be returned which can then be sent via response channel. * - * @param request + * @param request The request to reauthenticate or not * @param credentials The credentials from the prior authentication attempt - * @return null if re-request is not supported/necessary, response object otherwise. - * If an object is returned {@code channel.sendResponse()} must be called so that the request completes. + * @return Optional response if is not supported/necessary, response object otherwise. */ - BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials credentials); + Optional reRequestAuthentication(final SecurityRequest request, AuthCredentials credentials); } diff --git a/src/main/java/org/opensearch/security/auth/UserInjector.java b/src/main/java/org/opensearch/security/auth/UserInjector.java index 30df84ef5f..57750bfa9a 100644 --- a/src/main/java/org/opensearch/security/auth/UserInjector.java +++ b/src/main/java/org/opensearch/security/auth/UserInjector.java @@ -41,8 +41,8 @@ import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.common.settings.Settings; import org.opensearch.core.common.transport.TransportAddress; -import org.opensearch.rest.RestRequest; import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.filter.SecurityRequestChannel; import org.opensearch.security.http.XFFResolver; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.SecurityUtils; @@ -185,7 +185,7 @@ public InjectedUser getInjectedUser() { return injectedUser; } - boolean injectUser(RestRequest request) { + boolean injectUser(SecurityRequestChannel request) { InjectedUser injectedUser = getInjectedUser(); if (injectedUser == null) { return false; diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java index 293f934434..6cbd7eaf78 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java @@ -49,6 +49,7 @@ import org.opensearch.security.dlic.rest.validation.EndpointValidator; import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.dlic.rest.validation.ValidationResult; +import org.opensearch.security.filter.SecurityRequestFactory; import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -371,6 +372,7 @@ protected final ValidationResult> loadConfigurat ) { final var configuration = load(cType, logComplianceEvent); if (configuration.getSeqNo() < 0) { + return ValidationResult.error( RestStatus.FORBIDDEN, forbiddenMessage( @@ -411,16 +413,19 @@ public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { @Override public ValidationResult onConfigDelete(SecurityConfiguration securityConfiguration) throws IOException { + return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Access denied")); } @Override public ValidationResult onConfigLoad(SecurityConfiguration securityConfiguration) throws IOException { + return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Access denied")); } @Override public ValidationResult onConfigChange(SecurityConfiguration securityConfiguration) throws IOException { + return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Access denied")); } @@ -551,12 +556,12 @@ protected final RestChannelConsumer prepareRequest(RestRequest request, NodeClie final String userName = user == null ? null : user.getName(); if (authError != null) { LOGGER.error("No permission to access REST API: " + authError); - securityApiDependencies.auditLog().logMissingPrivileges(authError, userName, request); + securityApiDependencies.auditLog().logMissingPrivileges(authError, userName, SecurityRequestFactory.from(request)); // for rest request request.params().clear(); return channel -> forbidden(channel, "No permission to access REST API: " + authError); } else { - securityApiDependencies.auditLog().logGrantedPrivileges(userName, request); + securityApiDependencies.auditLog().logGrantedPrivileges(userName, SecurityRequestFactory.from(request)); } final var originalUserAndRemoteAddress = Utils.userAndRemoteAddressFrom(threadPool.getThreadContext()); diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/NodesDnApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/NodesDnApiAction.java index a10139b594..ed1f3e0fbb 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/NodesDnApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/NodesDnApiAction.java @@ -35,6 +35,7 @@ import org.opensearch.security.securityconf.impl.NodesDn; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.tools.SecurityAdmin; import org.opensearch.threadpool.ThreadPool; import static org.opensearch.security.dlic.rest.api.Responses.forbiddenMessage; diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java index 35f4332520..f1a336986b 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java @@ -35,6 +35,8 @@ import org.opensearch.rest.RestRequest.Method; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.dlic.rest.support.Utils; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.filter.SecurityRequestFactory; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.util.SSLRequestHelper; @@ -446,7 +448,8 @@ private String checkAdminCertBasedAccessPermissions(RestRequest request) throws } // Certificate based access, Check if we have an admin TLS certificate - SSLRequestHelper.SSLInfo sslInfo = SSLRequestHelper.getSSLInfo(settings, configPath, request, principalExtractor); + final SecurityRequest securityRequest = SecurityRequestFactory.from(request); + SSLRequestHelper.SSLInfo sslInfo = SSLRequestHelper.getSSLInfo(settings, configPath, securityRequest, principalExtractor); if (sslInfo == null) { // here we log on error level, since authentication finally failed diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/EndpointValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/EndpointValidator.java index 442d39cf43..6eb865e937 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/EndpointValidator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/EndpointValidator.java @@ -104,6 +104,7 @@ default ValidationResult entityStatic(final SecurityConfi final var configuration = securityConfiguration.configuration(); final var entityName = securityConfiguration.entityName(); if (configuration.isStatic(entityName)) { + return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Resource '" + entityName + "' is static.")); } return ValidationResult.success(securityConfiguration); @@ -122,6 +123,7 @@ default ValidationResult entityHidden(final SecurityConfi final var configuration = securityConfiguration.configuration(); final var entityName = securityConfiguration.entityName(); if (configuration.isHidden(entityName)) { + return ValidationResult.error(RestStatus.NOT_FOUND, notFoundMessage("Resource '" + entityName + "' is not available.")); } return ValidationResult.success(securityConfiguration); @@ -149,6 +151,7 @@ default ValidationResult isAllowedToChangeEntityWithRestA final var configuration = securityConfiguration.configuration(); final var existingEntity = configuration.getCEntry(securityConfiguration.entityName()); if (restApiAdminPrivilegesEvaluator().containsRestApiAdminPermissions(existingEntity)) { + return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Access denied")); } } else { @@ -158,6 +161,7 @@ default ValidationResult isAllowedToChangeEntityWithRestA configuration.getImplementingClass() ); if (restApiAdminPrivilegesEvaluator().containsRestApiAdminPermissions(configEntityContent)) { + return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Access denied")); } } diff --git a/src/main/java/org/opensearch/security/filter/OpenSearchRequest.java b/src/main/java/org/opensearch/security/filter/OpenSearchRequest.java new file mode 100644 index 0000000000..85c70b8f7a --- /dev/null +++ b/src/main/java/org/opensearch/security/filter/OpenSearchRequest.java @@ -0,0 +1,91 @@ +/* + * 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.filter; + +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.net.ssl.SSLEngine; + +import org.opensearch.http.netty4.Netty4HttpChannel; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestRequest.Method; + +import io.netty.handler.ssl.SslHandler; + +/** + * Wraps the functionality of RestRequest for use in the security plugin + */ +public class OpenSearchRequest implements SecurityRequest { + + protected final RestRequest underlyingRequest; + + OpenSearchRequest(final RestRequest request) { + underlyingRequest = request; + } + + @Override + public Map> getHeaders() { + return underlyingRequest.getHeaders(); + } + + @Override + public SSLEngine getSSLEngine() { + if (underlyingRequest == null + || underlyingRequest.getHttpChannel() == null + || !(underlyingRequest.getHttpChannel() instanceof Netty4HttpChannel)) { + return null; + } + + // We look for Ssl_handler called `ssl_http` in the outbound pipeline of Netty channel first, and if its not + // present we look for it in inbound channel. If its present in neither we return null, else we return the sslHandler. + final Netty4HttpChannel httpChannel = (Netty4HttpChannel) underlyingRequest.getHttpChannel(); + SslHandler sslhandler = (SslHandler) httpChannel.getNettyChannel().pipeline().get("ssl_http"); + if (sslhandler == null && httpChannel.inboundPipeline() != null) { + sslhandler = (SslHandler) httpChannel.inboundPipeline().get("ssl_http"); + } + + return sslhandler != null ? sslhandler.engine() : null; + } + + @Override + public String path() { + return underlyingRequest.path(); + } + + @Override + public Method method() { + return underlyingRequest.method(); + } + + @Override + public Optional getRemoteAddress() { + return Optional.ofNullable(this.underlyingRequest.getHttpChannel().getRemoteAddress()); + } + + @Override + public String uri() { + return underlyingRequest.uri(); + } + + @Override + public Map params() { + return underlyingRequest.params(); + } + + /** Gets access to the underlying request object */ + public RestRequest breakEncapsulationForRequest() { + return underlyingRequest; + } +} diff --git a/src/main/java/org/opensearch/security/filter/OpenSearchRequestChannel.java b/src/main/java/org/opensearch/security/filter/OpenSearchRequestChannel.java new file mode 100644 index 0000000000..45035e0d83 --- /dev/null +++ b/src/main/java/org/opensearch/security/filter/OpenSearchRequestChannel.java @@ -0,0 +1,97 @@ +/* + * 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.filter; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; + +public class OpenSearchRequestChannel extends OpenSearchRequest implements SecurityRequestChannel { + + private final Logger log = LogManager.getLogger(OpenSearchRequest.class); + + private final AtomicReference responseRef = new AtomicReference(null); + private final AtomicBoolean hasCompleted = new AtomicBoolean(false); + private final RestChannel underlyingChannel; + + OpenSearchRequestChannel(final RestRequest request, final RestChannel channel) { + super(request); + underlyingChannel = channel; + } + + /** Gets access to the underlying channel object */ + public RestChannel breakEncapsulationForChannel() { + return underlyingChannel; + } + + @Override + public void queueForSending(final SecurityResponse response) { + if (underlyingChannel == null) { + throw new UnsupportedOperationException("Channel was not defined"); + } + + if (hasCompleted.get()) { + throw new UnsupportedOperationException("This channel has already completed"); + } + + if (getQueuedResponse().isPresent()) { + throw new UnsupportedOperationException("Another response was already queued"); + } + + responseRef.set(response); + } + + @Override + public Optional getQueuedResponse() { + return Optional.ofNullable(responseRef.get()); + } + + @Override + public boolean sendResponse() { + if (underlyingChannel == null) { + throw new UnsupportedOperationException("Channel was not defined"); + } + + if (hasCompleted.get()) { + throw new UnsupportedOperationException("This channel has already completed"); + } + + if (getQueuedResponse().isEmpty()) { + throw new UnsupportedOperationException("No response has been associated with this channel"); + } + + final SecurityResponse response = responseRef.get(); + + try { + final BytesRestResponse restResponse = new BytesRestResponse(RestStatus.fromCode(response.getStatus()), response.getBody()); + if (response.getHeaders() != null) { + response.getHeaders().forEach(restResponse::addHeader); + } + underlyingChannel.sendResponse(restResponse); + + return true; + } catch (final Exception e) { + log.error("Error when attempting to send response", e); + throw new RuntimeException(e); + } finally { + hasCompleted.set(true); + } + + } +} diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index f433a5857d..00b117ebb8 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -464,6 +464,7 @@ public void onFailure(Exception e) { : String.format("no permissions for %s and %s", pres.getMissingPrivileges(), user); } log.debug(err); + listener.onFailure(new OpenSearchSecurityException(err, RestStatus.FORBIDDEN)); } } catch (OpenSearchException e) { diff --git a/src/main/java/org/opensearch/security/filter/SecurityRequest.java b/src/main/java/org/opensearch/security/filter/SecurityRequest.java new file mode 100644 index 0000000000..7e6e94e0a6 --- /dev/null +++ b/src/main/java/org/opensearch/security/filter/SecurityRequest.java @@ -0,0 +1,53 @@ +/* + * 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.filter; + +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import javax.net.ssl.SSLEngine; + +import org.opensearch.rest.RestRequest.Method; + +/** How the security plugin interacts with requests */ +public interface SecurityRequest { + + /** Collection of headers associated with the request */ + public Map> getHeaders(); + + /** The SSLEngine associated with the request */ + public SSLEngine getSSLEngine(); + + /** The path of the request */ + public String path(); + + /** The method type of this request */ + public Method method(); + + /** The remote address of the request, possible null */ + public Optional getRemoteAddress(); + + /** The full uri of the request */ + public String uri(); + + /** Finds the first value of the matching header or null */ + default public String header(final String headerName) { + final Optional>> headersMap = Optional.ofNullable(getHeaders()); + return headersMap.map(headers -> headers.get(headerName)).map(List::stream).flatMap(Stream::findFirst).orElse(null); + } + + /** The parameters associated with this request */ + public Map params(); +} diff --git a/src/main/java/org/opensearch/security/filter/SecurityRequestChannel.java b/src/main/java/org/opensearch/security/filter/SecurityRequestChannel.java new file mode 100644 index 0000000000..1eec754c08 --- /dev/null +++ b/src/main/java/org/opensearch/security/filter/SecurityRequestChannel.java @@ -0,0 +1,29 @@ +/* + * 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.filter; + +import java.util.Optional; + +/** + * When a request is recieved by the security plugin this governs getting information about the request and complete with with a response + */ +public interface SecurityRequestChannel extends SecurityRequest { + + /** Associate a response with this channel */ + public void queueForSending(final SecurityResponse response); + + /** Acess the queued response */ + public Optional getQueuedResponse(); + + /** Send the response through the channel */ + public boolean sendResponse(); +} diff --git a/src/main/java/org/opensearch/security/filter/SecurityRequestChannelUnsupported.java b/src/main/java/org/opensearch/security/filter/SecurityRequestChannelUnsupported.java new file mode 100644 index 0000000000..bcacc2cf7a --- /dev/null +++ b/src/main/java/org/opensearch/security/filter/SecurityRequestChannelUnsupported.java @@ -0,0 +1,17 @@ +/* + * 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.filter; + +/** Thrown when a security rest channel is not supported */ +public class SecurityRequestChannelUnsupported extends RuntimeException { + +} diff --git a/src/main/java/org/opensearch/security/filter/SecurityRequestFactory.java b/src/main/java/org/opensearch/security/filter/SecurityRequestFactory.java new file mode 100644 index 0000000000..de74df01ff --- /dev/null +++ b/src/main/java/org/opensearch/security/filter/SecurityRequestFactory.java @@ -0,0 +1,31 @@ +/* + * 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.filter; + +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; + +/** + * Generates wrapped versions of requests for use in the security plugin + */ +public class SecurityRequestFactory { + + /** Creates a security request from a RestRequest */ + public static SecurityRequest from(final RestRequest request) { + return new OpenSearchRequest(request); + } + + /** Creates a security request channel from a RestRequest & RestChannel */ + public static SecurityRequestChannel from(final RestRequest request, final RestChannel channel) { + return new OpenSearchRequestChannel(request, channel); + } +} diff --git a/src/main/java/org/opensearch/security/filter/SecurityResponse.java b/src/main/java/org/opensearch/security/filter/SecurityResponse.java new file mode 100644 index 0000000000..8618be3aab --- /dev/null +++ b/src/main/java/org/opensearch/security/filter/SecurityResponse.java @@ -0,0 +1,44 @@ +/* + * 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.filter; + +import java.util.Map; + +import org.apache.http.HttpHeaders; + +public class SecurityResponse { + + public static final Map CONTENT_TYPE_APP_JSON = Map.of(HttpHeaders.CONTENT_TYPE, "application/json"); + + private final int status; + private final Map headers; + private final String body; + + public SecurityResponse(final int status, final Map headers, final String body) { + this.status = status; + this.headers = headers; + this.body = body; + } + + public int getStatus() { + return status; + } + + public Map getHeaders() { + return headers; + } + + public String getBody() { + return body; + } + +} diff --git a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java index 54d98ffe13..ce80a9e143 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java @@ -35,6 +35,7 @@ import javax.net.ssl.SSLPeerUnverifiedException; +import org.apache.http.HttpStatus; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.greenrobot.eventbus.Subscribe; @@ -42,13 +43,9 @@ import org.opensearch.OpenSearchException; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.NamedRoute; -import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestHandler; -import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; -import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.auditlog.AuditLog.Origin; import org.opensearch.security.auth.BackendRegistry; @@ -131,17 +128,40 @@ public SecurityRestFilter( public RestHandler wrap(RestHandler original, AdminDNs adminDNs) { return (request, channel, client) -> { org.apache.logging.log4j.ThreadContext.clearAll(); - if (!checkAndAuthenticateRequest(request, channel)) { - User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - boolean isSuperAdminUser = userIsSuperAdmin(user, adminDNs); - if (isSuperAdminUser - || (whitelistingSettings.checkRequestIsAllowed(request, channel, client) - && allowlistingSettings.checkRequestIsAllowed(request, channel, client))) { - if (isSuperAdminUser || authorizeRequest(original, request, channel, user)) { - original.handleRequest(request, channel, client); - } - } + final SecurityRequestChannel requestChannel = SecurityRequestFactory.from(request, channel); + + // Authenticate request + checkAndAuthenticateRequest(requestChannel); + if (requestChannel.getQueuedResponse().isPresent()) { + requestChannel.sendResponse(); + return; + } + + // Authorize Request + final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + if (userIsSuperAdmin(user, adminDNs)) { + // Super admins are always authorized + original.handleRequest(request, channel, client); + return; + } + + final Optional deniedResponse = whitelistingSettings.checkRequestIsAllowed(requestChannel) + .or(() -> allowlistingSettings.checkRequestIsAllowed(requestChannel)); + + if (deniedResponse.isPresent()) { + requestChannel.queueForSending(deniedResponse.orElseThrow()); + requestChannel.sendResponse(); + return; + } + + authorizeRequest(original, requestChannel, user); + if (requestChannel.getQueuedResponse().isPresent()) { + requestChannel.sendResponse(); + return; } + + // Caller was authorized, forward the request to the handler + original.handleRequest(request, channel, client); }; } @@ -152,8 +172,7 @@ private boolean userIsSuperAdmin(User user, AdminDNs adminDNs) { return user != null && adminDNs.isAdmin(user); } - private boolean authorizeRequest(RestHandler original, RestRequest request, RestChannel channel, User user) { - + private void authorizeRequest(RestHandler original, SecurityRequestChannel request, User user) { List restRoutes = original.routes(); Optional handler = restRoutes.stream() .filter(rh -> rh.getMethod().equals(request.method())) @@ -190,38 +209,37 @@ private boolean authorizeRequest(RestHandler original, RestRequest request, Rest err = String.format("no permissions for %s and %s", pres.getMissingPrivileges(), user); } log.debug(err); - channel.sendResponse(new BytesRestResponse(RestStatus.UNAUTHORIZED, err)); - return false; + + request.queueForSending(new SecurityResponse(HttpStatus.SC_UNAUTHORIZED, null, err)); + return; } } - - // if handler is not an instance of NamedRoute then we pass through to eval at Transport Layer. - return true; } - private boolean checkAndAuthenticateRequest(RestRequest request, RestChannel channel) throws Exception { - + public void checkAndAuthenticateRequest(SecurityRequestChannel requestChannel) throws Exception { threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN, Origin.REST.toString()); - if (HTTPHelper.containsBadHeader(request)) { + if (HTTPHelper.containsBadHeader(requestChannel)) { final OpenSearchException exception = ExceptionUtils.createBadHeaderException(); log.error(exception.toString()); - auditLog.logBadHeaders(request); - channel.sendResponse(new BytesRestResponse(channel, RestStatus.FORBIDDEN, exception)); - return true; + auditLog.logBadHeaders(requestChannel); + + requestChannel.queueForSending(new SecurityResponse(HttpStatus.SC_FORBIDDEN, null, exception.toString())); + return; } if (SSLRequestHelper.containsBadHeader(threadContext, ConfigConstants.OPENDISTRO_SECURITY_CONFIG_PREFIX)) { final OpenSearchException exception = ExceptionUtils.createBadHeaderException(); log.error(exception.toString()); - auditLog.logBadHeaders(request); - channel.sendResponse(new BytesRestResponse(channel, RestStatus.FORBIDDEN, exception)); - return true; + auditLog.logBadHeaders(requestChannel); + + requestChannel.queueForSending(new SecurityResponse(HttpStatus.SC_FORBIDDEN, null, exception.toString())); + return; } final SSLInfo sslInfo; try { - if ((sslInfo = SSLRequestHelper.getSSLInfo(settings, configPath, request, principalExtractor)) != null) { + if ((sslInfo = SSLRequestHelper.getSSLInfo(settings, configPath, requestChannel, principalExtractor)) != null) { if (sslInfo.getPrincipal() != null) { threadContext.putTransient("_opendistro_security_ssl_principal", sslInfo.getPrincipal()); } @@ -234,22 +252,23 @@ private boolean checkAndAuthenticateRequest(RestRequest request, RestChannel cha } } catch (SSLPeerUnverifiedException e) { log.error("No ssl info", e); - auditLog.logSSLException(request, e); - channel.sendResponse(new BytesRestResponse(channel, RestStatus.FORBIDDEN, e)); - return true; + auditLog.logSSLException(requestChannel, e); + requestChannel.queueForSending(new SecurityResponse(HttpStatus.SC_FORBIDDEN, null, null)); + return; } if (!compatConfig.restAuthEnabled()) { - return false; + // Authentication is disabled + return; } - Matcher matcher = PATTERN_PATH_PREFIX.matcher(request.path()); + Matcher matcher = PATTERN_PATH_PREFIX.matcher(requestChannel.path()); final String suffix = matcher.matches() ? matcher.group(2) : null; - if (request.method() != Method.OPTIONS && !(HEALTH_SUFFIX.equals(suffix)) && !(WHO_AM_I_SUFFIX.equals(suffix))) { - if (!registry.authenticate(request, channel, threadContext)) { + if (requestChannel.method() != Method.OPTIONS && !(HEALTH_SUFFIX.equals(suffix)) && !(WHO_AM_I_SUFFIX.equals(suffix))) { + if (!registry.authenticate(requestChannel)) { // another roundtrip org.apache.logging.log4j.ThreadContext.remove("user"); - return true; + return; } else { // make it possible to filter logs by username org.apache.logging.log4j.ThreadContext.put( @@ -258,8 +277,6 @@ private boolean checkAndAuthenticateRequest(RestRequest request, RestChannel cha ); } } - - return false; } @Subscribe diff --git a/src/main/java/org/opensearch/security/http/HTTPBasicAuthenticator.java b/src/main/java/org/opensearch/security/http/HTTPBasicAuthenticator.java index 16a822df6d..ff07db147e 100644 --- a/src/main/java/org/opensearch/security/http/HTTPBasicAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/HTTPBasicAuthenticator.java @@ -27,16 +27,18 @@ package org.opensearch.security.http; import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; +import org.apache.http.HttpStatus; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestRequest; -import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auth.HTTPAuthenticator; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.filter.SecurityResponse; import org.opensearch.security.support.HTTPHelper; import org.opensearch.security.user.AuthCredentials; @@ -50,9 +52,9 @@ public HTTPBasicAuthenticator(final Settings settings, final Path configPath) { } @Override - public AuthCredentials extractCredentials(final RestRequest request, ThreadContext threadContext) { + public AuthCredentials extractCredentials(final SecurityRequest request, final ThreadContext threadContext) { - final boolean forceLogin = request.paramAsBoolean("force_login", false); + final boolean forceLogin = Boolean.getBoolean(request.params().get("force_login")); if (forceLogin) { return null; @@ -64,10 +66,10 @@ public AuthCredentials extractCredentials(final RestRequest request, ThreadConte } @Override - public BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials credentials) { - final BytesRestResponse wwwAuthenticateResponse = new BytesRestResponse(RestStatus.UNAUTHORIZED, "Unauthorized"); - wwwAuthenticateResponse.addHeader("WWW-Authenticate", "Basic realm=\"OpenSearch Security\""); - return wwwAuthenticateResponse; + public Optional reRequestAuthentication(final SecurityRequest request, AuthCredentials creds) { + return Optional.of( + new SecurityResponse(HttpStatus.SC_UNAUTHORIZED, Map.of("WWW-Authenticate", "Basic realm=\"OpenSearch Security\""), "") + ); } @Override diff --git a/src/main/java/org/opensearch/security/http/HTTPClientCertAuthenticator.java b/src/main/java/org/opensearch/security/http/HTTPClientCertAuthenticator.java index cea59f7311..433ec01458 100644 --- a/src/main/java/org/opensearch/security/http/HTTPClientCertAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/HTTPClientCertAuthenticator.java @@ -30,6 +30,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; import javax.naming.InvalidNameException; import javax.naming.ldap.LdapName; @@ -41,9 +42,9 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.common.Strings; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestRequest; import org.opensearch.security.auth.HTTPAuthenticator; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.filter.SecurityResponse; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.AuthCredentials; @@ -57,7 +58,7 @@ public HTTPClientCertAuthenticator(final Settings settings, final Path configPat } @Override - public AuthCredentials extractCredentials(final RestRequest request, final ThreadContext threadContext) { + public AuthCredentials extractCredentials(final SecurityRequest request, final ThreadContext threadContext) { final String principal = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL); @@ -98,8 +99,8 @@ public AuthCredentials extractCredentials(final RestRequest request, final Threa } @Override - public BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials credentials) { - return null; + public Optional reRequestAuthentication(final SecurityRequest response, AuthCredentials creds) { + return Optional.empty(); } @Override diff --git a/src/main/java/org/opensearch/security/http/HTTPProxyAuthenticator.java b/src/main/java/org/opensearch/security/http/HTTPProxyAuthenticator.java index 1db7b9769b..de57e2f9e6 100644 --- a/src/main/java/org/opensearch/security/http/HTTPProxyAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/HTTPProxyAuthenticator.java @@ -27,6 +27,7 @@ package org.opensearch.security.http; import java.nio.file.Path; +import java.util.Optional; import java.util.regex.Pattern; import com.google.common.base.Predicates; @@ -37,9 +38,9 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.common.Strings; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestRequest; import org.opensearch.security.auth.HTTPAuthenticator; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.filter.SecurityResponse; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.AuthCredentials; @@ -56,7 +57,7 @@ public HTTPProxyAuthenticator(Settings settings, final Path configPath) { } @Override - public AuthCredentials extractCredentials(final RestRequest request, ThreadContext context) { + public AuthCredentials extractCredentials(final SecurityRequest request, final ThreadContext context) { if (context.getTransient(ConfigConstants.OPENDISTRO_SECURITY_XFF_DONE) != Boolean.TRUE) { throw new OpenSearchSecurityException("xff not done"); @@ -89,8 +90,8 @@ public AuthCredentials extractCredentials(final RestRequest request, ThreadConte } @Override - public BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials credentials) { - return null; + public Optional reRequestAuthentication(final SecurityRequest response, AuthCredentials creds) { + return Optional.empty(); } @Override diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index 02077bed7c..8499b88f62 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -15,6 +15,7 @@ import java.security.PrivilegedAction; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -33,10 +34,10 @@ import org.opensearch.SpecialPermission; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestRequest; import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.filter.SecurityResponse; import org.opensearch.security.ssl.util.ExceptionUtils; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.util.KeyUtils; @@ -120,7 +121,8 @@ private String[] extractBackendRolesFromClaims(Claims claims) { @Override @SuppressWarnings("removal") - public AuthCredentials extractCredentials(RestRequest request, ThreadContext context) throws OpenSearchSecurityException { + public AuthCredentials extractCredentials(final SecurityRequest request, final ThreadContext context) + throws OpenSearchSecurityException { final SecurityManager sm = System.getSecurityManager(); if (sm != null) { @@ -137,7 +139,7 @@ public AuthCredentials run() { return creds; } - private AuthCredentials extractCredentials0(final RestRequest request) { + private AuthCredentials extractCredentials0(final SecurityRequest request) { if (!oboEnabled) { log.error("On-behalf-of authentication is disabled"); return null; @@ -201,7 +203,7 @@ private AuthCredentials extractCredentials0(final RestRequest request) { return null; } - private String extractJwtFromHeader(RestRequest request) { + private String extractJwtFromHeader(SecurityRequest request) { String jwtToken = request.header(HttpHeaders.AUTHORIZATION); if (jwtToken == null || jwtToken.isEmpty()) { @@ -229,7 +231,7 @@ private void logDebug(String message, Object... args) { } } - public Boolean isRequestAllowed(final RestRequest request) { + public Boolean isRequestAllowed(final SecurityRequest request) { Matcher matcher = PATTERN_PATH_PREFIX.matcher(request.path()); final String suffix = matcher.matches() ? matcher.group(2) : null; if (isAccessToRestrictedEndpoints(request, suffix)) { @@ -241,8 +243,8 @@ public Boolean isRequestAllowed(final RestRequest request) { } @Override - public BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials credentials) { - return null; + public Optional reRequestAuthentication(final SecurityRequest response, AuthCredentials creds) { + return Optional.empty(); } @Override diff --git a/src/main/java/org/opensearch/security/http/RemoteIpDetector.java b/src/main/java/org/opensearch/security/http/RemoteIpDetector.java index f464c0653a..7b76a82c42 100644 --- a/src/main/java/org/opensearch/security/http/RemoteIpDetector.java +++ b/src/main/java/org/opensearch/security/http/RemoteIpDetector.java @@ -43,6 +43,7 @@ package org.opensearch.security.http; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.LinkedList; import java.util.List; @@ -52,7 +53,7 @@ import org.apache.logging.log4j.Logger; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.rest.RestRequest; +import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.support.ConfigConstants; final class RemoteIpDetector { @@ -115,8 +116,12 @@ public String getRemoteIpHeader() { return remoteIpHeader; } - String detect(RestRequest request, ThreadContext threadContext) { - final String originalRemoteAddr = ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress().getHostAddress(); + String detect(SecurityRequest request, ThreadContext threadContext) { + + final String originalRemoteAddr = request.getRemoteAddress() + .map(InetSocketAddress::getAddress) + .map(InetAddress::getHostAddress) + .orElseThrow(); final boolean isTraceEnabled = log.isTraceEnabled(); if (isTraceEnabled) { @@ -173,8 +178,10 @@ String detect(RestRequest request, ThreadContext threadContext) { if (remoteIp != null) { if (isTraceEnabled) { - final String originalRemoteHost = ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress() - .getHostName(); + final String originalRemoteHost = request.getRemoteAddress() + .map(InetSocketAddress::getAddress) + .map(InetAddress::getHostName) + .orElseThrow(); log.trace( "Incoming request {} with originalRemoteAddr '{}', originalRemoteHost='{}', will be seen as newRemoteAddr='{}'", request.uri(), @@ -196,7 +203,7 @@ String detect(RestRequest request, ThreadContext threadContext) { log.trace( "Skip RemoteIpDetector for request {} with originalRemoteAddr '{}' cause no internal proxy matches", request.uri(), - request.getHttpChannel().getRemoteAddress() + request.getRemoteAddress().orElse(null) ); } } diff --git a/src/main/java/org/opensearch/security/http/XFFResolver.java b/src/main/java/org/opensearch/security/http/XFFResolver.java index ddb7255179..e9ad412831 100644 --- a/src/main/java/org/opensearch/security/http/XFFResolver.java +++ b/src/main/java/org/opensearch/security/http/XFFResolver.java @@ -34,9 +34,11 @@ import org.opensearch.OpenSearchSecurityException; import org.opensearch.core.common.transport.TransportAddress; -import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.http.netty4.Netty4HttpChannel; import org.opensearch.rest.RestRequest; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.filter.OpenSearchRequest; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.support.ConfigConstants; import org.opensearch.threadpool.ThreadPool; @@ -53,20 +55,23 @@ public XFFResolver(final ThreadPool threadPool) { this.threadContext = threadPool.getThreadContext(); } - public TransportAddress resolve(final RestRequest request) throws OpenSearchSecurityException { + public TransportAddress resolve(final SecurityRequest request) throws OpenSearchSecurityException { final boolean isTraceEnabled = log.isTraceEnabled(); if (isTraceEnabled) { - log.trace("resolve {}", request.getHttpChannel().getRemoteAddress()); + log.trace("resolve {}", request.getRemoteAddress().orElse(null)); } - if (enabled - && request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress - && request.getHttpChannel() instanceof Netty4HttpChannel) { + boolean requestFromNetty = false; + if (request instanceof OpenSearchRequest) { + final OpenSearchRequest securityRequestChannel = (OpenSearchRequest) request; + final RestRequest restRequest = securityRequestChannel.breakEncapsulationForRequest(); - final InetSocketAddress isa = new InetSocketAddress( - detector.detect(request, threadContext), - ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getPort() - ); + requestFromNetty = restRequest.getHttpChannel() instanceof Netty4HttpChannel; + } + + if (enabled && request.getRemoteAddress().isPresent() && requestFromNetty) { + final InetSocketAddress remoteAddress = request.getRemoteAddress().get(); + final InetSocketAddress isa = new InetSocketAddress(detector.detect(request, threadContext), remoteAddress.getPort()); if (isa.isUnresolved()) { throw new OpenSearchSecurityException("Cannot resolve address " + isa.getHostString()); @@ -74,23 +79,21 @@ public TransportAddress resolve(final RestRequest request) throws OpenSearchSecu if (isTraceEnabled) { if (threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_XFF_DONE) == Boolean.TRUE) { - log.trace("xff resolved {} to {}", request.getHttpChannel().getRemoteAddress(), isa); + log.trace("xff resolved {} to {}", remoteAddress, isa); } else { log.trace("no xff done for {}", request.getClass()); } } return new TransportAddress(isa); - } else if (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress) { - + } else if (request.getRemoteAddress().isPresent()) { if (isTraceEnabled) { log.trace("no xff done (enabled or no netty request) {},{},{},{}", enabled, request.getClass()); - } - return new TransportAddress((InetSocketAddress) request.getHttpChannel().getRemoteAddress()); + return new TransportAddress((InetSocketAddress) request.getRemoteAddress().get()); } else { throw new OpenSearchSecurityException( "Cannot handle this request. Remote address is " - + request.getHttpChannel().getRemoteAddress() + + request.getRemoteAddress().orElse(null) + " with request class " + request.getClass() ); diff --git a/src/main/java/org/opensearch/security/http/proxy/HTTPExtendedProxyAuthenticator.java b/src/main/java/org/opensearch/security/http/proxy/HTTPExtendedProxyAuthenticator.java index 0423fecefe..9a16e6c61b 100644 --- a/src/main/java/org/opensearch/security/http/proxy/HTTPExtendedProxyAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/proxy/HTTPExtendedProxyAuthenticator.java @@ -37,7 +37,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.common.Strings; -import org.opensearch.rest.RestRequest; +import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.http.HTTPProxyAuthenticator; import org.opensearch.security.user.AuthCredentials; @@ -54,7 +54,7 @@ public HTTPExtendedProxyAuthenticator(Settings settings, final Path configPath) } @Override - public AuthCredentials extractCredentials(final RestRequest request, ThreadContext context) { + public AuthCredentials extractCredentials(final SecurityRequest request, final ThreadContext context) { AuthCredentials credentials = super.extractCredentials(request, context); if (credentials == null) { return null; diff --git a/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java b/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java index c582c9f51b..bfbc16f98d 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java @@ -29,6 +29,7 @@ import org.opensearch.security.action.configupdate.ConfigUpdateAction; import org.opensearch.security.action.configupdate.ConfigUpdateRequest; import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.filter.SecurityRequestFactory; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.util.SSLRequestHelper; import org.opensearch.security.support.ConfigConstants; @@ -73,7 +74,12 @@ public List routes() { protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { String[] configTypes = request.paramAsStringArrayOrEmptyIfAll("config_types"); - SSLRequestHelper.SSLInfo sslInfo = SSLRequestHelper.getSSLInfo(settings, configPath, request, principalExtractor); + SSLRequestHelper.SSLInfo sslInfo = SSLRequestHelper.getSSLInfo( + settings, + configPath, + SecurityRequestFactory.from(request), + principalExtractor + ); if (sslInfo == null) { return channel -> channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, "")); diff --git a/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java b/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java index 8bab16484d..4f560f40b6 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java @@ -32,6 +32,7 @@ import org.opensearch.rest.RestRequest; import org.opensearch.core.rest.RestStatus; import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.filter.SecurityRequestFactory; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.util.SSLRequestHelper; import org.opensearch.security.ssl.util.SSLRequestHelper.SSLInfo; @@ -97,8 +98,12 @@ public void accept(RestChannel channel) throws Exception { BytesRestResponse response = null; try { - - SSLInfo sslInfo = SSLRequestHelper.getSSLInfo(settings, configPath, request, principalExtractor); + SSLInfo sslInfo = SSLRequestHelper.getSSLInfo( + settings, + configPath, + SecurityRequestFactory.from(request), + principalExtractor + ); if (sslInfo == null) { response = new BytesRestResponse(RestStatus.FORBIDDEN, "No security data"); diff --git a/src/main/java/org/opensearch/security/securityconf/impl/AllowlistingSettings.java b/src/main/java/org/opensearch/security/securityconf/impl/AllowlistingSettings.java index 98fc7a266a..ba249e8c63 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/AllowlistingSettings.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/AllowlistingSettings.java @@ -15,12 +15,13 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; -import org.opensearch.client.node.NodeClient; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestRequest; +import org.apache.http.HttpStatus; +import org.opensearch.common.xcontent.XContentFactory; import org.opensearch.core.rest.RestStatus; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.filter.SecurityResponse; public class AllowlistingSettings { private boolean enabled; @@ -75,7 +76,7 @@ public String toString() { * GET /_cluster/settings - OK * GET /_cluster/settings/ - OK */ - private boolean requestIsAllowlisted(RestRequest request) { + private boolean requestIsAllowlisted(final SecurityRequest request) { // ALSO ALLOWS REQUEST TO HAVE TRAILING '/' // pathWithoutTrailingSlash stores the endpoint path without extra '/'. eg: /_cat/nodes @@ -107,21 +108,30 @@ private boolean requestIsAllowlisted(RestRequest request) { * then all PUT /_opendistro/_security/api/rolesmapping/{resource_name} work. * Currently, each resource_name has to be allowlisted separately */ - public boolean checkRequestIsAllowed(RestRequest request, RestChannel channel, NodeClient client) throws IOException { + public Optional checkRequestIsAllowed(final SecurityRequest request) { // if allowlisting is enabled but the request is not allowlisted, then return false, otherwise true. if (this.enabled && !requestIsAllowlisted(request)) { - channel.sendResponse( - new BytesRestResponse( - RestStatus.FORBIDDEN, - channel.newErrorBuilder() - .startObject() - .field("error", request.method() + " " + request.path() + " API not allowlisted") - .field("status", RestStatus.FORBIDDEN) - .endObject() - ) + return Optional.of( + new SecurityResponse(HttpStatus.SC_FORBIDDEN, SecurityResponse.CONTENT_TYPE_APP_JSON, generateFailureMessage(request)) ); - return false; } - return true; + return Optional.empty(); + } + + protected String getVerb() { + return "allowlisted"; + } + + protected String generateFailureMessage(final SecurityRequest request) { + try { + return XContentFactory.jsonBuilder() + .startObject() + .field("error", request.method() + " " + request.path() + " API not " + getVerb()) + .field("status", RestStatus.FORBIDDEN) + .endObject() + .toString(); + } catch (final IOException ioe) { + throw new RuntimeException(ioe); + } } } diff --git a/src/main/java/org/opensearch/security/securityconf/impl/WhitelistingSettings.java b/src/main/java/org/opensearch/security/securityconf/impl/WhitelistingSettings.java index 4462bae90f..2e1ab791d2 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/WhitelistingSettings.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/WhitelistingSettings.java @@ -11,16 +11,14 @@ package org.opensearch.security.securityconf.impl; -import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; -import org.opensearch.client.node.NodeClient; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestRequest; -import org.opensearch.core.rest.RestStatus; +import org.apache.http.HttpStatus; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.filter.SecurityResponse; public class WhitelistingSettings extends AllowlistingSettings { private boolean enabled; @@ -75,7 +73,7 @@ public String toString() { * GET /_cluster/settings - OK * GET /_cluster/settings/ - OK */ - private boolean requestIsWhitelisted(RestRequest request) { + private boolean requestIsWhitelisted(final SecurityRequest request) { // ALSO ALLOWS REQUEST TO HAVE TRAILING '/' // pathWithoutTrailingSlash stores the endpoint path without extra '/'. eg: /_cat/nodes @@ -108,21 +106,17 @@ private boolean requestIsWhitelisted(RestRequest request) { * Currently, each resource_name has to be whitelisted separately */ @Override - public boolean checkRequestIsAllowed(RestRequest request, RestChannel channel, NodeClient client) throws IOException { + public Optional checkRequestIsAllowed(final SecurityRequest request) { // if whitelisting is enabled but the request is not whitelisted, then return false, otherwise true. if (this.enabled && !requestIsWhitelisted(request)) { - channel.sendResponse( - new BytesRestResponse( - RestStatus.FORBIDDEN, - channel.newErrorBuilder() - .startObject() - .field("error", request.method() + " " + request.path() + " API not whitelisted") - .field("status", RestStatus.FORBIDDEN) - .endObject() - ) + return Optional.of( + new SecurityResponse(HttpStatus.SC_FORBIDDEN, SecurityResponse.CONTENT_TYPE_APP_JSON, generateFailureMessage(request)) ); - return false; } - return true; + return Optional.empty(); + } + + protected String getVerb() { + return "whitelisted"; } } diff --git a/src/main/java/org/opensearch/security/ssl/SslExceptionHandler.java b/src/main/java/org/opensearch/security/ssl/SslExceptionHandler.java index 90f01df9b5..b274a2ecd3 100644 --- a/src/main/java/org/opensearch/security/ssl/SslExceptionHandler.java +++ b/src/main/java/org/opensearch/security/ssl/SslExceptionHandler.java @@ -17,13 +17,13 @@ package org.opensearch.security.ssl; -import org.opensearch.rest.RestRequest; +import org.opensearch.security.filter.SecurityRequestChannel; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportRequest; public interface SslExceptionHandler { - default void logError(Throwable t, RestRequest request, int type) { + default void logError(Throwable t, SecurityRequestChannel request, int type) { // no-op } diff --git a/src/main/java/org/opensearch/security/ssl/http/netty/ValidatingDispatcher.java b/src/main/java/org/opensearch/security/ssl/http/netty/ValidatingDispatcher.java index e053da7787..dcd25c2837 100644 --- a/src/main/java/org/opensearch/security/ssl/http/netty/ValidatingDispatcher.java +++ b/src/main/java/org/opensearch/security/ssl/http/netty/ValidatingDispatcher.java @@ -33,6 +33,8 @@ import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; import org.opensearch.core.rest.RestStatus; +import org.opensearch.security.filter.SecurityRequestChannel; +import org.opensearch.security.filter.SecurityRequestFactory; import org.opensearch.security.ssl.SslExceptionHandler; import org.opensearch.security.ssl.util.ExceptionUtils; import org.opensearch.security.ssl.util.SSLRequestHelper; @@ -64,17 +66,17 @@ public ValidatingDispatcher( @Override public void dispatchRequest(RestRequest request, RestChannel channel, ThreadContext threadContext) { - checkRequest(request, channel); + checkRequest(SecurityRequestFactory.from(request, channel)); originalDispatcher.dispatchRequest(request, channel, threadContext); } @Override public void dispatchBadRequest(RestChannel channel, ThreadContext threadContext, Throwable cause) { - checkRequest(channel.request(), channel); + checkRequest(SecurityRequestFactory.from(channel.request(), channel)); originalDispatcher.dispatchBadRequest(channel, threadContext, cause); } - protected void checkRequest(final RestRequest request, final RestChannel channel) { + protected void checkRequest(final SecurityRequestChannel request) { if (SSLRequestHelper.containsBadHeader(threadContext, "_opendistro_security_ssl_")) { final OpenSearchException exception = ExceptionUtils.createBadHeaderException(); diff --git a/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLInfoAction.java b/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLInfoAction.java index 54d109f497..8e32893eab 100644 --- a/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLInfoAction.java +++ b/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLInfoAction.java @@ -39,6 +39,7 @@ import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; import org.opensearch.core.rest.RestStatus; +import org.opensearch.security.filter.SecurityRequestFactory; import org.opensearch.security.ssl.SecurityKeyStore; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.util.SSLRequestHelper; @@ -84,8 +85,12 @@ public void accept(RestChannel channel) throws Exception { BytesRestResponse response = null; try { - - SSLInfo sslInfo = SSLRequestHelper.getSSLInfo(settings, configPath, request, principalExtractor); + SSLInfo sslInfo = SSLRequestHelper.getSSLInfo( + settings, + configPath, + SecurityRequestFactory.from(request), + principalExtractor + ); X509Certificate[] certs = sslInfo == null ? null : sslInfo.getX509Certs(); X509Certificate[] localCerts = sslInfo == null ? null : sslInfo.getLocalCertificates(); diff --git a/src/main/java/org/opensearch/security/ssl/util/SSLRequestHelper.java b/src/main/java/org/opensearch/security/ssl/util/SSLRequestHelper.java index 1a23d0bb59..df92bfc703 100644 --- a/src/main/java/org/opensearch/security/ssl/util/SSLRequestHelper.java +++ b/src/main/java/org/opensearch/security/ssl/util/SSLRequestHelper.java @@ -36,7 +36,6 @@ import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; -import io.netty.handler.ssl.SslHandler; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -45,8 +44,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.env.Environment; -import org.opensearch.http.netty4.Netty4HttpChannel; -import org.opensearch.rest.RestRequest; +import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.transport.PrincipalExtractor.Type; @@ -121,25 +119,14 @@ public String toString() { public static SSLInfo getSSLInfo( final Settings settings, final Path configPath, - final RestRequest request, + final SecurityRequest request, PrincipalExtractor principalExtractor ) throws SSLPeerUnverifiedException { - - if (request == null || request.getHttpChannel() == null || !(request.getHttpChannel() instanceof Netty4HttpChannel)) { - return null; - } - - final Netty4HttpChannel httpChannel = (Netty4HttpChannel) request.getHttpChannel(); - SslHandler sslhandler = (SslHandler) httpChannel.getNettyChannel().pipeline().get("ssl_http"); - if (sslhandler == null && httpChannel.inboundPipeline() != null) { - sslhandler = (SslHandler) httpChannel.inboundPipeline().get("ssl_http"); - } - - if (sslhandler == null) { + final SSLEngine engine = request.getSSLEngine(); + if (engine == null) { return null; } - final SSLEngine engine = sslhandler.engine(); final SSLSession session = engine.getSession(); X509Certificate[] x509Certs = null; diff --git a/src/main/java/org/opensearch/security/support/HTTPHelper.java b/src/main/java/org/opensearch/security/support/HTTPHelper.java index c3b191f770..fe590f0d34 100644 --- a/src/main/java/org/opensearch/security/support/HTTPHelper.java +++ b/src/main/java/org/opensearch/security/support/HTTPHelper.java @@ -32,8 +32,7 @@ import java.util.Map; import org.apache.logging.log4j.Logger; - -import org.opensearch.rest.RestRequest; +import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.user.AuthCredentials; public class HTTPHelper { @@ -86,7 +85,7 @@ public static AuthCredentials extractCredentials(String authorizationHeader, Log } } - public static boolean containsBadHeader(final RestRequest request) { + public static boolean containsBadHeader(final SecurityRequest request) { final Map> headers; diff --git a/src/main/java/org/opensearch/security/util/AuthTokenUtils.java b/src/main/java/org/opensearch/security/util/AuthTokenUtils.java index 30f331d3a7..3884bf75fe 100644 --- a/src/main/java/org/opensearch/security/util/AuthTokenUtils.java +++ b/src/main/java/org/opensearch/security/util/AuthTokenUtils.java @@ -12,7 +12,7 @@ package org.opensearch.security.util; import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestRequest; +import org.opensearch.security.filter.SecurityRequest; import static org.opensearch.rest.RestRequest.Method.POST; import static org.opensearch.rest.RestRequest.Method.PUT; @@ -21,7 +21,7 @@ public class AuthTokenUtils { private static final String ON_BEHALF_OF_SUFFIX = "api/generateonbehalfoftoken"; private static final String ACCOUNT_SUFFIX = "api/account"; - public static Boolean isAccessToRestrictedEndpoints(final RestRequest request, final String suffix) { + public static Boolean isAccessToRestrictedEndpoints(final SecurityRequest request, final String suffix) { if (suffix == null) { return false; } 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 04a30ba2db..4a28c0a752 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 @@ -85,7 +85,10 @@ public void testTokenMissing() throws Exception { HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); Map headers = new HashMap(); - AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + AuthCredentials credentials = jwtAuth.extractCredentials( + new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), + null + ); Assert.assertNull(credentials); } @@ -101,7 +104,10 @@ public void testInvalid() throws Exception { Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); - AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + AuthCredentials credentials = jwtAuth.extractCredentials( + new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), + null + ); Assert.assertNull(credentials); } @@ -120,7 +126,10 @@ public void testBearer() throws Exception { Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); - AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + AuthCredentials credentials = jwtAuth.extractCredentials( + new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), + null + ); Assert.assertNotNull(credentials); Assert.assertEquals("Leonard McCoy", credentials.getUsername()); @@ -139,7 +148,10 @@ public void testBearerWrongPosition() throws Exception { Map headers = new HashMap(); headers.put("Authorization", jwsToken + "Bearer " + " 123"); - AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + AuthCredentials credentials = jwtAuth.extractCredentials( + new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), + null + ); Assert.assertNull(credentials); } @@ -152,7 +164,10 @@ public void testBasicAuthHeader() throws Exception { String basicAuth = BaseEncoding.base64().encode("user:password".getBytes(StandardCharsets.UTF_8)); Map headers = Collections.singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + basicAuth); - AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, Collections.emptyMap()), null); + AuthCredentials credentials = jwtAuth.extractCredentials( + new FakeRestRequest(headers, Collections.emptyMap()).asSecurityRequest(), + null + ); Assert.assertNull(credentials); } @@ -261,7 +276,7 @@ public void testUrlParam() throws Exception { FakeRestRequest req = new FakeRestRequest(headers, new HashMap()); req.params().put("abc", jwsToken); - AuthCredentials credentials = jwtAuth.extractCredentials(req, null); + AuthCredentials credentials = jwtAuth.extractCredentials(req.asSecurityRequest(), null); Assert.assertNotNull(credentials); Assert.assertEquals("Leonard McCoy", credentials.getUsername()); @@ -311,7 +326,10 @@ public void testRS256() throws Exception { Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); - AuthCredentials creds = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + AuthCredentials creds = jwtAuth.extractCredentials( + new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), + null + ); Assert.assertNotNull(creds); Assert.assertEquals("Leonard McCoy", creds.getUsername()); @@ -334,7 +352,10 @@ public void testES512() throws Exception { Map headers = new HashMap(); headers.put("Authorization", jwsToken); - AuthCredentials creds = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + AuthCredentials creds = jwtAuth.extractCredentials( + new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), + null + ); Assert.assertNotNull(creds); Assert.assertEquals("Leonard McCoy", creds.getUsername()); @@ -411,7 +432,7 @@ private AuthCredentials extractCredentialsFromJwtHeader(final Settings.Builder s final String jwsToken = jwtBuilder.signWith(secretKey, SignatureAlgorithm.HS512).compact(); final HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); final Map headers = Map.of("Authorization", jwsToken); - return jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap<>()), null); + return jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap<>()).asSecurityRequest(), null); } } diff --git a/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/HTTPJwtKeyByOpenIdConnectAuthenticatorTest.java b/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/HTTPJwtKeyByOpenIdConnectAuthenticatorTest.java index ccdab8d77a..d483a2ec81 100644 --- a/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/HTTPJwtKeyByOpenIdConnectAuthenticatorTest.java +++ b/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/HTTPJwtKeyByOpenIdConnectAuthenticatorTest.java @@ -52,7 +52,8 @@ public void basicTest() { HTTPJwtKeyByOpenIdConnectAuthenticator jwtAuth = new HTTPJwtKeyByOpenIdConnectAuthenticator(settings, null); AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_1), new HashMap()), + new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_1), new HashMap()) + .asSecurityRequest(), null ); @@ -74,7 +75,7 @@ public void jwksUriTest() { HTTPJwtKeyByOpenIdConnectAuthenticator jwtAuth = new HTTPJwtKeyByOpenIdConnectAuthenticator(settings, null); AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_2), new HashMap<>()), + new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_2), new HashMap<>()).asSecurityRequest(), null ); @@ -95,7 +96,8 @@ public void jwksMissingRequiredIssuerInClaimTest() { HTTPJwtKeyByOpenIdConnectAuthenticator jwtAuth = new HTTPJwtKeyByOpenIdConnectAuthenticator(settings, null); AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_NO_ISSUER_OCT_1), new HashMap<>()), + new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_NO_ISSUER_OCT_1), new HashMap<>()) + .asSecurityRequest(), null ); @@ -109,7 +111,7 @@ public void jwksNotMatchingRequiredIssuerInClaimTest() { HTTPJwtKeyByOpenIdConnectAuthenticator jwtAuth = new HTTPJwtKeyByOpenIdConnectAuthenticator(settings, null); AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_2), new HashMap<>()), + new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_2), new HashMap<>()).asSecurityRequest(), null ); @@ -126,7 +128,8 @@ public void jwksMissingRequiredAudienceInClaimTest() { HTTPJwtKeyByOpenIdConnectAuthenticator jwtAuth = new HTTPJwtKeyByOpenIdConnectAuthenticator(settings, null); AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_NO_AUDIENCE_OCT_1), new HashMap<>()), + new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_NO_AUDIENCE_OCT_1), new HashMap<>()) + .asSecurityRequest(), null ); @@ -143,7 +146,7 @@ public void jwksNotMatchingRequiredAudienceInClaimTest() { HTTPJwtKeyByOpenIdConnectAuthenticator jwtAuth = new HTTPJwtKeyByOpenIdConnectAuthenticator(settings, null); AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_2), new HashMap<>()), + new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_2), new HashMap<>()).asSecurityRequest(), null ); @@ -155,7 +158,7 @@ public void jwksUriMissingTest() { var exception = Assert.assertThrows(Exception.class, () -> { HTTPJwtKeyByOpenIdConnectAuthenticator jwtAuth = new HTTPJwtKeyByOpenIdConnectAuthenticator(Settings.builder().build(), null); jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_1), new HashMap<>()), + new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_1), new HashMap<>()).asSecurityRequest(), null ); }); @@ -178,7 +181,7 @@ public void testEscapeKid() { new FakeRestRequest( ImmutableMap.of("Authorization", "Bearer " + TestJwts.MC_COY_SIGNED_OCT_1_INVALID_KID), new HashMap() - ), + ).asSecurityRequest(), null ); @@ -200,7 +203,8 @@ public void bearerTest() { HTTPJwtKeyByOpenIdConnectAuthenticator jwtAuth = new HTTPJwtKeyByOpenIdConnectAuthenticator(settings, null); AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", "Bearer " + TestJwts.MC_COY_SIGNED_OCT_1), new HashMap()), + new FakeRestRequest(ImmutableMap.of("Authorization", "Bearer " + TestJwts.MC_COY_SIGNED_OCT_1), new HashMap()) + .asSecurityRequest(), null ); @@ -223,7 +227,8 @@ public void testRoles() throws Exception { HTTPJwtKeyByOpenIdConnectAuthenticator jwtAuth = new HTTPJwtKeyByOpenIdConnectAuthenticator(settings, null); AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_1), new HashMap()), + new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_1), new HashMap()) + .asSecurityRequest(), null ); @@ -242,7 +247,7 @@ public void testExp() throws Exception { new FakeRestRequest( ImmutableMap.of("Authorization", "Bearer " + TestJwts.MC_COY_EXPIRED_SIGNED_OCT_1), new HashMap() - ), + ).asSecurityRequest(), null ); @@ -267,7 +272,7 @@ public void testExpInSkew() throws Exception { new FakeRestRequest( ImmutableMap.of("Authorization", "Bearer " + TestJwts.createMcCoySignedOct1(notBeforeDate, expiringDate)), new HashMap() - ), + ).asSecurityRequest(), null ); @@ -292,7 +297,7 @@ public void testNbf() throws Exception { new FakeRestRequest( ImmutableMap.of("Authorization", "Bearer " + TestJwts.createMcCoySignedOct1(notBeforeDate, expiringDate)), new HashMap() - ), + ).asSecurityRequest(), null ); @@ -317,7 +322,7 @@ public void testNbfInSkew() throws Exception { new FakeRestRequest( ImmutableMap.of("Authorization", "Bearer " + TestJwts.createMcCoySignedOct1(notBeforeDate, expiringDate)), new HashMap() - ), + ).asSecurityRequest(), null ); @@ -336,7 +341,8 @@ public void testRS256() throws Exception { HTTPJwtKeyByOpenIdConnectAuthenticator jwtAuth = new HTTPJwtKeyByOpenIdConnectAuthenticator(settings, null); AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_RSA_1), new HashMap()), + new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_RSA_1), new HashMap()) + .asSecurityRequest(), null ); @@ -355,7 +361,8 @@ public void testBadSignature() throws Exception { HTTPJwtKeyByOpenIdConnectAuthenticator jwtAuth = new HTTPJwtKeyByOpenIdConnectAuthenticator(settings, null); AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_RSA_X), new HashMap()), + new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_RSA_X), new HashMap()) + .asSecurityRequest(), null ); @@ -376,7 +383,7 @@ public void testPeculiarJsonEscaping() { new FakeRestRequest( ImmutableMap.of("Authorization", TestJwts.PeculiarEscaping.MC_COY_SIGNED_RSA_1), new HashMap() - ), + ).asSecurityRequest(), null ); diff --git a/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/SingleKeyHTTPJwtKeyByOpenIdConnectAuthenticatorTest.java b/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/SingleKeyHTTPJwtKeyByOpenIdConnectAuthenticatorTest.java index a30e43f9fa..6b5c541981 100644 --- a/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/SingleKeyHTTPJwtKeyByOpenIdConnectAuthenticatorTest.java +++ b/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/SingleKeyHTTPJwtKeyByOpenIdConnectAuthenticatorTest.java @@ -32,7 +32,8 @@ public void basicTest() throws Exception { HTTPJwtKeyByOpenIdConnectAuthenticator jwtAuth = new HTTPJwtKeyByOpenIdConnectAuthenticator(settings, null); AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_RSA_1), new HashMap()), + new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_RSA_1), new HashMap()) + .asSecurityRequest(), null ); @@ -58,7 +59,8 @@ public void wrongSigTest() throws Exception { HTTPJwtKeyByOpenIdConnectAuthenticator jwtAuth = new HTTPJwtKeyByOpenIdConnectAuthenticator(settings, null); AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.NoKid.MC_COY_SIGNED_RSA_X), new HashMap()), + new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.NoKid.MC_COY_SIGNED_RSA_X), new HashMap()) + .asSecurityRequest(), null ); @@ -80,7 +82,8 @@ public void noAlgTest() throws Exception { HTTPJwtKeyByOpenIdConnectAuthenticator jwtAuth = new HTTPJwtKeyByOpenIdConnectAuthenticator(settings, null); AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_RSA_1), new HashMap()), + new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_RSA_1), new HashMap()) + .asSecurityRequest(), null ); @@ -105,7 +108,8 @@ public void mismatchedAlgTest() throws Exception { HTTPJwtKeyByOpenIdConnectAuthenticator jwtAuth = new HTTPJwtKeyByOpenIdConnectAuthenticator(settings, null); AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.NoKid.MC_COY_SIGNED_RSA_1), new HashMap()), + new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.NoKid.MC_COY_SIGNED_RSA_1), new HashMap()) + .asSecurityRequest(), null ); @@ -128,7 +132,8 @@ public void keyExchangeTest() throws Exception { try { AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.NoKid.MC_COY_SIGNED_RSA_1), new HashMap()), + new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.NoKid.MC_COY_SIGNED_RSA_1), new HashMap()) + .asSecurityRequest(), null ); @@ -139,21 +144,24 @@ public void keyExchangeTest() throws Exception { Assert.assertEquals(4, creds.getAttributes().size()); creds = jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.NoKid.MC_COY_SIGNED_RSA_2), new HashMap()), + new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.NoKid.MC_COY_SIGNED_RSA_2), new HashMap()) + .asSecurityRequest(), null ); Assert.assertNull(creds); creds = jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.NoKid.MC_COY_SIGNED_RSA_X), new HashMap()), + new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.NoKid.MC_COY_SIGNED_RSA_X), new HashMap()) + .asSecurityRequest(), null ); Assert.assertNull(creds); creds = jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.NoKid.MC_COY_SIGNED_RSA_1), new HashMap()), + new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.NoKid.MC_COY_SIGNED_RSA_1), new HashMap()) + .asSecurityRequest(), null ); @@ -175,7 +183,8 @@ public void keyExchangeTest() throws Exception { try { AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.NoKid.MC_COY_SIGNED_RSA_2), new HashMap()), + new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.NoKid.MC_COY_SIGNED_RSA_2), new HashMap()) + .asSecurityRequest(), null ); diff --git a/src/test/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticatorTest.java b/src/test/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticatorTest.java index 2594388128..17a2148fa5 100644 --- a/src/test/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticatorTest.java +++ b/src/test/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticatorTest.java @@ -25,6 +25,7 @@ import java.util.Base64; import java.util.HashMap; import java.util.List; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -34,6 +35,7 @@ import com.google.common.collect.ImmutableMap; import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer; import org.apache.cxf.rs.security.jose.jwt.JwtToken; +import org.hamcrest.Matchers; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -42,24 +44,26 @@ import org.opensaml.saml.saml2.core.NameIDType; import org.opensearch.core.common.bytes.BytesArray; -import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.common.io.stream.BytesStreamOutput; import org.opensearch.common.settings.Settings; import org.opensearch.core.xcontent.MediaType; import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; import org.opensearch.rest.RestResponse; import org.opensearch.core.rest.RestStatus; import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.filter.SecurityRequestFactory; +import org.opensearch.security.filter.SecurityResponse; +import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.test.helper.file.FileHelper; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.util.FakeRestRequest; import static com.amazon.dlic.auth.http.saml.HTTPSamlAuthenticator.IDP_METADATA_CONTENT; import static com.amazon.dlic.auth.http.saml.HTTPSamlAuthenticator.IDP_METADATA_URL; +import static org.hamcrest.MatcherAssert.assertThat; public class HTTPSamlAuthenticatorTest { protected MockSamlIdpServer mockSamlIdpServer; @@ -140,12 +144,8 @@ public void basicTest() throws Exception { String encodedSamlResponse = mockSamlIdpServer.handleSsoGetRequestURI(authenticateHeaders.location); RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); - TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); - tokenRestChannel.sendResponse(authenticatorResponse); - - String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); + String responseJson = getResponse(samlAuthenticator, tokenRestRequest); HashMap response = DefaultObjectMapper.objectMapper.readValue( responseJson, new TypeReference>() { @@ -161,6 +161,17 @@ public void basicTest() throws Exception { Assert.assertEquals("horst", jwt.getClaim("sub")); } + private Optional sendToAuthenticator(HTTPSamlAuthenticator samlAuthenticator, RestRequest request) { + final SecurityRequest tokenRestChannel = SecurityRequestFactory.from(request); + + return samlAuthenticator.reRequestAuthentication(tokenRestChannel, null); + } + + private String getResponse(HTTPSamlAuthenticator samlAuthenticator, RestRequest request) throws Exception { + SecurityResponse response = sendToAuthenticator(samlAuthenticator, request).orElseThrow(); + return response.getBody(); + } + @Test public void decryptAssertionsTest() throws Exception { mockSamlIdpServer.setAuthenticateUser("horst"); @@ -188,12 +199,7 @@ public void decryptAssertionsTest() throws Exception { String encodedSamlResponse = mockSamlIdpServer.handleSsoGetRequestURI(authenticateHeaders.location); RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); - TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - - final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); - tokenRestChannel.sendResponse(authenticatorResponse); - - String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); + String responseJson = getResponse(samlAuthenticator, tokenRestRequest); HashMap response = DefaultObjectMapper.objectMapper.readValue( responseJson, new TypeReference>() { @@ -237,12 +243,7 @@ public void shouldUnescapeSamlEntitiesTest() throws Exception { String encodedSamlResponse = mockSamlIdpServer.handleSsoGetRequestURI(authenticateHeaders.location); RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); - TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - - final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); - tokenRestChannel.sendResponse(authenticatorResponse); - - String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); + String responseJson = getResponse(samlAuthenticator, tokenRestRequest); HashMap response = DefaultObjectMapper.objectMapper.readValue( responseJson, new TypeReference>() { @@ -289,12 +290,7 @@ public void shouldUnescapeSamlEntitiesTest2() throws Exception { String encodedSamlResponse = mockSamlIdpServer.handleSsoGetRequestURI(authenticateHeaders.location); RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); - TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - - final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); - tokenRestChannel.sendResponse(authenticatorResponse); - - String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); + String responseJson = getResponse(samlAuthenticator, tokenRestRequest); HashMap response = DefaultObjectMapper.objectMapper.readValue( responseJson, new TypeReference>() { @@ -341,12 +337,7 @@ public void shouldNotEscapeSamlEntities() throws Exception { String encodedSamlResponse = mockSamlIdpServer.handleSsoGetRequestURI(authenticateHeaders.location); RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); - TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - - final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); - tokenRestChannel.sendResponse(authenticatorResponse); - - String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); + String responseJson = getResponse(samlAuthenticator, tokenRestRequest); HashMap response = DefaultObjectMapper.objectMapper.readValue( responseJson, new TypeReference>() { @@ -393,12 +384,7 @@ public void shouldNotTrimWhitespaceInJwtRoles() throws Exception { String encodedSamlResponse = mockSamlIdpServer.handleSsoGetRequestURI(authenticateHeaders.location); RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); - TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - - final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); - tokenRestChannel.sendResponse(authenticatorResponse); - - String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); + String responseJson = getResponse(samlAuthenticator, tokenRestRequest); HashMap response = DefaultObjectMapper.objectMapper.readValue( responseJson, new TypeReference>() { @@ -441,12 +427,7 @@ public void testMetadataBody() throws Exception { String encodedSamlResponse = mockSamlIdpServer.handleSsoGetRequestURI(authenticateHeaders.location); RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); - TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - - final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); - tokenRestChannel.sendResponse(authenticatorResponse); - - String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); + String responseJson = getResponse(samlAuthenticator, tokenRestRequest); HashMap response = DefaultObjectMapper.objectMapper.readValue( responseJson, new TypeReference>() { @@ -507,12 +488,7 @@ public void unsolicitedSsoTest() throws Exception { null, "/opendistrosecurity/saml/acs/idpinitiated" ); - TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - - final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); - tokenRestChannel.sendResponse(authenticatorResponse); - - String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); + String responseJson = getResponse(samlAuthenticator, tokenRestRequest); HashMap response = DefaultObjectMapper.objectMapper.readValue( responseJson, new TypeReference>() { @@ -559,12 +535,9 @@ public void badUnsolicitedSsoTest() throws Exception { authenticateHeaders, "/opendistrosecurity/saml/acs/idpinitiated" ); - TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - - final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); - tokenRestChannel.sendResponse(authenticatorResponse); + SecurityResponse response = sendToAuthenticator(samlAuthenticator, tokenRestRequest).orElseThrow(); - Assert.assertEquals(RestStatus.UNAUTHORIZED, tokenRestChannel.response.status()); + Assert.assertEquals(RestStatus.UNAUTHORIZED.getStatus(), response.getStatus()); } @Test @@ -592,12 +565,9 @@ public void wrongCertTest() throws Exception { String encodedSamlResponse = mockSamlIdpServer.handleSsoGetRequestURI(authenticateHeaders.location); RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); - TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); + SecurityResponse response = sendToAuthenticator(samlAuthenticator, tokenRestRequest).orElseThrow(); - final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); - tokenRestChannel.sendResponse(authenticatorResponse); - - Assert.assertEquals(401, tokenRestChannel.response.status().getStatus()); + Assert.assertEquals(401, response.getStatus()); } @Test @@ -622,12 +592,9 @@ public void noSignatureTest() throws Exception { String encodedSamlResponse = mockSamlIdpServer.handleSsoGetRequestURI(authenticateHeaders.location); RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); - TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - - final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); - tokenRestChannel.sendResponse(authenticatorResponse); + SecurityResponse response = sendToAuthenticator(samlAuthenticator, tokenRestRequest).orElseThrow(); - Assert.assertEquals(401, tokenRestChannel.response.status().getStatus()); + Assert.assertEquals(401, response.getStatus()); } @SuppressWarnings("unchecked") @@ -656,12 +623,7 @@ public void rolesTest() throws Exception { String encodedSamlResponse = mockSamlIdpServer.handleSsoGetRequestURI(authenticateHeaders.location); RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); - TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - - final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); - tokenRestChannel.sendResponse(authenticatorResponse); - - String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); + String responseJson = getResponse(samlAuthenticator, tokenRestRequest); HashMap response = DefaultObjectMapper.objectMapper.readValue( responseJson, new TypeReference>() { @@ -704,12 +666,7 @@ public void idpEndpointWithQueryStringTest() throws Exception { String encodedSamlResponse = mockSamlIdpServer.handleSsoGetRequestURI(authenticateHeaders.location); RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); - TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - - final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); - tokenRestChannel.sendResponse(authenticatorResponse); - - String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); + String responseJson = getResponse(samlAuthenticator, tokenRestRequest); HashMap response = DefaultObjectMapper.objectMapper.readValue( responseJson, new TypeReference>() { @@ -759,12 +716,7 @@ private void commaSeparatedRoles(final String rolesAsString, final Settings.Buil String encodedSamlResponse = mockSamlIdpServer.handleSsoGetRequestURI(authenticateHeaders.location); RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); - TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - - final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); - tokenRestChannel.sendResponse(authenticatorResponse); - - String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); + String responseJson = getResponse(samlAuthenticator, tokenRestRequest); HashMap response = DefaultObjectMapper.objectMapper.readValue( responseJson, new TypeReference>() { @@ -864,11 +816,9 @@ public void initialConnectionFailureTest() throws Exception { HTTPSamlAuthenticator samlAuthenticator = new HTTPSamlAuthenticator(settings, null); RestRequest restRequest = new FakeRestRequest(ImmutableMap.of(), new HashMap()); - TestRestChannel restChannel = new TestRestChannel(restRequest); - BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(restRequest, null); - restChannel.sendResponse(authenticatorResponse); + Optional maybeResponse = sendToAuthenticator(samlAuthenticator, restRequest); - Assert.assertNull(restChannel.response); + assertThat(maybeResponse.isPresent(), Matchers.equalTo(false)); mockSamlIdpServer.start(); @@ -884,12 +834,7 @@ public void initialConnectionFailureTest() throws Exception { String encodedSamlResponse = mockSamlIdpServer.handleSsoGetRequestURI(authenticateHeaders.location); RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); - TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest); - - authenticatorResponse = samlAuthenticator.reRequestAuthentication(tokenRestRequest, null); - tokenRestChannel.sendResponse(authenticatorResponse); - - String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content())); + String responseJson = getResponse(samlAuthenticator, tokenRestRequest); HashMap response = DefaultObjectMapper.objectMapper.readValue( responseJson, new TypeReference>() { @@ -908,17 +853,11 @@ public void initialConnectionFailureTest() throws Exception { private AuthenticateHeaders getAutenticateHeaders(HTTPSamlAuthenticator samlAuthenticator) { RestRequest restRequest = new FakeRestRequest(ImmutableMap.of(), new HashMap()); - TestRestChannel restChannel = new TestRestChannel(restRequest); - - final BytesRestResponse authenticatorResponse = samlAuthenticator.reRequestAuthentication(restRequest, null); - restChannel.sendResponse(authenticatorResponse); - - List wwwAuthenticateHeaders = restChannel.response.getHeaders().get("WWW-Authenticate"); + SecurityResponse response = sendToAuthenticator(samlAuthenticator, restRequest).orElseThrow(); - Assert.assertNotNull(wwwAuthenticateHeaders); - Assert.assertEquals("More than one WWW-Authenticate header: " + wwwAuthenticateHeaders, 1, wwwAuthenticateHeaders.size()); + String wwwAuthenticateHeader = response.getHeaders().get("WWW-Authenticate"); - String wwwAuthenticateHeader = wwwAuthenticateHeaders.get(0); + Assert.assertNotNull(wwwAuthenticateHeader); Matcher wwwAuthenticateHeaderMatcher = WWW_AUTHENTICATE_PATTERN.matcher(wwwAuthenticateHeader); diff --git a/src/test/java/org/opensearch/security/auditlog/helper/MockRestRequest.java b/src/test/java/org/opensearch/security/auditlog/helper/MockRestRequest.java index 458e240596..80c8fd7b17 100644 --- a/src/test/java/org/opensearch/security/auditlog/helper/MockRestRequest.java +++ b/src/test/java/org/opensearch/security/auditlog/helper/MockRestRequest.java @@ -16,6 +16,8 @@ import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.rest.RestRequest; +import org.opensearch.security.filter.SecurityRequestChannel; +import org.opensearch.security.filter.SecurityRequestFactory; public class MockRestRequest extends RestRequest { @@ -44,4 +46,8 @@ public boolean hasContent() { public BytesReference content() { return null; } + + public SecurityRequestChannel asSecurityRequest() { + return SecurityRequestFactory.from(this, null); + } } diff --git a/src/test/java/org/opensearch/security/auditlog/impl/AuditlogTest.java b/src/test/java/org/opensearch/security/auditlog/impl/AuditlogTest.java index bbdbc3aefd..935fb924a3 100644 --- a/src/test/java/org/opensearch/security/auditlog/impl/AuditlogTest.java +++ b/src/test/java/org/opensearch/security/auditlog/impl/AuditlogTest.java @@ -21,10 +21,10 @@ import org.opensearch.cluster.node.DiscoveryNode; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestRequest; import org.opensearch.security.auditlog.AuditTestUtils; import org.opensearch.security.auditlog.helper.RetrySink; import org.opensearch.security.auditlog.integration.TestAuditlogImpl; +import org.opensearch.security.filter.SecurityRequestChannel; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.test.AbstractSecurityUnitTest; import org.opensearch.transport.TransportRequest; @@ -132,7 +132,7 @@ public void testRestFilterEnabledCheck() { final Settings settings = Settings.builder().put(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_ENABLE_REST, false).build(); final AbstractAuditLog al = AuditTestUtils.createAuditLog(settings, null, null, AbstractSecurityUnitTest.MOCK_POOL, null, cs); for (AuditCategory category : AuditCategory.values()) { - Assert.assertFalse(al.checkRestFilter(category, "user", mock(RestRequest.class))); + Assert.assertFalse(al.checkRestFilter(category, "user", mock(SecurityRequestChannel.class))); } } diff --git a/src/test/java/org/opensearch/security/auditlog/impl/DisabledCategoriesTest.java b/src/test/java/org/opensearch/security/auditlog/impl/DisabledCategoriesTest.java index 0d08abaeea..ba4ee7b55d 100644 --- a/src/test/java/org/opensearch/security/auditlog/impl/DisabledCategoriesTest.java +++ b/src/test/java/org/opensearch/security/auditlog/impl/DisabledCategoriesTest.java @@ -227,11 +227,16 @@ protected void logAll(AuditLog auditLog) { } protected void logRestSucceededLogin(AuditLog auditLog) { - auditLog.logSucceededLogin("testuser.rest.succeededlogin", false, "testuser.rest.succeededlogin", new MockRestRequest()); + auditLog.logSucceededLogin( + "testuser.rest.succeededlogin", + false, + "testuser.rest.succeededlogin", + new MockRestRequest().asSecurityRequest() + ); } protected void logRestFailedLogin(AuditLog auditLog) { - auditLog.logFailedLogin("testuser.rest.failedlogin", false, "testuser.rest.failedlogin", new MockRestRequest()); + auditLog.logFailedLogin("testuser.rest.failedlogin", false, "testuser.rest.failedlogin", new MockRestRequest().asSecurityRequest()); } protected void logMissingPrivileges(AuditLog auditLog) { @@ -243,7 +248,7 @@ protected void logTransportBadHeaders(AuditLog auditLog) { } protected void logRestBadHeaders(AuditLog auditLog) { - auditLog.logBadHeaders(new MockRestRequest()); + auditLog.logBadHeaders(new MockRestRequest().asSecurityRequest()); } protected void logSecurityIndexAttempt(AuditLog auditLog) { @@ -251,7 +256,7 @@ protected void logSecurityIndexAttempt(AuditLog auditLog) { } protected void logRestSSLException(AuditLog auditLog) { - auditLog.logSSLException(new MockRestRequest(), new Exception()); + auditLog.logSSLException(new MockRestRequest().asSecurityRequest(), new Exception()); } protected void logTransportSSLException(AuditLog auditLog) { diff --git a/src/test/java/org/opensearch/security/authtoken/jwt/AuthTokenUtilsTest.java b/src/test/java/org/opensearch/security/authtoken/jwt/AuthTokenUtilsTest.java index d563308e31..4072d94436 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/AuthTokenUtilsTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/AuthTokenUtilsTest.java @@ -14,6 +14,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.rest.RestRequest; +import org.opensearch.security.filter.SecurityRequestFactory; import org.opensearch.security.util.AuthTokenUtils; import org.opensearch.test.rest.FakeRestRequest; import org.junit.Test; @@ -33,7 +34,7 @@ public void testIsAccessToRestrictedEndpointsForOnBehalfOfToken() { .withMethod(RestRequest.Method.POST) .build(); - assertTrue(AuthTokenUtils.isAccessToRestrictedEndpoints(request, "api/generateonbehalfoftoken")); + assertTrue(AuthTokenUtils.isAccessToRestrictedEndpoints(SecurityRequestFactory.from(request), "api/generateonbehalfoftoken")); } @Test @@ -44,7 +45,7 @@ public void testIsAccessToRestrictedEndpointsForAccount() { .withMethod(RestRequest.Method.PUT) .build(); - assertTrue(AuthTokenUtils.isAccessToRestrictedEndpoints(request, "api/account")); + assertTrue(AuthTokenUtils.isAccessToRestrictedEndpoints(SecurityRequestFactory.from(request), "api/account")); } @Test @@ -55,7 +56,7 @@ public void testIsAccessToRestrictedEndpointsFalseCase() { .withMethod(RestRequest.Method.GET) .build(); - assertFalse(AuthTokenUtils.isAccessToRestrictedEndpoints(request, "api/someotherendpoint")); + assertFalse(AuthTokenUtils.isAccessToRestrictedEndpoints(SecurityRequestFactory.from(request), "api/someotherendpoint")); } @Test diff --git a/src/test/java/org/opensearch/security/cache/DummyHTTPAuthenticator.java b/src/test/java/org/opensearch/security/cache/DummyHTTPAuthenticator.java index 37ac45080b..2cfd23fc23 100644 --- a/src/test/java/org/opensearch/security/cache/DummyHTTPAuthenticator.java +++ b/src/test/java/org/opensearch/security/cache/DummyHTTPAuthenticator.java @@ -12,13 +12,14 @@ package org.opensearch.security.cache; import java.nio.file.Path; +import java.util.Optional; import org.opensearch.OpenSearchSecurityException; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestRequest; import org.opensearch.security.auth.HTTPAuthenticator; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.filter.SecurityResponse; import org.opensearch.security.user.AuthCredentials; public class DummyHTTPAuthenticator implements HTTPAuthenticator { @@ -33,14 +34,15 @@ public String getType() { } @Override - public AuthCredentials extractCredentials(RestRequest request, ThreadContext context) throws OpenSearchSecurityException { + public AuthCredentials extractCredentials(final SecurityRequest request, final ThreadContext context) + throws OpenSearchSecurityException { count++; return new AuthCredentials("dummy").markComplete(); } @Override - public BytesRestResponse reRequestAuthentication(RestRequest request, AuthCredentials credentials) { - return null; + public Optional reRequestAuthentication(SecurityRequest channel, AuthCredentials credentials) { + return Optional.empty(); } public static long getCount() { diff --git a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java index fbb03bf7c3..1ff6adee3a 100644 --- a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java @@ -95,7 +95,10 @@ public void testTokenMissing() throws Exception { OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); Map headers = new HashMap(); - AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + AuthCredentials credentials = jwtAuth.extractCredentials( + new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), + null + ); Assert.assertNull(credentials); } @@ -109,7 +112,10 @@ public void testInvalid() throws Exception { Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); - AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + AuthCredentials credentials = jwtAuth.extractCredentials( + new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), + null + ); Assert.assertNull(credentials); } @@ -126,7 +132,10 @@ public void testDisabled() throws Exception { Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); - AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + AuthCredentials credentials = jwtAuth.extractCredentials( + new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), + null + ); Assert.assertNull(credentials); } @@ -143,7 +152,10 @@ public void testNonSpecifyOBOSetting() throws Exception { Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); - AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + AuthCredentials credentials = jwtAuth.extractCredentials( + new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), + null + ); Assert.assertNotNull(credentials); } @@ -165,7 +177,10 @@ public void testBearer() throws Exception { Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); - AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + AuthCredentials credentials = jwtAuth.extractCredentials( + new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), + null + ); Assert.assertNotNull(credentials); Assert.assertEquals("Leonard McCoy", credentials.getUsername()); @@ -188,7 +203,10 @@ public void testBearerWrongPosition() throws Exception { Map headers = new HashMap(); headers.put("Authorization", jwsToken + "Bearer " + " 123"); - AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + AuthCredentials credentials = jwtAuth.extractCredentials( + new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), + null + ); Assert.assertNull(credentials); } @@ -205,7 +223,10 @@ public void testBasicAuthHeader() throws Exception { Map headers = Collections.singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + jwsToken); - AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, Collections.emptyMap()), null); + AuthCredentials credentials = jwtAuth.extractCredentials( + new FakeRestRequest(headers, Collections.emptyMap()).asSecurityRequest(), + null + ); Assert.assertNull(credentials); } @@ -349,7 +370,10 @@ public void testDifferentIssuer() throws Exception { Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); - AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + AuthCredentials credentials = jwtAuth.extractCredentials( + new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), + null + ); Assert.assertNull(credentials); } @@ -375,7 +399,7 @@ private AuthCredentials extractCredentialsFromJwtHeader( SignatureAlgorithm.HS512 ).compact(); final Map headers = Map.of("Authorization", "Bearer " + jwsToken); - return jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap<>()), null); + return jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap<>()).asSecurityRequest(), null); } private Settings defaultSettings() { diff --git a/src/test/java/org/opensearch/security/http/proxy/HTTPExtendedProxyAuthenticatorTest.java b/src/test/java/org/opensearch/security/http/proxy/HTTPExtendedProxyAuthenticatorTest.java index d3bf10d943..f7a2011a68 100644 --- a/src/test/java/org/opensearch/security/http/proxy/HTTPExtendedProxyAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/http/proxy/HTTPExtendedProxyAuthenticatorTest.java @@ -47,6 +47,8 @@ import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; import org.opensearch.core.rest.RestStatus; +import org.opensearch.security.filter.SecurityRequestChannel; +import org.opensearch.security.filter.SecurityRequestFactory; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.AuthCredentials; @@ -77,19 +79,19 @@ public void testGetType() { @Test(expected = OpenSearchSecurityException.class) public void testThrowsExceptionWhenMissingXFFDone() { authenticator = new HTTPExtendedProxyAuthenticator(Settings.EMPTY, null); - authenticator.extractCredentials(new TestRestRequest(), new ThreadContext(Settings.EMPTY)); + authenticator.extractCredentials(new TestRestRequest().asSecurityRequest(), new ThreadContext(Settings.EMPTY)); } @Test public void testReturnsNullWhenUserHeaderIsUnconfigured() { authenticator = new HTTPExtendedProxyAuthenticator(Settings.EMPTY, null); - assertNull(authenticator.extractCredentials(new TestRestRequest(), context)); + assertNull(authenticator.extractCredentials(new TestRestRequest().asSecurityRequest(), context)); } @Test public void testReturnsNullWhenUserHeaderIsMissing() { - assertNull(authenticator.extractCredentials(new TestRestRequest(), context)); + assertNull(authenticator.extractCredentials(new TestRestRequest().asSecurityRequest(), context)); } @Test @@ -105,7 +107,7 @@ public void testReturnsCredentials() { settings = Settings.builder().put(settings).put("attr_header_prefix", "proxy_").build(); authenticator = new HTTPExtendedProxyAuthenticator(settings, null); - AuthCredentials creds = authenticator.extractCredentials(new TestRestRequest(headers), context); + AuthCredentials creds = authenticator.extractCredentials(new TestRestRequest(headers).asSecurityRequest(), context); assertNotNull(creds); assertEquals("aValidUser", creds.getUsername()); assertEquals("123,456", creds.getAttributes().get("attr.proxy.uid")); @@ -122,7 +124,7 @@ public void testTrimOnRoles() { settings = Settings.builder().put(settings).put("roles_header", "roles").put("roles_separator", ",").build(); authenticator = new HTTPExtendedProxyAuthenticator(settings, null); - AuthCredentials creds = authenticator.extractCredentials(new TestRestRequest(headers), context); + AuthCredentials creds = authenticator.extractCredentials(new TestRestRequest(headers).asSecurityRequest(), context); assertNotNull(creds); assertEquals("aValidUser", creds.getUsername()); assertEquals(ImmutableSet.of("role1", "role2"), creds.getBackendRoles()); @@ -163,6 +165,9 @@ public boolean hasContent() { return false; } + public SecurityRequestChannel asSecurityRequest() { + return SecurityRequestFactory.from(this, null); + } } static class HttpRequestImpl implements HttpRequest { diff --git a/src/test/java/org/opensearch/security/util/FakeRestRequest.java b/src/test/java/org/opensearch/security/util/FakeRestRequest.java index f8d8aca508..121ddf778e 100644 --- a/src/test/java/org/opensearch/security/util/FakeRestRequest.java +++ b/src/test/java/org/opensearch/security/util/FakeRestRequest.java @@ -18,6 +18,8 @@ import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.rest.RestRequest; +import org.opensearch.security.filter.SecurityRequestChannel; +import org.opensearch.security.filter.SecurityRequestFactory; public class FakeRestRequest extends RestRequest { @@ -117,4 +119,7 @@ private static Map> convert(Map headers) { return ret; } + public SecurityRequestChannel asSecurityRequest() { + return SecurityRequestFactory.from(this, null); + } }